[
  {
    "path": ".changeset/config.json",
    "content": "{\n  \"$schema\": \"https://unpkg.com/@changesets/config@3.0.1/schema.json\",\n  \"access\": \"public\",\n  \"privatePackages\": {\n    \"version\": false\n  }\n}\n"
  },
  {
    "path": ".git-blame-ignore-revs",
    "content": "# oxfmt bulk reformat (Prettier → Oxfmt migration)\ne052c5666f770d1521b8e5e574d2fa69152f89dc\n"
  },
  {
    "path": ".githooks/pre-commit",
    "content": "#!/usr/bin/env sh\nset -eu\n\npnpm hooks:pre-commit\n\npnpm lint-staged\n"
  },
  {
    "path": ".githooks/pre-push",
    "content": "#!/usr/bin/env sh\nset -eu\n\npnpm hooks:pre-push\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "## Summary\n\n## Verification\n\n- [ ] Ran `pnpm verify:ci`\n- [ ] Committed updated `public/r` artifacts when Tool UI or registry sources changed\n"
  },
  {
    "path": ".github/workflows/changeset.yaml",
    "content": "name: Changesets\n\non:\n  push:\n    branches:\n      - main\n\npermissions:\n  contents: write\n  pull-requests: write\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\nenv:\n  CI: true\n\njobs:\n  version:\n    name: Create Version PR\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code repository\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 1\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Setup node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24\n          cache: \"pnpm\"\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Create version PR\n        id: changesets\n        uses: changesets/action@v1\n        with:\n          commit: \"chore: update versions\"\n          title: \"chore: update versions\"\n          version: pnpm ci:version\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Check if any packages need publishing\n        id: check-publish\n        if: steps.changesets.outputs.hasChangesets == 'false'\n        run: |\n          needs_publish=\"false\"\n          for pkg in packages/*/package.json; do\n            [ -f \"$pkg\" ] || continue\n\n            name=$(jq -r '.name' \"$pkg\")\n            version=$(jq -r '.version' \"$pkg\")\n            private=$(jq -r '.private // false' \"$pkg\")\n\n            if [ \"$private\" = \"true\" ]; then\n              echo \"Skipping private package: $name\"\n              continue\n            fi\n\n            if npm view \"${name}@${version}\" version 2>/dev/null | grep -q \"${version}\"; then\n              echo \"✓ ${name}@${version} already published\"\n            else\n              echo \"✗ ${name}@${version} needs publishing\"\n              needs_publish=\"true\"\n            fi\n          done\n\n          echo \"needsPublish=${needs_publish}\" >> $GITHUB_OUTPUT\n\n      - name: Trigger npm publish\n        if: steps.changesets.outputs.hasChangesets == 'false' && steps.check-publish.outputs.needsPublish == 'true'\n        uses: actions/github-script@v7\n        with:\n          script: |\n            await github.rest.repos.createDispatchEvent({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              event_type: 'npm-publish'\n            });\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  pull_request:\n    branches: [main]\n  push:\n    branches: [main]\n\njobs:\n  verify:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Format check\n        run: pnpm lint:format\n\n      - name: Lint - Oxlint\n        run: pnpm lint:oxlint\n\n      - name: Lint - ESLint custom rules\n        run: pnpm lint:eslint\n\n      - name: Typecheck\n        run: pnpm typecheck\n\n      - name: Test\n        run: pnpm test\n\n      - name: Check changelog structure\n        run: pnpm changelog:check\n\n      - name: Verify registry artifacts are up to date\n        run: pnpm registry:check\n"
  },
  {
    "path": ".github/workflows/npm-publish.yaml",
    "content": "name: npm Publish\n\non:\n  repository_dispatch:\n    types: [npm-publish]\n\npermissions:\n  id-token: write\n  contents: write\n\nenv:\n  CI: true\n\njobs:\n  publish:\n    name: Publish to npm\n    if: github.ref == 'refs/heads/main'\n    environment: npm Publish\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code repository\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 1\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Setup node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24\n          registry-url: \"https://registry.npmjs.org\"\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Publish to npm\n        run: pnpm ci:publish\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n\n      - name: Create GitHub Releases\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          git fetch --tags\n\n          for pkg in packages/*/package.json; do\n            name=$(jq -r '.name' \"$pkg\")\n            version=$(jq -r '.version' \"$pkg\")\n            private=$(jq -r '.private // false' \"$pkg\")\n\n            [[ \"$private\" == \"true\" ]] && continue\n\n            tag=\"${name}@${version}\"\n\n            git rev-parse \"$tag\" >/dev/null 2>&1 || continue\n            gh release view \"$tag\" >/dev/null 2>&1 && continue\n\n            changelog_dir=$(dirname \"$pkg\")\n            changelog=\"$changelog_dir/CHANGELOG.md\"\n\n            notes=\"\"\n            if [[ -f \"$changelog\" ]]; then\n              notes=$(awk \"/^## ${version//./\\\\.}\\$/,/^## [0-9]/\" \"$changelog\" | head -n -1 | tail -n +2)\n            fi\n            [[ -z \"$notes\" ]] && notes=\"Release ${tag}\"\n\n            echo \"Creating release: $tag\"\n            gh release create \"$tag\" \\\n              --title \"$tag\" \\\n              --notes \"$notes\" \\\n              --verify-tag || echo \"Warning: Failed to create release for $tag\"\n          done\n"
  },
  {
    "path": ".gitignore",
    "content": "# Dependencies\nnode_modules\n/.pnp\n.pnp.*\n\n# Testing\ncoverage\n\n# Next.js\n.next/\nout/\n\n# Production\nbuild\ndist\n\n# Misc\n.DS_Store\n*.pem\nprivate\n\n# Debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# Local env files\n.env*.local\n.env\n\n# Vercel\n.vercel\n\n# TypeScript\n*.tsbuildinfo\nnext-env.d.ts\n\n# Turbo\n.turbo\n\n# Cursor\n.cursor\n\n# Claude\n.claude/\n\n# VSCode\n.vscode/\n.vscode.playwright-mcp/\n.vscode/terminals.json\n.playwright-mcp\n"
  },
  {
    "path": ".prettierignore",
    "content": "# Generated artifacts (managed by build scripts)\napps/www/public/r/\napps/www/components/tool-ui/weather-widget/generated/\napps/www/lib/weather-authoring/weather-widget/effects/generated/\napps/www/lib/weather-authoring/weather-widget/weather-data-overlay.generated.ts\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# AGENTS.md\n\n## Cursor Cloud specific instructions\n\n### Project overview\n\nTool UI is a Next.js 16 documentation/demo site and copy-paste component library for AI assistant interfaces. The app lives in `apps/www/` within a pnpm monorepo. There is no database, no Docker, and no required background services beyond the Node.js dev server.\n\n### Running services\n\n- **Dev server**: `pnpm dev` (Turbopack, port 3000). The static docs, gallery, and component previews work without any API keys.\n- **Chat/Playground features** require `OPENAI_API_KEY` in `.env` (see `.env.example`). Without it the chat API routes return errors, but the rest of the site functions normally.\n\n### Commands reference\n\nAll standard commands are documented in `CLAUDE.md`. Key ones:\n\n| Task                      | Command          |\n| ------------------------- | ---------------- |\n| Dev server                | `pnpm dev`       |\n| Lint + typecheck + format | `pnpm check`     |\n| Tests (Vitest)            | `pnpm test`      |\n| Fix lint issues           | `pnpm lint:fix`  |\n| Typecheck only            | `pnpm typecheck` |\n\n### Non-obvious caveats\n\n- **Typecheck uses `tsgo`** (`@typescript/native-preview`), not standard `tsc`. The `pnpm typecheck` command runs `tsgo --noEmit`.\n- **Formatter is `oxfmt`**, not Prettier. Run `pnpm format` to format or `pnpm format:check` to verify. It handles Tailwind class sorting and import sorting.\n- **Linting is split**: `oxlint` handles standard rules; `eslint` is retained only for `no-restricted-syntax`, `no-restricted-imports`, custom `tool-ui/*` rules, and React Compiler hooks. `pnpm check` runs both in parallel.\n- **`pnpm install` triggers `prepare`** which sets up git hooks via `tsx apps/www/scripts/install-git-hooks.ts`. The hooks directory is `.githooks/`.\n- **Pre-existing lint warnings** (60 warnings, 0 errors): these are known oxlint a11y warnings in the codebase and are not regressions.\n- **Build uses `--experimental-build-mode=compile`**: `pnpm build` runs `next build --experimental-build-mode=compile`.\n"
  },
  {
    "path": "AGENT_CHANGELOG.md",
    "content": "# Agent Changelog\n\n> This file helps coding agents understand project evolution, key decisions, and deprecated patterns.\n> Updated: 2026-02-11\n\n## Current State Summary\n\nTool UI is a maintainer-owned copy/paste component library (shadcn/ui model) for AI assistant interfaces with a registry-first install path (`https://tool-ui.com/r/<component>.json`). Component APIs are flat, receipt semantics are unified on `choice`, and component directories remain the product surface (`schema`, `component`, `README`, exports). Registry adapters now use `@/lib/utils` for `cn` (no registry-shipped `lib/ui/cn.ts`), Tool UI component motion is constrained to Tailwind/tw-animate-compatible classes, and docs place Source/Install + GitHub source links ahead of feature marketing sections.\n\n## Stale Information Detected\n\n| Location                                                       | States                                                                             | Reality                                                                                                              | Since   |\n| -------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------- |\n| `README.md` (Shadcn Registry section)                          | Registry artifacts include `lib/ui/cn.ts`                                          | Registry adapters use `@/lib/utils`; `lib/ui/cn.ts` is no longer part of generated install artifacts                 | 2026-02 |\n| `.claude/plans/plan-sequential-munching-canyon.md`             | References `hooks/use-code-gen.ts` TypeScript generation                           | Tuning flow is apply/recover via repo routes; `use-code-gen.ts` removed                                              | 2026-02 |\n| `.claude/plans/plan-sequential-munching-canyon.md`             | Targets deletion of `app/sandbox/weather-compositor/`                              | `weather-tuning` still imports compositor presets/interpolation modules; compositor remains active                   | 2026-02 |\n| `.claude/docs/component-workflow.md`                           | New component contract requires `error-boundary.tsx`                               | Error-boundary layer was removed from component contracts; index exports no longer include local error boundaries    | 2026-02 |\n| `.claude/agents/tool-ui-implementer.md`                        | Instructs creating `error-boundary.tsx` and using `createToolUiErrorBoundary`      | Error-boundary wrappers were removed; component contract now centers on `_adapter`, schema, component, index, README | 2026-02 |\n| `.claude/agents/tool-ui-reviewer.md`                           | Blocks review if `error-boundary.tsx` is missing                                   | `error-boundary.tsx` is no longer required in component directories                                                  | 2026-02 |\n| `.claude/compiled/component-creation.md`                       | Uses `../shared` barrel and error-boundary scaffolding in canonical template       | Current standards forbid `../shared` barrel imports for core logic and remove per-component error boundary scaffolds | 2026-02 |\n| `docs/plans/2026-01-22-feat-wizard-step-component-plan.md`     | Active implementation target is `WizardStep` at `components/tool-ui/wizard-step/*` | Implemented component is `QuestionFlow` at `components/tool-ui/question-flow/*`                                      | 2026-01 |\n| `docs/plans/2026-01-23-feat-wizard-step-visual-polish-plan.md` | Polish targets `WizardStep` paths and naming                                       | Final shipped component naming/path is `QuestionFlow`                                                                | 2026-01 |\n\n## Timeline\n\n### 2026-02-11 — Component Docs Prioritize Install + Source Visibility\n\n**What changed:** All component docs were normalized so `## Source and Install` appears above `## Key Features`, with a GitHub source link for each component (`components/tool-ui/<id>`).\n\nHighlights:\n\n- Reordered sections across all component docs to surface install instructions first\n- Normalized mixed `## Features`/`## Key Features` headings to `## Key Features`\n- Added docs contract enforcement to prevent regressions in section order and source-link presence\n\n**Why:** Make integration steps and source discovery immediately visible for maintainers/copy-paste users.\n\n**Agent impact:** In component docs, place install/source sections before feature callouts. Include both:\n\n- `npx shadcn@latest add https://tool-ui.com/r/<component>.json`\n- GitHub source link to `components/tool-ui/<component>`\n\n**Files:** `app/docs/*/content.mdx`, `lib/tests/tool-ui/docs/registry-installation-contract.test.ts`\n\n---\n\n### 2026-02-11 — Tool UI Animation Portability Standardized for Registry Consumers\n\n**What changed:** Tool UI components were migrated off repo-private animation keyframes and now rely on Tailwind/tw-animate-compatible classes only.\n\nHighlights:\n\n- Replaced private animation names (e.g. `spring-bounce`, `check-draw`, `fade-blur-in`, `progress-pulse`) in shipped component code\n- Removed inline `@keyframes` from `stats-display/sparkline`\n- Added portability contract tests that fail on private keyframe tokens or inline keyframes in source and generated registry artifacts\n- Added registry dependency assertions for motion primitives (`accordion`/`collapsible`)\n\n**Why:** Prevent no-op/broken transitions in downstream shadcn apps that do not include this repo’s private CSS.\n\n**Agent impact:** For `components/tool-ui/**`, use Tailwind/tw-animate classes (`animate-in/out`, fade/zoom/slide, `animate-spin`, `animate-pulse`) and avoid custom keyframe names unless they are provided by stock shadcn/tw-animate setup.\n\n**Files:** `components/tool-ui/plan/plan.tsx`, `components/tool-ui/progress-tracker/progress-tracker.tsx`, `components/tool-ui/question-flow/question-flow.tsx`, `components/tool-ui/approval-card/approval-card.tsx`, `components/tool-ui/option-list/option-list.tsx`, `components/tool-ui/stats-display/sparkline.tsx`, `lib/tests/tool-ui/docs/animation-portability-contract.test.ts`, `lib/tests/registry/tool-ui-registry.test.ts`\n\n---\n\n### 2026-02-11 — Registry Adapter `cn` Dependency Migrated to shadcn `@/lib/utils`\n\n**What changed:** Registry component adapters were updated to import `cn` from `@/lib/utils`; generated artifacts no longer rely on/emit `lib/ui/cn.ts`.\n\nHighlights:\n\n- Adapter imports switched from `@/lib/ui/cn` to `@/lib/utils`\n- Registry artifact checks updated accordingly\n- Fresh install path validated against root-level shadcn command and hosted component JSON URLs\n\n**Why:** Align Tool UI install output with stock shadcn app structure and eliminate repo-specific `cn` scaffolding.\n\n**Agent impact:** For registry-consumed component adapters, expect:\n\n- `import { cn } from \"@/lib/utils\"`\n- no generated `lib/ui/cn.ts` dependency\n\n**Files:** `components/tool-ui/*/_adapter.tsx`, `lib/tests/registry/tool-ui-registry.test.ts`, `app/docs/quick-start/content.mdx`, `public/r/*.json`\n\n---\n\n### 2026-02-11 — Registry-First Install Path Finalized; ZIP/Manual Guidance Removed\n\n**What changed:** Docs were converted to a single registry-first install flow using full component URLs, and legacy ZIP/manual copy instructions were removed.\n\n**Why:** Reduce install ambiguity and support a single canonical setup path for consumers.\n\n**Agent impact:** Use only:\n\n- `npx shadcn@latest add https://tool-ui.com/r/<component>.json`\n\nDo not propose ZIP/manual copy workflows.\n\n**Files:** `app/docs/*/content.mdx`, `app/docs/quick-start/content.mdx`, `lib/tests/tool-ui/docs/registry-installation-contract.test.ts`\n\n---\n\n### 2026-02-11 — Import Boundary Enforcement + Error Boundary Layer Removal\n\n**What changed:** Tool UI component contracts were tightened around portability boundaries and local adapter ownership.\n\nHighlights:\n\n- Removed per-component `error-boundary.tsx` files and related exports across component directories\n- Normalized component `_adapter.tsx` files to alias-based UI imports\n- Enforced adapter-only UI primitive imports via ESLint (`@/components/ui/*` and `@/lib/ui/cn` are restricted outside `_adapter.tsx`)\n- Hardened registry/ID contracts and tests (`data-table` row keys, `question-flow` ids, registry generation edge cases like OS metadata files)\n\n**Why:** Reduce copy/paste friction, prevent component-internal import drift, and keep portability constraints enforceable by lint/tests instead of convention.\n\n**Agent impact:** Do not add local `error-boundary.tsx` files for new components. In non-adapter component modules, import UI primitives and `cn` only from `./_adapter`; keep shared imports as direct leaf modules (`../shared/*`), not barrels.\n\n**Files:** `eslint.config.ts`, `components/tool-ui/*/_adapter.tsx`, `components/tool-ui/*/index.ts*`, `lib/registry/tool-ui-registry.ts`, `lib/tests/tool-ui/data-table/row-keys-contract.test.ts`, `lib/tests/tool-ui/question-flow/ids-contract.test.ts`, `lib/tests/registry/tool-ui-registry.test.ts`\n\n---\n\n### 2026-02-11 — Maintainer Workflow + State Contract Hardening\n\n**What changed:** Shifted docs and tooling to a maintainer-first workflow and added targeted regression contracts for high-risk UI state paths.\n\nHighlights:\n\n- Reframed onboarding/docs around maintainers (`README.md`, `CONTRIBUTING.md`, `app/docs/contributing/content.mdx`, `docs/playground.md`, `docs/tests.md`)\n- Added component-local README coverage and scaffold automation via `pnpm component:new` (`scripts/new-tool-ui-component.ts`)\n- Added `components/tool-ui/index.ts` aggregate export surface\n- Added contract tests for deterministic row keys, outcome sync transitions, step ids, and tab search param resolution\n- Closed component contract gaps and improved scaffold consistency\n\n**Why:** Reduce time-to-first-working-component, make component directory contracts explicit, and keep state/ID behavior stable as internals evolve.\n\n**Agent impact:** Use scaffold + maintainer docs as the default path for new components. When changing stateful behavior, expose deterministic helpers and add contract tests under `lib/tests/**`.\n\n**Files:** `README.md`, `CONTRIBUTING.md`, `app/docs/contributing/content.mdx`, `scripts/new-tool-ui-component.ts`, `components/tool-ui/index.ts`, `lib/tests/tool-ui/**/*`\n\n---\n\n### 2026-02-10 — Registry Pipeline Hardened for Per-Component Install\n\n**What changed:** Registry generation moved to component-directory discovery with per-item artifacts and minimal dependency closure (instead of relying on prefixed/manual lists and shared monolith assumptions).\n\nHighlights:\n\n- Component discovery from `components/tool-ui/*` (excluding `shared`)\n- Registry items unprefixed (`tool-ui-foo` → `foo`)\n- Shared artifact dependencies inlined per item where needed\n- Registry tests assert discovered items match component directories\n\n**Why:** Keep shadcn registry output aligned with the actual product surface and reduce drift from manually curated registry definitions.\n\n**Agent impact:** Treat `components/tool-ui` directories as source of truth for registry coverage. After adding/refactoring components, run `pnpm registry:build` and `pnpm registry:check`.\n\n**Files:** `lib/registry/tool-ui-registry.ts`, `lib/tests/registry/tool-ui-registry.test.ts`, `scripts/build-tool-ui-registry.ts`, `public/r/*.json`\n\n---\n\n### 2026-02-10 — CI 1.0 Quality Gates\n\n**What changed:** CI now enforces lint/typecheck/test/registry artifact consistency on push/PR to `main`.\n\n**Why:** Prevent state contract regressions and stale registry artifacts from landing.\n\n**Agent impact:** A local \"done\" state for maintainer work is: `pnpm lint:ci`, `pnpm typecheck`, `pnpm test`, `pnpm registry:check`.\n\n**Files:** `.github/workflows/ci.yml`, `package.json`\n\n---\n\n### 2026-02-10 — Weather Widget V3.1 Clean Break\n\n**What changed:** Weather widget migrated to a strict V3.1 payload contract and removed legacy compatibility layers.\n\nKey contract shifts:\n\n- Canonical parser: `safeParseWeatherWidgetPayload`\n- Payload shape is widget prop contract (+ UI-only props)\n- Deterministic time input is now `time` (`timeBucket` or `localTimeOfDay`)\n- Field names normalized (`current.conditionCode`, `units.temperature`, `forecast[].label`, etc.)\n\n**Why:** Keep provider normalization in the tool layer, simplify widget rendering inputs, and make day/night/effects deterministic.\n\n**Agent impact:** Emit only V3.1 payloads when rendering WeatherWidget. Do not use legacy serializable weather parser/types or provider-specific fields in widget render payloads.\n\n**Files:** `components/tool-ui/weather-widget/schema.ts`, `components/tool-ui/weather-widget/time.ts`, `components/tool-ui/weather-widget/weather-widget.tsx`, `lib/presets/weather-widget.ts`\n\n---\n\n### 2026-02-10 — Weather Tuning Workflow Simplified (Apply-Only)\n\n**What changed:** Removed client-side weather tuning codegen hook (`app/sandbox/weather-tuning/hooks/use-code-gen.ts`) and standardized on repo-backed apply/recover endpoints.\n\n**Why:** Reduce duplicate export logic and keep one source of truth (`components/tool-ui/weather-widget/effects/tuned-presets.ts`).\n\n**Agent impact:** In tuning flows, treat `Apply to repo` as the path to production. Use:\n\n- `POST /api/weather-tuning/apply`\n- `GET /api/weather-tuning/recover`\n\nDo not add/restore parallel clipboard/download codegen paths for production tuning updates.\n\n---\n\n### 2026-02-10 — Test Location Policy Enforced\n\n**What changed:** Weather + shared tests were moved under `lib/tests/**` to ensure they run under current Vitest include globs and are not copied with component folders.\n\n**Why:** Components are copy-paste product surface; test fixtures/infra should stay internal to this repo.\n\n**Agent impact:** Place new executable tests in:\n\n- `lib/tests/**` (preferred)\n- `lib/playground/**` (playground-specific)\n\nTests under `components/tool-ui/**` are out-of-policy and may not run by default.\n\n**Files:** `vitest.config.ts`, `lib/tests/tool-ui/shared/*`, `lib/tests/tool-ui/weather-widget/*`\n\n---\n\n### 2026-01-30 — PostHog Analytics Added\n\n**What changed:** Added PostHog instrumentation with Vercel Analytics dual-tracking.\n\n**Why:** Track component usage, preset selection, and code copying to understand what components and presets are most valuable.\n\n**Files added:**\n\n- `instrumentation-client.ts` — Client-side PostHog initialization\n- `lib/posthog-server.ts` — Server-side PostHog SDK\n- `lib/analytics.ts` — Typed event tracking SDK\n\n**Files modified:**\n\n- `next.config.ts` — Added `/ph/*` proxy rewrites for PostHog\n- `preset-selector.tsx` — Tracks `component_preset_selected`\n- `component-preview-shell.tsx` — Tracks `component_code_copied`, now requires `componentId` prop\n\n**Agent impact:** When adding new trackable interactions, use `analytics.*` methods from `lib/analytics.ts`. The `ComponentPreviewShell` now requires a `componentId` prop.\n\n**Events tracked:**\n\n- `component_preset_selected` — User selects a preset\n- `component_code_copied` — User copies component code\n- `component_viewed` — User views a component (ready to instrument)\n- `search_no_results` — Search with no matches (ready to instrument)\n\n---\n\n### 2026-01-29 — Sandbox Middleware Added\n\n**What changed:** Added middleware to gate `/sandbox/*` routes behind a `?sandbox=true` query param in production. Development mode always allows access.\n\n**Why:** Keep experimental sandboxes (weather effects testing, etc.) accessible for development without exposing them in production.\n\n**Agent impact:** Sandbox pages work normally in dev. In production, add `?sandbox=true` to URL to access.\n\n---\n\n### 2026-01-29 — SVG Glass Panel Effect Added\n\n**What changed:** Added `GlassPanel` component and `useGlassStyles` hook for frosted glass refraction effects using SVG displacement maps via CSS `backdrop-filter`.\n\n**Why:** Provide realistic glass distortion effects for weather widget overlays without WebGL complexity. SVG approach composes naturally with DOM, handles transparency correctly, and doesn't require canvas management.\n\n**Technical approach:**\n\n- SVG `feDisplacementMap` filter encodes X/Y displacement via R/G color channels\n- Chromatic aberration displaces RGB channels by different amounts (R most, G middle, B least)\n- Filter embedded as data URI in `backdrop-filter` CSS property\n- Graceful degradation on unsupported browsers (no effect, content still visible)\n\n**Agent impact:** Use `useGlassStyles` hook or `GlassPanel` component for glass effects. Don't implement WebGL-based glass effects—the SVG approach is simpler and sufficient.\n\n```tsx\n// Hook for applying to existing elements\nconst glassStyles = useGlassStyles({\n  width: 300,\n  height: 200,\n  depth: 12,\n  strength: 40,\n  chromaticAberration: 8,\n});\n\n// Component wrapper\n<GlassPanel depth={10} strength={40}>\n  content\n</GlassPanel>;\n```\n\n**Files:** `components/tool-ui/weather-widget/effects/glass-panel-svg.tsx`\n\n---\n\n### 2026-01-26 — AI SDK v5 → v6 Upgrade\n\n**What changed:** Upgraded AI SDK from v5 to v6, assistant-ui to v0.12 with new hook APIs.\n\n**Why:** Keep dependencies current with latest AI SDK patterns.\n\n**Agent impact:** When referencing AI SDK or assistant-ui patterns, use v6/v0.12 APIs. Don't reference deprecated v5 patterns.\n\n---\n\n### 2026-01-23 — Question Flow Component Added\n\n**What changed:** Added Question Flow component (renamed from \"Wizard Step\" during development). Includes variants: `inline` (default) and `upfront`.\n\n**Why:** Provide structured multi-step question UI for AI assistants.\n\n**Agent impact:** Use `QuestionFlow` for multi-step user input. Component was briefly in showcase, then removed—current best practice is to use `defaultValue` prop for pre-selected state.\n\n**Files:** `components/tool-ui/question-flow/`\n\n---\n\n### 2026-01-22 — Copy Humanization Pass\n\n**What changed:** Systematic rewrite of component docs and non-component docs to remove promotional language and improve voice.\n\n**Why:** Copy should feel like real, believable interactions (see `.claude/docs/copy-guide.md`).\n\n**Agent impact:** Follow copy-guide.md when writing example content. Avoid promotional language, generic placeholders, and tech demo patterns.\n\n---\n\n### 2026-01-19 — Unified Receipt Prop (`choice`)\n\n**What changed:** All receipt state props unified to `choice` across components.\n\n**Before:** `confirmed`, `decision` (inconsistent per component)\n**After:** `choice` (universal)\n\n**Why:** Consistent API for LLM serialization and code readability.\n\n**Agent impact:** Always use `choice` prop for receipt state. Never use `confirmed` or `decision`.\n\n```tsx\n// Correct\n<ApprovalCard choice=\"approved\" />\n<OptionList choice=\"option-a\" />\n\n// Deprecated (don't use)\n<ApprovalCard decision=\"approved\" />\n<OptionList confirmed=\"option-a\" />\n```\n\n**Files:** Plan at `.claude/plans/2025-01-19-unified-receipt-prop.md`\n\n---\n\n### 2026-01-16 — MessageDraft Component Added\n\n**What changed:** Added MessageDraft component for email and Slack message previews.\n\n**Agent impact:** Use `MessageDraft` for email/Slack preview UIs, not custom implementations.\n\n**Files:** `components/tool-ui/message-draft/`\n\n---\n\n### 2026-01-14 — Gallery Removals (X Post, ParameterSlider)\n\n**What changed:** X Post and ParameterSlider removed from gallery (components still exist in codebase).\n\n**Why:** X Post scenario didn't pass believability test. ParameterSlider was experimental.\n\n**Agent impact:** Components exist but aren't prominently featured. ParameterSlider is still usable; X Post exists but has copy issues.\n\n---\n\n### 2026-01-06 — ImageGallery Migration to View Transitions API\n\n**What changed:** ImageGallery migrated from Framer Motion to native View Transitions API.\n\n**Why:** Reduce bundle size, use platform features.\n\n**Agent impact:** Don't add Framer Motion for ImageGallery animations. Use View Transitions API patterns.\n\n---\n\n## Deprecated Patterns\n\n| Don't                                                                                                           | Do Instead                                                                         | Since             |\n| --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ----------------- |\n| Use `confirmed` prop                                                                                            | Use `choice` prop                                                                  | 2026-01-19        |\n| Use `decision` prop                                                                                             | Use `choice` prop                                                                  | 2026-01-19        |\n| Add `maxWidth`/`padding` props                                                                                  | Let users customize via `className`                                                | Project inception |\n| Use nested config objects                                                                                       | Use flat props                                                                     | Project inception |\n| Add Framer Motion to ImageGallery                                                                               | Use View Transitions API                                                           | 2026-01-06        |\n| Use AI SDK v5 patterns                                                                                          | Use AI SDK v6 patterns                                                             | 2026-01-26        |\n| Implement WebGL glass effects                                                                                   | Use `useGlassStyles` or `GlassPanel` from glass-panel-svg                          | 2026-01-29        |\n| Use `ComponentPreviewShell` without `componentId`                                                               | Always pass `componentId` prop for analytics                                       | 2026-01-30        |\n| Use weather payload prop `visual`                                                                               | Use weather payload prop `time`                                                    | 2026-02-10        |\n| Use legacy serializable weather parser/types                                                                    | Use `WeatherWidgetPayloadSchema` + `safeParseWeatherWidgetPayload`                 | 2026-02-10        |\n| Put executable tests in `components/tool-ui/**`                                                                 | Put tests in `lib/tests/**` (or `lib/playground/**`)                               | 2026-02-10        |\n| Use `app/sandbox/weather-tuning/hooks/use-code-gen.ts` export flow                                              | Use apply/recover API routes and `tuned-presets.ts`                                | 2026-02-10        |\n| Curate registry component lists by hand                                                                         | Discover from `components/tool-ui/*` and validate with registry tests              | 2026-02-10        |\n| Import from `../shared` barrel in core interactive components                                                   | Import direct leaf modules from `../shared/*`                                      | 2026-02-10        |\n| Import shadcn primitives (`@/components/ui/*`, `@/lib/ui/cn`) directly in non-adapter component files           | Import those primitives from local `./_adapter` files                              | 2026-02-11        |\n| Require/export per-component `error-boundary.tsx` wrappers                                                      | Export component + schema contracts directly; rely on caller/app-level boundaries  | 2026-02-11        |\n| Add new components without local READMEs and contract scaffold files                                            | Use `pnpm component:new` and keep the full component directory contract            | 2026-02-11        |\n| Depend on registry-shipped `lib/ui/cn.ts` in generated component installs                                       | Use shadcn `@/lib/utils` (`cn`) in adapter output                                  | 2026-02-11        |\n| Use private animation keyframes in `components/tool-ui/**` (e.g. `spring-bounce`, `check-draw`, `fade-blur-in`) | Use Tailwind/tw-animate-compatible classes only                                    | 2026-02-11        |\n| Put `## Source and Install` below features in component docs                                                    | Put `## Source and Install` above `## Key Features` and include GitHub source link | 2026-02-11        |\n\n## Trajectory\n\nBased on recent changes, the project is:\n\n- **Standardizing APIs** — Receipt props unified, flat prop patterns enforced\n- **Maintainer-first DX** — onboarding and docs tuned for direct maintenance in this repo\n- **Polishing copy** — Moving from capability demos to believable scenarios\n- **Keeping dependencies current** — AI SDK v6, assistant-ui v0.12\n- **Reducing bundle** — View Transitions over Framer Motion where possible\n- **Adding specialized components** — MessageDraft, QuestionFlow, StatsDisplay for specific use cases\n- **Adding visual effects** — SVG-based glass refraction for weather widget, preferring CSS/SVG over WebGL\n- **Adding analytics** — PostHog + Vercel Analytics for usage tracking\n- **Hardening weather contracts** — V3.1 clean-break payloads with deterministic `time` input and apply-only tuning workflow\n- **Hardening delivery rails** — registry auto-discovery + CI gates to catch drift early\n- **Tightening portability boundaries** — adapter-only UI imports and removal of per-component error-boundary wrappers\n- **Registry-first distribution UX** — docs and tests now prioritize install/source visibility and hosted registry URLs\n- **Runtime portability over private styling** — shipped Tool UI components avoid repo-private keyframes and rely on tw-animate-compatible motion\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# Tool UI\n\nCopy/paste component library (shadcn/ui model) for AI assistant interfaces. Users copy component directories into projects and modify them. Source code is the product—readability over cleverness.\n\n## Commands\n\n```bash\npnpm dev          # Dev server (Turbopack)\npnpm build        # Production build\npnpm check        # Parallel: typecheck + oxlint + eslint + format check\npnpm lint:fix     # Fix lint errors (run before committing)\npnpm typecheck    # TypeScript checking (tsgo)\npnpm test         # Run tests (Vitest)\n```\n\n## Tooling\n\n- **Formatter**: Oxfmt (config: `apps/www/.oxfmtrc.jsonc`) — Tailwind class sorting + import sorting\n- **Linter**: Oxlint (`apps/www/.oxlintrc.json`) handles standard rules; ESLint (`apps/www/eslint.config.ts`) retained only for `no-restricted-syntax`, `no-restricted-imports`, custom `tool-ui/*` rules, and React Compiler hooks\n- **Typecheck**: tsgo (`@typescript/native-preview`) — native TypeScript compiler\n- **Parallel checks**: `pnpm check` runs typecheck + all linters + format in parallel via `npm-run-all2`\n\n## Stack\n\n- **Package manager**: pnpm (required)\n- **Dependencies**: Only shadcn/ui prerequisites (Tailwind, Radix, Lucide)—users shouldn't need new deps\n\n## Architecture\n\n### Component Structure\n\nEach component lives in `apps/www/components/tool-ui/{name}/`. Reference: `apps/www/components/tool-ui/approval-card/`\n\nKey files:\n\n- `index.tsx` — Barrel exports\n- `{name}.tsx` — Main component\n- `schema.ts` — Zod schema + SerializableX types\n- `_adapter.tsx` — shadcn re-exports\n\nThe `shared/` directory contains utilities all components need.\n\n### Documentation Site\n\nInterconnected registries:\n\n- Component metadata: `apps/www/lib/docs/component-registry.ts`\n- Presets (example data): `apps/www/lib/presets/{component}.ts`\n- Preview rendering: `apps/www/lib/docs/preview-config.tsx`\n- Doc pages: `apps/www/app/docs/{component}/content.mdx`\n- Gallery: `apps/www/app/docs/gallery/page.tsx`\n\n## Key Patterns\n\n### Component API\n\n- **Tailwind for layout**: No `maxWidth`/`padding` props—users customize via `className`\n- **Standard widths**: Cards use `min-w-80 max-w-md`, compact components use `max-w-sm`\n- **Flat props**: Avoid nested config objects\n- **Semantic action IDs**: Use `id: \"confirm\"` / `id: \"cancel\"` for local and decision actions\n- **Receipt state**: Use `choice` prop to render confirmed state (e.g., `<OptionList choice=\"option-a\" />`)\n\n### Main Component Structure\n\nReference: `apps/www/components/tool-ui/approval-card/approval-card.tsx:183`\n\n- Outer `<article>` with `data-slot`, `data-tool-ui-id`, `lang=\"en\"`, `aria-busy`\n- Loading skeleton via `isLoading` prop\n- Optional sibling `ToolUI.LocalActions` / `ToolUI.DecisionActions` surfaces\n\n## Discovery\n\n| What                    | Where                                            |\n| ----------------------- | ------------------------------------------------ |\n| Tool UI components      | `apps/www/components/tool-ui/` (scan barrels)    |\n| Component docs metadata | `apps/www/lib/docs/component-registry.ts`        |\n| Preset configurations   | `apps/www/lib/presets/*.ts`                      |\n| Types & validation      | Colocated `schema.ts` files                      |\n| assistant-ui reference  | `private/reference-docs/assistant-ui/`           |\n| Design system specs     | `private/design-system/`                         |\n\n## Task Guides\n\n- Adding/modifying components: `.claude/docs/component-workflow.md`\n- Writing doc pages: `.claude/docs/mdx-authoring.md`\n- Copy style for examples: `.claude/docs/copy-guide.md`\n- Writing changelog entries: `apps/www/docs/changelog.md`\n"
  },
  {
    "path": "LICENSE.md",
    "content": "MIT License\n\nCopyright (c) 2025 AgentbaseAI Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<a href=\"https://tool-ui.com\">\n  <img src=\".github/assets/header.png\" alt=\"Tool UI\" width=\"100%\">\n</a>\n\nCopy/paste React components for rendering tool calls in AI chat interfaces. Built by [assistant-ui](https://github.com/assistant-ui).\n\n**[Docs](https://tool-ui.com/docs/overview)** · **[Gallery](https://tool-ui.com/docs/gallery)** · **[Quick Start](https://tool-ui.com/docs/quick-start)**\n\nWhen a model calls a tool, most apps dump raw JSON into the conversation. These components turn tool payloads into interactive UI like approvals, forms, tables, charts, and media cards so users can understand and act without leaving the chat.\n\n## Featured Components\n\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\">\n  <tr>\n    <td valign=\"top\" width=\"50%\">\n      <strong><a href=\"https://www.tool-ui.com/docs/option-list\">Option List</a></strong><br>\n      Let users select from multiple choices<br><br>\n      <a href=\"https://tool-ui.com/docs/option-list\">\n        <img src=\".github/assets/option-list.png\" alt=\"Option List component\" width=\"100%\">\n      </a>\n    </td>\n    <td valign=\"top\" width=\"50%\">\n      <strong><a href=\"https://www.tool-ui.com/docs/question-flow\">Question Flow</a></strong><br>\n      Multi-step guided questions with branching<br><br>\n      <a href=\"https://tool-ui.com/docs/question-flow\">\n        <img src=\".github/assets/question-flow.png\" alt=\"Question Flow component\" width=\"100%\">\n      </a>\n    </td>\n  </tr>\n</table>\n\n\n## Why Tool UI?\n\n- **Copy/paste, not install** — shadcn/ui model. Components live in your codebase. No dependency lock-in.\n- **Schema-validated** — Every component has a Zod schema. Parse tool output, render when valid, fail safely when not.\n- **Interactive with receipts** — Components aren't just displays. Users make choices that flow back to the assistant. Choices persist as receipts.\n- **Built on shadcn/ui** — Radix primitives, Tailwind styling, your theme. No new design system to learn.\n\n## Components\n\n- **Progress**: Plan, Progress Tracker\n- **Input**: Option List, Parameter Slider, Preferences Panel, Question Flow\n- **Display**: Citation, Geo Map, Item Carousel, Link Preview, Stats Display, Terminal, Weather Widget\n- **Artifacts**: Chart, Code Block, Code Diff, Data Table, Instagram Post, LinkedIn Post, Message Draft, X Post\n- **Confirmation**: Approval Card, Order Summary\n- **Media**: Audio, Image, Image Gallery, Video\n\nEach component includes a Zod schema for payload validation and presets for realistic example data. Browse them all in the [Gallery](https://tool-ui.com/docs/gallery).\n\n<a href=\"https://tool-ui.com/docs/gallery\">\n  <img src=\".github/assets/gallery.png\" alt=\"Tool UI component gallery\" width=\"100%\">\n</a>\n\n## License\n\nMIT License. See [LICENSE](LICENSE.md) for details.\n"
  },
  {
    "path": "apps/www/.oxfmtrc.jsonc",
    "content": "{\n  \"$schema\": \"https://raw.githubusercontent.com/nicolo-ribaudo/oxfmt/main/npm/oxfmt/schema.json\",\n\n  // Match existing Prettier settings\n  \"semi\": true,\n  \"singleQuote\": false,\n  \"trailingComma\": \"all\",\n  \"tabWidth\": 2,\n  \"printWidth\": 80,\n\n  // Tailwind class sorting (v4 entry point)\n  \"tailwindcss\": {\n    \"stylesheet\": \"./app/styles/globals.css\",\n    \"functions\": [\"clsx\", \"cn\"],\n  },\n\n  // Import sorting (bonus — Prettier didn't do this)\n  \"importSort\": {},\n}\n"
  },
  {
    "path": "apps/www/.oxlintrc.json",
    "content": "{\n  \"$schema\": \"./node_modules/oxlint/configuration_schema.json\",\n  \"plugins\": [\n    \"typescript\",\n    \"react\",\n    \"unicorn\",\n    \"oxc\",\n    \"import\",\n    \"jsx-a11y\",\n    \"nextjs\",\n    \"vitest\"\n  ],\n  \"categories\": {\n    \"correctness\": \"error\"\n  },\n  \"rules\": {\n    \"@typescript-eslint/no-explicit-any\": \"error\",\n    \"no-unused-vars\": [\n      \"error\",\n      {\n        \"ignoreRestSiblings\": true,\n        \"argsIgnorePattern\": \"^_\",\n        \"varsIgnorePattern\": \"^_\"\n      }\n    ],\n\n    // jsx-a11y: match Next.js defaults (warn, not error) for rules\n    // that fire on component library patterns\n    \"jsx-a11y/heading-has-content\": \"warn\",\n    \"jsx-a11y/anchor-has-content\": \"warn\",\n    \"jsx-a11y/media-has-caption\": \"warn\",\n    \"jsx-a11y/no-autofocus\": \"warn\",\n    \"jsx-a11y/no-static-element-interactions\": \"warn\",\n    \"jsx-a11y/click-events-have-key-events\": \"warn\",\n    \"jsx-a11y/prefer-tag-over-role\": \"warn\",\n    \"jsx-a11y/iframe-has-title\": \"warn\",\n    \"jsx-a11y/aria-role\": \"warn\",\n\n    // tool-ui components use <img> for portability (not Next.js specific)\n    \"nextjs/no-img-element\": \"off\",\n\n    // jest rules leak through vitest plugin — disable\n    \"jest/valid-expect\": \"off\",\n\n    // false positives: \\$ before { in template literals, control chars in sanitizer\n    \"no-useless-escape\": \"warn\",\n    \"no-control-regex\": \"warn\"\n  },\n  \"env\": {\n    \"browser\": true,\n    \"builtin\": true,\n    \"es2024\": true,\n    \"node\": true\n  },\n  \"ignorePatterns\": [\n    \"**/dist/**\",\n    \"**/node_modules/**\",\n    \"**/.next/**\",\n    \"**/out/**\",\n    \"**/next-env.d.ts\",\n    \"components/tool-ui/weather-widget/generated/**\"\n  ],\n  \"settings\": {\n    \"next\": {\n      \"rootDir\": [\".\"]\n    },\n    \"react\": {\n      \"version\": \"19\"\n    }\n  }\n}\n"
  },
  {
    "path": "apps/www/.prettierignore",
    "content": "# Generated artifacts (managed by build scripts)\n.next/\npublic/r/\ncomponents/tool-ui/weather-widget/generated/\nlib/weather-authoring/weather-widget/effects/generated/\nlib/weather-authoring/weather-widget/weather-data-overlay.generated.ts\n"
  },
  {
    "path": "apps/www/app/api/builder/chat/route.ts",
    "content": "import { openai } from \"@ai-sdk/openai\";\nimport { anthropic } from \"@ai-sdk/anthropic\";\nimport { streamText, convertToModelMessages, stepCountIs } from \"ai\";\nimport { experimental_createMCPClient as createMCPClient } from \"@ai-sdk/mcp\";\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport { checkRateLimit } from \"@/lib/integrations/rate-limit/upstash\";\nimport { requestDevServer } from \"@/lib/integrations/freestyle/create-chat\";\nimport { SYSTEM_MESSAGE } from \"@/lib/system/tool-builder-message\";\n\nexport const maxDuration = 300;\n\nexport async function POST(req: Request) {\n  try {\n    if (!process.env.OPENAI_API_KEY && !process.env.ANTHROPIC_API_KEY) {\n      return new Response(\n        JSON.stringify({\n          error:\n            \"API key is not configured. Please set OPENAI_API_KEY or ANTHROPIC_API_KEY in your environment variables.\",\n        }),\n        {\n          status: 500,\n          headers: { \"Content-Type\": \"application/json\" },\n        },\n      );\n    }\n\n    const ip =\n      req.headers.get(\"x-forwarded-for\")?.split(\",\")[0] ||\n      req.headers.get(\"x-real-ip\") ||\n      \"anonymous\";\n\n    const rateLimitResult = await checkRateLimit(ip);\n\n    if (!rateLimitResult.success) {\n      return new Response(\n        JSON.stringify({\n          error: \"Rate limit exceeded. Please try again later.\",\n          reset: rateLimitResult.reset,\n        }),\n        {\n          status: 429,\n          headers: {\n            \"Content-Type\": \"application/json\",\n            \"X-RateLimit-Limit\": rateLimitResult.limit.toString(),\n            \"X-RateLimit-Remaining\": rateLimitResult.remaining.toString(),\n            \"X-RateLimit-Reset\": rateLimitResult.reset.toString(),\n          },\n        },\n      );\n    }\n\n    const { messages } = await req.json();\n    const repoId = req.headers.get(\"Repo-Id\");\n\n    if (!messages || !Array.isArray(messages)) {\n      return new Response(\n        JSON.stringify({ error: \"Invalid request: messages array required\" }),\n        {\n          status: 400,\n          headers: { \"Content-Type\": \"application/json\" },\n        },\n      );\n    }\n\n    const modelMessages = await convertToModelMessages(messages);\n\n    // Choose model based on what's available\n    const model = process.env.ANTHROPIC_API_KEY\n      ? anthropic(\"claude-sonnet-4-5-20250929\")\n      : openai(\"gpt-5-nano\");\n\n    // If we have a repoId and Freestyle is configured, use Freestyle tools\n    let tools = {};\n    if (repoId && process.env.FREESTYLE_API_KEY) {\n      try {\n        const { mcpEphemeralUrl } = await requestDevServer({ repoId });\n\n        if (mcpEphemeralUrl) {\n          const devServerMcp = await createMCPClient({\n            transport: new StreamableHTTPClientTransport(\n              new URL(mcpEphemeralUrl),\n            ),\n          });\n\n          tools = await devServerMcp.tools();\n        }\n      } catch (error) {\n        console.error(\"Error setting up Freestyle MCP:\", error);\n        // Continue without Freestyle tools if there's an error\n      }\n    }\n\n    const result = streamText({\n      model,\n      messages: modelMessages,\n      system: SYSTEM_MESSAGE,\n      tools,\n      stopWhen: stepCountIs(100),\n      temperature: 0.7,\n    });\n\n    result.consumeStream();\n\n    return result.toUIMessageStreamResponse();\n  } catch (error) {\n    console.error(\"Error in builder chat API route:\", error);\n    return new Response(\n      JSON.stringify({\n        error: \"An error occurred while processing your request.\",\n      }),\n      {\n        status: 500,\n        headers: { \"Content-Type\": \"application/json\" },\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "apps/www/app/api/builder/create-freestyle/route.ts",
    "content": "import { createChat } from \"@/lib/integrations/freestyle/create-chat\";\n\nexport async function POST() {\n  try {\n    if (!process.env.FREESTYLE_API_KEY) {\n      return new Response(\n        JSON.stringify({\n          error:\n            \"Freestyle API key is not configured. Please set FREESTYLE_API_KEY in your environment variables.\",\n        }),\n        {\n          status: 500,\n          headers: { \"Content-Type\": \"application/json\" },\n        },\n      );\n    }\n\n    const { repoId, ephemeralUrl, mcpEphemeralUrl } = await createChat();\n\n    return new Response(\n      JSON.stringify({\n        repoId,\n        ephemeralUrl,\n        mcpEphemeralUrl,\n      }),\n      {\n        status: 200,\n        headers: { \"Content-Type\": \"application/json\" },\n      },\n    );\n  } catch (error) {\n    console.error(\"Error creating Freestyle project:\", error);\n    return new Response(\n      JSON.stringify({\n        error: \"Failed to create Freestyle project.\",\n      }),\n      {\n        status: 500,\n        headers: { \"Content-Type\": \"application/json\" },\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "apps/www/app/api/chat/route.ts",
    "content": "import { openai } from \"@ai-sdk/openai\";\nimport { streamText, convertToModelMessages, tool } from \"ai\";\nimport { z } from \"zod\";\nimport { checkRateLimit } from \"@/lib/integrations/rate-limit/upstash\";\nimport { getMockTasks } from \"@/lib/mocks/tasks\";\nimport { STATS_DISPLAY_DATA } from \"@/lib/mocks/chat-showcase-data\";\n\nexport const runtime = \"edge\";\n\nconst DEMO_SYSTEM_PROMPT = `You are a helpful assistant that showcases the power of Tool UI—a copy-paste component library for AI assistant interfaces.\n\nYour role is to demonstrate how AI assistants can render rich, interactive UIs in chat. Use the available tools proactively and enthusiastically when they fit the user's request.\n\n## Tools and when to use them\n\n- **show_plan**: Present a step-by-step plan for multi-step tasks. Use when the user asks for a plan, workflow, checklist, deployment steps, research approach, or \"how do I\" questions that break down into phases.\n- **get_tasks**: Retrieve and display support tickets or task lists. Use when the user asks about tickets, tasks, support queue, what's open, or wants to see a table of work items.\n- **show_stats**: Display key metrics and KPIs. Use when the user asks about performance, revenue, dashboards, quarterly numbers, or business metrics.\n- **show_terminal**: Show command-line output (test results, logs, build output). Use when the user asks to run tests, show terminal output, execute a command, or see build/log output.\n\n## Guidelines\n\n- Be concise and friendly. Add a short preamble before or after tool output—don't repeat what the UI already shows.\n- NEVER describe or assume data without calling the tool. Always invoke the tool first—the tool returns real data.\n- When the user asks to see support tickets, tasks, or a queue → ALWAYS call get_tasks.\n- When the user asks for a plan, deployment steps, or a checklist → ALWAYS call show_plan.\n- When the user asks about Q4, metrics, revenue, or dashboards → ALWAYS call show_stats.\n- When the user asks to run tests, execute a command, or show terminal output → ALWAYS call show_terminal.\n- When using show_plan, create relevant plans (e.g. \"Deploy to production\", \"Research competitors\").\n- When using get_tasks, the tool returns a support ticket queue. Present it with a brief intro.\n- When get_tasks returns an empty list (no tickets), you MUST add a friendly message such as: \"It looks like there are currently no support tickets in the system. If you need assistance with anything else, feel free to ask!\"\n- When using show_stats, the tool returns Q4 metrics. Present it with a brief intro.\n- When using show_terminal, create realistic output for commands like \"pnpm test auth\", \"npm run build\".\n- Keep responses brief—let the Tool UI components do the visual heavy lifting.`;\n\nexport async function POST(req: Request) {\n  try {\n    if (!process.env.OPENAI_API_KEY) {\n      return new Response(\n        JSON.stringify({\n          error:\n            \"OpenAI API key is not configured. Please set OPENAI_API_KEY in your environment variables.\",\n        }),\n        {\n          status: 500,\n          headers: { \"Content-Type\": \"application/json\" },\n        },\n      );\n    }\n\n    const ip =\n      req.headers.get(\"x-forwarded-for\")?.split(\",\")[0] ||\n      req.headers.get(\"x-real-ip\") ||\n      \"anonymous\";\n\n    const rateLimitResult = await checkRateLimit(ip);\n\n    if (!rateLimitResult.success) {\n      return new Response(\n        JSON.stringify({\n          error: \"Rate limit exceeded. Please try again later.\",\n          reset: rateLimitResult.reset,\n        }),\n        {\n          status: 429,\n          headers: {\n            \"Content-Type\": \"application/json\",\n            \"X-RateLimit-Limit\": rateLimitResult.limit.toString(),\n            \"X-RateLimit-Remaining\": rateLimitResult.remaining.toString(),\n            \"X-RateLimit-Reset\": rateLimitResult.reset.toString(),\n          },\n        },\n      );\n    }\n\n    const { messages } = await req.json();\n\n    if (!messages || !Array.isArray(messages)) {\n      return new Response(\n        JSON.stringify({ error: \"Invalid request: messages array required\" }),\n        {\n          status: 400,\n          headers: { \"Content-Type\": \"application/json\" },\n        },\n      );\n    }\n\n    const modelMessages = await convertToModelMessages(messages);\n\n    const result = streamText({\n      model: openai(\"gpt-4o-mini\"),\n      messages: modelMessages,\n      system: DEMO_SYSTEM_PROMPT,\n      tools: {\n        show_plan: tool({\n          description:\n            \"Present a step-by-step plan for the user to follow. Use for workflows, checklists, deployment steps, research plans, or any multi-phase task.\",\n          inputSchema: z.object({\n            title: z.string().describe(\"Short title for the plan\"),\n            description: z.string().optional().describe(\"Context or goal\"),\n            todos: z\n              .array(\n                z.object({\n                  id: z.string(),\n                  label: z.string(),\n                  status: z\n                    .enum([\"pending\", \"in_progress\", \"completed\", \"cancelled\"])\n                    .default(\"pending\"),\n                  description: z.string().optional(),\n                }),\n              )\n              .min(1)\n              .describe(\"Steps in order\"),\n          }),\n          execute: async ({ title, description, todos }) => {\n            return {\n              id: `plan-${Date.now()}`,\n              title,\n              description: description ?? title,\n              todos: todos.map((t) => ({\n                ...t,\n                status: t.status ?? \"pending\",\n              })),\n            };\n          },\n        }),\n        get_tasks: tool({\n          description:\n            \"Retrieve the support ticket queue and display it in a sortable table. Use when the user asks about tickets, tasks, support queue, or open work.\",\n          inputSchema: z.object({\n            assignee: z\n              .string()\n              .optional()\n              .describe('Filter by assignee name (e.g. \"Chen\", \"Patel\")'),\n          }),\n          execute: async ({ assignee }) => {\n            const tasks = await getMockTasks({ assignee });\n            const rank = { high: 1, medium: 2, low: 3 } as const;\n            const sorted = [...tasks].sort((a, b) => {\n              const byUrgency = rank[a.priority] - rank[b.priority];\n              if (byUrgency !== 0) return byUrgency;\n              return (\n                new Date(b.created).getTime() - new Date(a.created).getTime()\n              );\n            });\n\n            const columns = [\n              {\n                key: \"priority\",\n                label: \"Urgency\",\n                format: {\n                  kind: \"status\" as const,\n                  statusMap: {\n                    high: { tone: \"danger\" as const, label: \"High\" },\n                    medium: { tone: \"warning\" as const, label: \"Medium\" },\n                    low: { tone: \"neutral\" as const, label: \"Low\" },\n                  },\n                },\n              },\n              {\n                key: \"issue\",\n                label: \"Issue\",\n                truncate: true,\n                priority: \"primary\" as const,\n              },\n              {\n                key: \"customer\",\n                label: \"Customer\",\n                priority: \"primary\" as const,\n              },\n              {\n                key: \"status\",\n                label: \"Status\",\n                format: {\n                  kind: \"badge\" as const,\n                  colorMap: {\n                    open: \"info\",\n                    \"in-progress\": \"warning\",\n                    waiting: \"neutral\",\n                    done: \"success\",\n                  },\n                },\n              },\n              { key: \"assignee\", label: \"Owner\" },\n              {\n                key: \"created\",\n                label: \"Created\",\n                format: {\n                  kind: \"date\" as const,\n                  dateFormat: \"relative\" as const,\n                },\n                hideOnMobile: true,\n              },\n            ];\n\n            const data = sorted.map((t) => ({\n              id: t.id,\n              issue: t.issue,\n              customer: t.customer,\n              priority: t.priority,\n              status: t.status,\n              assignee: t.assignee,\n              created: t.created,\n              urgencyOrder:\n                t.priority === \"high\" ? 1 : t.priority === \"medium\" ? 2 : 3,\n            }));\n\n            return {\n              id: `tasks-${Date.now()}`,\n              columns,\n              data,\n              rowIdKey: \"id\",\n              defaultSort: { by: \"urgencyOrder\", direction: \"asc\" as const },\n            };\n          },\n        }),\n        show_stats: tool({\n          description:\n            \"Display key metrics and KPIs in a visual stats grid. Use when the user asks about performance, revenue, dashboards, quarterly numbers, or business metrics.\",\n          inputSchema: z.object({}),\n          execute: async () => {\n            return {\n              id: `stats-${Date.now()}`,\n              ...STATS_DISPLAY_DATA,\n            };\n          },\n        }),\n        show_terminal: tool({\n          description:\n            \"Show command-line output (test results, logs, build output). Use when the user asks to run tests, execute a command, show terminal output, or see build/log results.\",\n          inputSchema: z.object({\n            command: z.string().describe(\"The command that was run\"),\n            stdout: z.string().describe(\"The terminal output\"),\n            exitCode: z.number().describe(\"Exit code (0 = success)\"),\n            durationMs: z.number().optional().describe(\"Execution time in ms\"),\n          }),\n          execute: async ({ command, stdout, exitCode, durationMs }) => {\n            return {\n              id: `terminal-${Date.now()}`,\n              command,\n              stdout,\n              exitCode,\n              durationMs,\n            };\n          },\n        }),\n      },\n      temperature: 0.7,\n    });\n\n    return result.toUIMessageStreamResponse();\n  } catch (error) {\n    console.error(\"Error in chat API route:\", error);\n    return new Response(\n      JSON.stringify({\n        error: \"An error occurred while processing your request.\",\n      }),\n      {\n        status: 500,\n        headers: { \"Content-Type\": \"application/json\" },\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "apps/www/app/api/mcp-tools/route.ts",
    "content": "import { experimental_createMCPClient as createMCPClient } from \"@ai-sdk/mcp\";\n\nexport const runtime = \"edge\";\n\ninterface MCPTool {\n  name: string;\n  description?: string;\n  inputSchema: {\n    type: string;\n    properties?: Record<string, unknown>;\n    required?: string[];\n  };\n}\n\nexport async function POST(req: Request) {\n  try {\n    const { serverUrl, transportType = \"http\" } = await req.json();\n\n    if (!serverUrl || typeof serverUrl !== \"string\") {\n      return new Response(\n        JSON.stringify({ error: \"Invalid request: serverUrl is required\" }),\n        {\n          status: 400,\n          headers: { \"Content-Type\": \"application/json\" },\n        },\n      );\n    }\n\n    // Validate URL format\n    try {\n      new URL(serverUrl);\n    } catch {\n      return new Response(\n        JSON.stringify({\n          error: \"Invalid URL format\",\n        }),\n        {\n          status: 400,\n          headers: { \"Content-Type\": \"application/json\" },\n        },\n      );\n    }\n\n    // Use Vercel AI SDK's MCP client\n    console.log(\n      `[MCP] Creating MCP client for ${serverUrl} with transport: ${transportType}`,\n    );\n\n    try {\n      const mcpClient = await createMCPClient({\n        transport: {\n          type: transportType as \"http\" | \"sse\",\n          url: serverUrl,\n        },\n      });\n\n      console.log(`[MCP] Client created, fetching tools...`);\n      const tools = await mcpClient.tools();\n      console.log(`[MCP] Received ${Object.keys(tools).length} tools`);\n\n      // Convert AI SDK tool format to our format\n      const mcpTools: MCPTool[] = Object.entries(tools).map(([name, tool]) => ({\n        name,\n        description: tool.description,\n        inputSchema: tool.inputSchema as unknown as {\n          type: string;\n          properties?: Record<string, unknown>;\n          required?: string[];\n        },\n      }));\n\n      // Close the client\n      await mcpClient.close();\n\n      return new Response(\n        JSON.stringify({\n          tools: mcpTools,\n          serverUrl,\n          count: mcpTools.length,\n        }),\n        {\n          status: 200,\n          headers: { \"Content-Type\": \"application/json\" },\n        },\n      );\n    } catch (error) {\n      console.error(`[MCP] Error with AI SDK client:`, error);\n\n      return new Response(\n        JSON.stringify({\n          error: `Could not connect to MCP server: ${error instanceof Error ? error.message : \"Unknown error\"}`,\n        }),\n        {\n          status: 500,\n          headers: { \"Content-Type\": \"application/json\" },\n        },\n      );\n    }\n  } catch (error) {\n    console.error(\"Error fetching MCP tools:\", error);\n    return new Response(\n      JSON.stringify({\n        error: \"An error occurred while fetching MCP tools\",\n        details: error instanceof Error ? error.message : \"Unknown error\",\n      }),\n      {\n        status: 500,\n        headers: { \"Content-Type\": \"application/json\" },\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "apps/www/app/api/playground/chat/route.ts",
    "content": "import type { NextRequest } from \"next/server\";\n\nimport type { UIMessage } from \"ai\";\n\nimport { findPrototype, streamPrototypeResponse } from \"@/lib/playground\";\nimport { PROTOTYPE_SLUG_HEADER } from \"@/lib/playground/constants\";\n\nconst isUiMessageArray = (value: unknown): value is UIMessage[] =>\n  Array.isArray(value);\n\nconst extractSlug = (request: NextRequest): string | null => {\n  const headerSlug = request.headers.get(PROTOTYPE_SLUG_HEADER)?.trim();\n  if (headerSlug) {\n    return headerSlug;\n  }\n  const url = new URL(request.url);\n  const querySlug = url.searchParams.get(\"slug\")?.trim();\n  if (querySlug) {\n    return querySlug;\n  }\n  return null;\n};\n\nexport async function POST(request: NextRequest) {\n  const slug = extractSlug(request);\n  if (!slug) {\n    return new Response(JSON.stringify({ error: \"Missing prototype slug.\" }), {\n      status: 400,\n      headers: { \"Content-Type\": \"application/json\" },\n    });\n  }\n\n  const prototype = findPrototype(slug);\n  if (!prototype) {\n    return new Response(\n      JSON.stringify({ error: `Prototype \"${slug}\" not found.` }),\n      {\n        status: 404,\n        headers: { \"Content-Type\": \"application/json\" },\n      },\n    );\n  }\n\n  let body: unknown;\n  try {\n    body = await request.json();\n  } catch {\n    return new Response(JSON.stringify({ error: \"Invalid JSON body.\" }), {\n      status: 400,\n      headers: { \"Content-Type\": \"application/json\" },\n    });\n  }\n\n  const messages =\n    typeof body === \"object\" && body !== null && \"messages\" in body\n      ? (body as Record<string, unknown>).messages\n      : undefined;\n\n  if (!isUiMessageArray(messages)) {\n    return new Response(\n      JSON.stringify({ error: \"Request body must include a messages array.\" }),\n      {\n        status: 400,\n        headers: { \"Content-Type\": \"application/json\" },\n      },\n    );\n  }\n\n  if (process.env.NODE_ENV !== \"production\") {\n    try {\n      const lastAssistantWithTools = [...messages]\n        .reverse()\n        .find(\n          (message) =>\n            message.role === \"assistant\" &&\n            Array.isArray(message.parts) &&\n            message.parts.some(\n              (part) =>\n                typeof part?.type === \"string\" && part.type.startsWith(\"tool-\"),\n            ),\n        );\n      if (lastAssistantWithTools) {\n        const toolParts = lastAssistantWithTools.parts?.filter(\n          (part) =>\n            typeof part?.type === \"string\" && part.type.startsWith(\"tool-\"),\n        );\n        console.debug(\n          \"[playground] forwarding tool parts:\",\n          JSON.stringify(toolParts, null, 2),\n        );\n      }\n    } catch (error) {\n      console.warn(\"[playground] failed to log tool parts\", error);\n    }\n  }\n\n  // Extract frontend tools from request body\n  const clientTools: unknown =\n    typeof body === \"object\" && body !== null && \"tools\" in body\n      ? (body as Record<string, unknown>).tools\n      : undefined;\n\n  const result = await streamPrototypeResponse(\n    prototype,\n    messages,\n    clientTools,\n  );\n  return result.toUIMessageStreamResponse();\n}\n"
  },
  {
    "path": "apps/www/app/api/weather-tuning/_lib/tuned-presets-io.ts",
    "content": "import { readFile } from \"fs/promises\";\nimport path from \"path\";\n\nimport type { WeatherEffectsTunedPresets } from \"../../../../lib/weather-authoring/weather-widget/effects/tuning\";\n\nexport const TOOL_UI_TUNED_PRESETS_PATH = path.join(\n  process.cwd(),\n  \"lib/weather-authoring/presets/tuned-presets.json\",\n);\n\nexport async function readToolUiTunedPresetsFromDisk(): Promise<WeatherEffectsTunedPresets> {\n  const source = await readFile(TOOL_UI_TUNED_PRESETS_PATH, \"utf8\");\n  return JSON.parse(source) as WeatherEffectsTunedPresets;\n}\n"
  },
  {
    "path": "apps/www/app/api/weather-tuning/apply/route.ts",
    "content": "import { writeFile } from \"fs/promises\";\n\nimport type { WeatherConditionCode } from \"../../../../lib/weather-authoring/weather-widget/schema\";\nimport type { CheckpointOverrides } from \"../../../sandbox/weather-compositor/presets\";\nimport {\n  buildCanonicalToolUiPresetsForEditedConditions,\n  replaceEditedConditions,\n} from \"../../../sandbox/weather-tuning/lib/tool-ui-export\";\nimport type { WeatherEffectsTunedPresets } from \"../../../../lib/weather-authoring/weather-widget/effects/tuning\";\nimport {\n  canonicalizeWeatherPresetData,\n  writeWeatherRuntimeArtifacts,\n} from \"../../../../lib/weather-codegen/compile-weather-runtime\";\nimport {\n  readToolUiTunedPresetsFromDisk,\n  TOOL_UI_TUNED_PRESETS_PATH,\n} from \"../_lib/tuned-presets-io\";\nimport { mapToolUiPresetsToCompositor } from \"../../../sandbox/weather-tuning/lib/tool-ui-import\";\n\nexport const runtime = \"nodejs\";\n\nexport async function POST(request: Request) {\n  if (process.env.NODE_ENV === \"production\") {\n    return new Response(\"Disabled in production.\", { status: 403 });\n  }\n\n  type ApplyPayload = {\n    checkpointOverrides?: Partial<\n      Record<WeatherConditionCode, CheckpointOverrides>\n    >;\n    signedOff?: WeatherConditionCode[];\n  };\n\n  let payload: ApplyPayload | null = null;\n  try {\n    payload = (await request.json()) as ApplyPayload;\n  } catch {\n    return new Response(\"Invalid JSON payload.\", { status: 400 });\n  }\n\n  if (\n    !payload?.checkpointOverrides ||\n    typeof payload.checkpointOverrides !== \"object\"\n  ) {\n    return new Response(\"Missing 'checkpointOverrides' field.\", {\n      status: 400,\n    });\n  }\n\n  if (Object.keys(payload.checkpointOverrides).length === 0) {\n    return new Response(\"No tuning changes to apply.\", { status: 400 });\n  }\n\n  let base: WeatherEffectsTunedPresets;\n  try {\n    base = await readToolUiTunedPresetsFromDisk();\n  } catch (error) {\n    console.warn(\n      \"Failed to read current tuned presets; falling back to empty.\",\n      error,\n    );\n    base = {};\n  }\n\n  const repoCheckpointOverrides = mapToolUiPresetsToCompositor(base);\n  const canonicalEditedConditions =\n    buildCanonicalToolUiPresetsForEditedConditions(\n      payload.checkpointOverrides,\n      repoCheckpointOverrides,\n    );\n\n  if (Object.keys(canonicalEditedConditions).length === 0) {\n    return new Response(\"No tuning changes to apply.\", { status: 400 });\n  }\n\n  const merged = replaceEditedConditions(base, canonicalEditedConditions);\n  const canonicalMerged = canonicalizeWeatherPresetData(\n    merged,\n  ) as WeatherEffectsTunedPresets;\n  const content = `${JSON.stringify(canonicalMerged, null, 2)}\\n`;\n  await writeFile(TOOL_UI_TUNED_PRESETS_PATH, content, \"utf8\");\n  const generated = await writeWeatherRuntimeArtifacts(process.cwd());\n\n  const updatedArtifacts = [\n    \"lib/weather-authoring/presets/tuned-presets.json\",\n    ...generated.written,\n  ];\n\n  return Response.json({\n    ok: true,\n    path: \"lib/weather-authoring/presets/tuned-presets.json\",\n    updatedArtifacts,\n    checkpointOverrides: mapToolUiPresetsToCompositor(canonicalMerged),\n  });\n}\n"
  },
  {
    "path": "apps/www/app/api/weather-tuning/recover/route.ts",
    "content": "import { mapToolUiPresetsToCompositor } from \"../../../sandbox/weather-tuning/lib/tool-ui-import\";\nimport { readToolUiTunedPresetsFromDisk } from \"../_lib/tuned-presets-io\";\n\nexport const runtime = \"nodejs\";\n\nexport async function GET() {\n  if (process.env.NODE_ENV === \"production\") {\n    return new Response(\"Disabled in production.\", { status: 403 });\n  }\n\n  try {\n    const presets = await readToolUiTunedPresetsFromDisk();\n    const checkpointOverrides = mapToolUiPresetsToCompositor(presets);\n    return Response.json({ ok: true, checkpointOverrides });\n  } catch (error) {\n    console.error(\"Failed to recover tuning from repo presets.\", error);\n    return new Response(\"Failed to recover tuning from repo presets.\", {\n      status: 500,\n    });\n  }\n}\n"
  },
  {
    "path": "apps/www/app/builder/layout.tsx",
    "content": "import type { ReactNode } from \"react\";\nimport ContentLayout from \"@/app/components/layout/page-shell\";\nimport { HeaderFrame } from \"@/app/components/layout/app-shell\";\nimport { ThemeToggle } from \"@/app/components/builder/theme-toggle\";\n\nexport default function BuilderLayout({ children }: { children: ReactNode }) {\n  return (\n    <HeaderFrame rightContent={<ThemeToggle />}>\n      <ContentLayout>{children}</ContentLayout>\n    </HeaderFrame>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/builder/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Builder\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\"Builder\", \"Construct components visually\");\n}\n"
  },
  {
    "path": "apps/www/app/builder/page.tsx",
    "content": "\"use client\";\n\nimport {\n  AssistantRuntimeProvider,\n  ThreadPrimitive,\n  ComposerPrimitive,\n  MessagePrimitive,\n  useAui,\n  useAuiState,\n  makeAssistantToolUI,\n  ActionBarPrimitive,\n  BranchPickerPrimitive,\n  ErrorPrimitive,\n  makeAssistantTool,\n} from \"@assistant-ui/react\";\nimport {\n  useChatRuntime,\n  AssistantChatTransport,\n} from \"@assistant-ui/react-ai-sdk\";\nimport { ThreadList } from \"@/app/components/assistant-ui/thread-list\";\nimport { MarkdownText } from \"@/app/components/assistant-ui/markdown-text\";\nimport WebView from \"@/app/components/builder/webview\";\nimport {\n  ArrowUpIcon,\n  Square,\n  Loader2,\n  Eye,\n  Code,\n  Copy,\n  Check,\n  PencilIcon,\n  RefreshCw,\n  FileEdit,\n  FileText,\n  CopyIcon,\n  CheckIcon,\n  RefreshCwIcon,\n  ChevronLeftIcon,\n  ChevronRightIcon,\n  ConstructionIcon,\n} from \"lucide-react\";\nimport { MCPIcon } from \"@/app/components/builder/mcp-icon\";\nimport { Button } from \"@/components/ui/button\";\nimport { CodeBlock, CodeBlockCode } from \"@/components/ui/code-block\";\nimport { getComponentCode } from \"@/lib/integrations/freestyle/get-code\";\nimport {\n  Card,\n  CardHeader,\n  CardTitle,\n  CardDescription,\n  CardContent,\n} from \"@/components/ui/card\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport {\n  InputGroup,\n  InputGroupInput,\n  InputGroupAddon,\n} from \"@/components/ui/input-group\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport { useState, useEffect, type FC, createContext, useContext } from \"react\";\nimport { ToolFallback } from \"@/app/components/assistant-ui/tool-fallback\";\nimport { TooltipIconButton } from \"@/app/components/assistant-ui/tooltip-icon-button\";\nimport React from \"react\";\nimport { cn } from \"@/lib/ui/cn\";\n\n// Context for refreshing the preview pane\nconst PreviewRefreshContext = createContext<(() => void) | null>(null);\n\nconst usePreviewRefresh = () => {\n  const refresh = useContext(PreviewRefreshContext);\n  return refresh;\n};\n\n// Module-level holder for the refresh function (accessible from streamCall)\nlet globalRefreshPreview: (() => void) | null = null;\n\nconst PreviewRefreshSetter: FC = () => {\n  const refresh = usePreviewRefresh();\n  useEffect(() => {\n    globalRefreshPreview = refresh;\n    return () => {\n      globalRefreshPreview = null;\n    };\n  }, [refresh]);\n  return null;\n};\n\nconst UserMessage: FC = () => {\n  return (\n    <MessagePrimitive.Root className=\"mx-auto grid w-full max-w-2xl auto-rows-auto grid-cols-[minmax(72px,1fr)_auto] gap-y-2 px-2 py-4 [&:where(>*)]:col-start-2\">\n      <div className=\"relative col-start-2 min-w-0\">\n        <div className=\"bg-muted text-foreground rounded-3xl px-5 py-2.5 break-words\">\n          <MessagePrimitive.Content components={{ Text: MarkdownText }} />\n        </div>\n        <div className=\"absolute top-1/2 left-0 -translate-x-full -translate-y-1/2 pr-2\">\n          <UserActionBar />\n        </div>\n      </div>\n      <BranchPicker className=\"col-span-full col-start-1 row-start-3 -mr-1 justify-end\" />\n    </MessagePrimitive.Root>\n  );\n};\n\nconst AssistantMessage: FC = () => {\n  return (\n    <MessagePrimitive.Root className=\"relative mx-auto w-full max-w-2xl py-4\">\n      <div className=\"text-foreground mx-2 leading-7 break-words\">\n        <MessagePrimitive.Content\n          components={{ Text: MarkdownText, tools: { Fallback: ToolFallback } }}\n        />\n        <MessageError />\n      </div>\n      <div className=\"mt-2 ml-2 flex\">\n        <BranchPicker />\n        <AssistantActionBar />\n      </div>\n    </MessagePrimitive.Root>\n  );\n};\n\nconst MessageError: FC = () => {\n  return (\n    <MessagePrimitive.Error>\n      <ErrorPrimitive.Root className=\"border-destructive bg-destructive/10 text-destructive dark:bg-destructive/5 mt-2 rounded-md border p-3 text-sm dark:text-red-200\">\n        <ErrorPrimitive.Message className=\"line-clamp-2\" />\n      </ErrorPrimitive.Root>\n    </MessagePrimitive.Error>\n  );\n};\n\nconst UserActionBar: FC = () => {\n  return (\n    <ActionBarPrimitive.Root\n      hideWhenRunning\n      autohide=\"not-last\"\n      className=\"flex flex-col items-end\"\n    >\n      <ActionBarPrimitive.Edit asChild>\n        <TooltipIconButton tooltip=\"Edit\" className=\"p-4\">\n          <PencilIcon />\n        </TooltipIconButton>\n      </ActionBarPrimitive.Edit>\n    </ActionBarPrimitive.Root>\n  );\n};\n\nconst AssistantActionBar: FC = () => {\n  return (\n    <ActionBarPrimitive.Root\n      hideWhenRunning\n      autohide=\"not-last\"\n      autohideFloat=\"single-branch\"\n      className=\"text-muted-foreground data-floating:bg-background col-start-3 row-start-2 -ml-1 flex gap-1 data-floating:absolute data-floating:rounded-md data-floating:border data-floating:p-1 data-floating:shadow-sm\"\n    >\n      <ActionBarPrimitive.Copy asChild>\n        <TooltipIconButton tooltip=\"Copy\">\n          <MessagePrimitive.If copied>\n            <CheckIcon />\n          </MessagePrimitive.If>\n          <MessagePrimitive.If copied={false}>\n            <CopyIcon />\n          </MessagePrimitive.If>\n        </TooltipIconButton>\n      </ActionBarPrimitive.Copy>\n      <ActionBarPrimitive.Reload asChild>\n        <TooltipIconButton tooltip=\"Refresh\">\n          <RefreshCwIcon />\n        </TooltipIconButton>\n      </ActionBarPrimitive.Reload>\n    </ActionBarPrimitive.Root>\n  );\n};\n\nconst BranchPicker: FC<BranchPickerPrimitive.Root.Props> = ({\n  className,\n  ...rest\n}) => {\n  return (\n    <BranchPickerPrimitive.Root\n      hideWhenSingleBranch\n      className={cn(\n        \"text-muted-foreground mr-2 -ml-2 inline-flex items-center text-xs\",\n        className,\n      )}\n      {...rest}\n    >\n      <BranchPickerPrimitive.Previous asChild>\n        <TooltipIconButton tooltip=\"Previous\">\n          <ChevronLeftIcon />\n        </TooltipIconButton>\n      </BranchPickerPrimitive.Previous>\n      <span className=\"font-medium\">\n        <BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />\n      </span>\n      <BranchPickerPrimitive.Next asChild>\n        <TooltipIconButton tooltip=\"Next\">\n          <ChevronRightIcon />\n        </TooltipIconButton>\n      </BranchPickerPrimitive.Next>\n    </BranchPickerPrimitive.Root>\n  );\n};\n\nconst EditComposer: FC = () => {\n  return (\n    <div className=\"mx-auto flex w-full max-w-2xl flex-col gap-4 px-2 first:mt-4\">\n      <ComposerPrimitive.Root className=\"bg-muted ml-auto flex w-full max-w-7/8 flex-col rounded-xl\">\n        <ComposerPrimitive.Input\n          className=\"text-foreground flex min-h-[60px] w-full resize-none bg-transparent p-4 outline-none\"\n          autoFocus\n        />\n\n        <div className=\"mx-3 mb-3 flex items-center justify-center gap-2 self-end\">\n          <ComposerPrimitive.Cancel asChild>\n            <Button variant=\"ghost\" size=\"sm\" aria-label=\"Cancel edit\">\n              Cancel\n            </Button>\n          </ComposerPrimitive.Cancel>\n          <ComposerPrimitive.Send asChild>\n            <Button size=\"sm\" aria-label=\"Update message\">\n              Update\n            </Button>\n          </ComposerPrimitive.Send>\n        </div>\n      </ComposerPrimitive.Root>\n    </div>\n  );\n};\n\ninterface MCPTool {\n  name: string;\n  description?: string;\n  inputSchema: {\n    type: string;\n    properties?: Record<string, unknown>;\n    required?: string[];\n  };\n}\n\nconst MCPModal: FC<{\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n}> = ({ open, onOpenChange }) => {\n  const aui = useAui();\n  const [mcpUrl, setMcpUrl] = useState(\"\");\n  const [transportType, setTransportType] = useState<\"http\" | \"sse\">(\"http\");\n  const [tools, setTools] = useState<MCPTool[]>([]);\n  const [loading, setLoading] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n\n  // Auto-detect transport type based on URL\n  useEffect(() => {\n    if (mcpUrl.toLowerCase().endsWith(\"/sse\")) {\n      setTransportType(\"sse\");\n    } else {\n      setTransportType(\"http\");\n    }\n  }, [mcpUrl]);\n\n  const loadTools = async () => {\n    if (!mcpUrl.trim()) return;\n\n    setLoading(true);\n    setError(null);\n\n    try {\n      const response = await fetch(\"/api/mcp-tools\", {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({\n          serverUrl: mcpUrl,\n          transportType,\n        }),\n      });\n\n      const data = await response.json();\n\n      if (!response.ok) {\n        throw new Error(data.error || \"Failed to fetch tools\");\n      }\n\n      setTools(data.tools || []);\n    } catch (err) {\n      setError(err instanceof Error ? err.message : \"Failed to fetch tools\");\n      setTools([]);\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const handleGenerateUI = (tool: MCPTool) => {\n    // Create a formatted prompt with tool information\n    const prompt = `Please create a Tool UI component for the following MCP tool:\n\n**Tool Name:** ${tool.name}\n\n**Description:** ${tool.description || \"No description provided\"}\n\n**Full Schema:**\n\\`\\`\\`json\n${JSON.stringify(tool.inputSchema, null, 2)}\n\\`\\`\\``;\n\n    // Send the message to the current thread\n    aui.thread().append({\n      role: \"user\",\n      content: [{ type: \"text\", text: prompt }],\n    });\n\n    // Close the modal\n    onOpenChange(false);\n  };\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"flex max-h-[80vh] max-w-2xl flex-col\">\n        <DialogHeader>\n          <DialogTitle>Import MCP Tool</DialogTitle>\n        </DialogHeader>\n\n        <div className=\"mb-4 flex gap-2\">\n          <InputGroup className=\"flex-1\">\n            <InputGroupInput\n              placeholder=\"Enter MCP server URL...\"\n              value={mcpUrl}\n              onChange={(e) => setMcpUrl(e.target.value)}\n              onKeyDown={(e) => {\n                if (e.key === \"Enter\") {\n                  loadTools();\n                }\n              }}\n            />\n            <InputGroupAddon align=\"inline-end\" className=\"pr-1\">\n              <Select\n                value={transportType}\n                onValueChange={(value: \"http\" | \"sse\") =>\n                  setTransportType(value)\n                }\n              >\n                <SelectTrigger className=\"h-6 w-auto gap-1 border-0 bg-transparent px-2 text-xs shadow-none hover:bg-transparent focus:ring-0 data-[state=open]:bg-transparent dark:bg-transparent dark:hover:bg-transparent\">\n                  <SelectValue />\n                </SelectTrigger>\n                <SelectContent>\n                  <SelectItem value=\"http\">HTTP</SelectItem>\n                  <SelectItem value=\"sse\">SSE</SelectItem>\n                </SelectContent>\n              </Select>\n            </InputGroupAddon>\n          </InputGroup>\n          <Button onClick={loadTools} disabled={loading || !mcpUrl.trim()}>\n            {loading ? (\n              <>\n                <Loader2 className=\"mr-2 size-4 animate-spin\" />\n                Loading\n              </>\n            ) : (\n              \"Load\"\n            )}\n          </Button>\n        </div>\n\n        {error && (\n          <div className=\"text-destructive bg-destructive/10 mb-4 rounded-md p-3 text-sm\">\n            {error}\n          </div>\n        )}\n\n        <div className=\"flex-1 overflow-y-auto rounded-md border\">\n          {tools.length === 0 ? (\n            <div className=\"text-muted-foreground flex h-32 items-center justify-center text-sm\">\n              {loading ? \"Loading tools...\" : \"No tools loaded\"}\n            </div>\n          ) : (\n            <div className=\"divide-y\">\n              {tools.map((tool) => (\n                <div\n                  key={tool.name}\n                  className=\"hover:bg-muted/50 flex items-center justify-between p-4 transition-colors\"\n                >\n                  <div className=\"mr-4 min-w-0 flex-1\">\n                    <div className=\"text-sm font-medium\">{tool.name}</div>\n                    {tool.description && (\n                      <div className=\"text-muted-foreground mt-1 truncate text-xs\">\n                        {tool.description}\n                      </div>\n                    )}\n                  </div>\n                  <Button\n                    size=\"sm\"\n                    onClick={() => handleGenerateUI(tool)}\n                    className=\"shrink-0\"\n                  >\n                    Generate UI\n                  </Button>\n                </div>\n              ))}\n            </div>\n          )}\n        </div>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nconst Composer: FC = () => {\n  const [mcpModalOpen, setMcpModalOpen] = useState(false);\n  const isNewThread = useAuiState(\n    ({ threadListItem }) => threadListItem.status === \"new\",\n  );\n\n  return (\n    <>\n      <MCPModal open={mcpModalOpen} onOpenChange={setMcpModalOpen} />\n      <div className=\"bg-background sticky bottom-0 mx-auto flex w-full max-w-2xl flex-col gap-4 overflow-visible rounded-t-3xl pb-4 md:pb-6\">\n        <ComposerPrimitive.Root className=\"group/input-group border-input bg-background has-[textarea:focus-visible]:border-ring has-[textarea:focus-visible]:ring-ring/50 relative flex w-full flex-col rounded-3xl border px-1 pt-2 shadow-xs transition-[color,box-shadow] outline-none has-[textarea:focus-visible]:ring-[3px]\">\n          <ComposerPrimitive.Input\n            placeholder=\"Describe the tool UI you want to build...\"\n            className=\"placeholder:text-muted-foreground mb-1 max-h-32 min-h-16 w-full resize-none bg-transparent px-3.5 pt-1.5 pb-3 text-base outline-none focus-visible:ring-0\"\n            rows={1}\n            autoFocus\n          />\n          <div className=\"relative mx-1 mt-2 mb-2 flex items-center justify-between\">\n            <div>\n              {isNewThread && (\n                <Button\n                  type=\"button\"\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  className=\"text-muted-foreground hover:text-foreground h-[34px] gap-1.5 rounded-full text-xs\"\n                  onClick={() => setMcpModalOpen(true)}\n                >\n                  <MCPIcon className=\"size-3.5\" />\n                  <span>MCP</span>\n                </Button>\n              )}\n            </div>\n            <div className=\"flex items-center justify-end\">\n              <ThreadPrimitive.If running={false}>\n                <ComposerPrimitive.Send asChild>\n                  <Button\n                    type=\"submit\"\n                    variant=\"default\"\n                    size=\"icon\"\n                    className=\"size-[34px] rounded-full p-1\"\n                  >\n                    <ArrowUpIcon className=\"size-5\" />\n                  </Button>\n                </ComposerPrimitive.Send>\n              </ThreadPrimitive.If>\n\n              <ThreadPrimitive.If running>\n                <ComposerPrimitive.Cancel asChild>\n                  <Button\n                    type=\"button\"\n                    variant=\"default\"\n                    size=\"icon\"\n                    className=\"border-muted-foreground/60 hover:bg-primary/75 dark:border-muted-foreground/90 size-[34px] rounded-full border\"\n                  >\n                    <Square className=\"size-3.5 fill-white dark:fill-black\" />\n                  </Button>\n                </ComposerPrimitive.Cancel>\n              </ThreadPrimitive.If>\n            </div>\n          </div>\n        </ComposerPrimitive.Root>\n        <div className=\"flex items-center justify-center gap-2 text-center text-xs text-amber-700 dark:text-amber-400\">\n          <ConstructionIcon className=\"size-3.5 shrink-0\" />\n          <span>\n            This builder is under heavy construction and may not always work as\n            expected.\n          </span>\n        </div>\n      </div>\n    </>\n  );\n};\n\nconst Thread: FC = () => {\n  return (\n    <ThreadPrimitive.Root className=\"bg-background flex h-full w-full flex-col\">\n      <ThreadPrimitive.Viewport className=\"relative flex flex-1 flex-col overflow-y-auto px-4\">\n        <ThreadPrimitive.If empty>\n          <div className=\"flex flex-1 items-center justify-center\">\n            <Composer />\n          </div>\n        </ThreadPrimitive.If>\n        <ThreadPrimitive.If empty={false}>\n          <ThreadPrimitive.Messages\n            components={{\n              UserMessage,\n              EditComposer,\n              AssistantMessage,\n            }}\n          />\n          <div className=\"min-h-8 grow\" />\n          <Composer />\n        </ThreadPrimitive.If>\n      </ThreadPrimitive.Viewport>\n    </ThreadPrimitive.Root>\n  );\n};\n\ntype ViewMode = \"rendered\" | \"code\";\n\nexport default function BuilderPage() {\n  const [repoId, setRepoId] = useState<string | null>(null);\n  const repoIdRef = useState({ current: repoId })[0];\n  const [_appId, setAppId] = useState<string | null>(null);\n  const [webviewWidth, setWebviewWidth] = useState(50); // percentage\n  const [viewMode, setViewMode] = useState<ViewMode>(\"rendered\");\n  const [codeContent, setCodeContent] = useState<string | null>(null);\n  const [isCodeLoading, setIsCodeLoading] = useState(false);\n  const [copied, setCopied] = useState(false);\n  const [refreshKey, setRefreshKey] = useState(0);\n  const timestampRef = useState(() => Date.now())[0];\n\n  // Keep ref in sync with state\n  repoIdRef.current = repoId;\n\n  // Load code when switching to code view\n  useEffect(() => {\n    if (viewMode === \"code\" && repoId && !codeContent) {\n      setIsCodeLoading(true);\n      getComponentCode(repoId, \"components/demo-tool-ui.tsx\")\n        .then((content) => {\n          setCodeContent(content);\n          setIsCodeLoading(false);\n        })\n        .catch((err) => {\n          console.error(\"Failed to load code:\", err);\n          setIsCodeLoading(false);\n        });\n    }\n  }, [viewMode, repoId, codeContent]);\n\n  const runtime = useChatRuntime({\n    transport: new AssistantChatTransport({\n      api: \"/api/builder/chat\",\n      headers: async () => {\n        // Auto-create Freestyle project on first message if not already created\n        if (\n          !repoIdRef.current &&\n          process.env.NEXT_PUBLIC_FREESTYLE_ENABLED !== \"false\"\n        ) {\n          try {\n            const response = await fetch(\"/api/builder/create-freestyle\", {\n              method: \"POST\",\n            });\n\n            if (response.ok) {\n              const data = await response.json();\n              setRepoId(data.repoId);\n              repoIdRef.current = data.repoId;\n              // Use a unique ID for this app instance\n              setAppId(data.repoId + \"-\" + timestampRef);\n            }\n          } catch (error) {\n            console.error(\"Failed to create Freestyle project:\", error);\n          }\n        }\n\n        return {\n          \"Repo-Id\": repoIdRef.current || \"\",\n        };\n      },\n    }),\n  });\n\n  const handleCopy = async () => {\n    if (!codeContent) return;\n\n    try {\n      await navigator.clipboard.writeText(codeContent);\n      setCopied(true);\n      setTimeout(() => setCopied(false), 2000);\n    } catch (err) {\n      console.error(\"Failed to copy:\", err);\n    }\n  };\n\n  const handleMouseDown = (e: React.MouseEvent) => {\n    e.preventDefault();\n    const startX = e.clientX;\n    const startWidth = webviewWidth;\n    const containerWidth = window.innerWidth - 240; // Subtract sidebar width\n\n    const handleMouseMove = (e: MouseEvent) => {\n      const diff = startX - e.clientX;\n      const percentageChange = (diff / containerWidth) * 100;\n      const newWidth = Math.min(\n        Math.max(startWidth + percentageChange, 20),\n        80,\n      );\n      setWebviewWidth(newWidth);\n    };\n\n    const handleMouseUp = () => {\n      document.removeEventListener(\"mousemove\", handleMouseMove);\n      document.removeEventListener(\"mouseup\", handleMouseUp);\n    };\n\n    document.addEventListener(\"mousemove\", handleMouseMove);\n    document.addEventListener(\"mouseup\", handleMouseUp);\n  };\n\n  const handleRefreshPreview = () => {\n    setRefreshKey((prev) => prev + 1);\n  };\n\n  return (\n    <PreviewRefreshContext.Provider value={handleRefreshPreview}>\n      <AssistantRuntimeProvider runtime={runtime}>\n        <PreviewRefreshSetter />\n        <div className=\"flex h-full flex-1 flex-col md:flex-row\">\n          {/* Thread List Sidebar - hidden on mobile */}\n          <div className=\"bg-background hidden w-[220px] shrink-0 overflow-y-auto p-4 md:block\">\n            <ThreadList />\n          </div>\n\n          {/* Main Thread Area */}\n          <div\n            className=\"overflow-hidden border md:rounded-t-lg\"\n            style={{ width: repoId ? `${100 - webviewWidth}%` : \"100%\" }}\n          >\n            <Thread />\n          </div>\n\n          {/* Preview/Code Panel */}\n          {repoId && (\n            <>\n              {/* Resize Handle - hidden on mobile */}\n              <div\n                role=\"separator\"\n                className=\"bg-border hover:bg-primary hidden w-1 cursor-col-resize transition-colors md:block\"\n                onMouseDown={handleMouseDown}\n              />\n\n              {/* Preview Panel */}\n              <div\n                className=\"flex flex-col\"\n                style={{ width: `${webviewWidth}%` }}\n              >\n                {/* Header with view toggle and copy/refresh button */}\n                <div className=\"bg-background flex h-12 shrink-0 items-center justify-between border-t border-b px-4\">\n                  <div className=\"flex items-center gap-2\">\n                    <Button\n                      variant={viewMode === \"rendered\" ? \"secondary\" : \"ghost\"}\n                      size=\"sm\"\n                      onClick={() => setViewMode(\"rendered\")}\n                      className=\"gap-2\"\n                    >\n                      <Eye className=\"h-4 w-4\" />\n                      <span className=\"hidden sm:inline\">Preview</span>\n                    </Button>\n                    <Button\n                      variant={viewMode === \"code\" ? \"secondary\" : \"ghost\"}\n                      size=\"sm\"\n                      onClick={() => setViewMode(\"code\")}\n                      className=\"gap-2\"\n                    >\n                      <Code className=\"h-4 w-4\" />\n                      <span className=\"hidden sm:inline\">Code</span>\n                    </Button>\n                  </div>\n                  {viewMode === \"rendered\" ? (\n                    <Button\n                      variant=\"ghost\"\n                      size=\"icon\"\n                      onClick={() => setRefreshKey((prev) => prev + 1)}\n                      title=\"Refresh preview\"\n                    >\n                      <RefreshCw className=\"h-4 w-4\" />\n                    </Button>\n                  ) : (\n                    <Button\n                      variant=\"ghost\"\n                      size=\"icon\"\n                      onClick={handleCopy}\n                      disabled={!codeContent || isCodeLoading}\n                    >\n                      {copied ? (\n                        <Check className=\"h-4 w-4\" />\n                      ) : (\n                        <Copy className=\"h-4 w-4\" />\n                      )}\n                    </Button>\n                  )}\n                </div>\n\n                <div className=\"flex-1 overflow-hidden\">\n                  {viewMode === \"rendered\" ? (\n                    <WebView key={refreshKey} repo={repoId} />\n                  ) : (\n                    <div className=\"h-full overflow-auto p-4\">\n                      {isCodeLoading ? (\n                        <div className=\"flex h-full items-center justify-center\">\n                          <div className=\"text-center\">\n                            <Loader2 className=\"mx-auto h-8 w-8 animate-spin\" />\n                            <p className=\"text-muted-foreground mt-4 text-sm\">\n                              Loading code...\n                            </p>\n                          </div>\n                        </div>\n                      ) : codeContent ? (\n                        <CodeBlock>\n                          <CodeBlockCode code={codeContent} language=\"tsx\" />\n                        </CodeBlock>\n                      ) : (\n                        <div className=\"flex h-full items-center justify-center\">\n                          <p className=\"text-muted-foreground text-sm\">\n                            Failed to load code\n                          </p>\n                        </div>\n                      )}\n                    </div>\n                  )}\n                </div>\n              </div>\n            </>\n          )}\n        </div>\n        <EditFileToolUI />\n        <WriteFileToolUI />\n        <ReadFileToolUI />\n      </AssistantRuntimeProvider>\n    </PreviewRefreshContext.Provider>\n  );\n}\n\nconst EditFileToolUI = makeAssistantTool<\n  {\n    path?: string;\n    edits?: Array<{\n      oldText?: string;\n      newText?: string;\n    }>;\n  },\n  {}\n>({\n  type: \"backend\",\n  toolName: \"edit_file\",\n  streamCall: async (reader) => {\n    await reader.response.get();\n\n    // Refresh the preview pane after edit completes\n    if (globalRefreshPreview) {\n      globalRefreshPreview();\n    }\n  },\n  render: ({ args }) => {\n    console.log(\"EditFileToolUI\", args);\n    const path = args?.path;\n    const edits = args?.edits;\n\n    if (!path && (!edits || edits.length === 0)) {\n      return null;\n    }\n\n    return (\n      <Card className=\"mb-4 w-full\">\n        <CardHeader>\n          <div className=\"flex items-center gap-2\">\n            <PencilIcon className=\"text-primary h-4 w-4\" />\n            <CardTitle className=\"text-base\">Editing File</CardTitle>\n          </div>\n          {path && (\n            <CardDescription className=\"mt-1 font-mono text-xs\">\n              {path}\n            </CardDescription>\n          )}\n        </CardHeader>\n        {edits && edits.length > 0 && (\n          <CardContent>\n            <div className=\"grid gap-2\">\n              {edits.map(\n                (edit, index) =>\n                  (edit.oldText || edit.newText) && (\n                    <CodeBlock\n                      key={index}\n                      className=\"grid overflow-scroll py-2\"\n                    >\n                      {edit.oldText && (\n                        <>\n                          <CodeBlockCode\n                            code={edit.oldText\n                              .split(\"\\n\")\n                              .slice(0, 5)\n                              .join(\"\\n\")}\n                            language=\"tsx\"\n                            className=\"col-start-1 col-end-1 row-start-1 row-end-1 overflow-visible bg-red-200 [&_code]:bg-red-200 [&>pre]:py-0\"\n                          />\n                          {edit.oldText.split(\"\\n\").length > 5 && (\n                            <div className=\"px-4 font-mono text-xs text-red-700\">\n                              +{edit.oldText.split(\"\\n\").length - 5} more\n                            </div>\n                          )}\n                        </>\n                      )}\n                      {edit.newText && (\n                        <>\n                          <CodeBlockCode\n                            code={edit.newText\n                              .trimEnd()\n                              .split(\"\\n\")\n                              .slice(0, 5)\n                              .join(\"\\n\")}\n                            language=\"tsx\"\n                            className=\"col-start-1 col-end-1 row-start-1 row-end-1 overflow-visible bg-green-200 [&_code]:bg-green-200 [&>pre]:py-0\"\n                          />\n                          {edit.newText.split(\"\\n\").length > 5 && (\n                            <div className=\"px-4 font-mono text-xs text-green-700\">\n                              +{edit.newText.split(\"\\n\").length - 5} more\n                            </div>\n                          )}\n                        </>\n                      )}\n                    </CodeBlock>\n                  ),\n              )}\n            </div>\n          </CardContent>\n        )}\n      </Card>\n    );\n  },\n});\n\nconst WriteFileToolUI = makeAssistantTool<\n  {\n    path?: string;\n    content?: string;\n  },\n  {}\n>({\n  type: \"backend\",\n  toolName: \"write_file\",\n  streamCall: async (reader) => {\n    await reader.response.get();\n\n    // Refresh the preview pane after write completes\n    if (globalRefreshPreview) {\n      globalRefreshPreview();\n    }\n  },\n  render: ({ args }) => {\n    const path = args?.path;\n\n    if (!path) {\n      return null;\n    }\n\n    return (\n      <Card className=\"mb-4 w-full\">\n        <CardHeader>\n          <div className=\"flex items-center gap-2\">\n            <FileEdit className=\"text-primary h-4 w-4\" />\n            <CardTitle className=\"text-base\">Writing File</CardTitle>\n          </div>\n          <CardDescription className=\"mt-1 font-mono text-xs\">\n            {path}\n          </CardDescription>\n        </CardHeader>\n      </Card>\n    );\n  },\n});\n\nconst ReadFileToolUI = makeAssistantToolUI<\n  {\n    path?: string;\n  },\n  {}\n>({\n  toolName: \"read_file\",\n  render: ({ args }) => {\n    const path = args?.path;\n\n    if (!path) {\n      return null;\n    }\n\n    return (\n      <Card className=\"mb-4 w-full\">\n        <CardHeader>\n          <div className=\"flex items-center gap-2\">\n            <FileText className=\"text-primary h-4 w-4\" />\n            <CardTitle className=\"text-base\">Reading File</CardTitle>\n          </div>\n          <CardDescription className=\"mt-1 font-mono text-xs\">\n            {path}\n          </CardDescription>\n        </CardHeader>\n      </Card>\n    );\n  },\n});\n"
  },
  {
    "path": "apps/www/app/components/analytics/posthog-init.client.tsx",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\n\nconst apiKey = process.env[\"NEXT_PUBLIC_POSTHOG_API_KEY\"];\nconst isDev = process.env.NODE_ENV === \"development\";\nlet didInit = false;\nlet initPromise: Promise<void> | null = null;\n\nexport function PostHogInit() {\n  useEffect(() => {\n    if (didInit || initPromise || !apiKey) {\n      return;\n    }\n\n    initPromise = (async () => {\n      const { default: posthog } = await import(\"posthog-js\");\n      if (didInit) {\n        return;\n      }\n\n      posthog.init(apiKey, {\n        api_host: \"/ph\",\n        ui_host: \"https://us.posthog.com\",\n        defaults: \"2025-11-30\",\n        capture_exceptions: true,\n        advanced_disable_flags: true, // Skip feature flags API call\n        loaded: (instance) => {\n          // Tag all events with environment for filtering.\n          instance.register({\n            environment: isDev ? \"development\" : \"production\",\n            app: \"tool-ui\",\n          });\n        },\n      });\n\n      didInit = true;\n    })()\n      .catch((error) => {\n        if (isDev) {\n          console.error(\"[PostHog] failed to initialize\", error);\n        }\n      })\n      .finally(() => {\n        initPromise = null;\n      });\n  }, []);\n\n  return null;\n}\n"
  },
  {
    "path": "apps/www/app/components/assistant-ui/markdown-text.tsx",
    "content": "\"use client\";\n\nimport \"@assistant-ui/react-markdown/styles/dot.css\";\n\nimport {\n  type CodeHeaderProps,\n  MarkdownTextPrimitive,\n  unstable_memoizeMarkdownComponents as memoizeMarkdownComponents,\n  useIsMarkdownCodeBlock,\n} from \"@assistant-ui/react-markdown\";\nimport remarkGfm from \"remark-gfm\";\nimport { type FC, memo, useState } from \"react\";\nimport { CheckIcon, CopyIcon } from \"lucide-react\";\n\nimport { TooltipIconButton } from \"@/app/components/assistant-ui/tooltip-icon-button\";\nimport { cn } from \"@/lib/ui/cn\";\n\nconst MarkdownTextImpl = () => {\n  return (\n    <MarkdownTextPrimitive\n      remarkPlugins={[remarkGfm]}\n      className=\"aui-md\"\n      components={defaultComponents}\n    />\n  );\n};\n\nexport const MarkdownText = memo(MarkdownTextImpl);\n\nconst CodeHeader: FC<CodeHeaderProps> = ({ language, code }) => {\n  const { isCopied, copyToClipboard } = useCopyToClipboard();\n  const onCopy = () => {\n    if (!code || isCopied) return;\n    copyToClipboard(code);\n  };\n\n  return (\n    <div className=\"aui-code-header-root bg-muted-foreground/15 text-foreground dark:bg-muted-foreground/20 mt-4 flex items-center justify-between gap-4 rounded-t-lg px-4 py-2 text-sm font-semibold\">\n      <span className=\"aui-code-header-language lowercase [&>span]:text-xs\">\n        {language}\n      </span>\n      <TooltipIconButton tooltip=\"Copy\" onClick={onCopy}>\n        {!isCopied && <CopyIcon />}\n        {isCopied && <CheckIcon />}\n      </TooltipIconButton>\n    </div>\n  );\n};\n\nconst useCopyToClipboard = ({\n  copiedDuration = 3000,\n}: {\n  copiedDuration?: number;\n} = {}) => {\n  const [isCopied, setIsCopied] = useState<boolean>(false);\n\n  const copyToClipboard = (value: string) => {\n    if (!value) return;\n\n    navigator.clipboard.writeText(value).then(() => {\n      setIsCopied(true);\n      setTimeout(() => setIsCopied(false), copiedDuration);\n    });\n  };\n\n  return { isCopied, copyToClipboard };\n};\n\nconst defaultComponents = memoizeMarkdownComponents({\n  h1: ({ className, ...props }) => (\n    <h1\n      className={cn(\n        \"aui-md-h1 mb-8 scroll-m-20 text-4xl font-extrabold last:mb-0\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n  h2: ({ className, ...props }) => (\n    <h2\n      className={cn(\n        \"aui-md-h2 mt-8 mb-4 scroll-m-20 text-3xl font-semibold first:mt-0 last:mb-0\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n  h3: ({ className, ...props }) => (\n    <h3\n      className={cn(\n        \"aui-md-h3 mt-6 mb-4 scroll-m-20 text-2xl font-semibold first:mt-0 last:mb-0\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n  h4: ({ className, ...props }) => (\n    <h4\n      className={cn(\n        \"aui-md-h4 mt-6 mb-4 scroll-m-20 text-xl font-semibold first:mt-0 last:mb-0\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n  h5: ({ className, ...props }) => (\n    <h5\n      className={cn(\n        \"aui-md-h5 my-4 text-lg font-semibold first:mt-0 last:mb-0\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n  h6: ({ className, ...props }) => (\n    <h6\n      className={cn(\n        \"aui-md-h6 my-4 font-semibold first:mt-0 last:mb-0\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n  p: ({ className, ...props }) => (\n    <p\n      className={cn(\"aui-md-p mt-5 mb-5 first:mt-0 last:mb-0\", className)}\n      {...props}\n    />\n  ),\n  a: ({ className, ...props }) => (\n    <a\n      className={cn(\n        \"aui-md-a text-primary font-medium underline underline-offset-4\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n  blockquote: ({ className, ...props }) => (\n    <blockquote\n      className={cn(\"aui-md-blockquote border-l-2 pl-6 italic\", className)}\n      {...props}\n    />\n  ),\n  ul: ({ className, ...props }) => (\n    <ul\n      className={cn(\n        \"aui-md-ul my-5 ml-6 list-disc border border-red-500 [&>li]:mt-2\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n  ol: ({ className, ...props }) => (\n    <ol\n      className={cn(\"aui-md-ol my-5 ml-6 list-decimal [&>li]:mt-2\", className)}\n      {...props}\n    />\n  ),\n  hr: ({ className, ...props }) => (\n    <hr className={cn(\"aui-md-hr my-5 border-b\", className)} {...props} />\n  ),\n  table: ({ className, ...props }) => (\n    <table\n      className={cn(\n        \"aui-md-table my-5 w-full border-separate border-spacing-0 overflow-y-auto\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n  th: ({ className, ...props }) => (\n    <th\n      className={cn(\n        \"aui-md-th bg-muted px-4 py-2 text-left font-bold first:rounded-tl-lg last:rounded-tr-lg [&[align=center]]:text-center [&[align=right]]:text-right\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n  td: ({ className, ...props }) => (\n    <td\n      className={cn(\n        \"aui-md-td border-b border-l px-4 py-2 text-left last:border-r [&[align=center]]:text-center [&[align=right]]:text-right\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n  tr: ({ className, ...props }) => (\n    <tr\n      className={cn(\n        \"aui-md-tr m-0 border-b p-0 first:border-t [&:last-child>td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n  sup: ({ className, ...props }) => (\n    <sup\n      className={cn(\"aui-md-sup [&>a]:text-xs [&>a]:no-underline\", className)}\n      {...props}\n    />\n  ),\n  pre: ({ className, ...props }) => (\n    <pre\n      className={cn(\n        \"aui-md-pre overflow-x-auto !rounded-t-none rounded-b-lg bg-black p-4 text-white\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n  code: function Code({ className, ...props }) {\n    const isCodeBlock = useIsMarkdownCodeBlock();\n    return (\n      <code\n        className={cn(\n          !isCodeBlock &&\n            \"aui-md-inline-code bg-muted rounded border font-semibold\",\n          className,\n        )}\n        {...props}\n      />\n    );\n  },\n  CodeHeader,\n});\n"
  },
  {
    "path": "apps/www/app/components/assistant-ui/thread-list.tsx",
    "content": "import { useCallback, type FC } from \"react\";\nimport {\n  ThreadListItemPrimitive,\n  ThreadListPrimitive,\n  useAuiState,\n} from \"@assistant-ui/react\";\nimport { ArchiveIcon, HistoryIcon, PlusIcon } from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { TooltipIconButton } from \"@/app/components/assistant-ui/tooltip-icon-button\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\n\nexport type ThreadListProps = {\n  allowedThreadIds?: Set<string>;\n  newLabel?: string;\n  emptyState?: React.ReactNode;\n  onReplay?: (threadId: string) => void;\n};\n\nexport const ThreadList: FC<ThreadListProps> = ({\n  allowedThreadIds,\n  newLabel = \"New Tool UI\",\n  emptyState,\n  onReplay,\n}) => {\n  return (\n    <ThreadListPrimitive.Root className=\"aui-root aui-thread-list-root flex flex-col items-stretch gap-1.5\">\n      <ThreadListNew label={newLabel} />\n      <ThreadListItems\n        allowedThreadIds={allowedThreadIds}\n        emptyState={emptyState}\n        onReplay={onReplay}\n      />\n    </ThreadListPrimitive.Root>\n  );\n};\n\nconst ThreadListNew: FC<{ label: string }> = ({ label }) => {\n  return (\n    <ThreadListPrimitive.New asChild>\n      <Button\n        className=\"aui-thread-list-new hover:bg-muted data-active:bg-muted flex items-center justify-start gap-1 rounded-lg px-2.5 py-2 text-start\"\n        variant=\"ghost\"\n      >\n        <PlusIcon />\n        {label}\n      </Button>\n    </ThreadListPrimitive.New>\n  );\n};\n\ntype ThreadListItemsProps = {\n  allowedThreadIds?: Set<string>;\n  emptyState?: React.ReactNode;\n  onReplay?: (threadId: string) => void;\n};\n\nconst ThreadListItems: FC<ThreadListItemsProps> = ({\n  allowedThreadIds,\n  emptyState,\n  onReplay,\n}) => {\n  const threadsState = useAuiState(useCallback((state) => state.threads, []));\n  const isLoading = threadsState.isLoading;\n  const threadIds = threadsState.threadIds;\n\n  if (isLoading) {\n    return <ThreadListSkeleton />;\n  }\n\n  const indexMap = threadIds\n    .map((id, index) => ({ id, index }))\n    .filter(({ id }) => !allowedThreadIds || allowedThreadIds.has(id));\n\n  if (indexMap.length === 0) {\n    return emptyState ? <>{emptyState}</> : null;\n  }\n\n  return indexMap.map(({ id, index }) => (\n    <ThreadListPrimitive.ItemByIndex\n      key={id}\n      index={index}\n      components={{\n        ThreadListItem: () => <ThreadListItem onReplay={onReplay} />,\n      }}\n    />\n  ));\n};\n\nconst ThreadListSkeleton: FC = () => {\n  return (\n    <>\n      {Array.from({ length: 5 }, (_, i) => (\n        <div\n          key={i}\n          role=\"status\"\n          aria-label=\"Loading threads\"\n          aria-live=\"polite\"\n          className=\"aui-thread-list-skeleton-wrapper flex items-center gap-2 rounded-md px-3 py-2\"\n        >\n          <Skeleton className=\"aui-thread-list-skeleton h-[22px] grow\" />\n        </div>\n      ))}\n    </>\n  );\n};\n\nconst ThreadListItem: FC<{ onReplay?: (threadId: string) => void }> = ({\n  onReplay,\n}) => {\n  return (\n    <ThreadListItemPrimitive.Root className=\"aui-thread-list-item hover:bg-muted focus-visible:bg-muted focus-visible:ring-ring data-active:bg-muted flex items-center gap-2 rounded-lg transition-all focus-visible:ring-2 focus-visible:outline-none\">\n      <ThreadListItemPrimitive.Trigger className=\"aui-thread-list-item-trigger grow px-3 py-2 text-start\">\n        <ThreadListItemTitle />\n      </ThreadListItemPrimitive.Trigger>\n      <ThreadListItemActions onReplay={onReplay} />\n    </ThreadListItemPrimitive.Root>\n  );\n};\n\nconst ThreadListItemTitle: FC = () => {\n  return (\n    <span className=\"aui-thread-list-item-title text-sm\">\n      <ThreadListItemPrimitive.Title fallback=\"New Chat\" />\n    </span>\n  );\n};\n\nconst ThreadListItemActions: FC<{ onReplay?: (threadId: string) => void }> = ({\n  onReplay,\n}) => {\n  const threadId = useAuiState(({ threadListItem }) => threadListItem.id);\n\n  return (\n    <div className=\"flex items-center gap-1 pr-2\">\n      {onReplay && (\n        <TooltipIconButton\n          className=\"aui-thread-list-item-replay text-foreground hover:text-primary size-4 p-0\"\n          variant=\"ghost\"\n          tooltip=\"Replay with another instance\"\n          onClick={(event) => {\n            event.stopPropagation();\n            onReplay(threadId);\n          }}\n        >\n          <HistoryIcon />\n        </TooltipIconButton>\n      )}\n      <ThreadListItemPrimitive.Archive asChild>\n        <TooltipIconButton\n          className=\"aui-thread-list-item-archive text-foreground hover:text-primary size-4 p-0\"\n          variant=\"ghost\"\n          tooltip=\"Archive thread\"\n        >\n          <ArchiveIcon />\n        </TooltipIconButton>\n      </ThreadListItemPrimitive.Archive>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/www/app/components/assistant-ui/tool-fallback.tsx",
    "content": "\"use client\";\n\nimport type { ToolCallMessagePartComponent } from \"@assistant-ui/react\";\nimport { CheckIcon, ChevronDownIcon, ChevronUpIcon } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\n\nexport const ToolFallback: ToolCallMessagePartComponent = ({\n  toolName,\n  argsText,\n  result,\n}) => {\n  const [isCollapsed, setIsCollapsed] = useState(true);\n  return (\n    <div className=\"aui-tool-fallback-root mb-4 flex w-full flex-col gap-3 rounded-lg border py-3\">\n      <div className=\"aui-tool-fallback-header flex items-center gap-2 px-4\">\n        <CheckIcon className=\"aui-tool-fallback-icon size-4\" />\n        <p className=\"aui-tool-fallback-title flex-grow\">\n          Used tool: <b>{toolName}</b>\n        </p>\n        <Button onClick={() => setIsCollapsed(!isCollapsed)}>\n          {isCollapsed ? <ChevronUpIcon /> : <ChevronDownIcon />}\n        </Button>\n      </div>\n      {!isCollapsed && (\n        <div className=\"aui-tool-fallback-content flex flex-col gap-2 border-t pt-2\">\n          <div className=\"aui-tool-fallback-args-root px-4\">\n            <pre className=\"aui-tool-fallback-args-value whitespace-pre-wrap\">\n              {argsText}\n            </pre>\n          </div>\n          {result !== undefined && (\n            <div className=\"aui-tool-fallback-result-root border-t border-dashed px-4 pt-2\">\n              <p className=\"aui-tool-fallback-result-header font-semibold\">\n                Result:\n              </p>\n              <pre className=\"aui-tool-fallback-result-content whitespace-pre-wrap\">\n                {typeof result === \"string\"\n                  ? result\n                  : JSON.stringify(result, null, 2)}\n              </pre>\n            </div>\n          )}\n        </div>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/www/app/components/assistant-ui/tooltip-icon-button.tsx",
    "content": "\"use client\";\n\nimport { ComponentPropsWithRef, forwardRef } from \"react\";\nimport { Slottable } from \"@radix-ui/react-slot\";\n\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/ui/cn\";\n\nexport type TooltipIconButtonProps = ComponentPropsWithRef<typeof Button> & {\n  tooltip: string;\n  side?: \"top\" | \"bottom\" | \"left\" | \"right\";\n};\n\nexport const TooltipIconButton = forwardRef<\n  HTMLButtonElement,\n  TooltipIconButtonProps\n>(({ children, tooltip, side = \"bottom\", className, ...rest }, ref) => {\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>\n        <Button\n          variant=\"ghost\"\n          size=\"icon\"\n          {...rest}\n          className={cn(\"aui-button-icon size-6 p-1\", className)}\n          ref={ref}\n        >\n          <Slottable>{children}</Slottable>\n          <span className=\"aui-sr-only sr-only\">{tooltip}</span>\n        </Button>\n      </TooltipTrigger>\n      <TooltipContent side={side}>{tooltip}</TooltipContent>\n    </Tooltip>\n  );\n});\n\nTooltipIconButton.displayName = \"TooltipIconButton\";\n"
  },
  {
    "path": "apps/www/app/components/builder/mcp-icon.tsx",
    "content": "import * as React from \"react\";\n\nexport function MCPIcon({\n  className,\n  ...props\n}: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg\n      viewBox=\"0 0 195 195\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      className={className}\n      {...props}\n    >\n      <path\n        d=\"M25 97.8528L92.8823 29.9706C102.255 20.598 117.451 20.598 126.823 29.9706V29.9706C136.196 39.3431 136.196 54.5391 126.823 63.9117L75.5581 115.177\"\n        stroke=\"currentColor\"\n        strokeWidth=\"12\"\n        strokeLinecap=\"round\"\n      />\n      <path\n        d=\"M76.2653 114.47L126.823 63.9117C136.196 54.5391 151.392 54.5391 160.765 63.9117L161.118 64.2652C170.491 73.6378 170.491 88.8338 161.118 98.2063L99.7248 159.6C96.6006 162.724 96.6006 167.789 99.7248 170.913L112.331 183.52\"\n        stroke=\"currentColor\"\n        strokeWidth=\"12\"\n        strokeLinecap=\"round\"\n      />\n      <path\n        d=\"M109.853 46.9411L59.6482 97.1457C50.2757 106.518 50.2757 121.714 59.6482 131.087V131.087C69.0208 140.459 84.2168 140.459 93.5894 131.087L143.794 80.8822\"\n        stroke=\"currentColor\"\n        strokeWidth=\"12\"\n        strokeLinecap=\"round\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/components/builder/theme-toggle.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Moon, Sun } from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/ui/cn\";\n\nexport function ThemeToggle() {\n  const { setTheme, resolvedTheme } = useTheme();\n  const [mounted, setMounted] = React.useState(false);\n\n  function changeTheme(newTheme: string) {\n    if (!document.startViewTransition) {\n      setTheme(newTheme);\n      return;\n    }\n\n    document.documentElement.dataset.themeTransition = \"\";\n    const transition = document.startViewTransition(() => {\n      setTheme(newTheme);\n    });\n    transition.finished.then(() => {\n      delete document.documentElement.dataset.themeTransition;\n    });\n  }\n\n  React.useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  const isDark = mounted && resolvedTheme === \"dark\";\n\n  const toggleTheme = () => {\n    changeTheme(isDark ? \"light\" : \"dark\");\n  };\n\n  return (\n    <Button\n      variant=\"ghost\"\n      size=\"icon\"\n      type=\"button\"\n      aria-label=\"Toggle theme\"\n      aria-pressed={isDark}\n      className=\"relative\"\n      onClick={toggleTheme}\n    >\n      <Sun\n        className={cn(\n          \"size-4 transition-all\",\n          isDark ? \"scale-0 rotate-90\" : \"scale-100 rotate-0\",\n        )}\n      />\n      <Moon\n        className={cn(\n          \"absolute size-4 transition-all\",\n          isDark ? \"scale-100 rotate-0\" : \"scale-0 -rotate-90\",\n        )}\n      />\n      <span className=\"sr-only\">Toggle theme</span>\n    </Button>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/components/builder/webview-actions.ts",
    "content": "\"use server\";\n\nimport { requestDevServer as requestDevServerInner } from \"@/lib/integrations/freestyle/create-chat\";\n\nexport async function requestDevServer({ repoId }: { repoId: string }) {\n  return await requestDevServerInner({ repoId });\n}\n"
  },
  {
    "path": "apps/www/app/components/builder/webview.tsx",
    "content": "\"use client\";\n\nimport { requestDevServer as requestDevServerInner } from \"./webview-actions\";\nimport \"@/app/styles/builder-loader.css\";\nimport {\n  forwardRef,\n  useCallback,\n  useEffect,\n  useImperativeHandle,\n  useMemo,\n  useState,\n} from \"react\";\n\nexport interface WebViewHandle {\n  refresh: () => void;\n}\n\ntype DevServerResponse = Awaited<ReturnType<typeof requestDevServerInner>>;\n\nexport default forwardRef<\n  WebViewHandle,\n  {\n    repo: string;\n  }\n>(function WebView(props, ref) {\n  const [devServer, setDevServer] = useState<DevServerResponse | null>(null);\n  const [requesting, setRequesting] = useState(false);\n  const [iframeLoading, setIframeLoading] = useState(true);\n  const [error, setError] = useState<string | null>(null);\n\n  const devServerUrl = useMemo(\n    () => devServer?.ephemeralUrl ?? null,\n    [devServer],\n  );\n\n  const requestDevServer = useCallback(async () => {\n    setRequesting(true);\n    setIframeLoading(true);\n    setError(null);\n    try {\n      const response = await requestDevServerInner({ repoId: props.repo });\n      setDevServer(response);\n    } catch (err) {\n      const message = err instanceof Error ? err.message : String(err);\n      setError(message);\n      setDevServer(null);\n      setIframeLoading(false);\n    } finally {\n      setRequesting(false);\n    }\n  }, [props.repo]);\n\n  useImperativeHandle(ref, () => ({\n    refresh: () => {\n      void requestDevServer();\n    },\n  }));\n\n  useEffect(() => {\n    if (!props.repo) {\n      setDevServer(null);\n      setError(\"Repo ID is missing.\");\n      return;\n    }\n    void requestDevServer();\n  }, [props.repo, requestDevServer]);\n\n  return (\n    <div className=\"flex h-full flex-col overflow-hidden border-l transition-opacity duration-700\">\n      {(requesting || iframeLoading) && !devServer?.devCommandRunning && (\n        <div className=\"flex h-full items-center justify-center\">\n          <div>\n            <div className=\"text-center\">\n              {iframeLoading ? \"JavaScript Loading\" : \"Starting VM\"}\n            </div>\n            <div>\n              <div className=\"loader\"></div>\n            </div>\n          </div>\n        </div>\n      )}\n      {error ? (\n        <div className=\"flex h-full items-center justify-center text-sm text-muted-foreground\">\n          {error}\n        </div>\n      ) : devServerUrl ? (\n        <iframe\n          key={devServerUrl}\n          className=\"h-full w-full border-0\"\n          src={devServerUrl}\n          onLoad={() => setIframeLoading(false)}\n        />\n      ) : null}\n    </div>\n  );\n});\n"
  },
  {
    "path": "apps/www/app/components/home/chat-showcase.tsx",
    "content": "\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { AnimatePresence, motion, type Transition } from \"motion/react\";\nimport { cn } from \"@/lib/ui/cn\";\nimport { CitationList } from \"@/components/tool-ui/citation\";\nimport { DataTable } from \"@/components/tool-ui/data-table\";\nimport { LinkPreview } from \"@/components/tool-ui/link-preview\";\nimport { Plan } from \"@/components/tool-ui/plan\";\nimport { Terminal } from \"@/components/tool-ui/terminal\";\nimport { CodeBlock } from \"@/components/tool-ui/code-block\";\nimport { ItemCarousel } from \"@/components/tool-ui/item-carousel\";\nimport { ParameterSlider } from \"@/components/tool-ui/parameter-slider\";\nimport { StatsDisplay } from \"@/components/tool-ui/stats-display\";\nimport { ProgressTracker } from \"@/components/tool-ui/progress-tracker\";\nimport { MessageDraft } from \"@/components/tool-ui/message-draft\";\nimport { WeatherWidget } from \"@/components/tool-ui/weather-widget/runtime\";\nimport {\n  type Flight,\n  TABLE_COLUMNS,\n  TABLE_DATA,\n  LINK_PREVIEW,\n  PLAN_TODO_LABELS,\n  ITEM_CAROUSEL_DATA,\n  LLM_CITATIONS,\n  PARAMETER_SLIDER_DATA,\n  STATS_DISPLAY_DATA,\n  PROGRESS_TRACKER_DATA,\n} from \"@/lib/mocks/chat-showcase-data\";\n\nconst TIMING = {\n  durations: {\n    userIn: 500,\n    preambleIn: 280,\n    toolIn: 600,\n  },\n  beats: {\n    afterUser: 700,\n    beforeContent: 500,\n    afterPreamble: 200,\n  },\n  sceneHold: 4500,\n  exitStagger: {\n    user: 0,\n    preamble: 80,\n    tool: 160,\n  },\n  reducedMotion: {\n    duration: 250,\n    sceneHold: 1500,\n  },\n} as const;\n\nconst SPRINGS = {\n  gentle: {\n    type: \"spring\",\n    damping: 28,\n    stiffness: 180,\n    mass: 0.8,\n  },\n  smooth: {\n    type: \"spring\",\n    damping: 24,\n    stiffness: 260,\n    mass: 0.8,\n  },\n  standard: {\n    type: \"spring\",\n    damping: 26,\n    stiffness: 220,\n    mass: 0.7,\n  },\n} as const satisfies Record<string, Transition>;\n\ntype SceneConfig = {\n  userMessage?: string;\n  preamble?: string;\n  toolUI: React.ReactNode;\n  toolFallbackHeight?: number;\n  holdDuration?: number;\n};\n\ntype SceneTimelineState = {\n  preambleReady: boolean;\n  showTool: boolean;\n  setShowTool: (value: boolean) => void;\n};\n\nfunction useReducedMotion(): boolean {\n  const [reducedMotion, setReducedMotion] = useState(false);\n\n  useEffect(() => {\n    if (typeof window === \"undefined\") return;\n\n    const mediaQuery = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    setReducedMotion(mediaQuery.matches);\n\n    const handleChange = () => setReducedMotion(mediaQuery.matches);\n    mediaQuery.addEventListener?.(\"change\", handleChange);\n    return () => mediaQuery.removeEventListener?.(\"change\", handleChange);\n  }, []);\n\n  return reducedMotion;\n}\n\nfunction useSceneTimeline({\n  reducedMotion,\n  onComplete,\n  hasUserMessage = true,\n  initialDelay = 0,\n  holdDuration,\n}: {\n  reducedMotion: boolean;\n  onComplete: () => void;\n  hasUserMessage?: boolean;\n  initialDelay?: number;\n  holdDuration?: number;\n}): SceneTimelineState {\n  const [preambleReady, setPreambleReady] = useState(reducedMotion);\n  const [showTool, setShowTool] = useState(reducedMotion);\n  const hasScheduledCompletion = useRef(false);\n\n  useEffect(() => {\n    if (!reducedMotion) return;\n\n    const timeoutId = window.setTimeout(\n      onComplete,\n      TIMING.reducedMotion.sceneHold,\n    );\n    return () => window.clearTimeout(timeoutId);\n  }, [reducedMotion, onComplete]);\n\n  useEffect(() => {\n    if (reducedMotion || preambleReady) return;\n\n    const delay =\n      initialDelay +\n      (hasUserMessage ? TIMING.durations.userIn + TIMING.beats.afterUser : 0);\n\n    const timeoutId = window.setTimeout(() => setPreambleReady(true), delay);\n    return () => window.clearTimeout(timeoutId);\n  }, [preambleReady, reducedMotion, hasUserMessage, initialDelay]);\n\n  useEffect(() => {\n    const shouldScheduleCompletion =\n      !hasScheduledCompletion.current &&\n      preambleReady &&\n      showTool &&\n      !reducedMotion;\n\n    if (!shouldScheduleCompletion) return;\n\n    hasScheduledCompletion.current = true;\n    const timeoutId = window.setTimeout(\n      onComplete,\n      holdDuration ?? TIMING.sceneHold,\n    );\n    return () => window.clearTimeout(timeoutId);\n  }, [preambleReady, showTool, onComplete, reducedMotion, holdDuration]);\n\n  return useMemo(\n    () => ({ preambleReady, showTool, setShowTool }),\n    [preambleReady, showTool],\n  );\n}\n\nfunction ToolReveal({ children }: { children: React.ReactNode }) {\n  return (\n    <motion.div\n      initial={{ opacity: 0, y: 30, filter: \"blur(10px)\" }}\n      animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n      transition={SPRINGS.gentle}\n    >\n      {children}\n    </motion.div>\n  );\n}\n\nfunction TypingIndicator() {\n  return (\n    <motion.span\n      className=\"bg-foreground/50 block size-4 rounded-full\"\n      animate={{ opacity: [0.4, 1, 0.4] }}\n      transition={{\n        duration: 1.4,\n        repeat: Infinity,\n        ease: \"easeInOut\",\n      }}\n      aria-label=\"Assistant is typing\"\n    />\n  );\n}\n\nconst TEST_LINES = [\n  \"\\x1b[32m✓\\x1b[0m login flow handles invalid credentials \\x1b[90m(3 tests)\\x1b[0m \\x1b[33m42ms\\x1b[0m\",\n  \"\\x1b[32m✓\\x1b[0m session tokens refresh correctly \\x1b[90m(5 tests)\\x1b[0m \\x1b[33m128ms\\x1b[0m\",\n  \"\\x1b[32m✓\\x1b[0m logout clears all cookies \\x1b[90m(2 tests)\\x1b[0m \\x1b[33m18ms\\x1b[0m\",\n  \"\",\n  \"\\x1b[32mTests:\\x1b[0m 10 passed, 10 total\",\n];\n\nfunction AnimatedTerminal({ className }: { className?: string }) {\n  const [lineCount, setLineCount] = useState(0);\n\n  useEffect(() => {\n    if (lineCount >= TEST_LINES.length) return;\n\n    const delay = lineCount === 0 ? 300 : 700;\n    const timeoutId = window.setTimeout(() => {\n      setLineCount((c) => c + 1);\n    }, delay);\n\n    return () => window.clearTimeout(timeoutId);\n  }, [lineCount]);\n\n  const visibleOutput = TEST_LINES.slice(0, lineCount).join(\"\\n\") || \" \";\n  const isComplete = lineCount >= TEST_LINES.length;\n\n  return (\n    <Terminal\n      id=\"chat-showcase-terminal\"\n      command=\"pnpm test auth\"\n      stdout={visibleOutput}\n      exitCode={0}\n      durationMs={isComplete ? 1243 : undefined}\n      className={className}\n    />\n  );\n}\n\nfunction AnimatedPlan({ className }: { className?: string }) {\n  const [completedCount, setCompletedCount] = useState(0);\n\n  useEffect(() => {\n    if (completedCount >= PLAN_TODO_LABELS.length) return;\n\n    const delay = completedCount === 0 ? 400 : 1100;\n    const timeoutId = window.setTimeout(() => {\n      setCompletedCount((c) => c + 1);\n    }, delay);\n\n    return () => window.clearTimeout(timeoutId);\n  }, [completedCount]);\n\n  const todoDescriptions = [\n    \"Analyzing social media activity and recent conversations\",\n    \"Browsing gift guides and personalized recommendations\",\n    \"Evaluating quality, reviews, and price ranges\",\n    \"Selecting the top 3 options with purchase links\",\n  ];\n\n  const todos = PLAN_TODO_LABELS.map((label: string, index: number) => ({\n    id: String(index + 1),\n    label,\n    description: todoDescriptions[index],\n    status:\n      index < completedCount\n        ? (\"completed\" as const)\n        : index === completedCount\n          ? (\"in_progress\" as const)\n          : (\"pending\" as const),\n  }));\n\n  return (\n    <Plan\n      id=\"chat-showcase-plan\"\n      title=\"Gift Research\"\n      description=\"Finding the perfect birthday gift for Sarah\"\n      todos={todos}\n      className={className}\n    />\n  );\n}\n\nfunction AnimatedProgressTracker({ className }: { className?: string }) {\n  const [currentStep, setCurrentStep] = useState(1);\n\n  useEffect(() => {\n    if (currentStep >= 3) return;\n\n    const delay = currentStep === 1 ? 1300 : 1500;\n    const timeoutId = window.setTimeout(() => {\n      setCurrentStep((s) => s + 1);\n    }, delay);\n\n    return () => window.clearTimeout(timeoutId);\n  }, [currentStep]);\n\n  const steps = PROGRESS_TRACKER_DATA.steps.map((step, index) => ({\n    ...step,\n    status:\n      index < currentStep\n        ? (\"completed\" as const)\n        : index === currentStep\n          ? (\"in-progress\" as const)\n          : (\"pending\" as const),\n  }));\n\n  const elapsedTime = PROGRESS_TRACKER_DATA.elapsedTime! + currentStep * 12000;\n\n  return (\n    <ProgressTracker\n      id=\"chat-showcase-progress-tracker\"\n      steps={steps}\n      elapsedTime={elapsedTime}\n      className={className}\n    />\n  );\n}\n\ntype ChatBubbleProps = {\n  role: \"user\" | \"assistant\";\n  children: React.ReactNode;\n  className?: string;\n};\n\nfunction ChatBubble({ role, children, className }: ChatBubbleProps) {\n  const isUser = role === \"user\";\n\n  return (\n    <div\n      className={cn(\n        \"flex w-full\",\n        isUser ? \"justify-end pb-3\" : \"justify-start\",\n      )}\n      aria-label={isUser ? \"User message\" : \"Assistant message\"}\n    >\n      <div\n        className={cn(\n          \"relative max-w-[min(720px,100%)] text-xl\",\n          isUser && \"rounded-full bg-[#007AFF] text-white dark:bg-[#002b90]\",\n          !isUser && \"text-foreground\",\n          className,\n        )}\n      >\n        {children}\n      </div>\n    </div>\n  );\n}\n\ntype PreambleBubbleProps = {\n  text: string;\n  msPerChar?: number;\n  reducedMotion?: boolean;\n  onComplete?: () => void;\n};\n\nfunction StreamingChar({ char, delay }: { char: string; delay: number }) {\n  return (\n    <motion.span\n      initial={{ opacity: 0 }}\n      animate={{ opacity: 1 }}\n      transition={{\n        duration: 0.4,\n        delay,\n        ease: [0.25, 0.1, 0.25, 1],\n      }}\n    >\n      {char}\n    </motion.span>\n  );\n}\n\nfunction PreambleBubble({\n  text,\n  msPerChar = 18,\n  reducedMotion,\n  onComplete,\n}: PreambleBubbleProps) {\n  const [isVisible, setIsVisible] = useState(reducedMotion);\n  const hasCalledComplete = useRef(false);\n\n  useEffect(() => {\n    if (reducedMotion) return;\n\n    const timeoutId = window.setTimeout(() => setIsVisible(true), 100);\n    return () => window.clearTimeout(timeoutId);\n  }, [reducedMotion]);\n\n  useEffect(() => {\n    if (hasCalledComplete.current) return;\n\n    if (reducedMotion) {\n      hasCalledComplete.current = true;\n      onComplete?.();\n      return;\n    }\n\n    if (!isVisible) return;\n\n    const totalDuration = text.length * msPerChar + 400;\n    const timeoutId = window.setTimeout(() => {\n      if (!hasCalledComplete.current) {\n        hasCalledComplete.current = true;\n        onComplete?.();\n      }\n    }, totalDuration);\n\n    return () => window.clearTimeout(timeoutId);\n  }, [reducedMotion, msPerChar, text.length, onComplete, isVisible]);\n\n  const characters = useMemo(() => {\n    return text.split(\"\").map((char, index) => ({\n      char,\n      delay: index * (msPerChar / 1000),\n    }));\n  }, [text, msPerChar]);\n\n  if (reducedMotion) {\n    return (\n      <ChatBubble role=\"assistant\">\n        <span>{text}</span>\n      </ChatBubble>\n    );\n  }\n\n  return (\n    <motion.div\n      initial={{ opacity: 0 }}\n      animate={{ opacity: isVisible ? 1 : 0 }}\n      transition={SPRINGS.smooth}\n    >\n      <ChatBubble role=\"assistant\">\n        <span>\n          {isVisible &&\n            characters.map(({ char, delay }, index) => (\n              <StreamingChar key={index} char={char} delay={delay} />\n            ))}\n        </span>\n      </ChatBubble>\n    </motion.div>\n  );\n}\n\nfunction createSceneConfigs(reducedMotion: boolean): SceneConfig[] {\n  const { title: _title, ...statsDataWithoutTitle } = STATS_DISPLAY_DATA;\n\n  return [\n    {\n      userMessage: \"How's the business doing this quarter?\",\n      preamble: \"Q4 numbers are in. Looking solid.\",\n      toolUI: (\n        <StatsDisplay\n          id=\"chat-showcase-stats-display\"\n          {...statsDataWithoutTitle}\n          className=\"w-full max-w-[560px]\"\n        />\n      ),\n      toolFallbackHeight: 280,\n    },\n    {\n      userMessage: \"What's the weather like this week?\",\n      preamble: \"Stormy night ahead. Clears up by Thursday.\",\n      toolUI: (\n        <WeatherWidget\n          version=\"3.1\"\n          id=\"chat-showcase-weather\"\n          location={{ name: \"San Francisco, CA\" }}\n          units={{ temperature: \"fahrenheit\" }}\n          current={{\n            temperature: 54,\n            tempMin: 51,\n            tempMax: 58,\n            conditionCode: \"thunderstorm\",\n          }}\n          forecast={[\n            {\n              label: \"Tue\",\n              tempMin: 50,\n              tempMax: 56,\n              conditionCode: \"heavy-rain\",\n            },\n            { label: \"Wed\", tempMin: 49, tempMax: 55, conditionCode: \"rain\" },\n            { label: \"Thu\", tempMin: 51, tempMax: 60, conditionCode: \"cloudy\" },\n            {\n              label: \"Fri\",\n              tempMin: 53,\n              tempMax: 64,\n              conditionCode: \"partly-cloudy\",\n            },\n            { label: \"Sat\", tempMin: 55, tempMax: 68, conditionCode: \"clear\" },\n          ]}\n          time={{ localTimeOfDay: 23 / 24 }}\n          updatedAt=\"2024-01-15T23:00:00Z\"\n          className=\"w-full max-w-[400px]\"\n          effects={{\n            enabled: !reducedMotion,\n            reducedMotion,\n            quality: \"auto\",\n          }}\n        />\n      ),\n      toolFallbackHeight: 280,\n      holdDuration: 6000,\n    },\n    {\n      userMessage: \"Boost the bass a bit on this track\",\n      preamble: \"Bass is up. Here's the full EQ.\",\n      toolUI: (\n        <ParameterSlider\n          id=\"chat-showcase-parameter-slider\"\n          sliders={[\n            {\n              id: \"bass\",\n              label: \"Bass\",\n              min: -12,\n              max: 12,\n              step: 1,\n              value: 4,\n              unit: \"dB\",\n              fillClassName: \"bg-fuchsia-500/30 dark:bg-fuchsia-400/35\",\n              handleClassName: \"bg-fuchsia-500 dark:bg-fuchsia-400\",\n            },\n            {\n              id: \"mid\",\n              label: \"Mid\",\n              min: -12,\n              max: 12,\n              step: 1,\n              value: -1,\n              unit: \"dB\",\n              fillClassName: \"bg-cyan-500/30 dark:bg-cyan-400/35\",\n              handleClassName: \"bg-cyan-500 dark:bg-cyan-400\",\n            },\n            {\n              id: \"treble\",\n              label: \"Treble\",\n              min: -12,\n              max: 12,\n              step: 1,\n              value: 3,\n              unit: \"dB\",\n              fillClassName: \"bg-violet-500/30 dark:bg-violet-400/35\",\n              handleClassName: \"bg-violet-500 dark:bg-violet-400\",\n            },\n          ]}\n          actions={PARAMETER_SLIDER_DATA.actions}\n          className=\"w-full max-w-[480px]\"\n        />\n      ),\n      toolFallbackHeight: 240,\n    },\n    {\n      userMessage: \"Find me a birthday gift for Sarah\",\n      preamble: \"On it. Checking her interests now.\",\n      toolUI: <AnimatedPlan className=\"w-full max-w-[480px]\" />,\n      toolFallbackHeight: 280,\n    },\n    {\n      userMessage: \"What should I listen to right now?\",\n      preamble: \"Found a few albums for you.\",\n      toolUI: (\n        <ItemCarousel\n          id=\"chat-showcase-item-carousel\"\n          {...ITEM_CAROUSEL_DATA}\n          className=\"w-full max-w-[640px]\"\n        />\n      ),\n      toolFallbackHeight: 320,\n    },\n    {\n      userMessage: \"Run the tests for the auth module\",\n      preamble: \"Running auth tests now.\",\n      toolUI: <AnimatedTerminal className=\"w-full max-w-[560px]\" />,\n      toolFallbackHeight: 200,\n    },\n    {\n      userMessage: \"Deploy the updates to production\",\n      preamble: \"Deployment started. Tracking progress.\",\n      toolUI: <AnimatedProgressTracker className=\"w-full max-w-[480px]\" />,\n      toolFallbackHeight: 260,\n    },\n    {\n      userMessage: \"Find me flights to Tokyo in March\",\n      preamble: \"Found 4 nonstop flights. Sorted by price.\",\n      toolUI: (\n        <DataTable.Table<Flight>\n          id=\"chat-showcase-data-table\"\n          rowIdKey=\"id\"\n          columns={TABLE_COLUMNS}\n          data={TABLE_DATA}\n          defaultSort={{ by: \"price\", direction: \"asc\" }}\n        />\n      ),\n      toolFallbackHeight: 320,\n    },\n    {\n      userMessage: \"Create a skill that learns from mistakes\",\n      preamble: \"Here's a self-improving metaskill.\",\n      toolUI: (\n        <CodeBlock\n          id=\"chat-showcase-code-block\"\n          language=\"markdown\"\n          lineNumbers=\"visible\"\n          filename=\"learn-from-errors.md\"\n          code={`name: learn-from-errors\ntriggers: [error, failed, mistake, wrong]\n\n## Behavior\n\nWhen an error occurs:\n1. Capture the context and attempted solution\n2. Analyze what went wrong\n3. Update \\`~/.claude/learnings.md\\` with the pattern\n4. Apply the learning to retry\n\n## Self-Improvement Loop\n\n\\`\\`\\`\nerror → analyze → document → retry → verify\n          ↑                            ↓\n          └──────── if failed ─────────┘\n\\`\\`\\``}\n          className=\"w-full\"\n        />\n      ),\n      toolFallbackHeight: 260,\n    },\n    {\n      userMessage: \"Find that physics article from Quanta\",\n      preamble: \"Was it this one?\",\n      toolUI: <LinkPreview {...LINK_PREVIEW} />,\n      toolFallbackHeight: 260,\n    },\n    {\n      userMessage: \"Send Marcus the updated proposal\",\n      preamble: \"Drafted this for you. Review before sending.\",\n      toolUI: (\n        <MessageDraft\n          id=\"chat-showcase-message-draft\"\n          channel=\"email\"\n          subject=\"Updated proposal attached\"\n          to={[\"marcus.chen@acme.co\"]}\n          body={`Hi Marcus,\n\nI've attached the revised proposal with the changes we discussed. The new timeline reflects the Q2 launch date, and I've adjusted the budget breakdown in section 3.\n\nLet me know if you have any questions.\n\nBest,\nSarah`}\n          className=\"w-full max-w-[480px]\"\n        />\n      ),\n      toolFallbackHeight: 340,\n    },\n    {\n      userMessage: \"What was the first LLM?\",\n      preamble: \"GPT-1 from OpenAI in 2018. Here are the key sources.\",\n      toolUI: (\n        <CitationList\n          id=\"showcase-citations\"\n          citations={LLM_CITATIONS}\n          variant=\"stacked\"\n          className=\"w-full max-w-[480px]\"\n        />\n      ),\n      toolFallbackHeight: 56,\n    },\n  ];\n}\n\nconst SCENE_COUNT = 11;\n\ntype AnimatedSceneProps = {\n  config: SceneConfig;\n  reducedMotion: boolean;\n  onComplete: () => void;\n  sceneId: string;\n  isExiting?: boolean;\n  composerPadding?: boolean;\n  initialDelay?: number;\n};\n\nfunction AnimatedScene({\n  config,\n  reducedMotion,\n  onComplete,\n  sceneId,\n  isExiting = false,\n  initialDelay = 0,\n}: AnimatedSceneProps) {\n  const timeline = useSceneTimeline({\n    reducedMotion,\n    onComplete,\n    hasUserMessage: !!config.userMessage,\n    initialDelay,\n    holdDuration: config.holdDuration,\n  });\n\n  const handlePreambleComplete = useCallback(() => {\n    timeline.setShowTool(true);\n  }, [timeline]);\n\n  useEffect(() => {\n    const shouldShowTool = timeline.preambleReady && !timeline.showTool;\n\n    if (shouldShowTool) {\n      timeline.setShowTool(true);\n    }\n  }, [timeline]);\n\n  const shouldRenderItems = !isExiting;\n  const shouldShowToolContent = config.preamble ? timeline.showTool : true;\n\n  const createEntryTransition = (delayMs: number) => ({\n    ...SPRINGS.standard,\n    delay: delayMs / 1000,\n  });\n\n  const createExitTransition = (delayMs: number) => ({\n    ...SPRINGS.standard,\n    delay: delayMs / 1000,\n  });\n\n  return (\n    <div className=\"flex flex-col pb-20\">\n      <AnimatePresence>\n        {shouldRenderItems && config.userMessage && (\n          <motion.div\n            key={`${sceneId}-user`}\n            initial={{ opacity: 0, y: 16 }}\n            animate={{\n              opacity: 1,\n              y: 0,\n              transition: createEntryTransition(initialDelay + 80),\n            }}\n            exit={{\n              opacity: 0,\n              y: -8,\n              transition: createExitTransition(TIMING.exitStagger.user),\n            }}\n            className=\"mb-11\"\n          >\n            <ChatBubble role=\"user\" className=\"px-6 py-3\">\n              {config.userMessage}\n            </ChatBubble>\n          </motion.div>\n        )}\n\n        {shouldRenderItems && config.preamble && (\n          <motion.div\n            key={`${sceneId}-preamble-area`}\n            className=\"relative mb-3\"\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{\n              opacity: 0,\n              y: -8,\n              transition: createExitTransition(TIMING.exitStagger.preamble),\n            }}\n          >\n            <AnimatePresence>\n              {config.userMessage &&\n                !timeline.preambleReady &&\n                !reducedMotion && (\n                  <motion.div\n                    key={`${sceneId}-indicator`}\n                    className=\"absolute top-1.5 left-0\"\n                    initial={{ opacity: 0, scale: 0.5, filter: \"blur(4px)\" }}\n                    animate={{\n                      opacity: 1,\n                      scale: 1,\n                      filter: \"blur(0px)\",\n                      transition: {\n                        ...SPRINGS.smooth,\n                        delay: TIMING.durations.userIn / 1000,\n                      },\n                    }}\n                    exit={{\n                      opacity: 0,\n                      x: 20,\n                      filter: \"blur(4px)\",\n                      transition: { duration: 0.2, ease: \"easeOut\" },\n                    }}\n                  >\n                    <TypingIndicator />\n                  </motion.div>\n                )}\n            </AnimatePresence>\n\n            {timeline.preambleReady && (\n              <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>\n                <PreambleBubble\n                  text={config.preamble}\n                  reducedMotion={reducedMotion}\n                  onComplete={handlePreambleComplete}\n                />\n              </motion.div>\n            )}\n          </motion.div>\n        )}\n\n        {shouldRenderItems &&\n          timeline.preambleReady &&\n          shouldShowToolContent && (\n            <motion.div\n              key={`${sceneId}-tool`}\n              initial={{ opacity: 0, y: 16 }}\n              animate={{\n                opacity: 1,\n                y: 0,\n                transition: createEntryTransition(\n                  (config.preamble\n                    ? TIMING.beats.afterPreamble\n                    : TIMING.beats.beforeContent) + 300,\n                ),\n              }}\n              exit={{\n                opacity: 0,\n                y: -8,\n                transition: createExitTransition(TIMING.exitStagger.tool),\n              }}\n              className={config.userMessage ? \"\" : \"mb-11\"}\n            >\n              <div className=\"flex w-full justify-start\">\n                <div className=\"w-full max-w-[720px] *:**:data-[slot=table]:min-w-0\">\n                  <ToolReveal>{config.toolUI}</ToolReveal>\n                </div>\n              </div>\n            </motion.div>\n          )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nexport function ChatShowcase() {\n  const reducedMotion = useReducedMotion();\n  const sceneConfigs = useMemo(\n    () => createSceneConfigs(reducedMotion),\n    [reducedMotion],\n  );\n\n  const [sceneIndex, setSceneIndex] = useState(0);\n  const [sceneRunId, setSceneRunId] = useState(0);\n  const [isExiting, setIsExiting] = useState(false);\n\n  const exitDuration = reducedMotion\n    ? TIMING.reducedMotion.duration\n    : TIMING.exitStagger.tool + 500;\n\n  const transitionToScene = useCallback(\n    (getNextIndex: (current: number) => number) => {\n      setIsExiting(true);\n      setTimeout(() => {\n        setIsExiting(false);\n        setSceneIndex(getNextIndex);\n        setSceneRunId((id) => id + 1);\n      }, exitDuration);\n    },\n    [exitDuration],\n  );\n\n  const advanceToNextScene = useCallback(() => {\n    transitionToScene((current) => (current + 1) % SCENE_COUNT);\n  }, [transitionToScene]);\n\n  const goToPreviousScene = useCallback(() => {\n    transitionToScene((current) => (current - 1 + SCENE_COUNT) % SCENE_COUNT);\n  }, [transitionToScene]);\n\n  useEffect(() => {\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === \"ArrowRight\") {\n        advanceToNextScene();\n      } else if (e.key === \"ArrowLeft\") {\n        goToPreviousScene();\n      }\n    };\n\n    window.addEventListener(\"keydown\", handleKeyDown);\n    return () => window.removeEventListener(\"keydown\", handleKeyDown);\n  }, [advanceToNextScene, goToPreviousScene]);\n\n  return (\n    <div className=\"relative flex h-full w-full flex-col\">\n      <div\n        className=\"pointer-events-none absolute inset-0 opacity-60 dark:opacity-40\"\n        aria-hidden=\"true\"\n      />\n      <div className=\"relative z-10 h-full min-h-0 w-full\">\n        <motion.div\n          key={`scene-${sceneIndex}-${sceneRunId}`}\n          className=\"absolute inset-0\"\n          initial={{ opacity: 0 }}\n          animate={{ opacity: 1 }}\n          transition={SPRINGS.smooth}\n        >\n          <AnimatedScene\n            config={sceneConfigs[sceneIndex]}\n            reducedMotion={reducedMotion}\n            onComplete={advanceToNextScene}\n            sceneId={`scene-${sceneIndex}`}\n            isExiting={isExiting}\n            initialDelay={sceneRunId === 0 ? 2500 : 0}\n          />\n        </motion.div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/components/home/demo-chat.tsx",
    "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport {\n  AssistantRuntimeProvider,\n  ThreadPrimitive,\n  Tools,\n  useAui,\n} from \"@assistant-ui/react\";\nimport {\n  AssistantChatTransport,\n  useChatRuntime,\n} from \"@assistant-ui/react-ai-sdk\";\nimport { lastAssistantMessageIsCompleteWithToolCalls } from \"ai\";\nimport { DEMO_CHAT_TOOLKIT } from \"@/lib/demo-chat/toolkit\";\nimport {\n  AssistantMessage,\n  Composer,\n  UserMessage,\n} from \"@/app/playground/chat-ui\";\n\n// Prompts designed to trigger specific tools: get_tasks, show_plan, show_stats, show_terminal\nconst SUGGESTIONS = [\n  { text: \"Show me the support tickets\", tool: \"get_tasks\" },\n  { text: \"Create a deployment plan\", tool: \"show_plan\" },\n  { text: \"How's Q4 looking?\", tool: \"show_stats\" },\n  { text: \"Run the auth tests\", tool: \"show_terminal\" },\n] as const;\n\nconst PASTEL_COLORS = [\n  \"bg-pink-100 text-pink-800 border-pink-200 hover:bg-pink-200/80 dark:bg-pink-950/40 dark:text-pink-200 dark:border-pink-800/50 dark:hover:bg-pink-900/30\",\n  \"bg-violet-100 text-violet-800 border-violet-200 hover:bg-violet-200/80 dark:bg-violet-950/40 dark:text-violet-200 dark:border-violet-800/50 dark:hover:bg-violet-900/30\",\n  \"bg-sky-100 text-sky-800 border-sky-200 hover:bg-sky-200/80 dark:bg-sky-950/40 dark:text-sky-200 dark:border-sky-800/50 dark:hover:bg-sky-900/30\",\n  \"bg-emerald-100 text-emerald-800 border-emerald-200 hover:bg-emerald-200/80 dark:bg-emerald-950/40 dark:text-emerald-200 dark:border-emerald-800/50 dark:hover:bg-emerald-900/30\",\n] as const;\n\nfunction SuggestionChip({\n  children,\n  onClick,\n  colorClass,\n}: {\n  children: string;\n  onClick: () => void;\n  colorClass: string;\n}) {\n  return (\n    <button\n      type=\"button\"\n      onClick={onClick}\n      className={`cursor-pointer rounded-2xl border px-6 py-4 text-left text-base font-medium transition-colors ${colorClass}`}\n    >\n      {children}\n    </button>\n  );\n}\n\nfunction SuggestionChips() {\n  const aui = useAui();\n  return (\n    <div className=\"flex flex-wrap justify-center gap-3\">\n      {SUGGESTIONS.map(({ text, tool }, i) => (\n        <SuggestionChip\n          key={tool}\n          colorClass={PASTEL_COLORS[i]}\n          onClick={() => {\n            const composer = aui.composer();\n            composer.setText(text);\n            composer.send();\n          }}\n        >\n          {text}\n        </SuggestionChip>\n      ))}\n    </div>\n  );\n}\n\nexport function DemoChat() {\n  const transport = useMemo(\n    () =>\n      new AssistantChatTransport({\n        api: \"/api/chat\",\n      }),\n    [],\n  );\n\n  const runtime = useChatRuntime({\n    transport,\n    sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,\n  });\n\n  const aui = useAui({\n    tools: Tools({ toolkit: DEMO_CHAT_TOOLKIT }),\n  });\n\n  return (\n    <AssistantRuntimeProvider runtime={runtime} aui={aui}>\n      <ThreadPrimitive.Root className=\"flex h-full flex-col overflow-hidden\">\n        <ThreadPrimitive.Viewport className=\"scrollbar-subtle bg-muted/30 flex flex-1 flex-col overflow-y-auto px-6 pt-20 pb-6\">\n          <ThreadPrimitive.If empty>\n            <div className=\"text-muted-foreground mx-auto flex max-w-md flex-1 flex-col items-center justify-center gap-6 text-center\">\n              <p className=\"text-2xl font-semibold\">Try Tool UI</p>\n              <p className=\"text-sm\">\n                Ask to see a plan, support tickets, metrics, or run a command.\n                Each response uses a different Tool UI component.\n              </p>\n              <SuggestionChips />\n            </div>\n          </ThreadPrimitive.If>\n          <ThreadPrimitive.Messages\n            components={{\n              UserMessage,\n              AssistantMessage,\n            }}\n          />\n        </ThreadPrimitive.Viewport>\n        <div className=\"border-t px-4 pb-4 pt-3\">\n          <Composer />\n        </div>\n      </ThreadPrimitive.Root>\n    </AssistantRuntimeProvider>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/components/home/faux-chat-shell-animated.tsx",
    "content": "\"use client\";\n\nimport { motion } from \"motion/react\";\nimport { FauxChatShellNoLightOverlay } from \"./faux-chat-shell\";\nimport { generateNoiseDataUri } from \"./noise-texture\";\n\nfunction generateSineEasedGradient(\n  angle: number,\n  centerPosition: number,\n  peakOpacity: number,\n  spreadWidth: number,\n  steps: number = 128,\n): string {\n  const stops: string[] = [];\n\n  for (let i = 0; i <= steps; i++) {\n    const t = i / steps;\n    const position = centerPosition - spreadWidth / 2 + spreadWidth * t;\n\n    const sineValue = Math.sin(t * Math.PI);\n    const eased = sineValue * sineValue;\n    const opacity = peakOpacity * eased;\n\n    stops.push(\n      `rgba(255, 255, 255, ${opacity.toFixed(4)}) ${position.toFixed(1)}%`,\n    );\n  }\n\n  return `linear-gradient(${angle}deg, ${stops.join(\", \")})`;\n}\n\nexport function FauxChatShellAnimated() {\n  // Hardcoded animation values\n  const initial = {\n    opacity: 0,\n    rotateX: 20,\n    rotateY: 37,\n    rotateZ: -13,\n    x: 429,\n    y: -298,\n    scale: 2,\n  };\n\n  const resting = {\n    opacity: 1,\n    rotateX: 37,\n    rotateY: 26,\n    rotateZ: -18,\n    x: -35,\n    y: -60,\n    scale: 1.05,\n  };\n\n  // Perspective and transform origin\n  const perspective = 3700;\n  const perspectiveOriginX = 100;\n  const perspectiveOriginY = 29;\n  const originX = 70;\n  const originY = 64;\n  const translateZInitial = 96;\n  const translateZResting = 96;\n\n  // Lighting parameters\n  const lightAngle = 140;\n  const lightPosition = 82;\n  const lightSpread = 29;\n  const lightOpacity = 0.07;\n\n  // Calculate light position based on surface normal (simulates static directional light)\n  function calculateLightPosition(rotateX: number, rotateY: number) {\n    const baseY = lightPosition;\n    const highlightY = baseY - rotateX * 0.8 - rotateY * 0.3;\n    return highlightY;\n  }\n\n  // Calculate highlight position for resting state\n  const restingLightPos = calculateLightPosition(\n    resting.rotateX,\n    resting.rotateY,\n  );\n\n  // Generate gradient centered at calculated position\n  const restingLightGradient = generateSineEasedGradient(\n    lightAngle,\n    restingLightPos,\n    lightOpacity,\n    lightSpread,\n    32,\n  );\n\n  // Animation config\n  const transition = {\n    type: \"spring\" as const,\n    duration: 5,\n    bounce: 0.2,\n  };\n\n  return (\n    <div\n      className=\"relative h-full w-full\"\n      style={{\n        perspective: `${perspective}px`,\n        perspectiveOrigin: `${perspectiveOriginX}% ${perspectiveOriginY}%`,\n      }}\n    >\n      <motion.div\n        className=\"relative h-full w-full overflow-hidden rounded-2xl\"\n        initial={{ ...initial, translateZ: translateZInitial }}\n        animate={{ ...resting, translateZ: translateZResting }}\n        transition={transition}\n        style={{\n          transformStyle: \"preserve-3d\",\n          transformOrigin: `${originX}% ${originY}%`,\n        }}\n      >\n        <FauxChatShellNoLightOverlay />\n\n        {/* Lighting overlay - fades in from 0 to full opacity */}\n        <motion.div\n          className=\"pointer-events-none absolute inset-0 z-30\"\n          initial={{ opacity: 0 }}\n          animate={{ opacity: 1 }}\n          transition={transition}\n          style={{\n            backgroundImage: `url(\"${generateNoiseDataUri(64, 0.015)}\"), ${restingLightGradient}`,\n            backgroundBlendMode: \"overlay, normal\",\n          }}\n          aria-hidden=\"true\"\n        />\n      </motion.div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/components/home/faux-chat-shell-mobile-animated.tsx",
    "content": "\"use client\";\n\nimport { motion } from \"motion/react\";\nimport { FauxChatShellMobile } from \"./faux-chat-shell-mobile\";\nimport { generateNoiseDataUri } from \"./noise-texture\";\n\nfunction generateSineEasedGradient(\n  angle: number,\n  centerPosition: number,\n  peakOpacity: number,\n  spreadWidth: number,\n  steps: number = 128,\n): string {\n  const stops: string[] = [];\n\n  for (let i = 0; i <= steps; i++) {\n    const t = i / steps;\n    const position = centerPosition - spreadWidth / 2 + spreadWidth * t;\n\n    const sineValue = Math.sin(t * Math.PI);\n    const eased = sineValue * sineValue;\n    const opacity = peakOpacity * eased;\n\n    stops.push(\n      `rgba(255, 255, 255, ${opacity.toFixed(4)}) ${position.toFixed(1)}%`,\n    );\n  }\n\n  return `linear-gradient(${angle}deg, ${stops.join(\", \")})`;\n}\n\nexport function FauxChatShellMobileAnimated() {\n  // Hardcoded animation values - adjusted for portrait phone\n  const initial = {\n    opacity: 0,\n    rotateX: 20,\n    rotateY: 37,\n    rotateZ: -13,\n    x: 429,\n    y: -298,\n    scale: 2,\n  };\n\n  const resting = {\n    opacity: 1,\n    rotateX: 37,\n    rotateY: 26,\n    rotateZ: -18,\n    x: -35,\n    y: -60,\n    scale: 1.05,\n  };\n\n  // Perspective and transform origin\n  const perspective = 3700;\n  const perspectiveOriginX = 100;\n  const perspectiveOriginY = 29;\n  const originX = 70;\n  const originY = 64;\n  const translateZInitial = 96;\n  const translateZResting = 96;\n\n  // Lighting parameters\n  const lightAngle = 140;\n  const lightPosition = 82;\n  const lightSpread = 29;\n  const lightOpacity = 0.07;\n\n  // Calculate light position based on surface normal (simulates static directional light)\n  function calculateLightPosition(rotateX: number, rotateY: number) {\n    const baseY = lightPosition;\n    const highlightY = baseY - rotateX * 0.8 - rotateY * 0.3;\n    return highlightY;\n  }\n\n  // Calculate highlight position for resting state\n  const restingLightPos = calculateLightPosition(\n    resting.rotateX,\n    resting.rotateY,\n  );\n\n  // Generate gradient centered at calculated position\n  const restingLightGradient = generateSineEasedGradient(\n    lightAngle,\n    restingLightPos,\n    lightOpacity,\n    lightSpread,\n    32,\n  );\n\n  // Animation config\n  const transition = {\n    type: \"spring\" as const,\n    duration: 5,\n    bounce: 0.2,\n  };\n\n  return (\n    <div\n      className=\"relative h-full w-full flex items-center justify-center\"\n      style={{\n        perspective: `${perspective}px`,\n        perspectiveOrigin: `${perspectiveOriginX}% ${perspectiveOriginY}%`,\n      }}\n    >\n      <motion.div\n        className=\"relative h-full w-full overflow-hidden rounded-[2.5rem]\"\n        initial={{ ...initial, translateZ: translateZInitial }}\n        animate={{ ...resting, translateZ: translateZResting }}\n        transition={transition}\n        style={{\n          transformStyle: \"preserve-3d\",\n          transformOrigin: `${originX}% ${originY}%`,\n        }}\n      >\n        <FauxChatShellMobile />\n\n        {/* Lighting overlay - fades in from 0 to full opacity */}\n        <motion.div\n          className=\"pointer-events-none absolute inset-0 z-30\"\n          initial={{ opacity: 0 }}\n          animate={{ opacity: 1 }}\n          transition={transition}\n          style={{\n            backgroundImage: `url(\"${generateNoiseDataUri(64, 0.015)}\"), ${restingLightGradient}`,\n            backgroundBlendMode: \"overlay, normal\",\n          }}\n          aria-hidden=\"true\"\n        />\n      </motion.div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/components/home/faux-chat-shell-mobile.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { cn } from \"@/lib/ui/cn\";\nimport { ChatShowcase } from \"./chat-showcase\";\n\ntype FauxChatShellMobileProps = {\n  className?: string;\n};\n\nfunction DynamicIsland() {\n  return (\n    <div\n      className=\"absolute left-1/2 top-3 z-30 -translate-x-1/2\"\n      aria-hidden=\"true\"\n    >\n      <div className=\"border-gradient-glow-notch h-9 w-32 rounded-full\" />\n    </div>\n  );\n}\n\nfunction StatusBar() {\n  const [time, setTime] = useState<string>(\"\");\n\n  useEffect(() => {\n    const updateTime = () => {\n      const now = new Date();\n      let hours = now.getHours();\n      const minutes = now.getMinutes().toString().padStart(2, \"0\");\n      hours = hours % 12 || 12;\n      setTime(`${hours}:${minutes}`);\n    };\n\n    updateTime();\n\n    const scheduleNextUpdate = () => {\n      const now = new Date();\n      const msUntilNextMinute =\n        (60 - now.getSeconds()) * 1000 - now.getMilliseconds();\n      return setTimeout(() => {\n        updateTime();\n        intervalRef = setInterval(updateTime, 60000);\n      }, msUntilNextMinute);\n    };\n\n    let intervalRef: ReturnType<typeof setInterval> | null = null;\n    const timeoutRef = scheduleNextUpdate();\n\n    return () => {\n      clearTimeout(timeoutRef);\n      if (intervalRef) clearInterval(intervalRef);\n    };\n  }, []);\n\n  return (\n    <div className=\"absolute inset-x-0 top-0 z-20 flex items-center justify-between px-6 pt-6 text-sm\">\n      <div className=\"font-medium\">{time}</div>\n      <div className=\"flex items-center gap-1\">\n        {/* Signal/WiFi/Battery icons would go here - keeping minimal */}\n      </div>\n    </div>\n  );\n}\n\nexport function FauxChatShellMobile({ className }: FauxChatShellMobileProps) {\n  return (\n    <div\n      className={cn(\n        \"border-gradient-glow relative flex h-full w-full max-w-[430px] flex-col overflow-hidden rounded-[2.5rem] backdrop-blur-lg\",\n        className,\n      )}\n      style={{\n        aspectRatio: \"9 / 19.5\",\n        boxShadow: [\n          \"0 1px 3px rgba(0, 0, 0, 0.05)\",\n          \"0 2px 4px rgba(0, 0, 0, 0.008)\",\n          \"0 4px 8px rgba(0, 0, 0, 0.02)\",\n          \"0 8px 16px rgba(0, 0, 0, 0.02)\",\n          \"0 16px 32px rgba(0, 0, 0, 0.02)\",\n          \"0 32px 48px rgba(0, 0, 0, 0.03)\",\n        ].join(\", \"),\n        backfaceVisibility: \"hidden\",\n        willChange: \"transform\",\n        WebkitFontSmoothing: \"subpixel-antialiased\",\n      }}\n    >\n      <DynamicIsland />\n      <StatusBar />\n\n      <div className=\"scrollbar-subtle relative z-0 grow overflow-y-auto px-4 pt-20\">\n        <ChatShowcase />\n      </div>\n\n      <div\n        className=\"pointer-events-none absolute inset-x-0 bottom-0 z-10 h-24\"\n        style={{\n          background:\n            \"linear-gradient(to top, var(--glow-surface-to) 0%, transparent 100%)\",\n        }}\n        aria-hidden=\"true\"\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/components/home/faux-chat-shell.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/ui/cn\";\nimport { ChatShowcase } from \"./chat-showcase\";\nimport { DemoChat } from \"./demo-chat\";\n\ntype FauxChatShellProps = {\n  className?: string;\n  /** When true, renders live DemoChat instead of static ChatShowcase */\n  live?: boolean;\n};\n\nexport function generateSineEasedGradient(\n  angle: number,\n  centerPosition: number,\n  peakOpacity: number,\n  spreadWidth: number,\n  steps: number = 128,\n): string {\n  const stops: string[] = [];\n\n  for (let i = 0; i <= steps; i++) {\n    const t = i / steps;\n    const position = centerPosition - spreadWidth / 2 + spreadWidth * t;\n\n    // Sine curve: peaks at center (t=0.5), zero at edges (t=0, t=1)\n    // Square the sine for even smoother falloff at edges\n    const sineValue = Math.sin(t * Math.PI);\n    const eased = sineValue * sineValue;\n    const opacity = peakOpacity * eased;\n\n    stops.push(\n      `rgba(255, 255, 255, ${opacity.toFixed(4)}) ${position.toFixed(1)}%`,\n    );\n  }\n\n  // No hard transparent stops — sine² already smoothly reaches zero at edges\n  return `linear-gradient(${angle}deg, ${stops.join(\", \")})`;\n}\n\nfunction useDirectionalLight(\n  containerRef: React.RefObject<HTMLDivElement | null>,\n) {\n  const [gradientStyle, setGradientStyle] = useState<React.CSSProperties>({});\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return;\n\n    const updateGradient = () => {\n      const parent = container.parentElement;\n      if (!parent) return;\n\n      const style = getComputedStyle(parent);\n      const rotateX =\n        parseFloat(style.getPropertyValue(\"--shell-rotate-x\")) || 0;\n      const rotateY =\n        parseFloat(style.getPropertyValue(\"--shell-rotate-y\")) || 0;\n\n      // Light params from CSS custom properties (with defaults)\n      const baseAngle =\n        parseFloat(style.getPropertyValue(\"--light-angle\")) || 135;\n      const basePosition =\n        parseFloat(style.getPropertyValue(\"--light-position\")) || 35;\n      const spread = parseFloat(style.getPropertyValue(\"--light-spread\")) || 40;\n      const opacity =\n        parseFloat(style.getPropertyValue(\"--light-opacity\")) || 0.12;\n      const rotateXFactor =\n        parseFloat(style.getPropertyValue(\"--light-rotate-x-factor\")) || 1.2;\n      const rotateYFactor =\n        parseFloat(style.getPropertyValue(\"--light-rotate-y-factor\")) || 0.8;\n\n      // Highlight position shifts based on rotation (light \"slides\" across surface)\n      const centerPosition =\n        basePosition - rotateX * rotateXFactor - rotateY * rotateYFactor;\n\n      const gradient = generateSineEasedGradient(\n        baseAngle,\n        centerPosition,\n        opacity,\n        spread,\n        32,\n      );\n\n      setGradientStyle({ background: gradient });\n    };\n\n    updateGradient();\n\n    // Watch for changes via MutationObserver on parent's style attribute\n    const parent = container.parentElement;\n    if (!parent) return;\n\n    const observer = new MutationObserver(updateGradient);\n    observer.observe(parent, { attributes: true, attributeFilter: [\"style\"] });\n\n    return () => observer.disconnect();\n  }, [containerRef]);\n\n  return gradientStyle;\n}\n\nfunction WindowDots() {\n  return (\n    <div className=\"flex items-center gap-1.5\" aria-hidden=\"true\">\n      <span className=\"border-gradient-glow border-gradient-glow-dot size-3.5 rounded-full\" />\n      <span className=\"border-gradient-glow border-gradient-glow-dot size-3.5 rounded-full\" />\n      <span className=\"border-gradient-glow border-gradient-glow-dot size-3.5 rounded-full\" />\n    </div>\n  );\n}\n\nfunction ComposerBar() {\n  return (\n    <div className=\"absolute inset-x-0 bottom-0 z-20 px-4 pb-4\">\n      <div className=\"border-gradient-glow border-gradient-glow-composer h-10 w-full rounded-full\" />\n    </div>\n  );\n}\n\nfunction FauxChatShellBase({\n  className,\n  showLightOverlay,\n  live = false,\n}: FauxChatShellProps & { showLightOverlay: boolean }) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const gradientStyle = useDirectionalLight(containerRef);\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\n        \"border-gradient-glow relative flex h-full w-full flex-col overflow-hidden rounded-2xl backdrop-blur-lg\",\n        className,\n      )}\n      style={{\n        boxShadow: [\n          \"0 1px 3px rgba(0, 0, 0, 0.05)\",\n          \"0 2px 4px rgba(0, 0, 0, 0.008)\",\n          \"0 4px 8px rgba(0, 0, 0, 0.02)\",\n          \"0 8px 16px rgba(0, 0, 0, 0.02)\",\n          \"0 16px 32px rgba(0, 0, 0, 0.02)\",\n          \"0 32px 48px rgba(0, 0, 0, 0.03)\",\n        ].join(\", \"),\n        // 3D rendering quality improvements\n        backfaceVisibility: \"hidden\",\n        willChange: \"transform\",\n        WebkitFontSmoothing: \"subpixel-antialiased\",\n      }}\n    >\n      {/* Directional light reflection overlay */}\n      {showLightOverlay && (\n        <div\n          className=\"pointer-events-none absolute inset-0 z-30 rounded-2xl\"\n          style={gradientStyle}\n          aria-hidden=\"true\"\n        />\n      )}\n\n      <div className=\"bg-background/80 absolute z-20 w-full rounded-t-2xl backdrop-blur-lg\">\n        <div className=\"flex h-10 shrink-0 items-center px-4 pt-0.5\">\n          <WindowDots />\n        </div>\n        <div className=\"gradient-line-header h-px\" />\n      </div>\n      <div\n        className={\n          live\n            ? \"relative z-0 flex min-h-0 flex-1 flex-col overflow-hidden\"\n            : \"scrollbar-subtle relative z-0 grow overflow-y-auto px-6 pt-24\"\n        }\n      >\n        {live ? <DemoChat /> : <ChatShowcase />}\n      </div>\n      {!live && (\n        <>\n          <div\n            className=\"pointer-events-none absolute inset-x-0 right-3 bottom-0 z-10 h-24\"\n            style={{\n              background:\n                \"linear-gradient(to top, var(--glow-surface-to) 0%, transparent 100%)\",\n            }}\n            aria-hidden=\"true\"\n          />\n          <ComposerBar />\n        </>\n      )}\n    </div>\n  );\n}\n\nexport function FauxChatShell(props: FauxChatShellProps) {\n  return <FauxChatShellBase {...props} showLightOverlay={true} />;\n}\n\nexport function FauxChatShellNoLightOverlay(props: FauxChatShellProps) {\n  return <FauxChatShellBase {...props} showLightOverlay={false} />;\n}\n"
  },
  {
    "path": "apps/www/app/components/home/home-background.tsx",
    "content": "\"use client\";\n\nexport function HomeBackground() {\n  return (\n    <>\n      <div\n        className=\"bg-background pointer-events-none fixed inset-0 opacity-60 dark:opacity-40\"\n        style={{ animation: \"home-bg-fade-in 0.6s ease-out forwards\" }}\n        aria-hidden=\"true\"\n      />\n      <style jsx>{`\n        @keyframes home-bg-fade-in {\n          from {\n            opacity: 0;\n          }\n          to {\n            opacity: 1;\n          }\n        }\n      `}</style>\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/components/home/home-hero.tsx",
    "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { useRouter } from \"next/navigation\";\nimport { ArrowRight } from \"lucide-react\";\nimport { motion } from \"motion/react\";\nimport { HomeHexnutScene } from \"./home-hexnut-scene\";\nimport { Button } from \"@/components/ui/button\";\nimport { analytics } from \"@/lib/analytics\";\n\nconst smoothSpring = {\n  type: \"spring\" as const,\n  stiffness: 200,\n  damping: 40,\n  duration: 0.8,\n  restDelta: 0.0001,\n};\n\nexport function HomeHero() {\n  const router = useRouter();\n  const preloadGallery = () => {\n    void router.prefetch(\"/docs/gallery\");\n  };\n\n  return (\n    <div className=\"flex flex-col gap-7\">\n      <motion.div\n        className=\"-mb-4 -ml-4 flex items-end justify-start\"\n        initial={{ opacity: 0 }}\n        animate={{ opacity: 1 }}\n        transition={{ ...smoothSpring, delay: 0.1 }}\n      >\n        <HomeHexnutScene />\n      </motion.div>\n      <div className=\"flex flex-col gap-3\">\n        <div className=\"flex flex-col gap-3\">\n          <motion.h1\n            className=\"text-6xl font-bold tracking-wide\"\n            initial={{ opacity: 0, y: 20 }}\n            animate={{ opacity: 1, y: 0 }}\n            transition={{ ...smoothSpring, delay: 0.1 }}\n          >\n            Tool UI\n          </motion.h1>\n        </div>\n        <motion.h2\n          className=\"text-2xl text-pretty\"\n          initial={{ opacity: 0, y: 20 }}\n          animate={{ opacity: 1, y: 0 }}\n          transition={{ ...smoothSpring, delay: 0.3 }}\n        >\n          UI components for AI interfaces\n        </motion.h2>\n      </div>\n      <motion.p\n        className=\"text-muted-foreground mb-2 text-lg text-pretty\"\n        initial={{ opacity: 0, y: 20 }}\n        animate={{ opacity: 1, y: 0 }}\n        transition={{ ...smoothSpring, delay: 0.4 }}\n      >\n        JSON-native, typed, accessible, copy-pasteable.{\" \"}\n        <br className=\"hidden md:block\" />\n        Built on Tailwind, Radix, and shadcn/ui. Open Source.\n      </motion.p>\n      <motion.div\n        initial={{ opacity: 0, y: 20, scale: 0.98 }}\n        animate={{ opacity: 1, y: 0, scale: 1 }}\n        transition={{ ...smoothSpring, delay: 0.5 }}\n      >\n        <Button\n          asChild\n          className=\"group font-medium tracking-wide\"\n          size=\"homeCTA\"\n        >\n          <Link\n            href=\"/docs/gallery\"\n            onMouseEnter={preloadGallery}\n            onFocus={preloadGallery}\n            onClick={() => analytics.cta.clicked(\"see_components\", \"home_hero\")}\n          >\n            See the Components\n            <ArrowRight className=\"size-5 shrink-0 transition-transform group-hover:translate-x-1\" />\n          </Link>\n        </Button>\n      </motion.div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/components/home/home-hexnut-scene.tsx",
    "content": "\"use client\";\n\nimport dynamic from \"next/dynamic\";\n\n// Dynamic import with SSR disabled for Three.js components\nconst HexnutSceneCanvas = dynamic(\n  () =>\n    import(\"@/app/components/visuals/spinning-hexnut/hexnut-scene\").then(\n      (mod) => mod.HexnutScene,\n    ),\n  {\n    ssr: false,\n    loading: () => <div className=\"size-36 sm:size-48 bg-transparent\" />,\n  },\n);\n\nexport function HomeHexnutScene() {\n  return (\n    <div className=\"size-36 sm:size-48\">\n      <HexnutSceneCanvas width=\"100%\" height=\"100%\" />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/components/home/noise-texture.ts",
    "content": "/**\n * Generates a data URI for a subtle noise texture to reduce gradient banding\n * @param size - Size of the noise tile (default: 64px)\n * @param opacity - Opacity of the noise (0-1, default: 0.02)\n */\nexport function generateNoiseDataUri(\n  size: number = 64,\n  opacity: number = 0.02,\n): string {\n  // Create a minimal noise pattern using SVG\n  // This is more efficient than canvas and works in SSR\n  const svg = `\n    <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\">\n      <filter id=\"noise\">\n        <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.9\" numOctaves=\"4\" />\n        <feComponentTransfer>\n          <feFuncA type=\"discrete\" tableValues=\"0 ${opacity}\" />\n        </feComponentTransfer>\n      </filter>\n      <rect width=\"${size}\" height=\"${size}\" filter=\"url(#noise)\" />\n    </svg>\n  `.trim();\n\n  // Use URL encoding consistently for both SSR and client to avoid hydration mismatch\n  return `data:image/svg+xml,${encodeURIComponent(svg)}`;\n}\n\n/**\n * CSS style object for applying noise texture over gradients\n */\nexport function getNoiseOverlayStyle(opacity: number = 0.02) {\n  return {\n    backgroundImage: `url(\"${generateNoiseDataUri(64, opacity)}\")`,\n    backgroundBlendMode: \"overlay\" as const,\n  };\n}\n"
  },
  {
    "path": "apps/www/app/components/layout/app-header.server.tsx",
    "content": "import { ReactNode } from \"react\";\nimport Link from \"next/link\";\nimport { Button } from \"@/components/ui/button\";\n\nimport { FaGithub } from \"react-icons/fa\";\nimport { FaXTwitter } from \"react-icons/fa6\";\nimport { ArrowUpRight } from \"lucide-react\";\nimport { LogoMark } from \"@/components/ui/logo\";\nimport { ActiveNavLink } from \"./header-active-link.client\";\nimport { TrackedExternalAnchor } from \"./tracked-external-anchor.client\";\n\ninterface ResponsiveHeaderProps {\n  rightContent?: ReactNode;\n}\n\nexport function ResponsiveHeader({ rightContent }: ResponsiveHeaderProps) {\n  const navLinks = [\n    { href: \"/docs/overview\", label: \"Docs\" },\n    { href: \"/docs/gallery\", label: \"Gallery\" },\n  ];\n\n  return (\n    <div className=\"pt-calc(env(safe-area-inset-top)+0.5rem) flex gap-4 pt-4 pb-2 sm:pt-8 sm:pb-3 md:gap-8\">\n      <div className=\"flex w-fit shrink-0 items-center justify-start gap-3 md:items-center\">\n        <Link href=\"/\" className=\"flex items-center gap-1.5\">\n          <LogoMark className=\"-mb-0.5 size-6\" />\n          <h1 className=\"text-2xl font-semibold\">Tool UI</h1>\n        </Link>\n      </div>\n\n      {/* Desktop Navigation */}\n      <div className=\"hidden flex-1 items-center justify-between md:flex md:pl-18\">\n        <nav className=\"flex items-center gap-1\">\n          {navLinks.map(({ href, label }) => (\n            <ActiveNavLink key={href} href={href}>\n              {label}\n            </ActiveNavLink>\n          ))}\n          <TrackedExternalAnchor\n            href=\"https://www.assistant-ui.com\"\n            destination=\"docs\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"hover:text-foreground hover:bg-muted/50 hidden items-center gap-0.5 rounded-lg px-4 py-2 text-sm font-medium transition-colors lg:flex\"\n          >\n            assistant-ui\n            <ArrowUpRight className=\"size-4\" />\n          </TrackedExternalAnchor>\n        </nav>\n        <div className=\"flex items-center gap-4\">\n          {rightContent}\n          <div className=\"flex items-center\">\n            <Button variant=\"ghost\" size=\"icon\" asChild>\n              <TrackedExternalAnchor\n                href=\"https://github.com/assistant-ui/tool-ui\"\n                destination=\"github\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n              >\n                <FaGithub className=\"size-5\" />\n                <span className=\"sr-only\">GitHub Repository</span>\n              </TrackedExternalAnchor>\n            </Button>\n            <Button variant=\"ghost\" size=\"icon\" asChild>\n              <TrackedExternalAnchor\n                href=\"https://x.com/assistantui\"\n                destination=\"other\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n              >\n                <FaXTwitter className=\"size-5\" />\n                <span className=\"sr-only\">X (Twitter)</span>\n              </TrackedExternalAnchor>\n            </Button>\n          </div>\n        </div>\n      </div>\n\n      {/* Mobile Right Content */}\n      <div className=\"flex flex-1 items-center justify-end gap-2 md:hidden\">\n        {rightContent}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/components/layout/app-shell-animated.client.tsx",
    "content": "\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { useEffect, useState } from \"react\";\nimport { ResponsiveHeader } from \"@/app/components/layout/app-header.server\";\nimport { cn } from \"@/lib/ui/cn\";\n\ntype HeaderFrameProps = {\n  children: ReactNode;\n  rightContent?: ReactNode;\n  background?: ReactNode;\n};\n\n// Persists for the life of the client bundle so the intro animation runs once.\nlet hasPlayedIntroAnimation = false;\n\nfunction HeaderFrameBase({\n  children,\n  rightContent,\n  background,\n  shouldAnimate,\n}: HeaderFrameProps & { shouldAnimate: boolean }) {\n  return (\n    <div className=\"relative flex h-full flex-col items-center overflow-hidden\">\n      {background ? (\n        <div className=\"pointer-events-none absolute inset-0 z-0\">\n          {background}\n        </div>\n      ) : null}\n      <div\n        className={cn(\n          \"relative z-10 w-full max-w-[1440px] shrink-0 px-4 md:px-8\",\n          shouldAnimate && \"animate-navbar-fade-in\",\n        )}\n      >\n        <ResponsiveHeader rightContent={rightContent} />\n      </div>\n      <div className=\"relative z-10 flex min-h-0 w-full flex-1 justify-center\">\n        {children}\n      </div>\n    </div>\n  );\n}\n\nexport function AnimatedHeaderFrame(props: HeaderFrameProps) {\n  const [shouldAnimate] = useState(() => !hasPlayedIntroAnimation);\n\n  useEffect(() => {\n    if (shouldAnimate) {\n      hasPlayedIntroAnimation = true;\n    }\n  }, [shouldAnimate]);\n\n  return <HeaderFrameBase {...props} shouldAnimate={shouldAnimate} />;\n}\n"
  },
  {
    "path": "apps/www/app/components/layout/app-shell.tsx",
    "content": "import type { ReactNode } from \"react\";\nimport { ResponsiveHeader } from \"@/app/components/layout/app-header.server\";\nimport { cn } from \"@/lib/ui/cn\";\n\nexport type HeaderFrameProps = {\n  children: ReactNode;\n  rightContent?: ReactNode;\n  background?: ReactNode;\n};\n\nfunction HeaderFrameBase({\n  children,\n  rightContent,\n  background,\n  shouldAnimate,\n}: HeaderFrameProps & { shouldAnimate: boolean }) {\n  return (\n    <div className=\"relative flex h-full flex-col items-center overflow-hidden\">\n      {background ? (\n        <div className=\"pointer-events-none absolute inset-0 z-0\">\n          {background}\n        </div>\n      ) : null}\n      <div\n        className={cn(\n          \"relative z-10 w-full max-w-[1440px] shrink-0 px-4 md:px-8\",\n          shouldAnimate && \"animate-navbar-fade-in\",\n        )}\n      >\n        <ResponsiveHeader rightContent={rightContent} />\n      </div>\n      <div className=\"relative z-10 flex min-h-0 w-full flex-1 justify-center\">\n        {children}\n      </div>\n    </div>\n  );\n}\n\nexport function HeaderFrame(props: HeaderFrameProps) {\n  return <HeaderFrameBase {...props} shouldAnimate={false} />;\n}\n"
  },
  {
    "path": "apps/www/app/components/layout/header-active-link.client.tsx",
    "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { usePathname } from \"next/navigation\";\nimport { cn } from \"@/lib/ui/cn\";\n\ninterface ActiveNavLinkProps {\n  href: string;\n  children: React.ReactNode;\n}\n\nexport function ActiveNavLink({ href, children }: ActiveNavLinkProps) {\n  const pathname = usePathname();\n  const isActive = href === \"/\" ? pathname === \"/\" : pathname.startsWith(href);\n\n  return (\n    <Link\n      href={href}\n      className={cn(\n        \"active:bg-primary/10 rounded-lg px-4 py-2 text-sm transition-[colors,background] duration-75\",\n        isActive ? \"bg-primary/5\" : \"hover:bg-primary/5\",\n      )}\n      aria-current={isActive ? \"page\" : undefined}\n    >\n      {children}\n    </Link>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/components/layout/mobile-nav-sheet-gate.client.tsx",
    "content": "\"use client\";\n\nimport dynamic from \"next/dynamic\";\nimport { usePathname } from \"next/navigation\";\nimport { useEffect, useState } from \"react\";\n\nconst MobileNavSheet = dynamic(\n  () =>\n    import(\"@/app/components/layout/mobile-nav-sheet.client\").then(\n      (mod) => mod.MobileNavSheet,\n    ),\n  {\n    ssr: false,\n  },\n);\n\nconst MOBILE_QUERY = \"(max-width: 767px)\";\n\nfunction shouldRenderForPath(pathname: string) {\n  return (\n    pathname === \"/\" ||\n    pathname.startsWith(\"/docs\") ||\n    pathname.startsWith(\"/builder\")\n  );\n}\n\nexport function MobileNavSheetGate() {\n  const pathname = usePathname();\n  const [isMobileViewport, setIsMobileViewport] = useState(false);\n\n  useEffect(() => {\n    const mediaQuery = window.matchMedia(MOBILE_QUERY);\n    const updateViewportMatch = () => setIsMobileViewport(mediaQuery.matches);\n\n    updateViewportMatch();\n    mediaQuery.addEventListener(\"change\", updateViewportMatch);\n\n    return () => {\n      mediaQuery.removeEventListener(\"change\", updateViewportMatch);\n    };\n  }, []);\n\n  if (!isMobileViewport || !shouldRenderForPath(pathname)) {\n    return null;\n  }\n\n  return <MobileNavSheet />;\n}\n"
  },
  {
    "path": "apps/www/app/components/layout/mobile-nav-sheet.client.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\nimport Link from \"next/link\";\nimport { usePathname } from \"next/navigation\";\nimport { Sheet, Scroll } from \"@silk-hq/components\";\nimport { FaGithub } from \"react-icons/fa\";\nimport { FaXTwitter } from \"react-icons/fa6\";\nimport { componentsRegistry } from \"@/lib/docs/component-registry\";\nimport { BASE_DOCS_PAGES } from \"@/app/docs/_components/docs-pages\";\nimport { cn } from \"@/lib/ui/cn\";\nimport { Button } from \"@/components/ui/button\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { TrackedExternalAnchor } from \"./tracked-external-anchor.client\";\n\nimport \"@/app/styles/nav-sheet.css\";\n\nexport function MobileNavSheet() {\n  const pathname = usePathname();\n  const [presented, setPresented] = React.useState(false);\n  const [showScrollIndicator, setShowScrollIndicator] = React.useState(true);\n  const [isMobileViewport, setIsMobileViewport] = React.useState(false);\n\n  React.useEffect(() => {\n    const mediaQuery = window.matchMedia(\"(max-width: 767px)\");\n    const updateViewportMatch = () => setIsMobileViewport(mediaQuery.matches);\n\n    updateViewportMatch();\n    mediaQuery.addEventListener(\"change\", updateViewportMatch);\n\n    return () => {\n      mediaQuery.removeEventListener(\"change\", updateViewportMatch);\n    };\n  }, []);\n\n  const isDocs = pathname.startsWith(\"/docs\") && pathname !== \"/docs/gallery\";\n  const isGallery = pathname === \"/docs/gallery\";\n\n  const mainNavLinks = [\n    { href: \"/docs/overview\", label: \"Docs\", isActive: isDocs },\n    { href: \"/docs/gallery\", label: \"Gallery\", isActive: isGallery },\n  ];\n\n  if (!isMobileViewport) {\n    return null;\n  }\n\n  return (\n    <Sheet.Root\n      license=\"non-commercial\"\n      presented={presented}\n      onPresentedChange={setPresented}\n      defaultActiveDetent={1}\n    >\n      {/* Floating Action Button */}\n      <Sheet.Trigger asChild>\n        <Button\n          variant=\"default\"\n          size=\"icon\"\n          className=\"MobileNavSheet-trigger z-50 size-14 rounded-full shadow-lg md:hidden\"\n          aria-label=\"Open navigation\"\n        >\n          <svg\n            className=\"size-6\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"2\"\n            strokeLinecap=\"round\"\n          >\n            <line x1=\"4\" y1=\"8\" x2=\"20\" y2=\"8\" />\n            <line x1=\"4\" y1=\"16\" x2=\"20\" y2=\"16\" />\n          </svg>\n        </Button>\n      </Sheet.Trigger>\n\n      {/* Bottom Sheet Portal */}\n      <Sheet.Portal>\n        <Sheet.View\n          className=\"MobileNavSheet-view\"\n          nativeEdgeSwipePrevention={true}\n        >\n          <Sheet.Backdrop\n            className=\"MobileNavSheet-backdrop\"\n            travelAnimation={{\n              opacity: [0, 0.8],\n            }}\n            themeColorDimming=\"auto\"\n          />\n          <Sheet.Content className=\"MobileNavSheet-content\">\n            <Sheet.BleedingBackground className=\"MobileNavSheet-bleedingBackground\" />\n\n            {/* handle */}\n            <div className=\"elative absolute top-0 right-0 left-0 z-20 flex items-center justify-center bg-transparent py-4\">\n              <div className=\"bg-muted-foreground h-1 w-12 rounded-full\" />\n            </div>\n\n            {/* Top Scroll Indicator Gradient */}\n            <div className=\"from-background pointer-events-none absolute top-0 right-0 left-0 z-10 h-24 rounded-tl-xl rounded-tr-xl bg-linear-to-b to-transparent\" />\n\n            <Scroll.Root className=\"scrollbar-subtle relative flex-1 overflow-hidden\">\n              <Scroll.View\n                className=\"h-full\"\n                safeArea=\"visual-viewport\"\n                nativeFocusScrollPrevention={true}\n                onScroll={({ progress }) => {\n                  // Hide bottom indicator when scrolled near the bottom (within 5%)\n                  setShowScrollIndicator(progress < 0.95);\n                }}\n              >\n                <Scroll.Content>\n                  <nav className=\"flex flex-col gap-2 p-4 pt-12\">\n                    {mainNavLinks.map(({ href, label, isActive }) => (\n                      <Link\n                        key={href}\n                        href={href}\n                        onClick={() => setPresented(false)}\n                        className={cn(\n                          \"rounded-lg px-4 py-4 text-3xl font-medium transition-colors\",\n                          isActive\n                            ? \"bg-muted text-foreground\"\n                            : \"text-primary hover:bg-muted/50 hover:text-foreground\",\n                        )}\n                      >\n                        {label}\n                      </Link>\n                    ))}\n                  </nav>\n                  <div className=\"my-3 px-8\">\n                    <Separator />\n                  </div>\n\n                  {/* Docs Section */}\n                  <div className=\"flex flex-col gap-1 px-4 py-8\">\n                    <div className=\"text-muted-foreground mb-3 px-4 text-xs tracking-widest uppercase\">\n                      Get Started\n                    </div>\n\n                    {BASE_DOCS_PAGES.map((page) => {\n                      const isActive = pathname === page.path;\n\n                      return (\n                        <Link\n                          key={page.path}\n                          href={page.path}\n                          onClick={() => setPresented(false)}\n                          className={cn(\n                            \"text-primary rounded-lg px-4 py-3.5\",\n                            isActive\n                              ? \"bg-muted text-foreground font-medium\"\n                              : \"text-primary hover:bg-muted/50 hover:text-foreground\",\n                          )}\n                        >\n                          {page.label}\n                        </Link>\n                      );\n                    })}\n                  </div>\n\n                  {/* Components Section */}\n                  <div className=\"flex flex-col gap-1 px-4 pb-8\">\n                    <div className=\"text-muted-foreground mb-3 px-4 text-xs tracking-widest uppercase\">\n                      Components\n                    </div>\n\n                    {componentsRegistry.map((component) => {\n                      const isActive = pathname === component.path;\n\n                      return (\n                        <Link\n                          key={component.id}\n                          href={component.path}\n                          onClick={() => setPresented(false)}\n                          className={cn(\n                            \"text-primary rounded-lg px-4 py-3.5\",\n                            isActive\n                              ? \"bg-muted text-foreground font-medium\"\n                              : \"text-primary hover:bg-muted/50 hover:text-foreground\",\n                          )}\n                        >\n                          {component.label}\n                        </Link>\n                      );\n                    })}\n                  </div>\n                </Scroll.Content>\n              </Scroll.View>\n\n              {/* Bottom Scroll Indicator Gradient */}\n              {showScrollIndicator && (\n                <div className=\"MobileNavSheet-bottomGradient\" />\n              )}\n            </Scroll.Root>\n\n            {/* Social Links Footer */}\n            <footer className=\"MobileNavSheet-footer\">\n              <div className=\"MobileNavSheet-footerInner\">\n                <Button variant=\"outline\" size=\"lg\" className=\"flex-1\" asChild>\n                  <TrackedExternalAnchor\n                    href=\"https://github.com/assistant-ui/tool-ui\"\n                    destination=\"github\"\n                    target=\"_blank\"\n                    rel=\"noopener noreferrer\"\n                  >\n                    <FaGithub className=\"size-6\" />\n                  </TrackedExternalAnchor>\n                </Button>\n                <Button variant=\"outline\" size=\"lg\" className=\"flex-1\" asChild>\n                  <TrackedExternalAnchor\n                    href=\"https://x.com/assistantui\"\n                    destination=\"other\"\n                    target=\"_blank\"\n                    rel=\"noopener noreferrer\"\n                  >\n                    <FaXTwitter className=\"size-6\" />\n                  </TrackedExternalAnchor>\n                </Button>\n              </div>\n            </footer>\n          </Sheet.Content>\n        </Sheet.View>\n      </Sheet.Portal>\n    </Sheet.Root>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/components/layout/page-shell.tsx",
    "content": "import type { ReactNode } from \"react\";\n\ntype ContentLayoutProps = {\n  children: ReactNode;\n  sidebar?: ReactNode;\n};\n\nexport default function ContentLayout({\n  children,\n  sidebar,\n}: ContentLayoutProps) {\n  return (\n    <div className=\"flex min-h-0 w-full max-w-[1440px] flex-1\">\n      {sidebar ? (\n        <div className=\"relative hidden w-[220px] shrink-0 md:block\">\n          <div\n            className=\"from-background pointer-events-none absolute top-0 right-2 left-0 z-10 h-12 bg-linear-to-b to-transparent\"\n            aria-hidden=\"true\"\n          />\n          <div className=\"scrollbar-subtle h-full overflow-y-auto pt-4\">\n            {sidebar}\n          </div>\n          <div\n            className=\"from-background pointer-events-none absolute right-2 bottom-0 left-0 z-10 h-12 bg-linear-to-t to-transparent\"\n            aria-hidden=\"true\"\n          />\n        </div>\n      ) : null}\n      <div className=\"flex min-h-0 min-w-0 w-full flex-1\">{children}</div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/components/layout/tracked-external-anchor.client.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { analytics } from \"@/lib/analytics\";\n\ntype ExternalDestination = \"github\" | \"npm\" | \"docs\" | \"other\";\n\ntype TrackedExternalAnchorProps = React.ComponentPropsWithoutRef<\"a\"> & {\n  destination: ExternalDestination;\n};\n\nexport const TrackedExternalAnchor = React.forwardRef<\n  HTMLAnchorElement,\n  TrackedExternalAnchorProps\n>(function TrackedExternalAnchor(\n  { destination, href, onClick, ...props },\n  ref,\n) {\n  const hrefValue = typeof href === \"string\" ? href : \"\";\n\n  const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {\n    analytics.external.linkClicked(destination, hrefValue);\n    onClick?.(event);\n  };\n\n  return <a ref={ref} href={href} onClick={handleClick} {...props} />;\n});\n"
  },
  {
    "path": "apps/www/app/components/mdx/features.tsx",
    "content": "import { cn } from \"@/lib/ui/cn\";\nimport type { LucideIcon } from \"lucide-react\";\nimport {\n  Palette,\n  Copy,\n  Hash,\n  Highlighter,\n  FoldVertical,\n  FileCode,\n  FileText,\n  Moon,\n  BarChart3,\n  LineChart,\n  MousePointerClick,\n  CircleCheck,\n  CheckCircle2,\n  ListChecks,\n  Settings2,\n  SquareCheck,\n  Paintbrush,\n  Terminal,\n  Timer,\n  AlertCircle,\n  AlertTriangle,\n  Code,\n  FileOutput,\n  Smartphone,\n  ArrowUpDown,\n  Table2,\n  Accessibility,\n  Link,\n  Image,\n  Video,\n  Headphones,\n  Download,\n  Expand,\n  PartyPopper,\n  HelpCircle,\n  Layers,\n  Play,\n  Eye,\n  Maximize2,\n  ListTodo,\n  List,\n  GalleryHorizontalEnd,\n  Keyboard,\n  Globe,\n  ExternalLink,\n  Ratio,\n  Quote,\n  ShieldCheck,\n  Target,\n  MessageSquare,\n  BadgeCheck,\n  Images,\n  Network,\n  Route,\n  ScanSearch,\n  Rocket,\n  DollarSign,\n  Package,\n  Grid2x2,\n  Move,\n  PlusCircle,\n  SlidersHorizontal,\n  CloudSun,\n  MapPin,\n  Thermometer,\n  Calendar,\n} from \"lucide-react\";\n\nconst ICON_MAP: Record<string, LucideIcon> = {\n  Palette,\n  Copy,\n  Hash,\n  Highlighter,\n  FoldVertical,\n  FileCode,\n  FileText,\n  Moon,\n  BarChart3,\n  LineChart,\n  MousePointerClick,\n  CircleCheck,\n  CheckCircle2,\n  ListChecks,\n  Settings2,\n  SquareCheck,\n  Paintbrush,\n  Terminal,\n  Timer,\n  AlertCircle,\n  AlertTriangle,\n  Code,\n  FileOutput,\n  Smartphone,\n  ArrowUpDown,\n  Table2,\n  Accessibility,\n  Link,\n  Image,\n  Video,\n  Headphones,\n  Download,\n  Expand,\n  PartyPopper,\n  HelpCircle,\n  Layers,\n  Play,\n  Eye,\n  Maximize2,\n  ListTodo,\n  List,\n  GalleryHorizontalEnd,\n  Keyboard,\n  Globe,\n  ExternalLink,\n  AspectRatio: Ratio,\n  Quote,\n  ShieldCheck,\n  Target,\n  MessageSquare,\n  BadgeCheck,\n  Images,\n  Network,\n  Route,\n  ScanSearch,\n  Rocket,\n  DollarSign,\n  Package,\n  Grid2x2,\n  Move,\n  PlusCircle,\n  SlidersHorizontal,\n  CloudSun,\n  MapPin,\n  Thermometer,\n  Calendar,\n};\n\ninterface FeatureGridProps {\n  children: React.ReactNode;\n  className?: string;\n}\n\nexport function FeatureGrid({ children, className }: FeatureGridProps) {\n  return (\n    <div\n      className={cn(\n        \"not-prose my-6 grid grid-cols-1 gap-4 sm:grid-cols-2\",\n        className,\n      )}\n    >\n      {children}\n    </div>\n  );\n}\n\ninterface FeatureProps {\n  icon: string;\n  title: string;\n  children: React.ReactNode;\n  className?: string;\n}\n\nexport function Feature({ icon, title, children, className }: FeatureProps) {\n  const IconComponent = ICON_MAP[icon] ?? HelpCircle;\n\n  return (\n    <div\n      className={cn(\n        \"border-border/50 bg-muted/50 flex min-h-26 flex-col justify-between gap-3 rounded-xl border p-4\",\n        className,\n      )}\n    >\n      <IconComponent className=\"text-muted-foreground h-5 w-5\" />\n      <div>\n        <div className=\"leading-none font-semibold text-pretty\">{title}</div>\n        <div className=\"text-muted-foreground mt-1.5 text-sm text-pretty\">\n          {children}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/components/mdx/mermaid.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { useTheme } from \"next-themes\";\n\ninterface MermaidProps {\n  chart: string;\n}\n\nlet mermaidPromise: Promise<typeof import(\"mermaid\")> | null = null;\n\nfunction getMermaid() {\n  if (!mermaidPromise) {\n    mermaidPromise = import(\"mermaid\");\n  }\n  return mermaidPromise;\n}\n\nexport function Mermaid({ chart }: MermaidProps) {\n  const id = useId();\n  const { resolvedTheme } = useTheme();\n  const [svg, setSvg] = useState<string>(\"\");\n  const [mounted, setMounted] = useState(false);\n  const renderHostRef = useRef<HTMLDivElement | null>(null);\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  useEffect(() => {\n    if (!mounted) return;\n    const renderHost = renderHostRef.current;\n    if (!renderHost) return;\n\n    const render = async () => {\n      const mermaid = await getMermaid();\n      const isDark = resolvedTheme === \"dark\";\n\n      mermaid.default.initialize({\n        startOnLoad: false,\n        theme: \"base\",\n        securityLevel: \"loose\",\n        themeVariables: {\n          // Background\n          background: isDark ? \"#0a0a0a\" : \"#ffffff\",\n          primaryColor: isDark ? \"#1e293b\" : \"#f1f5f9\",\n\n          // Text\n          primaryTextColor: isDark ? \"#e2e8f0\" : \"#1e293b\",\n          secondaryTextColor: isDark ? \"#94a3b8\" : \"#64748b\",\n\n          // Lines and borders\n          lineColor: isDark ? \"#475569\" : \"#cbd5e1\",\n          primaryBorderColor: isDark ? \"#475569\" : \"#cbd5e1\",\n\n          // Edge labels\n          edgeLabelBackground: isDark ? \"#1e293b\" : \"#f8fafc\",\n          tertiaryTextColor: isDark ? \"#cbd5e1\" : \"#475569\",\n\n          // Font\n          fontFamily: \"ui-sans-serif, system-ui, sans-serif\",\n          fontSize: \"14px\",\n        },\n      });\n\n      const { svg: renderedSvg } = await mermaid.default.render(\n        `mermaid-${id.replace(/:/g, \"\")}`,\n        chart,\n        renderHost,\n      );\n\n      // Post-process SVG to add padding to edge labels\n      const parser = new DOMParser();\n      const doc = parser.parseFromString(renderedSvg, \"image/svg+xml\");\n\n      // Add padding to edge label backgrounds\n      doc.querySelectorAll(\".edgeLabel rect\").forEach((rect) => {\n        const currentX = parseFloat(rect.getAttribute(\"x\") || \"0\");\n        const currentY = parseFloat(rect.getAttribute(\"y\") || \"0\");\n        const currentWidth = parseFloat(rect.getAttribute(\"width\") || \"0\");\n        const currentHeight = parseFloat(rect.getAttribute(\"height\") || \"0\");\n        const padding = 6;\n\n        rect.setAttribute(\"x\", String(currentX - padding));\n        rect.setAttribute(\"y\", String(currentY - padding / 2));\n        rect.setAttribute(\"width\", String(currentWidth + padding * 2));\n        rect.setAttribute(\"height\", String(currentHeight + padding));\n        rect.setAttribute(\"rx\", \"4\");\n      });\n\n      const serializer = new XMLSerializer();\n      setSvg(serializer.serializeToString(doc));\n    };\n\n    render();\n  }, [chart, id, mounted, resolvedTheme]);\n\n  return (\n    <div className=\"relative\">\n      <div\n        ref={renderHostRef}\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute -z-10 h-0 w-0 overflow-hidden opacity-0\"\n      />\n      {!mounted ? (\n        <div className=\"flex items-center justify-center rounded-lg border border-border bg-muted/50 p-8\">\n          <span className=\"text-sm text-muted-foreground\">\n            Loading diagram...\n          </span>\n        </div>\n      ) : (\n        <div\n          className=\"my-4 flex justify-center overflow-x-auto [&_svg]:max-w-full\"\n          dangerouslySetInnerHTML={{ __html: svg }}\n        />\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/components/theme/theme-provider.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ThemeProvider as NextThemesProvider } from \"next-themes\";\n\ntype ThemeProviderProps = React.ComponentProps<typeof NextThemesProvider>;\n\nexport function ThemeProvider({ children, ...props }: ThemeProviderProps) {\n  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;\n}\n"
  },
  {
    "path": "apps/www/app/components/visuals/spinning-hexnut/hexnut-scene.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useRef, useMemo, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { Canvas, useFrame } from \"@react-three/fiber\";\nimport { OrbitControls } from \"@react-three/drei\";\nimport * as THREE from \"three\";\nimport { Mesh, Shape, Path, ExtrudeGeometry } from \"three\";\nimport { useTheme } from \"next-themes\";\n\nfunction createHexnutGeometry(\n  outerRadius = 1,\n  innerRadius = 0.5,\n  height = 0.4,\n) {\n  const shape = new Shape();\n\n  // Create hexagon outer shape\n  for (let i = 0; i < 6; i++) {\n    const angle = (i / 6) * Math.PI * 2;\n    const x = Math.cos(angle) * outerRadius;\n    const y = Math.sin(angle) * outerRadius;\n    if (i === 0) {\n      shape.moveTo(x, y);\n    } else {\n      shape.lineTo(x, y);\n    }\n  }\n  shape.closePath();\n\n  // Create circular hole\n  const hole = new Path();\n  const segments = 32;\n  for (let i = 0; i < segments; i++) {\n    const angle = (i / segments) * Math.PI * 2;\n    const x = Math.cos(angle) * innerRadius;\n    const y = Math.sin(angle) * innerRadius;\n    if (i === 0) {\n      hole.moveTo(x, y);\n    } else {\n      hole.lineTo(x, y);\n    }\n  }\n  hole.closePath();\n  shape.holes.push(hole);\n\n  const geometry = new ExtrudeGeometry(shape, {\n    depth: height,\n    bevelEnabled: false,\n  });\n\n  // Center the geometry so it rotates around its true center\n  geometry.center();\n\n  return geometry;\n}\n\ninterface HexnutProps {\n  color?: string;\n  scale?: number;\n  initialRotation?: [number, number, number];\n  rotationSpeed?: number;\n  dragState: React.RefObject<{\n    isDragging: boolean;\n    deltaX: number;\n    velocity: number;\n  }>;\n}\n\nconst DRAG_SENSITIVITY = 0.01;\nconst MOMENTUM_FRICTION = 0.96;\nconst MAX_VELOCITY = 10;\n\ntype ThemeConfig = {\n  lightX: number;\n  lightY: number;\n  lightZ: number;\n  intensity: number;\n  rotX: number;\n  rotY: number;\n  scale: number;\n  speed: number;\n  cameraZ: number;\n};\n\nconst THEME_CONFIGS: Record<\"dark\" | \"light\", ThemeConfig> = {\n  dark: {\n    lightX: -50,\n    lightY: -50,\n    lightZ: -31,\n    intensity: 4,\n    rotX: -0.7,\n    rotY: 0.72,\n    scale: 2.1,\n    speed: 0.15,\n    cameraZ: 6.8,\n  },\n  light: {\n    lightX: 50,\n    lightY: 50,\n    lightZ: 50,\n    intensity: 50,\n    rotX: -0.7,\n    rotY: 0.72,\n    scale: 2.1,\n    speed: 0.15,\n    cameraZ: 6.8,\n  },\n};\n\nfunction RotatingHexnut({\n  color = \"#ffffff\",\n  scale = 2.1,\n  initialRotation = [0.75, -0.75, 0],\n  rotationSpeed = 0.1,\n  dragState,\n}: HexnutProps) {\n  const meshRef = useRef<Mesh>(null);\n  const geometry = useMemo(() => createHexnutGeometry(1.2, 0.6, 0.5), []);\n  const velocity = useRef(rotationSpeed);\n  const wasDragging = useRef(false);\n\n  useFrame((_, delta) => {\n    if (!meshRef.current) return;\n\n    if (dragState.current.isDragging) {\n      const dragDelta = dragState.current.deltaX * DRAG_SENSITIVITY;\n      meshRef.current.rotation.z += dragDelta;\n      dragState.current.deltaX = 0;\n      wasDragging.current = true;\n    } else {\n      if (wasDragging.current) {\n        const rawVelocity = dragState.current.velocity;\n        const sign = rawVelocity >= 0 ? 1 : -1;\n        velocity.current = sign * Math.min(Math.abs(rawVelocity), MAX_VELOCITY);\n        wasDragging.current = false;\n      }\n\n      const absVelocity = Math.abs(velocity.current);\n      const absTarget = Math.abs(rotationSpeed);\n\n      if (absVelocity > absTarget) {\n        velocity.current *= MOMENTUM_FRICTION;\n\n        if (absVelocity <= absTarget) {\n          velocity.current = rotationSpeed;\n        }\n      } else {\n        velocity.current = rotationSpeed;\n      }\n\n      meshRef.current.rotation.z += delta * velocity.current;\n    }\n  });\n\n  return (\n    <mesh\n      ref={meshRef}\n      geometry={geometry}\n      scale={scale}\n      rotation={initialRotation}\n    >\n      <meshStandardMaterial color={color} />\n    </mesh>\n  );\n}\n\ninterface PostFXOptions {\n  scanlines?: boolean;\n  scanlineOpacity?: number;\n  vignette?: boolean;\n  vignetteIntensity?: number;\n  noise?: boolean;\n  noiseOpacity?: number;\n}\n\ninterface HexnutSceneProps {\n  width?: string | number;\n  height?: string | number;\n  className?: string;\n  postfx?: PostFXOptions;\n  debug?: boolean;\n}\n\nexport function HexnutScene({\n  width = \"100%\",\n  height = \"100%\",\n  className,\n  postfx = {},\n  debug = false,\n}: HexnutSceneProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [mounted, setMounted] = useState(false);\n  const { resolvedTheme } = useTheme();\n\n  const dragState = useRef({ isDragging: false, deltaX: 0, velocity: 0 });\n  const previousX = useRef(0);\n  const lastMoveTime = useRef(0);\n\n  const handlePointerDown = (e: React.PointerEvent) => {\n    dragState.current.isDragging = true;\n    dragState.current.velocity = 0;\n    previousX.current = e.clientX;\n    lastMoveTime.current = performance.now();\n    containerRef.current?.setPointerCapture(e.pointerId);\n  };\n\n  const handlePointerUp = (e: React.PointerEvent) => {\n    dragState.current.isDragging = false;\n    containerRef.current?.releasePointerCapture(e.pointerId);\n  };\n\n  const handlePointerMove = (e: React.PointerEvent) => {\n    if (!dragState.current.isDragging) return;\n    const now = performance.now();\n    const dt = (now - lastMoveTime.current) / 1000;\n    const dx = e.clientX - previousX.current;\n\n    dragState.current.deltaX = dx;\n    if (dt > 0) {\n      dragState.current.velocity = (dx * DRAG_SENSITIVITY) / dt;\n    }\n\n    previousX.current = e.clientX;\n    lastMoveTime.current = now;\n  };\n\n  const isDark = resolvedTheme === \"dark\";\n  const currentConfig = isDark ? THEME_CONFIGS.dark : THEME_CONFIGS.light;\n\n  // Debug state - initialized from theme config\n  const [panelOpen, setPanelOpen] = useState(true);\n  const [lightX, setLightX] = useState(currentConfig.lightX);\n  const [lightY, setLightY] = useState(currentConfig.lightY);\n  const [lightZ, setLightZ] = useState(currentConfig.lightZ);\n  const [intensity, setIntensity] = useState(currentConfig.intensity);\n  const [rotX, setRotX] = useState(currentConfig.rotX);\n  const [rotY, setRotY] = useState(currentConfig.rotY);\n  const [hexScale, setHexScale] = useState(currentConfig.scale);\n  const [speed, setSpeed] = useState(currentConfig.speed);\n  const [cameraZ, setCameraZ] = useState(currentConfig.cameraZ);\n\n  // Sync state with theme when theme changes\n  useEffect(() => {\n    const config = isDark ? THEME_CONFIGS.dark : THEME_CONFIGS.light;\n    setLightX(config.lightX);\n    setLightY(config.lightY);\n    setLightZ(config.lightZ);\n    setIntensity(config.intensity);\n    setRotX(config.rotX);\n    setRotY(config.rotY);\n    setHexScale(config.scale);\n    setSpeed(config.speed);\n    setCameraZ(config.cameraZ);\n  }, [isDark]);\n\n  // Always use state values (synced with theme or manually adjusted)\n  const lightPosition: [number, number, number] = [lightX, lightY, lightZ];\n  const lightIntensity = intensity;\n  const initialRotation: [number, number, number] = [rotX, rotY, 0];\n\n  const {\n    scanlines = false,\n    scanlineOpacity = 0.1,\n    vignette = false,\n    vignetteIntensity = 0,\n    noise = false,\n    noiseOpacity = 0.05,\n  } = postfx;\n\n  const [visible, setVisible] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  // Called when WebGL context is fully initialized\n  const handleCreated = ({ gl }: { gl: THREE.WebGLRenderer }) => {\n    // Ensure transparent clear color from the start\n    gl.setClearColor(0x000000, 0);\n    // Now safe to fade in\n    setVisible(true);\n  };\n\n  if (!mounted) {\n    return (\n      <div ref={containerRef} className={className} style={{ width, height }} />\n    );\n  }\n\n  return (\n    <div\n      ref={containerRef}\n      className={`hexnut-scene ${className || \"\"}`}\n      style={{\n        width,\n        height,\n        position: \"relative\",\n        overflow: \"hidden\",\n        opacity: visible ? 1 : 0,\n        transition: \"opacity 0.6s ease-in-out\",\n        cursor: dragState.current.isDragging ? \"grabbing\" : \"grab\",\n        touchAction: \"none\",\n      }}\n      onPointerDown={handlePointerDown}\n      onPointerUp={handlePointerUp}\n      onPointerLeave={handlePointerUp}\n      onPointerMove={handlePointerMove}\n    >\n      <Canvas\n        camera={{ position: [0, 0, cameraZ], fov: 50 }}\n        gl={{ alpha: true, antialias: true }}\n        style={{ background: \"transparent\" }}\n        onCreated={handleCreated}\n      >\n        <directionalLight position={lightPosition} intensity={lightIntensity} />\n\n        <RotatingHexnut\n          scale={hexScale}\n          initialRotation={initialRotation}\n          rotationSpeed={speed}\n          dragState={dragState}\n        />\n        <OrbitControls enableDamping enableZoom={debug} enableRotate={false} />\n      </Canvas>\n\n      {/* Debug Panel - portaled to body to escape stacking contexts */}\n      {debug &&\n        createPortal(\n          <div\n            style={{\n              position: \"fixed\",\n              bottom: 16,\n              right: 16,\n              background: \"rgba(0,0,0,0.9)\",\n              color: \"#fff\",\n              padding: 8,\n              borderRadius: 6,\n              fontSize: 10,\n              fontFamily: \"monospace\",\n              zIndex: 2147483647,\n              maxWidth: 180,\n            }}\n          >\n            <button\n              onClick={() => setPanelOpen(!panelOpen)}\n              style={{\n                background: \"none\",\n                border: \"none\",\n                color: \"#fff\",\n                cursor: \"pointer\",\n                fontFamily: \"monospace\",\n                fontSize: 10,\n                padding: 0,\n                marginBottom: panelOpen ? 6 : 0,\n              }}\n            >\n              {panelOpen ? \"▼ Debug\" : \"▶ Debug\"}\n            </button>\n\n            {panelOpen && (\n              <>\n                <label style={{ display: \"block\", marginBottom: 4 }}>\n                  LightX: {lightX}\n                  <input\n                    type=\"range\"\n                    min={-50}\n                    max={50}\n                    step={1}\n                    value={lightX}\n                    onChange={(e) => setLightX(Number(e.target.value))}\n                    style={{ width: \"100%\", height: 12 }}\n                  />\n                </label>\n                <label style={{ display: \"block\", marginBottom: 4 }}>\n                  LightY: {lightY}\n                  <input\n                    type=\"range\"\n                    min={-50}\n                    max={50}\n                    step={1}\n                    value={lightY}\n                    onChange={(e) => setLightY(Number(e.target.value))}\n                    style={{ width: \"100%\", height: 12 }}\n                  />\n                </label>\n                <label style={{ display: \"block\", marginBottom: 4 }}>\n                  LightZ: {lightZ}\n                  <input\n                    type=\"range\"\n                    min={-50}\n                    max={50}\n                    step={1}\n                    value={lightZ}\n                    onChange={(e) => setLightZ(Number(e.target.value))}\n                    style={{ width: \"100%\", height: 12 }}\n                  />\n                </label>\n                <label style={{ display: \"block\", marginBottom: 4 }}>\n                  Intensity: {intensity}\n                  <input\n                    type=\"range\"\n                    min={0}\n                    max={50}\n                    step={0.5}\n                    value={intensity}\n                    onChange={(e) => setIntensity(Number(e.target.value))}\n                    style={{ width: \"100%\", height: 12 }}\n                  />\n                </label>\n                <label style={{ display: \"block\", marginBottom: 4 }}>\n                  RotX: {rotX.toFixed(2)}\n                  <input\n                    type=\"range\"\n                    min={-3.14}\n                    max={3.14}\n                    step={0.01}\n                    value={rotX}\n                    onChange={(e) => setRotX(Number(e.target.value))}\n                    style={{ width: \"100%\", height: 12 }}\n                  />\n                </label>\n                <label style={{ display: \"block\", marginBottom: 4 }}>\n                  RotY: {rotY.toFixed(2)}\n                  <input\n                    type=\"range\"\n                    min={-3.14}\n                    max={3.14}\n                    step={0.01}\n                    value={rotY}\n                    onChange={(e) => setRotY(Number(e.target.value))}\n                    style={{ width: \"100%\", height: 12 }}\n                  />\n                </label>\n                <label style={{ display: \"block\", marginBottom: 4 }}>\n                  Scale: {hexScale.toFixed(1)}\n                  <input\n                    type=\"range\"\n                    min={0.5}\n                    max={5}\n                    step={0.1}\n                    value={hexScale}\n                    onChange={(e) => setHexScale(Number(e.target.value))}\n                    style={{ width: \"100%\", height: 12 }}\n                  />\n                </label>\n                <label style={{ display: \"block\", marginBottom: 4 }}>\n                  Speed: {speed.toFixed(2)}\n                  <input\n                    type=\"range\"\n                    min={0}\n                    max={1}\n                    step={0.01}\n                    value={speed}\n                    onChange={(e) => setSpeed(Number(e.target.value))}\n                    style={{ width: \"100%\", height: 12 }}\n                  />\n                </label>\n                <label style={{ display: \"block\", marginBottom: 4 }}>\n                  CamZ: {cameraZ.toFixed(1)}\n                  <input\n                    type=\"range\"\n                    min={2}\n                    max={15}\n                    step={0.1}\n                    value={cameraZ}\n                    onChange={(e) => setCameraZ(Number(e.target.value))}\n                    style={{ width: \"100%\", height: 12 }}\n                  />\n                </label>\n                <div\n                  style={{\n                    marginTop: 6,\n                    padding: 4,\n                    background: \"rgba(255,255,255,0.1)\",\n                    borderRadius: 3,\n                    fontSize: 9,\n                    lineHeight: 1.4,\n                  }}\n                >\n                  light: [{lightX}, {lightY}, {lightZ}]<br />\n                  int: {intensity} | rot: [{rotX.toFixed(2)}, {rotY.toFixed(2)}]\n                  <br />\n                  scale: {hexScale.toFixed(1)} | spd: {speed.toFixed(2)} | cam:{\" \"}\n                  {cameraZ.toFixed(1)}\n                </div>\n              </>\n            )}\n          </div>,\n          document.body,\n        )}\n\n      {/* Scanlines overlay */}\n      {scanlines && (\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            pointerEvents: \"none\",\n            background: `repeating-linear-gradient(\n              0deg,\n              transparent,\n              transparent 2px,\n              rgba(255, 255, 255, ${scanlineOpacity}) 2px,\n              rgba(255, 255, 255, ${scanlineOpacity}) 4px\n            )`,\n            zIndex: 10,\n          }}\n        />\n      )}\n\n      {/* Vignette overlay */}\n      {vignette && (\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            pointerEvents: \"none\",\n            background: `radial-gradient(\n              ellipse at center,\n              transparent 0%,\n              transparent 40%,\n              rgba(0, 0, 0, ${vignetteIntensity}) 100%\n            )`,\n            zIndex: 11,\n          }}\n        />\n      )}\n\n      {/* Noise overlay */}\n      {noise && (\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            pointerEvents: \"none\",\n            opacity: noiseOpacity,\n            backgroundImage: `url(\"data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E\")`,\n            zIndex: 12,\n          }}\n        />\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/chat-context-preview.tsx",
    "content": "import type { ReactNode } from \"react\";\nimport { cn } from \"@/lib/ui/cn\";\n\ninterface ChatBubbleProps {\n  role: \"user\" | \"assistant\";\n  children: ReactNode;\n}\n\nfunction ChatBubble({ role, children }: ChatBubbleProps) {\n  const isUser = role === \"user\";\n\n  return (\n    <div\n      className={cn(\"flex w-full\", isUser ? \"justify-end\" : \"justify-start\")}\n    >\n      <div\n        className={cn(\n          \"max-w-[85%] text-base leading-relaxed\",\n          isUser &&\n            \"rounded-2xl bg-blue-600 px-4 py-2.5 text-white dark:bg-blue-700\",\n          !isUser && \"text-foreground\",\n        )}\n      >\n        {children}\n      </div>\n    </div>\n  );\n}\n\ninterface ChatContextPreviewProps {\n  userMessage: string;\n  preamble?: string;\n  children: ReactNode;\n}\n\nexport function ChatContextPreview({\n  userMessage,\n  preamble,\n  children,\n}: ChatContextPreviewProps) {\n  return (\n    <div className=\"flex flex-col gap-4\">\n      <ChatBubble role=\"user\">{userMessage}</ChatBubble>\n      <div className=\"flex flex-col gap-3\">\n        {preamble && (\n          <ChatBubble role=\"assistant\">\n            <span>{preamble}</span>\n          </ChatBubble>\n        )}\n        <div className=\"flex w-full justify-start\">{children}</div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/collaboration-diagram.tsx",
    "content": "\"use client\";\n\nexport function CollaborationDiagram() {\n  return (\n    <svg\n      viewBox=\"0 0 600 200\"\n      className=\"mx-auto w-full max-w-2xl\"\n      aria-label=\"Collaboration model: User controls Tool UI, Tool UI mediates with Assistant, Assistant narrates to User\"\n    >\n      <defs>\n        <marker\n          id=\"arrow-blue\"\n          markerWidth=\"8\"\n          markerHeight=\"8\"\n          refX=\"7\"\n          refY=\"4\"\n          orient=\"auto\"\n        >\n          <path d=\"M0,0 L8,4 L0,8 L2,4 Z\" fill=\"#3b82f6\" />\n        </marker>\n        <marker\n          id=\"arrow-green\"\n          markerWidth=\"8\"\n          markerHeight=\"8\"\n          refX=\"7\"\n          refY=\"4\"\n          orient=\"auto\"\n        >\n          <path d=\"M0,0 L8,4 L0,8 L2,4 Z\" fill=\"#10b981\" />\n        </marker>\n        <marker\n          id=\"arrow-purple\"\n          markerWidth=\"8\"\n          markerHeight=\"8\"\n          refX=\"7\"\n          refY=\"4\"\n          orient=\"auto\"\n        >\n          <path d=\"M0,0 L8,4 L0,8 L2,4 Z\" fill=\"#8b5cf6\" />\n        </marker>\n      </defs>\n\n      {/* User circle */}\n      <circle cx=\"80\" cy=\"100\" r=\"48\" fill=\"#3b82f6\" />\n      <text\n        x=\"80\"\n        y=\"100\"\n        textAnchor=\"middle\"\n        dominantBaseline=\"middle\"\n        fill=\"white\"\n        fontSize=\"16\"\n        fontWeight=\"500\"\n        fontFamily=\"system-ui, sans-serif\"\n      >\n        User\n      </text>\n\n      {/* Tool UI rounded rectangle */}\n      <rect\n        x=\"240\"\n        y=\"60\"\n        width=\"120\"\n        height=\"80\"\n        rx=\"12\"\n        ry=\"12\"\n        fill=\"#10b981\"\n      />\n      <text\n        x=\"300\"\n        y=\"100\"\n        textAnchor=\"middle\"\n        dominantBaseline=\"middle\"\n        fill=\"white\"\n        fontSize=\"16\"\n        fontWeight=\"500\"\n        fontFamily=\"system-ui, sans-serif\"\n      >\n        Tool UI\n      </text>\n\n      {/* Assistant circle */}\n      <circle cx=\"520\" cy=\"100\" r=\"48\" fill=\"#8b5cf6\" />\n      <text\n        x=\"520\"\n        y=\"100\"\n        textAnchor=\"middle\"\n        dominantBaseline=\"middle\"\n        fill=\"white\"\n        fontSize=\"16\"\n        fontWeight=\"500\"\n        fontFamily=\"system-ui, sans-serif\"\n      >\n        Assistant\n      </text>\n\n      {/* User → Surface: \"controls\" (solid) */}\n      <path\n        d=\"M128,100 L232,100\"\n        fill=\"none\"\n        stroke=\"#3b82f6\"\n        strokeWidth=\"2\"\n        markerEnd=\"url(#arrow-blue)\"\n      />\n      <text\n        x=\"180\"\n        y=\"92\"\n        textAnchor=\"middle\"\n        fill=\"#3b82f6\"\n        fontSize=\"12\"\n        fontFamily=\"system-ui, sans-serif\"\n      >\n        controls\n      </text>\n\n      {/* Surface ↔ Assistant: \"mediates\" (solid, bidirectional) */}\n      <path\n        d=\"M360,90 L464,90\"\n        fill=\"none\"\n        stroke=\"#10b981\"\n        strokeWidth=\"2\"\n        markerEnd=\"url(#arrow-green)\"\n      />\n      <path\n        d=\"M472,110 L368,110\"\n        fill=\"none\"\n        stroke=\"#10b981\"\n        strokeWidth=\"2\"\n        markerEnd=\"url(#arrow-green)\"\n      />\n      <text\n        x=\"412\"\n        y=\"82\"\n        textAnchor=\"middle\"\n        fill=\"#10b981\"\n        fontSize=\"12\"\n        fontFamily=\"system-ui, sans-serif\"\n      >\n        mediates\n      </text>\n\n      {/* Assistant → User: \"narrates\" (dashed, curved) */}\n      <path\n        d=\"M480,140 Q300,200 120,140\"\n        fill=\"none\"\n        stroke=\"#8b5cf6\"\n        strokeWidth=\"1.5\"\n        strokeDasharray=\"4,3\"\n        markerEnd=\"url(#arrow-purple)\"\n      />\n      <text\n        x=\"300\"\n        y=\"185\"\n        textAnchor=\"middle\"\n        fill=\"#8b5cf6\"\n        fontSize=\"12\"\n        fontFamily=\"system-ui, sans-serif\"\n      >\n        narrates\n      </text>\n    </svg>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/component-docs-tabs.tsx",
    "content": "\"use client\";\n\nimport dynamic from \"next/dynamic\";\nimport { memo, useCallback, useMemo, useRef, type ReactNode } from \"react\";\nimport { usePathname } from \"next/navigation\";\nimport { cn } from \"@/lib/ui/cn\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport { DocsBorderedShell } from \"./docs-bordered-shell\";\nimport { DocsContent } from \"./docs-content\";\nimport type { ComponentId } from \"@/lib/docs/component-ids\";\nimport { useTabSearchParam } from \"@/hooks/use-tab-search-param\";\nimport { analytics } from \"@/lib/analytics\";\nimport { componentsRegistry } from \"@/lib/docs/component-registry\";\n\ntype DocsTab = \"docs\" | \"examples\";\n\nconst VALID_TABS = [\"docs\", \"examples\"] as const;\n\ninterface ComponentDocsTabsProps {\n  docs: ReactNode;\n  componentId?: ComponentId;\n  examples?: ReactNode;\n}\n\nconst LazyComponentPreview = dynamic(\n  () => import(\"./component-preview\").then((m) => m.ComponentPreview),\n  {\n    loading: () => (\n      <div className=\"text-muted-foreground flex h-full w-full items-center justify-center text-sm\">\n        Loading examples...\n      </div>\n    ),\n  },\n);\n\nexport const ComponentDocsTabs = memo(function ComponentDocsTabs({\n  docs,\n  componentId,\n  examples,\n}: ComponentDocsTabsProps) {\n  const contentRef = useRef<HTMLDivElement>(null);\n  const pathname = usePathname();\n  const componentMeta = useMemo(\n    () => componentsRegistry.find((component) => component.path === pathname),\n    [pathname],\n  );\n  const tabsIdBase = useMemo(\n    () =>\n      componentId\n        ? `docs-tabs-${componentId}`\n        : `docs-tabs-${pathname.replaceAll(\"/\", \"-\") || \"root\"}`,\n    [componentId, pathname],\n  );\n\n  const { activeTab, setActiveTab } = useTabSearchParam<DocsTab>({\n    defaultTab: \"docs\",\n    validTabs: VALID_TABS,\n    scrollTargetRef: contentRef,\n    hashTrigger: \"#examples\",\n  });\n\n  const handleTabChange = useCallback(\n    (value: string) => {\n      if (value === \"docs\" || value === \"examples\") {\n        if (componentMeta && value !== activeTab) {\n          analytics.component.tabSwitched(componentMeta.id, value);\n        }\n        setActiveTab(value);\n      }\n    },\n    [activeTab, componentMeta, setActiveTab],\n  );\n\n  return (\n    <DocsBorderedShell>\n      <Tabs\n        value={activeTab}\n        onValueChange={handleTabChange}\n        className=\"relative flex h-full min-h-0 flex-col gap-0\"\n      >\n        <div\n          className={cn(\n            \"absolute inset-x-0 top-0 z-20 flex items-center justify-center\",\n            \"px-3 py-2 sm:px-6 sm:py-3\",\n          )}\n        >\n          <div\n            className=\"from-background pointer-events-none absolute inset-x-0 top-0 -bottom-4 bg-linear-to-b to-transparent\"\n            aria-hidden=\"true\"\n          />\n          <TabsList className=\"relative\">\n            <TabsTrigger\n              id={`${tabsIdBase}-trigger-docs`}\n              aria-controls={`${tabsIdBase}-content-docs`}\n              value=\"docs\"\n            >\n              Docs\n            </TabsTrigger>\n            <TabsTrigger\n              id={`${tabsIdBase}-trigger-examples`}\n              aria-controls={`${tabsIdBase}-content-examples`}\n              value=\"examples\"\n            >\n              Examples\n            </TabsTrigger>\n          </TabsList>\n        </div>\n\n        <div\n          id=\"examples\"\n          ref={contentRef}\n          className=\"relative flex min-h-0 flex-1 scroll-mt-16 flex-col\"\n        >\n          <TabsContent\n            id={`${tabsIdBase}-content-docs`}\n            aria-labelledby={`${tabsIdBase}-trigger-docs`}\n            value=\"docs\"\n            className=\"scrollbar-subtle h-full min-h-0 flex-1 overflow-y-auto pt-[52px] sm:pt-[60px]\"\n          >\n            <div className=\"z-0 min-h-0 min-w-0 flex-1 px-4 pt-8 pb-24 sm:px-6 lg:px-10 xl:px-12 xl:pt-12\">\n              <DocsContent>{docs}</DocsContent>\n            </div>\n          </TabsContent>\n          <TabsContent\n            id={`${tabsIdBase}-content-examples`}\n            aria-labelledby={`${tabsIdBase}-trigger-examples`}\n            value=\"examples\"\n            className=\"flex h-full min-h-0 flex-1 flex-col overflow-hidden pt-[52px] sm:pt-[60px]\"\n          >\n            {activeTab === \"examples\"\n              ? (examples ??\n                (componentId ? (\n                  <LazyComponentPreview componentId={componentId} />\n                ) : null))\n              : null}\n          </TabsContent>\n        </div>\n      </Tabs>\n    </DocsBorderedShell>\n  );\n});\n"
  },
  {
    "path": "apps/www/app/docs/_components/component-preview-shell.tsx",
    "content": "\"use client\";\n\nimport { memo, useCallback, type ReactNode } from \"react\";\nimport { Panel, PanelGroup, PanelResizeHandle } from \"react-resizable-panels\";\nimport type { ImperativePanelGroupHandle } from \"react-resizable-panels\";\nimport { Check, Code, Copy, Eye, MessageCircle } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { ToggleGroup, ToggleGroupItem } from \"@/components/ui/toggle-group\";\nimport { cn } from \"@/lib/ui/cn\";\nimport { useResponsivePreview } from \"@/hooks/use-responsive-preview\";\nimport { useTabSearchParam } from \"@/hooks/use-tab-search-param\";\nimport { useCopyToClipboard } from \"@/components/tool-ui/shared\";\nimport { InstallCommandBlock } from \"./install-command-block\";\nimport { analytics } from \"@/lib/analytics\";\n\nconst PREVIEW_MIN_WIDTH = 40;\nconst PREVIEW_MAX_WIDTH = 100;\n\nconst VALID_VIEW_MODES = [\"canvas\", \"chat\", \"code\"] as const;\ntype ViewMode = (typeof VALID_VIEW_MODES)[number];\n\nfunction toStablePanelIdSegment(value: string): string {\n  const normalized = value\n    .toLowerCase()\n    .replace(/[^a-z0-9-]+/g, \"-\")\n    .replace(/^-+|-+$/g, \"\");\n  return normalized || \"component\";\n}\n\nfunction ViewModeToggle({\n  value,\n  onValueChange,\n}: {\n  value: ViewMode;\n  onValueChange: (value: ViewMode) => void;\n}) {\n  return (\n    <ToggleGroup\n      type=\"single\"\n      value={value}\n      onValueChange={(v) => v && onValueChange(v as ViewMode)}\n      size=\"sm\"\n      className=\"bg-primary/5 rounded-lg p-[3px]\"\n    >\n      <ToggleGroupItem\n        value=\"canvas\"\n        aria-label=\"View canvas\"\n        className=\"text-muted-foreground hover:text-primary data-[state=on]:bg-background data-[state=on]:text-foreground hover:bg-transparent data-[state=on]:rounded-md data-[state=on]:shadow-sm\"\n      >\n        <Eye className=\"size-4\" />\n      </ToggleGroupItem>\n      <ToggleGroupItem\n        value=\"chat\"\n        aria-label=\"View in chat context\"\n        className=\"text-muted-foreground hover:text-primary data-[state=on]:bg-background data-[state=on]:text-foreground hover:bg-transparent data-[state=on]:rounded-md data-[state=on]:shadow-sm\"\n      >\n        <MessageCircle className=\"size-4\" />\n      </ToggleGroupItem>\n      <ToggleGroupItem\n        value=\"code\"\n        aria-label=\"View code\"\n        className=\"text-muted-foreground hover:text-primary data-[state=on]:bg-background data-[state=on]:text-foreground hover:bg-transparent data-[state=on]:rounded-md data-[state=on]:shadow-sm\"\n      >\n        <Code className=\"size-4\" />\n      </ToggleGroupItem>\n    </ToggleGroup>\n  );\n}\n\nconst RESIZE_HANDLE_STYLES = cn(\n  \"absolute top-1/2 left-1/2 h-12 w-1\",\n  \"-translate-x-1/2 -translate-y-1/2 rounded-full\",\n  \"transition-all duration-200\",\n  \"bg-gray-300 opacity-0\",\n  \"group-hover/canvas:opacity-60\",\n  \"group-data-resize-handle-active/handle:bg-gray-500 group-data-resize-handle-active/handle:opacity-100\",\n  \"dark:bg-gray-500 dark:group-data-resize-handle-active/handle:bg-gray-400\",\n);\n\nconst ResizablePreviewArea = memo(function ResizablePreviewArea({\n  panelGroupRef,\n  handleLayout,\n  panelIdBase,\n  children,\n}: {\n  panelGroupRef: React.RefObject<ImperativePanelGroupHandle | null>;\n  handleLayout: (sizes: number[]) => void;\n  panelIdBase: string;\n  children: ReactNode;\n}) {\n  return (\n    <PanelGroup\n      id={`${panelIdBase}-group`}\n      ref={panelGroupRef}\n      direction=\"horizontal\"\n      onLayout={handleLayout}\n      className=\"group/canvas\"\n    >\n      <Panel id={`${panelIdBase}-left`} defaultSize={7.5} minSize={0} />\n      <PanelResizeHandle\n        id={`${panelIdBase}-left-handle`}\n        className=\"group/handle relative w-4\"\n      >\n        <div className={RESIZE_HANDLE_STYLES} />\n      </PanelResizeHandle>\n      <Panel\n        id={`${panelIdBase}-center`}\n        defaultSize={85}\n        minSize={PREVIEW_MIN_WIDTH}\n        maxSize={PREVIEW_MAX_WIDTH}\n      >\n        <div\n          className={cn(\n            \"scrollbar-subtle relative overflow-x-auto\",\n            \"transition-all\",\n          )}\n        >\n          {children}\n        </div>\n      </Panel>\n      <PanelResizeHandle\n        id={`${panelIdBase}-right-handle`}\n        className=\"group/handle relative w-4\"\n      >\n        <div className={RESIZE_HANDLE_STYLES} />\n      </PanelResizeHandle>\n      <Panel id={`${panelIdBase}-right`} defaultSize={7.5} minSize={0} />\n    </PanelGroup>\n  );\n});\n\ninterface ComponentPreviewShellProps {\n  componentId: string;\n  sidebar: ReactNode;\n  preview: ReactNode;\n  chatPanel: ReactNode;\n  codePanel: ReactNode;\n  code: string;\n}\n\nconst COPY_ID = \"code-panel\";\n\nexport function ComponentPreviewShell({\n  componentId,\n  sidebar,\n  preview,\n  chatPanel,\n  codePanel,\n  code,\n}: ComponentPreviewShellProps) {\n  const { activeTab: viewMode, setActiveTab: setViewMode } =\n    useTabSearchParam<ViewMode>({\n      paramName: \"view\",\n      defaultTab: \"canvas\",\n      validTabs: VALID_VIEW_MODES,\n    });\n  const { copiedId, copy } = useCopyToClipboard();\n  const copied = copiedId === COPY_ID;\n  const panelIdBase = `component-preview-${toStablePanelIdSegment(componentId)}`;\n  const { panelGroupRef, handleLayout } = useResponsivePreview({\n    minWidth: PREVIEW_MIN_WIDTH,\n    maxWidth: PREVIEW_MAX_WIDTH,\n  });\n\n  const handleCopy = useCallback(() => {\n    analytics.component.codeCopied(componentId, \"full\");\n    analytics.code.blockCopied(\"tsx\", \"component_preview\");\n    copy(code, COPY_ID);\n  }, [componentId, code, copy]);\n\n  const handleViewModeChange = useCallback(\n    (nextViewMode: ViewMode) => {\n      if (nextViewMode === viewMode) return;\n\n      analytics.component.tabSwitched(componentId, nextViewMode);\n      analytics.component.previewInteracted(\n        componentId,\n        `view_mode_${nextViewMode}`,\n      );\n      setViewMode(nextViewMode);\n    },\n    [componentId, setViewMode, viewMode],\n  );\n\n  return (\n    <div className=\"flex h-full min-h-0 w-full flex-1 flex-col overflow-clip lg:flex-row\">\n      {/* Desktop sidebar */}\n      <aside\n        className={cn(\n          \"bg-background scrollbar-subtle\",\n          \"hidden h-full w-72 shrink-0 flex-col\",\n          \"overflow-x-hidden overflow-y-auto overscroll-contain\",\n          \"lg:flex\",\n        )}\n      >\n        <div className=\"z-10 flex min-h-0 flex-1 flex-col gap-4 px-3 pb-24\">\n          {sidebar}\n        </div>\n      </aside>\n\n      {/* Main content area */}\n      <div className=\"relative flex min-h-0 min-w-0 flex-1 flex-col\">\n        {/* Install commands - above the gray preview, in main column only */}\n        <div className=\"shrink-0 border-b px-4 py-3 sm:px-6 lg:px-6\">\n          <InstallCommandBlock componentId={componentId} variant=\"block\" />\n        </div>\n\n        {/* Mobile toolbar */}\n        <div className=\"flex flex-col gap-3 border-b px-4 pt-3 pb-3 lg:hidden\">\n          <div className=\"scrollbar-subtle overflow-x-auto\">{sidebar}</div>\n          <div className=\"flex items-center justify-end\">\n            <ViewModeToggle\n              value={viewMode}\n              onValueChange={handleViewModeChange}\n            />\n          </div>\n        </div>\n\n        <div\n          className={cn(\n            \"relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden bg-neutral-100 dark:bg-neutral-950\",\n            \"z-10\",\n            \"border lg:mr-4 lg:mb-4 lg:rounded-lg lg:border-l\",\n          )}\n        >\n          {viewMode === \"canvas\" && (\n            <div\n              className=\"bg-dot-grid pointer-events-none absolute inset-0 z-0 dark:opacity-60\"\n              aria-hidden=\"true\"\n            />\n          )}\n\n          {viewMode === \"code\" && (\n            <div\n              className={cn(\n                \"pointer-events-none absolute top-0 right-12 left-0 z-20 h-20\",\n                \"bg-linear-to-b from-neutral-100 via-neutral-100/80 to-transparent dark:from-neutral-950 dark:via-neutral-950/80\",\n                \"hidden lg:block\",\n              )}\n              aria-hidden=\"true\"\n            />\n          )}\n\n          {/* View mode toggle - top left corner */}\n          <div className=\"absolute top-3 left-3 z-30 hidden lg:block\">\n            <ViewModeToggle\n              value={viewMode}\n              onValueChange={handleViewModeChange}\n            />\n          </div>\n\n          {/* Copy button - top right corner (code view only) */}\n          {viewMode === \"code\" && (\n            <div className=\"absolute top-3 right-3 z-30 hidden lg:block\">\n              <Button\n                variant=\"secondary\"\n                size=\"icon\"\n                onClick={handleCopy}\n                aria-label={copied ? \"Copied\" : \"Copy code\"}\n                title={copied ? \"Copied\" : \"Copy code\"}\n                className=\"size-8 shadow-sm\"\n              >\n                {copied ? (\n                  <Check className=\"size-4 text-green-500\" />\n                ) : (\n                  <Copy className=\"size-4\" />\n                )}\n              </Button>\n            </div>\n          )}\n\n          <div\n            className={cn(\n              \"scrollbar-subtle relative z-10\",\n              \"flex min-h-0 min-w-0 flex-1\",\n              viewMode === \"code\"\n                ? \"flex-col\"\n                : \"items-start justify-center overflow-y-auto\",\n            )}\n          >\n            {viewMode === \"canvas\" && (\n              <div className=\"relative h-fit w-full p-4 pt-12 lg:pt-16\">\n                <ResizablePreviewArea\n                  panelGroupRef={panelGroupRef}\n                  handleLayout={handleLayout}\n                  panelIdBase={panelIdBase}\n                >\n                  {preview}\n                </ResizablePreviewArea>\n              </div>\n            )}\n            {viewMode === \"chat\" && (\n              <div className=\"relative h-fit w-full p-4 pt-12 lg:pt-16\">\n                <div className=\"mx-auto max-w-2xl\">{chatPanel}</div>\n              </div>\n            )}\n            {viewMode === \"code\" && codePanel}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/component-preview.tsx",
    "content": "\"use client\";\n\nimport { useCallback, useState } from \"react\";\nimport { ComponentPreviewShell } from \"./component-preview-shell\";\nimport { ChatContextPreview } from \"./chat-context-preview\";\nimport { PresetSelector } from \"./preset-selector\";\nimport { getImportLine } from \"@/lib/docs/gallery-usage-code\";\nimport {\n  type ComponentId,\n  getPreviewConfig,\n  type PreviewState,\n} from \"@/lib/docs/preview-config\";\nimport { usePresetParam } from \"@/hooks/use-preset-param\";\nimport { TrackedDynamicCodeBlock } from \"./tracked-dynamic-codeblock\";\n\ninterface ComponentPreviewProps {\n  componentId: ComponentId;\n}\n\nconst EMPTY_STATE: PreviewState = {};\n\nexport function ComponentPreview({ componentId }: ComponentPreviewProps) {\n  const config = getPreviewConfig(componentId);\n\n  const { currentPreset, setPreset } = usePresetParam({\n    presets: config.presets,\n    defaultPreset: config.defaultPreset,\n  });\n\n  const [state, setState] = useState<PreviewState>(EMPTY_STATE);\n  const [prevPreset, setPrevPreset] = useState(currentPreset);\n\n  if (currentPreset !== prevPreset) {\n    setPrevPreset(currentPreset);\n    setState(EMPTY_STATE);\n  }\n\n  const handleSelectPreset = useCallback(\n    (preset: unknown) => {\n      setPreset(preset as string);\n      setState(EMPTY_STATE);\n    },\n    [setPreset],\n  );\n\n  const handleSetState = useCallback((newState: Partial<PreviewState>) => {\n    setState((prev) => ({ ...prev, ...newState }));\n  }, []);\n\n  const preset =\n    config.presets[currentPreset] ?? config.presets[config.defaultPreset];\n  const selectedPreset = config.presets[currentPreset] ?? preset;\n  const body = selectedPreset.generateExampleCode(selectedPreset.data);\n  const code = `${getImportLine(componentId)}\\n\\n${body}`;\n\n  const previewContent = config.renderComponent({\n    data: preset.data,\n    presetName: currentPreset,\n    state,\n    setState: handleSetState,\n  });\n\n  const wrappedPreview = config.wrapper ? (\n    <config.wrapper>{previewContent}</config.wrapper>\n  ) : (\n    previewContent\n  );\n\n  const displayPreview = wrappedPreview;\n\n  const chatPanel = (\n    <ChatContextPreview\n      userMessage={config.chatContext.userMessage}\n      preamble={config.chatContext.preamble}\n    >\n      {displayPreview}\n    </ChatContextPreview>\n  );\n\n  return (\n    <ComponentPreviewShell\n      componentId={componentId}\n      sidebar={\n        <PresetSelector\n          componentId={componentId}\n          currentPreset={currentPreset}\n          onSelectPreset={handleSelectPreset}\n        />\n      }\n      preview={displayPreview}\n      chatPanel={chatPanel}\n      codePanel={\n        <div className=\"code-panel-fullbleed scrollbar-subtle\">\n          <TrackedDynamicCodeBlock\n            lang=\"tsx\"\n            code={code}\n            copyButtonLabel=\"component example code\"\n          />\n        </div>\n      }\n      code={code}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/component-previews/social-post-preview.tsx",
    "content": "\"use client\";\n\nimport { useCallback, useMemo, useState, type ReactNode } from \"react\";\nimport { useSearchParams, usePathname, useRouter } from \"next/navigation\";\nimport { ComponentPreviewShell } from \"../component-preview-shell\";\nimport { ChatContextPreview } from \"../chat-context-preview\";\nimport { XPost } from \"@/components/tool-ui/x-post\";\nimport { InstagramPost } from \"@/components/tool-ui/instagram-post\";\nimport { LinkedInPost } from \"@/components/tool-ui/linkedin-post\";\nimport { ToolUI, type Action } from \"@/components/tool-ui/shared\";\nimport { xPostPresets, type XPostPresetName } from \"@/lib/presets/x-post\";\nimport {\n  instagramPostPresets,\n  type InstagramPostPresetName,\n} from \"@/lib/presets/instagram-post\";\nimport {\n  linkedInPostPresets,\n  type LinkedInPostPresetName,\n} from \"@/lib/presets/linkedin-post\";\nimport {\n  Item,\n  ItemContent,\n  ItemDescription,\n  ItemGroup,\n  ItemTitle,\n} from \"@/components/ui/item\";\nimport { Tabs, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport { cn } from \"@/lib/ui/cn\";\n\ntype Platform = \"x\" | \"instagram\" | \"linkedin\";\ntype PresetName =\n  | XPostPresetName\n  | InstagramPostPresetName\n  | LinkedInPostPresetName;\n\nconst VALID_PLATFORMS: readonly Platform[] = [\"x\", \"instagram\", \"linkedin\"];\n\nfunction resolveActions(actions: unknown): Action[] | null {\n  if (Array.isArray(actions)) return actions as Action[];\n\n  if (typeof actions === \"object\" && actions !== null) {\n    const items = (actions as { items?: unknown }).items;\n    if (Array.isArray(items)) return items as Action[];\n  }\n\n  return null;\n}\n\nconst platformConfig = {\n  x: {\n    label: \"X (Twitter)\",\n    presets: xPostPresets,\n    presetNames: Object.keys(xPostPresets) as XPostPresetName[],\n  },\n  instagram: {\n    label: \"Instagram\",\n    presets: instagramPostPresets,\n    presetNames: Object.keys(instagramPostPresets) as InstagramPostPresetName[],\n  },\n  linkedin: {\n    label: \"LinkedIn\",\n    presets: linkedInPostPresets,\n    presetNames: Object.keys(linkedInPostPresets) as LinkedInPostPresetName[],\n  },\n} as const;\n\nfunction getPresetNames(platform: Platform): PresetName[] {\n  return [...platformConfig[platform].presetNames] as PresetName[];\n}\n\nfunction getValidPreset(platform: Platform, preset: string | null): PresetName {\n  const presetNames = getPresetNames(platform);\n  if (preset && (presetNames as readonly string[]).includes(preset)) {\n    return preset as PresetName;\n  }\n  return presetNames[0];\n}\n\nfunction PlatformSelector({\n  currentPlatform,\n  onSelectPlatform,\n}: {\n  currentPlatform: Platform;\n  onSelectPlatform: (platform: Platform) => void;\n}) {\n  return (\n    <div className=\"mb-4\">\n      <div className=\"text-muted-foreground mb-2 text-xs font-medium tracking-wide uppercase\">\n        Platform\n      </div>\n      <Tabs\n        value={currentPlatform}\n        onValueChange={(value) => onSelectPlatform(value as Platform)}\n      >\n        <TabsList className=\"w-full bg-primary/5\">\n          <TabsTrigger value=\"x\" className=\"flex-1\">\n            X\n          </TabsTrigger>\n          <TabsTrigger value=\"instagram\" className=\"flex-1\">\n            Instagram\n          </TabsTrigger>\n          <TabsTrigger value=\"linkedin\" className=\"flex-1\">\n            LinkedIn\n          </TabsTrigger>\n        </TabsList>\n      </Tabs>\n    </div>\n  );\n}\n\nfunction PresetSelector({\n  platform,\n  currentPreset,\n  onSelectPreset,\n}: {\n  platform: Platform;\n  currentPreset: PresetName;\n  onSelectPreset: (preset: PresetName) => void;\n}) {\n  const config = platformConfig[platform];\n  const presets = config.presets as Record<\n    string,\n    { description: string; data: unknown }\n  >;\n  const presetNames = getPresetNames(platform);\n\n  return (\n    <ItemGroup className=\"gap-1\">\n      {presetNames.map((preset) => (\n        <Item\n          key={preset}\n          variant=\"default\"\n          size=\"sm\"\n          data-selected={currentPreset === preset}\n          className={cn(\n            \"group/item relative py-[2px] lg:py-3!\",\n            currentPreset === preset\n              ? \"bg-muted cursor-pointer border-transparent shadow-xs\"\n              : \"hover:bg-primary/5 active:bg-primary/10 cursor-pointer transition-[colors,shadow,border,background] duration-150 ease-out\",\n          )}\n          onClick={() => onSelectPreset(preset as PresetName)}\n        >\n          <ItemContent className=\"transform-gpu transition-transform duration-300 ease-[cubic-bezier(0.3,-0.55,0.27,1.55)] will-change-transform group-active/item:scale-[0.98] group-active/item:duration-100 group-active/item:ease-out\">\n            <div className=\"relative flex items-start justify-between\">\n              <div className=\"flex flex-1 flex-col gap-0 lg:gap-1\">\n                <ItemTitle className=\"flex w-full items-center justify-between capitalize\">\n                  <span className=\"text-foreground\">\n                    {preset.replace(\"-\", \" \").replace(\"_\", \" \")}\n                  </span>\n                </ItemTitle>\n                <ItemDescription className=\"text-sm font-light\">\n                  {presets[preset].description}\n                </ItemDescription>\n              </div>\n            </div>\n            <span\n              aria-hidden=\"true\"\n              data-selected={currentPreset === preset}\n              className=\"bg-foreground absolute top-2.5 -left-4.5 h-5 w-1 origin-center -translate-y-1/2 scale-y-0 transform-gpu rounded-full opacity-0 transition-[opacity,transform] delay-100 duration-200 ease-out data-[selected=true]:scale-y-100 data-[selected=true]:opacity-100\"\n            />\n          </ItemContent>\n        </Item>\n      ))}\n    </ItemGroup>\n  );\n}\n\nexport function SocialPostPreview({\n  platformLock,\n}: {\n  platformLock?: Platform;\n}) {\n  const router = useRouter();\n  const pathname = usePathname();\n  const searchParams = useSearchParams();\n  const lockedPlatform = platformLock;\n  const isPlatformLocked = Boolean(lockedPlatform);\n\n  const platformParam = searchParams.get(\"platform\");\n  const presetParam = searchParams.get(\"preset\");\n\n  const initialPlatform = useMemo((): Platform => {\n    if (lockedPlatform) return lockedPlatform;\n    if (platformParam && VALID_PLATFORMS.includes(platformParam as Platform)) {\n      return platformParam as Platform;\n    }\n    return \"x\";\n  }, [lockedPlatform, platformParam]);\n\n  const initialPreset = useMemo(\n    () => getValidPreset(initialPlatform, presetParam),\n    [initialPlatform, presetParam],\n  );\n\n  const [currentPlatform, setCurrentPlatform] =\n    useState<Platform>(initialPlatform);\n  const [currentPreset, setCurrentPreset] = useState<PresetName>(initialPreset);\n\n  const [prevPlatformParam, setPrevPlatformParam] = useState(platformParam);\n  const [prevPresetParam, setPrevPresetParam] = useState(presetParam);\n  const [prevLockedPlatform, setPrevLockedPlatform] = useState(lockedPlatform);\n\n  if (\n    platformParam !== prevPlatformParam ||\n    presetParam !== prevPresetParam ||\n    lockedPlatform !== prevLockedPlatform\n  ) {\n    setPrevPlatformParam(platformParam);\n    setPrevPresetParam(presetParam);\n    setPrevLockedPlatform(lockedPlatform);\n\n    const nextPlatform = lockedPlatform\n      ? lockedPlatform\n      : platformParam && VALID_PLATFORMS.includes(platformParam as Platform)\n        ? (platformParam as Platform)\n        : currentPlatform;\n\n    setCurrentPlatform(nextPlatform);\n    setCurrentPreset(getValidPreset(nextPlatform, presetParam));\n  }\n\n  const updateUrl = useCallback(\n    (platform: Platform, preset: PresetName) => {\n      const params = new URLSearchParams(searchParams.toString());\n\n      if (isPlatformLocked) {\n        params.delete(\"platform\");\n      } else {\n        params.set(\"platform\", platform);\n      }\n      params.set(\"preset\", preset);\n      router.push(`${pathname}?${params.toString()}`, { scroll: false });\n    },\n    [isPlatformLocked, pathname, router, searchParams],\n  );\n\n  const handleSelectPlatform = useCallback(\n    (platform: Platform) => {\n      if (isPlatformLocked) return;\n      const defaultPreset = platformConfig[platform].presetNames[0];\n      setCurrentPlatform(platform);\n      setCurrentPreset(defaultPreset);\n      updateUrl(platform, defaultPreset);\n    },\n    [isPlatformLocked, updateUrl],\n  );\n\n  const handleSelectPreset = useCallback(\n    (preset: PresetName) => {\n      setCurrentPreset(preset);\n      updateUrl(currentPlatform, preset);\n    },\n    [currentPlatform, updateUrl],\n  );\n\n  const effectivePreset = currentPreset as\n    | XPostPresetName\n    | InstagramPostPresetName\n    | LinkedInPostPresetName;\n\n  const renderWithLocalActions = (\n    id: string,\n    surface: ReactNode,\n    actions: Action[] | null,\n  ) => {\n    if (!actions || actions.length === 0) {\n      return <div className=\"flex flex-col gap-3\">{surface}</div>;\n    }\n\n    return (\n      <ToolUI id={id}>\n        <div className=\"flex flex-col gap-3\">\n          <ToolUI.Surface>{surface}</ToolUI.Surface>\n          <ToolUI.Actions>\n            <ToolUI.LocalActions\n              actions={actions}\n              onAction={(actionId) => alert(`Local action: ${actionId}`)}\n            />\n          </ToolUI.Actions>\n        </div>\n      </ToolUI>\n    );\n  };\n\n  const renderedPost =\n    currentPlatform === \"x\"\n      ? renderWithLocalActions(\n          xPostPresets[effectivePreset as XPostPresetName].data.post.id,\n          <XPost\n            post={xPostPresets[effectivePreset as XPostPresetName].data.post}\n            onAction={(action, post) =>\n              console.log(\"X action:\", action, post.id)\n            }\n          />,\n          resolveActions(\n            xPostPresets[effectivePreset as XPostPresetName].data.localActions,\n          ),\n        )\n      : currentPlatform === \"instagram\"\n        ? renderWithLocalActions(\n            instagramPostPresets[effectivePreset as InstagramPostPresetName]\n              .data.post.id,\n            <InstagramPost\n              post={\n                instagramPostPresets[effectivePreset as InstagramPostPresetName]\n                  .data.post\n              }\n              onAction={(action, post) =>\n                console.log(\"Instagram action:\", action, post.id)\n              }\n            />,\n            resolveActions(\n              instagramPostPresets[effectivePreset as InstagramPostPresetName]\n                .data.localActions,\n            ),\n          )\n        : renderWithLocalActions(\n            linkedInPostPresets[effectivePreset as LinkedInPostPresetName].data\n              .post.id,\n            <LinkedInPost\n              post={\n                linkedInPostPresets[effectivePreset as LinkedInPostPresetName]\n                  .data.post\n              }\n              onAction={(action, post) =>\n                console.log(\"LinkedIn action:\", action, post.id)\n              }\n            />,\n            resolveActions(\n              linkedInPostPresets[effectivePreset as LinkedInPostPresetName]\n                .data.localActions,\n            ),\n          );\n\n  const previewContent = (\n    <div className=\"mx-auto w-full max-w-[500px]\">{renderedPost}</div>\n  );\n\n  const chatPanel = (\n    <ChatContextPreview\n      userMessage=\"What are the trending posts about AI on social media right now?\"\n      preamble=\"Here's a popular post I found:\"\n    >\n      {previewContent}\n    </ChatContextPreview>\n  );\n\n  return (\n    <ComponentPreviewShell\n      componentId={`${currentPlatform}-post`}\n      sidebar={\n        <>\n          {!isPlatformLocked && (\n            <PlatformSelector\n              currentPlatform={currentPlatform}\n              onSelectPlatform={handleSelectPlatform}\n            />\n          )}\n          <PresetSelector\n            platform={currentPlatform}\n            currentPreset={currentPreset}\n            onSelectPreset={handleSelectPreset}\n          />\n        </>\n      }\n      preview={previewContent}\n      chatPanel={chatPanel}\n      codePanel={\n        <div className=\"text-muted-foreground flex h-full items-center justify-center\">\n          Code panel coming soon\n        </div>\n      }\n      code=\"\"\n    />\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/copy-markdown-button.tsx",
    "content": "\"use client\";\n\nimport type { MouseEventHandler } from \"react\";\nimport { useCopyButton } from \"fumadocs-ui/utils/use-copy-button\";\nimport { Button } from \"@/components/ui/button\";\nimport { Check, Copy as CopyIcon } from \"lucide-react\";\nimport { analytics } from \"@/lib/analytics\";\n\ntype CopyMarkdownButtonProps = {\n  markdown: string;\n};\n\nexport function CopyMarkdownButton({ markdown }: CopyMarkdownButtonProps) {\n  const [checked, copyMarkdown] = useCopyButton(async () => {\n    await navigator.clipboard.writeText(markdown);\n  });\n\n  const onClick: MouseEventHandler<HTMLButtonElement> = (event) => {\n    analytics.code.blockCopied(\"markdown\", \"docs_header\");\n    copyMarkdown(event);\n  };\n\n  return (\n    <Button\n      type=\"button\"\n      variant=\"outline\"\n      size=\"sm\"\n      onClick={onClick}\n      aria-label=\"Copy page\"\n      className=\"gap-2\"\n    >\n      {checked ? <Check className=\"size-4\" /> : <CopyIcon className=\"size-4\" />}\n      {checked ? \"Copied\" : \"Copy Page\"}\n    </Button>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/docs-article.tsx",
    "content": "\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { DocsBorderedShell } from \"./docs-bordered-shell\";\nimport { DocsContent } from \"./docs-content\";\nimport { useDocsToc } from \"./docs-toc-context\";\nimport { DocsTocWrapper } from \"./docs-toc-wrapper\";\n\nexport function DocsArticle({\n  children,\n  className,\n}: {\n  children: ReactNode;\n  className?: string;\n}) {\n  const { scrollContainerRef } = useDocsToc();\n\n  return (\n    <DocsBorderedShell>\n      <div\n        ref={scrollContainerRef}\n        className=\"scrollbar-subtle z-10 min-h-0 flex-1 overflow-y-auto overscroll-contain\"\n      >\n        <div className=\"mx-auto flex max-w-[1200px] gap-8 px-4 pt-12 pb-96 sm:px-6 lg:px-10 lg:pt-16 lg:pb-96 xl:px-12 xl:pb-96\">\n          <DocsContent className={className}>{children}</DocsContent>\n          <DocsTocWrapper />\n        </div>\n      </div>\n    </DocsBorderedShell>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/docs-bordered-shell.tsx",
    "content": "import type { ReactNode } from \"react\";\nimport { cn } from \"@/lib/ui/cn\";\n\nexport function DocsBorderedShell({\n  children,\n  className,\n}: {\n  children: ReactNode;\n  className?: string;\n}) {\n  return (\n    <div\n      className={cn(\n        \"squircle relative box-border flex min-h-0 w-full grow flex-col overflow-hidden border lg:mr-2 lg:mb-2\",\n        className,\n      )}\n    >\n      <div className=\"relative flex h-full min-h-0 min-w-0 w-full flex-1 flex-col overflow-hidden\">\n        {children}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/docs-content.tsx",
    "content": "import type { ReactNode } from \"react\";\nimport { cn } from \"@/lib/ui/cn\";\nimport { DocsPager } from \"./docs-pager\";\n\ninterface DocsContentProps {\n  children: ReactNode;\n  className?: string;\n  includePager?: boolean;\n}\n\nexport function DocsContent({\n  children,\n  className,\n  includePager = true,\n}: DocsContentProps) {\n  return (\n    <div\n      className={cn(\n        \"prose dark:prose-invert mx-auto min-w-0 max-w-3xl\",\n        className,\n      )}\n    >\n      {children}\n      {includePager && <DocsPager />}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/docs-header.tsx",
    "content": "import { CopyMarkdownButton } from \"./copy-markdown-button\";\nimport { HeaderPreviewTabs } from \"./header-preview-tabs\";\nimport { InstallCommandLine } from \"./install-command-line\";\nimport { getMdxAsMarkdown } from \"./mdx-to-markdown\";\nimport { isComponentId } from \"@/lib/docs/component-ids\";\n\ntype DocsHeaderProps = {\n  title: string;\n  description?: string;\n  mdxPath?: string;\n};\n\nexport function DocsHeader({ title, description, mdxPath }: DocsHeaderProps) {\n  const markdown = mdxPath ? getMdxAsMarkdown(mdxPath) : undefined;\n  const componentIdMatch = mdxPath?.match(/^app\\/docs\\/([^/]+)\\/content\\.mdx$/);\n  const parsedComponentId = componentIdMatch?.[1];\n  const componentId =\n    parsedComponentId && isComponentId(parsedComponentId)\n      ? parsedComponentId\n      : null;\n\n  return (\n    <div className=\"mb-12 flex flex-col gap-2\">\n      <div className=\"flex items-start justify-between gap-3\">\n        <h1 className=\"text-4xl font-bold tracking-tight\">{title}</h1>\n        {markdown && <CopyMarkdownButton markdown={markdown} />}\n      </div>\n      {description && (\n        <div className=\"text-muted-foreground text-lg\">{description}</div>\n      )}\n      {componentId && (\n        <InstallCommandLine componentId={componentId} className=\"mt-2\" />\n      )}\n      {componentId && <HeaderPreviewTabs componentId={componentId} />}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/docs-nav.tsx",
    "content": "\"use client\";\n\nimport React, { useState } from \"react\";\nimport Link from \"next/link\";\nimport { usePathname } from \"next/navigation\";\nimport { useQueryState } from \"nuqs\";\nimport { LayoutDashboardIcon } from \"lucide-react\";\nimport { analytics } from \"@/lib/analytics\";\nimport {\n  componentsRegistry,\n  componentsByCategory,\n  CATEGORY_META,\n  type ComponentCategory,\n} from \"@/lib/docs/component-registry\";\nimport { cn } from \"@/lib/ui/cn\";\nimport { CONCEPTS_DOCS_PAGES, GET_STARTED_DOCS_PAGES } from \"./docs-pages\";\n\nconst STORAGE_KEY = \"tool-ui-components-nav-collapsed:v1\";\n\nexport function DocsNav() {\n  const pathname = usePathname();\n  const [currentTab] = useQueryState(\"tab\");\n  const [currentView] = useQueryState(\"view\");\n  const [collapsed, setCollapsed] = useState(false);\n  const [isPressing, setIsPressing] = useState(false);\n  const [prevPathname, setPrevPathname] = useState(pathname);\n  const previousPathRef = React.useRef<string | null>(null);\n\n  if (pathname !== prevPathname) {\n    setPrevPathname(pathname);\n    setIsPressing(false);\n  }\n\n  React.useEffect(() => {\n    try {\n      if (typeof window !== \"undefined\") {\n        const stored = window.localStorage.getItem(STORAGE_KEY);\n        if (stored != null) setCollapsed(stored === \"true\");\n      }\n    } catch {}\n  }, []);\n\n  React.useEffect(() => {\n    const handleMouseUp = (e: MouseEvent) => {\n      const target = e.target as HTMLElement;\n      const isNavLink = target.closest('a[href^=\"/docs\"]');\n      if (!isNavLink) {\n        setIsPressing(false);\n      }\n    };\n    document.addEventListener(\"mouseup\", handleMouseUp);\n    return () => document.removeEventListener(\"mouseup\", handleMouseUp);\n  }, []);\n\n  React.useEffect(() => {\n    const component = componentsRegistry.find(\n      (entry) => entry.path === pathname,\n    );\n    if (component) {\n      const source =\n        previousPathRef.current === \"/docs/gallery\" ? \"gallery\" : \"direct\";\n      analytics.component.viewed(component.id, source);\n    }\n\n    previousPathRef.current = pathname;\n  }, [pathname]);\n\n  const buildLinkClasses = (isActive: boolean) =>\n    cn(\n      \"flex items-center gap-2 rounded-lg px-4  bg-background text-primary  py-2 text-sm transition-[colors,background] duration-75\",\n      {\n        \"justify-center px-0\": collapsed,\n\n        \"bg-primary/5\": isActive && !isPressing,\n        \"hover:bg-primary/2\": !isActive,\n        \"hover:bg-primary/5\": isPressing && !isActive,\n      },\n    );\n\n  const handleLinkMouseDown = () => setIsPressing(true);\n\n  const galleryPath = \"/docs/gallery\";\n  const isGalleryActive = pathname === galleryPath;\n\n  const categorizedComponents = (\n    Object.entries(CATEGORY_META) as [\n      ComponentCategory,\n      (typeof CATEGORY_META)[ComponentCategory],\n    ][]\n  )\n    .sort(([, a], [, b]) => a.order - b.order)\n    .map(([category, meta]) => ({\n      category,\n      label: meta.label,\n      components: componentsByCategory.get(category) || [],\n    }));\n\n  return (\n    <aside\n      className={cn(\n        \"bg-background flex h-full shrink-0 flex-col transition-all duration-300\",\n        collapsed ? \"w-16\" : \"w-full\",\n      )}\n    >\n      <nav className=\"flex flex-1 flex-col py-4 pb-24\">\n        <div className=\"mb-4 flex flex-col gap-2 px-4\">\n          <Link\n            href={galleryPath}\n            className={buildLinkClasses(isGalleryActive)}\n            title={collapsed ? \"Gallery\" : undefined}\n            onMouseDown={handleLinkMouseDown}\n            onClick={() =>\n              analytics.docs.navigationClicked(\"Gallery\", galleryPath)\n            }\n          >\n            {!collapsed && (\n              <div className=\"flex flex-col overflow-hidden\">\n                <span className=\"truncate\">Gallery</span>\n              </div>\n            )}\n            <LayoutDashboardIcon\n              className={cn(\"text-muted-foreground size-4 shrink-0\", {\n                \"text-primary\": isGalleryActive,\n              })}\n            />\n          </Link>\n        </div>\n\n        <div className=\"flex flex-col gap-1 px-4 pt-4\">\n          {!collapsed && (\n            <div className=\"text-primary/60 mb-3 cursor-default px-4 text-xs tracking-widest uppercase select-none\">\n              Get Started\n            </div>\n          )}\n          {GET_STARTED_DOCS_PAGES.map((page) => {\n            const isActive = pathname === page.path;\n            return (\n              <Link\n                key={page.path}\n                href={page.path}\n                className={buildLinkClasses(isActive)}\n                title={collapsed ? page.label : undefined}\n                onMouseDown={handleLinkMouseDown}\n                onClick={() =>\n                  analytics.docs.navigationClicked(page.label, page.path)\n                }\n              >\n                {!collapsed && (\n                  <div className=\"overflow-hidden\">\n                    <span className=\"truncate\">{page.label}</span>\n                  </div>\n                )}\n              </Link>\n            );\n          })}\n        </div>\n\n        <div className=\"flex flex-col gap-1 px-4 pt-8\">\n          {!collapsed && (\n            <div className=\"text-primary/60 mb-3 cursor-default px-4 text-xs tracking-widest uppercase select-none\">\n              Concepts\n            </div>\n          )}\n          {CONCEPTS_DOCS_PAGES.map((page) => {\n            const isActive = pathname === page.path;\n            return (\n              <Link\n                key={page.path}\n                href={page.path}\n                className={buildLinkClasses(isActive)}\n                title={collapsed ? page.label : undefined}\n                onMouseDown={handleLinkMouseDown}\n                onClick={() =>\n                  analytics.docs.navigationClicked(page.label, page.path)\n                }\n              >\n                {!collapsed && (\n                  <div className=\"overflow-hidden\">\n                    <span className=\"truncate\">{page.label}</span>\n                  </div>\n                )}\n              </Link>\n            );\n          })}\n        </div>\n\n        {categorizedComponents.map(({ category, label, components }) => (\n          <div key={category} className=\"flex flex-col gap-1 px-4 pt-8\">\n            {!collapsed && (\n              <div className=\"text-primary/60 mb-3 cursor-default px-4 text-xs tracking-widest uppercase select-none\">\n                {label}\n              </div>\n            )}\n            {components.map((component) => {\n              const isActive = pathname === component.path;\n              const href = (() => {\n                if (currentTab !== \"examples\") return component.path;\n\n                const params = new URLSearchParams();\n                params.set(\"tab\", \"examples\");\n\n                // Preserve view mode across component navigation when browsing examples.\n                if (currentView === \"chat\" || currentView === \"code\") {\n                  params.set(\"view\", currentView);\n                }\n\n                return `${component.path}?${params.toString()}`;\n              })();\n              return (\n                <Link\n                  key={component.id}\n                  href={href}\n                  className={buildLinkClasses(isActive)}\n                  title={collapsed ? component.label : undefined}\n                  onMouseDown={handleLinkMouseDown}\n                  onClick={() =>\n                    analytics.docs.navigationClicked(component.label, href)\n                  }\n                >\n                  {!collapsed && (\n                    <div className=\"overflow-hidden\">\n                      <span className=\"truncate\">{component.label}</span>\n                    </div>\n                  )}\n                </Link>\n              );\n            })}\n          </div>\n        ))}\n      </nav>\n    </aside>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/docs-pager.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport Link from \"next/link\";\nimport { usePathname } from \"next/navigation\";\nimport { ArrowLeft, ArrowRight } from \"lucide-react\";\nimport { getAllDocsPageLinks } from \"./docs-pages\";\nimport { cn } from \"@/lib/ui/cn\";\nimport { Button } from \"@/components/ui/button\";\nimport { analytics } from \"@/lib/analytics\";\n\nfunction useDocsPagination() {\n  const pathname = usePathname();\n\n  return React.useMemo(() => {\n    const links = getAllDocsPageLinks();\n    const currentIndex = links.findIndex((link) => link.path === pathname);\n    if (currentIndex === -1) return { prev: null, next: null };\n    const prev = currentIndex > 0 ? links[currentIndex - 1] : null;\n    const next =\n      currentIndex < links.length - 1 ? links[currentIndex + 1] : null;\n    return { prev, next };\n  }, [pathname]);\n}\n\ntype PagerLinkProps = {\n  href: string;\n  label: string;\n  direction: \"prev\" | \"next\";\n};\n\nfunction PagerLink({ href, label, direction }: PagerLinkProps) {\n  const isPrev = direction === \"prev\";\n  return (\n    <Button\n      asChild\n      variant=\"outline\"\n      className={cn(\n        \"w-fit gap-3 py-5\",\n        isPrev ? \"justify-start\" : \"justify-end text-right\",\n      )}\n      aria-label={`Go to ${label}`}\n    >\n      <Link\n        href={href}\n        onClick={() => analytics.docs.navigationClicked(label, href)}\n        className={cn(\n          \"inline-flex items-center gap-3\",\n          isPrev ? \"text-left\" : \"text-right\",\n        )}\n      >\n        {isPrev ? (\n          <>\n            <ArrowLeft className=\"text-muted-foreground size-4 shrink-0\" />\n            <span className=\"text-foreground font-medium\">{label}</span>\n          </>\n        ) : (\n          <>\n            <span className=\"text-foreground flex-1 text-right font-medium\">\n              {label}\n            </span>\n            <ArrowRight className=\"text-muted-foreground size-4 shrink-0\" />\n          </>\n        )}\n      </Link>\n    </Button>\n  );\n}\n\nexport function DocsPager() {\n  const { prev, next } = useDocsPagination();\n\n  if (!prev && !next) return null;\n\n  return (\n    <div className=\"not-prose mt-24\">\n      <div className=\"flex flex-row items-center justify-between gap-4\">\n        {prev ? (\n          <PagerLink href={prev.path} label={prev.label} direction=\"prev\" />\n        ) : (\n          <span className=\"flex-1\" />\n        )}\n        {next ? (\n          <PagerLink href={next.path} label={next.label} direction=\"next\" />\n        ) : (\n          <span className=\"flex-1\" />\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/docs-pages.ts",
    "content": "import { componentsRegistry } from \"@/lib/docs/component-registry\";\n\nexport type DocsPageLink = {\n  path: string;\n  label: string;\n};\n\nexport const GET_STARTED_DOCS_PAGES: DocsPageLink[] = [\n  { path: \"/docs/overview\", label: \"Overview\" },\n  { path: \"/docs/quick-start\", label: \"Quick Start\" },\n  { path: \"/docs/agent-skills\", label: \"Agent Skills\" },\n  { path: \"/docs/advanced\", label: \"Advanced\" },\n  { path: \"/docs/design-guidelines\", label: \"UI Guidelines\" },\n  { path: \"/docs/changelog\", label: \"Changelog\" },\n];\n\nexport const CONCEPTS_DOCS_PAGES: DocsPageLink[] = [\n  { path: \"/docs/actions\", label: \"Actions\" },\n  { path: \"/docs/receipts\", label: \"Receipts\" },\n];\n\nexport const BASE_DOCS_PAGES: DocsPageLink[] = [\n  ...GET_STARTED_DOCS_PAGES,\n  ...CONCEPTS_DOCS_PAGES,\n];\n\nexport function getAllDocsPageLinks(): DocsPageLink[] {\n  return [\n    ...BASE_DOCS_PAGES,\n    ...componentsRegistry.map((component) => ({\n      path: component.path,\n      label: component.label,\n    })),\n  ];\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/docs-search-shortcut.ts",
    "content": "export type SearchShortcutEvent = Pick<\n  KeyboardEvent,\n  \"metaKey\" | \"ctrlKey\" | \"key\" | \"preventDefault\" | \"defaultPrevented\"\n>;\n\nconst handledShortcutEvents = new WeakSet<SearchShortcutEvent>();\n\nexport const triggerSearchFromShortcut = (\n  event: SearchShortcutEvent,\n  onOpen: () => void,\n) => {\n  if (handledShortcutEvents.has(event) || event.defaultPrevented) return false;\n\n  const isModifierPressed = event.metaKey || event.ctrlKey;\n  const isShortcutKey = event.key.toLowerCase() === \"k\";\n\n  if (!isModifierPressed || !isShortcutKey) return false;\n\n  handledShortcutEvents.add(event);\n  event.preventDefault();\n  onOpen();\n  return true;\n};\n"
  },
  {
    "path": "apps/www/app/docs/_components/docs-search.client.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport Link from \"next/link\";\nimport { SearchIcon } from \"lucide-react\";\nimport { analytics } from \"@/lib/analytics\";\nimport { Button } from \"@/components/ui/button\";\nimport { Dialog, DialogContent, DialogTitle } from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { componentsRegistry } from \"@/lib/docs/component-registry\";\nimport { cn } from \"@/lib/ui/cn\";\nimport { BASE_DOCS_PAGES } from \"./docs-pages\";\nimport { triggerSearchFromShortcut } from \"./docs-search-shortcut\";\n\ntype SearchResult = {\n  kind: \"page\" | \"component\";\n  label: string;\n  href: string;\n};\n\nconst SEARCH_RESULTS: SearchResult[] = [\n  { kind: \"page\", label: \"Gallery\", href: \"/docs/gallery\" },\n  ...BASE_DOCS_PAGES.map((page) => ({\n    kind: \"page\" as const,\n    label: page.label,\n    href: page.path,\n  })),\n  ...componentsRegistry.map((component) => ({\n    kind: \"component\" as const,\n    label: component.label,\n    href: component.path,\n  })),\n];\n\nconst MAX_RESULTS = 10;\n\nexport function DocsSearch() {\n  const [open, setOpen] = React.useState(false);\n  const [query, setQuery] = React.useState(\"\");\n  const lastTrackedSubmissionRef = React.useRef<string | null>(null);\n\n  const trimmedQuery = query.trim();\n\n  const filteredResults = React.useMemo(() => {\n    if (trimmedQuery.length === 0) return SEARCH_RESULTS.slice(0, MAX_RESULTS);\n\n    const normalized = trimmedQuery.toLowerCase();\n    return SEARCH_RESULTS.filter((result) =>\n      result.label.toLowerCase().includes(normalized),\n    ).slice(0, MAX_RESULTS);\n  }, [trimmedQuery]);\n\n  const trackSubmittedQuery = React.useCallback(() => {\n    if (trimmedQuery.length === 0) return;\n\n    const resultsCount = filteredResults.length;\n    const signature = `${trimmedQuery.toLowerCase()}::${resultsCount}`;\n\n    if (lastTrackedSubmissionRef.current === signature) return;\n\n    analytics.search.querySubmitted(trimmedQuery, resultsCount);\n    if (resultsCount === 0) {\n      analytics.search.noResults(trimmedQuery);\n    }\n    lastTrackedSubmissionRef.current = signature;\n  }, [filteredResults.length, trimmedQuery]);\n\n  const openSearch = React.useCallback((source: \"header\" | \"keyboard\") => {\n    analytics.search.opened(source);\n    setOpen(true);\n  }, []);\n\n  const closeSearch = React.useCallback(() => {\n    setOpen(false);\n    setQuery(\"\");\n    lastTrackedSubmissionRef.current = null;\n  }, []);\n\n  React.useEffect(() => {\n    const onKeyDown = (event: KeyboardEvent) => {\n      triggerSearchFromShortcut(event, () => openSearch(\"keyboard\"));\n    };\n\n    window.addEventListener(\"keydown\", onKeyDown);\n    return () => window.removeEventListener(\"keydown\", onKeyDown);\n  }, [openSearch]);\n\n  return (\n    <>\n      <Button\n        variant=\"outline\"\n        size=\"sm\"\n        className=\"h-9 gap-2 px-3\"\n        onClick={() => openSearch(\"header\")}\n      >\n        <SearchIcon className=\"size-4\" />\n        <span className=\"hidden text-sm sm:inline\">Search</span>\n        <span className=\"text-muted-foreground hidden rounded border px-1.5 py-0.5 text-xs sm:inline\">\n          Cmd+K\n        </span>\n      </Button>\n\n      <Dialog\n        open={open}\n        onOpenChange={(nextOpen) => {\n          if (!nextOpen) {\n            closeSearch();\n            return;\n          }\n          setOpen(true);\n        }}\n      >\n        <DialogContent showCloseButton={false} className=\"p-0 sm:max-w-xl\">\n          <DialogTitle className=\"sr-only\">Search Tool UI Docs</DialogTitle>\n          <form\n            className=\"border-b p-3\"\n            onSubmit={(event) => {\n              event.preventDefault();\n              trackSubmittedQuery();\n            }}\n          >\n            <Input\n              autoFocus\n              value={query}\n              onChange={(event) => setQuery(event.target.value)}\n              placeholder=\"Search pages and components...\"\n              aria-label=\"Search pages and components\"\n              className=\"h-10\"\n            />\n          </form>\n\n          <div className=\"max-h-[360px] overflow-y-auto p-2\">\n            {filteredResults.length === 0 ? (\n              <div className=\"text-muted-foreground px-3 py-8 text-center text-sm\">\n                No results found\n              </div>\n            ) : (\n              <ul className=\"space-y-1\">\n                {filteredResults.map((result, index) => (\n                  <li key={`${result.kind}:${result.href}`}>\n                    <Link\n                      href={result.href}\n                      onClick={() => {\n                        trackSubmittedQuery();\n                        analytics.search.resultClicked(\n                          trimmedQuery,\n                          result.href,\n                          index + 1,\n                        );\n                        closeSearch();\n                      }}\n                      className={cn(\n                        \"hover:bg-muted flex items-center justify-between rounded-md px-3 py-2 transition-colors\",\n                      )}\n                    >\n                      <span className=\"text-sm\">{result.label}</span>\n                      <span className=\"text-muted-foreground text-xs uppercase\">\n                        {result.kind}\n                      </span>\n                    </Link>\n                  </li>\n                ))}\n              </ul>\n            )}\n          </div>\n        </DialogContent>\n      </Dialog>\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/docs-toc-context.tsx",
    "content": "\"use client\";\n\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useState,\n  type ReactNode,\n} from \"react\";\nimport { useExtractHeadings, type Heading } from \"@/hooks/use-extract-headings\";\nimport { useHeadingsObserver } from \"@/hooks/use-headings-observer\";\n\ntype DocsTocContextValue = {\n  scrollContainerRef: (node: HTMLElement | null) => void;\n  scrollContainer: HTMLElement | null;\n  headings: Heading[];\n  activeId: string | null;\n};\n\nconst DocsTocContext = createContext<DocsTocContextValue | null>(null);\n\nexport function useDocsToc() {\n  const context = useContext(DocsTocContext);\n  if (!context) {\n    throw new Error(\"useDocsToc must be used within DocsTocProvider\");\n  }\n  return context;\n}\n\nexport function DocsTocProvider({ children }: { children: ReactNode }) {\n  const [scrollContainer, setScrollContainer] = useState<HTMLElement | null>(\n    null,\n  );\n\n  const scrollContainerRef = useCallback((node: HTMLElement | null) => {\n    if (node) {\n      setScrollContainer(node);\n    }\n  }, []);\n\n  const headings = useExtractHeadings(scrollContainer);\n  const activeId = useHeadingsObserver(headings, scrollContainer);\n\n  return (\n    <DocsTocContext.Provider\n      value={{\n        scrollContainerRef,\n        scrollContainer,\n        headings,\n        activeId,\n      }}\n    >\n      {children}\n    </DocsTocContext.Provider>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/docs-toc-wrapper.tsx",
    "content": "import { DocsToc } from \"./docs-toc\";\n\nexport function DocsTocWrapper() {\n  return (\n    <div className=\"hidden min-w-0 w-[200px] shrink-0 xl:block\">\n      <div className=\"sticky top-6\">\n        <DocsToc />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/docs-toc.tsx",
    "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/ui/cn\";\nimport { useDocsToc } from \"./docs-toc-context\";\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\nimport { useTocKeyboardNav } from \"@/hooks/use-toc-keyboard-nav\";\nimport { analytics } from \"@/lib/analytics\";\n\nexport function DocsToc() {\n  const { scrollContainer, headings, activeId } = useDocsToc();\n  const reducedMotion = useReducedMotion();\n  const linkRefs = useRef<(HTMLAnchorElement | null)[]>([]);\n  const previousIndicatorTopRef = useRef<number | null>(null);\n  const [indicatorTop, setIndicatorTop] = useState<number | null>(null);\n\n  const scrollToHeading = useCallback(\n    (id: string) => {\n      const element = document.getElementById(id);\n      const container = scrollContainer;\n      if (!element || !container) return;\n\n      const stickyHeader = container.querySelector('[role=\"tablist\"]');\n      const offset = stickyHeader\n        ? stickyHeader.getBoundingClientRect().height + 20\n        : 80;\n\n      const targetScroll = element.offsetTop - offset;\n\n      if (reducedMotion) {\n        container.scrollTop = targetScroll;\n      } else {\n        container.scrollTo({ top: targetScroll, behavior: \"smooth\" });\n      }\n    },\n    [scrollContainer, reducedMotion],\n  );\n\n  const { handleKeyDown, setLinkRef: setKeyboardLinkRef } = useTocKeyboardNav(\n    headings,\n    scrollToHeading,\n  );\n\n  const handleClick = (\n    e: React.MouseEvent<HTMLAnchorElement>,\n    id: string,\n    title: string,\n  ) => {\n    e.preventDefault();\n    analytics.docs.tocLinkClicked(title, 2);\n    scrollToHeading(id);\n  };\n\n  useEffect(() => {\n    if (typeof window === \"undefined\" || headings.length === 0) return;\n\n    const hash = window.location.hash.slice(1);\n    if (hash && headings.some((h) => h.id === hash)) {\n      setTimeout(() => scrollToHeading(hash), 200);\n    }\n  }, [headings, scrollToHeading]);\n\n  useEffect(() => {\n    linkRefs.current = linkRefs.current.slice(0, headings.length);\n  }, [headings]);\n\n  // Update indicator position when activeId changes\n  useEffect(() => {\n    const activeIndex = headings.findIndex((h) => h.id === activeId);\n    if (activeIndex >= 0 && linkRefs.current[activeIndex]) {\n      const activeLink = linkRefs.current[activeIndex];\n      if (activeLink) {\n        const linkRect = activeLink.getBoundingClientRect();\n        const navRect = activeLink.parentElement?.getBoundingClientRect();\n        if (navRect) {\n          const relativeTop = linkRect.top - navRect.top + 8;\n          setIndicatorTop(relativeTop);\n          return;\n        }\n      }\n    }\n\n    setIndicatorTop(null);\n  }, [activeId, headings]);\n\n  useEffect(() => {\n    previousIndicatorTopRef.current = indicatorTop;\n  }, [indicatorTop]);\n\n  if (headings.length === 0) {\n    return null;\n  }\n\n  const activeIndex = headings.findIndex((h) => h.id === activeId);\n  const shouldAnimateIndicator =\n    !reducedMotion &&\n    previousIndicatorTopRef.current !== null &&\n    indicatorTop !== null;\n\n  const setLinkRef = (index: number) => (el: HTMLAnchorElement | null) => {\n    linkRefs.current[index] = el;\n    setKeyboardLinkRef(index)(el);\n  };\n\n  return (\n    <nav\n      aria-label=\"Table of contents\"\n      className=\"relative space-y-1\"\n      onKeyDown={handleKeyDown}\n    >\n      <p className=\"text-primary/60 mb-4 cursor-default text-xs tracking-widest uppercase select-none\">\n        On This Page\n      </p>\n      {activeIndex >= 0 && indicatorTop !== null && (\n        <span\n          aria-hidden=\"true\"\n          className=\"bg-foreground pointer-events-none absolute -left-3 h-3 w-0.5 rounded-full\"\n          style={{\n            top: indicatorTop,\n            opacity: 1,\n            transition: shouldAnimateIndicator ? \"top 150ms ease-out\" : \"none\",\n          }}\n        />\n      )}\n      {headings.map((heading, index) => {\n        const isActive = heading.id === activeId;\n        return (\n          <a\n            key={heading.id}\n            ref={setLinkRef(index)}\n            href={`#${heading.id}`}\n            onClick={(e) => handleClick(e, heading.id, heading.text)}\n            className={cn(\n              \"relative block py-1 text-sm transition-colors\",\n              \"hover:text-foreground focus-visible:outline-none focus-visible:text-foreground\",\n              isActive && \"text-foreground font-medium\",\n              !isActive && \"text-muted-foreground\",\n            )}\n            aria-current={isActive ? \"true\" : undefined}\n            title={heading.text}\n          >\n            <span className=\"line-clamp-2\">{heading.text}</span>\n          </a>\n        );\n      })}\n    </nav>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/gallery-analytics.client.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { analytics } from \"@/lib/analytics\";\n\nexport function GalleryPageAnalytics() {\n  useEffect(() => {\n    analytics.gallery.pageViewed();\n  }, []);\n\n  return null;\n}\n\ninterface GalleryPreviewImpressionProps {\n  componentId: string;\n}\n\nexport function GalleryPreviewImpression({\n  componentId,\n}: GalleryPreviewImpressionProps) {\n  const trackedRef = useRef(false);\n  const markerRef = useRef<HTMLSpanElement>(null);\n\n  useEffect(() => {\n    if (trackedRef.current) {\n      return;\n    }\n\n    const marker = markerRef.current;\n    if (!marker) {\n      return;\n    }\n\n    if (typeof IntersectionObserver !== \"function\") {\n      trackedRef.current = true;\n      analytics.gallery.componentPreviewed(componentId);\n      return;\n    }\n\n    const observer = new IntersectionObserver(\n      (entries) => {\n        for (const entry of entries) {\n          if (!entry.isIntersecting || trackedRef.current) continue;\n\n          trackedRef.current = true;\n          analytics.gallery.componentPreviewed(componentId);\n          observer.disconnect();\n          break;\n        }\n      },\n      { threshold: 0.2 },\n    );\n\n    observer.observe(marker);\n\n    return () => observer.disconnect();\n  }, [componentId]);\n\n  return <span ref={markerRef} className=\"sr-only\" aria-hidden=\"true\" />;\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/gallery-card-header.tsx",
    "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Sheet,\n  SheetContent,\n  SheetDescription,\n  SheetHeader,\n  SheetTitle,\n  SheetTrigger,\n} from \"@/components/ui/sheet\";\nimport { componentsRegistry } from \"@/lib/docs/component-registry\";\nimport { getGalleryUsageCode } from \"@/lib/docs/gallery-usage-code\";\nimport type { GalleryComponentDocId } from \"@/lib/docs/gallery-component-docs\";\nimport { analytics } from \"@/lib/analytics\";\nimport { cn } from \"@/lib/ui/cn\";\nimport { TrackedDynamicCodeBlock } from \"./tracked-dynamic-codeblock\";\nimport { InstallCommandBlock } from \"./install-command-block\";\n\nconst registryById = new Map(componentsRegistry.map((c) => [c.id, c]));\n\ninterface GalleryCardHeaderProps {\n  componentId: GalleryComponentDocId;\n  className?: string;\n  hideDescription?: boolean;\n}\n\nexport function GalleryCardHeader({\n  componentId,\n  className,\n  hideDescription = false,\n}: GalleryCardHeaderProps) {\n  const meta = registryById.get(componentId);\n  const name = meta?.label ?? componentId;\n  const description = meta?.description;\n  const docsHref = (meta?.path ?? `/docs/${componentId}`) as `/docs/${string}`;\n  const usageCode = getGalleryUsageCode(componentId);\n\n  return (\n    <header\n      className={cn(\"flex w-full min-w-0 flex-col gap-1.5 px-1\", className)}\n    >\n      <div className=\"flex min-w-0 flex-wrap items-center justify-between gap-x-3 gap-y-2\">\n        <Link\n          href={docsHref}\n          className=\"text-muted-foreground shrink-0 rounded-md px-0.5 text-xs font-mono tracking-wide underline-offset-4 hover:text-foreground hover:underline\"\n          onClick={() => {\n            analytics.gallery.componentClicked(componentId);\n            analytics.docs.navigationClicked(name, docsHref);\n          }}\n        >\n          <h2>{name}</h2>\n        </Link>\n        <div>\n          <Sheet>\n            <SheetTrigger asChild>\n              <Button\n                type=\"button\"\n                variant=\"outline\"\n                size=\"sm\"\n                className=\"h-7 shrink-0 gap-1.5 border border-transparent px-2 font-mono text-[11px] text-muted-foreground hover:border-border/60 hover:text-foreground\"\n              >\n                Install & use\n              </Button>\n            </SheetTrigger>\n            <SheetContent\n              side=\"right\"\n              className=\"flex w-full flex-col gap-0 sm:max-w-2xl\"\n            >\n              <SheetHeader className=\"shrink-0 border-b border-border/50 pb-4 pr-10\">\n                <SheetTitle className=\"text-lg\">Code</SheetTitle>\n                <SheetDescription className=\"text-sm\">\n                  Copy the command to install, then use the example in your app.\n                </SheetDescription>\n              </SheetHeader>\n              <div className=\"scrollbar-subtle flex min-h-0 flex-1 flex-col gap-6 overflow-y-auto p-4\">\n                <section className=\"space-y-2\">\n                  <h3 className=\"text-foreground text-sm font-medium\">\n                    Install\n                  </h3>\n                  <InstallCommandBlock\n                    componentId={componentId}\n                    variant=\"block\"\n                  />\n                </section>\n                {usageCode && (\n                  <section className=\"flex min-h-0 flex-1 flex-col space-y-2\">\n                    <h3 className=\"text-foreground text-sm font-medium\">\n                      Example\n                    </h3>\n                    <div className=\"scrollbar-subtle min-h-[200px] flex-1 overflow-auto rounded-lg border border-border/60 bg-muted/50 [&_pre]:!rounded-lg [&_pre]:!border-0 [&_pre]:!bg-transparent [&_pre]:!p-4 [&_pre]:!text-sm [&_pre]:!leading-relaxed\">\n                      <TrackedDynamicCodeBlock\n                        lang=\"tsx\"\n                        code={usageCode}\n                        copyButtonLabel=\"gallery usage example\"\n                      />\n                    </div>\n                  </section>\n                )}\n              </div>\n            </SheetContent>\n          </Sheet>\n        </div>\n      </div>\n\n      {!hideDescription && description && (\n        <p className=\"text-muted-foreground text-xs\">{description}</p>\n      )}\n    </header>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/gallery-docs-link.tsx",
    "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { ArrowRight } from \"lucide-react\";\nimport { analytics } from \"@/lib/analytics\";\nimport type { GalleryComponentDocId } from \"@/lib/docs/gallery-component-docs\";\nimport { cn } from \"@/lib/ui/cn\";\n\ninterface GalleryDocsLinkProps {\n  componentId: GalleryComponentDocId;\n  label: string;\n  href: `/docs/${string}`;\n  className?: string;\n}\n\nexport function GalleryDocsLink({\n  componentId,\n  label,\n  href,\n  className,\n}: GalleryDocsLinkProps) {\n  const handleClick = () => {\n    analytics.gallery.componentClicked(componentId);\n    analytics.docs.navigationClicked(label, href);\n  };\n\n  return (\n    <Link\n      href={href}\n      className={cn(\n        \"group inline-flex items-center gap-1 whitespace-nowrap rounded-md hover:no-underline focus-visible:no-underline focus-visible:ring-2 focus-visible:ring-neutral-200/90 focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-900 dark:focus-visible:ring-neutral-800 dark:focus-visible:ring-offset-neutral-100\",\n        className,\n      )}\n      onClick={handleClick}\n    >\n      <span className=\"whitespace-nowrap text-sm font-semibold\">{label}</span>\n      <span\n        className=\"text-neutral-400 dark:text-neutral-500\"\n        aria-hidden=\"true\"\n      >\n        •\n      </span>\n      <span className=\"inline-flex items-center gap-1 whitespace-nowrap text-xs\">\n        <span className=\"underline-offset-2 group-hover:underline group-focus-visible:underline\">\n          View Docs\n        </span>\n        <ArrowRight className=\"size-3\" aria-hidden=\"true\" />\n      </span>\n    </Link>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/header-preview-tabs.tsx",
    "content": "\"use client\";\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport { Tab, Tabs } from \"fumadocs-ui/components/tabs\";\nimport { analytics } from \"@/lib/analytics\";\nimport { getImportLine } from \"@/lib/docs/gallery-usage-code\";\nimport { TrackedDynamicCodeBlock } from \"./tracked-dynamic-codeblock\";\nimport {\n  type ComponentId,\n  getPreviewConfig,\n  type PreviewState,\n} from \"@/lib/docs/preview-config\";\n\ninterface HeaderPreviewTabsProps {\n  componentId: ComponentId;\n}\n\nconst EMPTY_STATE: PreviewState = {};\n\nexport function HeaderPreviewTabs({ componentId }: HeaderPreviewTabsProps) {\n  const config = getPreviewConfig(componentId);\n  const [state, setState] = useState<PreviewState>(EMPTY_STATE);\n  const [isMounted, setIsMounted] = useState(false);\n\n  useEffect(() => {\n    setIsMounted(true);\n  }, []);\n\n  const handleSetState = useCallback((partialState: Partial<PreviewState>) => {\n    setState((prev) => ({ ...prev, ...partialState }));\n  }, []);\n\n  const handleTabClickCapture = useCallback(\n    (event: React.MouseEvent<HTMLDivElement>) => {\n      const target = event.target as HTMLElement;\n      const trigger = target.closest('[role=\"tab\"]');\n      if (!trigger) return;\n      if (trigger.getAttribute(\"aria-selected\") === \"true\") return;\n\n      const rawLabel = trigger.textContent?.trim().toLowerCase();\n      if (rawLabel === \"preview\" || rawLabel === \"code\") {\n        analytics.component.tabSwitched(componentId, `header_${rawLabel}`);\n      }\n    },\n    [componentId],\n  );\n\n  if (!config || !isMounted) {\n    return null;\n  }\n\n  const preset = config.presets[config.defaultPreset];\n  const preview = config.renderComponent({\n    data: preset.data,\n    presetName: config.defaultPreset,\n    state,\n    setState: handleSetState,\n  });\n  const wrappedPreview = config.wrapper ? (\n    <config.wrapper>{preview}</config.wrapper>\n  ) : (\n    preview\n  );\n  const body = preset.generateExampleCode(preset.data);\n  const code = `${getImportLine(componentId)}\\n\\n${body}`;\n\n  return (\n    <div className=\"not-prose mt-6\" onClickCapture={handleTabClickCapture}>\n      <Tabs items={[\"Preview\", \"Code\"]}>\n        <Tab value=\"Preview\">\n          <div className=\"header-preview-center flex w-full justify-center\">\n            {wrappedPreview}\n          </div>\n        </Tab>\n        <Tab value=\"Code\">\n          <TrackedDynamicCodeBlock\n            lang=\"tsx\"\n            code={code}\n            copyButtonLabel=\"header example code\"\n          />\n        </Tab>\n      </Tabs>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/install-command-block.tsx",
    "content": "\"use client\";\n\nimport type { MouseEventHandler } from \"react\";\nimport { Check, Copy as CopyIcon } from \"lucide-react\";\nimport { useCopyButton } from \"fumadocs-ui/utils/use-copy-button\";\nimport { Tabs, Tab } from \"fumadocs-ui/components/tabs\";\nimport { Button } from \"@/components/ui/button\";\nimport { analytics } from \"@/lib/analytics\";\nimport {\n  detectInstallSnippetType,\n  getDocsCodeCopySource,\n} from \"@/lib/docs/install-snippet-analytics\";\nimport { componentsRegistry } from \"@/lib/docs/component-registry\";\nimport { cn } from \"@/lib/ui/cn\";\n\nconst registryById = new Map(componentsRegistry.map((c) => [c.id, c]));\n\ninterface InstallCommandBlockProps {\n  componentId: string;\n  className?: string;\n  /** Compact single-line style (e.g. docs header) vs full block (e.g. gallery sheet) */\n  variant?: \"compact\" | \"block\";\n  /** Override tool-agent prompt when component not in registry (e.g. quick-start) */\n  toolAgentPrompt?: string;\n}\n\nfunction CopyableCommand({\n  command,\n  location,\n  variant,\n}: {\n  command: string;\n  location: \"docs_code_block\" | \"docs_header\";\n  variant: \"compact\" | \"block\";\n}) {\n  const installSnippetType = detectInstallSnippetType(command);\n  const copySource = getDocsCodeCopySource(installSnippetType);\n\n  const [checked, copyCommand] = useCopyButton(async () => {\n    await navigator.clipboard.writeText(command);\n  });\n\n  const onCopy: MouseEventHandler<HTMLButtonElement> = (event) => {\n    analytics.code.blockCopied(\"bash\", copySource);\n    if (installSnippetType) {\n      analytics.docs.installSnippetCopied(installSnippetType, location);\n    }\n    copyCommand(event);\n  };\n\n  const isCompact = variant === \"compact\";\n\n  return (\n    <div\n      className={cn(\n        \"group/install flex items-center gap-2 overflow-hidden rounded-lg\",\n        isCompact\n          ? \"bg-muted/50 pl-4 pr-2.5 py-1.5\"\n          : \"border border-border/60 bg-muted/40 px-3 py-2.5\",\n      )}\n    >\n      <code\n        className={cn(\n          \"text-muted-foreground group-hover/install:text-foreground min-w-0 flex-1 break-all font-mono transition-colors duration-200\",\n          isCompact ? \"text-xs\" : \"text-sm leading-relaxed\",\n        )}\n      >\n        {command}\n      </code>\n      <Button\n        type=\"button\"\n        variant=\"ghost\"\n        size=\"sm\"\n        className={cn(\n          \"shrink-0 px-2 opacity-60 group-hover/install:opacity-100 transition-opacity duration-200\",\n          isCompact ? \"h-7\" : \"h-8 px-2.5\",\n        )}\n        onClick={onCopy}\n        aria-label={checked ? \"Copied\" : \"Copy command\"}\n      >\n        {checked ? (\n          <Check className=\"size-4 text-green-600\" />\n        ) : (\n          <CopyIcon className=\"size-4\" />\n        )}\n      </Button>\n    </div>\n  );\n}\n\nexport function InstallCommandBlock({\n  componentId,\n  className,\n  variant = \"compact\",\n  toolAgentPrompt: promptOverride,\n}: InstallCommandBlockProps) {\n  const meta = registryById.get(componentId);\n  const toolAgentPrompt =\n    promptOverride ??\n    meta?.toolAgentPrompt ??\n    `integrate the ${componentId} component`;\n  const toolAgentCommand = `npx tool-agent \"${toolAgentPrompt}\"`;\n  const shadcnCommand = `npx shadcn@latest add @tool-ui/${componentId}`;\n\n  return (\n    <div className={cn(\"space-y-2\", className)}>\n      <Tabs items={[\"tool-agent\", \"shadcn\"]}>\n        <Tab value=\"tool-agent\">\n          <CopyableCommand\n            command={toolAgentCommand}\n            location=\"docs_header\"\n            variant={variant}\n          />\n        </Tab>\n        <Tab value=\"shadcn\">\n          <CopyableCommand\n            command={shadcnCommand}\n            location=\"docs_header\"\n            variant={variant}\n          />\n        </Tab>\n      </Tabs>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/install-command-line.tsx",
    "content": "\"use client\";\n\nimport { InstallCommandBlock } from \"./install-command-block\";\n\ninterface InstallCommandLineProps {\n  componentId: string;\n  className?: string;\n}\n\nexport function InstallCommandLine({\n  componentId,\n  className,\n}: InstallCommandLineProps) {\n  return (\n    <InstallCommandBlock\n      componentId={componentId}\n      className={className}\n      variant=\"compact\"\n    />\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/interactive-option-demo.tsx",
    "content": "\"use client\";\n\nimport { useCallback, useState } from \"react\";\nimport { RotateCcw } from \"lucide-react\";\nimport { MockThread, MockMessage } from \"./mock-thread\";\nimport { OptionList } from \"@/components/tool-ui/option-list\";\nimport { cn, Button } from \"@/components/tool-ui/option-list/_adapter\";\n\nconst OPTIONS = [\n  {\n    id: \"postgres\",\n    label: \"PostgreSQL\",\n    description: \"Best for relational data with complex queries\",\n  },\n  {\n    id: \"sqlite\",\n    label: \"SQLite\",\n    description: \"Simple, embedded, great for single-server apps\",\n  },\n  {\n    id: \"dynamo\",\n    label: \"DynamoDB\",\n    description: \"Fully managed NoSQL, scales automatically\",\n  },\n];\n\nconst FOLLOW_UP: Record<string, string> = {\n  postgres:\n    \"Good choice. PostgreSQL gives you strong typing, JSON support, and a mature ecosystem. Want me to set up the schema?\",\n  sqlite:\n    \"Nice pick. SQLite keeps things simple — no server to manage, and it's fast for read-heavy workloads. Want me to set up the database file?\",\n  dynamo:\n    \"Solid choice. DynamoDB handles scale automatically with single-digit-millisecond reads. Want me to set up the table definitions?\",\n};\n\nexport function InteractiveOptionDemo() {\n  const [choice, setChoice] = useState<string | null>(null);\n\n  const handleAction = useCallback(\n    (actionId: string, selection: string | string[] | null) => {\n      if (actionId === \"confirm\" && typeof selection === \"string\") {\n        setChoice(selection);\n      }\n    },\n    [],\n  );\n\n  const handleReset = useCallback(() => {\n    setChoice(null);\n  }, []);\n\n  return (\n    <div>\n      <MockThread caption=\"Click an option and confirm — then reset to try again\">\n        <MockMessage role=\"user\">\n          Help me pick a database for the new project\n        </MockMessage>\n        <MockMessage role=\"assistant\">\n          <span className=\"text-foreground text-sm\">\n            {\"Based on your requirements, here are some options:\"}\n          </span>\n          <div className=\"mt-3\">\n            <OptionList\n              id=\"overview-demo-db-picker\"\n              selectionMode=\"single\"\n              options={OPTIONS}\n              choice={choice ?? undefined}\n              onAction={handleAction}\n            />\n          </div>\n        </MockMessage>\n        {choice && (\n          <MockMessage role=\"assistant\">\n            <span className=\"text-foreground text-sm\">\n              {FOLLOW_UP[choice] ?? FOLLOW_UP.postgres}\n            </span>\n          </MockMessage>\n        )}\n      </MockThread>\n      {choice && (\n        <div\n          className={cn(\n            \"flex justify-center\",\n            \"motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-1 motion-safe:duration-200\",\n          )}\n        >\n          <Button\n            variant=\"outline\"\n            size=\"sm\"\n            onClick={handleReset}\n            className=\"bg-background gap-1.5 rounded-full border shadow-sm\"\n          >\n            <RotateCcw className=\"size-3\" />\n            Try again\n          </Button>\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/mdx-to-markdown.ts",
    "content": "import { readFileSync } from \"fs\";\nimport { join } from \"path\";\n\n/**\n * Reads an MDX file and returns it with minimal cleanup.\n * Removes the DocsHeader component and its import since that's just UI chrome.\n */\nexport function getMdxAsMarkdown(mdxPath: string): string {\n  const fullPath = join(process.cwd(), mdxPath);\n  const rawContent = readFileSync(fullPath, \"utf-8\");\n\n  let result = rawContent;\n\n  // Remove DocsHeader import\n  result = result.replace(\n    /^import\\s*\\{\\s*DocsHeader\\s*\\}\\s*from\\s*[\"'][^\"']+[\"'];?\\n?/gm,\n    \"\",\n  );\n\n  // Remove DocsHeader component usage\n  result = result.replace(/<DocsHeader[^>]*\\/>\\n?/g, \"\");\n\n  // Trim leading/trailing whitespace\n  result = result.trim();\n\n  return result;\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/mock-thread.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/ui/cn\";\n\ninterface MockMessageProps {\n  role: \"user\" | \"assistant\";\n  children: React.ReactNode;\n}\n\nexport function MockMessage({ role, children }: MockMessageProps) {\n  if (role === \"user\") {\n    return (\n      <div className=\"flex justify-end\" data-role=\"user\">\n        <div className=\"rounded-full bg-[#007AFF] px-4 py-2 text-white dark:bg-[#002b90]\">\n          {children}\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"space-y-2\" data-role=\"assistant\">\n      {children}\n    </div>\n  );\n}\n\ninterface MockThreadProps {\n  children: React.ReactNode;\n  className?: string;\n  caption?: string;\n}\n\nexport function MockThread({ children, className, caption }: MockThreadProps) {\n  return (\n    <figure\n      className={cn(\n        \"not-prose my-8 flex min-w-0 flex-col overflow-hidden\",\n        className,\n      )}\n    >\n      <div className=\"border-border bg-background flex flex-1 flex-col overflow-hidden rounded-xl border shadow-sm\">\n        {/* Title bar */}\n        <div className=\"bg-muted/50 border-border border-b px-4 py-1 text-center\">\n          <span className=\"text-muted-foreground text-xs font-medium\">\n            Chat\n          </span>\n        </div>\n        {/* Messages */}\n        <div className=\"flex flex-col gap-4 p-4 [&>[data-role=user]+[data-role=assistant]]:mt-2 [&>[data-role=assistant]+[data-role=user]]:mt-2\">\n          {children}\n        </div>\n      </div>\n      {caption && (\n        <figcaption className=\"text-muted-foreground mt-3 text-center text-sm\">\n          {caption}\n        </figcaption>\n      )}\n    </figure>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/preset-example.tsx",
    "content": "\"use client\";\n\nimport { TrackedDynamicCodeBlock } from \"./tracked-dynamic-codeblock\";\nimport { Tabs, Tab } from \"fumadocs-ui/components/tabs\";\nimport { Chart } from \"@/components/tool-ui/chart\";\nimport { chartPresets, ChartPresetName } from \"@/lib/presets/chart\";\nimport { OptionList } from \"@/components/tool-ui/option-list\";\nimport { CodeBlock } from \"@/components/tool-ui/code-block\";\nimport { Terminal } from \"@/components/tool-ui/terminal\";\nimport {\n  optionListPresets,\n  OptionListPresetName,\n} from \"@/lib/presets/option-list\";\nimport {\n  codeBlockPresets,\n  CodeBlockPresetName,\n} from \"@/lib/presets/code-block\";\nimport { terminalPresets, TerminalPresetName } from \"@/lib/presets/terminal\";\nimport { Plan } from \"@/components/tool-ui/plan\";\nimport { planPresets, PlanPresetName } from \"@/lib/presets/plan\";\nimport { ItemCarousel } from \"@/components/tool-ui/item-carousel\";\nimport {\n  itemCarouselPresets,\n  ItemCarouselPresetName,\n} from \"@/lib/presets/item-carousel\";\nimport { ApprovalCard } from \"@/components/tool-ui/approval-card\";\nimport {\n  approvalCardPresets,\n  ApprovalCardPresetName,\n} from \"@/lib/presets/approval-card\";\nimport { QuestionFlow } from \"@/components/tool-ui/question-flow\";\nimport {\n  questionFlowPresets,\n  QuestionFlowPresetName,\n} from \"@/lib/presets/question-flow\";\n\nfunction generateOptionListCode(preset: OptionListPresetName): string {\n  const list = optionListPresets[preset].data;\n  const props: string[] = [];\n\n  props.push(\n    `  options={${JSON.stringify(list.options, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n\n  if (list.selectionMode) {\n    props.push(`  selectionMode=\"${list.selectionMode}\"`);\n  }\n\n  if (list.minSelections && list.minSelections !== 1) {\n    props.push(`  minSelections={${list.minSelections}}`);\n  }\n\n  if (list.maxSelections) {\n    props.push(`  maxSelections={${list.maxSelections}}`);\n  }\n\n  if (list.choice) {\n    const choiceValue =\n      typeof list.choice === \"string\"\n        ? `\"${list.choice}\"`\n        : JSON.stringify(list.choice);\n    props.push(`  choice={${choiceValue}}`);\n  }\n\n  if (list.actions) {\n    const hasActions = Array.isArray(list.actions)\n      ? list.actions.length > 0\n      : list.actions.items.length > 0;\n\n    if (hasActions) {\n      props.push(\n        `  actions={${JSON.stringify(list.actions, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n      );\n    }\n  }\n\n  if (!list.choice) {\n    props.push(\n      `  onAction={(actionId, selection) => {\\n    if (actionId === \"confirm\") {\\n      console.log(selection);\\n    }\\n  }}`,\n    );\n  }\n\n  return `<OptionList\\n${props.join(\"\\n\")}\\n/>`;\n}\n\nfunction generateCodeBlockCode(preset: CodeBlockPresetName): string {\n  const block = codeBlockPresets[preset].data;\n  const props: string[] = [];\n\n  props.push(`  id=\"${block.id}\"`);\n  props.push(`  code={\\`${block.code.replace(/`/g, \"\\\\`\")}\\`}`);\n  props.push(`  language=\"${block.language}\"`);\n  props.push(`  lineNumbers=\"${block.lineNumbers}\"`);\n\n  if (block.filename) {\n    props.push(`  filename=\"${block.filename}\"`);\n  }\n\n  if (block.highlightLines && block.highlightLines.length > 0) {\n    props.push(`  highlightLines={[${block.highlightLines.join(\", \")}]}`);\n  }\n\n  if (block.maxCollapsedLines) {\n    props.push(`  maxCollapsedLines={${block.maxCollapsedLines}}`);\n  }\n\n  return `<CodeBlock\\n${props.join(\"\\n\")}\\n/>`;\n}\n\nfunction generateTerminalCode(preset: TerminalPresetName): string {\n  const term = terminalPresets[preset].data;\n  const props: string[] = [];\n\n  props.push(`  command=\"${term.command.replace(/\"/g, '\\\\\"')}\"`);\n\n  if (term.stdout) {\n    props.push(`  stdout={\\`${term.stdout.replace(/`/g, \"\\\\`\")}\\`}`);\n  }\n\n  if (term.stderr) {\n    props.push(`  stderr={\\`${term.stderr.replace(/`/g, \"\\\\`\")}\\`}`);\n  }\n\n  props.push(`  exitCode={${term.exitCode}}`);\n\n  if (term.durationMs !== undefined) {\n    props.push(`  durationMs={${term.durationMs}}`);\n  }\n\n  if (term.cwd) {\n    props.push(`  cwd=\"${term.cwd}\"`);\n  }\n\n  if (term.truncated) {\n    props.push(`  truncated={${term.truncated}}`);\n  }\n\n  if (term.maxCollapsedLines) {\n    props.push(`  maxCollapsedLines={${term.maxCollapsedLines}}`);\n  }\n\n  return `<Terminal\\n${props.join(\"\\n\")}\\n/>`;\n}\n\ninterface OptionListPresetExampleProps {\n  preset: OptionListPresetName;\n}\n\nexport function OptionListPresetExample({\n  preset,\n}: OptionListPresetExampleProps) {\n  const data = optionListPresets[preset].data;\n  const code = generateOptionListCode(preset);\n\n  return (\n    <Tabs items={[\"Preview\", \"Code\"]}>\n      <Tab value=\"Preview\">\n        <div className=\"not-prose mx-auto max-w-md\">\n          <OptionList {...data} />\n        </div>\n      </Tab>\n      <Tab value=\"Code\">\n        <TrackedDynamicCodeBlock\n          lang=\"tsx\"\n          code={code}\n          copyButtonLabel=\"preset example code\"\n        />\n      </Tab>\n    </Tabs>\n  );\n}\n\nfunction generateChartCode(preset: ChartPresetName): string {\n  const config = chartPresets[preset].data;\n  const props: string[] = [];\n\n  props.push(`  id=\"chart-example\"`);\n  props.push(`  type=\"${config.type}\"`);\n\n  if (config.title) {\n    props.push(`  title=\"${config.title}\"`);\n  }\n\n  if (config.description) {\n    props.push(`  description=\"${config.description}\"`);\n  }\n\n  props.push(\n    `  data={${JSON.stringify(config.data, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n  props.push(`  xKey=\"${config.xKey}\"`);\n  props.push(\n    `  series={${JSON.stringify(config.series, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n\n  if (config.showLegend) {\n    props.push(`  showLegend`);\n  }\n\n  if (config.showGrid) {\n    props.push(`  showGrid`);\n  }\n\n  return `<Chart\\n${props.join(\"\\n\")}\\n/>`;\n}\n\ninterface ChartPresetExampleProps {\n  preset: ChartPresetName;\n}\n\nexport function ChartPresetExample({ preset }: ChartPresetExampleProps) {\n  const config = chartPresets[preset].data;\n  const code = generateChartCode(preset);\n\n  return (\n    <Tabs items={[\"Preview\", \"Code\"]}>\n      <Tab value=\"Preview\">\n        <div className=\"not-prose\">\n          <Chart id={`chart-${preset}`} {...config} />\n        </div>\n      </Tab>\n      <Tab value=\"Code\">\n        <TrackedDynamicCodeBlock\n          lang=\"tsx\"\n          code={code}\n          copyButtonLabel=\"preset example code\"\n        />\n      </Tab>\n    </Tabs>\n  );\n}\n\ninterface CodeBlockPresetExampleProps {\n  preset: CodeBlockPresetName;\n}\n\nexport function CodeBlockPresetExample({\n  preset,\n}: CodeBlockPresetExampleProps) {\n  const data = codeBlockPresets[preset].data;\n  const code = generateCodeBlockCode(preset);\n\n  return (\n    <Tabs items={[\"Preview\", \"Code\"]}>\n      <Tab value=\"Preview\">\n        <div className=\"not-prose\">\n          <CodeBlock {...data} />\n        </div>\n      </Tab>\n      <Tab value=\"Code\">\n        <TrackedDynamicCodeBlock\n          lang=\"tsx\"\n          code={code}\n          copyButtonLabel=\"preset example code\"\n        />\n      </Tab>\n    </Tabs>\n  );\n}\n\ninterface TerminalPresetExampleProps {\n  preset: TerminalPresetName;\n}\n\nexport function TerminalPresetExample({ preset }: TerminalPresetExampleProps) {\n  const data = terminalPresets[preset].data;\n  const code = generateTerminalCode(preset);\n\n  return (\n    <Tabs items={[\"Preview\", \"Code\"]}>\n      <Tab value=\"Preview\">\n        <div className=\"not-prose\">\n          <Terminal {...data} />\n        </div>\n      </Tab>\n      <Tab value=\"Code\">\n        <TrackedDynamicCodeBlock\n          lang=\"tsx\"\n          code={code}\n          copyButtonLabel=\"preset example code\"\n        />\n      </Tab>\n    </Tabs>\n  );\n}\n\nfunction generatePlanCode(preset: PlanPresetName): string {\n  const plan = planPresets[preset].data;\n  const props: string[] = [];\n\n  props.push(`  id=\"${plan.id}\"`);\n  props.push(`  title=\"${plan.title}\"`);\n\n  if (plan.description) {\n    props.push(`  description=\"${plan.description}\"`);\n  }\n\n  props.push(\n    `  todos={${JSON.stringify(plan.todos, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n\n  if (plan.maxVisibleTodos) {\n    props.push(`  maxVisibleTodos={${plan.maxVisibleTodos}}`);\n  }\n\n  return `<Plan\\n${props.join(\"\\n\")}\\n/>`;\n}\n\ninterface PlanPresetExampleProps {\n  preset: PlanPresetName;\n}\n\nexport function PlanPresetExample({ preset }: PlanPresetExampleProps) {\n  const data = planPresets[preset].data;\n  const code = generatePlanCode(preset);\n\n  return (\n    <Tabs items={[\"Preview\", \"Code\"]}>\n      <Tab value=\"Preview\">\n        <div className=\"not-prose mx-auto max-w-xl\">\n          <Plan {...data} />\n        </div>\n      </Tab>\n      <Tab value=\"Code\">\n        <TrackedDynamicCodeBlock\n          lang=\"tsx\"\n          code={code}\n          copyButtonLabel=\"preset example code\"\n        />\n      </Tab>\n    </Tabs>\n  );\n}\n\nfunction generateItemCarouselCode(preset: ItemCarouselPresetName): string {\n  const list = itemCarouselPresets[preset].data;\n  const props: string[] = [];\n\n  props.push(`  id=\"${list.id}\"`);\n  props.push(\n    `  items={${JSON.stringify(list.items, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n  props.push(`  onItemClick={(itemId) => console.log(\"Clicked:\", itemId)}`);\n  props.push(\n    `  onItemAction={(itemId, actionId) => console.log(\"Action:\", itemId, actionId)}`,\n  );\n\n  return `<ItemCarousel\\n${props.join(\"\\n\")}\\n/>`;\n}\n\ninterface ItemCarouselPresetExampleProps {\n  preset: ItemCarouselPresetName;\n}\n\nexport function ItemCarouselPresetExample({\n  preset,\n}: ItemCarouselPresetExampleProps) {\n  const data = itemCarouselPresets[preset].data;\n  const code = generateItemCarouselCode(preset);\n\n  return (\n    <Tabs items={[\"Preview\", \"Code\"]}>\n      <Tab value=\"Preview\">\n        <div className=\"not-prose\">\n          <ItemCarousel {...data} />\n        </div>\n      </Tab>\n      <Tab value=\"Code\">\n        <TrackedDynamicCodeBlock\n          lang=\"tsx\"\n          code={code}\n          copyButtonLabel=\"preset example code\"\n        />\n      </Tab>\n    </Tabs>\n  );\n}\n\nfunction generateApprovalCardCode(preset: ApprovalCardPresetName): string {\n  const data = approvalCardPresets[preset].data;\n  const props: string[] = [];\n\n  props.push(`  id=\"${data.id}\"`);\n  props.push(`  title=\"${data.title}\"`);\n\n  if (data.description) {\n    props.push(`  description=\"${data.description}\"`);\n  }\n\n  if (data.icon) {\n    props.push(`  icon=\"${data.icon}\"`);\n  }\n\n  if (data.metadata && data.metadata.length > 0) {\n    props.push(\n      `  metadata={${JSON.stringify(data.metadata, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n    );\n  }\n\n  if (data.variant) {\n    props.push(`  variant=\"${data.variant}\"`);\n  }\n\n  if (data.confirmLabel) {\n    props.push(`  confirmLabel=\"${data.confirmLabel}\"`);\n  }\n\n  if (data.cancelLabel) {\n    props.push(`  cancelLabel=\"${data.cancelLabel}\"`);\n  }\n\n  if (data.choice) {\n    props.push(`  choice=\"${data.choice}\"`);\n  }\n\n  return `<ApprovalCard\\n${props.join(\"\\n\")}\\n/>`;\n}\n\ninterface ApprovalCardPresetExampleProps {\n  preset: ApprovalCardPresetName;\n}\n\nexport function ApprovalCardPresetExample({\n  preset,\n}: ApprovalCardPresetExampleProps) {\n  const data = approvalCardPresets[preset].data;\n  const code = generateApprovalCardCode(preset);\n\n  return (\n    <Tabs items={[\"Preview\", \"Code\"]}>\n      <Tab value=\"Preview\">\n        <div className=\"not-prose mx-auto max-w-sm\">\n          <ApprovalCard {...data} />\n        </div>\n      </Tab>\n      <Tab value=\"Code\">\n        <TrackedDynamicCodeBlock\n          lang=\"tsx\"\n          code={code}\n          copyButtonLabel=\"preset example code\"\n        />\n      </Tab>\n    </Tabs>\n  );\n}\n\ninterface QuestionFlowPresetExampleProps {\n  preset: QuestionFlowPresetName;\n}\n\nexport function QuestionFlowPresetExample({\n  preset,\n}: QuestionFlowPresetExampleProps) {\n  const presetData = questionFlowPresets[preset];\n  const code = presetData.generateExampleCode(presetData.data);\n\n  return (\n    <Tabs items={[\"Preview\", \"Code\"]}>\n      <Tab value=\"Preview\">\n        <div className=\"not-prose mx-auto max-w-md\">\n          <QuestionFlow {...presetData.data} />\n        </div>\n      </Tab>\n      <Tab value=\"Code\">\n        <TrackedDynamicCodeBlock\n          lang=\"tsx\"\n          code={code}\n          copyButtonLabel=\"preset example code\"\n        />\n      </Tab>\n    </Tabs>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/preset-selector.tsx",
    "content": "\"use client\";\n\nimport {\n  Item,\n  ItemContent,\n  ItemDescription,\n  ItemGroup,\n  ItemTitle,\n} from \"@/components/ui/item\";\nimport { analytics } from \"@/lib/analytics\";\nimport { approvalCardPresets } from \"@/lib/presets/approval-card\";\nimport { chartPresets } from \"@/lib/presets/chart\";\nimport { citationPresets } from \"@/lib/presets/citation\";\nimport { codeBlockPresets } from \"@/lib/presets/code-block\";\nimport { codeDiffPresets } from \"@/lib/presets/code-diff\";\nimport { dataTablePresets } from \"@/lib/presets/data-table\";\nimport { geoMapPresets } from \"@/lib/presets/geo-map\";\nimport { imagePresets } from \"@/lib/presets/image\";\nimport { imageGalleryPresets } from \"@/lib/presets/image-gallery\";\nimport { videoPresets } from \"@/lib/presets/video\";\nimport { audioPresets } from \"@/lib/presets/audio\";\nimport { linkPreviewPresets } from \"@/lib/presets/link-preview\";\nimport { messageDraftPresets } from \"@/lib/presets/message-draft\";\nimport { itemCarouselPresets } from \"@/lib/presets/item-carousel\";\nimport { optionListPresets } from \"@/lib/presets/option-list\";\nimport { orderSummaryPresets } from \"@/lib/presets/order-summary\";\nimport { parameterSliderPresets } from \"@/lib/presets/parameter-slider\";\nimport { planPresets } from \"@/lib/presets/plan\";\nimport { preferencesPanelPresets } from \"@/lib/presets/preferences-panel\";\nimport { progressTrackerPresets } from \"@/lib/presets/progress-tracker\";\nimport { questionFlowPresets } from \"@/lib/presets/question-flow\";\nimport { statsDisplayPresets } from \"@/lib/presets/stats-display\";\nimport { terminalPresets } from \"@/lib/presets/terminal\";\nimport { weatherWidgetPresets } from \"@/lib/presets/weather-widget\";\nimport type { Preset } from \"@/lib/presets/types\";\nimport { cn } from \"@/lib/ui/cn\";\n\ntype PresetMap = Record<string, Preset<unknown>>;\n\nconst PRESET_REGISTRY: Record<string, PresetMap> = {\n  \"approval-card\": approvalCardPresets,\n  chart: chartPresets,\n  citation: citationPresets,\n  \"code-block\": codeBlockPresets,\n  \"code-diff\": codeDiffPresets,\n  \"data-table\": dataTablePresets,\n  \"geo-map\": geoMapPresets,\n  image: imagePresets,\n  \"image-gallery\": imageGalleryPresets,\n  video: videoPresets,\n  audio: audioPresets,\n  \"link-preview\": linkPreviewPresets,\n  \"message-draft\": messageDraftPresets,\n  \"item-carousel\": itemCarouselPresets,\n  \"option-list\": optionListPresets,\n  \"order-summary\": orderSummaryPresets,\n  \"parameter-slider\": parameterSliderPresets,\n  plan: planPresets,\n  \"preferences-panel\": preferencesPanelPresets,\n  \"progress-tracker\": progressTrackerPresets,\n  \"question-flow\": questionFlowPresets,\n  \"stats-display\": statsDisplayPresets,\n  terminal: terminalPresets,\n  \"weather-widget\": weatherWidgetPresets,\n};\n\nconst DEFAULT_COMPONENT = \"chart\";\n\nfunction getPresets(componentId: string): PresetMap {\n  return PRESET_REGISTRY[componentId] ?? PRESET_REGISTRY[DEFAULT_COMPONENT];\n}\n\nfunction formatPresetName(preset: string): string {\n  return preset.replaceAll(\"-\", \" \").replaceAll(\"_\", \" \");\n}\n\ninterface PresetSelectorProps {\n  componentId: string;\n  currentPreset: string;\n  onSelectPreset: (preset: string) => void;\n}\n\nexport function PresetSelector({\n  componentId,\n  currentPreset,\n  onSelectPreset,\n}: PresetSelectorProps) {\n  const presets = getPresets(componentId);\n  const presetNames = Object.keys(presets);\n\n  const handleSelect = (preset: string) => {\n    analytics.component.presetSelected(componentId, preset);\n    onSelectPreset(preset);\n  };\n\n  return (\n    <ItemGroup className=\"gap-1\">\n      {presetNames.map((name) => (\n        <PresetItem\n          key={name}\n          preset={name}\n          description={presets[name].description}\n          isSelected={currentPreset === name}\n          onSelect={handleSelect}\n        />\n      ))}\n    </ItemGroup>\n  );\n}\n\ninterface PresetItemProps {\n  preset: string;\n  description: string;\n  isSelected: boolean;\n  onSelect: (preset: string) => void;\n}\n\nfunction PresetItem({\n  preset,\n  description,\n  isSelected,\n  onSelect,\n}: PresetItemProps) {\n  return (\n    <Item\n      variant=\"default\"\n      size=\"sm\"\n      data-selected={isSelected}\n      className={cn(\n        \"group/item relative py-[2px] pb-[2px] lg:py-3!\",\n        isSelected\n          ? \"bg-primary/5 cursor-pointer border-transparent shadow-xs\"\n          : \"hover:bg-primary/5 active:bg-primary/10 cursor-pointer transition-[colors,shadow,border,background] duration-150 ease-out\",\n      )}\n      onClick={() => onSelect(preset)}\n    >\n      <ItemContent className=\"transform-gpu transition-transform duration-300 ease-[cubic-bezier(0.3,-0.55,0.27,1.55)] will-change-transform group-active/item:scale-[0.98] group-active/item:duration-100 group-active/item:ease-out\">\n        <div className=\"relative flex items-start justify-between\">\n          <div className=\"flex flex-1 flex-col gap-0 lg:gap-1\">\n            <ItemTitle className=\"flex w-full items-center justify-between capitalize\">\n              <span className=\"text-foreground\">\n                {formatPresetName(preset)}\n              </span>\n            </ItemTitle>\n            <ItemDescription className=\"text-sm font-light\">\n              {description}\n            </ItemDescription>\n          </div>\n        </div>\n        <SelectionIndicator isSelected={isSelected} />\n      </ItemContent>\n    </Item>\n  );\n}\n\ninterface SelectionIndicatorProps {\n  isSelected: boolean;\n}\n\nfunction SelectionIndicator({ isSelected }: SelectionIndicatorProps) {\n  return (\n    <span\n      aria-hidden=\"true\"\n      data-selected={isSelected}\n      className=\"bg-foreground absolute top-2.5 -left-4.5 h-5 w-1 origin-center -translate-y-1/2 scale-y-0 transform-gpu rounded-full opacity-0 transition-[opacity,transform] delay-100 duration-200 ease-out data-[selected=true]:scale-y-100 data-[selected=true]:opacity-100\"\n    />\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/_components/tracked-dynamic-codeblock.tsx",
    "content": "\"use client\";\n\nimport {\n  Children,\n  cloneElement,\n  isValidElement,\n  useCallback,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\nimport {\n  DynamicCodeBlock,\n  type DynamicCodeblockProps,\n} from \"fumadocs-ui/components/dynamic-codeblock\";\nimport { analytics } from \"@/lib/analytics\";\nimport {\n  detectInstallSnippetType,\n  getDocsCodeCopySource,\n} from \"@/lib/docs/install-snippet-analytics\";\n\ntype InstallSnippetType = ReturnType<typeof detectInstallSnippetType>;\n\ntype TrackedDynamicCodeBlockProps = DynamicCodeblockProps & {\n  copyButtonLabel?: string;\n};\n\ntype CopyButtonElementProps = {\n  \"aria-label\"?: string;\n  children?: ReactNode;\n  containerRef?: unknown;\n  \"data-checked\"?: unknown;\n  title?: string;\n};\n\nfunction resolveSnippetPreview(code: string): string {\n  const firstLine = code\n    .split(/\\r?\\n/)\n    .map((line) => line.trim())\n    .find((line) => line.length > 0);\n  if (!firstLine) {\n    return \"code snippet\";\n  }\n\n  const normalized = firstLine.replace(/\\s+/g, \" \");\n  return normalized.length > 60 ? `${normalized.slice(0, 57)}...` : normalized;\n}\n\nfunction resolveInstallCopyLabel(type: InstallSnippetType): string | null {\n  if (type === \"registry\") {\n    return \"registry install command\";\n  }\n\n  if (type === \"skills\") {\n    return \"skills install command\";\n  }\n\n  if (type === \"package_manager\") {\n    return \"package manager install command\";\n  }\n\n  return null;\n}\n\nfunction resolveCopyContextLabel({\n  code,\n  lang,\n  installSnippetType,\n  copyButtonLabel,\n}: {\n  code: string;\n  lang: string;\n  installSnippetType: InstallSnippetType;\n  copyButtonLabel?: string;\n}): string {\n  if (copyButtonLabel && copyButtonLabel.trim()) {\n    return copyButtonLabel.trim();\n  }\n\n  const installLabel = resolveInstallCopyLabel(installSnippetType);\n  if (installLabel) {\n    return installLabel;\n  }\n\n  const language = lang.trim().length > 0 ? lang.toUpperCase() : \"CODE\";\n  return `${language} snippet: ${resolveSnippetPreview(code)}`;\n}\n\nfunction relabelCopyButtons(\n  children: ReactNode,\n  contextLabel: string,\n): ReactNode {\n  return Children.map(children, (child) => {\n    if (!isValidElement(child)) {\n      return child;\n    }\n\n    const element = child as ReactElement<CopyButtonElementProps>;\n    const currentAriaLabel = element.props[\"aria-label\"];\n    const nestedChildren = element.props.children;\n    const relabeledChildren = nestedChildren\n      ? relabelCopyButtons(nestedChildren, contextLabel)\n      : nestedChildren;\n    const isFumadocsCopyButton =\n      typeof element.type === \"string\" &&\n      element.type === \"button\" &&\n      (currentAriaLabel === \"Copy Text\" || currentAriaLabel === \"Copied Text\");\n    const isFumadocsCopyButtonComponent =\n      typeof element.type !== \"string\" && \"containerRef\" in element.props;\n\n    if (isFumadocsCopyButton) {\n      const isCopied = Boolean(element.props[\"data-checked\"]);\n      const action = isCopied ? \"Copied\" : \"Copy\";\n      const ariaLabel = `${action} ${contextLabel}`;\n\n      return cloneElement(\n        element,\n        {\n          \"aria-label\": ariaLabel,\n          title: ariaLabel,\n        },\n        relabeledChildren,\n      );\n    }\n\n    if (isFumadocsCopyButtonComponent) {\n      const ariaLabel = `Copy ${contextLabel}`;\n      return cloneElement(element, {\n        \"aria-label\": ariaLabel,\n        title: ariaLabel,\n      });\n    }\n\n    if (relabeledChildren !== nestedChildren) {\n      return cloneElement(element, undefined, relabeledChildren);\n    }\n\n    return child;\n  });\n}\n\nexport function TrackedDynamicCodeBlock({\n  lang,\n  code,\n  codeblock,\n  copyButtonLabel,\n  ...props\n}: TrackedDynamicCodeBlockProps) {\n  const installSnippetType = detectInstallSnippetType(code);\n  const source = getDocsCodeCopySource(installSnippetType);\n  const copyContextLabel = resolveCopyContextLabel({\n    code,\n    lang,\n    installSnippetType,\n    copyButtonLabel,\n  });\n\n  const Actions = useCallback(\n    ({ className, children }: { className?: string; children?: ReactNode }) => (\n      <div\n        className={className}\n        onClick={(event) => {\n          const target = event.target as HTMLElement | null;\n          const clickedCopyButton = target?.closest(\"button\");\n          if (!clickedCopyButton) return;\n\n          analytics.code.blockCopied(lang, source);\n          if (installSnippetType) {\n            analytics.docs.installSnippetCopied(\n              installSnippetType,\n              \"docs_code_block\",\n            );\n          }\n        }}\n      >\n        {relabelCopyButtons(children, copyContextLabel)}\n      </div>\n    ),\n    [copyContextLabel, installSnippetType, lang, source],\n  );\n\n  return (\n    <DynamicCodeBlock\n      lang={lang}\n      code={code}\n      codeblock={{ ...codeblock, Actions }}\n      {...props}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/actions/actions-examples.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { DataTable } from \"@/components/tool-ui/data-table\";\nimport {\n  OptionList,\n  type OptionListSelection,\n} from \"@/components/tool-ui/option-list\";\nimport { OrderSummary } from \"@/components/tool-ui/order-summary\";\nimport {\n  ParameterSlider,\n  type SliderValue,\n} from \"@/components/tool-ui/parameter-slider\";\nimport {\n  PreferencesPanel,\n  type PreferencesValue,\n} from \"@/components/tool-ui/preferences-panel\";\nimport { ToolUI, createDecisionResult } from \"@/components/tool-ui/shared\";\nimport { Button } from \"@/components/ui/button\";\nimport { MockMessage, MockThread } from \"../_components/mock-thread\";\n\nconst tableRows = [\n  { id: \"1\", merchant: \"Delta Airlines\", amount: 847 },\n  { id: \"2\", merchant: \"Acme Hotel\", amount: 312 },\n];\n\nconst orderItems = [\n  { id: \"sku-1\", name: \"Wireless Keyboard\", unitPrice: 89, quantity: 1 },\n  { id: \"sku-2\", name: \"Mouse\", unitPrice: 49, quantity: 1 },\n];\n\nconst orderPricing = {\n  subtotal: 138,\n  shipping: 0,\n  tax: 12.42,\n  total: 150.42,\n  currency: \"USD\",\n};\n\nconst initialSliderValues: SliderValue[] = [\n  { id: \"exposure\", value: 0.2 },\n  { id: \"contrast\", value: 12 },\n];\n\nconst preferencesSections = [\n  {\n    heading: \"Notifications\",\n    items: [\n      {\n        id: \"marketing-email\",\n        type: \"switch\" as const,\n        label: \"Marketing email\",\n        description: \"Receive product announcements and feature updates.\",\n        defaultChecked: true,\n      },\n      {\n        id: \"digest-frequency\",\n        type: \"toggle\" as const,\n        label: \"Digest frequency\",\n        description: \"How often we send summary emails.\",\n        options: [\n          { value: \"daily\", label: \"Daily\" },\n          { value: \"weekly\", label: \"Weekly\" },\n        ],\n        defaultValue: \"weekly\",\n      },\n    ],\n  },\n];\n\nexport function ActionFlowToolCallVisual() {\n  return (\n    <MockThread caption=\"Step 1: the model chooses a tool call.\">\n      <MockMessage role=\"user\">\n        Show last month&apos;s travel expenses.\n      </MockMessage>\n      <MockMessage role=\"assistant\">\n        <div className=\"border-border bg-card rounded-xl border p-3 text-sm\">\n          Calling <code>getExpenses</code> with args:\n          <pre className=\"bg-muted mt-2 overflow-x-auto rounded-md p-2 text-xs\">\n            {`{\n  \"month\": \"2026-01\",\n  \"category\": \"travel\"\n}`}\n          </pre>\n        </div>\n      </MockMessage>\n    </MockThread>\n  );\n}\n\nexport function ActionFlowRenderVisual() {\n  return (\n    <div className=\"not-prose max-w-2xl\">\n      <DataTable\n        id=\"flow-render-surface\"\n        rowIdKey=\"id\"\n        columns={[\n          { key: \"merchant\", label: \"Merchant\" },\n          { key: \"amount\", label: \"Amount\", align: \"right\" },\n        ]}\n        data={tableRows}\n      />\n    </div>\n  );\n}\n\nexport function ActionFlowUserActionVisual() {\n  const [event, setEvent] = useState(\"No action clicked yet.\");\n\n  return (\n    <div className=\"not-prose flex max-w-2xl flex-col gap-3\">\n      <ToolUI id=\"flow-local-action\">\n        <ToolUI.Surface>\n          <DataTable\n            id=\"flow-local-action\"\n            rowIdKey=\"id\"\n            columns={[\n              { key: \"merchant\", label: \"Merchant\" },\n              { key: \"amount\", label: \"Amount\", align: \"right\" },\n            ]}\n            data={tableRows}\n          />\n        </ToolUI.Surface>\n        <ToolUI.Actions>\n          <ToolUI.LocalActions\n            actions={[\n              { id: \"export-csv\", label: \"Export CSV\", variant: \"secondary\" },\n              {\n                id: \"open-report\",\n                label: \"Open Full Report\",\n                variant: \"outline\",\n              },\n            ]}\n            onAction={(actionId) => {\n              setEvent(`Clicked: ${actionId}`);\n            }}\n          />\n        </ToolUI.Actions>\n      </ToolUI>\n      <p className=\"text-muted-foreground text-xs\">{event}</p>\n    </div>\n  );\n}\n\nexport function ActionFlowRuntimeHandleVisual() {\n  const [state, setState] = useState<\"idle\" | \"handling\" | \"done\">(\"idle\");\n\n  return (\n    <div className=\"not-prose flex max-w-xl flex-col gap-3\">\n      <div className=\"border-border bg-card rounded-xl border p-4\">\n        <p className=\"text-sm font-medium\">Runtime status</p>\n        <p className=\"text-muted-foreground mt-1 text-sm\">\n          {state === \"idle\" && \"Waiting for a user action.\"}\n          {state === \"handling\" && \"Handling action and running side effect...\"}\n          {state === \"done\" && \"Action handled successfully.\"}\n        </p>\n      </div>\n      <div className=\"flex items-center gap-2\">\n        <Button\n          size=\"sm\"\n          variant=\"secondary\"\n          onClick={() => {\n            setState(\"handling\");\n            setTimeout(() => setState(\"done\"), 600);\n          }}\n        >\n          Simulate handler\n        </Button>\n        <Button size=\"sm\" variant=\"ghost\" onClick={() => setState(\"idle\")}>\n          Reset\n        </Button>\n      </div>\n    </div>\n  );\n}\n\nexport function ActionFlowCommitVisual() {\n  const [orderChoice, setOrderChoice] = useState<\n    { action: \"confirm\"; orderId?: string; confirmedAt?: string } | undefined\n  >();\n  const [event, setEvent] = useState(\"No decision committed yet.\");\n\n  return (\n    <div className=\"not-prose flex max-w-md flex-col gap-3\">\n      <ToolUI id=\"flow-decision\">\n        <ToolUI.Surface>\n          {orderChoice ? (\n            <OrderSummary.Receipt\n              id=\"flow-decision\"\n              title=\"Order Summary\"\n              items={orderItems}\n              pricing={orderPricing}\n              choice={orderChoice}\n            />\n          ) : (\n            <OrderSummary.Display\n              id=\"flow-decision\"\n              title=\"Order Summary\"\n              items={orderItems}\n              pricing={orderPricing}\n            />\n          )}\n        </ToolUI.Surface>\n        {!orderChoice && (\n          <ToolUI.Actions>\n            <ToolUI.DecisionActions\n              actions={[\n                { id: \"cancel\", label: \"Cancel\", variant: \"outline\" },\n                { id: \"confirm\", label: \"Purchase\", variant: \"default\" },\n              ]}\n              onAction={(action) =>\n                createDecisionResult({\n                  decisionId: \"flow-order-decision\",\n                  action,\n                  payload:\n                    action.id === \"confirm\"\n                      ? {\n                          orderId: `ORD-${Date.now()}`,\n                          confirmedAt: new Date().toISOString(),\n                        }\n                      : undefined,\n                })\n              }\n              onCommit={(result) => {\n                if (result.actionId !== \"confirm\") {\n                  setEvent(\"Decision cancelled.\");\n                  return;\n                }\n\n                const orderId =\n                  typeof result.payload?.orderId === \"string\"\n                    ? result.payload.orderId\n                    : `ORD-${Date.now()}`;\n                const confirmedAt =\n                  typeof result.payload?.confirmedAt === \"string\"\n                    ? result.payload.confirmedAt\n                    : new Date().toISOString();\n\n                setOrderChoice({\n                  action: \"confirm\",\n                  orderId,\n                  confirmedAt,\n                });\n                setEvent(`Committed decision: ${orderId}`);\n              }}\n            />\n          </ToolUI.Actions>\n        )}\n      </ToolUI>\n      <div className=\"flex items-center gap-2\">\n        <p className=\"text-muted-foreground text-xs\">{event}</p>\n        {orderChoice && (\n          <Button\n            size=\"sm\"\n            variant=\"ghost\"\n            onClick={() => {\n              setOrderChoice(undefined);\n              setEvent(\"Decision reset for demo.\");\n            }}\n          >\n            Reset\n          </Button>\n        )}\n      </div>\n    </div>\n  );\n}\n\nexport function DisplaySurfaceLocalActionsExample() {\n  const [event, setEvent] = useState(\"Try a local action below.\");\n\n  return (\n    <div className=\"not-prose flex max-w-2xl flex-col gap-3\">\n      <ToolUI id=\"display-local-actions-table\">\n        <ToolUI.Surface>\n          <DataTable\n            id=\"display-local-actions-table\"\n            rowIdKey=\"id\"\n            columns={[\n              { key: \"merchant\", label: \"Merchant\" },\n              { key: \"amount\", label: \"Amount\", align: \"right\" },\n            ]}\n            data={tableRows}\n          />\n        </ToolUI.Surface>\n        <ToolUI.Actions>\n          <ToolUI.LocalActions\n            actions={[\n              { id: \"export-csv\", label: \"Export CSV\", variant: \"secondary\" },\n              {\n                id: \"open-report\",\n                label: \"Open Full Report\",\n                variant: \"outline\",\n              },\n            ]}\n            onAction={(actionId) => {\n              setEvent(`Local action executed: ${actionId}`);\n            }}\n          />\n        </ToolUI.Actions>\n      </ToolUI>\n      <p className=\"text-muted-foreground text-xs\">{event}</p>\n    </div>\n  );\n}\n\nexport function DecisionSurfaceExample() {\n  const [orderChoice, setOrderChoice] = useState<\n    { action: \"confirm\"; orderId?: string; confirmedAt?: string } | undefined\n  >();\n  const [event, setEvent] = useState(\"No decision committed yet.\");\n\n  return (\n    <div className=\"not-prose flex max-w-md flex-col gap-3\">\n      <ToolUI id=\"decision-actions-order\">\n        <ToolUI.Surface>\n          {orderChoice ? (\n            <OrderSummary.Receipt\n              id=\"decision-actions-order\"\n              title=\"Order Summary\"\n              items={orderItems}\n              pricing={orderPricing}\n              choice={orderChoice}\n            />\n          ) : (\n            <OrderSummary.Display\n              id=\"decision-actions-order\"\n              title=\"Order Summary\"\n              items={orderItems}\n              pricing={orderPricing}\n            />\n          )}\n        </ToolUI.Surface>\n        {!orderChoice && (\n          <ToolUI.Actions>\n            <ToolUI.DecisionActions\n              actions={[\n                { id: \"cancel\", label: \"Cancel\", variant: \"outline\" },\n                { id: \"confirm\", label: \"Purchase\", variant: \"default\" },\n              ]}\n              onAction={(action) =>\n                createDecisionResult({\n                  decisionId: \"decision-actions-order-decision\",\n                  action,\n                  payload:\n                    action.id === \"confirm\"\n                      ? {\n                          orderId: `ORD-${Date.now()}`,\n                          confirmedAt: new Date().toISOString(),\n                        }\n                      : undefined,\n                })\n              }\n              onCommit={(result) => {\n                if (result.actionId !== \"confirm\") {\n                  setEvent(\"Decision cancelled.\");\n                  return;\n                }\n\n                const orderId =\n                  typeof result.payload?.orderId === \"string\"\n                    ? result.payload.orderId\n                    : `ORD-${Date.now()}`;\n                const confirmedAt =\n                  typeof result.payload?.confirmedAt === \"string\"\n                    ? result.payload.confirmedAt\n                    : new Date().toISOString();\n\n                setOrderChoice({\n                  action: \"confirm\",\n                  orderId,\n                  confirmedAt,\n                });\n                setEvent(`Committed decision envelope: ${orderId}`);\n              }}\n            />\n          </ToolUI.Actions>\n        )}\n      </ToolUI>\n      <div className=\"flex items-center gap-2\">\n        <p className=\"text-muted-foreground text-xs\">{event}</p>\n        {orderChoice && (\n          <Button\n            size=\"sm\"\n            variant=\"ghost\"\n            onClick={() => {\n              setOrderChoice(undefined);\n              setEvent(\"Decision reset for demo.\");\n            }}\n          >\n            Reset\n          </Button>\n        )}\n      </div>\n    </div>\n  );\n}\n\nexport function ActionCentricExceptionsExample() {\n  const [optionChoice, setOptionChoice] = useState<OptionListSelection>();\n  const [optionOutput, setOptionOutput] = useState<{\n    actionId: string;\n    state: OptionListSelection;\n  } | null>(null);\n  const [sliderValues, setSliderValues] =\n    useState<SliderValue[]>(initialSliderValues);\n  const [sliderOutput, setSliderOutput] = useState<{\n    actionId: string;\n    state: SliderValue[];\n  } | null>(null);\n  const [savedPreferences, setSavedPreferences] =\n    useState<PreferencesValue | null>(null);\n  const [preferencesOutput, setPreferencesOutput] = useState<{\n    actionId: string;\n    state: PreferencesValue;\n  } | null>(null);\n\n  return (\n    <div className=\"not-prose grid gap-8\">\n      <div className=\"grid gap-4 xl:grid-cols-[minmax(0,1fr)_300px] xl:items-start\">\n        <div className=\"flex flex-col gap-2\">\n          <h4 className=\"text-base font-semibold\">OptionList</h4>\n          <OptionList\n            id=\"action-centric-option-list\"\n            selectionMode=\"single\"\n            options={[\n              {\n                id: \"merge\",\n                label: \"Merge duplicates\",\n                description: \"Combine records and preserve all unique fields.\",\n              },\n              {\n                id: \"keep\",\n                label: \"Keep separate\",\n                description: \"Leave both records untouched.\",\n              },\n              {\n                id: \"review\",\n                label: \"Review manually\",\n                description: \"Open each pair for manual confirmation.\",\n              },\n            ]}\n            choice={optionChoice}\n            actions={[\n              { id: \"cancel\", label: \"Clear\", variant: \"ghost\" },\n              { id: \"confirm\", label: \"Confirm Selection\", variant: \"default\" },\n            ]}\n            onAction={(actionId, selection) => {\n              setOptionOutput({ actionId, state: selection });\n\n              if (actionId === \"confirm\") {\n                setOptionChoice(selection);\n                return;\n              }\n\n              if (actionId === \"cancel\") {\n                setOptionChoice(undefined);\n              }\n            }}\n          />\n          <div className=\"flex items-center gap-2\">\n            {optionChoice !== undefined && (\n              <Button\n                size=\"sm\"\n                variant=\"ghost\"\n                onClick={() => {\n                  setOptionChoice(undefined);\n                  setOptionOutput(null);\n                }}\n              >\n                Reset\n              </Button>\n            )}\n          </div>\n        </div>\n        <div className=\"border-border bg-card rounded-xl border p-3\">\n          <p className=\"text-sm font-medium\">Mock output</p>\n          <p className=\"text-muted-foreground mt-1 text-xs\">\n            <code>onAction(actionId, state)</code>\n          </p>\n          <pre className=\"bg-muted mt-3 overflow-auto rounded-md p-3 text-xs leading-relaxed\">\n            {optionOutput\n              ? JSON.stringify(optionOutput, null, 2)\n              : `{\n  \"actionId\": \"confirm\",\n  \"state\": \"merge\"\n}`}\n          </pre>\n        </div>\n      </div>\n\n      <div className=\"grid gap-4 xl:grid-cols-[minmax(0,1fr)_300px] xl:items-start\">\n        <div className=\"flex flex-col gap-2\">\n          <h4 className=\"text-base font-semibold\">ParameterSlider</h4>\n          <ParameterSlider\n            id=\"action-centric-parameter-slider\"\n            sliders={[\n              {\n                id: \"exposure\",\n                label: \"Exposure\",\n                min: -2,\n                max: 2,\n                step: 0.1,\n                value: 0.2,\n                unit: \" EV\",\n                precision: 1,\n              },\n              {\n                id: \"contrast\",\n                label: \"Contrast\",\n                min: -50,\n                max: 50,\n                step: 1,\n                value: 12,\n                unit: \"%\",\n              },\n            ]}\n            values={sliderValues}\n            onChange={setSliderValues}\n            actions={[\n              { id: \"reset\", label: \"Reset\", variant: \"ghost\" },\n              { id: \"apply\", label: \"Apply Adjustments\", variant: \"default\" },\n            ]}\n            onAction={(actionId, values) => {\n              setSliderOutput({ actionId, state: values });\n            }}\n          />\n        </div>\n        <div className=\"border-border bg-card rounded-xl border p-3\">\n          <p className=\"text-sm font-medium\">Mock output</p>\n          <p className=\"text-muted-foreground mt-1 text-xs\">\n            <code>onAction(actionId, state)</code>\n          </p>\n          <pre className=\"bg-muted mt-3 overflow-auto rounded-md p-3 text-xs leading-relaxed\">\n            {sliderOutput\n              ? JSON.stringify(sliderOutput, null, 2)\n              : `{\n  \"actionId\": \"apply\",\n  \"state\": [\n    { \"id\": \"exposure\", \"value\": 0.2 },\n    { \"id\": \"contrast\", \"value\": 12 }\n  ]\n}`}\n          </pre>\n        </div>\n      </div>\n\n      <div className=\"grid gap-4 xl:grid-cols-[minmax(0,1fr)_300px] xl:items-start\">\n        <div className=\"flex flex-col gap-2\">\n          <h4 className=\"text-base font-semibold\">PreferencesPanel</h4>\n          <PreferencesPanel\n            id=\"action-centric-preferences-panel\"\n            title=\"Notification Preferences\"\n            sections={preferencesSections}\n            actions={[\n              { id: \"cancel\", label: \"Cancel\", variant: \"ghost\" },\n              { id: \"save\", label: \"Save Preferences\", variant: \"default\" },\n            ]}\n            onAction={(actionId, value) => {\n              setPreferencesOutput({ actionId, state: value });\n\n              if (actionId === \"save\") {\n                setSavedPreferences(value);\n                return;\n              }\n              if (actionId === \"cancel\") {\n                setSavedPreferences(null);\n              }\n            }}\n          />\n        </div>\n        <div className=\"border-border bg-card rounded-xl border p-3\">\n          <p className=\"text-sm font-medium\">Mock output</p>\n          <p className=\"text-muted-foreground mt-1 text-xs\">\n            <code>onAction(actionId, state)</code>\n          </p>\n          <pre className=\"bg-muted mt-3 overflow-auto rounded-md p-3 text-xs leading-relaxed\">\n            {preferencesOutput\n              ? JSON.stringify(preferencesOutput, null, 2)\n              : `{\n  \"actionId\": \"save\",\n  \"state\": {\n    \"marketing-email\": true,\n    \"digest-frequency\": \"weekly\"\n  }\n}`}\n          </pre>\n          {savedPreferences && (\n            <p className=\"text-muted-foreground mt-2 text-xs\">\n              Last saved values are shown in the callback payload.\n            </p>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/actions/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\nimport {\n  ActionFlowCommitVisual,\n  ActionFlowRenderVisual,\n  ActionFlowRuntimeHandleVisual,\n  ActionFlowToolCallVisual,\n  ActionFlowUserActionVisual,\n  ActionCentricExceptionsExample,\n  DecisionSurfaceExample,\n  DisplaySurfaceLocalActionsExample,\n} from \"./actions-examples\";\n\n<DocsHeader\n  title=\"Actions\"\n  description=\"Interactive controls on tool results.\"\n  mdxPath=\"app/docs/actions/content.mdx\"\n/>\n\nBy default, tool results are read-only. The assistant calls a tool, a component renders the data, and that's it. Actions change that: they let users **respond** to what the assistant shows them.\n\nAn action might be a small utility (exporting a table as CSV, copying a code snippet) or a consequential decision like approving a deploy or confirming a purchase. Tool UI splits these into two categories: **local actions** for in-context side effects, and **decision actions** for choices that return to the assistant and produce a permanent [receipt](/docs/receipts).\n\nFor background on how components relate to the assistant and the user, see [Design Guidelines](/docs/design-guidelines).\n\n## Mental Model\n\n<Steps>\n\n<Step title=\"The model calls a tool\">\n\nThe model chooses a tool and sends structured arguments.\n\n<ActionFlowToolCallVisual />\n\n```ts\ntools: {\n  getExpenses: tool({\n    description: \"Return travel expenses for a month\",\n    execute: async ({ month }) => ({ id: \"expenses-jan\", month }),\n  }),\n}\n```\n\n</Step>\n\n<Step title=\"Your app renders a Tool UI surface\">\n\nThe returned args are parsed and rendered as a display surface.\n\n<ActionFlowRenderVisual />\n\n```tsx\nrender: ({ result }) => {\n  const parsed = safeParseSerializableDataTable(result);\n  if (!parsed) {\n    return null;\n  }\n  return <DataTable {...parsed} />;\n},\n```\n\n</Step>\n\n<Step title=\"The user triggers an action\">\n\nThe user clicks an action control associated with the surface.\n\n<ActionFlowUserActionVisual />\n\n```tsx\nconst localActions = [{ id: \"export-csv\", label: \"Export CSV\" }];\n\nasync function handleLocalAction(actionId: string) {\n  // side effects only\n}\n```\n\n</Step>\n\n<Step title=\"The runtime handles the action\">\n\nYour action handler runs side effects (navigation, export, API call, etc.).\n\n<ActionFlowRuntimeHandleVisual />\n\n```ts\nasync function handleLocalAction(actionId: string) {\n  if (actionId === \"export-csv\") await exportCsv();\n  if (actionId === \"open-report\") router.push(\"/reports/monthly\");\n}\n```\n\n</Step>\n\n<Step title=\"For consequential actions, commit decision outcome\">\n\nIf the action is consequential, commit a durable outcome (typically via `addResult(...)` in your renderer runtime).\n\n<ActionFlowCommitVisual />\n\n```tsx\nconst decision = createDecisionResult({\n  decisionId: \"order-123\",\n  action: { id: \"confirm\", label: \"Purchase\" },\n});\n\nawait addResult?.(decision);\n```\n\n</Step>\n\n</Steps>\n\n## Action Surfaces\n\nTool UI has two action surfaces:\n\n- **`ToolUI.LocalActions`**: Non-consequential side effects. Exporting, copying, opening a link. The assistant doesn't need to know it happened.\n- **`ToolUI.DecisionActions`**: Consequential choices that produce a durable decision envelope. Approving, confirming, selecting. The result returns to the assistant and the component shows a [receipt](/docs/receipts).\n\n**When to use which:** If the user's action changes the conversation, if the assistant should know about it and respond, use `DecisionActions`. If it's a utility that stays local to the UI, use `LocalActions`.\n\n### Display Components + LocalActions\n\nFor display-first components, keep rendering and actions separate: the display component goes inside `ToolUI.Surface`, and local actions go inside sibling `ToolUI.Actions`.\n\n<DisplaySurfaceLocalActionsExample />\n\n`LocalActions` handlers should not commit durable result state.\n\n### Decision Components + DecisionActions\n\nWhen user intent is consequential (approve, confirm, purchase, publish, delete), use `DecisionActions`. This makes decision payload shape explicit and commit behavior auditable.\n\n<DecisionSurfaceExample />\n\n`DecisionActions` handlers should return a typed envelope, then commit in the commit phase.\n\n## Action-Centric Components (Intentional Exception)\n\nSome components are action-centric by design. Their action handling is part of component semantics, so actions stay embedded on the component rather than using sibling `ToolUI.Actions` surfaces.\n\n### Shared Embedded Contract\n\nAll three action-centric components use the same embedded action interface:\n\n- `actions`: action buttons rendered by the component.\n- `onAction(actionId, state)`: runs after the action behavior and receives post-action state.\n- `onBeforeAction(actionId, state)`: guard evaluated before an action runs.\n\nOnly the `state` type changes by component:\n\n- `OptionList`: `OptionListSelection`\n- `ParameterSlider`: `SliderValue[]`\n- `PreferencesPanel`: `PreferencesValue`\n\n### Component Examples + Mock Outputs\n\nEach example below shows the live component on the left and the corresponding mock runtime output on the right.\n\n<ActionCentricExceptionsExample />\n"
  },
  {
    "path": "apps/www/app/docs/actions/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Actions\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\n    \"Actions\",\n    \"External local and decision action surfaces for Tool UI\",\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/actions/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { DocsArticle } from \"../_components/docs-article\";\n\nexport const metadata: Metadata = {\n  title: \"Actions\",\n  description: \"Action model basics.\",\n};\n\nexport const revalidate = 3600;\n\nexport default function ActionsPage() {\n  return (\n    <DocsArticle>\n      <Content />\n    </DocsArticle>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/advanced/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\n\n<DocsHeader\n  title=\"Advanced\"\n  description=\"Typing, frontend tools, and more.\"\n  mdxPath=\"app/docs/advanced/content.mdx\"\n/>\n\nThe [Quick Start](/docs/quick-start) covers the basics — installing a component, registering it, and seeing it render. This page adds three patterns: type-safe tool results with `InferUITools`, frontend tools that run entirely in the browser, and auto-continue for multi-step flows.\n\n## Tool Type Inference\n\n`InferUITools` (AI SDK 6) infers tool input/output types from your tool set and gives you fully typed `message.parts` in the UI.\n\n- [**InferUITools reference**](https://ai-sdk.dev/docs/reference/ai-sdk-ui/infer-ui-tools)\n\n<Steps>\n\n<Step>\n\n### Define Tools with Output Schemas\n\n```ts title=\"lib/tools.ts\"\nimport { tool } from \"ai\";\nimport { z } from \"zod\";\nimport { SerializableLinkPreviewSchema } from \"@/components/tool-ui/link-preview/schema\";\n\nexport const tools = {\n  previewLink: tool({\n    description: \"Return a simple link preview\",\n    inputSchema: z.object({ url: z.url() }),\n    outputSchema: SerializableLinkPreviewSchema,\n    async execute({ url }) {\n      return {\n        id: \"link-preview-1\",\n        href: url,\n        title: \"React Server Components\",\n        image:\n          \"https://images.unsplash.com/photo-1633356122544-f134324a6cee?auto=format&fit=crop&q=80&w=1200\",\n        domain: new URL(url).hostname,\n      };\n    },\n  }),\n} as const;\n\n// Export a type only; the client should import types, not server code.\nexport type Tools = typeof tools;\n```\n\n</Step>\n\n<Step>\n\n### Use Tools on the Server\n\n```ts\nimport { streamText, convertToModelMessages } from \"ai\";\nimport { openai } from \"@ai-sdk/openai\";\nimport { tools } from \"@/lib/tools\";\n\nexport async function POST(req: Request) {\n  const { messages } = await req.json();\n\n  const result = streamText({\n    model: openai(\"gpt-5-nano\"),\n    messages: await convertToModelMessages(messages),\n    tools,\n  });\n\n  return result.toUIMessageStreamResponse();\n}\n```\n\n</Step>\n\n<Step>\n\n### Infer UI-Level Types in the Client\n\n```tsx\n\"use client\";\n\nimport { useChat } from \"@ai-sdk/react\";\nimport { InferUITools } from \"ai\";\nimport type { Tools } from \"@/lib/tools\";\nimport { LinkPreview } from \"@/components/tool-ui/link-preview\";\nimport { safeParseSerializableLinkPreview } from \"@/components/tool-ui/link-preview/schema\";\n\ntype MyUITools = InferUITools<Tools>;\n\nexport default function Chat() {\n  const { messages } = useChat<MyUITools>({ api: \"/api/chat\" });\n\n  return (\n    <div>\n      {messages.map((m) => (\n        <div key={m.id}>\n          {m.parts.map((part, i) => {\n            // Fully typed: 'tool-previewLink' with correct output shape\n            if (\n              part.type === \"tool-previewLink\" &&\n              part.state === \"output-available\"\n            ) {\n              const preview = safeParseSerializableLinkPreview(part.output);\n              if (!preview) return null;\n              return <LinkPreview key={i} {...preview} />;\n            }\n            return null;\n          })}\n        </div>\n      ))}\n    </div>\n  );\n}\n```\n\n</Step>\n\n</Steps>\n\n## Tips\n\n### Keep runtime boundaries clean\n\nExport only types from `lib/tools.ts` to the client. **Never import server code into the browser.**\n\n### Single-tool helpers\n\n`InferUITool` (singular) does the same thing for a single tool definition.\n\n### End-to-end validation\n\nUse the component's `outputSchema` on the server and `safeParseSerializable{componentName}` on the client to validate at both ends.\n\n### Routing by lifecycle state (outside Tool UI renderers)\n\nIf you consume raw `message.parts`, keep lifecycle handling outside Tool UI components:\n\n- `state === 'invocation'` / `state === 'output-pending'` / `state === 'errored'`:\n  route to app-level UI (message shell, transport error, telemetry), not the Tool UI component.\n- `state === 'output-available'`:\n  parse `part.output` with `safeParseSerializable{componentName}` and render only when parsing succeeds.\n\nInside Tool UI toolkit renderers, use a strict gate:\n\n```tsx\nconst parsed = safeParseSerializableX(resultOrArgs);\nif (!parsed) return null;\nreturn <X {...parsed} />;\n```\n\n## Frontend tools (interactive Tool UIs)\n\nDecision surfaces like OptionList work best as **frontend tools**. The model calls a tool with component props as arguments, then your UI calls `addResult(...)` after the user confirms. That result feeds back into the conversation.\n\nIf you're using `AssistantChatTransport`, it forwards **system instructions** and **registered tools** in the request body. Your `/api/chat` route needs to read and forward them, otherwise the model won't see your frontend tools.\n\n```ts title=\"app/api/chat/route.ts\"\nimport { openai } from \"@ai-sdk/openai\";\nimport {\n  streamText,\n  convertToModelMessages,\n  jsonSchema,\n  type UIMessage,\n  type JSONSchema7,\n} from \"ai\";\n\ntype ForwardedTools = Record<\n  string,\n  { description?: string; parameters: JSONSchema7 }\n>;\n\nfunction toStreamTextTools(tools?: ForwardedTools) {\n  if (!tools) return undefined;\n  return Object.fromEntries(\n    Object.entries(tools).map(([name, tool]) => [\n      name,\n      {\n        ...(tool.description ? { description: tool.description } : {}),\n        inputSchema: jsonSchema(tool.parameters),\n      },\n    ]),\n  );\n}\n\nexport async function POST(req: Request) {\n  const body = (await req.json()) as {\n    messages: UIMessage[];\n    system?: string;\n    tools?: ForwardedTools;\n  };\n\n  const result = streamText({\n    model: openai(\"gpt-4o\"),\n    messages: await convertToModelMessages(body.messages),\n    system: body.system,\n    tools: toStreamTextTools(body.tools),\n  });\n\n  return result.toUIMessageStreamResponse();\n}\n```\n\n### Register a frontend tool\n\nRegister the tool on the client so `AssistantChatTransport` forwards it to the model.\n\n```tsx\n\"use client\";\n\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { OptionList } from \"@/components/tool-ui/option-list\";\nimport {\n  safeParseSerializableOptionList,\n  SerializableOptionListSchema,\n} from \"@/components/tool-ui/option-list/schema\";\n\nexport const toolkit: Toolkit = {\n  selectFormat: {\n    // description and parameters are forwarded to the model.\n    description: \"Ask the user to choose an output format.\",\n    parameters: SerializableOptionListSchema,\n    // For frontend tools, parse args and render; handle result for receipt state.\n    render: ({ args, toolCallId, result, addResult }) => {\n      const parsedArgs = safeParseSerializableOptionList({\n        ...args,\n        id: args?.id ?? `format-selection-${toolCallId}`,\n      });\n      if (!parsedArgs) {\n        return null;\n      }\n      if (result) {\n        // After the user confirms, render the receipt state.\n        return <OptionList {...parsedArgs} value={undefined} choice={result} />;\n      }\n      return (\n        <OptionList\n          {...parsedArgs}\n          value={undefined}\n          onAction={(actionId, selection) => {\n            if (actionId === \"confirm\") addResult?.(selection);\n          }}\n        />\n      );\n    },\n  },\n};\n```\n\nPass this toolkit to `Tools({ toolkit })` under `<AssistantRuntimeProvider>` so the definition and renderer are forwarded by `AssistantChatTransport`.\n\n### Auto-continue after `addResult(...)`\n\nWhen a tool produces results that need follow-up — a search that needs a summary, a calculation that needs interpretation — auto-continue tells the assistant to proceed without waiting for user input. Enable it with `sendAutomaticallyWhen`:\n\n```tsx\nimport {\n  useChatRuntime,\n  AssistantChatTransport,\n} from \"@assistant-ui/react-ai-sdk\";\nimport { lastAssistantMessageIsCompleteWithToolCalls } from \"ai\";\n\nconst runtime = useChatRuntime({\n  transport: new AssistantChatTransport({ api: \"/api/chat\" }),\n  // Automatically send when the assistant finishes with tool calls\n  sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,\n});\n```\n"
  },
  {
    "path": "apps/www/app/docs/advanced/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Advanced\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\"Advanced\", \"Deep dive into customization\");\n}\n"
  },
  {
    "path": "apps/www/app/docs/advanced/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { DocsArticle } from \"../_components/docs-article\";\n\nexport const metadata: Metadata = {\n  title: \"Advanced\",\n  description: \"Advanced configuration and usage patterns\",\n};\n\nexport const revalidate = 3600;\n\nexport default function AdvancedDocsPage() {\n  return (\n    <DocsArticle>\n      <Content />\n    </DocsArticle>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/agent-skills/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\n\n<DocsHeader\n  title=\"Agent Skills\"\n  description=\"AI-assisted component integration.\"\n  mdxPath=\"app/docs/agent-skills/content.mdx\"\n/>\n\n## Skill installation\n\n```bash\nnpx skills add https://github.com/assistant-ui/tool-ui --skill tool-ui\n```\n\n## What's included\n\nThis package gives coding agents everything they need to integrate Tool UI components:\n\n- **SKILL.md** — the five-step workflow (see below)\n- **Reference docs** — component catalog, recipes, integration patterns, troubleshooting\n- **CLI scripts** — compatibility checks, search, scaffolding\n- **Test suites** — regression coverage for the scripts\n\n## SKILL.md — the workflow\n\n`.agents/skills/tool-ui/SKILL.md` is the entry point. Follow these five steps:\n\n1. **Compatibility check** — verify prerequisites (shadcn/ui, Tailwind, Radix)\n2. **Discover** — search and browse available components\n3. **Install** — pull components from the registry\n4. **Scaffold** — generate adapter files and runtime wiring code\n5. **Validate** — confirm the integration works\n\nEach step links to the relevant script or reference doc.\n\n## Reference docs\n\nAll reference material lives in `.agents/skills/tool-ui/references/`.\n\n### Discover\n\n**`components-catalog.md`** — Browse components by category. Each entry includes a description and guidelines for picking the right one for your use case.\n\n**`components-data.json`** — Machine-readable metadata that the CLI scripts use for search and install operations.\n\n### Install\n\n**`recipes.md`** — Pre-built bundles for common patterns (planning flow, research output, commerce flow). Each recipe has a one-line install command that pulls the entire bundle.\n\n### Scaffold\n\n**`integration-patterns.md`** — Five wiring patterns with full code examples:\n\n- **Backend display** — render tool results as read-only UI\n- **Backend + local actions** — add inline actions alongside backend data\n- **Frontend decision** — collect user input before proceeding\n- **Action-centric** — surface standalone actions without surrounding content\n- **Manual** — wire components directly without the runtime framework\n\n### Validate\n\n**`troubleshooting.md`** — Symptom → cause → fix matrix for common install and runtime issues.\n\n## CLI scripts\n\nAll scripts live in `.agents/skills/tool-ui/scripts/`.\n\n### `tool_ui_compat.py`\n\nChecks `components.json` compatibility, auto-fixes the `@tool-ui` registry entry, and runs deeper doctor checks — shared folder presence, import resolution, and package dependencies.\n\n### `tool_ui_components.py`\n\nLists, searches, and generates install commands for components. Also handles bundle recipes from `recipes.md`. Reads from `components-data.json` for fast lookups.\n\n### `tool_ui_scaffold.py`\n\nGenerates ready-to-paste runtime wiring code in three modes: `assistant-backend`, `assistant-frontend`, and `manual`. Output adapts to the project's framework and integration pattern.\n\n## Test suites\n\nScript tests in `.agents/skills/tool-ui/tests/` provide regression coverage for the CLI scripts. They validate SKILL.md references, script syntax, and data integrity.\n"
  },
  {
    "path": "apps/www/app/docs/agent-skills/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Agent Skills\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\n    \"Agent Skills\",\n    \"Let your coding agent to the heavy lifting.\",\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/agent-skills/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { DocsArticle } from \"../_components/docs-article\";\n\nexport const metadata: Metadata = {\n  title: \"Agent Skills\",\n  description: \"Let your coding agent to the heavy lifting.\",\n};\n\nexport const revalidate = 3600;\n\nexport default function AgentSkillsPage() {\n  return (\n    <DocsArticle>\n      <Content />\n    </DocsArticle>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/approval-card/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\nimport { ApprovalCard } from \"@/components/tool-ui/approval-card\";\n\n<DocsHeader\n  title=\"Approval Card\"\n  description=\"Binary confirmation for agent actions.\"\n  mdxPath=\"app/docs/approval-card/content.mdx\"\n/>\n\nBefore the assistant deploys to production, deletes a project, or sends an email on your behalf, it should ask first. ApprovalCard renders a confirmation prompt with context about the action (metadata, consequences, a clear approve/deny choice) and records the decision as a receipt once the user responds.\n\n**Role:** Decision. For choices that return to the assistant. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"approval-card\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nRender `ApprovalCard` in your UI with tool-compatible props.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\nimport { ApprovalCard } from \"@/components/tool-ui/approval-card\";\n\nexport function Example() {\n  return (\n    <ApprovalCard\n      id=\"approval-card-example\"\n      title=\"Deploy to Production?\"\n      description=\"This will push the latest changes to all users.\"\n      icon=\"Rocket\"\n      confirmLabel=\"Deploy\"\n      onConfirm={() => console.log(\"Approved\")}\n      onCancel={() => console.log(\"Denied\")}\n    />\n  );\n}\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegister this renderer so tool results display as `ApprovalCard`.\n\n```tsx\n\"use client\";\n\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { ApprovalCard } from \"@/components/tool-ui/approval-card\";\nimport {\n  safeParseSerializableApprovalCard,\n  SerializableApprovalCardSchema,\n} from \"@/components/tool-ui/approval-card/schema\";\n\nexport const toolkit: Toolkit = {\n  requestApproval: {\n    description: \"Request user approval before performing an action.\",\n    parameters: SerializableApprovalCardSchema,\n    render: ({ args, toolCallId, result, addResult }) => {\n      const parsedArgs = safeParseSerializableApprovalCard({\n        ...args,\n        id: args?.id ?? `approval-${toolCallId}`,\n      });\n      if (!parsedArgs) {\n        return null;\n      }\n      return result ? (\n        <ApprovalCard {...parsedArgs} choice={result} />\n      ) : (\n        <ApprovalCard\n          {...parsedArgs}\n          onConfirm={() => addResult?.(\"approved\")}\n          onCancel={() => addResult?.(\"denied\")}\n        />\n      );\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"ShieldCheck\" title=\"Human-in-the-loop\">\n    Blocks agent actions until the user approves or denies\n  </Feature>\n  <Feature icon=\"AlertTriangle\" title=\"Destructive variant\">\n    Red styling for irreversible or dangerous operations\n  </Feature>\n  <Feature icon=\"List\" title=\"Contextual metadata\">\n    Key-value pairs giving context about the action\n  </Feature>\n  <Feature icon=\"CheckCircle2\" title=\"Receipt state\">\n    Compact read-only view of the decision in conversation history\n  </Feature>\n</FeatureGrid>\n\n## Receipt State\n\nPass a `choice` prop to render this component in its receipt state. See [Receipts](/docs/receipts) for the pattern.\n\nThe receipt shows the outcome (approved or denied) with the custom button label you specified, fades in with a subtle animation, and is read-only.\n\n## Destructive Variant\n\nSet `variant=\"destructive\"` for dangerous or irreversible actions. The icon background and confirm button turn red.\n\n## Props\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for the approval card\",\n      type: \"string\",\n      required: true,\n    },\n    title: {\n      description: \"The action being approved\",\n      type: \"string\",\n      required: true,\n    },\n    description: {\n      description: \"Additional context about the action\",\n      type: \"string\",\n    },\n    icon: {\n      description:\n        'Lucide icon name (e.g., \"Rocket\", \"trash-2\"). Accepts PascalCase or kebab-case.',\n      type: \"string\",\n    },\n    metadata: {\n      description: \"Key-value pairs providing context\",\n      type: \"MetadataItem[]\",\n    },\n    variant: {\n      description: \"Visual style variant\",\n      type: \"'default' | 'destructive'\",\n      default: \"'default'\",\n    },\n    confirmLabel: {\n      description: \"Label for the confirm button\",\n      type: \"string\",\n      default: \"'Approve'\",\n    },\n    cancelLabel: {\n      description: \"Label for the cancel button\",\n      type: \"string\",\n      default: \"'Deny'\",\n    },\n    choice: {\n      description: \"The finalized decision (renders receipt state when set)\",\n      type: \"'approved' | 'denied'\",\n    },\n    onConfirm: {\n      description: \"Handler when user approves\",\n      type: \"() => void | Promise<void>\",\n    },\n    onCancel: {\n      description: \"Handler when user denies\",\n      type: \"() => void | Promise<void>\",\n    },\n  }}\n/>\n\n## Metadata Schema\n\n<TypeTable\n  type={{\n    key: {\n      description: \"Label for the metadata item\",\n      type: \"string\",\n      required: true,\n    },\n    value: {\n      description: \"Value to display\",\n      type: \"string\",\n      required: true,\n    },\n  }}\n/>\n\nLong metadata values are automatically truncated with an ellipsis. Keep values concise for best results.\n\n## Accessibility\n\n- Uses `role=\"dialog\"` with `aria-labelledby` and `aria-describedby`\n- **Keyboard navigation:**\n  - `Tab` to move between buttons\n  - `Enter` or `Space` to activate focused button\n  - `Escape` triggers the cancel action\n- Receipt state uses `role=\"status\"` for screen reader announcements\n- Fade-in animation respects `prefers-reduced-motion`\n\n## Related\n\n- [Order Summary](/docs/order-summary): itemized confirmation for purchases\n- [Option List](/docs/option-list): multiple choices when the decision isn't binary\n- [Message Draft](/docs/message-draft): review and approve outgoing messages\n- [Preferences Panel](/docs/preferences-panel): structured settings capture\n"
  },
  {
    "path": "apps/www/app/docs/approval-card/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Approval Card\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\n    \"Approval Card\",\n    \"Binary confirmation for agent actions\",\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/approval-card/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"Approval Card\",\n  description: \"Binary confirmation for agent actions\",\n};\n\nexport const revalidate = 3600;\n\nexport default function ApprovalCardDocsPage() {\n  return <ComponentDocsTabs docs={<Content />} componentId=\"approval-card\" />;\n}\n"
  },
  {
    "path": "apps/www/app/docs/audio/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\nimport { Audio } from \"@/components/tool-ui/audio\";\n\n<DocsHeader\n  title=\"Audio\"\n  description=\"Audio playback with artwork and metadata.\"\n  mdxPath=\"app/docs/audio/content.mdx\"\n/>\n\nAn audio URL by itself is invisible. No artwork, no title, no way to know what you are about to hear. Audio wraps the browser's native player with album art, title, duration, and source attribution so the track looks intentional rather than dumped into the conversation.\n\n**Role:** Information. For displaying data without user input. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"audio\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nRender `Audio` in your UI with tool-compatible props.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\nimport { Audio } from \"@/components/tool-ui/audio\";\n\nexport function Example() {\n  return (\n    <Audio\n      id=\"audio-example\"\n      assetId=\"sample-audio\"\n      src=\"https://samplelib.com/lib/preview/mp3/sample-6s.mp3\"\n      title=\"Bell Labs hallway recording\"\n      description=\"Ambient sounds where UNIX, C, and more took shape.\"\n      artwork=\"https://images.unsplash.com/photo-1454165205744-3b78555e5572\"\n      durationMs={30000}\n    />\n  );\n}\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegistration tells assistant-ui which component to render when a tool returns audio data. Without it, tool results appear as raw JSON.\n\n```tsx\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { Audio } from \"@/components/tool-ui/audio\";\nimport { safeParseSerializableAudio } from \"@/components/tool-ui/audio/schema\";\n\nexport const toolkit: Toolkit = {\n  showAudio: {\n    type: \"backend\",\n    render: ({ result }) => {\n      const parsed = safeParseSerializableAudio(result);\n      if (!parsed) {\n        return null;\n      }\n      return <Audio {...parsed} />;\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"Headphones\" title=\"Native audio controls\">\n    Browser-native play, pause, seek, and volume\n  </Feature>\n  <Feature icon=\"Image\" title=\"Artwork display\">\n    Album art or thumbnail displayed beside the player\n  </Feature>\n  <Feature icon=\"FileText\" title=\"Metadata display\">\n    Title, description, duration, and file size at a glance\n  </Feature>\n  <Feature icon=\"Link\" title=\"Source attribution\">\n    Artist, album, or source with an optional link\n  </Feature>\n</FeatureGrid>\n\n## Props\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for this tool UI instance\",\n      type: \"string\",\n      required: true,\n    },\n    assetId: {\n      description: \"Persistent asset identifier\",\n      type: \"string\",\n      required: true,\n    },\n    src: {\n      description: \"Audio source URL\",\n      type: \"string\",\n      required: true,\n    },\n    title: { description: \"Title text\", type: \"string\" },\n    description: { description: \"Description\", type: \"string\" },\n    artwork: { description: \"Artwork/thumbnail URL\", type: \"string\" },\n    durationMs: { description: \"Duration in milliseconds\", type: \"number\" },\n    fileSizeBytes: { description: \"File size in bytes\", type: \"number\" },\n    source: {\n      description: \"Source attribution\",\n      type: \"{ label: string; iconUrl?: string; url?: string }\",\n    },\n    variant: {\n      description: \"Display variant\",\n      type: \"'full' | 'compact'\",\n      default: \"'full'\",\n    },\n    createdAt: { description: \"Creation timestamp (ISO 8601)\", type: \"string\" },\n    onMediaEvent: {\n      description: \"Called on play, pause, mute, or unmute\",\n      type: \"(type: 'play' | 'pause' | 'mute' | 'unmute') => void\",\n    },\n  }}\n/>\n\nUse [`ToolUI.LocalActions`](/docs/actions) to compose external actions next to `Audio`.\n\n## Related\n\n- [Video](/docs/video): the same inline media pattern for video content\n- [Image](/docs/image): for static visual content with metadata and attribution\n- [Item Carousel](/docs/item-carousel): for browsable collections of media items like albums or playlists\n"
  },
  {
    "path": "apps/www/app/docs/audio/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Audio\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\"Audio\", \"Audio playback with artwork and metadata\");\n}\n"
  },
  {
    "path": "apps/www/app/docs/audio/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"Audio\",\n  description: \"Audio playback with artwork and metadata\",\n};\n\nexport const revalidate = 3600;\n\nexport default function AudioDocsPage() {\n  return <ComponentDocsTabs docs={<Content />} componentId=\"audio\" />;\n}\n"
  },
  {
    "path": "apps/www/app/docs/changelog/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\n\n<DocsHeader title=\"Changelog\" mdxPath=\"app/docs/changelog/content.mdx\" />\n\n{/* changelog-generated-to: c84896f */}\n\n## 2026-02-26\n\n### Changes\n\n- New component: [Geo Map](/docs/geo-map) — themed Leaflet shell with automatic dark tile inheritance from the document theme.\n- [Gallery](/docs/gallery): deterministic ranking, centered layout, and masonry column breakpoint refinements.\n\n## 2026-02-17\n\n### Changes\n\n- New component: [CodeDiff](/docs/code-diff) — unified and split diff views for code.\n- Compound composition API shipped across [Code Block](/docs/code-block), [Terminal](/docs/terminal), [Order Summary](/docs/order-summary), [Video](/docs/video), [Progress Tracker](/docs/progress-tracker), and [Item Carousel](/docs/item-carousel).\n- [Weather Widget](/docs/weather-widget): runtime artifacts now generate as TypeScript; generated payload shrunk with size guardrails.\n- Shared theme tokens introduced across code components; `Code Block` mirrors shared tokens locally with tightened content line-height.\n- Normalized action button styling and layout across components; default spacing added between `ToolUI` surface and actions.\n- [Data Table](/docs/data-table): added links-tags preset; removed escalate from actions preset; removed resources preset from docs.\n\n### Migration prompt\n\n```text\nUpdate Tool UI components to the compound composition API.\n\nGoals:\n1) Re-install only the affected components that already exist in this project.\n2) Migrate usage from monolithic component props to compound subcomponents (e.g., CodeBlock.Root, Terminal.Output).\n3) Validate UI rendering with updated component APIs.\n\nSteps:\n- Check which of these components exist in this project by looking for their directories under components/tool-ui/:\n  code-block, terminal, order-summary, video, progress-tracker, item-carousel\n- For each component that exists, re-install it to pull in the new compound exports:\n  npx shadcn@latest add @tool-ui/code-block\n  npx shadcn@latest add @tool-ui/terminal\n  npx shadcn@latest add @tool-ui/order-summary\n  npx shadcn@latest add @tool-ui/video\n  npx shadcn@latest add @tool-ui/progress-tracker\n  npx shadcn@latest add @tool-ui/item-carousel\n- Skip any component not already installed — do not add new components.\n- Update imports and usage to reference compound subcomponents instead of monolithic component props.\n- Run lint, typecheck, and tests; fix any breakages.\n- Validate UI rendering with updated component APIs.\n```\n\n## 2026-02-12\n\n### Breaking changes\n\n- Tool UI component entrypoints now enforce /schema boundaries; imports from non-schema entrypoints may break.\n- Removed the error boundary layer across Tool UI components; existing error handling may no longer apply.\n- Tool UI action model migrated to bound compound action surfaces. Legacy inline response-action patterns were removed in favor of `LocalActions` / `DecisionActions`, and outlier components now use dedicated action configs. See [Actions](/docs/actions).\n\n### Changes\n\n- Tool UI: repo-wide enforcement of /schema entrypoints.\n- Tool UI action model refactor: migrated components to bound compound action surfaces via `LocalActions` and `DecisionActions`. See [Actions](/docs/actions).\n- Action-model cutover shipped across core components (including [Audio](/docs/audio), [Citation](/docs/citation), [Code Block](/docs/code-block), [Data Table](/docs/data-table), [Image](/docs/image), [Link Preview](/docs/link-preview), [Order Summary](/docs/order-summary), [Plan](/docs/plan), [Terminal](/docs/terminal), [Video](/docs/video), and social post renderers: [X Post](/docs/x-post), [Instagram Post](/docs/instagram-post), [LinkedIn Post](/docs/linkedin-post)).\n- Outlier workflows were split to component-specific action configs ([Option List](/docs/option-list), [Parameter Slider](/docs/parameter-slider), [Preferences Panel](/docs/preferences-panel)) with contracts tightened around supported action props. See [Actions](/docs/actions).\n- [Option List](/docs/option-list): receipt presets codegen updated to omit onConfirm; multi-receipt preset added; receipt spacing refined; related tests updated.\n- [Progress Tracker](/docs/progress-tracker): shipped v2 display with result-driven UI; tests updated.\n- [Plan](/docs/plan): compact variant steps-only mode implemented.\n- [Image Gallery](/docs/image-gallery) and [Item Carousel](/docs/item-carousel): aligned tool-ui contracts.\n\n### Migration prompt\n\n```text\nMigrate codebase to adopt explicit /schema entrypoints for Tool UI schemas.\n\nGoals:\n1) Move all schema helper imports to dedicated /schema entrypoints per component.\n2) Keep public UI imports under their component paths; import schema only from /schema modules.\n3) Migrate action handling to the bound compound model using LocalActions / DecisionActions.\n4) Replace legacy response-action usage on outlier components with their dedicated action configs.\n5) Update tests and docs to reflect new entrypoints and action model semantics.\n6) Regenerate artifacts (registry/public assets) and update docs accordingly.\n7) Validate UI rendering with updated imports and schema/action contracts.\n\nSteps:\n- Replace imports of Serializable*Schema, parseSerializable*, and safeParseSerializable* from non-schema tool-ui entrypoints with imports from the component's /schema module (e.g., @/components/tool-ui/<component>/schema).\n- For all affected components, update imports to reference /schema entrypoints; preserve existing UI imports.\n- Migrate shared action composition to ToolUI-bound LocalActions / DecisionActions surfaces.\n- Remove legacy inline response-action usage and adopt dedicated outlier action configs (for example selection/adjustment/form action props) where applicable.\n- Update relevant tests and docs to reflect new entrypoints and action model semantics.\n- Run lint, typecheck, and tests; fix breakages.\n- Regenerate artifacts (registry/public assets) and update docs accordingly.\n- Validate UI rendering with updated schema and action contracts.\n```\n"
  },
  {
    "path": "apps/www/app/docs/changelog/opengraph-image.tsx",
    "content": "import {\n  contentType as ogContentType,\n  generateOgImage,\n  size as ogSize,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Changelog\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\"Changelog\", \"Release notes and migration guidance\");\n}\n"
  },
  {
    "path": "apps/www/app/docs/changelog/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { DocsArticle } from \"../_components/docs-article\";\n\nexport const metadata: Metadata = {\n  title: \"Changelog\",\n  description: \"Release notes and migration guidance\",\n};\n\nexport const revalidate = 3600;\n\nexport default function ChangelogDocsPage() {\n  return (\n    <DocsArticle>\n      <Content />\n    </DocsArticle>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/chart/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\n\n<DocsHeader\n  title=\"Chart\"\n  description=\"Display interactive data visualizations.\"\n  mdxPath=\"app/docs/chart/content.mdx\"\n/>\n\n\"How did I spend money this week?\" is a question best answered visually. Chart renders bar or line visualizations inline in the conversation. You pass data, an x-axis key, and one or more series. The assistant can show spending breakdowns, revenue trends, or performance over time without describing the numbers in prose.\n\n**Role:** Information. For displaying data the user reads. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"chart\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nRender `Chart` in your UI with tool-compatible props.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\nimport { Chart } from \"@/components/tool-ui/chart\";\n\n<Chart\n  id=\"monthly-revenue\"\n  type=\"bar\"\n  title=\"Monthly Revenue\"\n  description=\"2024 YTD\"\n  data={[\n    { month: \"Jan\", revenue: 4000, expenses: 2400 },\n    { month: \"Feb\", revenue: 3000, expenses: 1398 },\n    // ...\n  ]}\n  xKey=\"month\"\n  series={[\n    { key: \"revenue\", label: \"Revenue\" },\n    { key: \"expenses\", label: \"Expenses\" },\n  ]}\n  showLegend\n  showGrid\n/>;\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegister this renderer so tool results display as `Chart`.\n\n```tsx\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { Chart } from \"@/components/tool-ui/chart\";\nimport { safeParseSerializableChart } from \"@/components/tool-ui/chart/schema\";\n\nexport const toolkit: Toolkit = {\n  showChart: {\n    type: \"backend\",\n    render: ({ result }) => {\n      const parsed = safeParseSerializableChart(result);\n      if (!parsed) {\n        return null;\n      }\n      return <Chart {...parsed} />;\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"BarChart3\" title=\"Bar and line charts\">\n    Bar for categorical data, line for time-series\n  </Feature>\n  <Feature icon=\"Layers\" title=\"Multiple series\">\n    Plot multiple datasets with automatic color assignment\n  </Feature>\n  <Feature icon=\"MousePointerClick\" title=\"Interactive tooltips\">\n    Hover tooltips and click handlers on data points\n  </Feature>\n  <Feature icon=\"Settings2\" title=\"Configurable display\">\n    Optional legends, grid lines, and custom color palettes\n  </Feature>\n</FeatureGrid>\n\n## Variants\n\n### Line Charts\n\nUse `type=\"line\"` for time-series or continuous data.\n\n### Minimal Configuration\n\nOnly `data`, `xKey`, and `series` are required. Title, description, legend, and grid are all optional.\n\n## Props\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for this chart instance\",\n      type: \"string\",\n      required: true,\n    },\n    type: {\n      description: \"Chart type\",\n      type: \"'bar' | 'line'\",\n      required: true,\n    },\n    data: {\n      description: \"Array of data records\",\n      type: \"Record<string, unknown>[]\",\n      required: true,\n    },\n    xKey: {\n      description: \"Key for x-axis values\",\n      type: \"string\",\n      required: true,\n    },\n    series: {\n      description: \"Data series to plot\",\n      type: \"ChartSeries[]\",\n      required: true,\n    },\n    title: {\n      description: \"Chart title (renders in Card header)\",\n      type: \"string\",\n    },\n    description: {\n      description: \"Chart description (renders below title)\",\n      type: \"string\",\n    },\n    colors: {\n      description: \"Custom color palette for series\",\n      type: \"string[]\",\n    },\n    showLegend: {\n      description: \"Show legend below chart\",\n      type: \"boolean\",\n      default: \"false\",\n    },\n    showGrid: {\n      description: \"Show background grid lines\",\n      type: \"boolean\",\n      default: \"true\",\n    },\n    onDataPointClick: {\n      description: \"Click handler for data points\",\n      type: \"(point: ChartDataPoint) => void\",\n    },\n  }}\n/>\n\n## Series Schema\n\n<TypeTable\n  type={{\n    key: {\n      description: \"Data key to plot\",\n      type: \"string\",\n      required: true,\n    },\n    label: {\n      description: \"Display label for legend/tooltip\",\n      type: \"string\",\n      required: true,\n    },\n    color: {\n      description: \"Override color for this series\",\n      type: \"string\",\n    },\n  }}\n/>\n\n## ChartDataPoint (click handler payload)\n\n<TypeTable\n  type={{\n    seriesKey: {\n      description: \"Key of the clicked series\",\n      type: \"string\",\n    },\n    seriesLabel: {\n      description: \"Label of the clicked series\",\n      type: \"string\",\n    },\n    xValue: {\n      description: \"X-axis value at click point\",\n      type: \"unknown\",\n    },\n    yValue: {\n      description: \"Y-axis value at click point\",\n      type: \"unknown\",\n    },\n    index: {\n      description: \"Index in data array\",\n      type: \"number\",\n    },\n    payload: {\n      description: \"Full data record for clicked point\",\n      type: \"Record<string, unknown>\",\n    },\n  }}\n/>\n\n## Accessibility\n\n- Built on Recharts with semantic SVG structure\n- Tooltips and legends provide text alternatives for visual data\n- Inherits focus management from shadcn/ui Card primitives\n\n## Related\n\n- [Stats Display](/docs/stats-display): key metrics as a grid of numbers with sparklines\n"
  },
  {
    "path": "apps/www/app/docs/chart/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Chart\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\"Chart\", \"Visualize data with interactive charts\");\n}\n"
  },
  {
    "path": "apps/www/app/docs/chart/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"Chart\",\n  description: \"Visualize data with interactive charts\",\n};\n\nexport const revalidate = 3600;\n\nexport default function ChartDocsPage() {\n  return <ComponentDocsTabs docs={<Content />} componentId=\"chart\" />;\n}\n"
  },
  {
    "path": "apps/www/app/docs/citation/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\nimport { Citation } from \"@/components/tool-ui/citation\";\n\n<DocsHeader\n  title=\"Citation\"\n  description=\"Display source references with attribution.\"\n  mdxPath=\"app/docs/citation/content.mdx\"\n/>\n\nUsers trust answers they can verify. Citation renders a clickable reference card with title, domain, favicon, and an optional text snippet so the reader can trace any claim back to its source. Three variants cover different density needs: a full card for prominent references, an inline chip for tight spaces, and stacked favicons for citation lists.\n\n**Role:** Information. For displaying data without user input. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"citation\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nRender `Citation` in your UI with tool-compatible props.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\nimport { Citation } from \"@/components/tool-ui/citation\";\n\nexport function Example() {\n  return (\n    <Citation\n      id=\"citation-example\"\n      href=\"https://react.dev/reference/react/useState\"\n      title=\"useState – React\"\n      snippet=\"useState is a React Hook that lets you add a state variable to your component.\"\n      domain=\"react.dev\"\n      favicon=\"https://www.google.com/s2/favicons?domain=react.dev&sz=32\"\n      type=\"document\"\n    />\n  );\n}\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegistration tells assistant-ui which component to render when a tool returns citation data. Without it, tool results appear as raw JSON.\n\n```tsx\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { Citation } from \"@/components/tool-ui/citation\";\nimport { safeParseSerializableCitation } from \"@/components/tool-ui/citation/schema\";\n\nexport const toolkit: Toolkit = {\n  showCitation: {\n    type: \"backend\",\n    render: ({ result }) => {\n      const parsed = safeParseSerializableCitation(result);\n      if (!parsed) {\n        return null;\n      }\n      return <Citation {...parsed} />;\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"FileText\" title=\"Source attribution\">\n    Title, domain, author, and publication date in one card\n  </Feature>\n  <Feature icon=\"Quote\" title=\"Snippet preview\">\n    Shows a relevant excerpt so users can judge relevance before clicking\n  </Feature>\n  <Feature icon=\"Layers\" title=\"Three variants\">\n    Full card for detail, inline chip for tight spaces, stacked favicons for\n    lists\n  </Feature>\n  <Feature icon=\"Keyboard\" title=\"Keyboard accessible\">\n    Arrow keys and Enter navigate the stacked variant popover\n  </Feature>\n</FeatureGrid>\n\n## Props\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for this tool UI instance\",\n      type: \"string\",\n      required: true,\n    },\n    variant: {\n      description: \"Visual style\",\n      type: \"'default' | 'inline' | 'stacked'\",\n      default: \"'default'\",\n    },\n    href: {\n      description: \"Source URL\",\n      type: \"string\",\n      required: true,\n    },\n    title: {\n      description: \"Source title\",\n      type: \"string\",\n      required: true,\n    },\n    snippet: {\n      description: \"Relevant excerpt from the source\",\n      type: \"string\",\n    },\n    domain: {\n      description: \"Source domain (auto-extracted from href if not provided)\",\n      type: \"string\",\n    },\n    favicon: {\n      description: \"Favicon URL\",\n      type: \"string\",\n    },\n    author: {\n      description: \"Author or organization name\",\n      type: \"string\",\n    },\n    publishedAt: {\n      description: \"Publication date (ISO 8601)\",\n      type: \"string\",\n    },\n    type: {\n      description: \"Source type for icon selection\",\n      type: \"'webpage' | 'document' | 'article' | 'api' | 'code' | 'other'\",\n      default: \"'webpage'\",\n    },\n    onNavigate: {\n      description: \"Custom navigation handler\",\n      type: \"(href: string, citation: SerializableCitation) => void\",\n    },\n  }}\n/>\n\nUse [`ToolUI.LocalActions`](/docs/actions) to compose external actions next to citations.\n\n## Accessibility\n\nThe stacked variant popover supports full keyboard navigation:\n\n- **Enter / Space:** Open the popover\n- **Tab:** Navigate between citations\n- **Escape:** Close popover and return focus to trigger\n\nMouse users can hover to reveal the popover. All variants open links in new tabs with `noopener,noreferrer` for security.\n\n## Related\n\n- [Link Preview](/docs/link-preview): both surface external references, but LinkPreview shows Open Graph cards while Citation focuses on source attribution\n- [Data Table](/docs/data-table): for displaying multiple references in a structured, sortable format\n"
  },
  {
    "path": "apps/www/app/docs/citation/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Citation\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\n    \"Citation\",\n    \"Display source references with attribution\",\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/citation/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"Citation\",\n  description: \"Display source references with attribution\",\n};\n\nexport const revalidate = 3600;\n\nexport default function CitationDocsPage() {\n  return <ComponentDocsTabs docs={<Content />} componentId=\"citation\" />;\n}\n"
  },
  {
    "path": "apps/www/app/docs/code-block/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\n\n<DocsHeader\n  title=\"Code Block\"\n  description=\"Display syntax-highlighted code snippets.\"\n  mdxPath=\"app/docs/code-block/content.mdx\"\n/>\n\nWhen the assistant generates or references code, unstyled monospace text sells it short. CodeBlock renders syntax-highlighted snippets with line numbers and a copy button. You can highlight specific lines to draw attention to changes or bugs, and collapse long snippets with `maxCollapsedLines` to keep conversations scannable.\n\n**Role:** Information. For displaying data the user reads. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"code-block\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nRender `CodeBlock` in your UI with tool-compatible props.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\nimport { CodeBlock } from \"@/components/tool-ui/code-block\";\n\nexport function MyComponent() {\n  return (\n    <CodeBlock\n      id=\"my-code-example\"\n      code={`function hello(name: string) {\n  return \\`Hello, \\${name}!\\`;\n}`}\n      language=\"typescript\"\n      lineNumbers=\"visible\"\n      filename=\"hello.ts\"\n    />\n  );\n}\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegister this renderer so tool results display as `CodeBlock`.\n\n```tsx\n// Backend tool\nimport { tool, jsonSchema } from \"ai\";\n\nconst showCodeBlock = tool({\n  description: \"Show a code snippet\",\n  inputSchema: jsonSchema<{}>({\n    type: \"object\",\n    properties: {},\n    additionalProperties: false,\n  }),\n  async execute() {\n    return {\n      id: \"code-block-1\",\n      code: \"console.log('hello');\",\n      language: \"javascript\",\n      filename: \"hello.js\",\n      highlightLines: [1],\n    };\n  },\n});\n\n// Frontend with assistant-ui\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { CodeBlock } from \"@/components/tool-ui/code-block\";\nimport { safeParseSerializableCodeBlock } from \"@/components/tool-ui/code-block/schema\";\n\nexport const toolkit: Toolkit = {\n  showCodeBlock: {\n    type: \"backend\",\n    render: ({ result }) => {\n      const parsed = safeParseSerializableCodeBlock(result);\n      if (!parsed) {\n        return null;\n      }\n      return <CodeBlock {...parsed} />;\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"Palette\" title=\"Syntax highlighting\">\n    Shiki-powered with 100+ languages\n  </Feature>\n  <Feature icon=\"Highlighter\" title=\"Line highlighting\">\n    Draw attention to specific lines for explanations or diffs\n  </Feature>\n  <Feature icon=\"FoldVertical\" title=\"Collapsible mode\">\n    Opt-in collapse for long snippets via `maxCollapsedLines`\n  </Feature>\n  <Feature icon=\"FileCode\" title=\"Filename header\">\n    Filename and language badge in the header\n  </Feature>\n</FeatureGrid>\n\n## Props\n\n### Core\n\n<TypeTable\n  type={{\n    code: {\n      description: \"The code content to display\",\n      type: \"string\",\n      required: true,\n    },\n    language: {\n      description: \"Programming language for syntax highlighting\",\n      type: \"string\",\n      default: '\"text\"',\n    },\n    filename: {\n      description: \"Filename to display in the header\",\n      type: \"string\",\n    },\n  }}\n/>\n\n### Display Options\n\n<TypeTable\n  type={{\n    lineNumbers: {\n      description: \"Line number variant\",\n      type: '\"visible\" | \"hidden\"',\n      default: '\"visible\"',\n    },\n    highlightLines: {\n      description: \"Array of line numbers to highlight\",\n      type: \"number[]\",\n    },\n    maxCollapsedLines: {\n      description: \"Enable collapse when code exceeds this many lines (opt-in)\",\n      type: \"number\",\n    },\n    expanded: {\n      description: \"Controlled expand state (for collapsible mode)\",\n      type: \"boolean\",\n    },\n    defaultExpanded: {\n      description: \"Initial expand state (for collapsible mode)\",\n      type: \"boolean\",\n    },\n    onExpandedChange: {\n      description: \"Called when expand state changes\",\n      type: \"(expanded: boolean) => void\",\n    },\n  }}\n/>\n\n### Standard Props\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for this component\",\n      type: \"string\",\n      required: true,\n    },\n    className: {\n      description: \"Additional CSS classes\",\n      type: \"string\",\n    },\n  }}\n/>\n\nUse [`ToolUI.LocalActions`](/docs/actions) to compose external actions next to `CodeBlock`.\n\n## Supported Languages\n\nCodeBlock supports all languages included with Shiki, including:\n\n- TypeScript/JavaScript\n- Python\n- JSON\n- Bash/Shell\n- CSS/HTML\n- Markdown\n- SQL\n- YAML\n- Go\n- Rust\n- And many more...\n\nSee [Shiki documentation](https://shiki.style/languages) for the complete list.\n\n## Accessibility\n\n- Collapsible sections use Radix Collapsible with proper ARIA attributes\n- Copy button is keyboard-accessible with focus indication\n- Code content is selectable and works with screen readers\n\n## Related\n\n- [Terminal](/docs/terminal): command-line output and logs\n"
  },
  {
    "path": "apps/www/app/docs/code-block/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Code Block\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\n    \"Code Block\",\n    \"Display syntax-highlighted code snippets\",\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/code-block/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"Code Block\",\n  description: \"Display syntax-highlighted code snippets\",\n};\n\nexport const revalidate = 3600;\n\nexport default function CodeBlockDocsPage() {\n  return <ComponentDocsTabs docs={<Content />} componentId=\"code-block\" />;\n}\n"
  },
  {
    "path": "apps/www/app/docs/code-diff/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\n\n<DocsHeader\n  title=\"Code Diff\"\n  description=\"Display syntax-highlighted diffs.\"\n  mdxPath=\"app/docs/code-diff/content.mdx\"\n/>\n\nCodeDiff renders syntax-highlighted diffs with additions and deletions marked in color. Pass `oldCode` / `newCode` for file comparisons or a `patch` string for unified diffs from version control.\n\n**Role:** Information. For displaying data the user reads. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"code-diff\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Install the peer dependency\">\n\nCodeDiff uses [`@pierre/diffs`](https://www.npmjs.com/package/@pierre/diffs) for diff computation and rendering.\n\n```bash\npnpm add @pierre/diffs\n```\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nRender `CodeDiff` in your UI with tool-compatible props.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\nimport { CodeDiff } from \"@/components/tool-ui/code-diff\";\n\nexport function MyComponent() {\n  return (\n    <CodeDiff\n      id=\"my-diff-example\"\n      language=\"typescript\"\n      filename=\"lib/auth.ts\"\n      oldCode={`function checkAuth(token: string) {\n  if (!token) throw new Error(\"Missing token\");\n  return jwt.verify(token, SECRET);\n}`}\n      newCode={`function checkAuth(token: string): AuthResult {\n  if (!token) return { ok: false, error: \"missing_token\" };\n  const decoded = jwt.verify(token, SECRET);\n  return { ok: true, user: decoded };\n}`}\n    />\n  );\n}\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegister this renderer so tool results display as `CodeDiff`.\n\n```tsx\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { CodeDiff } from \"@/components/tool-ui/code-diff\";\nimport { safeParseSerializableCodeDiff } from \"@/components/tool-ui/code-diff/schema\";\n\nexport const toolkit: Toolkit = {\n  showCodeDiff: {\n    type: \"backend\",\n    render: ({ result }) => {\n      const parsed = safeParseSerializableCodeDiff(result);\n      if (!parsed) {\n        return null;\n      }\n      return <CodeDiff {...parsed} />;\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"Code\" title=\"Two input modes\">\n    Pass `oldCode`/`newCode` for file diffs, or `patch` for unified diffs\n  </Feature>\n  <Feature icon=\"Palette\" title=\"Syntax highlighting\">\n    Language-aware highlighting powered by @pierre/diffs\n  </Feature>\n  <Feature icon=\"Expand\" title=\"Split view\">\n    Side-by-side comparison with `diffStyle=\"split\"`\n  </Feature>\n  <Feature icon=\"FoldVertical\" title=\"Collapsible diffs\">\n    Collapse long diffs with `maxCollapsedLines`\n  </Feature>\n</FeatureGrid>\n\n## Props\n\n### Core\n\n<TypeTable\n  type={{\n    oldCode: {\n      description: \"Original code content (file comparison mode)\",\n      type: \"string\",\n    },\n    newCode: {\n      description: \"Modified code content (file comparison mode)\",\n      type: \"string\",\n    },\n    patch: {\n      description: \"Unified diff string (patch mode)\",\n      type: \"string\",\n    },\n    language: {\n      description: \"Programming language for syntax highlighting\",\n      type: \"string\",\n      default: '\"text\"',\n    },\n    filename: {\n      description: \"File path displayed in the header\",\n      type: \"string\",\n    },\n  }}\n/>\n\n### Display Options\n\n<TypeTable\n  type={{\n    lineNumbers: {\n      description: \"Show or hide line numbers\",\n      type: '\"visible\" | \"hidden\"',\n      default: '\"visible\"',\n    },\n    diffStyle: {\n      description: \"Unified (interleaved) or split (side-by-side) view\",\n      type: '\"unified\" | \"split\"',\n      default: '\"unified\"',\n    },\n    maxCollapsedLines: {\n      description: \"Collapse the diff when it exceeds this many lines\",\n      type: \"number\",\n    },\n  }}\n/>\n\n### Standard Props\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for this component\",\n      type: \"string\",\n      required: true,\n    },\n    className: {\n      description: \"Additional CSS classes\",\n      type: \"string\",\n    },\n  }}\n/>\n\nUse [`ToolUI.LocalActions`](/docs/actions) to compose external actions next to `CodeDiff`.\n\n## Accessibility\n\n- Collapsible sections use Radix Collapsible with proper ARIA attributes\n- Copy button is keyboard-accessible with focus indication\n- Diff content is selectable and works with screen readers\n\n## Related\n\n- [Code Block](/docs/code-block): single-file syntax-highlighted code display\n- [Terminal](/docs/terminal): command-line output and logs\n"
  },
  {
    "path": "apps/www/app/docs/code-diff/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Code Diff\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\n    \"Code Diff\",\n    \"Compare code changes with syntax-highlighted diffs\",\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/code-diff/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"Code Diff\",\n  description: \"Compare code changes with syntax-highlighted diffs\",\n};\n\nexport const revalidate = 3600;\n\nexport default function CodeDiffDocsPage() {\n  return <ComponentDocsTabs docs={<Content />} componentId=\"code-diff\" />;\n}\n"
  },
  {
    "path": "apps/www/app/docs/data-table/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\nimport { DataTable } from \"@/components/tool-ui/data-table\";\nimport { FormatInlineExample } from \"./formatting-gallery\";\nimport {\n  NumberValue,\n  CurrencyValue,\n  PercentValue,\n  DeltaValue,\n  DateValue,\n  BooleanValue,\n  LinkValue,\n  BadgeValue,\n  StatusBadge,\n  ArrayValue,\n} from \"@/components/tool-ui/data-table\";\n\n<DocsHeader\n  title=\"Data Table\"\n  description=\"Present structured data in sortable tables.\"\n  mdxPath=\"app/docs/data-table/content.mdx\"\n/>\n\nSearch results, issue lists, transaction logs: the assistant surfaces tabular data constantly. DataTable renders those rows as a sortable table on desktop and stacked cards on mobile. You define columns with declarative formatting configs for currency, dates, status badges, and links. No raw JSON, no custom table from scratch.\n\n**Role:** Information. For displaying data the user reads. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"data-table\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nRender `DataTable` in your UI with tool-compatible props.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\nimport { DataTable, type Column } from \"@/components/tool-ui/data-table\";\n\ntype Row = {\n  id: string;\n  issue: string;\n  priority: \"high\" | \"medium\" | \"low\";\n  createdAt: string;\n  amount: number;\n};\n\nconst columns: Column<Row>[] = [\n  { key: \"issue\", label: \"Issue\", priority: \"primary\", truncate: true },\n  {\n    key: \"priority\",\n    label: \"Urgency\",\n    format: {\n      kind: \"status\",\n      statusMap: {\n        high: { tone: \"danger\", label: \"High\" },\n        medium: { tone: \"warning\", label: \"Medium\" },\n        low: { tone: \"neutral\", label: \"Low\" },\n      },\n    },\n  },\n  {\n    key: \"createdAt\",\n    label: \"Created\",\n    format: { kind: \"date\", dateFormat: \"relative\" },\n  },\n  {\n    key: \"amount\",\n    label: \"Amount\",\n    align: \"right\",\n    format: { kind: \"currency\", currency: \"USD\", decimals: 2 },\n  },\n];\n\nconst data: Row[] = [\n  {\n    id: \"1\",\n    issue: \"Payment failing on checkout\",\n    priority: \"high\",\n    createdAt: \"2025-11-11T09:24:00Z\",\n    amount: 129.99,\n  },\n  {\n    id: \"2\",\n    issue: \"Billing UI alignment bug\",\n    priority: \"medium\",\n    createdAt: \"2025-11-10T17:03:00Z\",\n    amount: 42,\n  },\n  {\n    id: \"3\",\n    issue: \"Export CSV missing headers\",\n    priority: \"low\",\n    createdAt: \"2025-11-08T08:00:00Z\",\n    amount: 0,\n  },\n  {\n    id: \"4\",\n    issue: \"Webhook retries inconsistent\",\n    priority: \"high\",\n    createdAt: \"2025-11-13T22:15:00Z\",\n    amount: 527.5,\n  },\n];\n\nexport default function Example() {\n  return (\n    <DataTable\n      rowIdKey=\"id\"\n      columns={columns}\n      data={data}\n      defaultSort={{ by: \"createdAt\", direction: \"desc\" }}\n    />\n  );\n}\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegister this renderer so tool results display as `DataTable`.\n\n```tsx\n// Backend tool\nimport { tool, jsonSchema } from \"ai\";\n\nconst searchProducts = tool({\n  description: \"Search products and return results in a table\",\n  inputSchema: jsonSchema<{ query: string }>({\n    type: \"object\",\n    properties: { query: { type: \"string\" } },\n    required: [\"query\"],\n    additionalProperties: false,\n  }),\n  async execute({ query }) {\n    const results = await db.products.search(query);\n    return {\n      id: \"data-table-search-products\",\n      columns: [\n        { key: \"name\", label: \"Product\", priority: \"primary\" },\n        {\n          key: \"price\",\n          label: \"Price\",\n          format: { kind: \"currency\", currency: \"USD\" },\n        },\n        { key: \"stock\", label: \"Stock\", align: \"right\" },\n      ],\n      data: results.map((p) => ({\n        id: p.id,\n        name: p.name,\n        price: p.price,\n        stock: p.stock,\n      })),\n    };\n  },\n});\n\n// Frontend with assistant-ui\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { DataTable } from \"@/components/tool-ui/data-table\";\nimport { safeParseSerializableDataTable } from \"@/components/tool-ui/data-table/schema\";\n\nexport const toolkit: Toolkit = {\n  searchProducts: {\n    type: \"backend\",\n    render: ({ result }) => {\n      const parsed = safeParseSerializableDataTable(result);\n      if (!parsed) {\n        return null;\n      }\n      return <DataTable rowIdKey=\"id\" {...parsed} />;\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"Smartphone\" title=\"Responsive layout\">\n    Table on desktop, stacked cards on mobile\n  </Feature>\n  <Feature icon=\"ArrowUpDown\" title=\"Sorting\">\n    Tri-state column headers: none, ascending, descending\n  </Feature>\n  <Feature icon=\"Table2\" title=\"Declarative formatting\">\n    Currency, dates, links, badges, and status pills via column config\n  </Feature>\n  <Feature icon=\"MousePointerClick\" title=\"External actions\">\n    Compose action buttons below the table\n  </Feature>\n</FeatureGrid>\n\n## Layout Variants\n\nUse explicit components when you want to force a layout:\n\n- `<DataTable>` (default): responsive auto layout\n- `<DataTable.Table>`: always render the table view\n- `<DataTable.Cards>`: always render the card view\n\n```tsx\nimport { DataTable } from \"@/components/tool-ui/data-table\";\n```\n\n## Props\n\n### Core\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for this component instance\",\n      type: \"string\",\n      required: true,\n    },\n    columns: {\n      description: \"Column definitions\",\n      type: \"Column[]\",\n      required: true,\n    },\n    data: {\n      description: \"Rows (JSON primitives only)\",\n      type: \"T[]\",\n      required: true,\n    },\n    rowIdKey: {\n      description:\n        \"Unique key per row (strongly recommended for stable reconciliation)\",\n      type: \"keyof T\",\n      required: false,\n    },\n    emptyMessage: {\n      description: \"Message to display when data is empty\",\n      type: \"string\",\n    },\n    maxHeight: {\n      description: \"Max height for scrollable table body (CSS value)\",\n      type: \"string\",\n    },\n    locale: { description: \"Intl locale\", type: \"string\", default: '\"en-US\"' },\n  }}\n/>\n\n### Column Options\n\n<TypeTable\n  type={{\n    key: { description: \"Row data key\", type: \"keyof T\", required: true },\n    label: { description: \"Header text\", type: \"string\", required: true },\n    format: { description: \"Cell formatting\", type: \"FormatConfig\" },\n    priority: {\n      description: \"Mobile visibility\",\n      type: \"'primary' | 'secondary' | 'tertiary'\",\n    },\n    sortable: {\n      description: \"Enable sorting\",\n      type: \"boolean\",\n      default: \"true\",\n    },\n    align: {\n      description: \"Text alignment\",\n      type: \"'left' | 'right' | 'center'\",\n    },\n    abbr: {\n      description: \"Abbreviated header text for narrow columns\",\n      type: \"string\",\n    },\n    width: {\n      description: \"CSS width for the column (e.g., '120px', '20%')\",\n      type: \"string\",\n    },\n    truncate: { description: \"Truncate with ellipsis\", type: \"boolean\" },\n    hideOnMobile: {\n      description: \"Hide this column in the mobile card layout\",\n      type: \"boolean\",\n    },\n  }}\n/>\n\n### Sorting\n\n<TypeTable\n  type={{\n    defaultSort: {\n      description: \"Initial sort\",\n      type: \"{ by?: keyof T; direction?: 'asc' | 'desc' }\",\n    },\n    sort: {\n      description: \"Controlled sort\",\n      type: \"{ by?: keyof T; direction?: 'asc' | 'desc' }\",\n    },\n    onSortChange: { description: \"Sort handler\", type: \"(sort) => void\" },\n  }}\n/>\n\n### External Actions\n\nUse [`ToolUI.LocalActions`](/docs/actions) to compose surface-level actions below `DataTable`.\n\n## Formatting\n\nAll visual formatting for data stays declarative via `columns[i].format`. Keep row values as primitives and describe presentation through format configs.\n\n<TypeTable\n  type={{\n    number: {\n      description: (\n        <FormatInlineExample\n          value={12345.678}\n          output={\n            <NumberValue\n              value={12345.678}\n              options={{ kind: \"number\", decimals: 2, unit: \" ms\" }}\n            />\n          }\n        />\n      ),\n      type: \"{ kind: 'number', decimals?: number, unit?: string, compact?: boolean, showSign?: boolean }\",\n    },\n    currency: {\n      description: (\n        <FormatInlineExample\n          value={178.25}\n          output={\n            <CurrencyValue\n              value={178.25}\n              options={{ kind: \"currency\", currency: \"USD\", decimals: 2 }}\n            />\n          }\n        />\n      ),\n      type: \"{ kind: 'currency', currency: string, decimals?: number }\",\n    },\n    percent: {\n      description: (\n        <>\n          <FormatInlineExample\n            value={0.123}\n            output={\n              <PercentValue\n                value={0.123}\n                options={{\n                  kind: \"percent\",\n                  basis: \"fraction\",\n                  decimals: 1,\n                  showSign: true,\n                }}\n              />\n            }\n          />\n          <FormatInlineExample\n            value={1.23}\n            output={\n              <PercentValue\n                value={1.23}\n                options={{\n                  kind: \"percent\",\n                  basis: \"unit\",\n                  decimals: 2,\n                  showSign: true,\n                }}\n              />\n            }\n          />\n        </>\n      ),\n      type: \"{ kind: 'percent', basis: 'fraction' | 'unit', decimals?: number, showSign?: boolean }\",\n    },\n    delta: {\n      description: (\n        <>\n          <FormatInlineExample\n            value={2.35}\n            output={\n              <DeltaValue\n                value={2.35}\n                options={{\n                  kind: \"delta\",\n                  decimals: 2,\n                  upIsPositive: true,\n                  showSign: true,\n                }}\n              />\n            }\n          />\n          <FormatInlineExample\n            value={-1.2}\n            output={\n              <DeltaValue\n                value={-1.2}\n                options={{\n                  kind: \"delta\",\n                  decimals: 2,\n                  upIsPositive: true,\n                  showSign: true,\n                }}\n              />\n            }\n          />\n        </>\n      ),\n      type: \"{ kind: 'delta', decimals?: number, upIsPositive?: boolean, showSign?: boolean }\",\n    },\n    date: {\n      description: (\n        <>\n          <FormatInlineExample\n            value={\"2025-01-05T12:00:00Z\"}\n            output={\n              <DateValue\n                value={\"2025-01-05T12:00:00Z\"}\n                options={{ kind: \"date\", dateFormat: \"relative\" }}\n              />\n            }\n          />\n          <FormatInlineExample\n            value={\"2025-01-05T12:00:00Z\"}\n            output={\n              <DateValue\n                value={\"2025-01-05T12:00:00Z\"}\n                options={{ kind: \"date\", dateFormat: \"long\" }}\n                locale=\"de-DE\"\n              />\n            }\n            note=\"Long date with locale\"\n          />\n        </>\n      ),\n      type: \"{ kind: 'date', dateFormat?: 'short' | 'long' | 'relative' }\",\n    },\n    boolean: {\n      description: (\n        <FormatInlineExample\n          value={true}\n          output={\n            <BooleanValue\n              value={true}\n              options={{\n                kind: \"boolean\",\n                labels: { true: \"Yes\", false: \"No\" },\n              }}\n            />\n          }\n        />\n      ),\n      type: \"{ kind: 'boolean', labels?: { true: string; false: string } }\",\n    },\n    link: {\n      description: (\n        <>\n          <FormatInlineExample\n            value={\"Docs Portal\"}\n            output={\n              <LinkValue\n                value={\"Docs Portal\"}\n                row={{ url: \"/docs\" }}\n                options={{ kind: \"link\", hrefKey: \"url\" }}\n              />\n            }\n          />\n          <FormatInlineExample\n            value={\"https://example.com\"}\n            output={\n              <LinkValue\n                value={\"https://example.com\"}\n                options={{ kind: \"link\", external: true }}\n              />\n            }\n          />\n        </>\n      ),\n      type: \"{ kind: 'link', hrefKey?: string, external?: boolean }\",\n    },\n    badge: {\n      description: (\n        <FormatInlineExample\n          value={\"open\"}\n          output={\n            <BadgeValue\n              value={\"open\"}\n              options={{\n                kind: \"badge\",\n                colorMap: { open: \"info\", closed: \"success\" },\n              }}\n            />\n          }\n        />\n      ),\n      type: \"{ kind: 'badge', colorMap?: Record<string, Tone> }\",\n    },\n    status: {\n      description: (\n        <FormatInlineExample\n          value={\"high\"}\n          output={\n            <StatusBadge\n              value={\"high\"}\n              options={{\n                kind: \"status\",\n                statusMap: {\n                  high: { tone: \"danger\", label: \"High\" },\n                  low: { tone: \"neutral\", label: \"Low\" },\n                },\n              }}\n            />\n          }\n        />\n      ),\n      type: \"{ kind: 'status', statusMap: Record<string, { tone: Tone; label?: string }> }\",\n    },\n    array: {\n      description: (\n        <FormatInlineExample\n          value={[\"alpha\", \"beta\", \"gamma\"]}\n          output={\n            <ArrayValue\n              value={[\"alpha\", \"beta\", \"gamma\"]}\n              options={{ kind: \"array\", maxVisible: 2 }}\n            />\n          }\n        />\n      ),\n      type: \"{ kind: 'array', maxVisible?: number }\",\n    },\n  }}\n/>\n\n## Accessibility\n\n- Uses semantic `<table>` markup with proper header associations\n- Sortable columns are keyboard-accessible via shadcn/ui Button\n- Sort changes are announced via `aria-live` for assistive technologies\n\n## Related\n\n- [Item Carousel](/docs/item-carousel): horizontal browsing for collection display\n"
  },
  {
    "path": "apps/www/app/docs/data-table/formatting-gallery.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  renderFormattedValue,\n  type FormatConfig,\n} from \"@/components/tool-ui/data-table\";\n\ntype Row = Record<\n  string,\n  string | number | boolean | null | (string | number | boolean | null)[]\n>;\n\ninterface FormatSampleProps {\n  title: string;\n  value:\n    | string\n    | number\n    | boolean\n    | null\n    | (string | number | boolean | null)[];\n  format: FormatConfig;\n  row?: Row;\n  locale?: string;\n  code?: string;\n  note?: string;\n}\n\nfunction FormatSample({\n  title,\n  value,\n  format,\n  row,\n  locale,\n  code,\n  note,\n}: FormatSampleProps) {\n  const output = React.useMemo(() => {\n    return renderFormattedValue({ value, column: { format }, row, locale });\n  }, [value, format, row, locale]);\n\n  const codeSnippet = code ?? `format: ${JSON.stringify(format, null, 2)}`;\n  const inputSnippet = JSON.stringify(value);\n\n  return (\n    <div className=\"bg-card rounded-lg border p-4 shadow-sm\">\n      <div className=\"text-foreground mb-2 text-sm font-medium\">{title}</div>\n      <div className=\"grid grid-cols-1 gap-3 text-sm md:grid-cols-2\">\n        <div className=\"space-y-1\">\n          <div className=\"text-muted-foreground\">Format</div>\n          <pre className=\"bg-muted/40 overflow-auto rounded-md p-2 leading-relaxed\">\n            <code className=\"language-ts\">{codeSnippet}</code>\n          </pre>\n          {note ? (\n            <div className=\"text-muted-foreground mt-1\">{note}</div>\n          ) : null}\n        </div>\n        <div className=\"space-y-2\">\n          <div>\n            <div className=\"text-muted-foreground\">Input value</div>\n            <div className=\"font-mono\">{inputSnippet}</div>\n          </div>\n          <div>\n            <div className=\"text-muted-foreground\">Rendered</div>\n            <div className=\"not-prose flex min-h-[32px] items-center gap-2\">\n              {output}\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport function FormattingGallery() {\n  const nowIso = \"2025-01-05T12:00:00Z\";\n\n  return (\n    <div className=\"not-prose grid grid-cols-1 gap-3\">\n      {/* number */}\n      <FormatSample\n        title=\"number (decimals, unit)\"\n        value={12345.678}\n        format={{ kind: \"number\", decimals: 2, unit: \" ms\" }}\n      />\n      <FormatSample\n        title=\"number (compact, showSign)\"\n        value={52430000}\n        format={{ kind: \"number\", compact: true, showSign: true }}\n      />\n\n      {/* currency */}\n      <FormatSample\n        title=\"currency (USD)\"\n        value={178.25}\n        format={{ kind: \"currency\", currency: \"USD\", decimals: 2 }}\n      />\n\n      {/* percent */}\n      <FormatSample\n        title=\"percent (fraction basis)\"\n        value={0.123}\n        format={{\n          kind: \"percent\",\n          basis: \"fraction\",\n          decimals: 1,\n          showSign: true,\n        }}\n        note=\"0.123 → 12.3%\"\n      />\n      <FormatSample\n        title=\"percent (unit basis)\"\n        value={1.23}\n        format={{ kind: \"percent\", basis: \"unit\", decimals: 2, showSign: true }}\n        note=\"1.23 → 1.23%\"\n      />\n\n      {/* delta */}\n      <FormatSample\n        title=\"delta (positive)\"\n        value={2.35}\n        format={{\n          kind: \"delta\",\n          decimals: 2,\n          upIsPositive: true,\n          showSign: true,\n        }}\n      />\n      <FormatSample\n        title=\"delta (negative)\"\n        value={-1.2}\n        format={{\n          kind: \"delta\",\n          decimals: 2,\n          upIsPositive: true,\n          showSign: true,\n        }}\n      />\n\n      {/* date */}\n      <FormatSample\n        title=\"date (relative)\"\n        value={nowIso}\n        format={{ kind: \"date\", dateFormat: \"relative\" }}\n      />\n      <FormatSample\n        title=\"date (long, de-DE)\"\n        value={nowIso}\n        format={{ kind: \"date\", dateFormat: \"long\" }}\n        locale=\"de-DE\"\n      />\n\n      {/* boolean */}\n      <FormatSample\n        title=\"boolean (custom labels)\"\n        value={true}\n        format={{\n          kind: \"boolean\",\n          labels: { true: \"Enabled\", false: \"Disabled\" },\n        }}\n      />\n\n      {/* link */}\n      <FormatSample\n        title=\"link (hrefKey)\"\n        value={\"Docs Portal\"}\n        format={{ kind: \"link\", hrefKey: \"url\" }}\n        row={{ url: \"/docs\" }}\n      />\n      <FormatSample\n        title=\"link (external)\"\n        value={\"https://example.com\"}\n        format={{ kind: \"link\", external: true }}\n      />\n\n      {/* badge */}\n      <FormatSample\n        title=\"badge (colorMap)\"\n        value={\"in-progress\"}\n        format={{\n          kind: \"badge\",\n          colorMap: {\n            open: \"info\",\n            \"in-progress\": \"warning\",\n            closed: \"success\",\n            blocked: \"danger\",\n          },\n        }}\n      />\n\n      {/* status */}\n      <FormatSample\n        title=\"status (tone + label)\"\n        value={\"high\"}\n        format={{\n          kind: \"status\",\n          statusMap: {\n            high: { tone: \"danger\", label: \"High\" },\n            medium: { tone: \"warning\", label: \"Medium\" },\n            low: { tone: \"neutral\", label: \"Low\" },\n          },\n        }}\n      />\n\n      {/* array */}\n      <FormatSample\n        title=\"array (maxVisible)\"\n        value={[\"alpha\", \"beta\", \"gamma\", \"delta\"]}\n        format={{ kind: \"array\", maxVisible: 2 }}\n      />\n    </div>\n  );\n}\n\nexport function FormatInlineExample({\n  value,\n  format,\n  row,\n  locale,\n  note,\n  output,\n}: {\n  value:\n    | string\n    | number\n    | boolean\n    | null\n    | (string | number | boolean | null)[];\n  format?: FormatConfig;\n  row?: Row;\n  locale?: string;\n  note?: string;\n  output?: React.ReactNode;\n}) {\n  const rendered = React.useMemo(() => {\n    if (output) return output;\n    if (format)\n      return renderFormattedValue({ value, column: { format }, row, locale });\n    return String(value ?? \"\");\n  }, [value, format, row, locale, output]);\n\n  return (\n    <div className=\"not-prose bg-card mt-2 rounded-md border p-3\">\n      <div className=\"grid grid-cols-1 gap-2 md:grid-cols-2\">\n        <div className=\"flex items-center gap-2\">\n          <span className=\"text-muted-foreground\">Input</span>{\" \"}\n          <code>{JSON.stringify(value)}</code>\n        </div>\n        <div className=\"flex items-center gap-2\">\n          <span className=\"text-muted-foreground\">Output</span>{\" \"}\n          <span className=\"inline-flex min-h-[20px] items-center gap-1 align-middle\">\n            {rendered}\n          </span>\n        </div>\n      </div>\n      {note ? (\n        <div className=\"text-muted-foreground mt-2 text-[11px]\">{note}</div>\n      ) : null}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/data-table/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Data Table\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\n    \"Data Table\",\n    \"Present structured data in sortable tables\",\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/data-table/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"Data Table\",\n  description: \"Present structured data in sortable tables\",\n};\n\nexport const revalidate = 3600;\n\nexport default function DataTableDocsPage() {\n  return <ComponentDocsTabs docs={<Content />} componentId=\"data-table\" />;\n}\n"
  },
  {
    "path": "apps/www/app/docs/data-table/tasks-demo.tsx",
    "content": "\"use client\";\n\nimport {\n  Column,\n  DataTable,\n  DataTableRowData,\n} from \"@/components/tool-ui/data-table\";\nimport { getMockTasks } from \"@/lib/mocks/tasks\";\nimport { useEffect, useState } from \"react\";\n\nexport function TasksDemo() {\n  const [rows, setRows] = useState<DataTableRowData[] | null>(null);\n\n  useEffect(() => {\n    getMockTasks().then((tasks) => {\n      setRows(\n        tasks.map((t) => ({\n          id: t.id,\n          issue: t.issue,\n          customer: t.customer,\n          priority: t.priority,\n          status: t.status,\n          assignee: t.assignee,\n          created: t.created,\n          urgencyOrder:\n            t.priority === \"high\" ? 1 : t.priority === \"medium\" ? 2 : 3,\n        })),\n      );\n    });\n  }, []);\n\n  const columns = [\n    {\n      key: \"priority\",\n      label: \"Urgency\",\n      format: {\n        kind: \"status\" as const,\n        statusMap: {\n          high: { tone: \"danger\", label: \"High\" },\n          medium: { tone: \"warning\", label: \"Medium\" },\n          low: { tone: \"neutral\", label: \"Low\" },\n        },\n      },\n    },\n    {\n      key: \"issue\",\n      label: \"Issue\",\n      truncate: true,\n      priority: \"primary\" as const,\n    },\n    { key: \"customer\", label: \"Customer\", priority: \"primary\" as const },\n    {\n      key: \"status\",\n      label: \"Status\",\n      format: {\n        kind: \"badge\" as const,\n        colorMap: {\n          open: \"info\",\n          \"in-progress\": \"warning\",\n          waiting: \"neutral\",\n          done: \"success\",\n        },\n      },\n    },\n    { key: \"assignee\", label: \"Owner\" },\n    {\n      key: \"created\",\n      label: \"Created\",\n      format: { kind: \"date\" as const, dateFormat: \"relative\" as const },\n      hideOnMobile: true,\n    },\n    { key: \"urgencyOrder\", label: \"Order\", hideOnMobile: true },\n  ] satisfies Column<DataTableRowData>[];\n\n  return (\n    <div className=\"not-prose\">\n      <DataTable.Table\n        id=\"data-table-tasks-demo\"\n        rowIdKey=\"id\"\n        columns={columns}\n        data={rows ?? []}\n        defaultSort={{ by: \"urgencyOrder\", direction: \"asc\" }}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/design-guidelines/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\nimport { MockThread, MockMessage } from \"../_components/mock-thread\";\nimport { CollaborationDiagram } from \"../_components/collaboration-diagram\";\nimport { DataTable } from \"@/components/tool-ui/data-table\";\n\n<DocsHeader\n  title=\"Design Guidelines\"\n  description=\"Principles for agentic UI.\"\n  mdxPath=\"app/docs/design-guidelines/content.mdx\"\n/>\n\n## The Collaboration Model\n\nIn AI chat, users interact with both the assistant and any tool UIs it renders. Most tool UIs treat this as incidental — a tool UI appears, the user clicks, done. That undersells what's happening. The assistant, the tool UI, and the user form a collaborative triad.\n\n<CollaborationDiagram />\n\nThe assistant gives context and narration. The tool UI gives structure that prose cannot — sortable tables, controls, media. They work together.\n\n<div>\n  <MockThread caption=\"The triadic loop in action\">\n    <MockMessage role=\"user\">\n      <span className=\"text-sm\">find a good auth library for React</span>\n    </MockMessage>\n    <MockMessage role=\"assistant\">\n      <span className=\"text-foreground\">\n        Found 8 React auth libraries on GitHub. Here are the top results by\n        recent activity:\n      </span>\n      <div className=\"mt-3\">\n        <DataTable\n          rowIdKey=\"id\"\n          columns={[\n            {\n              key: \"name\",\n              label: \"Repository\",\n              priority: \"primary\",\n              format: { kind: \"link\", hrefKey: \"url\", external: true },\n            },\n            {\n              key: \"stars\",\n              label: \"Stars\",\n              align: \"right\",\n              format: { kind: \"number\" },\n            },\n            {\n              key: \"weeklyChange\",\n              label: \"Weekly\",\n              align: \"right\",\n              format: { kind: \"delta\", decimals: 0, showSign: true },\n            },\n            {\n              key: \"issues\",\n              label: \"Issues\",\n              align: \"right\",\n              format: { kind: \"number\" },\n            },\n            {\n              key: \"license\",\n              label: \"License\",\n              format: { kind: \"badge\" },\n            },\n          ]}\n          data={[\n            {\n              id: \"1\",\n              name: \"next-auth\",\n              url: \"https://github.com/nextauthjs/next-auth\",\n              stars: 19420,\n              weeklyChange: 342,\n              issues: 86,\n              license: \"MIT\",\n            },\n            {\n              id: \"2\",\n              name: \"passport\",\n              url: \"https://github.com/jaredhanson/passport\",\n              stars: 847,\n              weeklyChange: -12,\n              issues: 124,\n              license: \"MIT\",\n            },\n            {\n              id: \"3\",\n              name: \"auth0-react\",\n              url: \"https://github.com/auth0/auth0-react\",\n              stars: 3210,\n              weeklyChange: 89,\n              issues: 23,\n              license: \"MIT\",\n            },\n            {\n              id: \"4\",\n              name: \"firebase-auth\",\n              url: \"https://github.com/nicholasgriffintn/firebase-auth\",\n              stars: 562,\n              weeklyChange: 8,\n              issues: 45,\n              license: \"Apache-2.0\",\n            },\n            {\n              id: \"5\",\n              name: \"clerk-sdk\",\n              url: \"https://github.com/clerk/javascript\",\n              stars: 1580,\n              weeklyChange: 156,\n              issues: 31,\n              license: \"MIT\",\n            },\n          ]}\n        />\n      </div>\n    </MockMessage>\n    <MockMessage role=\"user\">\n      <span className=\"text-sm\">\n        Which one would you recommend for a Next.js app?\n      </span>\n    </MockMessage>\n    <MockMessage role=\"assistant\">\n      <span className=\"text-foreground\">\n        I recommend <strong>next-auth</strong>. It's purpose-built for Next.js,\n        has by far the most stars, and was updated most recently. Want me to\n        show you setup instructions?\n      </span>\n    </MockMessage>\n  </MockThread>\n</div>\n\nThe assistant introduces the tool UI, the user scans it and asks a follow-up, the assistant answers by referencing specific rows. This is the triadic loop.\n\n## Roles\n\nEvery Tool UI has a job:\n\n- **Information** — Display data. Users read more than they click. Examples: [Data Table](/docs/data-table), [Chart](/docs/chart), [Link Preview](/docs/link-preview).\n\n- **Decision** — For choices that matter (approve/reject, send/cancel). Show clear options and [receipts](/docs/receipts) that confirm what happened. Examples: [Approval Card](/docs/approval-card), [Option List](/docs/option-list).\n\n- **Control** — Adjust parameters without commitment (filters, sort orders, date ranges). Changes are reversible. Examples: [Parameter Slider](/docs/parameter-slider), [Preferences Panel](/docs/preferences-panel).\n\n- **State** — Show internal activity (progress indicators, status logs, loading states). Examples: [Progress Tracker](/docs/progress-tracker), [Terminal](/docs/terminal).\n\nSome tool UIs combine roles. A data table with row actions is \"information with control.\" When combining, pick one role to lead.\n\n## Constraints\n\nTool UIs live in chat: narrow widths, variable heights, mixed with prose.\n\n- **Vertical:** Communicate purpose within the first 300px or so.\n- **Horizontal:** Expect 400–600px. Prefer single-column layouts.\n- **Touch:** Interactive elements need at least 44×44px tap area.\n- **Choices:** Limit visible options to 5–7. The assistant can offer to show more.\n\n## Addressability\n\nIf the assistant can't point at something later, you lose half the value of rendering it.\n\n- **`id`**: Identifies this specific tool UI in the conversation (\"the preview above\", \"that table\").\n- **`assetId` / row IDs / option IDs**: Identify real objects (\"that link\", \"row 4\", \"the merge option\").\n\nUse stable IDs from your backend — database IDs, canonical URLs — not array indexes or render-time UUIDs. Anything the user can act on should have an ID the assistant can cite.\n\n## Anti-Patterns\n\n- **Input fields:** Input fields compete with the main chat composer. Ask whether the assistant could gather that information through conversation, or link to a form outside the chat.\n- **Hidden mutations:** Actions that change state should show what happened. Terminal decisions need a receipt showing what the user chose.\n- **Kitchen sinks:** If it needs tabs or navigation, consider breaking it into separate Tool UIs.\n- **Uncontextualized tool UIs:** Always have the assistant introduce a tool UI and explain what it shows.\n- **Redundant narration:** The assistant shouldn't describe what the tool UI already shows. The tool UI shouldn't echo what the assistant just said. If the assistant asks \"Where to?\", the tool UI doesn't need a header saying \"Where to?\" — just show the options. Divide the work between them: the assistant gives context, the tool UI gives structure.\n\n## Further reading\n\n- [**Overview**](/docs/overview): What Tool UI is and how schema-first rendering works\n- [**Actions**](/docs/actions): The local vs. decision action model in detail\n- [**Receipts**](/docs/receipts): How to show durable records of user decisions\n- [**Gallery**](/docs/gallery): Browse all available components\n"
  },
  {
    "path": "apps/www/app/docs/design-guidelines/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Design Guidelines\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\n    \"Design Guidelines\",\n    \"Principles for AI tool interfaces\",\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/design-guidelines/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { DocsArticle } from \"../_components/docs-article\";\n\nexport const metadata: Metadata = {\n  title: \"Design Guidelines\",\n  description: \"Design principles and patterns for Tool UI components\",\n};\n\nexport const revalidate = 3600;\n\nexport default function DesignGuidelinesPage() {\n  return (\n    <DocsArticle>\n      <Content />\n    </DocsArticle>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/gallery/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Gallery\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\"Gallery\", \"Browse all component examples\");\n}\n"
  },
  {
    "path": "apps/www/app/docs/gallery/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport dynamic from \"next/dynamic\";\nimport type { ReactNode } from \"react\";\n\nimport {\n  GalleryPageAnalytics,\n  GalleryPreviewImpression,\n} from \"@/app/docs/_components/gallery-analytics.client\";\nimport { DocsBorderedShell } from \"@/app/docs/_components/docs-bordered-shell\";\nimport { GalleryCardHeader } from \"@/app/docs/_components/gallery-card-header\";\nimport { DataTable } from \"@/components/tool-ui/data-table\";\nimport { ItemCarousel } from \"@/components/tool-ui/item-carousel\";\nimport { OrderSummary } from \"@/components/tool-ui/order-summary\";\nimport { StatsDisplay } from \"@/components/tool-ui/stats-display\";\nimport { type GalleryComponentDocId } from \"@/lib/docs/gallery-component-docs\";\nimport {\n  GALLERY_COLUMN_STACK_CLASS,\n  GALLERY_DESKTOP_GRID_CLASS,\n  GALLERY_LAYOUT_CLASS,\n  GALLERY_MOBILE_STACK_CLASS,\n  GALLERY_STANDARD_PREVIEW_WRAPPER_CLASS,\n} from \"@/lib/docs/gallery-layout\";\nimport { approvalCardPresets } from \"@/lib/presets/approval-card\";\nimport { audioPresets } from \"@/lib/presets/audio\";\nimport { chartPresets } from \"@/lib/presets/chart\";\nimport { citationPresets } from \"@/lib/presets/citation\";\nimport { codeBlockPresets } from \"@/lib/presets/code-block\";\nimport { codeDiffPresets } from \"@/lib/presets/code-diff\";\nimport { dataTablePresets } from \"@/lib/presets/data-table\";\nimport { geoMapPresets } from \"@/lib/presets/geo-map\";\nimport { instagramPostPresets } from \"@/lib/presets/instagram-post\";\nimport { imageGalleryPresets } from \"@/lib/presets/image-gallery\";\nimport { itemCarouselPresets } from \"@/lib/presets/item-carousel\";\nimport { linkPreviewPresets } from \"@/lib/presets/link-preview\";\nimport { linkedInPostPresets } from \"@/lib/presets/linkedin-post\";\nimport { messageDraftPresets } from \"@/lib/presets/message-draft\";\nimport { optionListPresets } from \"@/lib/presets/option-list\";\nimport { orderSummaryPresets } from \"@/lib/presets/order-summary\";\nimport { parameterSliderPresets } from \"@/lib/presets/parameter-slider\";\nimport { planPresets } from \"@/lib/presets/plan\";\nimport { preferencesPanelPresets } from \"@/lib/presets/preferences-panel\";\nimport { progressTrackerPresets } from \"@/lib/presets/progress-tracker\";\nimport { questionFlowPresets } from \"@/lib/presets/question-flow\";\nimport { statsDisplayPresets } from \"@/lib/presets/stats-display\";\nimport { terminalPresets } from \"@/lib/presets/terminal\";\nimport { weatherWidgetPresets } from \"@/lib/presets/weather-widget\";\nimport { xPostPresets } from \"@/lib/presets/x-post\";\nimport { cn } from \"@/lib/ui/cn\";\n\nconst ApprovalCard = dynamic(() =>\n  import(\"@/components/tool-ui/approval-card\").then((m) => m.ApprovalCard),\n);\nconst CitationList = dynamic(() =>\n  import(\"@/components/tool-ui/citation\").then((m) => m.CitationList),\n);\nconst ImageGallery = dynamic(() =>\n  import(\"@/components/tool-ui/image-gallery\").then((m) => m.ImageGallery),\n);\nconst Audio = dynamic(() =>\n  import(\"@/components/tool-ui/audio\").then((m) => m.Audio),\n);\nconst LinkPreview = dynamic(() =>\n  import(\"@/components/tool-ui/link-preview\").then((m) => m.LinkPreview),\n);\nconst LinkedInPost = dynamic(() =>\n  import(\"@/components/tool-ui/linkedin-post\").then((m) => m.LinkedInPost),\n);\nconst InstagramPost = dynamic(() =>\n  import(\"@/components/tool-ui/instagram-post\").then((m) => m.InstagramPost),\n);\nconst OptionList = dynamic(() =>\n  import(\"@/components/tool-ui/option-list\").then((m) => m.OptionList),\n);\nconst ParameterSlider = dynamic(() =>\n  import(\"@/components/tool-ui/parameter-slider\").then(\n    (m) => m.ParameterSlider,\n  ),\n);\nconst Plan = dynamic(() =>\n  import(\"@/components/tool-ui/plan\").then((m) => m.Plan),\n);\nconst Terminal = dynamic(() =>\n  import(\"@/components/tool-ui/terminal\").then((m) => m.Terminal),\n);\nconst CodeBlock = dynamic(() =>\n  import(\"@/components/tool-ui/code-block\").then((m) => m.CodeBlock),\n);\nconst CodeDiff = dynamic(() =>\n  import(\"@/components/tool-ui/code-diff\").then((m) => m.CodeDiff),\n);\nconst Chart = dynamic(() =>\n  import(\"@/components/tool-ui/chart\").then((m) => m.Chart),\n);\nconst GeoMap = dynamic(() =>\n  import(\"@/components/tool-ui/geo-map\").then((m) => m.GeoMap),\n);\nconst PreferencesPanel = dynamic(() =>\n  import(\"@/components/tool-ui/preferences-panel\").then(\n    (m) => m.PreferencesPanel,\n  ),\n);\nconst ProgressTracker = dynamic(() =>\n  import(\"@/components/tool-ui/progress-tracker\").then(\n    (m) => m.ProgressTracker,\n  ),\n);\nconst QuestionFlow = dynamic(() =>\n  import(\"@/components/tool-ui/question-flow\").then((m) => m.QuestionFlow),\n);\nconst MessageDraft = dynamic(() =>\n  import(\"@/components/tool-ui/message-draft\").then((m) => m.MessageDraft),\n);\nconst XPost = dynamic(() =>\n  import(\"@/components/tool-ui/x-post\").then((m) => m.XPost),\n);\nconst WeatherWidget = dynamic(() =>\n  import(\"@/components/tool-ui/weather-widget/runtime\").then(\n    (m) => m.WeatherWidget,\n  ),\n);\n\nexport const metadata: Metadata = {\n  title: \"Gallery\",\n  description: \"Browse all Tool UI components in a visual gallery\",\n};\n\nexport const revalidate = 3600;\n\ninterface GalleryPreviewCardProps {\n  componentId: GalleryComponentDocId;\n  className?: string;\n  children: ReactNode;\n}\n\ninterface GalleryCardConfig {\n  componentId: GalleryComponentDocId;\n  className: string;\n  render: () => ReactNode;\n}\n\nfunction GalleryPreviewCard({\n  componentId,\n  className,\n  children,\n}: GalleryPreviewCardProps) {\n  return (\n    <article\n      className={cn(\n        \"group/gallery-card relative flex items-center justify-center w-full min-w-0 max-w-full flex-col gap-3 overflow-hidden rounded-xl  bg-card/40 p-3 pb-5 shadow-sm transition-[background-color,box-shadow] duration-200 hover:bg-card/80 focus-within:bg-card/80\",\n        className,\n      )}\n    >\n      <GalleryPreviewImpression componentId={componentId} />\n      <GalleryCardHeader componentId={componentId} hideDescription />\n      {children}\n    </article>\n  );\n}\n\nexport default function ComponentsGalleryPage() {\n  const standardPreviewWidthClass = \"w-full max-w-[680px]\";\n  const withStandardPreviewWidth = (node: ReactNode) => (\n    <div className={GALLERY_STANDARD_PREVIEW_WRAPPER_CLASS}>{node}</div>\n  );\n\n  const galleryCards: GalleryCardConfig[] = [\n    {\n      componentId: \"option-list\",\n      className: \"mb-5 flex break-inside-avoid justify-center 2xl:mb-5\",\n      render: () => (\n        <OptionList {...optionListPresets[\"max-selections\"].data} />\n      ),\n    },\n    {\n      componentId: \"question-flow\",\n      className: \"mb-5 flex break-inside-avoid justify-center 2xl:mb-5\",\n      render: () =>\n        withStandardPreviewWidth(\n          <QuestionFlow {...questionFlowPresets.upfront.data} />,\n        ),\n    },\n    {\n      componentId: \"plan\",\n      className: \"mb-5 flex break-inside-avoid justify-center 2xl:mb-5\",\n      render: () =>\n        withStandardPreviewWidth(<Plan {...planPresets.comprehensive.data} />),\n    },\n    {\n      componentId: \"order-summary\",\n      className: \"mb-5 break-inside-avoid 2xl:mb-5\",\n      render: () => (\n        <OrderSummary.Display\n          {...orderSummaryPresets.default.data}\n          className={standardPreviewWidthClass}\n        />\n      ),\n    },\n    {\n      componentId: \"item-carousel\",\n      className: \"mb-5 flex justify-center 2xl:col-span-full 2xl:mb-5\",\n      render: () => (\n        <div className=\"w-full max-w-full min-w-0\">\n          <ItemCarousel {...itemCarouselPresets.recommendations.data} />\n        </div>\n      ),\n    },\n    {\n      componentId: \"data-table\",\n      className: \"mb-5 flex justify-center 2xl:col-span-full 2xl:mb-5\",\n      render: () => (\n        <div className=\"w-full max-w-full min-w-0\">\n          <DataTable {...dataTablePresets.stocks.data} />\n        </div>\n      ),\n    },\n    {\n      componentId: \"code-diff\",\n      className: \"mb-5 flex break-inside-avoid justify-center 2xl:mb-5\",\n      render: () => <CodeDiff {...codeDiffPresets[\"refactor\"].data} />,\n    },\n    {\n      componentId: \"stats-display\",\n      className: \"mb-5 flex break-inside-avoid justify-center 2xl:mb-5\",\n      render: () => (\n        <StatsDisplay {...statsDisplayPresets[\"business-metrics\"].data} />\n      ),\n    },\n    {\n      componentId: \"weather-widget\",\n      className: \"mb-5 flex break-inside-avoid justify-center 2xl:mb-5\",\n      render: () => (\n        <WeatherWidget\n          {...weatherWidgetPresets[\"sunny-forecast\"].data}\n          current={{\n            temperature: 64,\n            tempMin: 58,\n            tempMax: 72,\n            conditionCode: \"thunderstorm\",\n          }}\n          updatedAt=\"2026-01-29T02:30:00Z\"\n          effects={{ enabled: true, quality: \"low\" }}\n        />\n      ),\n    },\n    {\n      componentId: \"image-gallery\",\n      className: \"mb-5 flex break-inside-avoid justify-center 2xl:mb-5\",\n      render: () => (\n        <ImageGallery {...imageGalleryPresets[\"search-results\"].data} />\n      ),\n    },\n    {\n      componentId: \"geo-map\",\n      className: \"mb-5 flex break-inside-avoid justify-center 2xl:mb-5\",\n      render: () => (\n        <GeoMap id=\"gallery-geo-map\" {...geoMapPresets.fleet.data} />\n      ),\n    },\n    {\n      componentId: \"link-preview\",\n      className: \"mb-5 flex break-inside-avoid justify-center 2xl:mb-5\",\n      render: () => (\n        <LinkPreview {...linkPreviewPresets[\"with-image\"].data.linkPreview} />\n      ),\n    },\n    {\n      componentId: \"citation\",\n      className: \"mb-5 flex break-inside-avoid justify-center 2xl:mb-5\",\n      render: () => (\n        <CitationList\n          id=\"gallery-citations\"\n          citations={citationPresets.stacked.data.citations}\n          variant=\"stacked\"\n        />\n      ),\n    },\n    {\n      componentId: \"audio\",\n      className: \"mb-5 flex break-inside-avoid justify-center 2xl:mb-5\",\n      render: () => <Audio {...audioPresets.full.data.audio} />,\n    },\n    {\n      componentId: \"approval-card\",\n      className: \"mb-5 flex break-inside-avoid justify-center 2xl:mb-5\",\n      render: () => (\n        <ApprovalCard {...approvalCardPresets[\"with-metadata\"].data} />\n      ),\n    },\n    {\n      componentId: \"message-draft\",\n      className: \"mb-5 flex break-inside-avoid justify-center 2xl:mb-5\",\n      render: () =>\n        withStandardPreviewWidth(\n          <MessageDraft {...messageDraftPresets.email.data} />,\n        ),\n    },\n    {\n      componentId: \"progress-tracker\",\n      className: \"mb-5 flex break-inside-avoid justify-center 2xl:mb-5\",\n      render: () =>\n        withStandardPreviewWidth(\n          <ProgressTracker {...progressTrackerPresets[\"in-progress\"].data} />,\n        ),\n    },\n    {\n      componentId: \"parameter-slider\",\n      className: \"mb-5 flex break-inside-avoid justify-center 2xl:mb-5\",\n      render: () =>\n        withStandardPreviewWidth(\n          <ParameterSlider\n            {...parameterSliderPresets[\"photo-adjustments\"].data}\n          />,\n        ),\n    },\n    {\n      componentId: \"terminal\",\n      className: \"mb-5 flex break-inside-avoid justify-center 2xl:mb-5\",\n      render: () => <Terminal {...terminalPresets.success.data} />,\n    },\n    {\n      componentId: \"code-block\",\n      className: \"mb-5 flex break-inside-avoid justify-center 2xl:mb-5\",\n      render: () => <CodeBlock {...codeBlockPresets.typescript.data} />,\n    },\n    {\n      componentId: \"chart\",\n      className: \"mb-5 flex break-inside-avoid justify-center 2xl:mb-5\",\n      render: () => <Chart id=\"gallery-chart\" {...chartPresets.revenue.data} />,\n    },\n    {\n      componentId: \"preferences-panel\",\n      className: \"mb-5 flex break-inside-avoid justify-center 2xl:mb-5\",\n      render: () =>\n        withStandardPreviewWidth(\n          <PreferencesPanel {...preferencesPanelPresets.privacy.data} />,\n        ),\n    },\n    {\n      componentId: \"x-post\",\n      className: \"mb-5 flex break-inside-avoid justify-center 2xl:mb-5\",\n      render: () => <XPost post={xPostPresets.basic.data.post} />,\n    },\n    {\n      componentId: \"instagram-post\",\n      className: \"mb-5 flex break-inside-avoid justify-center 2xl:mb-5\",\n      render: () => (\n        <InstagramPost post={instagramPostPresets.basic.data.post} />\n      ),\n    },\n    {\n      componentId: \"linkedin-post\",\n      className: \"mb-5 flex break-inside-avoid justify-center 2xl:mb-5\",\n      render: () => <LinkedInPost post={linkedInPostPresets.basic.data.post} />,\n    },\n  ];\n\n  const stackRankOrder: GalleryComponentDocId[] = [\n    \"option-list\",\n    \"question-flow\",\n    \"weather-widget\",\n    \"plan\",\n    \"parameter-slider\",\n    \"item-carousel\",\n    \"code-diff\",\n    \"data-table\",\n    \"stats-display\",\n    \"geo-map\",\n    \"image-gallery\",\n    \"code-block\",\n    \"audio\",\n    \"terminal\",\n  ];\n  const stackRankByComponentId = new Map(\n    stackRankOrder.map((componentId, rank) => [componentId, rank]),\n  );\n  const rankedGalleryCards = galleryCards\n    .map((card, originalIndex) => ({ card, originalIndex }))\n    .sort((a, b) => {\n      const aRank =\n        stackRankByComponentId.get(a.card.componentId) ??\n        Number.POSITIVE_INFINITY;\n      const bRank =\n        stackRankByComponentId.get(b.card.componentId) ??\n        Number.POSITIVE_INFINITY;\n\n      if (aRank !== bRank) {\n        return aRank - bRank;\n      }\n      return a.originalIndex - b.originalIndex;\n    })\n    .map(({ card }) => card);\n\n  const [leftColumnCards, rightColumnCards] = rankedGalleryCards.reduce<\n    [GalleryCardConfig[], GalleryCardConfig[]]\n  >(\n    (columns, card, index) => {\n      columns[index % 2].push(card);\n      return columns;\n    },\n    [[], []],\n  );\n\n  const renderGalleryCard = (card: GalleryCardConfig) => (\n    <GalleryPreviewCard\n      key={card.componentId}\n      componentId={card.componentId}\n      className={card.className}\n    >\n      <div className=\"flex w-full justify-center\">{card.render()}</div>\n    </GalleryPreviewCard>\n  );\n\n  return (\n    <DocsBorderedShell>\n      <main\n        aria-label=\"Tool UI component gallery\"\n        className=\"scrollbar-subtle z-10 min-h-0 min-w-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-contain p-6 sm:p-10 lg:p-12\"\n      >\n        <h1 className=\"sr-only\">Tool UI Component Gallery</h1>\n        <GalleryPageAnalytics />\n        <div className={GALLERY_LAYOUT_CLASS}>\n          <div className={GALLERY_MOBILE_STACK_CLASS}>\n            {rankedGalleryCards.map(renderGalleryCard)}\n          </div>\n\n          <div className={GALLERY_DESKTOP_GRID_CLASS}>\n            <div className={GALLERY_COLUMN_STACK_CLASS}>\n              {leftColumnCards.map(renderGalleryCard)}\n            </div>\n            <div className={GALLERY_COLUMN_STACK_CLASS}>\n              {rightColumnCards.map(renderGalleryCard)}\n            </div>\n          </div>\n\n          {/* <div className=\"mb-5 flex justify-center break-inside-avoid 2xl:mb-5\">\n            <Link\n              href=\"/builder\"\n              className=\"bg-foreground/5 text-muted-foreground bg-dot-grid hover:text-foreground hover:bg-primary/7 group flex min-h-[180px] w-full flex-row items-center justify-center gap-2 rounded-2xl p-6 text-center shadow-[inset_0_6px_20px_rgba(0,0,0,0.09)] transition-colors duration-300\"\n            >\n              <span className=\"text-primary text-2xl font-light tracking-wide transition-transform duration-600 will-change-transform group-hover:scale-105\">\n                Build your own tool UI\n              </span>\n              <ArrowRightIcon className=\"size-6 shrink-0 transition-transform duration-600 will-change-transform group-hover:translate-x-3 group-hover:scale-105\" />\n            </Link>\n          </div> */}\n        </div>\n      </main>\n    </DocsBorderedShell>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/geo-map/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\n\n<DocsHeader\n  title=\"Geo Map\"\n  description=\"Display geolocated entities, clusters, and routes.\"\n  mdxPath=\"app/docs/geo-map/content.mdx\"\n/>\n\nGeo Map renders one or many latitude/longitude markers directly in the conversation so users can see locations and routes on an interactive map.\n\n**Role:** Information. For visualizing location and route data. See [Design Guidelines](/docs/design-guidelines) for role definitions.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this command from your project root.\n\n<InstallCommandBlock componentId=\"geo-map\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Add Leaflet CSS to app/layout.tsx\">\n\nOpen `app/layout.tsx` and add the Leaflet import next to your global CSS import.\n\n```tsx\nimport \"./styles/globals.css\";\nimport \"leaflet/dist/leaflet.css\";\n```\n\n</Step>\n\n<Step title=\"Render it in your UI\">\n\nRender `GeoMap` in your UI.\n\n```tsx\nimport { GeoMap } from \"@/components/tool-ui/geo-map\";\n\n<GeoMap\n  id=\"fleet-map\"\n  title=\"Fleet Positions\"\n  markers={[\n    {\n      id: \"truck-14\",\n      lat: 34.0522,\n      lng: -118.2437,\n      label: \"Truck 14\",\n      icon: { type: \"emoji\", value: \"🚚\" },\n    },\n    {\n      id: \"truck-22\",\n      lat: 36.1699,\n      lng: -115.1398,\n      label: \"Truck 22\",\n      icon: { type: \"dot\", color: \"#0EA5E9\" },\n    },\n  ]}\n  routes={[\n    {\n      id: \"route-west-14\",\n      points: [\n        { lat: 33.94, lng: -118.4 },\n        { lat: 34.0522, lng: -118.2437 },\n      ],\n      color: \"#2563EB\",\n    },\n  ]}\n  clustering={{ enabled: true }}\n  viewport={{ mode: \"fit\", target: \"all\", padding: 40, maxZoom: 11 }}\n/>;\n```\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegister `GeoMap` in your runtime provider so tool results render as map UI instead of raw JSON.\n\n```tsx\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { GeoMap } from \"@/components/tool-ui/geo-map\";\nimport { safeParseSerializableGeoMap } from \"@/components/tool-ui/geo-map/schema\";\n\nexport const toolkit: Toolkit = {\n  showGeoMap: {\n    type: \"backend\",\n    render: ({ result }) => {\n      const parsed = safeParseSerializableGeoMap(result);\n      if (!parsed) {\n        return null;\n      }\n      return <GeoMap {...parsed} />;\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"MapPin\" title=\"Custom marker icons\">\n    Dot, emoji, and image marker icon variants with schema validation\n  </Feature>\n  <Feature icon=\"Network\" title=\"Marker clustering\">\n    Aggregates dense markers by zoom level with click-to-expand behavior\n  </Feature>\n  <Feature icon=\"Route\" title=\"Route polylines\">\n    Static, styled route overlays with tooltip/popup metadata\n  </Feature>\n  <Feature icon=\"ScanSearch\" title=\"Targeted fit bounds\">\n    Fit markers, routes, or both using viewport target controls\n  </Feature>\n</FeatureGrid>\n\n## Accessibility & Keyboard\n\n- The map viewport is announced as a named region using `title` + `description`.\n- Marker and cluster icons expose descriptive labels for assistive tech.\n- Press `Escape` while focused in the map area to close any open popup.\n\n## Popup & Tooltip Theme Tokens\n\nGeoMap shell styling is shipped with the component in a colocated CSS module (`geo-map-theme.module.css`), scoped to `[data-slot=\"geo-map\"]`, and exposed through CSS custom properties. Import Leaflet base CSS once in your root layout, then use these token overrides to customize popup/tooltip/zoom styling.\n\nTheme behavior:\n\n- When `theme` is omitted, GeoMap inherits the environment theme (`.dark`/`.light` class or `data-theme`) and falls back to `prefers-color-scheme`.\n- Pass `theme=\"light\"` or `theme=\"dark\"` to force a tile theme.\n- `--geo-map-canvas-bg` is set by default from the resolved theme (`var(--background)` in dark mode, `var(--muted)` in light mode), and can still be overridden via `style`.\n\n```tsx\n<GeoMap\n  id=\"fleet-map\"\n  markers={markers}\n  style={{\n    \"--geo-map-popup-bg\": \"var(--card)\",\n    \"--geo-map-popup-fg\": \"var(--card-foreground)\",\n    \"--geo-map-popup-border\": \"var(--border)\",\n    \"--geo-map-popup-radius\": \"calc(var(--radius) + 4px)\",\n    \"--geo-map-tooltip-bg\": \"var(--primary)\",\n    \"--geo-map-tooltip-fg\": \"var(--primary-foreground)\",\n  }}\n/>\n```\n\nSee `geo-map-theme.module.css` for all available token keys.\n\n## Props\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for this map instance\",\n      type: \"string\",\n      required: true,\n    },\n    markers: {\n      description: \"Markers to render\",\n      type: \"GeoMapMarker[]\",\n      required: true,\n    },\n    routes: {\n      description: \"Optional route polylines\",\n      type: \"GeoMapRoute[]\",\n    },\n    clustering: {\n      description: \"Marker clustering behavior\",\n      type: \"GeoMapClustering\",\n    },\n    title: {\n      description: \"Optional map title\",\n      type: \"string\",\n    },\n    description: {\n      description: \"Optional subtitle/description\",\n      type: \"string\",\n    },\n    viewport: {\n      description: \"Map viewport behavior\",\n      type: \"GeoMapViewport\",\n    },\n    showZoomControl: {\n      description: \"Whether zoom controls are visible\",\n      type: \"boolean\",\n      default: \"true\",\n    },\n    theme: {\n      description:\n        \"Optional explicit tile theme override. If omitted, GeoMap inherits document/system theme.\",\n      type: \"'light' | 'dark'\",\n      default: \"inherits environment\",\n    },\n    className: {\n      description: \"Tailwind classes for the root wrapper\",\n      type: \"string\",\n    },\n    style: {\n      description:\n        \"Inline style overrides for GeoMap CSS custom properties (for popup/tooltip shell theming)\",\n      type: \"CSSProperties\",\n    },\n    popupClassName: {\n      description: \"Tailwind classes applied to the Leaflet popup shell\",\n      type: \"string\",\n    },\n    tooltipClassName: {\n      description: \"Tailwind classes for marker/route tooltip shell\",\n      type: \"string\",\n    },\n    onMarkerClick: {\n      description: \"Client callback when a marker is clicked\",\n      type: \"(marker: GeoMapMarker) => void\",\n    },\n    onRouteClick: {\n      description: \"Client callback when a route polyline is clicked\",\n      type: \"(route: GeoMapRoute) => void\",\n    },\n  }}\n/>\n\n## Marker Schema\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Optional stable marker ID\",\n      type: \"string\",\n    },\n    lat: {\n      description: \"Latitude in decimal degrees\",\n      type: \"number (-90 to 90)\",\n      required: true,\n    },\n    lng: {\n      description: \"Longitude in decimal degrees\",\n      type: \"number (-180 to 180)\",\n      required: true,\n    },\n    label: {\n      description: \"Primary marker label\",\n      type: \"string\",\n    },\n    description: {\n      description: \"Optional detail text shown in popup\",\n      type: \"string\",\n    },\n    tooltip: {\n      description: \"Tooltip behavior\",\n      type: \"'none' | 'hover' | 'always'\",\n    },\n    icon: {\n      description: \"Marker icon configuration\",\n      type: \"GeoMapMarkerIcon\",\n    },\n  }}\n/>\n\n## Marker Icon Variants\n\n`GeoMapMarkerIcon` supports:\n\n1. `dot` — optional `color`, `borderColor`, and `radius`.\n2. `emoji` — required `value`, optional `size`, `bgColor`, `borderColor`.\n3. `image` — required `url` (`http/https`), optional size and border styling.\n\n## Route Schema\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Optional stable route ID\",\n      type: \"string\",\n    },\n    points: {\n      description: \"Ordered coordinates making up the route\",\n      type: \"Array<{ lat: number; lng: number }> (min 2)\",\n      required: true,\n    },\n    label: {\n      description: \"Route label for tooltip/popup\",\n      type: \"string\",\n    },\n    description: {\n      description: \"Route detail text\",\n      type: \"string\",\n    },\n    tooltip: {\n      description: \"Tooltip behavior\",\n      type: \"'none' | 'hover' | 'always'\",\n    },\n    color: {\n      description: \"Polyline color\",\n      type: \"string\",\n    },\n    weight: {\n      description: \"Polyline stroke width\",\n      type: \"number (1..12)\",\n    },\n    opacity: {\n      description: \"Polyline opacity\",\n      type: \"number (0..1)\",\n    },\n    dashArray: {\n      description: \"SVG dash pattern\",\n      type: \"string (e.g. '6 4')\",\n    },\n  }}\n/>\n\n## Clustering Schema\n\n<TypeTable\n  type={{\n    enabled: {\n      description: \"Enable marker clustering\",\n      type: \"boolean\",\n      default: \"false\",\n    },\n    radius: {\n      description: \"Cluster radius in pixels\",\n      type: \"number (20..120)\",\n      default: \"60\",\n    },\n    maxZoom: {\n      description: \"Maximum zoom level at which clustering applies\",\n      type: \"number (1..22)\",\n      default: \"16\",\n    },\n    minPoints: {\n      description: \"Minimum points required to form a cluster\",\n      type: \"number (2..20)\",\n      default: \"2\",\n    },\n  }}\n/>\n\n## Viewport Schema\n\n`viewport` supports two modes:\n\n1. `fit` — auto-fit points in `target` (`markers`, `routes`, or `all`). Supports optional `padding` and `maxZoom`.\n2. `center` — fixed `center` + `zoom`.\n\n## Accessibility\n\n- Marker and route popups provide text equivalents for map points and paths\n- Tooltips can be made persistent using `tooltip: \"always\"` (and hide while the related popup is open)\n- Leaflet controls remain keyboard accessible\n\n## Related\n\n- [Chart](/docs/chart): compare numeric time-series\n- [Data Table](/docs/data-table): inspect structured marker and route metadata\n"
  },
  {
    "path": "apps/www/app/docs/geo-map/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Geo Map\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\n    \"Geo Map\",\n    \"Display geolocated entities, clusters, and routes.\",\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/geo-map/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"Geo Map\",\n  description: \"Display geolocated entities, clusters, and routes.\",\n};\n\nexport const revalidate = 3600;\n\nexport default function GeoMapDocsPage() {\n  return <ComponentDocsTabs docs={<Content />} componentId=\"geo-map\" />;\n}\n"
  },
  {
    "path": "apps/www/app/docs/image/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\nimport { Image } from \"@/components/tool-ui/image\";\n\n<DocsHeader\n  title=\"Image\"\n  description=\"Display images with metadata and attribution.\"\n  mdxPath=\"app/docs/image/content.mdx\"\n/>\n\nWhen a tool returns an image URL, a bare `<img>` tag has no context. No title, no attribution, no way for the assistant to reference it later. Image renders pictures inline in the conversation with title, description, source attribution, and configurable aspect ratios. The user sees the image in context instead of opening a raw link in a new tab.\n\n**Role:** Information. For displaying data without user input. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"image\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nRender `Image` in your UI with tool-compatible props.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\nimport { Image } from \"@/components/tool-ui/image\";\n\nexport function Example() {\n  return (\n    <Image\n      id=\"image-example\"\n      assetId=\"vintage-mainframe\"\n      src=\"https://images.unsplash.com/photo-1504548840739-580b10ae7715\"\n      alt=\"Vintage mainframe with blinking lights\"\n      title=\"From mainframes to microchips\"\n      description=\"A snapshot of when rooms were computers.\"\n      ratio=\"4:3\"\n      domain=\"unsplash.com\"\n    />\n  );\n}\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegistration tells assistant-ui which component to render when a tool returns image data. Without it, tool results appear as raw JSON.\n\n```tsx\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { Image } from \"@/components/tool-ui/image\";\nimport { safeParseSerializableImage } from \"@/components/tool-ui/image/schema\";\n\nexport const toolkit: Toolkit = {\n  showImage: {\n    type: \"backend\",\n    render: ({ result }) => {\n      const parsed = safeParseSerializableImage(result);\n      if (!parsed) {\n        return null;\n      }\n      return <Image {...parsed} />;\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"Eye\" title=\"Hover overlay\">\n    Title and source appear on hover, stay hidden otherwise\n  </Feature>\n  <Feature icon=\"ExternalLink\" title=\"Source attribution\">\n    Links back to the original source with favicon and domain\n  </Feature>\n  <Feature icon=\"Maximize2\" title=\"Aspect ratios\">\n    Constrain to 1:1, 4:3, 16:9, 9:16, or let the image decide with auto\n  </Feature>\n</FeatureGrid>\n\n## Props\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for this tool UI instance\",\n      type: \"string\",\n      required: true,\n    },\n    assetId: {\n      description: \"Persistent asset identifier\",\n      type: \"string\",\n      required: true,\n    },\n    src: {\n      description: \"Image source URL\",\n      type: \"string\",\n      required: true,\n    },\n    alt: {\n      description: \"Alt text for accessibility\",\n      type: \"string\",\n      required: true,\n    },\n    title: { description: \"Title text\", type: \"string\" },\n    description: { description: \"Description\", type: \"string\" },\n    href: {\n      description: \"Makes image clickable with this URL\",\n      type: \"string\",\n    },\n    domain: { description: \"Source domain label\", type: \"string\" },\n    ratio: {\n      description: \"Aspect ratio\",\n      type: \"'auto' | '1:1' | '4:3' | '16:9' | '9:16'\",\n      default: \"'auto'\",\n    },\n    fit: {\n      description: \"Object fit mode\",\n      type: \"'cover' | 'contain'\",\n      default: \"'cover'\",\n    },\n    source: {\n      description: \"Source attribution\",\n      type: \"{ label: string; iconUrl?: string; url?: string }\",\n    },\n    createdAt: { description: \"Creation timestamp (ISO 8601)\", type: \"string\" },\n    fileSizeBytes: { description: \"File size in bytes\", type: \"number\" },\n    onNavigate: {\n      description: \"Called when user clicks the image link (requires href)\",\n      type: \"(href: string, image: SerializableImage) => void\",\n    },\n  }}\n/>\n\nUse [`ToolUI.LocalActions`](/docs/actions) to compose external actions next to `Image`.\n\n## Related\n\n- [Image Gallery](/docs/image-gallery): for displaying multiple images in a masonry grid with a lightbox\n- [Video](/docs/video): the same inline media pattern for video content\n- [Link Preview](/docs/link-preview): for when the image is part of a richer link card with Open Graph metadata\n"
  },
  {
    "path": "apps/www/app/docs/image/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Image\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\n    \"Image\",\n    \"Display images with metadata and attribution\",\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/image/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"Image\",\n  description: \"Display images with metadata and attribution\",\n};\n\nexport const revalidate = 3600;\n\nexport default function ImageDocsPage() {\n  return <ComponentDocsTabs docs={<Content />} componentId=\"image\" />;\n}\n"
  },
  {
    "path": "apps/www/app/docs/image-gallery/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\nimport { ImageGallery } from \"@/components/tool-ui/image-gallery\";\n\n<DocsHeader\n  title=\"Image Gallery\"\n  description=\"Masonry grid with fullscreen lightbox viewer.\"\n  mdxPath=\"app/docs/image-gallery/content.mdx\"\n/>\n\nFive images stacked vertically push the rest of the conversation off-screen. ImageGallery arranges multiple images in a masonry grid that respects each photo's aspect ratio, with a fullscreen lightbox for inspecting detail. Captions, source attribution, and keyboard navigation come built in.\n\n**Role:** Information. For displaying data without user input. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"image-gallery\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nRender `ImageGallery` in your UI with tool-compatible props.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\nimport { ImageGallery } from \"@/components/tool-ui/image-gallery\";\n\nexport function Example() {\n  return (\n    <ImageGallery\n      id=\"image-gallery-example\"\n      title=\"Mountain landscapes\"\n      description=\"Here are some images matching your search\"\n      images={[\n        {\n          id: \"img-1\",\n          src: \"https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&h=600&fit=crop\",\n          alt: \"Dramatic mountain peaks at sunrise\",\n          width: 800,\n          height: 600,\n          title: \"Alpine Sunrise\",\n          caption: \"Dolomites, Italy\",\n        },\n        {\n          id: \"img-2\",\n          src: \"https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?w=800&h=1200&fit=crop\",\n          alt: \"Misty mountain valley\",\n          width: 800,\n          height: 1200,\n          title: \"Misty Valley\",\n        },\n        // ... more images\n      ]}\n      onImageClick={(id, image) => console.log(\"Clicked:\", id, image)}\n    />\n  );\n}\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegistration tells assistant-ui which component to render when a tool returns a set of images. Without it, tool results appear as raw JSON.\n\n```tsx\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { ImageGallery } from \"@/components/tool-ui/image-gallery\";\nimport { safeParseSerializableImageGallery } from \"@/components/tool-ui/image-gallery/schema\";\n\nexport const toolkit: Toolkit = {\n  showImageGallery: {\n    type: \"backend\",\n    render: ({ result }) => {\n      const parsed = safeParseSerializableImageGallery(result);\n      if (!parsed) {\n        return null;\n      }\n      return <ImageGallery {...parsed} />;\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"Grid2x2\" title=\"Masonry grid\">\n    Tiles images by aspect ratio so tall and wide shots sit together naturally\n  </Feature>\n  <Feature icon=\"Expand\" title=\"Fullscreen lightbox\">\n    Click any image to open it full-screen with title and caption\n  </Feature>\n  <Feature icon=\"Move\" title=\"Smooth transitions\">\n    Animated layout shift from grid thumbnail to lightbox view\n  </Feature>\n</FeatureGrid>\n\n## Props\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for this tool UI instance\",\n      type: \"string\",\n      required: true,\n    },\n    images: {\n      description: \"Array of image objects to display\",\n      type: \"ImageGalleryItem[]\",\n      required: true,\n    },\n    title: { description: \"Gallery header title\", type: \"string\" },\n    description: { description: \"Gallery header description\", type: \"string\" },\n    onImageClick: {\n      description: \"Image click handler\",\n      type: \"(imageId: string, image: ImageGalleryItem) => void\",\n    },\n  }}\n/>\n\n### ImageGalleryItem\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for this image\",\n      type: \"string\",\n      required: true,\n    },\n    src: {\n      description: \"Image source URL\",\n      type: \"string\",\n      required: true,\n    },\n    alt: {\n      description: \"Alt text for accessibility\",\n      type: \"string\",\n      required: true,\n    },\n    width: {\n      description: \"Image width in pixels (for layout calculation)\",\n      type: \"number\",\n      required: true,\n    },\n    height: {\n      description: \"Image height in pixels (for layout calculation)\",\n      type: \"number\",\n      required: true,\n    },\n    title: { description: \"Image title (shown in lightbox)\", type: \"string\" },\n    caption: {\n      description: \"Image caption (shown in lightbox)\",\n      type: \"string\",\n    },\n    source: {\n      description: \"Source attribution\",\n      type: \"{ label: string; url?: string }\",\n    },\n  }}\n/>\n\n## Related\n\n- [Image](/docs/image): for displaying a single image with metadata and attribution\n- [Video](/docs/video): for inline video playback in the same media pattern\n- [Item Carousel](/docs/item-carousel): for browsable collections where each card has actions, not just images\n"
  },
  {
    "path": "apps/www/app/docs/image-gallery/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Image Gallery\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\n    \"Image Gallery\",\n    \"Masonry grid with fullscreen lightbox viewer\",\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/image-gallery/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"Image Gallery\",\n  description: \"Masonry grid with fullscreen carousel viewer\",\n};\n\nexport const revalidate = 3600;\n\nexport default function ImageGalleryDocsPage() {\n  return <ComponentDocsTabs docs={<Content />} componentId=\"image-gallery\" />;\n}\n"
  },
  {
    "path": "apps/www/app/docs/instagram-post/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\nimport { InstagramPost } from \"@/components/tool-ui/instagram-post\";\n\n<DocsHeader\n  title=\"Instagram Post\"\n  description=\"Platform-native post card for Instagram.\"\n  mdxPath=\"app/docs/instagram-post/content.mdx\"\n/>\n\nInstagram is a visual platform, and its content deserves a visual preview. InstagramPost renders a media-first card that matches Instagram's layout: author info, image carousel, caption, and engagement stats. Use it when the assistant surfaces or drafts Instagram content in the conversation.\n\n**Role:** Information. For displaying data the user reads. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"instagram-post\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nRender `InstagramPost` in your UI with tool-compatible props.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\nimport { InstagramPost } from \"@/components/tool-ui/instagram-post\";\n\nexport function Example() {\n  return (\n    <InstagramPost\n      post={{\n        id: \"ig-post-1\",\n        author: {\n          name: \"Alex Rivera\",\n          handle: \"alexrivera\",\n          avatarUrl: \"https://example.com/avatar.jpg\",\n          verified: true,\n        },\n        text: \"Golden hour vibes in the city.\",\n        media: [\n          {\n            type: \"image\",\n            url: \"https://example.com/post-image.jpg\",\n            alt: \"City skyline\",\n          },\n        ],\n        stats: { likes: 8421 },\n        createdAt: \"2025-01-05T18:00:00.000Z\",\n      }}\n    />\n  );\n}\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegister this renderer so tool results display as `InstagramPost`.\n\n```tsx\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { InstagramPost } from \"@/components/tool-ui/instagram-post\";\nimport { safeParseSerializableInstagramPost } from \"@/components/tool-ui/instagram-post/schema\";\n\nexport const toolkit: Toolkit = {\n  showInstagramPost: {\n    type: \"backend\",\n    render: ({ result }) => {\n      const parsed = safeParseSerializableInstagramPost(result);\n      if (!parsed) {\n        return null;\n      }\n      return <InstagramPost post={parsed} />;\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"Paintbrush\" title=\"Platform-native styling\">\n    Uses Instagram's layout, typography, and color scheme\n  </Feature>\n  <Feature icon=\"Images\" title=\"Media-first layout\">\n    Single images and swipeable carousels with proper aspect ratios\n  </Feature>\n  <Feature icon=\"MousePointerClick\" title=\"External actions\">\n    Add actions like \"Save Draft\" or \"Share\" via ToolUI action surfaces\n  </Feature>\n  <Feature icon=\"BadgeCheck\" title=\"Verified identity\">\n    Verified badges and creator context on author profiles\n  </Feature>\n</FeatureGrid>\n\n## Props\n\n<TypeTable\n  type={{\n    post: {\n      description:\n        \"Post payload validated by `SerializableInstagramPostSchema`\",\n      type: \"InstagramPostData\",\n      required: true,\n    },\n    className: {\n      description: \"Optional className for the outer card\",\n      type: \"string\",\n    },\n    onAction: {\n      description: \"Fires when the user triggers a built-in post action\",\n      type: \"(action: string, post: InstagramPostData) => void\",\n    },\n  }}\n/>\n\nUse [`ToolUI.LocalActions`](/docs/actions) to compose external actions next to `InstagramPost`.\n\n## Related\n\n- [X Post](/docs/x-post): platform-native card for X (Twitter) content\n- [LinkedIn Post](/docs/linkedin-post): platform-native card for LinkedIn content\n"
  },
  {
    "path": "apps/www/app/docs/instagram-post/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Instagram Post\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\"Instagram Post\", \"Render Instagram post previews\");\n}\n"
  },
  {
    "path": "apps/www/app/docs/instagram-post/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"Instagram Post\",\n  description: \"Render Instagram post previews\",\n};\n\nexport const revalidate = 3600;\n\nexport default function InstagramPostDocsPage() {\n  return <ComponentDocsTabs docs={<Content />} componentId=\"instagram-post\" />;\n}\n"
  },
  {
    "path": "apps/www/app/docs/item-carousel/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\n\n<DocsHeader\n  title=\"Item Carousel\"\n  description=\"Horizontal carousel for browsing collections.\"\n  mdxPath=\"app/docs/item-carousel/content.mdx\"\n/>\n\nA vertical list of ten search results buries everything below the fold. ItemCarousel lays them out in a scrollable row of cards so the user can browse products, restaurants, or any collection the assistant returns without losing their place in the conversation. Each card shows an image, subtitle, and action buttons.\n\n**Role:** Information. For displaying data without user input. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"item-carousel\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nRender `ItemCarousel` in your UI with tool-compatible props.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\nimport { ItemCarousel } from \"@/components/tool-ui/item-carousel\";\n\nexport function Example({\n  payload,\n}: {\n  payload: React.ComponentProps<typeof ItemCarousel>;\n}) {\n  return <ItemCarousel {...payload} />;\n}\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegistration tells assistant-ui which component to render when a tool named `searchItems` returns data. Without it, tool results appear as raw JSON.\n\n```tsx\n// Backend tool\nimport { tool } from \"ai\";\nimport { z } from \"zod\";\nimport { SerializableItemCarouselSchema } from \"@/components/tool-ui/item-carousel/schema\";\n\nconst searchItems = tool({\n  description: \"Search for items matching criteria\",\n  inputSchema: z.object({\n    query: z.string(),\n    maxPrice: z.number().optional(),\n  }),\n  outputSchema: SerializableItemCarouselSchema,\n  async execute({ query, maxPrice }) {\n    const items = await fetchItems(query, maxPrice);\n    return {\n      id: `item-search-${query}`,\n      items: items.map((item) => ({\n        id: item.id,\n        name: item.name,\n        subtitle: `$${item.price.toFixed(2)}`,\n        image: item.imageUrl,\n        actions: [\n          { id: \"view\", label: \"View Details\" },\n          { id: \"select\", label: \"Select\", variant: \"default\" },\n        ],\n      })),\n    };\n  },\n});\n\n// Frontend with assistant-ui\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { ItemCarousel } from \"@/components/tool-ui/item-carousel\";\nimport { safeParseSerializableItemCarousel } from \"@/components/tool-ui/item-carousel/schema\";\n\nexport const toolkit: Toolkit = {\n  searchItems: {\n    type: \"backend\",\n    render: ({ result }) => {\n      const parsed = safeParseSerializableItemCarousel(result);\n      if (!parsed) {\n        return null;\n      }\n      return (\n        <ItemCarousel\n          {...parsed}\n          onItemClick={(id) => router.push(`/items/${id}`)}\n          onItemAction={(itemId, actionId) => {\n            if (actionId === \"select\") {\n              selectItem(itemId);\n            }\n          }}\n        />\n      );\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"Layers\" title=\"Collection browsing\">\n    Equal-height cards via CSS Grid so items line up regardless of title length\n  </Feature>\n  <Feature icon=\"GalleryHorizontalEnd\" title=\"Smart scrolling\">\n    Smooth scroll with snap points and reduced-motion fallback\n  </Feature>\n  <Feature icon=\"MousePointerClick\" title=\"Per-card actions\">\n    Buttons stack or sit side-by-side based on card width via container queries\n  </Feature>\n  <Feature icon=\"Keyboard\" title=\"Full accessibility\">\n    Enter/Space activation, focus rings, and ARIA roles throughout\n  </Feature>\n</FeatureGrid>\n\n## Props\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for this component instance\",\n      type: \"string\",\n      required: true,\n    },\n    items: {\n      description: \"Array of items to display\",\n      type: \"Item[]\",\n      required: true,\n    },\n    title: {\n      description: \"Optional header title\",\n      type: \"string\",\n    },\n    description: {\n      description: \"Optional header description\",\n      type: \"string\",\n    },\n    className: {\n      description: \"Additional CSS classes\",\n      type: \"string\",\n    },\n    onItemClick: {\n      description: \"Handler when an item card is clicked\",\n      type: \"(itemId: string) => void\",\n    },\n    onItemAction: {\n      description: \"Handler when an item action button is clicked\",\n      type: \"(itemId: string, actionId: string) => void\",\n    },\n  }}\n/>\n\n## Item Schema\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique item identifier\",\n      type: \"string\",\n      required: true,\n    },\n    name: {\n      description: \"Item display name\",\n      type: \"string\",\n      required: true,\n    },\n    subtitle: {\n      description: \"Secondary text (price, description, etc.)\",\n      type: \"string\",\n    },\n    image: {\n      description: \"Item image URL\",\n      type: \"string (URL)\",\n    },\n    color: {\n      description: \"Background color (hex) when no image is provided\",\n      type: \"string\",\n    },\n    actions: {\n      description: \"Action buttons for this item\",\n      type: \"Action[]\",\n    },\n  }}\n/>\n\n## Accessibility\n\n- Scroll animations respect `prefers-reduced-motion` and fall back to instant scrolling\n- Cards are keyboard-navigable with Enter/Space activation\n- Proper ARIA roles and labels for screen reader announcements\n- Focus rings visible for keyboard navigation\n\n## Related\n\n- [Data Table](/docs/data-table): both present collections, but DataTable is better for dense, sortable comparisons\n- [Image Gallery](/docs/image-gallery): for image-heavy collections where the visuals are the primary content\n"
  },
  {
    "path": "apps/www/app/docs/item-carousel/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Product List\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\n    \"Product List\",\n    \"Horizontal carousel for product comparison\",\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/item-carousel/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"Item Carousel\",\n  description: \"Display items in a horizontal scrollable carousel\",\n};\n\nexport const revalidate = 3600;\n\nexport default function ItemCarouselDocsPage() {\n  return <ComponentDocsTabs docs={<Content />} componentId=\"item-carousel\" />;\n}\n"
  },
  {
    "path": "apps/www/app/docs/layout.tsx",
    "content": "import type { ReactNode } from \"react\";\nimport type { Metadata } from \"next\";\nimport { NuqsAdapter } from \"nuqs/adapters/next/app\";\nimport ContentLayout from \"@/app/components/layout/page-shell\";\nimport { HeaderFrame } from \"@/app/components/layout/app-shell\";\nimport { ThemeToggle } from \"@/app/components/builder/theme-toggle\";\nimport { DocsNav } from \"./_components/docs-nav\";\nimport { DocsTocProvider } from \"./_components/docs-toc-context\";\nimport { DocsSearch } from \"./_components/docs-search.client\";\n\nexport const metadata: Metadata = {\n  title: {\n    template: \"%s | Tool UI\",\n    default: \"Docs | Tool UI\",\n  },\n  description: \"Documentation for Tool UI components\",\n};\n\nexport default function DocsLayout({ children }: { children: ReactNode }) {\n  return (\n    <NuqsAdapter>\n      <HeaderFrame\n        rightContent={\n          <div className=\"flex items-center gap-2\">\n            <DocsSearch />\n            <ThemeToggle />\n          </div>\n        }\n      >\n        <DocsTocProvider>\n          <ContentLayout sidebar={<DocsNav />}>{children}</ContentLayout>\n        </DocsTocProvider>\n      </HeaderFrame>\n    </NuqsAdapter>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/link-preview/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\nimport { LinkPreview } from \"@/components/tool-ui/link-preview\";\n\n<DocsHeader\n  title=\"Link Preview\"\n  description=\"Rich link previews with Open Graph data.\"\n  mdxPath=\"app/docs/link-preview/content.mdx\"\n/>\n\nEvery shared link is a black box until someone clicks it. The user has no idea if the URL leads to documentation, a blog post, or a 404. LinkPreview renders fetched URLs as clickable cards with the page title, description, image, and favicon, the same kind of rich embed you see in Slack or iMessage. Without it, the user sees a bare href or a blob of Open Graph JSON.\n\n**Role:** Information. For displaying data without user input. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"link-preview\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nRender `LinkPreview` in your UI with tool-compatible props.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\nimport { LinkPreview } from \"@/components/tool-ui/link-preview\";\n\nexport function Example() {\n  return (\n    <LinkPreview\n      id=\"link-preview-example\"\n      href=\"https://react.dev/reference/rsc/server-components\"\n      title=\"React Server Components\"\n      description=\"Server Components are a new type of Component that renders ahead of time.\"\n      image=\"https://images.unsplash.com/photo-1633356122544-f134324a6cee\"\n      domain=\"react.dev\"\n      ratio=\"16:9\"\n    />\n  );\n}\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegistration tells assistant-ui which component to render when a tool returns link metadata. Without it, tool results appear as raw JSON.\n\n```tsx\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { LinkPreview } from \"@/components/tool-ui/link-preview\";\nimport { safeParseSerializableLinkPreview } from \"@/components/tool-ui/link-preview/schema\";\n\nexport const toolkit: Toolkit = {\n  showLinkPreview: {\n    type: \"backend\",\n    render: ({ result }) => {\n      const parsed = safeParseSerializableLinkPreview(result);\n      if (!parsed) {\n        return null;\n      }\n      return <LinkPreview {...parsed} />;\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"Link\" title=\"Open Graph metadata\">\n    Title, description, and image pulled from Open Graph tags\n  </Feature>\n  <Feature icon=\"Globe\" title=\"Domain with favicon\">\n    Site favicon next to the domain name\n  </Feature>\n  <Feature icon=\"AspectRatio\" title=\"Flexible aspect ratios\">\n    Auto, 1:1, 4:3, 16:9, and 9:16 image crops\n  </Feature>\n  <Feature icon=\"MousePointerClick\" title=\"External actions\">\n    Compose action buttons below the preview card\n  </Feature>\n</FeatureGrid>\n\n## Props\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for this tool UI instance\",\n      type: \"string\",\n      required: true,\n    },\n    href: {\n      description: \"Target URL\",\n      type: \"string\",\n      required: true,\n    },\n    title: { description: \"Link title\", type: \"string\" },\n    description: { description: \"Link description\", type: \"string\" },\n    image: { description: \"Open Graph image URL\", type: \"string\" },\n    domain: { description: \"Source domain\", type: \"string\" },\n    favicon: { description: \"Favicon URL\", type: \"string\" },\n    ratio: {\n      description: \"Image aspect ratio\",\n      type: \"'auto' | '1:1' | '4:3' | '16:9' | '9:16'\",\n      default: \"'16:9'\",\n    },\n    fit: {\n      description: \"Image object fit mode\",\n      type: \"'cover' | 'contain'\",\n      default: \"'cover'\",\n    },\n    createdAt: { description: \"Creation timestamp (ISO 8601)\", type: \"string\" },\n  }}\n/>\n\nUse [`ToolUI.LocalActions`](/docs/actions) to compose external actions next to `LinkPreview`.\n\n## Related\n\n- [Citation](/docs/citation): both surface external references, but Citation is optimized for source attribution with snippets\n- [Image](/docs/image): for when the link target is primarily visual content\n"
  },
  {
    "path": "apps/www/app/docs/link-preview/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Link Preview\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\"Link Preview\", \"Rich link previews with OG data\");\n}\n"
  },
  {
    "path": "apps/www/app/docs/link-preview/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"Link Preview\",\n  description: \"Rich link previews with OG data\",\n};\n\nexport const revalidate = 3600;\n\nexport default function LinkPreviewDocsPage() {\n  return <ComponentDocsTabs docs={<Content />} componentId=\"link-preview\" />;\n}\n"
  },
  {
    "path": "apps/www/app/docs/linkedin-post/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\nimport { LinkedInPost } from \"@/components/tool-ui/linkedin-post\";\n\n<DocsHeader\n  title=\"LinkedIn Post\"\n  description=\"Platform-native post card for LinkedIn.\"\n  mdxPath=\"app/docs/linkedin-post/content.mdx\"\n/>\n\nAnnouncements, thought leadership, company updates: professional content benefits from AI-assisted drafting, but a plain text blob doesn't convey the final result. LinkedInPost renders a card that matches LinkedIn's layout: author headline, long-form text, link previews, and engagement stats.\n\n**Role:** Information. For displaying data the user reads. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"linkedin-post\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nRender `LinkedInPost` in your UI with tool-compatible props.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\nimport { LinkedInPost } from \"@/components/tool-ui/linkedin-post\";\n\nexport function Example() {\n  return (\n    <LinkedInPost\n      post={{\n        id: \"li-post-1\",\n        author: {\n          name: \"Jordan Lee\",\n          avatarUrl: \"https://example.com/avatar.jpg\",\n          headline: \"Engineering Lead | Platform\",\n        },\n        text: \"Excited to share that our team shipped a major product update this week.\",\n        stats: { likes: 234 },\n        createdAt: \"2025-01-05T09:00:00.000Z\",\n      }}\n    />\n  );\n}\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegister this renderer so tool results display as `LinkedInPost`.\n\n```tsx\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { LinkedInPost } from \"@/components/tool-ui/linkedin-post\";\nimport { safeParseSerializableLinkedInPost } from \"@/components/tool-ui/linkedin-post/schema\";\n\nexport const toolkit: Toolkit = {\n  showLinkedInPost: {\n    type: \"backend\",\n    render: ({ result }) => {\n      const parsed = safeParseSerializableLinkedInPost(result);\n      if (!parsed) {\n        return null;\n      }\n      return <LinkedInPost post={parsed} />;\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"Paintbrush\" title=\"Platform-native styling\">\n    Uses LinkedIn's layout, typography, and color scheme\n  </Feature>\n  <Feature icon=\"FileText\" title=\"Long-form post support\">\n    Handles professional long-form text with proper truncation and expansion\n  </Feature>\n  <Feature icon=\"MousePointerClick\" title=\"External actions\">\n    Add actions like \"Post\" or \"Edit\" via ToolUI action surfaces\n  </Feature>\n  <Feature icon=\"Link\" title=\"Link previews\">\n    Rich article previews with title, image, and domain\n  </Feature>\n</FeatureGrid>\n\n## Props\n\n<TypeTable\n  type={{\n    post: {\n      description: \"Post payload validated by `SerializableLinkedInPostSchema`\",\n      type: \"LinkedInPostData\",\n      required: true,\n    },\n    className: {\n      description: \"Optional className for the outer card\",\n      type: \"string\",\n    },\n    onAction: {\n      description: \"Fires when the user triggers a built-in post action\",\n      type: \"(action: string, post: LinkedInPostData) => void\",\n    },\n  }}\n/>\n\nUse [`ToolUI.LocalActions`](/docs/actions) to compose external actions next to `LinkedInPost`.\n\n## Related\n\n- [X Post](/docs/x-post): platform-native card for X (Twitter) content\n- [Instagram Post](/docs/instagram-post): platform-native card for Instagram content\n"
  },
  {
    "path": "apps/www/app/docs/linkedin-post/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - LinkedIn Post\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\"LinkedIn Post\", \"Render LinkedIn post previews\");\n}\n"
  },
  {
    "path": "apps/www/app/docs/linkedin-post/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"LinkedIn Post\",\n  description: \"Render LinkedIn post previews\",\n};\n\nexport const revalidate = 3600;\n\nexport default function LinkedInPostDocsPage() {\n  return <ComponentDocsTabs docs={<Content />} componentId=\"linkedin-post\" />;\n}\n"
  },
  {
    "path": "apps/www/app/docs/message-draft/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\nimport { MessageDraft } from \"@/components/tool-ui/message-draft\";\n\n<DocsHeader\n  title=\"Message Draft\"\n  description=\"Review messages before sending.\"\n  mdxPath=\"app/docs/message-draft/content.mdx\"\n/>\n\nAn AI sending a message on your behalf without asking is a trust violation. MessageDraft shows the full email or Slack message for review, gives you a Send button with an undo grace period, and collapses into a compact receipt once sent or cancelled. The agent never sends silently.\n\n**Role:** Decision. For choices that return to the assistant. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"message-draft\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nRender `MessageDraft` in your UI with tool-compatible props.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\nimport { MessageDraft } from \"@/components/tool-ui/message-draft\";\n\nexport function Example() {\n  return (\n    <MessageDraft\n      id=\"message-draft-example\"\n      channel=\"email\"\n      subject=\"Q4 Planning Follow-up\"\n      to={[\"sarah.chen@company.com\"]}\n      body={`Hi Sarah,\n\nThanks for joining the planning meeting today. I've attached the updated timeline.\n\nBest,\nAlex`}\n      onSend={() => console.log(\"Sent\")}\n      onCancel={() => console.log(\"Cancelled\")}\n    />\n  );\n}\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegister this renderer so tool results display as `MessageDraft`.\n\n```tsx\n\"use client\";\n\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { MessageDraft } from \"@/components/tool-ui/message-draft\";\nimport {\n  safeParseSerializableMessageDraft,\n  SerializableMessageDraftSchema,\n} from \"@/components/tool-ui/message-draft/schema\";\n\nexport const toolkit: Toolkit = {\n  draftMessage: {\n    description: \"Draft a message for the user to review before sending.\",\n    parameters: SerializableMessageDraftSchema,\n    render: ({ args, toolCallId, result, addResult }) => {\n      const parsedArgs = safeParseSerializableMessageDraft({\n        ...args,\n        id: args?.id ?? `message-draft-${toolCallId}`,\n      });\n      if (!parsedArgs) {\n        return null;\n      }\n      return result ? (\n        <MessageDraft {...parsedArgs} outcome={result} />\n      ) : (\n        <MessageDraft\n          {...parsedArgs}\n          onSend={async () => {\n            // Perform actual send logic here\n            await sendMessage(parsedArgs);\n            await addResult?.(\"sent\");\n          }}\n          onCancel={() => addResult?.(\"cancelled\")}\n        />\n      );\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"ShieldCheck\" title=\"Human-in-the-loop\">\n    Nothing sends until the user clicks Send\n  </Feature>\n  <Feature icon=\"Timer\" title=\"Undo grace period\">\n    Configurable countdown after Send where the user can still cancel\n  </Feature>\n  <Feature icon=\"Layers\" title=\"Channel skins\">\n    Email shows subject/recipients, Slack shows channel or DM target\n  </Feature>\n  <Feature icon=\"CheckCircle2\" title=\"Receipt states\">\n    Collapses to a one-line confirmation after send or cancel\n  </Feature>\n</FeatureGrid>\n\n## Channel Skins\n\nTwo channel types, each with its own metadata layout.\n\n### Email\n\nShows subject line, To/CC/BCC recipients, and message body.\n\n### Slack\n\nShows channel or DM target with the Slack icon.\n\n## Send Flow\n\nClicking Send starts a countdown. The user can undo before it completes:\n\n1. **Review** - User reviews the draft with Send/Cancel buttons\n2. **Sending** - Countdown with Undo button (default 5 seconds)\n3. **Sent** - Compact receipt confirming the message was sent\n\nConfigure the countdown length with `undoGracePeriod` (in milliseconds).\n\n## Receipt States\n\nPass `outcome=\"sent\"` to render a one-line confirmation receipt. `outcome=\"cancelled\"` hides the component entirely.\n\n## Props\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for the message draft\",\n      type: \"string\",\n      required: true,\n    },\n    channel: {\n      description: \"Message channel type\",\n      type: \"'email' | 'slack'\",\n      required: true,\n    },\n    body: {\n      description: \"Message content\",\n      type: \"string\",\n      required: true,\n    },\n    outcome: {\n      description: \"Final state (renders receipt when set)\",\n      type: \"'sent' | 'cancelled'\",\n    },\n    undoGracePeriod: {\n      description: \"Milliseconds before send is finalized\",\n      type: \"number\",\n      default: \"5000\",\n    },\n    onSend: {\n      description: \"Called when grace period completes\",\n      type: \"() => void | Promise<void>\",\n    },\n    onUndo: {\n      description: \"Called when user clicks Undo during grace period\",\n      type: \"() => void\",\n    },\n    onCancel: {\n      description: \"Called when user cancels the draft\",\n      type: \"() => void\",\n    },\n    className: {\n      description: \"Additional CSS classes\",\n      type: \"string\",\n    },\n  }}\n/>\n\n### Email-specific Props\n\n<TypeTable\n  type={{\n    subject: {\n      description: \"Email subject line\",\n      type: \"string\",\n      required: true,\n    },\n    from: {\n      description: \"Sender address\",\n      type: \"string\",\n    },\n    to: {\n      description: \"Primary recipients\",\n      type: \"string[]\",\n      required: true,\n    },\n    cc: {\n      description: \"Carbon copy recipients\",\n      type: \"string[]\",\n    },\n    bcc: {\n      description: \"Blind carbon copy recipients\",\n      type: \"string[]\",\n    },\n  }}\n/>\n\n### Slack-specific Props\n\n<TypeTable\n  type={{\n    target: {\n      description: \"Slack destination\",\n      type: \"{ type: 'channel' | 'dm', name: string, memberCount?: number }\",\n      required: true,\n    },\n  }}\n/>\n\n## Accessibility\n\n- Uses `article` with `aria-labelledby` for the draft content\n- **Keyboard navigation:**\n  - `Tab` to move between buttons\n  - `Enter` or `Space` to activate focused button\n  - `Escape` triggers cancel while in review state\n- Sending state announces countdown via `aria-live=\"polite\"`\n- Receipt state uses `role=\"status\"` for screen reader announcements\n- Focus moves to Undo button when entering sending state\n\n## Related\n\n- [Approval Card](/docs/approval-card): binary approve/deny for other consequential actions\n"
  },
  {
    "path": "apps/www/app/docs/message-draft/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Message Draft\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\n    \"Message Draft\",\n    \"Review and approve messages before sending\",\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/message-draft/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"Message Draft\",\n  description: \"Review and approve messages before an AI agent sends them\",\n};\n\nexport const revalidate = 3600;\n\nexport default function MessageDraftDocsPage() {\n  return <ComponentDocsTabs docs={<Content />} componentId=\"message-draft\" />;\n}\n"
  },
  {
    "path": "apps/www/app/docs/option-list/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\n\n<DocsHeader\n  title=\"Option List\"\n  description=\"Let users select from multiple choices.\"\n  mdxPath=\"app/docs/option-list/content.mdx\"\n/>\n\nNot every choice fits in a text reply. When the assistant needs the user to pick an output format, a plan tier, or a movie genre, OptionList renders the options as selectable cards with single or multi-select behavior. The selection feeds back as a tool result, and the chosen option stays visible as a receipt. No free-text parsing required.\n\n**Role:** Decision. For choices that return to the assistant. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"option-list\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nRender `OptionList` in your UI with tool-compatible props.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\nimport { OptionList } from \"@/components/tool-ui/option-list\";\n\nexport function Example({\n  payload,\n}: {\n  payload: React.ComponentProps<typeof OptionList>;\n}) {\n  return <OptionList {...payload} />;\n}\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegister this renderer so tool results display as `OptionList`.\n\n```tsx\n\"use client\";\n\n// Frontend tool (recommended for decision surfaces like Option List):\n// - The model calls the tool with SerializableOptionList props as the args\n// - Your UI calls addResult(selection) from onAction when actionId is \"confirm\"\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { OptionList } from \"@/components/tool-ui/option-list\";\nimport {\n  safeParseSerializableOptionList,\n  SerializableOptionListSchema,\n} from \"@/components/tool-ui/option-list/schema\";\n\nexport const toolkit: Toolkit = {\n  selectFormat: {\n    description: \"Let the user choose an output format.\",\n    parameters: SerializableOptionListSchema,\n    render: ({ args, toolCallId, result, addResult }) => {\n      const parsedArgs = safeParseSerializableOptionList({\n        ...args,\n        id: args?.id ?? `format-selection-${toolCallId}`,\n      });\n      if (!parsedArgs) {\n        return null;\n      }\n      return result ? (\n        <OptionList {...parsedArgs} value={undefined} choice={result} />\n      ) : (\n        <OptionList\n          {...parsedArgs}\n          // Avoid controlled selection in LLM-driven payloads.\n          value={undefined}\n          onAction={(actionId, selection) => {\n            if (actionId === \"confirm\") {\n              void addResult?.(selection);\n            }\n          }}\n        />\n      );\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"CircleCheck\" title=\"Single or multi-select\">\n    Radio buttons for one choice, checkboxes for many\n  </Feature>\n  <Feature icon=\"ListChecks\" title=\"Selection constraints\">\n    Set minimum and maximum selection counts\n  </Feature>\n  <Feature icon=\"Settings2\" title=\"Configurable actions\">\n    Confirm, cancel, and custom footer buttons\n  </Feature>\n  <Feature icon=\"CheckCircle2\" title=\"Receipt state\">\n    Shows only the chosen option(s) as a read-only record\n  </Feature>\n</FeatureGrid>\n\n## Receipt State\n\nPass a `choice` prop to render this component in its receipt state. See [Receipts](/docs/receipts) for the pattern.\n\nThe receipt shows only the selected option(s) with a checkmark. Unselected options and action buttons are hidden.\n\n## Props\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for this component instance\",\n      type: \"string\",\n      required: true,\n    },\n    options: {\n      description: \"List of selectable options\",\n      type: \"OptionListOption[]\",\n      required: true,\n    },\n    selectionMode: {\n      description: \"Selection behavior\",\n      type: \"'single' | 'multi'\",\n      default: \"'multi'\",\n    },\n    value: {\n      description:\n        \"Controlled selection value (runtime only). Avoid in tool/LLM payloads; prefer defaultValue or choice.\",\n      type: \"string | string[] | null\",\n    },\n    defaultValue: {\n      description: \"Initial selection (uncontrolled)\",\n      type: \"string | string[] | null\",\n    },\n    choice: {\n      description: \"Chosen selection (renders receipt state)\",\n      type: \"string | string[] | null\",\n    },\n    minSelections: {\n      description: \"Minimum required selections\",\n      type: \"number\",\n      default: \"1\",\n    },\n    maxSelections: {\n      description: \"Maximum allowed selections\",\n      type: \"number\",\n    },\n    actions: {\n      description: \"Action buttons\",\n      type: \"Action[] | ActionsConfig\",\n    },\n    onChange: {\n      description: \"Selection change handler\",\n      type: \"(value) => void\",\n    },\n    onAction: {\n      description: \"Action handler with post-action selection state\",\n      type: \"(actionId: string, value: string | string[] | null) => void | Promise<void>\",\n    },\n    onBeforeAction: {\n      description: \"Action guard with current selection state\",\n      type: \"(actionId: string, value: string | string[] | null) => boolean | Promise<boolean>\",\n    },\n  }}\n/>\n\n## Option Schema\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique option identifier\",\n      type: \"string\",\n      required: true,\n    },\n    label: { description: \"Display text\", type: \"string\", required: true },\n    description: { description: \"Secondary text\", type: \"string\" },\n    icon: { description: \"Optional icon element\", type: \"ReactNode\" },\n    disabled: { description: \"Disable this option\", type: \"boolean\" },\n  }}\n/>\n\n## Accessibility\n\n- Uses a semantic listbox + option pattern (`role=\"listbox\"` with `role=\"option\"` on each option button)\n- Exposes selection state with `aria-selected` and multi-select intent with `aria-multiselectable`\n- Implements roving tab focus with keyboard navigation (`ArrowUp/ArrowDown`, `Home/End`, `Enter`/`Space`, `Escape`)\n- Keeps footer actions keyboard-accessible through native button semantics\n\n## Related\n\n- [Approval Card](/docs/approval-card): binary yes/no for a single consequential action\n- [Preferences Panel](/docs/preferences-panel): structured settings with switches and toggles\n- [Question Flow](/docs/question-flow): multi-step guided decisions where each answer shapes the next\n"
  },
  {
    "path": "apps/www/app/docs/option-list/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Option List\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\n    \"Option List\",\n    \"Let users select from multiple choices\",\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/option-list/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"Option List\",\n  description: \"Let users select from multiple choices\",\n};\n\nexport const revalidate = 3600;\n\nexport default function OptionListDocsPage() {\n  return <ComponentDocsTabs docs={<Content />} componentId=\"option-list\" />;\n}\n"
  },
  {
    "path": "apps/www/app/docs/order-summary/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\nimport { OrderSummary } from \"@/components/tool-ui/order-summary\";\n\n<DocsHeader\n  title=\"Order Summary\"\n  description=\"Display purchases with itemized pricing.\"\n  mdxPath=\"app/docs/order-summary/content.mdx\"\n/>\n\nNobody clicks \"Buy\" without seeing what they're buying. OrderSummary renders line items with images, quantities, and an itemized price breakdown, everything the user needs to review before committing. Use `OrderSummary.Display` during confirmation and `OrderSummary.Receipt` after the purchase is confirmed.\n\n**Role:** Decision. For choices that return to the assistant. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"order-summary\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nRender `OrderSummary` in your UI with tool-compatible props.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\nimport { OrderSummary } from \"@/components/tool-ui/order-summary\";\n\nexport function Example() {\n  return (\n    <OrderSummary.Display\n      id=\"order-summary-example\"\n      items={[\n        {\n          id: \"item-1\",\n          name: \"Wireless Earbuds Pro\",\n          description: \"Active noise cancellation\",\n          imageUrl: \"https://example.com/earbuds.jpg\",\n          quantity: 1,\n          unitPrice: 149.99,\n        },\n        {\n          id: \"item-2\",\n          name: \"USB-C Charging Cable\",\n          description: \"2m braided\",\n          quantity: 2,\n          unitPrice: 19.99,\n        },\n      ]}\n      pricing={{\n        subtotal: 189.97,\n        tax: 16.62,\n        shipping: 0,\n        total: 206.59,\n        currency: \"USD\",\n      }}\n    />\n  );\n}\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegister this renderer so tool results display as `OrderSummary`.\n\n```tsx\n\"use client\";\n\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { OrderSummary } from \"@/components/tool-ui/order-summary\";\nimport {\n  safeParseSerializableOrderSummary,\n  SerializableOrderSummarySchema,\n} from \"@/components/tool-ui/order-summary/schema\";\nimport { ToolUI, createDecisionResult } from \"@/components/tool-ui/shared\";\n\nexport const toolkit: Toolkit = {\n  createOrder: {\n    description: \"Display a purchase order for user confirmation\",\n    parameters: SerializableOrderSummarySchema,\n    render: ({ args, toolCallId, result, addResult }) => {\n      const parsedArgs = safeParseSerializableOrderSummary({\n        ...args,\n        id: args?.id ?? `order-${toolCallId}`,\n      });\n      if (!parsedArgs) {\n        return null;\n      }\n      if (result) {\n        return <OrderSummary.Receipt {...parsedArgs} choice={result} />;\n      }\n      return (\n        <ToolUI id={parsedArgs.id}>\n          <ToolUI.Surface>\n            <OrderSummary.Display {...parsedArgs} />\n          </ToolUI.Surface>\n          <ToolUI.Actions>\n            <ToolUI.DecisionActions\n              actions={[\n                { id: \"cancel\", label: \"Cancel\", variant: \"outline\" },\n                { id: \"confirm\", label: \"Purchase\", variant: \"default\" },\n              ]}\n              onAction={(action) =>\n                createDecisionResult({\n                  decisionId: `${parsedArgs.id}-decision`,\n                  action,\n                  payload:\n                    action.id === \"confirm\"\n                      ? {\n                          orderId: `ORD-${Date.now()}`,\n                          confirmedAt: new Date().toISOString(),\n                        }\n                      : undefined,\n                })\n              }\n              onCommit={async (decision) => {\n                if (decision.actionId !== \"confirm\") return;\n                const orderId =\n                  typeof decision.payload?.orderId === \"string\"\n                    ? decision.payload.orderId\n                    : `ORD-${Date.now()}`;\n                const confirmedAt =\n                  typeof decision.payload?.confirmedAt === \"string\"\n                    ? decision.payload.confirmedAt\n                    : new Date().toISOString();\n                await addResult?.({\n                  action: \"confirm\",\n                  orderId,\n                  confirmedAt,\n                });\n              }}\n            />\n          </ToolUI.Actions>\n        </ToolUI>\n      );\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"DollarSign\" title=\"Currency formatting\">\n    Locale-aware prices via `Intl.NumberFormat` for any ISO currency code\n  </Feature>\n  <Feature icon=\"List\" title=\"Pricing breakdown\">\n    Subtotal, tax, shipping, discounts, and total in one place\n  </Feature>\n  <Feature icon=\"CheckCircle2\" title=\"Receipt state\">\n    Dims the card and shows order ID after purchase confirmation\n  </Feature>\n  <Feature icon=\"Package\" title=\"Image fallbacks\">\n    Package icon placeholder when a product image is missing\n  </Feature>\n</FeatureGrid>\n\n## Receipt State\n\nPass a `choice` prop to render this component in its receipt state. See [Receipts](/docs/receipts) for the pattern.\n\nRender `OrderSummary.Receipt` with a `choice` payload to show the confirmed order metadata.\n\nWhen parsing tool payloads with `SerializableOrderSummarySchema`, keep variant semantics aligned:\n\n- `variant: \"summary\"` must not include `choice`\n- `variant: \"receipt\"` must include `choice`\n\n## With Discount\n\nAdd `discount` and `discountLabel` to `pricing` to show promotional savings.\n\n## Shared Props\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for the order summary\",\n      type: \"string\",\n      required: true,\n    },\n    title: {\n      description: \"Header text displayed at the top\",\n      type: \"string\",\n      default: '\"Order Summary\"',\n    },\n    items: {\n      description: \"Array of items in the order\",\n      type: \"OrderItem[]\",\n      required: true,\n    },\n    pricing: {\n      description: \"Pricing breakdown for the order\",\n      type: \"Pricing\",\n      required: true,\n    },\n    className: {\n      description: \"Additional CSS classes\",\n      type: \"string\",\n    },\n  }}\n/>\n\n## Receipt Props\n\n<TypeTable\n  type={{\n    choice: {\n      description: \"Confirmed decision metadata rendered in receipt mode\",\n      type: \"OrderDecision\",\n      required: true,\n    },\n  }}\n/>\n\nUse [`ToolUI.DecisionActions`](/docs/actions) to compose consequential actions next to `OrderSummary`.\n\n## OrderItem Schema\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for the item\",\n      type: \"string\",\n      required: true,\n    },\n    name: {\n      description: \"Product or service name\",\n      type: \"string\",\n      required: true,\n    },\n    description: {\n      description: \"Additional details (size, color, options)\",\n      type: \"string\",\n    },\n    imageUrl: {\n      description: \"URL to product image (shows placeholder if missing)\",\n      type: \"string\",\n    },\n    quantity: {\n      description: \"Number of units (hidden when 1)\",\n      type: \"number\",\n      default: \"1\",\n    },\n    unitPrice: {\n      description: \"Price per unit in decimal (e.g., 29.99)\",\n      type: \"number\",\n      required: true,\n    },\n  }}\n/>\n\n## Pricing Schema\n\n<TypeTable\n  type={{\n    subtotal: {\n      description: \"Sum of all item prices before tax/shipping\",\n      type: \"number\",\n      required: true,\n    },\n    tax: {\n      description: \"Tax amount\",\n      type: \"number\",\n    },\n    taxLabel: {\n      description: 'Custom tax label (e.g., \"VAT (19%)\")',\n      type: \"string\",\n      default: '\"Tax\"',\n    },\n    shipping: {\n      description: 'Shipping cost (shows \"Free\" when 0)',\n      type: \"number\",\n    },\n    discount: {\n      description: \"Discount amount (displayed as negative)\",\n      type: \"number\",\n    },\n    discountLabel: {\n      description: 'Discount label (e.g., \"SAVE10 (10% off)\")',\n      type: \"string\",\n      default: '\"Discount\"',\n    },\n    total: {\n      description: \"Final amount due\",\n      type: \"number\",\n      required: true,\n    },\n    currency: {\n      description: \"ISO 4217 currency code\",\n      type: \"string\",\n      default: '\"USD\"',\n    },\n  }}\n/>\n\n## Accessibility\n\n- Uses semantic `<article>` with `aria-labelledby` pointing to the title\n- Action buttons are keyboard accessible with visible focus states\n- Images use `alt=\"\"` since item names provide the description\n- Receipt state shows clear visual indication of confirmation\n\n## Related\n\n- [Approval Card](/docs/approval-card): binary confirmation for non-purchase actions\n- [Option List](/docs/option-list): selecting from choices without a pricing breakdown\n"
  },
  {
    "path": "apps/www/app/docs/order-summary/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Order Summary\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\n    \"Order Summary\",\n    \"Agent-suggested purchases with itemized pricing\",\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/order-summary/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"Order Summary\",\n  description:\n    \"Display agent-suggested purchases with itemized pricing for user confirmation\",\n};\n\nexport const revalidate = 3600;\n\nexport default function OrderSummaryDocsPage() {\n  return <ComponentDocsTabs docs={<Content />} componentId=\"order-summary\" />;\n}\n"
  },
  {
    "path": "apps/www/app/docs/overview/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\nimport { MockThread, MockMessage } from \"../_components/mock-thread\";\nimport { LinkPreview } from \"@/components/tool-ui/link-preview\";\nimport { InteractiveOptionDemo } from \"../_components/interactive-option-demo\";\n\n<DocsHeader\n  title=\"Overview\"\n  description=\"UI components for tool call results.\"\n  mdxPath=\"app/docs/overview/content.mdx\"\n/>\n\nWhen an AI assistant calls a tool, the result is often plain text or raw JSON. Without purpose-built components, users see a wall of unformatted data they have to parse themselves. The experience breaks because nothing presents the data well.\n\nTool UI is a component library built for this. Each component turns a specific kind of tool output into real UI: a card, a table, an option list, a chart. They render inline in the conversation and users can interact without leaving the chat.\n\n<div className=\"mt-8 grid min-w-0 gap-6 md:grid-cols-2\">\n  <MockThread caption=\"Without Tool UI\">\n    <MockMessage role=\"user\">Find me a link to the Tailwind docs</MockMessage>\n    <MockMessage role=\"assistant\">\n      <span className=\"text-foreground text-sm\">\n        {\"Here's the link to the Tailwind CSS documentation:\"}\n      </span>\n      <pre className=\"bg-muted text-muted-foreground mt-2 overflow-x-auto rounded-lg p-3 font-mono text-xs\">\n{`{\n  \"href\": \"https://tailwindcss.com/docs\",\n  \"title\": \"Tailwind CSS\",\n  \"description\": \"Rapidly build modern websites without ever leaving your HTML.\"\n}`}\n      </pre>\n    </MockMessage>\n  </MockThread>\n\n  <MockThread caption=\"With Tool UI\">\n    <MockMessage role=\"user\">Find me a link to the Tailwind docs</MockMessage>\n    <MockMessage role=\"assistant\">\n      <span className=\"text-foreground text-sm\">\n        {\"Found it.\"}\n      </span>\n      <div className=\"mt-3\">\n        <LinkPreview\n          id=\"lp-tailwind\"\n          href=\"https://tailwindcss.com/docs\"\n          title=\"Tailwind CSS\"\n          description=\"Rapidly build modern websites without ever leaving your HTML.\"\n          domain=\"tailwindcss.com\"\n          favicon=\"https://tailwindcss.com/favicons/favicon-32x32.png\"\n        />\n      </div>\n    </MockMessage>\n  </MockThread>\n</div>\n\nSame data, different experience.\n\n## What if tool results could render UI?\n\nWhen a tool returns JSON that matches a known **schema**, your app can render a component instead of showing raw data.\n\nEvery Tool UI component has a corresponding schema: a Zod definition that describes the shape of the data it needs. When the tool result matches, the component renders. When it doesn't, parsing fails safely and nothing renders.\n\n```ts title=\"Tool output → Component\"\n// The tool returns structured JSON...\n{\n  id: \"lp-1\",\n  href: \"https://tailwindcss.com/docs\",\n  title: \"Tailwind CSS\",\n  description: \"Rapidly build modern websites...\"\n}\n\n// ...that matches the LinkPreview schema → renders as a card\n```\n\n**Schema-first rendering** means the data contract between server and client is typed and validated. No brittle string parsing. No hoping the model formats things correctly.\n\n## What if that UI could be interactive?\n\nSome tool UIs just display information like a link preview, a chart, or a code block. But others let users **make decisions** that feed back into the conversation.\n\n<div className=\"pb-4\">\n  <InteractiveOptionDemo />\n</div>\n\nThe user selects an option, and the choice returns to the assistant as a **tool result**. The assistant can then continue the conversation with that context. The selected state stays visible as a **receipt** — a record of what was chosen.\n\nThe [actions](/docs/actions) model covers user interactions on tool UIs that produce side effects and records.\n\n## Where Tool UI fits\n\nTool UI sits between your design system and your LLM orchestration layer:\n\n[Radix](https://www.radix-ui.com/)/[shadcn](https://ui.shadcn.com/) (design primitives) → **Tool UI** (conversation-native components) → [AI SDK](https://ai-sdk.dev/) / [LangGraph](https://langchain-ai.github.io/langgraphjs/) / etc. (LLM orchestration)\n\n- **Radix / shadcn** give you the base UI primitives (buttons, dialogs, inputs).\n- **Tool UI** gives you components designed for chat (inline cards, tables, option lists, approval flows) with schemas that map to tool outputs.\n- **AI SDK / LangGraph** handle the model communication, streaming, and tool execution.\n\nTool UI doesn't replace your design system. It extends it. Components use shadcn primitives internally and follow your theme.\n\n## How it works\n\n<Mermaid chart={`\nflowchart LR\n    A[\"Assistant calls tool\"] --> B[\"Tool returns JSON\"]\n    B --> C[\"Schema match → Component renders\"]\n    C --> D[\"User interacts\"]\n    D --> E[\"Result returns\"]\n\n    style A fill:#8b5cf6,stroke:#7c3aed,stroke-width:2px,color:#fff\n    style B fill:#10b981,stroke:#059669,stroke-width:2px,color:#fff\n    style C fill:#3b82f6,stroke:#2563eb,stroke-width:2px,color:#fff\n    style D fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff\n    style E fill:#ef4444,stroke:#dc2626,stroke-width:2px,color:#fff\n\n`} />\n\n### Minimal example\n\n**Server:** define a tool that returns structured data.\n\n```ts title=\"app/api/chat/route.ts\"\nimport { streamText, tool, convertToModelMessages } from \"ai\";\nimport { openai } from \"@ai-sdk/openai\";\nimport { z } from \"zod\";\nimport { SerializableLinkPreviewSchema } from \"@/components/tool-ui/link-preview/schema\";\n\nexport async function POST(req: Request) {\n  const { messages } = await req.json();\n\n  const result = streamText({\n    model: openai(\"gpt-4o\"),\n    messages: await convertToModelMessages(messages),\n    tools: {\n      previewLink: tool({\n        description: \"Show a preview card for a URL\",\n        inputSchema: z.object({ url: z.url() }),\n        // outputSchema tells the AI SDK what shape the result will have\n        outputSchema: SerializableLinkPreviewSchema,\n        async execute({ url }) {\n          // Fetch metadata and return structured data\n          return {\n            id: \"link-preview-1\",\n            href: url,\n            title: \"Example Site\",\n            description: \"A description of the linked content\",\n            image: \"https://example.com/image.jpg\",\n          };\n        },\n      }),\n    },\n  });\n\n  return result.toUIMessageStreamResponse();\n}\n```\n\n**Client:** register the component renderer.\n\n```tsx title=\"app/page.tsx\"\n\"use client\";\nimport {\n  AssistantRuntimeProvider,\n  Tools,\n  useAui,\n  type Toolkit,\n} from \"@assistant-ui/react\";\nimport {\n  useChatRuntime,\n  AssistantChatTransport,\n} from \"@assistant-ui/react-ai-sdk\";\nimport { LinkPreview } from \"@/components/tool-ui/link-preview\";\nimport { safeParseSerializableLinkPreview } from \"@/components/tool-ui/link-preview/schema\";\n\n// Register a renderer for each tool that should display a component\nconst toolkit: Toolkit = {\n  previewLink: {\n    type: \"backend\",\n    render: ({ result }) => {\n      const parsed = safeParseSerializableLinkPreview(result);\n      if (!parsed) return null; // Wait for full payload before rendering\n      return <LinkPreview {...parsed} />;\n    },\n  },\n};\n\nexport default function App() {\n  // Connect to your API route\n  const runtime = useChatRuntime({\n    transport: new AssistantChatTransport({ api: \"/api/chat\" }),\n  });\n\n  // Make tool renderers available to the runtime\n  const aui = useAui({ tools: Tools({ toolkit }) });\n\n  return (\n    <AssistantRuntimeProvider runtime={runtime} aui={aui}>\n      {/* Your chat thread component */}\n    </AssistantRuntimeProvider>\n  );\n}\n```\n\n`toolkit` maps tool names to renderers. Each renderer parses the tool result with `safeParse` and renders when valid. `useAui` and `Tools` connect everything to the assistant-ui runtime.\n\n## Next steps\n\n- **[Quick Start](/docs/quick-start):** Add your first Tool UI component to a chat app\n- **[Design Guidelines](/docs/design-guidelines):** The collaboration model, component roles, and constraints for building in chat\n- **[Actions](/docs/actions):** How interactive tool UIs feed user decisions back to the assistant\n- **[Receipts](/docs/receipts):** How components show permanent records of past decisions\n- **[Gallery](/docs/gallery):** Browse all available components\n"
  },
  {
    "path": "apps/www/app/docs/overview/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Overview\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\"Overview\", \"Introduction to Tool UI components\");\n}\n"
  },
  {
    "path": "apps/www/app/docs/overview/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { DocsArticle } from \"../_components/docs-article\";\n\nexport const metadata: Metadata = {\n  title: \"Overview\",\n  description: \"Introduction to Tool UI component library\",\n};\n\nexport const revalidate = 3600;\n\nexport default function OverviewPage() {\n  return (\n    <DocsArticle>\n      <Content />\n    </DocsArticle>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/page.tsx",
    "content": "import { redirect } from \"next/navigation\";\n\nexport default function DocsIndexPage() {\n  redirect(\"/docs/gallery\");\n}\n"
  },
  {
    "path": "apps/www/app/docs/parameter-slider/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\nimport { ParameterSlider } from \"@/components/tool-ui/parameter-slider\";\n\n<DocsHeader\n  title=\"Parameter Slider\"\n  description=\"Numeric parameter adjustment controls.\"\n  mdxPath=\"app/docs/parameter-slider/content.mdx\"\n/>\n\nText input is a poor fit for numeric tuning. When the user needs to dial in image exposure, audio levels, or model temperature, ParameterSlider renders grouped sliders with labels, units, and Reset/Apply actions. The confirmed values feed back to the assistant as a tool result.\n\n**Role:** Control. Adjusts parameters without commitment. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"parameter-slider\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nRender `ParameterSlider` in your UI with tool-compatible props.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\nimport { ParameterSlider } from \"@/components/tool-ui/parameter-slider\";\n\nexport function Example() {\n  return (\n    <ParameterSlider\n      id=\"parameter-slider-example\"\n      sliders={[\n        {\n          id: \"exposure\",\n          label: \"Exposure\",\n          min: -3,\n          max: 3,\n          step: 0.1,\n          value: 0.3,\n          unit: \"EV\",\n          precision: 1,\n        },\n        {\n          id: \"contrast\",\n          label: \"Contrast\",\n          min: -100,\n          max: 100,\n          step: 5,\n          value: 15,\n          unit: \"%\",\n        },\n        {\n          id: \"highlights\",\n          label: \"Highlights\",\n          min: -100,\n          max: 100,\n          step: 5,\n          value: -20,\n          unit: \"%\",\n        },\n      ]}\n      onAction={(actionId, values) => {\n        console.log(actionId, values);\n      }}\n    />\n  );\n}\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegister this renderer so tool results display as `ParameterSlider`.\n\n```tsx\n\"use client\";\n\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { ParameterSlider } from \"@/components/tool-ui/parameter-slider\";\nimport {\n  safeParseSerializableParameterSlider,\n  SerializableParameterSliderSchema,\n} from \"@/components/tool-ui/parameter-slider/schema\";\n\nexport const toolkit: Toolkit = {\n  adjust_parameters: {\n    description: \"Adjust numeric parameters with sliders\",\n    parameters: SerializableParameterSliderSchema,\n    render: ({ args, toolCallId, addResult }) => {\n      const parsedArgs = safeParseSerializableParameterSlider({\n        ...args,\n        id: args?.id ?? `parameter-slider-${toolCallId}`,\n      });\n      if (!parsedArgs) {\n        return null;\n      }\n      return (\n        <ParameterSlider\n          {...parsedArgs}\n          onAction={(actionId, values) => {\n            if (actionId === \"apply\") {\n              void addResult?.({ values });\n            }\n          }}\n        />\n      );\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"ListChecks\" title=\"Multiple parameters\">\n    Group related sliders under shared Reset/Apply actions\n  </Feature>\n  <Feature icon=\"SlidersHorizontal\" title=\"Inline labels and values\">\n    Label and current value sit directly on the slider track\n  </Feature>\n  <Feature icon=\"Settings2\" title=\"Custom units\">\n    Show domain-specific units: EV, dB, %, Mbps, or custom strings\n  </Feature>\n  <Feature icon=\"Accessibility\" title=\"Built-in accessibility\">\n    Arrow keys adjust values; screen readers announce the unit\n  </Feature>\n</FeatureGrid>\n\n## Props\n\n### ParameterSlider\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for this tool UI instance\",\n      type: \"string\",\n      required: true,\n    },\n    sliders: {\n      description: \"Array of slider configurations\",\n      type: \"SliderConfig[]\",\n      required: true,\n    },\n    values: {\n      description: \"Controlled values array (for controlled mode)\",\n      type: \"SliderValue[]\",\n    },\n    onChange: {\n      description: \"Called when any slider value changes\",\n      type: \"(values: SliderValue[]) => void\",\n    },\n    actions: {\n      description: \"Action buttons (defaults to Reset/Apply)\",\n      type: \"Action[] | ActionsConfig\",\n    },\n    onAction: {\n      description:\n        \"Called when an action button is clicked with post-action values\",\n      type: \"(actionId: string, values: SliderValue[]) => void | Promise<void>\",\n    },\n    onBeforeAction: {\n      description:\n        \"Called before action with current values, return false to cancel\",\n      type: \"(actionId: string, values: SliderValue[]) => boolean | Promise<boolean>\",\n    },\n    className: {\n      description: \"Additional CSS classes\",\n      type: \"string\",\n    },\n  }}\n/>\n\n### SliderConfig\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for this slider\",\n      type: \"string\",\n      required: true,\n    },\n    label: {\n      description: \"Display label for the slider\",\n      type: \"string\",\n      required: true,\n    },\n    min: {\n      description: \"Minimum value\",\n      type: \"number\",\n      required: true,\n    },\n    max: {\n      description: \"Maximum value\",\n      type: \"number\",\n      required: true,\n    },\n    value: {\n      description: \"Initial value\",\n      type: \"number\",\n      required: true,\n    },\n    step: {\n      description: \"Step increment (default: 1)\",\n      type: \"number\",\n    },\n    unit: {\n      description: \"Unit suffix shown after value (e.g., 'EV', '%', 'dB')\",\n      type: \"string\",\n    },\n    precision: {\n      description: \"Decimal places to display (default: auto)\",\n      type: \"number\",\n    },\n    disabled: {\n      description: \"Disable interaction for this slider\",\n      type: \"boolean\",\n    },\n  }}\n/>\n\n### SliderValue\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Slider identifier\",\n      type: \"string\",\n      required: true,\n    },\n    value: {\n      description: \"Current slider value\",\n      type: \"number\",\n      required: true,\n    },\n  }}\n/>\n\n## Accessibility\n\n- Full keyboard support: Tab to focus, Arrow keys to adjust values\n- `aria-valuetext` includes unit for screen readers (e.g., \"plus 0.3 EV\")\n- Tick marks provide visual reference for the range\n- Disabled state reduces opacity and disables pointer interactions\n\n## Related\n\n- [Preferences Panel](/docs/preferences-panel): structured settings with switches, toggles, and selects\n"
  },
  {
    "path": "apps/www/app/docs/parameter-slider/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Parameter Slider\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\n    \"Parameter Slider\",\n    \"Numeric parameter adjustment controls\",\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/parameter-slider/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"Parameter Slider\",\n  description: \"Numeric parameter adjustment controls\",\n};\n\nexport const revalidate = 3600;\n\nexport default function ParameterSliderDocsPage() {\n  return (\n    <ComponentDocsTabs docs={<Content />} componentId=\"parameter-slider\" />\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/plan/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\n\n<DocsHeader\n  title=\"Plan\"\n  description=\"Display step-by-step task workflows.\"\n  mdxPath=\"app/docs/plan/content.mdx\"\n/>\n\nWhen the assistant breaks work into steps (a deployment pipeline, a research plan, a migration checklist) the Plan component turns that structure into a visual progress tracker. Each phase shows its status, and the overall shape of the work is scannable at a glance.\n\n**Role:** State. Shows internal activity and progress. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"plan\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nCreate a backend tool that returns a serializable plan payload.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\n// Backend tool\nimport { tool } from \"ai\";\n\nconst showPlan = tool({\n  description: \"Display an implementation plan to the user\",\n  inputSchema: {\n    type: \"object\",\n    properties: { task: { type: \"string\" } },\n    required: [\"task\"],\n    additionalProperties: false,\n  },\n  async execute({ task }) {\n    return {\n      id: \"plan-1\",\n      title: \"Implementation Plan\",\n      description: \"Steps to complete: \" + task,\n      todos: [\n        { id: \"1\", label: \"Analyze requirements\", status: \"completed\" },\n        { id: \"2\", label: \"Implement solution\", status: \"in_progress\" },\n        { id: \"3\", label: \"Write tests\", status: \"pending\" },\n      ],\n    };\n  },\n});\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nDrop this into your runtime provider so tool results render as `Plan`.\n\n```tsx\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { Plan } from \"@/components/tool-ui/plan\";\nimport { safeParseSerializablePlan } from \"@/components/tool-ui/plan/schema\";\nimport { ToolUI } from \"@/components/tool-ui/shared\";\n\nexport const toolkit: Toolkit = {\n  showPlan: {\n    type: \"backend\",\n    render: ({ result }) => {\n      const parsed = safeParseSerializablePlan(result);\n      if (!parsed) {\n        return null;\n      }\n      return (\n        <ToolUI id={parsed.id}>\n          <ToolUI.Surface>\n            <Plan {...parsed} />\n          </ToolUI.Surface>\n          <ToolUI.Actions>\n            <ToolUI.LocalActions\n              actions={[\n                { id: \"approve\", label: \"Approve Plan\" },\n                {\n                  id: \"revise\",\n                  label: \"Request Changes\",\n                  variant: \"secondary\",\n                },\n              ]}\n              onAction={(actionId) => console.log(actionId)}\n            />\n          </ToolUI.Actions>\n        </ToolUI>\n      );\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"BarChart3\" title=\"Progress visualization\">\n    Progress bar and \"X of Y complete\" at a glance\n  </Feature>\n  <Feature icon=\"CircleCheck\" title=\"Status indicators\">\n    Distinct icons for pending, in progress, completed, and cancelled\n  </Feature>\n  <Feature icon=\"Expand\" title=\"Expandable details\">\n    Click any todo to reveal additional context\n  </Feature>\n  <Feature icon=\"PartyPopper\" title=\"Completion celebration\">\n    Visual feedback when all tasks complete\n  </Feature>\n</FeatureGrid>\n\n## Props\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for this component\",\n      type: \"string\",\n      required: true,\n    },\n    title: {\n      description: \"Plan title displayed as the header\",\n      type: \"string\",\n      required: true,\n    },\n    description: {\n      description: \"Context description below the title\",\n      type: \"string\",\n    },\n    todos: {\n      description: \"Array of todo items (min 1)\",\n      type: \"PlanTodo[]\",\n      required: true,\n    },\n    maxVisibleTodos: {\n      description: \"Items to show before collapsing\",\n      type: \"number\",\n      default: \"4\",\n    },\n    className: { description: \"Additional CSS classes\", type: \"string\" },\n  }}\n/>\n\nUse [`ToolUI.LocalActions`](/docs/actions) to compose external actions below `Plan`.\n\n### Compact Variant\n\nUse `Plan.Compact` for a steps-only card body:\n\n- No title/description header\n- No progress summary or progress bar\n- No muted inner container\n- No embedded footer actions\n\n## Todo Item Schema\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique todo identifier\",\n      type: \"string\",\n      required: true,\n    },\n    label: { description: \"Display text\", type: \"string\", required: true },\n    status: {\n      description: \"Current state\",\n      type: \"'pending' | 'in_progress' | 'completed' | 'cancelled'\",\n      required: true,\n    },\n    description: { description: \"Expandable detail text\", type: \"string\" },\n  }}\n/>\n\n## Status States\n\n<TypeTable\n  type={{\n    pending: {\n      description: \"Circle outline icon, muted color\",\n      type: \"Not started\",\n    },\n    in_progress: {\n      description:\n        \"Spinner icon with subtle highlight ring, shimmer text effect\",\n      type: \"Active\",\n    },\n    completed: {\n      description: \"Checkmark icon with emphasized completion state\",\n      type: \"Done\",\n    },\n    cancelled: {\n      description: \"X circle icon with destructive emphasis\",\n      type: \"Skipped\",\n    },\n  }}\n/>\n\n## Accessibility\n\n- Decorative motion classes (spinner rotation, shimmer, enter/exit effects) are gated behind `motion-safe` variants\n- The progress bar fill uses a base CSS transition, so completion updates can still animate unless your app adds `motion-reduce` overrides\n- Functionality and content remain available regardless of motion preferences\n\n## Related\n\n- [Progress Tracker](/docs/progress-tracker): real-time status for operations like build/test/deploy\n"
  },
  {
    "path": "apps/www/app/docs/plan/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Plan\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\"Plan\", \"Display step-by-step task workflows\");\n}\n"
  },
  {
    "path": "apps/www/app/docs/plan/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"Plan\",\n  description: \"Display step-by-step task workflows\",\n};\n\nexport const revalidate = 3600;\n\nexport default function PlanDocsPage() {\n  return <ComponentDocsTabs docs={<Content />} componentId=\"plan\" />;\n}\n"
  },
  {
    "path": "apps/www/app/docs/preferences-panel/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\nimport { PreferencesPanel } from \"@/components/tool-ui/preferences-panel\";\n\n<DocsHeader\n  title=\"Preferences Panel\"\n  description=\"Compact settings panel with staged changes.\"\n  mdxPath=\"app/docs/preferences-panel/content.mdx\"\n/>\n\nAsking the user to type \"turn off notifications and set theme to dark\" is fragile. PreferencesPanel renders switches, toggles, and selects in a compact card with explicit Save/Cancel actions. Changes are staged locally until the user commits, and the result shows as a receipt.\n\n**Role:** Decision. For choices that return to the assistant. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"preferences-panel\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nRender `PreferencesPanel` in your UI with tool-compatible props.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\nimport { PreferencesPanel } from \"@/components/tool-ui/preferences-panel\";\n\nexport function Example() {\n  return (\n    <PreferencesPanel\n      id=\"preferences-panel-example\"\n      sections={[\n        {\n          items: [\n            {\n              id: \"notifications\",\n              label: \"Notifications\",\n              description: \"Receive push notifications\",\n              type: \"switch\",\n              defaultChecked: true,\n            },\n            {\n              id: \"theme\",\n              label: \"Theme\",\n              description: \"Choose your appearance\",\n              type: \"toggle\",\n              options: [\n                { value: \"light\", label: \"Light\" },\n                { value: \"dark\", label: \"Dark\" },\n                { value: \"auto\", label: \"Auto\" },\n              ],\n              defaultValue: \"auto\",\n            },\n            {\n              id: \"analytics\",\n              label: \"Analytics\",\n              description: \"Help improve the product\",\n              type: \"switch\",\n              defaultChecked: false,\n            },\n          ],\n        },\n      ]}\n    />\n  );\n}\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegister this renderer so tool results display as `PreferencesPanel`.\n\n```tsx\n\"use client\";\n\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { PreferencesPanel } from \"@/components/tool-ui/preferences-panel\";\nimport {\n  safeParseSerializablePreferencesPanel,\n  SerializablePreferencesPanelSchema,\n} from \"@/components/tool-ui/preferences-panel/schema\";\n\nexport const toolkit: Toolkit = {\n  show_preferences_panel: {\n    description: \"Display user preference settings with staged changes\",\n    parameters: SerializablePreferencesPanelSchema,\n    render: ({ args, toolCallId, addResult }) => {\n      const parsedArgs = safeParseSerializablePreferencesPanel({\n        ...args,\n        id: args?.id ?? `preferences-panel-${toolCallId}`,\n      });\n      if (!parsedArgs) {\n        return null;\n      }\n      return (\n        <PreferencesPanel\n          {...parsedArgs}\n          onAction={async (actionId, values) => {\n            if (actionId === \"save\") {\n              await addResult({ choice: values });\n              return;\n            }\n            if (actionId === \"cancel\") {\n              await addResult({ choice: values });\n            }\n          }}\n        />\n      );\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"Settings2\" title=\"Three control types\">\n    Switch (on/off), toggle (2-4 options), or select (5+ options)\n  </Feature>\n  <Feature icon=\"SquareCheck\" title=\"Staged changes\">\n    Save/Cancel buttons prevent accidental changes\n  </Feature>\n  <Feature icon=\"CheckCircle2\" title=\"Receipt state\">\n    `PreferencesPanel.Receipt` shows confirmed values with per-field error\n    badges\n  </Feature>\n  <Feature icon=\"Layers\" title=\"Organized sections\">\n    Group related settings under optional section headings\n  </Feature>\n</FeatureGrid>\n\n## Receipt State\n\nPass a `choice` prop to render this component in its receipt state. See [Receipts](/docs/receipts) for the pattern.\n\n### Receipt Example\n\n```tsx\nimport { PreferencesPanel } from \"@/components/tool-ui/preferences-panel\";\n\n<PreferencesPanel.Receipt\n  id=\"preferences-panel-receipt\"\n  title=\"Privacy Settings\"\n  sections={sections}\n  choice={{\n    \"profile-visibility\": \"private\",\n    \"activity-status\": false,\n  }}\n  error={{\n    \"activity-status\": \"Requires premium plan\",\n  }}\n/>;\n```\n\n## Props\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for the preferences panel instance\",\n      type: \"string\",\n      required: true,\n    },\n    title: {\n      description: \"Optional header displayed at top of card\",\n      type: \"string\",\n    },\n    sections: {\n      description: \"Preference sections with controls\",\n      type: \"PreferenceSection[]\",\n      required: true,\n    },\n    value: {\n      description:\n        \"Controlled values object. When provided, component becomes controlled.\",\n      type: \"PreferencesValue\",\n    },\n    onChange: {\n      description: \"Called when any preference value changes (before save)\",\n      type: \"(value: PreferencesValue) => void\",\n    },\n    actions: {\n      description:\n        \"Custom action buttons. Defaults to Cancel (ghost) and Save Changes (default).\",\n      type: \"Action[] | ActionsConfig\",\n    },\n    onAction: {\n      description:\n        \"Handler for actions. Receives actionId and post-action values.\",\n      type: \"(actionId: string, value: PreferencesValue) => void | Promise<void>\",\n    },\n    onBeforeAction: {\n      description:\n        \"Action guard. Receives actionId and current values before the action runs.\",\n      type: \"(actionId: string, value: PreferencesValue) => boolean | Promise<boolean>\",\n    },\n    className: {\n      description: \"Additional CSS classes for the root element\",\n      type: \"string\",\n    },\n  }}\n/>\n\n### PreferencesPanel.Receipt\n\nUse the receipt variant to display confirmed preferences or validation errors.\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for the receipt instance\",\n      type: \"string\",\n      required: true,\n    },\n    title: {\n      description: \"Optional header displayed at top of card\",\n      type: \"string\",\n    },\n    sections: {\n      description: \"Preference sections describing each setting\",\n      type: \"PreferenceSection[]\",\n      required: true,\n    },\n    choice: {\n      description: \"Confirmed values keyed by item ID\",\n      type: \"PreferencesValue\",\n      required: true,\n    },\n    error: {\n      description: \"Field-level error messages keyed by item ID\",\n      type: \"Record<string, string>\",\n    },\n    className: {\n      description: \"Additional CSS classes for the root element\",\n      type: \"string\",\n    },\n  }}\n/>\n\n### PreferenceSection\n\n<TypeTable\n  type={{\n    heading: {\n      description: \"Optional section heading displayed above items\",\n      type: \"string\",\n    },\n    items: {\n      description: \"Preference controls in this section\",\n      type: \"PreferenceItem[]\",\n      required: true,\n    },\n  }}\n/>\n\n### PreferenceItem\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier used as key in values object\",\n      type: \"string\",\n      required: true,\n    },\n    label: {\n      description: \"Primary label displayed above the control\",\n      type: \"string\",\n      required: true,\n    },\n    description: {\n      description: \"Optional help text displayed below the label\",\n      type: \"string\",\n    },\n    type: {\n      description:\n        \"Control type: switch (binary), toggle (2+ options), select (5+ options)\",\n      type: '\"switch\" | \"toggle\" | \"select\"',\n      required: true,\n    },\n    defaultChecked: {\n      description: \"Initial checked state (defaults to false).\",\n      type: \"boolean\",\n    },\n    options: {\n      description:\n        'Available choices. Required when type is \"toggle\". Supports 2-4 options.',\n      type: \"{ value: string; label: string }[]\",\n    },\n    defaultValue: {\n      description:\n        \"Initially selected option value (defaults to first option).\",\n      type: \"string\",\n    },\n    selectOptions: {\n      description:\n        'Available choices. Required when type is \"select\". Use for 5+ options.',\n      type: \"{ value: string; label: string }[]\",\n    },\n    defaultSelected: {\n      description:\n        \"Initially selected option value (defaults to first option).\",\n      type: \"string\",\n    },\n  }}\n/>\n\n### PreferencesValue\n\n<TypeTable\n  type={{\n    \"[itemId: string]\": {\n      description:\n        \"Values keyed by item ID. Boolean for switches, string for toggles and selects.\",\n      type: \"string | boolean\",\n    },\n  }}\n/>\n\n## Control Type Guide\n\n- **Toggle options**: minimum 2 choices (use select for 5+)\n- **Select options**: minimum 5 choices (use toggle for fewer)\n\n## Accessibility\n\n- Full keyboard navigation support for all control types\n- Labels properly associated with controls using `htmlFor` and `id` attributes\n- Save button disabled when no changes exist to prevent unnecessary actions\n- Receipt state uses `role=\"status\"` for screen reader announcements\n- All animations respect `prefers-reduced-motion` user preference\n- Sufficient color contrast meets WCAG AA standards\n- Focus indicators visible on all interactive elements\n\n## Related\n\n- [Parameter Slider](/docs/parameter-slider): numeric adjustment with sliders\n- [Option List](/docs/option-list): selection from discrete choices\n- [Approval Card](/docs/approval-card): binary confirmation for consequential actions\n"
  },
  {
    "path": "apps/www/app/docs/preferences-panel/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Preferences Panel\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\"Preferences Panel\", \"Compact settings panel\");\n}\n"
  },
  {
    "path": "apps/www/app/docs/preferences-panel/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"Preferences Panel\",\n  description: \"Compact settings panel for user preferences\",\n};\n\nexport const revalidate = 3600;\n\nexport default function PreferencesPanelDocsPage() {\n  return (\n    <ComponentDocsTabs docs={<Content />} componentId=\"preferences-panel\" />\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/progress-tracker/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\nimport { ProgressTracker } from \"@/components/tool-ui/progress-tracker\";\n\n<DocsHeader\n  title=\"Progress Tracker\"\n  description=\"Real-time multi-step status.\"\n  mdxPath=\"app/docs/progress-tracker/content.mdx\"\n/>\n\nWhen an operation takes more than a moment (build, test, deploy) the user deserves to know where things stand. ProgressTracker shows each step of a multi-step workflow with its current status: pending, in-progress, completed, or failed. If something breaks, the error is visible immediately. Once the operation finishes, the tracker collapses into a receipt with the final outcome.\n\n**Role:** State. Shows internal activity and progress. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"progress-tracker\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nRender `ProgressTracker` in your UI with tool-compatible props.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\nimport { ProgressTracker } from \"@/components/tool-ui/progress-tracker\";\n\nexport function Example() {\n  return (\n    <ProgressTracker\n      id=\"progress-tracker-example\"\n      steps={[\n        {\n          id: \"build\",\n          label: \"Building\",\n          description: \"Compiling TypeScript and bundling assets\",\n          status: \"completed\",\n        },\n        {\n          id: \"test\",\n          label: \"Running Tests\",\n          description: \"147 tests across 23 suites\",\n          status: \"in-progress\",\n        },\n        {\n          id: \"deploy\",\n          label: \"Deploy to Production\",\n          description: \"Upload to edge nodes\",\n          status: \"pending\",\n        },\n      ]}\n      elapsedTime={43200}\n    />\n  );\n}\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegister this renderer so tool results display as `ProgressTracker`.\n\n```tsx\n\"use client\";\n\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { ProgressTracker } from \"@/components/tool-ui/progress-tracker\";\nimport { safeParseSerializableProgressTracker } from \"@/components/tool-ui/progress-tracker/schema\";\n\nexport const toolkit: Toolkit = {\n  trackProgress: {\n    type: \"backend\",\n    render: ({ result }) => {\n      const parsed = safeParseSerializableProgressTracker(result);\n      if (!parsed) {\n        return null;\n      }\n      return <ProgressTracker {...parsed} />;\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\nProgressTracker is display-only and result-driven: render it from backend tool result updates. For interactive decisions or confirmations, use OptionList or ApprovalCard.\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"ListChecks\" title=\"Status indicators\">\n    Pending, in-progress, completed, and failed with distinct icons\n  </Feature>\n  <Feature icon=\"Timer\" title=\"Elapsed time\">\n    Auto-formatted timer badge (seconds or minutes)\n  </Feature>\n  <Feature icon=\"AlertCircle\" title=\"Error handling\">\n    Failed steps show red indicators with error details\n  </Feature>\n  <Feature icon=\"CheckCircle2\" title=\"Receipt state\">\n    Read-only summary of the completed operation\n  </Feature>\n</FeatureGrid>\n\n## Step Statuses\n\nEach step has one of four statuses:\n\n- **Pending**: not yet started (empty circle)\n- **In-progress**: currently executing (animated spinner)\n- **Completed**: finished successfully (filled checkmark)\n- **Failed**: encountered an error (filled X icon)\n\nThe component automatically highlights the current step (first `in-progress`, otherwise first `failed`, otherwise first `pending`) with a subtle background and `aria-current=\"step\"` for accessibility.\n\n## Receipt Pattern\n\nPass a `choice` prop to render this component in its receipt state. See [Receipts](/docs/receipts) for the pattern.\n\nThe receipt shows all steps with their final statuses, an elapsed time badge, and an outcome indicator in the top-right corner. Supports `success`, `partial`, `failed`, and `cancelled` outcomes. Read-only with no action buttons.\n\n## Elapsed Time\n\nPass `elapsedTime` in milliseconds to display a timer badge. Automatically formatted as:\n\n- Under 60 seconds: `3.4s`\n- Over 60 seconds: `2m 15s`\n\n## Props\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for the progress tracker instance\",\n      type: \"string\",\n      required: true,\n    },\n    steps: {\n      description: \"Sequential list of operation steps with status indicators\",\n      type: \"ProgressStep[]\",\n      required: true,\n    },\n    elapsedTime: {\n      description:\n        \"Operation duration in milliseconds (finite, non-negative). Displayed as formatted timer badge.\",\n      type: \"number\",\n    },\n    choice: {\n      description:\n        \"When provided, renders terminal state showing full step history with outcome indicator\",\n      type: \"ProgressTrackerChoice\",\n    },\n  }}\n/>\n\n## ProgressStep Schema\n\n<TypeTable\n  type={{\n    id: {\n      description:\n        \"Unique identifier for the step (must be unique within the steps array)\",\n      type: \"string\",\n      required: true,\n    },\n    label: {\n      description: \"Step name displayed to the user\",\n      type: \"string\",\n      required: true,\n    },\n    description: {\n      description:\n        \"Additional details shown below the label. In interactive mode, visible for in-progress and failed steps. In receipt mode, all step descriptions are shown.\",\n      type: \"string\",\n    },\n    status: {\n      description: \"Current state of the step\",\n      type: \"'pending' | 'in-progress' | 'completed' | 'failed'\",\n      required: true,\n    },\n  }}\n/>\n\n## ToolUIReceipt Schema\n\n<TypeTable\n  type={{\n    outcome: {\n      description:\n        \"Final result of the operation. Determines outcome indicator color/icon.\",\n      type: \"'success' | 'partial' | 'failed' | 'cancelled'\",\n      required: true,\n    },\n    summary: {\n      description:\n        \"Brief outcome message displayed in top-right (e.g., 'Deployment complete')\",\n      type: \"string\",\n      required: true,\n    },\n    at: {\n      description: \"ISO 8601 timestamp when the operation completed\",\n      type: \"string\",\n      required: true,\n    },\n    identifiers: {\n      description:\n        \"Optional reference IDs for the operation (e.g., { deploymentId: 'abc-123' })\",\n      type: \"Record<string, string>\",\n    },\n  }}\n/>\n\n## Accessibility\n\n- Semantic `<article>` with `role=\"status\"` and `aria-live=\"polite\"` for live updates\n- `aria-busy` indicates when an operation is in progress\n- `aria-current=\"step\"` marks the currently active step\n- Steps rendered as semantic ordered list (`<ol>` / `<li>`)\n- Elapsed duration is rendered with semantic `<time>` and machine-readable `dateTime`\n- All animations respect `prefers-reduced-motion`\n- Text is unselectable to prevent accidental highlighting during status changes\n\n## Related\n\n- [Plan](/docs/plan): task-oriented progress with expandable detail per step\n"
  },
  {
    "path": "apps/www/app/docs/progress-tracker/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Progress Tracker\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\n    \"Progress Tracker\",\n    \"Real-time status feedback for multi-step operations\",\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/progress-tracker/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"Progress Tracker\",\n  description: \"Real-time status feedback for multi-step operations\",\n};\n\nexport const revalidate = 3600;\n\nexport default function ProgressTrackerDocsPage() {\n  return (\n    <ComponentDocsTabs docs={<Content />} componentId=\"progress-tracker\" />\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/question-flow/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\n\n<DocsHeader\n  title=\"Question Flow\"\n  description=\"Multi-step guided questions with branching.\"\n  mdxPath=\"app/docs/question-flow/content.mdx\"\n/>\n\nA single question rarely captures a full intent. Project setup, onboarding, and triage all involve a chain of decisions where each answer shapes the next. QuestionFlow walks the user through single- or multi-select steps with back/next navigation. You can define all steps upfront or let the assistant generate each step based on previous answers.\n\n**Role:** Decision. For choices that return to the assistant. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"question-flow\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nRender `QuestionFlow` in your UI with tool-compatible props.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\nimport { QuestionFlow } from \"@/components/tool-ui/question-flow\";\n\nexport function Example({\n  payload,\n}: {\n  payload: React.ComponentProps<typeof QuestionFlow>;\n}) {\n  return <QuestionFlow {...payload} />;\n}\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegister this renderer so tool results display as `QuestionFlow`.\n\n```tsx\n\"use client\";\n\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { QuestionFlow } from \"@/components/tool-ui/question-flow\";\nimport {\n  safeParseSerializableQuestionFlow,\n  SerializableQuestionFlowSchema,\n} from \"@/components/tool-ui/question-flow/schema\";\n\nexport const toolkit: Toolkit = {\n  configureProject: {\n    description: \"Guide user through project configuration.\",\n    parameters: SerializableQuestionFlowSchema,\n    render: ({ args, toolCallId, result, addResult }) => {\n      const parsedArgs = safeParseSerializableQuestionFlow({\n        ...args,\n        id: args?.id ?? `wizard-${toolCallId}`,\n      });\n      if (!parsedArgs) {\n        return null;\n      }\n      return result ? (\n        <QuestionFlow\n          id={parsedArgs.id}\n          choice={{\n            title: \"Project configured\",\n            summary: Object.entries(result as Record<string, string[]>).map(\n              ([key, values]) => ({\n                label: key,\n                value: values.join(\", \"),\n              }),\n            ),\n          }}\n        />\n      ) : (\n        <QuestionFlow\n          {...parsedArgs}\n          onComplete={(answers) => addResult?.(answers)}\n        />\n      );\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"Play\" title=\"Progressive mode\">\n    The AI generates each step based on previous answers\n  </Feature>\n  <Feature icon=\"ListChecks\" title=\"Upfront mode\">\n    All steps defined upfront; the component handles navigation\n  </Feature>\n  <Feature icon=\"SquareCheck\" title=\"Single or multi-select\">\n    Each step allows one pick or multiple selections\n  </Feature>\n  <Feature icon=\"CheckCircle2\" title=\"Receipt state\">\n    Shows a summary of all choices once the flow completes\n  </Feature>\n</FeatureGrid>\n\n## Modes\n\n### Progressive Mode\n\nPass `step`, `title`, and `options` to render one step at a time. The AI generates each subsequent step based on the user's previous selections.\n\n### Upfront Mode\n\nPass `steps` with all step definitions. The component manages navigation internally.\n\n### Multi-Select Steps\n\nSet `selectionMode=\"multi\"` to allow multiple selections per step.\n\n## Receipt State\n\nPass a `choice` prop to render this component in its receipt state. See [Receipts](/docs/receipts) for the pattern.\n\nThe receipt shows a \"Complete\" badge with a summary of all choices listed. It is read-only.\n\n## Progressive Mode Props\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique wizard identifier\",\n      type: \"string\",\n      required: true,\n    },\n    step: {\n      description: \"Current step number (1-indexed)\",\n      type: \"number\",\n      required: true,\n    },\n    title: {\n      description: \"Step title\",\n      type: \"string\",\n      required: true,\n    },\n    description: {\n      description: \"Optional step description\",\n      type: \"string\",\n    },\n    options: {\n      description: \"Available choices for this step\",\n      type: \"QuestionFlowOption[]\",\n      required: true,\n    },\n    selectionMode: {\n      description: \"Selection behavior for this step\",\n      type: \"'single' | 'multi'\",\n      default: \"'single'\",\n    },\n    defaultValue: {\n      description: \"Pre-selected option IDs for this step\",\n      type: \"string[]\",\n    },\n    onSelect: {\n      description: \"Called when user selects option(s) and clicks Next\",\n      type: \"(optionIds: string[]) => void | Promise<void>\",\n    },\n    onBack: {\n      description: \"Called when user clicks Back\",\n      type: \"() => void\",\n    },\n  }}\n/>\n\n## Upfront Mode Props\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique wizard identifier\",\n      type: \"string\",\n      required: true,\n    },\n    steps: {\n      description: \"All step definitions\",\n      type: \"QuestionFlowDefinition[]\",\n      required: true,\n    },\n    onStepChange: {\n      description: \"Called when the active step changes\",\n      type: \"(stepId: string) => void\",\n    },\n    onComplete: {\n      description: \"Called with all answers when wizard finishes\",\n      type: \"(answers: Record<string, string[]>) => void | Promise<void>\",\n    },\n  }}\n/>\n\n## Receipt Mode Props\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique wizard identifier\",\n      type: \"string\",\n      required: true,\n    },\n    choice: {\n      description: \"Configuration summary to display\",\n      type: \"QuestionFlowChoice\",\n      required: true,\n    },\n  }}\n/>\n\n## Option Schema\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique option identifier\",\n      type: \"string\",\n      required: true,\n    },\n    label: {\n      description: \"Display text\",\n      type: \"string\",\n      required: true,\n    },\n    description: {\n      description: \"Secondary text\",\n      type: \"string\",\n    },\n    icon: {\n      description: \"Optional icon element\",\n      type: \"ReactNode\",\n    },\n    disabled: {\n      description: \"Disable this option\",\n      type: \"boolean\",\n    },\n  }}\n/>\n\n## Step Definition Schema\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique step identifier (used as answer key)\",\n      type: \"string\",\n      required: true,\n    },\n    title: {\n      description: \"Step title\",\n      type: \"string\",\n      required: true,\n    },\n    description: {\n      description: \"Optional step description\",\n      type: \"string\",\n    },\n    options: {\n      description: \"Available choices\",\n      type: \"QuestionFlowOption[]\",\n      required: true,\n    },\n    selectionMode: {\n      description: \"Selection behavior\",\n      type: \"'single' | 'multi'\",\n      default: \"'single'\",\n    },\n  }}\n/>\n\n## Accessibility\n\n- Option buttons use keyboard-accessible controls\n- Focus management follows WAI-ARIA patterns for step navigation\n- Progress indicator provides context for screen readers\n- Back and Next buttons are keyboard-accessible\n\n## Related\n\n- [Option List](/docs/option-list): single-step selection when one question is enough\n- [Preferences Panel](/docs/preferences-panel): structured settings with switches, toggles, and selects\n"
  },
  {
    "path": "apps/www/app/docs/question-flow/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Question Flow\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\n    \"Question Flow\",\n    \"Multi-step guided questions with branching\",\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/question-flow/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"Question Flow\",\n  description: \"Multi-step guided questions with branching\",\n};\n\nexport const revalidate = 3600;\n\nexport default function QuestionFlowDocsPage() {\n  return <ComponentDocsTabs docs={<Content />} componentId=\"question-flow\" />;\n}\n"
  },
  {
    "path": "apps/www/app/docs/quick-start/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\";\nimport { Bot } from \"lucide-react\";\n\n<DocsHeader\n  title=\"Quick Start\"\n  description=\"Your first component in minutes.\"\n  mdxPath=\"app/docs/quick-start/content.mdx\"\n/>\n\n<Alert className=\"squircle my-6 flex max-w-2xl flex-col gap-3 rounded-4xl border-sky-300 bg-sky-50/50 p-5 dark:border-sky-950 dark:bg-sky-950/20\">\n  <AlertTitle className=\"mb-0! flex items-center gap-2 pb-0! font-mono text-sky-700 dark:text-sky-300\">\n    <Bot className=\"size-4 text-sky-600! dark:text-sky-300!\" />\n    {\"Skills now available\"}\n  </AlertTitle>\n  <AlertDescription className=\"my-0! py-0! text-pretty text-sky-800 dark:text-sky-100 [&_p]:m-0 [&_p]:p-0\">\n    Give your coding agent advanced Tool UI skills.{\" \"}\n    <a href=\"/docs/agent-skills\">Install now.</a>\n  </AlertDescription>\n</Alert>\n\nAdd a Tool UI component to a chat app powered by [assistant-ui](https://www.assistant-ui.com/).\n\n## Install\n\n<Steps>\n\n<Step title=\"Set up assistant-ui\">\n\n[assistant-ui](https://www.assistant-ui.com/) is the React runtime that manages chat state, message streaming, and tool rendering. If you already have it set up, skip to [Step 2](#add-a-tool-ui-component).\n\nThe fastest way to start is with the CLI. It creates a working chat app in a Next.js project:\n\n```sh\nnpx assistant-ui@latest init\n```\n\nOr install packages manually into an existing project:\n\n```sh\npnpm add @assistant-ui/react @assistant-ui/react-ai-sdk ai @ai-sdk/openai zod\n```\n\nAdd your OpenAI API key to `.env.local`:\n\n```env\nOPENAI_API_KEY=sk-...\n```\n\nRun `pnpm dev` and confirm you see a working chat interface before continuing.\n\n</Step>\n\n<Step title=\"Add a Tool UI component\">\n\n<InstallCommandBlock\n  componentId=\"link-preview\"\n  toolAgentPrompt=\"integrate the link preview component for rich link previews with Open Graph data in the chat experience\"\n  variant=\"block\"\n/>\n\nThis copies the source files into your project. The code is yours - change it however you want.\n\n</Step>\n\n</Steps>\n\n## Wire it up\n\n<Steps>\n\n<Step title=\"Define a backend tool\">\n\nCreate (or update) your API route to include a tool the LLM can call. This tool accepts a URL, fetches metadata, and returns JSON matching the LinkPreview schema.\n\n```ts title=\"app/api/chat/route.ts\"\nimport { streamText, tool, convertToModelMessages } from \"ai\";\nimport { openai } from \"@ai-sdk/openai\";\nimport { z } from \"zod\";\nimport { SerializableLinkPreviewSchema } from \"@/components/tool-ui/link-preview/schema\";\n\nexport async function POST(req: Request) {\n  const { messages } = await req.json();\n\n  const result = streamText({\n    model: openai(\"gpt-4o\"),\n    messages: await convertToModelMessages(messages),\n    tools: {\n      // The LLM decides when to call this tool based on the description.\n      previewLink: tool({\n        description: \"Show a preview card for a URL\",\n        inputSchema: z.object({ url: z.url() }),\n        // outputSchema validates the result against the component's schema.\n        outputSchema: SerializableLinkPreviewSchema,\n        // execute() runs on the server when the LLM calls the tool.\n        // It returns JSON that matches the LinkPreview component's schema.\n        async execute({ url }) {\n          return {\n            id: \"link-preview-1\",\n            href: url,\n            title: \"Example Site\",\n            description: \"A description of the linked content\",\n            image: \"https://example.com/image.jpg\",\n          };\n        },\n      }),\n    },\n  });\n\n  return result.toUIMessageStreamResponse();\n}\n```\n\nIn a real app, fetch actual metadata here (Open Graph tags, screenshots, etc.).\n\n</Step>\n\n<Step title=\"Register the renderer\">\n\nRegistering tells assistant-ui: \"when the model calls a tool named `previewLink`, parse the result and render this component.\" Without registration, tool results appear as raw JSON.\n\n```tsx title=\"app/page.tsx\"\n\"use client\";\n\nimport {\n  AssistantRuntimeProvider,\n  Tools,\n  useAui,\n  type Toolkit,\n} from \"@assistant-ui/react\";\nimport {\n  useChatRuntime,\n  AssistantChatTransport,\n} from \"@assistant-ui/react-ai-sdk\";\nimport { LinkPreview } from \"@/components/tool-ui/link-preview\";\nimport { safeParseSerializableLinkPreview } from \"@/components/tool-ui/link-preview/schema\";\n\n// Map backend tool names to frontend renderers.\n// The key \"previewLink\" must match the tool name in your API route.\nconst toolkit: Toolkit = {\n  previewLink: {\n    type: \"backend\",\n    render: ({ result }) => {\n      const parsed = safeParseSerializableLinkPreview(result);\n      if (!parsed) {\n        return null;\n      }\n      return <LinkPreview {...parsed} />;\n    },\n  },\n};\n\nexport default function Page() {\n  // Connect to your /api/chat route.\n  const runtime = useChatRuntime({\n    transport: new AssistantChatTransport({ api: \"/api/chat\" }),\n  });\n\n  // Register the toolkit so the runtime knows how to render tool results.\n  const aui = useAui({ tools: Tools({ toolkit }) });\n\n  return (\n    <AssistantRuntimeProvider runtime={runtime} aui={aui}>\n      {/* Your chat thread component */}\n    </AssistantRuntimeProvider>\n  );\n}\n```\n\nRun `pnpm dev`, then ask the assistant to \"preview https://example.com.\" Instead of raw JSON, you'll see a styled LinkPreview card in the conversation.\n\n</Step>\n\n</Steps>\n\n## Add more components\n\nSame pattern for any component:\n\n1. Install from the registry:\n\n<InstallCommandBlock\n  componentId=\"approval-card\"\n  toolAgentPrompt=\"integrate the approval card component for binary confirmation of agent actions\"\n  variant=\"block\"\n/>\n\n2. Define a backend tool that returns data matching the component's schema.\n3. Register a renderer in your toolkit that maps the tool name to the component.\n\nEvery component ships with a colocated `schema.ts` exporting a Zod schema and a `safeParseSerializable{ComponentName}` function for validating tool output on both server and client.\n\nBrowse all available components in the [Gallery](/docs/gallery).\n\n## Interactive tool UIs\n\nThe example above is a **display-only** component. The tool returns data, and the component renders it. Some components go further: they let the user make a choice that returns to the assistant.\n\nComponents like [Option List](/docs/option-list) and [Approval Card](/docs/approval-card) support this pattern through **frontend tools**, where the model calls a tool, your UI renders the component, and the user's response is sent back via `addResult(...)`.\n\nFor the full implementation pattern, including how to forward frontend tools through your API route and enable auto-continue after user decisions, see the [Advanced](/docs/advanced) page.\n\n## Next steps\n\n- [**Gallery**](/docs/gallery): Browse all available components\n- [**Actions**](/docs/actions): How interactive components feed user decisions back to the assistant\n- [**Receipts**](/docs/receipts): How components show permanent records of past decisions\n- [**Design Guidelines**](/docs/design-guidelines): The collaboration model and design constraints for chat UI\n- [**Advanced**](/docs/advanced): Frontend tools, type inference, and deeper integration patterns\n"
  },
  {
    "path": "apps/www/app/docs/quick-start/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Quick Start\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\"Quick Start\", \"Get up and running in minutes\");\n}\n"
  },
  {
    "path": "apps/www/app/docs/quick-start/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { DocsArticle } from \"../_components/docs-article\";\n\nexport const metadata: Metadata = {\n  title: \"Quick Start\",\n  description: \"Get started with Tool UI in minutes\",\n};\n\nexport const revalidate = 3600;\n\nexport default function QuickStartPage() {\n  return (\n    <DocsArticle>\n      <Content />\n    </DocsArticle>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/receipts/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\nimport { MockThread, MockMessage } from \"../_components/mock-thread\";\nimport { OptionList } from \"@/components/tool-ui/option-list\";\nimport { ApprovalCard } from \"@/components/tool-ui/approval-card\";\n\n<DocsHeader\n  title=\"Receipts\"\n  description=\"Confirm what happened after a user decision.\"\n  mdxPath=\"app/docs/receipts/content.mdx\"\n/>\n\nWhen a user approves a deploy, selects a plan, or confirms a purchase, the interactive UI should become a permanent record of what happened. Without this, scrolling back shows a live button for a decision that was already made, confusing at best, dangerous at worst if someone clicks it again.\n\nA **receipt state** replaces the interactive controls with a confirmation of the choice. It's proof of the decision, visible to both the user and the assistant. This is the natural counterpart to [decision actions](/docs/actions): the action captures the choice, the receipt displays it.\n\n## Why Receipts Matter\n\nWithout receipts, decisions vanish into the conversation history.\n\n<div>\n  <MockThread caption=\"A decision with receipt\">\n    <MockMessage role=\"assistant\">\n      <span className=\"text-foreground\">\n        How would you like to handle the duplicate contacts?\n      </span>\n      <div className=\"mt-3\">\n        <OptionList\n          selectionMode=\"single\"\n          choice=\"merge\"\n          options={[\n            {\n              id: \"merge\",\n              label: \"Merge duplicates\",\n              description: \"Combine into single contacts, keeping all data\",\n            },\n            {\n              id: \"keep\",\n              label: \"Keep all\",\n              description: \"Leave duplicates as separate contacts\",\n            },\n            {\n              id: \"review\",\n              label: \"Review manually\",\n              description: \"I'll decide for each duplicate\",\n            },\n          ]}\n        />\n      </div>\n    </MockMessage>\n    <MockMessage role=\"assistant\">\n      <span className=\"text-foreground\">\n        Done. I merged 12 duplicate contacts. You can undo this from Settings →\n        Contact History if needed.\n      </span>\n    </MockMessage>\n  </MockThread>\n</div>\n\nThe receipt shows what was chosen. The follow-up confirms the outcome and offers an undo path.\n\n## Writing Receipt Copy\n\nReceipt copy describes a past decision.\n\n### The Decision Label\n\nDecision labels use **past tense**: they describe what the user decided.\n\n| Interactive State | Receipt State |\n| ----------------- | ------------- |\n| Approve           | Approved      |\n| Deny              | Denied        |\n| Select            | Selected      |\n| Confirm           | Confirmed     |\n\n### The Action Description\n\nAction descriptions stay in **imperative form**: they describe the request, not the outcome.\n\n<div className=\"not-prose grid gap-4 sm:grid-cols-2 my-6\">\n  <div>\n    <p className=\"text-sm text-muted-foreground mb-2\">Approved action</p>\n    <ApprovalCard\n      id=\"receipt-approved-example\"\n      title=\"Back up database\"\n      choice=\"approved\"\n    />\n  </div>\n  <div>\n    <p className=\"text-sm text-muted-foreground mb-2\">Denied action</p>\n    <ApprovalCard\n      id=\"receipt-denied-example\"\n      title=\"Delete all project files\"\n      choice=\"denied\"\n    />\n  </div>\n</div>\n\nThe pattern is: **[Past-tense decision] [Imperative action]**\n\n- \"Approved: Back up database\" (the request to back up was approved)\n- \"Denied: Delete all project files\" (the request to delete was denied)\n- \"Selected: Merge duplicates\" (the option to merge was selected)\n\n## Components with Receipt States\n\nPass a `choice` prop to switch any of these components into receipt mode:\n\n| Component                                    | Receipt Prop | What It Shows                 |\n| -------------------------------------------- | ------------ | ----------------------------- |\n| [Approval Card](/docs/approval-card)         | `choice`     | The decision and action title |\n| [Option List](/docs/option-list)             | `choice`     | Only the selected option(s)   |\n| [Question Flow](/docs/question-flow)         | `choice`     | Summary of completed answers  |\n| [Preferences Panel](/docs/preferences-panel) | `choice`     | Summary of saved preferences  |\n| [Progress Tracker](/docs/progress-tracker)   | `choice`     | Final status of the workflow  |\n| [Order Summary](/docs/order-summary)         | `choice`     | Confirmed order details       |\n\n[Message Draft](/docs/message-draft) uses a similar pattern with an `outcome` prop (`\"sent\"` or `\"cancelled\"`) that renders a one-line confirmation receipt after a message is sent or cancelled.\n\n## When to Use Receipts\n\nUse receipts for decisions that:\n\n- Change state (approvals, deletions, submissions)\n- Can't be easily undone\n- The user or assistant might need to reference later\n\nSkip receipts for reversible controls like:\n\n- Sorting or filtering data\n- Adjusting parameters\n- Navigating between views\n\nThe assistant's follow-up should reference the receipt and, when appropriate, offer a way to undo or modify the decision.\n"
  },
  {
    "path": "apps/www/app/docs/receipts/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Receipts\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\n    \"Receipts\",\n    \"Confirm what happened after a user decision\",\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/receipts/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { DocsArticle } from \"../_components/docs-article\";\n\nexport const metadata: Metadata = {\n  title: \"Receipts\",\n  description: \"Confirm what happened after a user decision\",\n};\n\nexport const revalidate = 3600;\n\nexport default function ReceiptsPage() {\n  return (\n    <DocsArticle>\n      <Content />\n    </DocsArticle>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/stats-display/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\nimport { StatsDisplay } from \"@/components/tool-ui/stats-display\";\n\n<DocsHeader\n  title=\"Stats Display\"\n  description=\"Display key metrics in a grid.\"\n  mdxPath=\"app/docs/stats-display/content.mdx\"\n/>\n\nNumeric data buried in a chat message is hard to scan. When the assistant reports four KPIs in a paragraph, the user has to parse each number from the surrounding text. StatsDisplay renders those numbers in a responsive grid with sparklines, delta indicators, and locale-aware formatting so trends are visible at a glance.\n\n**Role:** Information. For displaying data without user input. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"stats-display\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nRender `StatsDisplay` in your UI with tool-compatible props.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\nimport { StatsDisplay } from \"@/components/tool-ui/stats-display\";\n\nexport function Example() {\n  return (\n    <StatsDisplay\n      id=\"stats-example\"\n      title=\"Q4 Performance\"\n      description=\"October through December 2024\"\n      stats={[\n        {\n          key: \"revenue\",\n          label: \"Revenue\",\n          value: 847300,\n          format: { kind: \"currency\", currency: \"USD\", decimals: 0 },\n          sparkline: {\n            data: [\n              72000, 68000, 74000, 81000, 78000, 85000, 89000, 91000, 86000,\n              94000, 97000, 102000,\n            ],\n            color: \"var(--chart-1)\",\n          },\n          diff: { value: 12.4, decimals: 1 },\n        },\n        {\n          key: \"active-users\",\n          label: \"Active Users\",\n          value: 24890,\n          format: { kind: \"number\", compact: true },\n          sparkline: {\n            data: [\n              18200, 19100, 19800, 20400, 21200, 21900, 22600, 23100, 23800,\n              24200, 24500, 24890,\n            ],\n            color: \"var(--chart-3)\",\n          },\n          diff: { value: 8.2, decimals: 1 },\n        },\n        {\n          key: \"churn\",\n          label: \"Churn Rate\",\n          value: 2.1,\n          format: { kind: \"percent\", decimals: 1, basis: \"unit\" },\n          sparkline: {\n            data: [3.2, 3.0, 2.8, 2.9, 2.7, 2.5, 2.4, 2.3, 2.2, 2.1, 2.1, 2.1],\n            color: \"var(--chart-4)\",\n          },\n          diff: { value: -0.8, decimals: 1, upIsPositive: false },\n        },\n        {\n          key: \"nps\",\n          label: \"NPS Score\",\n          value: 72,\n          format: { kind: \"number\" },\n          sparkline: {\n            data: [58, 61, 64, 62, 65, 68, 66, 69, 70, 71, 71, 72],\n            color: \"var(--chart-5)\",\n          },\n          diff: { value: 5.0, decimals: 0 },\n        },\n      ]}\n    />\n  );\n}\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegistration tells assistant-ui which component to render when a tool named `showStats` returns data. Without it, tool results appear as raw JSON.\n\n```tsx\n\"use client\";\n\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { StatsDisplay } from \"@/components/tool-ui/stats-display\";\nimport {\n  safeParseSerializableStatsDisplay,\n  SerializableStatsDisplaySchema,\n} from \"@/components/tool-ui/stats-display/schema\";\n\nexport const toolkit: Toolkit = {\n  showStats: {\n    description: \"Display key metrics in a grid\",\n    parameters: SerializableStatsDisplaySchema,\n    render: ({ args, toolCallId }) => {\n      const parsedArgs = safeParseSerializableStatsDisplay({\n        ...args,\n        id: args?.id ?? `stats-${toolCallId}`,\n      });\n      if (!parsedArgs) {\n        return null;\n      }\n      return <StatsDisplay {...parsedArgs} />;\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"Grid2x2\" title=\"Auto-responsive grid\">\n    Tiles stats in columns that reflow to fit the container\n  </Feature>\n  <Feature icon=\"LineChart\" title=\"Inline sparklines\">\n    Pure SVG trend lines with no extra dependencies\n  </Feature>\n  <Feature icon=\"BarChart3\" title=\"Delta indicators\">\n    Green for up, red for down (flip with `upIsPositive: false` for metrics like\n    churn)\n  </Feature>\n  <Feature icon=\"DollarSign\" title=\"Format options\">\n    Currency, percent, number, and text with locale-aware formatting\n  </Feature>\n  <Feature icon=\"Target\" title=\"Single-stat hero mode\">\n    Pass one stat for a larger, centered card\n  </Feature>\n</FeatureGrid>\n\n## Props\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for this component instance\",\n      type: \"string\",\n      required: true,\n    },\n    title: {\n      description: \"Optional card title\",\n      type: \"string\",\n    },\n    description: {\n      description: \"Optional card description\",\n      type: \"string\",\n    },\n    stats: {\n      description: \"Array of stat items to display\",\n      type: \"StatItem[]\",\n      required: true,\n    },\n    className: {\n      description: \"Additional CSS classes\",\n      type: \"string\",\n    },\n    locale: {\n      description: \"Locale for number formatting\",\n      type: \"string\",\n    },\n  }}\n/>\n\n## StatItem Schema\n\n<TypeTable\n  type={{\n    key: {\n      description: \"Unique key for the stat\",\n      type: \"string\",\n      required: true,\n    },\n    label: {\n      description: \"Label displayed above the value\",\n      type: \"string\",\n      required: true,\n    },\n    value: {\n      description: \"The stat value\",\n      type: \"number | string\",\n      required: true,\n    },\n    format: {\n      description: \"Value formatting config\",\n      type: \"StatFormat\",\n    },\n    diff: {\n      description: \"Change indicator\",\n      type: \"StatDiff\",\n    },\n    sparkline: {\n      description: \"Mini trend visualization\",\n      type: \"StatSparkline\",\n    },\n  }}\n/>\n\n## StatFormat Variants\n\n<TypeTable\n  type={{\n    text: {\n      description: \"Display value as-is\",\n      type: \"{ kind: 'text' }\",\n    },\n    number: {\n      description:\n        \"Format as number with optional decimals and compact notation\",\n      type: \"{ kind: 'number', decimals?: number, compact?: boolean }\",\n    },\n    currency: {\n      description: \"Format as currency\",\n      type: \"{ kind: 'currency', currency: string, decimals?: number }\",\n    },\n    percent: {\n      description: \"Format as percentage\",\n      type: \"{ kind: 'percent', decimals?: number, basis?: 'fraction' | 'unit' }\",\n    },\n  }}\n/>\n\n## StatDiff Schema\n\n<TypeTable\n  type={{\n    value: {\n      description: \"Change percentage value\",\n      type: \"number\",\n      required: true,\n    },\n    decimals: {\n      description: \"Decimal places for diff display\",\n      type: \"number\",\n      default: \"1\",\n    },\n    upIsPositive: {\n      description: \"Whether positive change is good (green) or bad (red)\",\n      type: \"boolean\",\n      default: \"true\",\n    },\n    label: {\n      description: \"Context label for the change (e.g., 'vs Q3')\",\n      type: \"string\",\n    },\n  }}\n/>\n\n## StatSparkline Schema\n\n<TypeTable\n  type={{\n    data: {\n      description: \"Array of numeric values (minimum 2)\",\n      type: \"number[]\",\n      required: true,\n    },\n    color: {\n      description: \"Custom stroke color\",\n      type: \"string\",\n    },\n  }}\n/>\n\n## Accessibility\n\n- All values are rendered as text, accessible to screen readers\n- Sparklines are decorative SVG with appropriate hiding from assistive tech\n\n## Related\n\n- [Chart](/docs/chart): both visualize numeric data, but Chart is better for time-series and multi-axis comparisons\n- [Weather Widget](/docs/weather-widget): another structured data display with a specialized visual format\n"
  },
  {
    "path": "apps/www/app/docs/stats-display/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Stats Display\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\"Stats Display\", \"Display key metrics in a grid\");\n}\n"
  },
  {
    "path": "apps/www/app/docs/stats-display/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"Stats Display\",\n  description: \"Display key metrics in a grid\",\n};\n\nexport const revalidate = 3600;\n\nexport default function StatsDisplayDocsPage() {\n  return <ComponentDocsTabs docs={<Content />} componentId=\"stats-display\" />;\n}\n"
  },
  {
    "path": "apps/www/app/docs/terminal/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\n\n<DocsHeader\n  title=\"Terminal\"\n  description=\"Show command-line output and logs.\"\n  mdxPath=\"app/docs/terminal/content.mdx\"\n/>\n\nTerminal renders command output with ANSI colors, exit codes, and stderr separation preserved. Long output can be collapsed with `maxCollapsedLines`.\n\n**Role:** Information. For displaying data without user input. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"terminal\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nRender `Terminal` in your UI with tool-compatible props.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\nimport { Terminal } from \"@/components/tool-ui/terminal\";\n\nexport function MyComponent() {\n  return (\n    <Terminal\n      id=\"my-terminal-example\"\n      command=\"ls -la\"\n      stdout={`total 16\ndrwxr-xr-x  3 user user 4096 Dec  3 10:23 .\ndrwxr-xr-x 12 user user 4096 Dec  3 10:20 ..\n-rw-r--r--  1 user user  220 Dec  3 10:23 package.json`}\n      exitCode={0}\n      durationMs={45}\n      cwd=\"~/project\"\n    />\n  );\n}\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegistration tells assistant-ui which component to render when a tool returns command output. Without it, tool results appear as raw JSON.\n\n```tsx\n// Backend tool\nimport { tool, jsonSchema } from \"ai\";\n\nconst showTerminal = tool({\n  description: \"Show a command result\",\n  inputSchema: jsonSchema<{}>({\n    type: \"object\",\n    properties: {},\n    additionalProperties: false,\n  }),\n  async execute() {\n    return {\n      id: \"terminal-1\",\n      command: \"pnpm test\",\n      cwd: \"~/project\",\n      exitCode: 0,\n      durationMs: 1288,\n      stdout: \"\\u001b[32m✓\\u001b[0m 42 tests passed\",\n    };\n  },\n});\n\n// Frontend with assistant-ui\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { Terminal } from \"@/components/tool-ui/terminal\";\nimport { safeParseSerializableTerminal } from \"@/components/tool-ui/terminal/schema\";\n\nexport const toolkit: Toolkit = {\n  showTerminal: {\n    type: \"backend\",\n    render: ({ result }) => {\n      const parsed = safeParseSerializableTerminal(result);\n      if (!parsed) {\n        return null;\n      }\n      return <Terminal {...parsed} />;\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"Paintbrush\" title=\"ANSI color support\">\n    Renders colored output exactly as it appears in your shell\n  </Feature>\n  <Feature icon=\"CircleCheck\" title=\"Exit code display\">\n    Green for success (0), red for any non-zero code\n  </Feature>\n  <Feature icon=\"Timer\" title=\"Duration tracking\">\n    Badge showing how long the command took\n  </Feature>\n  <Feature icon=\"AlertCircle\" title=\"Stdout/Stderr separation\">\n    Stderr rendered separately with distinct styling\n  </Feature>\n</FeatureGrid>\n\n## Props\n\n### Core\n\n<TypeTable\n  type={{\n    command: {\n      description: \"The command that was executed\",\n      type: \"string\",\n      required: true,\n    },\n    stdout: {\n      description: \"Standard output content\",\n      type: \"string\",\n    },\n    stderr: {\n      description: \"Standard error content\",\n      type: \"string\",\n    },\n    exitCode: {\n      description: \"Process exit code (0 = success)\",\n      type: \"number\",\n      required: true,\n    },\n  }}\n/>\n\n### Metadata\n\n<TypeTable\n  type={{\n    durationMs: {\n      description: \"Command execution time in milliseconds\",\n      type: \"number\",\n    },\n    cwd: {\n      description: \"Working directory where command ran\",\n      type: \"string\",\n    },\n    truncated: {\n      description: \"Indicates output was truncated\",\n      type: \"boolean\",\n    },\n  }}\n/>\n\n### Display Options\n\n<TypeTable\n  type={{\n    maxCollapsedLines: {\n      description: \"Auto-collapse if output exceeds this many lines\",\n      type: \"number\",\n    },\n    expanded: {\n      description: \"Controlled expand state (for collapsible mode)\",\n      type: \"boolean\",\n    },\n    defaultExpanded: {\n      description: \"Initial expand state (for collapsible mode)\",\n      type: \"boolean\",\n    },\n    onExpandedChange: {\n      description: \"Called when expand state changes\",\n      type: \"(expanded: boolean) => void\",\n    },\n  }}\n/>\n\n### Standard Props\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for this component\",\n      type: \"string\",\n      required: true,\n    },\n    className: {\n      description: \"Additional CSS classes\",\n      type: \"string\",\n    },\n  }}\n/>\n\nUse [`ToolUI.LocalActions`](/docs/actions) to compose external actions next to `Terminal`.\n\n## Accessibility\n\n- ANSI colors maintain WCAG contrast ratios against both light and dark backgrounds\n- Screen readers announce the command, exit status, and output content\n- Collapsible sections are keyboard-accessible with proper ARIA attributes\n\n## Related\n\n- [Code Block](/docs/code-block): both show monospaced content, but CodeBlock is for source code while Terminal is for command execution results\n- [Progress Tracker](/docs/progress-tracker): for when you need real-time status updates during a long-running operation\n"
  },
  {
    "path": "apps/www/app/docs/terminal/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Terminal\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\"Terminal\", \"Show command-line output and logs\");\n}\n"
  },
  {
    "path": "apps/www/app/docs/terminal/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"Terminal\",\n  description: \"Show command-line output and logs\",\n};\n\nexport const revalidate = 3600;\n\nexport default function TerminalDocsPage() {\n  return <ComponentDocsTabs docs={<Content />} componentId=\"terminal\" />;\n}\n"
  },
  {
    "path": "apps/www/app/docs/video/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\nimport { Video } from \"@/components/tool-ui/video\";\n\n<DocsHeader\n  title=\"Video\"\n  description=\"Video playback with controls and poster.\"\n  mdxPath=\"app/docs/video/content.mdx\"\n/>\n\nPasting a video URL into a chat gives the user a clickable link and nothing else. No preview, no playback, no context. Video renders an inline player with a poster image, native controls, and metadata so the user can watch without leaving the conversation.\n\n**Role:** Information. For displaying data without user input. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"video\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nRender `Video` in your UI with tool-compatible props.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\nimport { Video } from \"@/components/tool-ui/video\";\n\nexport function Example() {\n  return (\n    <Video\n      id=\"video-example\"\n      assetId=\"sample-video\"\n      src=\"https://samplelib.com/lib/preview/mp4/sample-5s.mp4\"\n      poster=\"https://images.unsplash.com/photo-1518770660439-4636190af475\"\n      title=\"The GUI moment\"\n      description=\"From command lines to windows, icons, menus, and pointers.\"\n      ratio=\"16:9\"\n      durationMs={128000}\n    />\n  );\n}\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegistration tells assistant-ui which component to render when a tool returns video data. Without it, tool results appear as raw JSON.\n\n```tsx\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { Video } from \"@/components/tool-ui/video\";\nimport { safeParseSerializableVideo } from \"@/components/tool-ui/video/schema\";\n\nexport const toolkit: Toolkit = {\n  showVideo: {\n    type: \"backend\",\n    render: ({ result }) => {\n      const parsed = safeParseSerializableVideo(result);\n      if (!parsed) {\n        return null;\n      }\n      return <Video {...parsed} />;\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"Play\" title=\"Muted autoplay\">\n    Starts muted for inline previews so it won't surprise the user\n  </Feature>\n  <Feature icon=\"Image\" title=\"Poster image\">\n    Shows a thumbnail before the first play\n  </Feature>\n  <Feature icon=\"Video\" title=\"Native controls\">\n    Uses the browser's built-in play, pause, seek, and fullscreen\n  </Feature>\n  <Feature icon=\"AspectRatio\" title=\"Aspect ratio control\">\n    Locks to auto, 1:1, 4:3, 16:9, or 9:16\n  </Feature>\n</FeatureGrid>\n\n## Props\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for this tool UI instance\",\n      type: \"string\",\n      required: true,\n    },\n    assetId: {\n      description: \"Persistent asset identifier\",\n      type: \"string\",\n      required: true,\n    },\n    src: {\n      description: \"Video source URL\",\n      type: \"string\",\n      required: true,\n    },\n    poster: { description: \"Poster/thumbnail URL\", type: \"string\" },\n    title: { description: \"Title text\", type: \"string\" },\n    description: { description: \"Description\", type: \"string\" },\n    ratio: {\n      description: \"Aspect ratio\",\n      type: \"'auto' | '1:1' | '4:3' | '16:9' | '9:16'\",\n      default: \"'16:9'\",\n    },\n    fit: {\n      description: \"Object fit mode\",\n      type: \"'cover' | 'contain'\",\n      default: \"'cover'\",\n    },\n    durationMs: { description: \"Duration in milliseconds\", type: \"number\" },\n    autoPlay: {\n      description: \"Auto-play video (muted)\",\n      type: \"boolean\",\n      default: \"true\",\n    },\n    defaultMuted: {\n      description: \"Initial mute state\",\n      type: \"boolean\",\n      default: \"true\",\n    },\n    href: {\n      description: \"Makes video clickable with this URL\",\n      type: \"string\",\n    },\n    domain: {\n      description: \"Source domain label shown on the video\",\n      type: \"string\",\n    },\n    source: {\n      description: \"Source attribution\",\n      type: \"{ label: string; iconUrl?: string; url?: string }\",\n    },\n    createdAt: { description: \"Creation timestamp (ISO 8601)\", type: \"string\" },\n    onNavigate: {\n      description: \"Called when user clicks the video link\",\n      type: \"(href: string, video: SerializableVideo) => void\",\n    },\n    onMediaEvent: {\n      description: \"Called on play, pause, mute, or unmute\",\n      type: \"(type: 'play' | 'pause' | 'mute' | 'unmute') => void\",\n    },\n  }}\n/>\n\nUse [`ToolUI.LocalActions`](/docs/actions) to compose external actions next to `Video`.\n\n## Related\n\n- [Audio](/docs/audio): the same inline media pattern for audio content\n- [Image](/docs/image): for static visual content with metadata and attribution\n- [Image Gallery](/docs/image-gallery): for presenting multiple visual assets in a grid\n"
  },
  {
    "path": "apps/www/app/docs/video/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Video\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\"Video\", \"Video playback with controls and poster\");\n}\n"
  },
  {
    "path": "apps/www/app/docs/video/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"Video\",\n  description: \"Video playback with controls and poster\",\n};\n\nexport const revalidate = 3600;\n\nexport default function VideoDocsPage() {\n  return <ComponentDocsTabs docs={<Content />} componentId=\"video\" />;\n}\n"
  },
  {
    "path": "apps/www/app/docs/weather-widget/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\nimport { WeatherWidget } from \"@/components/tool-ui/weather-widget/runtime\";\n\n<DocsHeader\n  title=\"Weather Widget\"\n  description=\"The most overbuilt weather widget you'll ever see. On purpose.\"\n  mdxPath=\"app/docs/weather-widget/content.mdx\"\n/>\n\nWeatherWidget renders current conditions and a multi-day forecast with WebGL atmospheric effects, time-of-day scene rendering, and 13 condition codes.\n\n**Role:** Information. For displaying data without user input. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"weather-widget\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nRender `WeatherWidget` in your UI with tool-compatible props.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\nimport { WeatherWidget } from \"@/components/tool-ui/weather-widget/runtime\";\n\nexport function Example() {\n  return (\n    <WeatherWidget\n      version=\"3.1\"\n      id=\"weather-widget-example\"\n      location={{ name: \"Kansas City, MO\" }}\n      units={{ temperature: \"fahrenheit\" }}\n      current={{\n        temperature: 72,\n        tempMin: 65,\n        tempMax: 78,\n        conditionCode: \"thunderstorm\",\n      }}\n      forecast={[\n        { label: \"Tue\", tempMin: 62, tempMax: 75, conditionCode: \"heavy-rain\" },\n        { label: \"Wed\", tempMin: 58, tempMax: 70, conditionCode: \"rain\" },\n        { label: \"Thu\", tempMin: 55, tempMax: 68, conditionCode: \"cloudy\" },\n        {\n          label: \"Fri\",\n          tempMin: 52,\n          tempMax: 72,\n          conditionCode: \"partly-cloudy\",\n        },\n        { label: \"Sat\", tempMin: 58, tempMax: 76, conditionCode: \"clear\" },\n      ]}\n      time={{ localTimeOfDay: 22 / 24 }}\n      updatedAt=\"2026-01-28T22:00:00Z\"\n    />\n  );\n}\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegistration tells assistant-ui which component to render when a tool named `get_weather` returns data. Without it, tool results appear as raw JSON.\n\n```tsx\n\"use client\";\n\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { z } from \"zod\";\nimport {\n  WeatherWidget,\n  type WeatherWidgetProps,\n} from \"@/components/tool-ui/weather-widget/runtime\";\n\nconst WeatherWidgetPayloadSchema = z.object({}).passthrough();\n\nfunction safeParseWeatherWidgetPayload(\n  input: unknown,\n): WeatherWidgetProps | null {\n  const result = WeatherWidgetPayloadSchema.safeParse(input);\n  if (\n    !result.success ||\n    typeof result.data !== \"object\" ||\n    result.data === null\n  ) {\n    return null;\n  }\n  return {\n    version: \"3.1\",\n    ...(result.data as Omit<WeatherWidgetProps, \"version\">),\n  };\n}\n\nexport const toolkit: Toolkit = {\n  get_weather: {\n    description: \"Display current weather and forecast for a location\",\n    parameters: WeatherWidgetPayloadSchema,\n    render: ({ args, toolCallId }) => {\n      const parsedArgs = safeParseWeatherWidgetPayload({\n        version: \"3.1\",\n        ...(args as Record<string, unknown>),\n        id: args?.id ?? `weather-${toolCallId}`,\n      });\n      if (!parsedArgs) {\n        return null;\n      }\n      return <WeatherWidget {...parsedArgs} />;\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Coding Agent Prompt\n\nCopy this prompt into your coding agent (Claude Code, Cursor, etc.) and it will wire `WeatherWidget` to whatever weather API you use.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```text\nIntegrate the Tool UI WeatherWidget with a live weather API.\n\nStep 1 — Discover the weather API:\n- Check the codebase for an existing weather API integration (look for API keys, env vars, fetch calls to weather services).\n- If one exists, use that provider. If not, ask the user which weather API they want to use.\n\nStep 2 — Build the adapter:\nCreate a provider adapter module (e.g. weatherAdapter.ts) that:\n- Fetches current conditions and forecast data from the chosen provider\n- Maps provider-specific weather codes to WeatherConditionCode with an explicit lookup table and a sensible fallback\n- Derives localTimeOfDay (0..1) from the location's local time\n- Returns a validated WeatherWidgetProps payload\n\nThe payload must match WeatherWidgetProps exactly:\n- version: \"3.1\"\n- id: stable id string\n- location: { name }\n- units: { temperature: \"celsius\" | \"fahrenheit\" }\n- current: { temperature, tempMin, tempMax, conditionCode, windSpeed?, precipitationLevel?, visibility? }\n- forecast: 1–7 items of { label, tempMin, tempMax, conditionCode }\n- time: { localTimeOfDay } where localTimeOfDay is 0..1 based on location local time\n- updatedAt: ISO timestamp\n\nValid WeatherConditionCode values:\nclear | partly-cloudy | cloudy | overcast | fog | drizzle | rain | heavy-rain | thunderstorm | snow | sleet | hail | windy\n\nStep 3 — Register the tool:\nRegister a get_weather tool renderer so tool output renders WeatherWidget (not raw JSON).\nKeep the existing runtime import:\n  import { WeatherWidget } from \"@/components/tool-ui/weather-widget/runtime\";\n\nAdditional requirements:\n- Add lightweight runtime validation before rendering (zod safeParse + fallback behavior).\n- Keep all API keys/secrets server-side only.\n- Add tests for condition code mapping and payload shape.\n```\n\n</div>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"CloudSun\" title=\"Weather conditions\">\n    13 condition codes because someone had to distinguish sleet from hail\n  </Feature>\n  <Feature icon=\"MapPin\" title=\"Location display\">\n    City name, current temperature, and today's high/low range\n  </Feature>\n  <Feature icon=\"Thermometer\" title=\"Temperature units\">\n    Celsius or Fahrenheit via a single config flag\n  </Feature>\n  <Feature icon=\"Calendar\" title=\"Multi-day forecast\">\n    1 to 7 day forecasts with daily highs, lows, and condition icons\n  </Feature>\n</FeatureGrid>\n\n## Props\n\n<TypeTable\n  type={{\n    id: {\n      description: \"Unique identifier for this tool UI instance\",\n      type: \"string\",\n      required: true,\n    },\n    location: {\n      description: \"Location object\",\n      type: \"{ name: string }\",\n      required: true,\n    },\n    units: {\n      description: \"Display units\",\n      type: '{ temperature: \"celsius\" | \"fahrenheit\" }',\n      required: true,\n    },\n    current: {\n      description: \"Current weather conditions\",\n      type: \"CurrentWeather\",\n      required: true,\n    },\n    forecast: {\n      description: \"Daily forecast array (1-7 days)\",\n      type: \"ForecastDay[]\",\n      required: true,\n    },\n    time: {\n      description: \"Deterministic scene time source\",\n      type: \"{ timeBucket?: 0..11; localTimeOfDay?: 0..1 }\",\n      required: true,\n    },\n    updatedAt: {\n      description: \"Last update timestamp (ISO 8601)\",\n      type: \"string\",\n    },\n  }}\n/>\n\n### CurrentWeather\n\n<TypeTable\n  type={{\n    temperature: {\n      description: \"Current temperature\",\n      type: \"number\",\n      required: true,\n    },\n    tempMin: {\n      description: \"Today's low temperature\",\n      type: \"number\",\n      required: true,\n    },\n    tempMax: {\n      description: \"Today's high temperature\",\n      type: \"number\",\n      required: true,\n    },\n    conditionCode: {\n      description: \"Weather condition\",\n      type: \"WeatherConditionCode\",\n      required: true,\n    },\n    windSpeed: {\n      description: \"Wind speed value\",\n      type: \"number\",\n    },\n    precipitationLevel: {\n      description: \"Precipitation intensity\",\n      type: \"'none' | 'light' | 'moderate' | 'heavy'\",\n    },\n    visibility: {\n      description: \"Visibility distance value\",\n      type: \"number\",\n    },\n  }}\n/>\n\n### ForecastDay\n\n<TypeTable\n  type={{\n    label: {\n      description: \"Day label (e.g., 'Mon', 'Tuesday')\",\n      type: \"string\",\n      required: true,\n    },\n    tempMin: {\n      description: \"Daily low temperature\",\n      type: \"number\",\n      required: true,\n    },\n    tempMax: {\n      description: \"Daily high temperature\",\n      type: \"number\",\n      required: true,\n    },\n    conditionCode: {\n      description: \"Weather condition\",\n      type: \"WeatherConditionCode\",\n      required: true,\n    },\n  }}\n/>\n\n### WeatherConditionCode\n\nAvailable condition values with their corresponding icons:\n\n| Condition       | Icon           | Description    |\n| --------------- | -------------- | -------------- |\n| `clear`         | Sun            | Clear skies    |\n| `partly-cloudy` | CloudSun       | Partly cloudy  |\n| `cloudy`        | Cloud          | Cloudy         |\n| `overcast`      | Cloud          | Overcast skies |\n| `fog`           | CloudFog       | Fog            |\n| `drizzle`       | CloudDrizzle   | Light rain     |\n| `rain`          | CloudRain      | Rain           |\n| `heavy-rain`    | CloudRain      | Heavy rain     |\n| `thunderstorm`  | CloudLightning | Thunderstorm   |\n| `snow`          | Snowflake      | Snow           |\n| `sleet`         | CloudHail      | Sleet          |\n| `hail`          | CloudHail      | Hail           |\n| `windy`         | Wind           | Windy          |\n\n## Accessibility\n\n- Forecast list uses proper `role=\"list\"` and `role=\"listitem\"` structure\n- Temperature values include screen reader labels with unit names\n- Condition icons are decorative (`aria-hidden`) with text labels provided\n\n## Related\n\n- [Stats Display](/docs/stats-display): for structured numeric data that is not weather-specific\n- [Image](/docs/image): WeatherWidget includes visual scene rendering; use Image when the visual itself is the content\n"
  },
  {
    "path": "apps/www/app/docs/weather-widget/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Weather Widget\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\n    \"Weather Widget\",\n    \"Display weather conditions and forecasts\",\n  );\n}\n"
  },
  {
    "path": "apps/www/app/docs/weather-widget/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"Weather Widget\",\n  description: \"Display weather conditions and forecasts\",\n};\n\nexport const revalidate = 3600;\n\nexport default function WeatherWidgetDocsPage() {\n  return <ComponentDocsTabs docs={<Content />} componentId=\"weather-widget\" />;\n}\n"
  },
  {
    "path": "apps/www/app/docs/x-post/content.mdx",
    "content": "import { DocsHeader } from \"../_components/docs-header\";\nimport { XPost } from \"@/components/tool-ui/x-post\";\n\n<DocsHeader\n  title=\"X Post\"\n  description=\"Platform-native post card for X (Twitter).\"\n  mdxPath=\"app/docs/x-post/content.mdx\"\n/>\n\nA link to a tweet tells you nothing until you click it. XPost embeds the full post inline (author, text, media, quoted posts, and engagement stats) styled to match the platform. Use it when the assistant fetches or drafts X content and you want it displayed as a real post.\n\n**Role:** Information. For displaying data the user reads. See [Design Guidelines](/docs/design-guidelines) for how component roles work.\n\n## Getting Started\n\n<Steps>\n\n<Step title=\"Install the component\">\n\nRun this once from your project root.\n\n<InstallCommandBlock componentId=\"x-post\" variant=\"block\" />\n\n</Step>\n\n<Step title=\"Use it in your application\">\n\nRender `XPost` in your UI with tool-compatible props.\n\n<div className=\"[&_pre]:max-h-[30rem] [&_pre]:overflow-y-auto\">\n\n```tsx\nimport { XPost } from \"@/components/tool-ui/x-post\";\n\nexport function Example() {\n  return (\n    <XPost\n      post={{\n        id: \"x-post-1\",\n        author: {\n          name: \"Taylor Kim\",\n          handle: \"taylorkim\",\n          avatarUrl: \"https://example.com/avatar.jpg\",\n          verified: true,\n        },\n        text: \"Thinking about how to make Tool UIs feel more collaborative.\",\n        stats: { likes: 12 },\n        createdAt: \"2025-01-05T12:00:00.000Z\",\n      }}\n    />\n  );\n}\n```\n\n</div>\n\n</Step>\n\n<Step title=\"Register it in your runtime provider\">\n\nRegister this renderer so tool results display as `XPost`.\n\n```tsx\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { XPost } from \"@/components/tool-ui/x-post\";\nimport { safeParseSerializableXPost } from \"@/components/tool-ui/x-post/schema\";\n\nexport const toolkit: Toolkit = {\n  showXPost: {\n    type: \"backend\",\n    render: ({ result }) => {\n      const parsed = safeParseSerializableXPost(result);\n      if (!parsed) {\n        return null;\n      }\n      return <XPost post={parsed} />;\n    },\n  },\n};\n```\n\n</Step>\n\n</Steps>\n\n## Key Features\n\n<FeatureGrid>\n  <Feature icon=\"Paintbrush\" title=\"Platform-native styling\">\n    Uses X's layout, typography, and color scheme\n  </Feature>\n  <Feature icon=\"MessageSquare\" title=\"Rich post content\">\n    Text, quoted posts, media attachments, and link previews\n  </Feature>\n  <Feature icon=\"MousePointerClick\" title=\"External actions\">\n    Add actions like \"Save Draft\" or \"Share\" via ToolUI action surfaces\n  </Feature>\n  <Feature icon=\"ShieldCheck\" title=\"Safe links\">\n    External URLs open safely with built-in navigation helpers\n  </Feature>\n</FeatureGrid>\n\n## Props\n\n<TypeTable\n  type={{\n    post: {\n      description: \"Post payload validated by `SerializableXPostSchema`\",\n      type: \"XPostData\",\n      required: true,\n    },\n    className: {\n      description: \"Optional className for the outer card\",\n      type: \"string\",\n    },\n    onAction: {\n      description: \"Fires when the user triggers a built-in post action\",\n      type: \"(action: string, post: XPostData) => void\",\n    },\n  }}\n/>\n\nUse [`ToolUI.LocalActions`](/docs/actions) to compose external actions next to `XPost`.\n\n## Related\n\n- [Instagram Post](/docs/instagram-post): platform-native card for Instagram content\n- [LinkedIn Post](/docs/linkedin-post): platform-native card for LinkedIn content\n"
  },
  {
    "path": "apps/www/app/docs/x-post/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - X Post\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\"X Post\", \"Render X post previews\");\n}\n"
  },
  {
    "path": "apps/www/app/docs/x-post/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Content from \"./content.mdx\";\nimport { ComponentDocsTabs } from \"../_components/component-docs-tabs\";\n\nexport const metadata: Metadata = {\n  title: \"X Post\",\n  description: \"Render X post previews\",\n};\n\nexport const revalidate = 3600;\n\nexport default function XPostDocsPage() {\n  return <ComponentDocsTabs docs={<Content />} componentId=\"x-post\" />;\n}\n"
  },
  {
    "path": "apps/www/app/global-error.tsx",
    "content": "\"use client\";\n\nexport default function GlobalError({\n  error,\n  reset,\n}: {\n  error: Error & { digest?: string };\n  reset: () => void;\n}) {\n  return (\n    <html lang=\"en\">\n      <body>\n        <div\n          style={{\n            display: \"flex\",\n            flexDirection: \"column\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            minHeight: \"100vh\",\n            fontFamily: \"system-ui, sans-serif\",\n            padding: \"2rem\",\n            textAlign: \"center\",\n          }}\n        >\n          <h1 style={{ fontSize: \"2rem\", marginBottom: \"1rem\" }}>\n            Something went wrong\n          </h1>\n          <p style={{ color: \"#666\", marginBottom: \"1.5rem\" }}>\n            {error.message || \"An unexpected error occurred\"}\n          </p>\n          <button\n            onClick={reset}\n            style={{\n              padding: \"0.75rem 1.5rem\",\n              backgroundColor: \"#000\",\n              color: \"#fff\",\n              border: \"none\",\n              borderRadius: \"0.5rem\",\n              cursor: \"pointer\",\n              fontSize: \"1rem\",\n            }}\n          >\n            Try again\n          </button>\n        </div>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/layout.tsx",
    "content": "import \"./styles/globals.css\";\nimport \"leaflet/dist/leaflet.css\";\nimport type { ReactNode } from \"react\";\nimport { GeistSans } from \"geist/font/sans\";\nimport { GeistMono } from \"geist/font/mono\";\nimport { PostHogInit } from \"@/app/components/analytics/posthog-init.client\";\nimport { ThemeProvider } from \"@/app/components/theme/theme-provider\";\nimport { MobileNavSheetGate } from \"@/app/components/layout/mobile-nav-sheet-gate.client\";\n\nconst isProduction = process.env.NODE_ENV === \"production\";\nconst title = isProduction ? \"Tool UI\" : \"Tool UI — Dev\";\nconst description = \"UI components for AI interfaces\";\n\nexport const metadata = {\n  title,\n  description,\n};\n\nexport const viewport = {\n  width: \"device-width\",\n  initialScale: 1,\n  viewportFit: \"cover\",\n};\n\nexport default function RootLayout({ children }: { children: ReactNode }) {\n  return (\n    <html\n      lang=\"en\"\n      className={`${GeistSans.variable} ${GeistMono.variable} bg-background`}\n      suppressHydrationWarning\n    >\n      <body className=\"bg-background overscroll-none\">\n        <div id=\"app-root\" className=\"flex h-screen h-svh flex-col\">\n          <ThemeProvider\n            attribute=\"class\"\n            defaultTheme=\"light\"\n            disableTransitionOnChange\n          >\n            {children}\n            <MobileNavSheetGate />\n            <PostHogInit />\n          </ThemeProvider>\n        </div>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/not-found.tsx",
    "content": "import Link from \"next/link\";\n\nexport default function NotFound() {\n  return (\n    <div className=\"flex min-h-screen flex-col items-center justify-center p-8 text-center\">\n      <h1 className=\"mb-4 text-4xl font-bold\">404</h1>\n      <p className=\"text-muted-foreground mb-6\">\n        The page you&apos;re looking for doesn&apos;t exist.\n      </p>\n      <Link\n        href=\"/\"\n        className=\"bg-primary text-primary-foreground rounded-lg px-6 py-3 transition-opacity hover:opacity-90\"\n      >\n        Go Home\n      </Link>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - UI components for AI interfaces\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\"Tool UI\", \"UI components for AI interfaces\");\n}\n"
  },
  {
    "path": "apps/www/app/page.tsx",
    "content": "import ContentLayout from \"@/app/components/layout/page-shell\";\nimport { AnimatedHeaderFrame } from \"@/app/components/layout/app-shell-animated.client\";\nimport { ThemeToggle } from \"@/app/components/builder/theme-toggle\";\nimport { HomeHero } from \"@/app/components/home/home-hero\";\nimport { HomeBackground } from \"@/app/components/home/home-background\";\nimport { FauxChatShellMobileAnimated } from \"@/app/components/home/faux-chat-shell-mobile-animated\";\nimport { FauxChatShellAnimated } from \"@/app/components/home/faux-chat-shell-animated\";\n\nexport default function HomePage() {\n  return (\n    <AnimatedHeaderFrame\n      rightContent={<ThemeToggle />}\n      background={<HomeBackground />}\n    >\n      <ContentLayout>\n        <main className=\"relative flex h-full max-h-[800px] min-h-0 w-full max-w-[1440px] flex-col justify-end gap-10 overflow-x-clip md:p-6 lg:flex-row\">\n          <div className=\"relative z-10 flex w-full max-w-[500px] flex-col justify-end pb-[calc(2.75rem+env(safe-area-inset-bottom,0px))] pl-6 md:pb-[10vh] lg:max-w-[40w] lg:min-w-[400x] lg:shrink lg:grow-0 lg:basis-[40w]\">\n            <HomeHero />\n          </div>\n\n          <div\n            className=\"pointer-events-none absolute inset-0 z-[5] md:hidden\"\n            style={{\n              background:\n                \"linear-gradient(to top, var(--color-background) 0%, transparent 100%)\",\n            }}\n            aria-hidden=\"true\"\n          />\n\n          <div className=\"absolute inset-0 flex h-full min-h-0 w-full min-w-0 translate-x-[45%] -translate-y-12 scale-[0.7] items-center justify-end sm:translate-x-[10%] sm:scale-[0.85] md:translate-x-0 md:translate-y-0 md:scale-100 lg:relative lg:flex-1 lg:justify-center\">\n            <div className=\"block h-full w-full max-w-[430px] lg:hidden\">\n              <FauxChatShellMobileAnimated />\n            </div>\n            <div className=\"hidden h-full w-full lg:block\">\n              <FauxChatShellAnimated />\n            </div>\n          </div>\n        </main>\n      </ContentLayout>\n    </AnimatedHeaderFrame>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/playground/chat-pane.tsx",
    "content": "\"use client\";\n\nimport {\n  useMemo,\n  useCallback,\n  useImperativeHandle,\n  forwardRef,\n  useEffect,\n} from \"react\";\nimport {\n  AssistantRuntimeProvider,\n  Tools,\n  ThreadPrimitive,\n  useAui,\n  useAuiState,\n} from \"@assistant-ui/react\";\nimport {\n  AssistantChatTransport,\n  useChatRuntime,\n} from \"@assistant-ui/react-ai-sdk\";\nimport type {\n  ExportedMessageRepository,\n  MessageFormatAdapter,\n  MessageFormatItem,\n  MessageFormatRepository,\n  Toolkit,\n  ThreadHistoryAdapter,\n} from \"@assistant-ui/react\";\nimport {\n  lastAssistantMessageIsCompleteWithToolCalls,\n  type UIMessage,\n} from \"ai\";\n\nimport type { Prototype } from \"@/lib/playground\";\nimport { PROTOTYPE_SLUG_HEADER } from \"@/lib/playground/constants\";\nimport { AssistantMessage, Composer, UserMessage } from \"./chat-ui\";\nimport { SelectFrequentLocationTool } from \"@/lib/playground/prototypes/waymo/select-frequent-location-tool\";\nimport {\n  SelectDestinationTool,\n  SelectPickupTool,\n  GetRideQuoteTool,\n  GetTripStatusTool,\n} from \"@/lib/playground/prototypes/waymo-v2\";\n\nconst THREAD_STORAGE_KEY_PREFIX = \"playground:thread:\";\n\nconst WAYMO_BOOKING_TOOLKIT: Toolkit = {\n  select_frequent_location: SelectFrequentLocationTool,\n};\n\nconst WAYMO_V2_TOOLKIT: Toolkit = {\n  select_destination: SelectDestinationTool,\n  select_pickup: SelectPickupTool,\n  get_ride_quote: GetRideQuoteTool,\n  get_trip_status: GetTripStatusTool,\n};\n\nconst getThreadStorageKey = (slug: string) =>\n  `${THREAD_STORAGE_KEY_PREFIX}${slug}`;\n\nconst readThreadRepo = (slug: string): ExportedMessageRepository => {\n  if (typeof window === \"undefined\") {\n    return { headId: null, messages: [] };\n  }\n\n  const raw = window.localStorage.getItem(getThreadStorageKey(slug));\n  if (!raw) {\n    return { headId: null, messages: [] };\n  }\n\n  try {\n    const parsed = JSON.parse(raw) as ExportedMessageRepository;\n    return {\n      headId: parsed.headId ?? null,\n      messages: parsed.messages ?? [],\n    };\n  } catch (error) {\n    console.warn(\"Failed to parse stored playground thread\", error);\n    return { headId: null, messages: [] };\n  }\n};\n\nconst writeThreadRepo = (slug: string, repo: ExportedMessageRepository) => {\n  if (typeof window === \"undefined\") {\n    return;\n  }\n  try {\n    window.localStorage.setItem(\n      getThreadStorageKey(slug),\n      JSON.stringify(repo),\n    );\n  } catch (error) {\n    console.warn(\"Failed to persist playground thread\", error);\n  }\n};\n\nconst createLocalStorageHistoryAdapter = (\n  slug: string,\n): ThreadHistoryAdapter => {\n  const read = () => readThreadRepo(slug);\n  const write = (repo: ExportedMessageRepository) =>\n    writeThreadRepo(slug, repo);\n\n  const upsertMessage = (\n    repo: ExportedMessageRepository,\n    item: ExportedMessageRepository[\"messages\"][number],\n  ): ExportedMessageRepository => {\n    const existingIndex = repo.messages.findIndex(\n      (entry) => entry.message.id === item.message.id,\n    );\n\n    if (existingIndex >= 0) {\n      // Merge the updated message while preserving existing data\n      repo.messages[existingIndex] = {\n        ...repo.messages[existingIndex],\n        ...item,\n        message: {\n          ...repo.messages[existingIndex].message,\n          ...item.message,\n        },\n      };\n    } else {\n      repo.messages.push(item);\n    }\n\n    return repo;\n  };\n\n  return {\n    async load() {\n      return read();\n    },\n\n    async append(item) {\n      if (typeof window === \"undefined\") {\n        return;\n      }\n\n      const repo = read();\n      const next = upsertMessage(repo, item);\n      next.headId = item.message.id;\n      write(next);\n    },\n\n    withFormat<TMessage, TStorageFormat>(\n      formatAdapter: MessageFormatAdapter<TMessage, TStorageFormat>,\n    ) {\n      return {\n        async load(): Promise<MessageFormatRepository<TMessage>> {\n          const repo = read();\n          return {\n            headId: repo.headId,\n            messages: repo.messages.map((entry) => {\n              const storageEntry = {\n                id: entry.message.id,\n                parent_id: entry.parentId,\n                format: formatAdapter.format,\n                content: formatAdapter.encode({\n                  parentId: entry.parentId,\n                  message: entry.message as unknown as TMessage,\n                }),\n              };\n              return formatAdapter.decode(storageEntry);\n            }),\n          };\n        },\n\n        async append(item: MessageFormatItem<TMessage>) {\n          if (typeof window === \"undefined\") {\n            return;\n          }\n\n          const repo = read();\n          const exportedItem: ExportedMessageRepository[\"messages\"][number] = {\n            parentId: item.parentId,\n            message:\n              item.message as unknown as ExportedMessageRepository[\"messages\"][number][\"message\"],\n          };\n          const next = upsertMessage(repo, exportedItem);\n          next.headId = exportedItem.message.id;\n          write(next);\n        },\n      };\n    },\n  };\n};\n\nexport type ChatPaneProps = {\n  prototype: Prototype;\n};\n\nexport type ChatPaneRef = {\n  resetThread: () => void;\n};\n\nconst createTransportForPrototype = (slug: string) =>\n  new AssistantChatTransport({\n    api: \"/api/playground/chat\",\n    headers: async () => ({\n      [PROTOTYPE_SLUG_HEADER]: slug,\n    }),\n  });\n\n// Component to sync tool call results to localStorage\nconst ToolResultPersistence = ({ slug }: { slug: string }) => {\n  const messages = useAuiState(({ thread }) => thread.messages);\n\n  useEffect(() => {\n    if (!messages || messages.length === 0) return;\n\n    // Only update assistant messages with tool calls in storage\n    const repo = readThreadRepo(slug);\n    let hasChanges = false;\n\n    for (const runtimeMsg of messages) {\n      if (runtimeMsg.role !== \"assistant\") continue;\n\n      // Find this message in storage\n      const storedMsgIndex = repo.messages.findIndex(\n        (entry) => entry.message.id === runtimeMsg.id,\n      );\n\n      if (storedMsgIndex === -1) continue;\n\n      const storedMsg = repo.messages[storedMsgIndex].message;\n\n      // Stored messages use \"parts\", runtime messages use \"content\"\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      const storedParts = (storedMsg as any).parts;\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      const runtimeParts = (runtimeMsg as any).content;\n\n      if (\n        storedMsg.role === \"assistant\" &&\n        Array.isArray(storedParts) &&\n        Array.isArray(runtimeParts)\n      ) {\n        // Update tool call results by matching toolCallId\n        for (const runtimePart of runtimeParts) {\n          if (\n            runtimePart &&\n            typeof runtimePart === \"object\" &&\n            \"type\" in runtimePart &&\n            runtimePart.type === \"tool-call\" &&\n            \"result\" in runtimePart &&\n            \"toolCallId\" in runtimePart\n          ) {\n            // Find matching part by toolCallId in stored message\n            // Stored parts use specific tool names like \"tool-select_frequent_location\"\n            const storedPart = storedParts.find(\n              // eslint-disable-next-line @typescript-eslint/no-explicit-any\n              (part: any) =>\n                part?.type?.startsWith?.(\"tool-\") &&\n                part?.toolCallId === runtimePart.toolCallId,\n            );\n\n            if (storedPart) {\n              // Stored parts use \"output\" instead of \"result\"\n              // eslint-disable-next-line @typescript-eslint/no-explicit-any\n              const storedResult = (storedPart as any).output;\n              const runtimeResult = runtimePart.result;\n\n              if (\n                JSON.stringify(storedResult) !== JSON.stringify(runtimeResult)\n              ) {\n                // eslint-disable-next-line @typescript-eslint/no-explicit-any\n                (storedPart as any).output = runtimeResult;\n                hasChanges = true;\n              }\n            }\n          }\n        }\n      }\n    }\n\n    if (hasChanges) {\n      writeThreadRepo(slug, repo);\n    }\n  }, [messages, slug]);\n\n  return null;\n};\n\nexport const ChatPane = forwardRef<ChatPaneRef, ChatPaneProps>(\n  ({ prototype }, ref) => {\n    const { slug, title } = prototype;\n\n    const transport = useMemo(() => createTransportForPrototype(slug), [slug]);\n\n    const historyAdapter = useMemo(\n      () => createLocalStorageHistoryAdapter(slug),\n      [slug],\n    );\n\n    const seedMessages = useMemo<UIMessage[]>(() => {\n      const repo = readThreadRepo(slug);\n      return (repo.messages ?? []).map(\n        (entry) => entry.message as unknown as UIMessage,\n      );\n    }, [slug]);\n\n    const runtime = useChatRuntime({\n      transport,\n      messages: seedMessages,\n      adapters: { history: historyAdapter },\n      sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,\n    });\n    const toolkit = useMemo<Toolkit>(() => {\n      if (slug === \"waymo-booking\") return WAYMO_BOOKING_TOOLKIT;\n      if (slug === \"waymo-v2\") return WAYMO_V2_TOOLKIT;\n      return {};\n    }, [slug]);\n    const aui = useAui({\n      tools: Tools({ toolkit }),\n    });\n\n    const resetThread = useCallback(() => {\n      if (typeof window === \"undefined\") {\n        return;\n      }\n\n      // Clear localStorage\n      window.localStorage.removeItem(getThreadStorageKey(slug));\n\n      // Reset runtime to empty state\n      runtime.switchToNewThread();\n    }, [slug, runtime]);\n\n    useImperativeHandle(\n      ref,\n      () => ({\n        resetThread,\n      }),\n      [resetThread],\n    );\n\n    return (\n      <AssistantRuntimeProvider runtime={runtime} aui={aui}>\n        {/* Auto-persist tool result updates to localStorage */}\n        <ToolResultPersistence slug={slug} />\n\n        <ThreadPrimitive.Root className=\"flex flex-1 flex-col overflow-hidden\">\n          <ThreadPrimitive.Viewport className=\"flex flex-1 flex-col overflow-y-auto px-6 py-6\">\n            <ThreadPrimitive.If empty>\n              <div className=\"text-muted-foreground mx-auto flex max-w-lg flex-1 flex-col items-center justify-center gap-3 text-center\">\n                <p className=\"text-base font-medium\">Start exploring {title}</p>\n                <p className=\"text-sm\">\n                  Describe a task or ask a question to see how this tool\n                  collection responds.\n                </p>\n              </div>\n            </ThreadPrimitive.If>\n            <ThreadPrimitive.Messages\n              components={{\n                UserMessage,\n                AssistantMessage,\n              }}\n            />\n          </ThreadPrimitive.Viewport>\n\n          <Composer />\n        </ThreadPrimitive.Root>\n      </AssistantRuntimeProvider>\n    );\n  },\n);\n\nChatPane.displayName = \"ChatPane\";\n"
  },
  {
    "path": "apps/www/app/playground/chat-ui.tsx",
    "content": "\"use client\";\n\nimport {\n  ActionBarPrimitive,\n  ComposerPrimitive,\n  MessagePrimitive,\n  ThreadPrimitive,\n} from \"@assistant-ui/react\";\nimport { Button } from \"@/components/ui/button\";\nimport { MarkdownText } from \"@/app/components/assistant-ui/markdown-text\";\nimport { ToolFallback } from \"@/app/components/assistant-ui/tool-fallback\";\n\nconst actionBarClassName = \"text-muted-foreground flex gap-2 text-xs\";\n\nexport const UserMessage = () => (\n  <MessagePrimitive.Root className=\"mx-auto w-full max-w-2xl py-3\">\n    <div className=\"ml-auto flex max-w-[85%] flex-col items-end gap-2\">\n      <div className=\"bg-primary text-primary-foreground rounded-2xl px-4 py-2\">\n        <MessagePrimitive.Content components={{ Text: MarkdownText }} />\n      </div>\n      <UserActionBar />\n    </div>\n  </MessagePrimitive.Root>\n);\n\nexport const AssistantMessage = () => (\n  <MessagePrimitive.Root className=\"mx-auto w-full max-w-2xl py-3\">\n    <div className=\"flex max-w-[85%] flex-col gap-3\">\n      <MessagePrimitive.Content\n        components={{\n          Text: MarkdownText,\n          tools: {\n            Fallback: ToolFallback,\n          },\n        }}\n      />\n      <AssistantActionBar />\n    </div>\n  </MessagePrimitive.Root>\n);\n\nconst UserActionBar = () => (\n  <ActionBarPrimitive.Root className={actionBarClassName}>\n    {/* <ActionBarPrimitive.Edit asChild>\n      <Button variant=\"ghost\" size=\"sm\">\n        Edit\n      </Button>\n    </ActionBarPrimitive.Edit>\n    <ActionBarPrimitive.Copy asChild>\n      <Button variant=\"ghost\" size=\"sm\">\n        Copy\n      </Button>\n    </ActionBarPrimitive.Copy> */}\n  </ActionBarPrimitive.Root>\n);\n\nconst AssistantActionBar = () => (\n  <ActionBarPrimitive.Root\n    hideWhenRunning\n    autohide=\"not-last\"\n    className={actionBarClassName}\n  >\n    {/* <ActionBarPrimitive.Copy asChild>\n      <Button variant=\"ghost\" size=\"sm\">\n        Copy\n      </Button>\n    </ActionBarPrimitive.Copy> */}\n    {/* <ActionBarPrimitive.Reload asChild>\n      <Button variant=\"ghost\" size=\"sm\">\n        Retry\n      </Button>\n    </ActionBarPrimitive.Reload> */}\n  </ActionBarPrimitive.Root>\n);\n\nexport const Composer = () => (\n  <ComposerPrimitive.Root className=\"border-input bg-background flex w-full flex-col rounded-3xl border px-1 pt-2 shadow-sm transition-all\">\n    <ComposerPrimitive.Input\n      placeholder=\"Send a message...\"\n      className=\"placeholder:text-muted-foreground max-h-48 min-h-16 w-full resize-none bg-transparent px-4 pb-3 text-base outline-none\"\n      rows={1}\n      autoFocus\n    />\n    <div className=\"mx-3 mb-2 flex items-center justify-end gap-2\">\n      <ThreadPrimitive.If running>\n        <ComposerPrimitive.Cancel asChild>\n          <Button variant=\"outline\" size=\"sm\">\n            Stop\n          </Button>\n        </ComposerPrimitive.Cancel>\n      </ThreadPrimitive.If>\n      <ThreadPrimitive.If running={false}>\n        <ComposerPrimitive.Send asChild>\n          <Button size=\"sm\">Send</Button>\n        </ComposerPrimitive.Send>\n      </ThreadPrimitive.If>\n    </div>\n  </ComposerPrimitive.Root>\n);\n"
  },
  {
    "path": "apps/www/app/playground/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Playground\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\"Playground\", \"Experiment with components live\");\n}\n"
  },
  {
    "path": "apps/www/app/playground/page.tsx",
    "content": "\"use client\";\n\nimport { ChatPane } from \"./chat-pane\";\nimport type { ChatPaneRef } from \"./chat-pane\";\nimport {\n  Suspense,\n  useEffect,\n  useMemo,\n  useState,\n  useRef,\n  useCallback,\n} from \"react\";\nimport { useRouter, useSearchParams } from \"next/navigation\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport { RotateCcw, Copy, Check } from \"lucide-react\";\nimport { listPrototypes } from \"@/lib/playground\";\nimport type { Prototype } from \"@/lib/playground\";\nimport { ToolInspector } from \"./tool-inspector\";\n\nconst PROTOTYPES = listPrototypes();\n\nconst getInitialSlug = (slugParam: string | null, prototypes: Prototype[]) => {\n  if (\n    slugParam &&\n    prototypes.some((prototype) => prototype.slug === slugParam)\n  ) {\n    return slugParam;\n  }\n  return prototypes[0]?.slug ?? null;\n};\n\nconst PlaygroundContent = () => {\n  const searchParams = useSearchParams();\n  const router = useRouter();\n  const slugParam = searchParams.get(\"slug\");\n\n  const [activeSlug, setActiveSlug] = useState<string | null>(() =>\n    getInitialSlug(slugParam, PROTOTYPES),\n  );\n  const [inspectorOpen, setInspectorOpen] = useState(false);\n  const [copiedState, setCopiedState] = useState(false);\n  const chatPaneRef = useRef<ChatPaneRef>(null);\n\n  useEffect(() => {\n    if (slugParam === null) {\n      return;\n    }\n    const nextSlug = getInitialSlug(slugParam, PROTOTYPES);\n    setActiveSlug((current) => (current === nextSlug ? current : nextSlug));\n  }, [slugParam]);\n\n  const resolvedSlug = activeSlug ?? PROTOTYPES[0]?.slug ?? \"\";\n\n  const activePrototype = useMemo(\n    () =>\n      PROTOTYPES.find((prototype) => prototype.slug === resolvedSlug) ?? null,\n    [resolvedSlug],\n  );\n\n  const copyChatState = useCallback(() => {\n    if (typeof window === \"undefined\") {\n      return;\n    }\n\n    const storageKey = `playground:thread:${resolvedSlug}`;\n    const raw = window.localStorage.getItem(storageKey);\n\n    let stateToShare: string;\n\n    if (!raw) {\n      stateToShare = JSON.stringify(\n        {\n          prototype: activePrototype?.title ?? resolvedSlug,\n          slug: resolvedSlug,\n          messages: [],\n          note: \"No messages in thread yet\",\n        },\n        null,\n        2,\n      );\n    } else {\n      try {\n        const parsed = JSON.parse(raw);\n        stateToShare = JSON.stringify(\n          {\n            prototype: activePrototype?.title ?? resolvedSlug,\n            slug: resolvedSlug,\n            systemPrompt: activePrototype?.systemPrompt,\n            tools: activePrototype?.tools?.map((tool) => ({\n              name: tool.name,\n              description: tool.description,\n            })),\n            ...parsed,\n          },\n          null,\n          2,\n        );\n      } catch {\n        stateToShare = JSON.stringify(\n          {\n            prototype: activePrototype?.title ?? resolvedSlug,\n            slug: resolvedSlug,\n            error: \"Failed to parse chat state\",\n            rawState: raw,\n          },\n          null,\n          2,\n        );\n      }\n    }\n\n    navigator.clipboard\n      .writeText(stateToShare)\n      .then(() => {\n        setCopiedState(true);\n        setTimeout(() => setCopiedState(false), 2000);\n      })\n      .catch((error) => {\n        console.error(\"Failed to copy chat state\", error);\n      });\n  }, [resolvedSlug, activePrototype]);\n\n  useEffect(() => {\n    if (!activePrototype) {\n      return;\n    }\n    if (slugParam === activePrototype.slug) {\n      return;\n    }\n    if (typeof window === \"undefined\") {\n      return;\n    }\n    const nextSearch = new URLSearchParams(window.location.search);\n    nextSearch.set(\"slug\", activePrototype.slug);\n    router.replace(`?${nextSearch.toString()}`, { scroll: false });\n  }, [slugParam, activePrototype, router]);\n\n  if (PROTOTYPES.length === 0 || !activePrototype) {\n    return (\n      <div className=\"bg-background text-foreground flex min-h-screen items-center justify-center px-6 py-12\">\n        <Card className=\"max-w-xl\">\n          <CardHeader>\n            <CardTitle>Playground</CardTitle>\n            <CardDescription>\n              Add a prototype definition to `lib/playground/registry.ts` to get\n              started.\n            </CardDescription>\n          </CardHeader>\n          <CardContent className=\"text-muted-foreground space-y-3 text-sm\">\n            <p>Example entry:</p>\n            <pre className=\"bg-muted text-muted-foreground rounded-lg p-3 text-xs\">\n              {`const prototype: Prototype = {\n  slug: \"my-prototype\",\n  title: \"My Prototype\",\n  systemPrompt: \"You are a helpful assistant...\",\n  tools: [{ name: \"my_tool\", description: \"Does something\", execute: mockTool({ ok: true }) }],\n};`}\n            </pre>\n          </CardContent>\n        </Card>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"bg-background text-foreground flex h-screen overflow-hidden\">\n      <aside className=\"border-border/60 bg-muted/30 flex w-64 flex-col overflow-hidden border-r\">\n        <div className=\"border-border/60 border-b px-4 py-5\">\n          <h1 className=\"text-lg font-semibold\">Tool UI Playground</h1>\n        </div>\n        <div className=\"flex-1 overflow-y-auto px-2 py-4\">\n          <div className=\"flex flex-col gap-2\">\n            {PROTOTYPES.map((prototype) => {\n              const isActive = prototype.slug === activePrototype.slug;\n              return (\n                <Button\n                  key={prototype.slug}\n                  variant={isActive ? \"secondary\" : \"ghost\"}\n                  className=\"w-full justify-start py-4\"\n                  onClick={() => setActiveSlug(prototype.slug)}\n                >\n                  <div className=\"flex flex-col items-start\">\n                    <span className=\"text-sm font-medium\">\n                      {prototype.title}\n                    </span>\n                  </div>\n                </Button>\n              );\n            })}\n          </div>\n        </div>\n      </aside>\n      <main className=\"flex flex-1 flex-col overflow-hidden\">\n        <header className=\"border-border bg-background/95 flex items-center justify-between border-b px-6 py-4\">\n          <div>\n            <h2 className=\"text-xl font-semibold\">{activePrototype.title}</h2>\n            {activePrototype.summary ? (\n              <p className=\"text-muted-foreground text-sm\">\n                {activePrototype.summary}\n              </p>\n            ) : null}\n          </div>\n          <div className=\"flex items-center gap-2\">\n            <Button\n              variant=\"outline\"\n              size=\"icon\"\n              onClick={copyChatState}\n              title=\"Copy chat state for debugging\"\n            >\n              {copiedState ? (\n                <Check className=\"h-4 w-4\" />\n              ) : (\n                <Copy className=\"h-4 w-4\" />\n              )}\n            </Button>\n            <Button\n              variant=\"outline\"\n              size=\"icon\"\n              onClick={() => chatPaneRef.current?.resetThread()}\n              title=\"Reset thread\"\n            >\n              <RotateCcw className=\"h-4 w-4\" />\n            </Button>\n            <Button variant=\"outline\" onClick={() => setInspectorOpen(true)}>\n              View tools\n            </Button>\n          </div>\n        </header>\n        <div className=\"flex flex-1 flex-col overflow-hidden\">\n          <ChatPane\n            key={activePrototype.slug}\n            ref={chatPaneRef}\n            prototype={activePrototype}\n          />\n        </div>\n      </main>\n      <ToolInspector\n        open={inspectorOpen}\n        onOpenChange={setInspectorOpen}\n        tools={activePrototype.tools}\n        prototypeTitle={activePrototype.title}\n      />\n    </div>\n  );\n};\n\nconst PlaygroundPage = () => (\n  <Suspense\n    fallback={\n      <div className=\"bg-background text-foreground flex min-h-screen items-center justify-center px-6 py-12\">\n        <Card className=\"max-w-xl\">\n          <CardHeader>\n            <CardTitle>Tool UI Playground</CardTitle>\n            <CardDescription>Loading playground…</CardDescription>\n          </CardHeader>\n        </Card>\n      </div>\n    }\n  >\n    <PlaygroundContent />\n  </Suspense>\n);\n\nexport default PlaygroundPage;\n"
  },
  {
    "path": "apps/www/app/playground/tool-inspector.tsx",
    "content": "\"use client\";\n\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { Item, ItemContent, ItemGroup, ItemTitle } from \"@/components/ui/item\";\nimport type { Tool } from \"@/lib/playground\";\n\nconst ToolItem = ({ tool }: { tool: Tool }) => (\n  <Item variant=\"outline\" className=\"shadow-none\">\n    <ItemContent>\n      <ItemTitle className=\"text-base font-semibold\">{tool.name}</ItemTitle>\n      <div className=\"mt-1 flex flex-wrap gap-4 text-sm\">\n        <span className=\"text-muted-foreground\">\n          UI: {tool.uiId ?? \"fallback\"}\n        </span>\n        <span className=\"text-muted-foreground\">\n          Input schema: {tool.input ? \"provided\" : \"—\"}\n        </span>\n        <span className=\"text-muted-foreground\">\n          Output schema: {tool.output ? \"provided\" : \"—\"}\n        </span>\n      </div>\n    </ItemContent>\n  </Item>\n);\n\nexport const ToolInspector = ({\n  open,\n  onOpenChange,\n  tools,\n  prototypeTitle,\n}: {\n  open: boolean;\n  onOpenChange: (next: boolean) => void;\n  tools: Tool[];\n  prototypeTitle: string;\n}) => (\n  <Dialog open={open} onOpenChange={onOpenChange}>\n    <DialogContent className=\"max-w-3xl\">\n      <DialogHeader>\n        <DialogTitle>{prototypeTitle} tools</DialogTitle>\n        <DialogDescription>\n          Review the tools available to this prototype. Assignments are\n          code-defined for now.\n        </DialogDescription>\n      </DialogHeader>\n      <div className=\"max-h-[60vh] overflow-y-auto pr-2\">\n        <ItemGroup className=\"space-y-3\">\n          {tools.map((tool) => (\n            <ToolItem key={tool.name} tool={tool} />\n          ))}\n        </ItemGroup>\n      </div>\n    </DialogContent>\n  </Dialog>\n);\n"
  },
  {
    "path": "apps/www/app/playground/waymo-demo/opengraph-image.tsx",
    "content": "import {\n  generateOgImage,\n  size as ogSize,\n  contentType as ogContentType,\n} from \"@/lib/og/og-image\";\n\nexport const runtime = \"nodejs\";\nexport const alt = \"Tool UI - Waymo Demo\";\nexport const size = ogSize;\nexport const contentType = ogContentType;\n\nexport default async function Image() {\n  return generateOgImage(\"Waymo Demo\", \"Interactive ride-sharing prototype\");\n}\n"
  },
  {
    "path": "apps/www/app/playground/waymo-demo/page.tsx",
    "content": "import { WaymoDemo } from \"@/lib/playground/prototypes/waymo\";\n\nexport default function WaymoDemoPage() {\n  return <WaymoDemo />;\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/celestial-effect/celestial-canvas.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useRef, useCallback } from \"react\";\n\n// =============================================================================\n// TYPES\n// =============================================================================\n\ninterface CelestialCanvasProps {\n  className?: string;\n  // Time\n  timeOfDay?: number;\n  animateTime?: boolean;\n  dayDuration?: number;\n  // Sun\n  sunSize?: number;\n  sunGlowIntensity?: number;\n  sunGlowSize?: number;\n  sunRaysEnabled?: boolean;\n  sunRayCount?: number;\n  sunRayLength?: number;\n  // Moon\n  moonSize?: number;\n  moonPhase?: number;\n  moonGlowIntensity?: number;\n  moonGlowSize?: number;\n  showCraters?: boolean;\n  craterDetail?: number;\n  // Sky\n  horizonOffset?: number;\n  atmosphereThickness?: number;\n  starDensity?: number;\n  // Debug\n  showPath?: boolean;\n  debug?: boolean;\n}\n\ninterface UniformLocations {\n  u_time: WebGLUniformLocation | null;\n  u_resolution: WebGLUniformLocation | null;\n  u_timeOfDay: WebGLUniformLocation | null;\n  u_sunSize: WebGLUniformLocation | null;\n  u_sunGlowIntensity: WebGLUniformLocation | null;\n  u_sunGlowSize: WebGLUniformLocation | null;\n  u_sunRaysEnabled: WebGLUniformLocation | null;\n  u_sunRayCount: WebGLUniformLocation | null;\n  u_sunRayLength: WebGLUniformLocation | null;\n  u_moonSize: WebGLUniformLocation | null;\n  u_moonPhase: WebGLUniformLocation | null;\n  u_moonGlowIntensity: WebGLUniformLocation | null;\n  u_moonGlowSize: WebGLUniformLocation | null;\n  u_showCraters: WebGLUniformLocation | null;\n  u_craterDetail: WebGLUniformLocation | null;\n  u_horizonOffset: WebGLUniformLocation | null;\n  u_atmosphereThickness: WebGLUniformLocation | null;\n  u_starDensity: WebGLUniformLocation | null;\n  u_showPath: WebGLUniformLocation | null;\n  u_debug: WebGLUniformLocation | null;\n  u_moonTexture: WebGLUniformLocation | null;\n  u_hasMoonTexture: WebGLUniformLocation | null;\n}\n\n// =============================================================================\n// SHADER: VERTEX\n// =============================================================================\n\nconst VERTEX_SHADER = /* glsl */ `#version 300 es\nin vec4 a_position;\nout vec2 v_uv;\n\nvoid main() {\n  gl_Position = a_position;\n  v_uv = a_position.xy * 0.5 + 0.5;\n}\n`;\n\n// =============================================================================\n// SHADER: FRAGMENT - GLSL functions organized by category\n// =============================================================================\n\nconst GLSL_CONSTANTS = /* glsl */ `\n#define PI 3.14159265359\n#define TAU 6.28318530718\n`;\n\nconst GLSL_UNIFORMS = /* glsl */ `\nuniform float u_time;\nuniform vec2 u_resolution;\nuniform float u_timeOfDay;\nuniform float u_sunSize;\nuniform float u_sunGlowIntensity;\nuniform float u_sunGlowSize;\nuniform bool u_sunRaysEnabled;\nuniform int u_sunRayCount;\nuniform float u_sunRayLength;\nuniform float u_moonSize;\nuniform float u_moonPhase;\nuniform float u_moonGlowIntensity;\nuniform float u_moonGlowSize;\nuniform bool u_showCraters;\nuniform float u_craterDetail;\nuniform float u_horizonOffset;\nuniform float u_atmosphereThickness;\nuniform float u_starDensity;\nuniform bool u_showPath;\nuniform bool u_debug;\nuniform sampler2D u_moonTexture;\nuniform bool u_hasMoonTexture;\n`;\n\nconst GLSL_NOISE = /* glsl */ `\nfloat hash(vec2 p) {\n  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nfloat hash3(vec3 p) {\n  return fract(sin(dot(p, vec3(127.1, 311.7, 74.7))) * 43758.5453123);\n}\n\nvec2 hash2(vec2 p) {\n  p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));\n  return fract(sin(p) * 43758.5453);\n}\n\nfloat noise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  f = f * f * (3.0 - 2.0 * f);\n\n  float a = hash(i);\n  float b = hash(i + vec2(1.0, 0.0));\n  float c = hash(i + vec2(0.0, 1.0));\n  float d = hash(i + vec2(1.0, 1.0));\n\n  return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);\n}\n\nfloat fbm(vec2 p, int octaves) {\n  float value = 0.0;\n  float amplitude = 0.5;\n  float frequency = 1.0;\n\n  for (int i = 0; i < 6; i++) {\n    if (i >= octaves) break;\n    value += amplitude * noise(p * frequency);\n    frequency *= 2.0;\n    amplitude *= 0.5;\n  }\n\n  return value;\n}\n`;\n\nconst GLSL_CELESTIAL_POSITIONS = /* glsl */ `\nvec2 getSunPosition(float timeOfDay, float horizonOffset) {\n  float angle = (timeOfDay - 0.25) * 2.0 * PI;\n  float x = 0.5 - cos(angle) * 0.4;\n  float y = horizonOffset + sin(angle) * 0.45;\n  return vec2(x, y);\n}\n\nvec2 getMoonPosition(float timeOfDay, float horizonOffset) {\n  float moonTime = fract(timeOfDay + 0.5);\n  float angle = (moonTime - 0.25) * 2.0 * PI;\n  float x = 0.5 - cos(angle) * 0.4;\n  float y = horizonOffset + sin(angle) * 0.45;\n  return vec2(x, y);\n}\n\nfloat getSunVisibility(float timeOfDay) {\n  float rise = smoothstep(0.2, 0.3, timeOfDay);\n  float set = smoothstep(0.8, 0.7, timeOfDay);\n  return rise * set;\n}\n\nfloat getMoonVisibility(float timeOfDay) {\n  float nightStart = smoothstep(0.7, 0.85, timeOfDay);\n  float nightEnd = smoothstep(0.3, 0.15, timeOfDay);\n  return max(nightStart, nightEnd);\n}\n`;\n\nconst GLSL_SKY = /* glsl */ `\nvec3 getSkyColor(vec2 uv, float timeOfDay, float atmosphereThickness) {\n  vec3 dayTop = vec3(0.4, 0.6, 0.9);\n  vec3 dayHorizon = vec3(0.7, 0.8, 0.95);\n\n  vec3 sunsetTop = vec3(0.2, 0.2, 0.4);\n  vec3 sunsetHorizon = vec3(0.9, 0.5, 0.2);\n\n  vec3 nightTop = vec3(0.02, 0.02, 0.05);\n  vec3 nightHorizon = vec3(0.05, 0.05, 0.1);\n\n  float dayAmount = smoothstep(0.25, 0.4, timeOfDay) * smoothstep(0.75, 0.6, timeOfDay);\n  float sunsetAmount = max(\n    smoothstep(0.2, 0.3, timeOfDay) * smoothstep(0.4, 0.3, timeOfDay),\n    smoothstep(0.6, 0.7, timeOfDay) * smoothstep(0.8, 0.7, timeOfDay)\n  );\n  float nightAmount = max(0.0, 1.0 - dayAmount - sunsetAmount);\n\n  float gradientFactor = pow(1.0 - uv.y, atmosphereThickness);\n\n  vec3 topColor = dayTop * dayAmount + sunsetTop * sunsetAmount + nightTop * nightAmount;\n  vec3 horizonColor = dayHorizon * dayAmount + sunsetHorizon * sunsetAmount + nightHorizon * nightAmount;\n\n  return mix(topColor, horizonColor, gradientFactor);\n}\n`;\n\nconst GLSL_STARS = /* glsl */ `\nfloat drawStars(vec2 uv, float density, float time) {\n  float stars = 0.0;\n\n  for (int layer = 0; layer < 3; layer++) {\n    float layerScale = 100.0 + float(layer) * 50.0;\n    vec2 gridUV = uv * layerScale;\n    vec2 gridID = floor(gridUV);\n    vec2 gridFract = fract(gridUV);\n\n    vec2 starOffset = hash2(gridID + float(layer) * 100.0);\n    vec2 starPos = starOffset;\n\n    float dist = length(gridFract - starPos);\n\n    float starPresent = step(1.0 - density * 0.3, hash(gridID * (float(layer) + 1.0)));\n\n    float starSize = 0.02 + hash(gridID.yx) * 0.03;\n\n    float twinkle = sin(time * (2.0 + hash(gridID) * 3.0) + hash(gridID.yx) * TAU) * 0.3 + 0.7;\n\n    float star = smoothstep(starSize, 0.0, dist) * starPresent * twinkle;\n\n    star *= 1.0 - float(layer) * 0.3;\n\n    stars += star;\n  }\n\n  return stars;\n}\n`;\n\nconst GLSL_SUN = /* glsl */ `\nvec3 drawSun(vec2 uv, vec2 sunPos, float size, float glowIntensity, float glowSize,\n             bool showRays, int rayCount, float rayLength, float time) {\n  vec2 aspect = vec2(u_resolution.x / u_resolution.y, 1.0);\n  vec2 diff = (uv - sunPos) * aspect;\n  float dist = length(diff);\n\n  float disc = 1.0 - smoothstep(size * 0.9, size, dist);\n\n  vec3 sunCore = vec3(1.0, 1.0, 0.95);\n  vec3 sunEdge = vec3(1.0, 0.9, 0.4);\n  float edgeFactor = clamp(dist / size, 0.0, 1.0);\n  vec3 sunColor = mix(sunCore, sunEdge, edgeFactor);\n\n  float limbDarkening = 1.0 - pow(clamp(dist / size, 0.0, 1.0), 2.0) * 0.3;\n  sunColor *= limbDarkening;\n\n  float scaledDist = dist / (glowSize * 0.1);\n  float glow1 = exp(-scaledDist * 8.0) * glowIntensity * 0.5;\n  float glow2 = exp(-scaledDist * 3.0) * glowIntensity * 0.3;\n  float glow3 = exp(-scaledDist * 1.5) * glowIntensity * 0.15;\n\n  vec3 glowColor = vec3(1.0, 0.8, 0.4);\n\n  float rays = 0.0;\n  if (showRays && dist > size * 0.5) {\n    float angle = atan(diff.y, diff.x);\n    float rayPattern = sin(angle * float(rayCount) + time * 0.5) * 0.5 + 0.5;\n    rayPattern = pow(rayPattern, 3.0);\n\n    float rayFalloff = exp(-dist / (rayLength * 0.5));\n    rays = rayPattern * rayFalloff * 0.4 * glowIntensity;\n  }\n\n  vec3 result = sunColor * disc * 2.0;\n  result += glowColor * (glow1 + glow2 + glow3);\n  result += vec3(1.0, 0.9, 0.6) * rays;\n\n  return result;\n}\n`;\n\nconst GLSL_MOON = /* glsl */ `\nvec3 getSphereNormal(vec2 discUV) {\n  float r2 = dot(discUV, discUV);\n  if (r2 > 1.0) return vec3(0.0);\n  float z = sqrt(1.0 - r2);\n  return normalize(vec3(discUV.x, discUV.y, z));\n}\n\nvec2 sphereToEquirectangular(vec3 normal) {\n  float longitude = atan(normal.x, normal.z);\n  float u = longitude / TAU + 0.5;\n\n  float latitude = asin(clamp(normal.y, -1.0, 1.0));\n  float v = latitude / PI + 0.5;\n\n  return vec2(u, v);\n}\n\nvec3 getMoonSurfaceColor(vec3 normal, vec2 discUV) {\n  if (u_hasMoonTexture) {\n    vec2 texUV = sphereToEquirectangular(normal);\n    vec3 texColor = texture(u_moonTexture, texUV).rgb;\n    return texColor;\n  }\n\n  float brightness = 0.7 + fbm(discUV * 5.0, 3) * 0.3;\n  return vec3(brightness * 0.85, brightness * 0.83, brightness * 0.8);\n}\n\nvec4 drawMoon(vec2 uv, vec2 moonPos, float size, float phase, float glowIntensity,\n              float glowSize, bool showCraters, float craterDetail, float time) {\n  vec2 aspect = vec2(u_resolution.x / u_resolution.y, 1.0);\n  vec2 diff = (uv - moonPos) * aspect;\n  float dist = length(diff);\n\n  vec2 discUV = diff / size;\n  float discDist = length(discUV);\n\n  float disc = 1.0 - smoothstep(0.95, 1.0, discDist);\n\n  if (disc < 0.001) {\n    float scaledDist = dist / (glowSize * 0.08);\n    float glow1 = exp(-scaledDist * 6.0) * glowIntensity * 0.25;\n    float glow2 = exp(-scaledDist * 2.0) * glowIntensity * 0.1;\n    vec3 glowColor = vec3(0.8, 0.85, 0.95);\n\n    float phaseAngle = phase * TAU;\n    vec3 sunDir = vec3(sin(phaseAngle), 0.0, -cos(phaseAngle));\n    float glowPhase = max(0.2, dot(normalize(vec3(discUV, 0.5)), sunDir) * 0.5 + 0.5);\n\n    return vec4(glowColor * (glow1 + glow2) * glowPhase, 0.0);\n  }\n\n  vec3 normal = getSphereNormal(discUV);\n\n  float phaseAngle = phase * TAU;\n  vec3 sunDir = vec3(sin(phaseAngle), 0.0, -cos(phaseAngle));\n\n  float NdotL = dot(normal, sunDir);\n\n  float terminator = smoothstep(-0.02, 0.08, NdotL);\n\n  vec3 baseColor = getMoonSurfaceColor(normal, discUV);\n\n  vec3 ambient = baseColor * 0.03;\n\n  vec3 lit = baseColor * terminator;\n\n  vec3 moonSurface = ambient + lit;\n\n  float limbDarkening = 1.0 - pow(discDist, 3.0) * 0.15;\n  moonSurface *= limbDarkening;\n\n  float rimLight = pow(1.0 - abs(NdotL), 4.0) * terminator * 0.1;\n  moonSurface += vec3(1.0, 0.98, 0.95) * rimLight;\n\n  float scaledDist = dist / (glowSize * 0.08);\n  float glow1 = exp(-scaledDist * 6.0) * glowIntensity * 0.2;\n  float glow2 = exp(-scaledDist * 2.0) * glowIntensity * 0.1;\n  vec3 glowColor = vec3(0.8, 0.85, 0.95);\n\n  float litAmount = max(0.1, terminator);\n\n  vec3 result = moonSurface;\n  vec3 glow = glowColor * (glow1 + glow2) * litAmount;\n\n  return vec4(result * disc + glow, disc);\n}\n`;\n\nconst GLSL_DEBUG = /* glsl */ `\nvec3 drawOrbitalPath(vec2 uv, float horizonOffset) {\n  vec2 aspect = vec2(u_resolution.x / u_resolution.y, 1.0);\n\n  vec2 arcCenter = vec2(0.5, horizonOffset);\n  vec2 diff = (uv - arcCenter) * aspect;\n\n  float ellipseX = diff.x / 0.4;\n  float ellipseY = diff.y / 0.45;\n  float ellipseDist = abs(length(vec2(ellipseX, ellipseY)) - 1.0);\n\n  float upperHalf = step(0.0, diff.y);\n  float path = smoothstep(0.02, 0.01, ellipseDist) * upperHalf;\n\n  float horizon = smoothstep(0.005, 0.002, abs(uv.y - horizonOffset));\n\n  return vec3(0.3, 0.3, 0.5) * path + vec3(0.5, 0.3, 0.3) * horizon;\n}\n`;\n\nconst GLSL_MAIN = /* glsl */ `\nvoid main() {\n  vec2 uv = v_uv;\n\n  vec3 color = getSkyColor(uv, u_timeOfDay, u_atmosphereThickness);\n\n  float nightFactor = getMoonVisibility(u_timeOfDay);\n  if (nightFactor > 0.01) {\n    float stars = drawStars(uv, u_starDensity, u_time);\n    color += vec3(stars) * nightFactor;\n  }\n\n  if (u_showPath) {\n    color += drawOrbitalPath(uv, u_horizonOffset);\n  }\n\n  float sunVis = getSunVisibility(u_timeOfDay);\n  if (sunVis > 0.01) {\n    vec2 sunPos = getSunPosition(u_timeOfDay, u_horizonOffset);\n    vec3 sun = drawSun(uv, sunPos, u_sunSize, u_sunGlowIntensity, u_sunGlowSize,\n                       u_sunRaysEnabled, u_sunRayCount, u_sunRayLength, u_time);\n    color += sun * sunVis;\n  }\n\n  float moonVis = getMoonVisibility(u_timeOfDay);\n  if (moonVis > 0.01) {\n    vec2 moonPos = getMoonPosition(u_timeOfDay, u_horizonOffset);\n    vec4 moon = drawMoon(uv, moonPos, u_moonSize, u_moonPhase, u_moonGlowIntensity,\n                         u_moonGlowSize, u_showCraters, u_craterDetail, u_time);\n    float alpha = moon.a * moonVis;\n    color = mix(color, moon.rgb, alpha) + moon.rgb * (1.0 - moon.a) * moonVis;\n  }\n\n  if (u_debug) {\n    if (uv.x < 0.1 && uv.y > 0.95) {\n      color = vec3(u_timeOfDay, 0.0, 0.0);\n    }\n  }\n\n  fragColor = vec4(color, 1.0);\n}\n`;\n\nconst FRAGMENT_SHADER = /* glsl */ `#version 300 es\nprecision highp float;\n\nin vec2 v_uv;\nout vec4 fragColor;\n\n${GLSL_CONSTANTS}\n${GLSL_UNIFORMS}\n${GLSL_NOISE}\n${GLSL_CELESTIAL_POSITIONS}\n${GLSL_SKY}\n${GLSL_STARS}\n${GLSL_SUN}\n${GLSL_MOON}\n${GLSL_DEBUG}\n${GLSL_MAIN}\n`;\n\n// =============================================================================\n// WEBGL UTILITIES\n// =============================================================================\n\nfunction createShader(\n  gl: WebGL2RenderingContext,\n  type: GLenum,\n  source: string,\n): WebGLShader | null {\n  const shader = gl.createShader(type);\n  if (!shader) return null;\n\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n    console.error(\"Shader compile error:\", gl.getShaderInfoLog(shader));\n    gl.deleteShader(shader);\n    return null;\n  }\n\n  return shader;\n}\n\nfunction createProgram(\n  gl: WebGL2RenderingContext,\n  vertexShader: WebGLShader,\n  fragmentShader: WebGLShader,\n): WebGLProgram | null {\n  const program = gl.createProgram();\n  if (!program) return null;\n\n  gl.attachShader(program, vertexShader);\n  gl.attachShader(program, fragmentShader);\n  gl.linkProgram(program);\n\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n    console.error(\"Program link error:\", gl.getProgramInfoLog(program));\n    gl.deleteProgram(program);\n    return null;\n  }\n\n  return program;\n}\n\nfunction getUniformLocations(\n  gl: WebGL2RenderingContext,\n  program: WebGLProgram,\n): UniformLocations {\n  const uniformNames: Array<keyof UniformLocations> = [\n    \"u_time\",\n    \"u_resolution\",\n    \"u_timeOfDay\",\n    \"u_sunSize\",\n    \"u_sunGlowIntensity\",\n    \"u_sunGlowSize\",\n    \"u_sunRaysEnabled\",\n    \"u_sunRayCount\",\n    \"u_sunRayLength\",\n    \"u_moonSize\",\n    \"u_moonPhase\",\n    \"u_moonGlowIntensity\",\n    \"u_moonGlowSize\",\n    \"u_showCraters\",\n    \"u_craterDetail\",\n    \"u_horizonOffset\",\n    \"u_atmosphereThickness\",\n    \"u_starDensity\",\n    \"u_showPath\",\n    \"u_debug\",\n    \"u_moonTexture\",\n    \"u_hasMoonTexture\",\n  ];\n\n  const locations = {} as UniformLocations;\n  for (const name of uniformNames) {\n    locations[name] = gl.getUniformLocation(program, name);\n  }\n  return locations;\n}\n\nfunction setupQuadGeometry(\n  gl: WebGL2RenderingContext,\n  program: WebGLProgram,\n): void {\n  const positions = new Float32Array([\n    -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1,\n  ]);\n\n  const positionBuffer = gl.createBuffer();\n  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);\n  gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);\n\n  const positionLoc = gl.getAttribLocation(program, \"a_position\");\n  gl.enableVertexAttribArray(positionLoc);\n  gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);\n}\n\nfunction loadMoonTexture(\n  gl: WebGL2RenderingContext,\n  onLoad: () => void,\n): WebGLTexture | null {\n  const texture = gl.createTexture();\n  if (!texture) return null;\n\n  gl.bindTexture(gl.TEXTURE_2D, texture);\n\n  gl.texImage2D(\n    gl.TEXTURE_2D,\n    0,\n    gl.RGBA,\n    1,\n    1,\n    0,\n    gl.RGBA,\n    gl.UNSIGNED_BYTE,\n    new Uint8Array([128, 128, 128, 255]),\n  );\n\n  const image = new Image();\n  image.crossOrigin = \"anonymous\";\n  image.onload = () => {\n    gl.bindTexture(gl.TEXTURE_2D, texture);\n    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);\n    gl.generateMipmap(gl.TEXTURE_2D);\n    gl.texParameteri(\n      gl.TEXTURE_2D,\n      gl.TEXTURE_MIN_FILTER,\n      gl.LINEAR_MIPMAP_LINEAR,\n    );\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n    onLoad();\n  };\n  image.src = \"/assets/moon-texture.jpg\";\n\n  return texture;\n}\n\nfunction updateCanvasSize(\n  canvas: HTMLCanvasElement,\n  gl: WebGL2RenderingContext,\n): void {\n  const displayWidth = canvas.clientWidth * window.devicePixelRatio;\n  const displayHeight = canvas.clientHeight * window.devicePixelRatio;\n\n  if (canvas.width !== displayWidth || canvas.height !== displayHeight) {\n    canvas.width = displayWidth;\n    canvas.height = displayHeight;\n    gl.viewport(0, 0, canvas.width, canvas.height);\n  }\n}\n\n// =============================================================================\n// COMPONENT\n// =============================================================================\n\nexport function CelestialCanvas({\n  className,\n  timeOfDay = 0.5,\n  animateTime = false,\n  dayDuration = 60,\n  sunSize = 0.08,\n  sunGlowIntensity = 1.0,\n  sunGlowSize = 3.0,\n  sunRaysEnabled = true,\n  sunRayCount = 12,\n  sunRayLength = 0.3,\n  moonSize = 0.06,\n  moonPhase = 0.5,\n  moonGlowIntensity = 0.6,\n  moonGlowSize = 2.5,\n  showCraters = true,\n  craterDetail = 0.5,\n  horizonOffset = 0.1,\n  atmosphereThickness = 1.0,\n  starDensity = 0.5,\n  showPath = false,\n  debug = false,\n}: CelestialCanvasProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const glRef = useRef<WebGL2RenderingContext | null>(null);\n  const programRef = useRef<WebGLProgram | null>(null);\n  const uniformsRef = useRef<UniformLocations | null>(null);\n  const animationFrameRef = useRef<number>(0);\n  const startTimeRef = useRef<number>(0);\n  const animatedTimeRef = useRef<number>(timeOfDay);\n  const moonTextureRef = useRef<WebGLTexture | null>(null);\n  const moonTextureLoadedRef = useRef<boolean>(false);\n\n  const propsRef = useRef({\n    timeOfDay,\n    animateTime,\n    dayDuration,\n    sunSize,\n    sunGlowIntensity,\n    sunGlowSize,\n    sunRaysEnabled,\n    sunRayCount,\n    sunRayLength,\n    moonSize,\n    moonPhase,\n    moonGlowIntensity,\n    moonGlowSize,\n    showCraters,\n    craterDetail,\n    horizonOffset,\n    atmosphereThickness,\n    starDensity,\n    showPath,\n    debug,\n  });\n\n  propsRef.current = {\n    timeOfDay,\n    animateTime,\n    dayDuration,\n    sunSize,\n    sunGlowIntensity,\n    sunGlowSize,\n    sunRaysEnabled,\n    sunRayCount,\n    sunRayLength,\n    moonSize,\n    moonPhase,\n    moonGlowIntensity,\n    moonGlowSize,\n    showCraters,\n    craterDetail,\n    horizonOffset,\n    atmosphereThickness,\n    starDensity,\n    showPath,\n    debug,\n  };\n\n  const initGL = useCallback(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return false;\n\n    const gl = canvas.getContext(\"webgl2\");\n    if (!gl) {\n      console.error(\"WebGL2 not supported\");\n      return false;\n    }\n    glRef.current = gl;\n\n    const vertexShader = createShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);\n    const fragmentShader = createShader(\n      gl,\n      gl.FRAGMENT_SHADER,\n      FRAGMENT_SHADER,\n    );\n    if (!vertexShader || !fragmentShader) return false;\n\n    const program = createProgram(gl, vertexShader, fragmentShader);\n    if (!program) return false;\n    programRef.current = program;\n\n    uniformsRef.current = getUniformLocations(gl, program);\n\n    moonTextureRef.current = loadMoonTexture(gl, () => {\n      moonTextureLoadedRef.current = true;\n    });\n\n    setupQuadGeometry(gl, program);\n\n    startTimeRef.current = performance.now();\n    return true;\n  }, []);\n\n  const render = useCallback(() => {\n    const gl = glRef.current;\n    const program = programRef.current;\n    const uniforms = uniformsRef.current;\n    const canvas = canvasRef.current;\n    const props = propsRef.current;\n\n    if (!gl || !program || !uniforms || !canvas) return;\n\n    updateCanvasSize(canvas, gl);\n\n    const time = (performance.now() - startTimeRef.current) / 1000;\n\n    if (props.animateTime) {\n      animatedTimeRef.current = (time / props.dayDuration) % 1.0;\n    } else {\n      animatedTimeRef.current = props.timeOfDay;\n    }\n\n    gl.useProgram(program);\n\n    gl.uniform1f(uniforms.u_time, time);\n    gl.uniform2f(uniforms.u_resolution, canvas.width, canvas.height);\n    gl.uniform1f(uniforms.u_timeOfDay, animatedTimeRef.current);\n    gl.uniform1f(uniforms.u_sunSize, props.sunSize);\n    gl.uniform1f(uniforms.u_sunGlowIntensity, props.sunGlowIntensity);\n    gl.uniform1f(uniforms.u_sunGlowSize, props.sunGlowSize);\n    gl.uniform1i(uniforms.u_sunRaysEnabled, props.sunRaysEnabled ? 1 : 0);\n    gl.uniform1i(uniforms.u_sunRayCount, props.sunRayCount);\n    gl.uniform1f(uniforms.u_sunRayLength, props.sunRayLength);\n    gl.uniform1f(uniforms.u_moonSize, props.moonSize);\n    gl.uniform1f(uniforms.u_moonPhase, props.moonPhase);\n    gl.uniform1f(uniforms.u_moonGlowIntensity, props.moonGlowIntensity);\n    gl.uniform1f(uniforms.u_moonGlowSize, props.moonGlowSize);\n    gl.uniform1i(uniforms.u_showCraters, props.showCraters ? 1 : 0);\n    gl.uniform1f(uniforms.u_craterDetail, props.craterDetail);\n    gl.uniform1f(uniforms.u_horizonOffset, props.horizonOffset);\n    gl.uniform1f(uniforms.u_atmosphereThickness, props.atmosphereThickness);\n    gl.uniform1f(uniforms.u_starDensity, props.starDensity);\n    gl.uniform1i(uniforms.u_showPath, props.showPath ? 1 : 0);\n    gl.uniform1i(uniforms.u_debug, props.debug ? 1 : 0);\n\n    gl.activeTexture(gl.TEXTURE0);\n    gl.bindTexture(gl.TEXTURE_2D, moonTextureRef.current);\n    gl.uniform1i(uniforms.u_moonTexture, 0);\n    gl.uniform1i(\n      uniforms.u_hasMoonTexture,\n      moonTextureLoadedRef.current ? 1 : 0,\n    );\n\n    gl.drawArrays(gl.TRIANGLES, 0, 6);\n\n    animationFrameRef.current = requestAnimationFrame(render);\n  }, []);\n\n  useEffect(() => {\n    if (initGL()) {\n      render();\n    }\n\n    return () => {\n      if (animationFrameRef.current) {\n        cancelAnimationFrame(animationFrameRef.current);\n      }\n    };\n  }, [initGL, render]);\n\n  return (\n    <canvas\n      ref={canvasRef}\n      className={className}\n      style={{ width: \"100%\", height: \"100%\" }}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/celestial-effect/page.tsx",
    "content": "\"use client\";\n\nimport { useControls, Leva } from \"leva\";\nimport { CelestialCanvas } from \"./celestial-canvas\";\n\nexport default function CelestialEffectSandbox() {\n  const time = useControls(\"Time of Day\", {\n    timeOfDay: {\n      value: 0.5,\n      min: 0,\n      max: 1,\n      step: 0.01,\n      label: \"Time (0=midnight, 0.5=noon)\",\n    },\n    animateTime: { value: false, label: \"Animate Time\" },\n    dayDuration: {\n      value: 60,\n      min: 10,\n      max: 300,\n      step: 10,\n      label: \"Day Duration (s)\",\n    },\n  });\n\n  const sun = useControls(\"Sun\", {\n    sunSize: { value: 0.08, min: 0.02, max: 0.2, step: 0.005, label: \"Size\" },\n    sunGlowIntensity: {\n      value: 1.0,\n      min: 0,\n      max: 2,\n      step: 0.05,\n      label: \"Glow Intensity\",\n    },\n    sunGlowSize: { value: 3.0, min: 1, max: 6, step: 0.1, label: \"Glow Size\" },\n    sunRaysEnabled: { value: true, label: \"Show Rays\" },\n    sunRayCount: { value: 12, min: 4, max: 24, step: 2, label: \"Ray Count\" },\n    sunRayLength: {\n      value: 0.3,\n      min: 0.1,\n      max: 0.8,\n      step: 0.05,\n      label: \"Ray Length\",\n    },\n  });\n\n  const moon = useControls(\"Moon\", {\n    moonSize: { value: 0.06, min: 0.02, max: 0.15, step: 0.005, label: \"Size\" },\n    moonPhase: {\n      value: 0.5,\n      min: 0,\n      max: 1,\n      step: 0.01,\n      label: \"Phase (0=new, 0.5=full)\",\n    },\n    moonGlowIntensity: {\n      value: 0.6,\n      min: 0,\n      max: 1.5,\n      step: 0.05,\n      label: \"Glow Intensity\",\n    },\n    moonGlowSize: { value: 2.5, min: 1, max: 5, step: 0.1, label: \"Glow Size\" },\n    showCraters: { value: true, label: \"Show Craters\" },\n    craterDetail: {\n      value: 0.5,\n      min: 0,\n      max: 1,\n      step: 0.05,\n      label: \"Crater Detail\",\n    },\n  });\n\n  const sky = useControls(\"Sky\", {\n    horizonOffset: {\n      value: 0.1,\n      min: -0.2,\n      max: 0.4,\n      step: 0.01,\n      label: \"Horizon Offset\",\n    },\n    atmosphereThickness: {\n      value: 1.0,\n      min: 0.5,\n      max: 2,\n      step: 0.1,\n      label: \"Atmosphere\",\n    },\n    starDensity: {\n      value: 0.5,\n      min: 0,\n      max: 1,\n      step: 0.05,\n      label: \"Star Density\",\n    },\n  });\n\n  const debug = useControls(\"Debug\", {\n    showPath: { value: false, label: \"Show Orbital Path\" },\n    showDebug: { value: false, label: \"Debug Mode\" },\n  });\n\n  return (\n    <div className=\"relative min-h-screen bg-black\">\n      <Leva\n        collapsed={false}\n        flat={false}\n        titleBar={{ title: \"Sun & Moon Effect\" }}\n        theme={{\n          sizes: {\n            rootWidth: \"280px\",\n            controlWidth: \"140px\",\n          },\n        }}\n      />\n      <CelestialCanvas\n        className=\"absolute inset-0\"\n        timeOfDay={time.timeOfDay}\n        animateTime={time.animateTime}\n        dayDuration={time.dayDuration}\n        sunSize={sun.sunSize}\n        sunGlowIntensity={sun.sunGlowIntensity}\n        sunGlowSize={sun.sunGlowSize}\n        sunRaysEnabled={sun.sunRaysEnabled}\n        sunRayCount={sun.sunRayCount}\n        sunRayLength={sun.sunRayLength}\n        moonSize={moon.moonSize}\n        moonPhase={moon.moonPhase}\n        moonGlowIntensity={moon.moonGlowIntensity}\n        moonGlowSize={moon.moonGlowSize}\n        showCraters={moon.showCraters}\n        craterDetail={moon.craterDetail}\n        horizonOffset={sky.horizonOffset}\n        atmosphereThickness={sky.atmosphereThickness}\n        starDensity={sky.starDensity}\n        showPath={debug.showPath}\n        debug={debug.showDebug}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/cloud-effect/cloud-canvas.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useRef, useCallback } from \"react\";\n\ninterface CloudCanvasProps {\n  className?: string;\n  // Cloud shape\n  cloudScale?: number; // 0.5-3, affects cloud size (lower = bigger clouds, higher = finer detail)\n  coverage?: number; // 0-1, how much sky has clouds\n  density?: number; // 0-1, opacity/thickness\n  softness?: number; // 0-1, edge sharpness (0=sharp cumulus, 1=soft stratus)\n  // Animation\n  windSpeed?: number; // Wind animation speed\n  windAngle?: number; // Wind direction in radians\n  turbulence?: number; // How much clouds morph/churn\n  // Lighting\n  sunAltitude?: number; // -0.2 to 1, sun height (negative = night, 0 = horizon, 1 = zenith)\n  sunAzimuth?: number; // Sun horizontal angle\n  lightIntensity?: number; // Overall brightness\n  ambientDarkness?: number; // How dark the cloud shadows are\n  // Layers\n  numLayers?: number; // 1-5 depth layers\n  layerSpread?: number; // How spread out the layers are\n  // Stars\n  starDensity?: number; // 0-1, how many stars\n  starSize?: number; // 0.5-3, how large stars appear\n  starTwinkleSpeed?: number; // 0-3, how fast stars twinkle\n  starTwinkleAmount?: number; // 0-1, how much stars twinkle\n  // Positioning\n  horizonLine?: number; // 0-1, vertical position of cloud layer (0=bottom, 1=top)\n  // Compositing\n  transparentBackground?: boolean; // When true, outputs alpha for compositing over other layers\n  // Debug\n  debug?: boolean;\n}\n\nconst VERTEX_SHADER = `#version 300 es\nin vec4 a_position;\nout vec2 v_uv;\n\nvoid main() {\n  gl_Position = a_position;\n  v_uv = a_position.xy * 0.5 + 0.5;\n}\n`;\n\nconst FRAGMENT_SHADER = `#version 300 es\nprecision highp float;\n\nin vec2 v_uv;\nout vec4 fragColor;\n\nuniform float u_time;\nuniform vec2 u_resolution;\nuniform float u_cloudScale;\nuniform float u_coverage;\nuniform float u_density;\nuniform float u_softness;\nuniform float u_windSpeed;\nuniform float u_windAngle;\nuniform float u_turbulence;\nuniform float u_sunAltitude;\nuniform float u_sunAzimuth;\nuniform float u_lightIntensity;\nuniform float u_ambientDarkness;\nuniform int u_numLayers;\nuniform float u_layerSpread;\nuniform float u_starDensity;\nuniform float u_starSize;\nuniform float u_starTwinkleSpeed;\nuniform float u_starTwinkleAmount;\nuniform float u_horizonLine;\nuniform bool u_transparentBackground;\nuniform bool u_debug;\n\n#define PI 3.14159265359\n#define MAX_LAYERS 10\n\n// ============================================================================\n// NOISE FUNCTIONS\n// ============================================================================\n\nfloat hash21(vec2 p) {\n  p = fract(p * vec2(234.34, 435.345));\n  p += dot(p, p + 34.23);\n  return fract(p.x * p.y);\n}\n\nfloat hash31(vec3 p) {\n  p = fract(p * vec3(234.34, 435.345, 123.45));\n  p += dot(p, p.yzx + 34.23);\n  return fract(p.x * p.y * p.z);\n}\n\n// Smooth value noise\nfloat valueNoise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  f = f * f * (3.0 - 2.0 * f); // Smoothstep\n\n  float a = hash21(i);\n  float b = hash21(i + vec2(1.0, 0.0));\n  float c = hash21(i + vec2(0.0, 1.0));\n  float d = hash21(i + vec2(1.0, 1.0));\n\n  return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);\n}\n\n// Worley/cellular noise for billowy cumulus\nfloat worleyNoise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n\n  float minDist = 1.0;\n  for (int y = -1; y <= 1; y++) {\n    for (int x = -1; x <= 1; x++) {\n      vec2 neighbor = vec2(float(x), float(y));\n      vec2 point = hash21(i + neighbor) * 0.5 + 0.25 + neighbor;\n      float d = length(point - f);\n      minDist = min(minDist, d);\n    }\n  }\n  return minDist;\n}\n\n// Inverted worley for billowy shapes\nfloat billowNoise(vec2 p) {\n  return 1.0 - worleyNoise(p);\n}\n\n// ============================================================================\n// FRACTAL BROWNIAN MOTION\n// ============================================================================\n\nfloat fbmValue(vec2 p, int octaves, float lacunarity, float gain) {\n  float value = 0.0;\n  float amplitude = 0.5;\n  float frequency = 1.0;\n  float maxValue = 0.0;\n\n  for (int i = 0; i < 8; i++) {\n    if (i >= octaves) break;\n    value += amplitude * valueNoise(p * frequency);\n    maxValue += amplitude;\n    amplitude *= gain;\n    frequency *= lacunarity;\n  }\n\n  return value / maxValue;\n}\n\nfloat fbmBillow(vec2 p, int octaves, float lacunarity, float gain) {\n  float value = 0.0;\n  float amplitude = 0.5;\n  float frequency = 1.0;\n  float maxValue = 0.0;\n\n  for (int i = 0; i < 8; i++) {\n    if (i >= octaves) break;\n    value += amplitude * billowNoise(p * frequency);\n    maxValue += amplitude;\n    amplitude *= gain;\n    frequency *= lacunarity;\n  }\n\n  return value / maxValue;\n}\n\n// ============================================================================\n// DOMAIN WARPING\n// ============================================================================\n\n// Warp coordinates using noise for more organic shapes\n// time and turbulence parameters allow animated morphing\nvec2 domainWarp(vec2 p, float strength, float scale, float time, float turbulence) {\n  // Time-varying offset creates churning motion\n  // Turbulence controls intensity, wind speed controls rapidity\n  float morphSpeed = 0.15 * turbulence * (0.3 + u_windSpeed * 0.7);\n  vec2 timeOffset1 = vec2(time * morphSpeed, time * morphSpeed * 0.7);\n  vec2 timeOffset2 = vec2(time * morphSpeed * -0.5, time * morphSpeed * 0.3);\n\n  // First layer of warp (animated)\n  float warpX = fbmValue(p * scale + timeOffset1, 3, 2.0, 0.5) * 2.0 - 1.0;\n  float warpY = fbmValue(p * scale + vec2(50.0, 50.0) + timeOffset1 * 0.8, 3, 2.0, 0.5) * 2.0 - 1.0;\n\n  // Turbulence increases warp strength\n  float dynamicStrength = strength * (1.0 + turbulence * 0.5);\n  vec2 warped = p + vec2(warpX, warpY) * dynamicStrength;\n\n  // Second layer for more complexity (animated differently)\n  float warpX2 = fbmValue(warped * scale * 0.5 + vec2(100.0, 0.0) + timeOffset2, 2, 2.0, 0.5) * 2.0 - 1.0;\n  float warpY2 = fbmValue(warped * scale * 0.5 + vec2(0.0, 100.0) + timeOffset2 * 1.2, 2, 2.0, 0.5) * 2.0 - 1.0;\n\n  return warped + vec2(warpX2, warpY2) * dynamicStrength * 0.5;\n}\n\n// ============================================================================\n// CLOUD DENSITY FUNCTIONS\n// ============================================================================\n\n// Cumulus: billowy, puffy, well-defined edges\nfloat cumulusDensity(vec2 uv, vec2 offset, float scale, float time, float turbulence) {\n  vec2 p = (uv + offset) * scale;\n\n  // Apply domain warping for organic, flowing shapes\n  // Time and turbulence create animated morphing\n  vec2 warped = domainWarp(p, 0.4, 0.3, time, turbulence);\n\n  // Wind-scaled animation rate\n  float animRate = turbulence * (0.3 + u_windSpeed * 0.7);\n\n  // Large-scale variation to create bigger cloud masses\n  // Also animate slightly for slow billowing\n  float largeMass = fbmValue(p * 0.15 + vec2(time * 0.02 * animRate), 2, 2.0, 0.6);\n\n  // Base billow shape with warped coordinates\n  float billow = fbmBillow(warped * 0.5, 4, 2.0, 0.5);\n\n  // Add some value noise for detail (less warped to preserve fine detail)\n  // Animate detail layer for additional churning at high turbulence\n  float detail = fbmValue(p * 2.0 + vec2(time * 0.05 * animRate, 0.0), 3, 2.0, 0.5);\n\n  // Combine: large masses modulate the billow, detail adds texture\n  float base = billow * 0.6 + largeMass * 0.25 + detail * 0.15;\n\n  // Boost contrast slightly to maintain cloud definition\n  return smoothstep(0.1, 0.9, base);\n}\n\n// Stratus: flat, layered, smooth\nfloat stratusDensity(vec2 uv, vec2 offset, float scale) {\n  vec2 p = (uv + offset) * scale;\n\n  // Stretched horizontally for layered look\n  vec2 stretchedP = p * vec2(1.0, 3.0);\n\n  // Smooth value noise\n  float base = fbmValue(stretchedP * 0.3, 5, 2.0, 0.6);\n\n  // Subtle detail\n  float detail = fbmValue(p * 1.5, 3, 2.0, 0.4);\n\n  return base * 0.8 + detail * 0.2;\n}\n\n// Cirrus: wispy, feathery, high altitude\nfloat cirrusDensity(vec2 uv, vec2 offset, float scale) {\n  vec2 p = (uv + offset) * scale;\n\n  // Very stretched for wispy streaks\n  vec2 stretchedP = p * vec2(1.0, 8.0);\n\n  // Base wispy shape\n  float base = fbmValue(stretchedP * 0.2, 4, 2.2, 0.55);\n\n  // Turbulent detail with rotation\n  float angle = p.x * 0.5 + base * 2.0;\n  vec2 rotP = vec2(\n    p.x * cos(angle * 0.1) - p.y * sin(angle * 0.1),\n    p.x * sin(angle * 0.1) + p.y * cos(angle * 0.1)\n  );\n  float detail = fbmValue(rotP * 3.0, 3, 2.0, 0.4);\n\n  // Threshold for wispy strands\n  float wispy = smoothstep(0.3, 0.6, base + detail * 0.3);\n\n  return wispy * 0.6;\n}\n\n// Stratocumulus: mix of lumpy and layered\nfloat stratocumulusDensity(vec2 uv, vec2 offset, float scale) {\n  vec2 p = (uv + offset) * scale;\n\n  // Mix of billow and value noise\n  float billow = fbmBillow(p * 0.4, 3, 2.0, 0.5);\n  float layered = fbmValue(p * vec2(1.0, 2.0) * 0.5, 4, 2.0, 0.55);\n\n  // Combine for clumpy but layered look\n  return billow * 0.5 + layered * 0.5;\n}\n\n// Interpolate between cloud types\nfloat cloudDensity(vec2 uv, vec2 offset, float scale, float cloudType, float time, float turbulence) {\n  if (cloudType < 0.33) {\n    // Cumulus to stratocumulus\n    float t = cloudType / 0.33;\n    float cumulus = cumulusDensity(uv, offset, scale, time, turbulence);\n    float stratocumulus = stratocumulusDensity(uv, offset, scale);\n    return mix(cumulus, stratocumulus, t);\n  } else if (cloudType < 0.66) {\n    // Stratocumulus to stratus\n    float t = (cloudType - 0.33) / 0.33;\n    float stratocumulus = stratocumulusDensity(uv, offset, scale);\n    float stratus = stratusDensity(uv, offset, scale);\n    return mix(stratocumulus, stratus, t);\n  } else {\n    // Stratus to cirrus\n    float t = (cloudType - 0.66) / 0.34;\n    float stratus = stratusDensity(uv, offset, scale);\n    float cirrus = cirrusDensity(uv, offset, scale);\n    return mix(stratus, cirrus, t);\n  }\n}\n\n// ============================================================================\n// STAR FIELD\n// ============================================================================\n\nvec3 renderStars(vec2 uv, float time, float sunAlt, float density, float starSize, float twinkleSpeed, float twinkleAmount) {\n  // Fade in during twilight\n  float visibility = smoothstep(0.02, -0.1, sunAlt);\n  if (visibility < 0.001) return vec3(0.0);\n\n  vec3 stars = vec3(0.0);\n\n  // Grid-based star placement (aspect-ratio independent)\n  float cellCount = 30.0 + density * 50.0; // 30-80 cells based on density\n  vec2 grid = uv * cellCount;\n  vec2 cellID = floor(grid);\n  vec2 cellUV = fract(grid);\n\n  // Check this cell and neighbors for stars (handles edge cases)\n  for (int dy = -1; dy <= 1; dy++) {\n    for (int dx = -1; dx <= 1; dx++) {\n      vec2 neighborID = cellID + vec2(float(dx), float(dy));\n      vec2 neighborUV = cellUV - vec2(float(dx), float(dy));\n\n      // Deterministic random for this cell\n      float h1 = hash21(neighborID);\n      float h2 = hash21(neighborID + vec2(127.0, 311.0));\n      float h3 = hash21(neighborID + vec2(269.0, 183.0));\n\n      // Star probability based on density\n      float prob = 0.15 + density * 0.25;\n      if (h1 > prob) continue;\n\n      // Star position within cell\n      vec2 starPos = vec2(\n        hash21(neighborID + vec2(50.0, 0.0)),\n        hash21(neighborID + vec2(0.0, 50.0))\n      ) * 0.8 + 0.1;\n\n      float dist = length(neighborUV - starPos);\n\n      // Star size (slight variation, scaled by parameter)\n      float size = (0.02 + h2 * 0.03) * starSize;\n\n      // Sharp point with soft glow\n      float star = smoothstep(size, 0.0, dist);\n      star += smoothstep(size * 2.5, 0.0, dist) * 0.2;\n\n      // Brightness variation\n      float brightness = 0.5 + h3 * 0.5;\n\n      // Twinkling\n      float twinklePhase = h1 * 6.283 + h2 * 100.0;\n      float twinkleFreq = 2.0 + h3 * 3.0;\n      float twinkle = sin(time * twinkleFreq * twinkleSpeed + twinklePhase);\n      twinkle = twinkle * 0.5 + 0.5;\n      twinkle = mix(1.0, twinkle, twinkleAmount);\n\n      float intensity = star * brightness * twinkle;\n\n      // Subtle color variation (mostly white with hints of warm/cool)\n      vec3 warmTint = vec3(1.0, 0.92, 0.85);\n      vec3 coolTint = vec3(0.9, 0.95, 1.0);\n      vec3 white = vec3(1.0);\n      vec3 color;\n      if (h2 < 0.15) {\n        color = warmTint; // Warm star\n      } else if (h2 > 0.85) {\n        color = coolTint; // Cool star\n      } else {\n        color = white;    // Most stars white\n      }\n\n      stars += color * intensity;\n    }\n  }\n\n  return stars * visibility;\n}\n\n// ============================================================================\n// SHOOTING STARS\n// ============================================================================\n\nvec3 renderShootingStars(vec2 uv, float time, float sunAlt) {\n  // Only visible at night\n  float visibility = smoothstep(0.0, -0.1, sunAlt);\n  if (visibility < 0.001) return vec3(0.0);\n\n  vec3 result = vec3(0.0);\n\n  // Check multiple potential shooting stars (staggered timing)\n  for (int i = 0; i < 3; i++) {\n    float fi = float(i);\n\n    // Each meteor has a different cycle offset and duration\n    float cycleLength = 12.0 + fi * 7.0; // 12-26 second cycles\n    float cycleTime = mod(time + fi * 37.0, cycleLength);\n\n    // Meteor is only active for a brief window\n    float meteorDuration = 0.8 + fi * 0.3;\n    float meteorStart = cycleLength * 0.7; // Appear near end of cycle\n\n    if (cycleTime < meteorStart || cycleTime > meteorStart + meteorDuration) continue;\n\n    float t = (cycleTime - meteorStart) / meteorDuration; // 0 to 1 over meteor lifetime\n\n    // Random start position and angle for this meteor (based on cycle)\n    float cycle = floor((time + fi * 37.0) / cycleLength);\n    float startX = hash21(vec2(cycle, fi * 100.0)) * 0.8 + 0.1;\n    float startY = hash21(vec2(cycle + 50.0, fi * 100.0)) * 0.4 + 0.5; // Upper half\n    float angle = -0.5 - hash21(vec2(cycle + 100.0, fi)) * 0.8; // Downward angle\n\n    // Meteor travels across sky\n    float speed = 0.4 + hash21(vec2(cycle + 150.0, fi)) * 0.3;\n    vec2 meteorPos = vec2(startX, startY) + vec2(cos(angle), sin(angle)) * t * speed;\n\n    // Meteor trail (line segment)\n    float trailLength = 0.08 + hash21(vec2(cycle + 200.0, fi)) * 0.06;\n    vec2 trailDir = normalize(vec2(cos(angle), sin(angle)));\n    vec2 trailStart = meteorPos;\n    vec2 trailEnd = meteorPos - trailDir * trailLength;\n\n    // Distance to line segment\n    vec2 pa = uv - trailStart;\n    vec2 ba = trailEnd - trailStart;\n    float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);\n    float dist = length(pa - ba * h);\n\n    // Fade along trail (bright at head, dim at tail)\n    float trailFade = 1.0 - h;\n\n    // Meteor intensity with distance falloff\n    float width = 0.003;\n    float meteor = smoothstep(width, 0.0, dist) * trailFade;\n\n    // Fade in and out over lifetime\n    float lifeFade = sin(t * 3.14159);\n\n    // Bright white-blue color with slight tail coloring\n    vec3 meteorColor = mix(vec3(0.8, 0.85, 1.0), vec3(1.0, 0.95, 0.8), h);\n\n    result += meteorColor * meteor * lifeFade * 0.8;\n  }\n\n  return result * visibility;\n}\n\n// ============================================================================\n// ATMOSPHERIC SKY\n// ============================================================================\n\nvec3 atmosphericSky(vec2 uv, float sunAlt) {\n  // Sun altitude: 0 = horizon, 1 = zenith, can go negative for night\n\n  // Night sky colors (sun below -0.1)\n  vec3 nightTop = vec3(0.02, 0.02, 0.06);\n  vec3 nightBot = vec3(0.04, 0.04, 0.1);\n\n  // Twilight / blue hour colors (-0.1 to 0.05)\n  vec3 twilightTop = vec3(0.15, 0.2, 0.4);\n  vec3 twilightBot = vec3(0.25, 0.2, 0.35);\n\n  // Golden hour / sunset colors (0.0 to 0.2)\n  vec3 sunsetTop = vec3(0.3, 0.45, 0.7);\n  vec3 sunsetMid = vec3(0.85, 0.5, 0.4);\n  vec3 sunsetBot = vec3(1.0, 0.55, 0.25);\n\n  // Day colors (above 0.3)\n  vec3 dayTop = vec3(0.35, 0.55, 0.9);\n  vec3 dayBot = vec3(0.65, 0.8, 0.95);\n\n  // Calculate blend factors\n  float nightFactor = 1.0 - smoothstep(-0.15, 0.0, sunAlt);\n  float twilightFactor = smoothstep(-0.15, -0.05, sunAlt) * (1.0 - smoothstep(0.05, 0.15, sunAlt));\n  float sunsetFactor = smoothstep(-0.05, 0.05, sunAlt) * (1.0 - smoothstep(0.15, 0.35, sunAlt));\n  float dayFactor = smoothstep(0.2, 0.4, sunAlt);\n\n  // Vertical gradient position with horizon emphasis during sunset\n  float horizonEmphasis = sunsetFactor * pow(1.0 - abs(uv.y - 0.3) * 1.5, 2.0);\n\n  // Build sky color\n  vec3 skyTop = nightTop * nightFactor\n              + twilightTop * twilightFactor\n              + sunsetTop * sunsetFactor\n              + dayTop * dayFactor;\n\n  vec3 skyBot = nightBot * nightFactor\n              + twilightBot * twilightFactor\n              + sunsetBot * sunsetFactor\n              + dayBot * dayFactor;\n\n  // Base gradient\n  vec3 sky = mix(skyBot, skyTop, pow(uv.y, 0.8));\n\n  // Add warm horizon band during sunset/sunrise\n  vec3 horizonGlow = sunsetMid * sunsetFactor * exp(-pow((uv.y - 0.2) * 4.0, 2.0));\n  sky += horizonGlow * 0.6;\n\n  // Add extra orange at very low horizon during golden hour\n  float lowHorizonGlow = sunsetFactor * exp(-pow(uv.y * 6.0, 2.0));\n  sky += vec3(0.4, 0.15, 0.0) * lowHorizonGlow;\n\n  // Purple band above orange during sunset (Rayleigh scattering of remaining blue)\n  float purpleBand = sunsetFactor * exp(-pow((uv.y - 0.45) * 5.0, 2.0)) * 0.3;\n  sky += vec3(0.2, 0.1, 0.3) * purpleBand;\n\n  return sky;\n}\n\n// ============================================================================\n// CLOUD LIGHTING\n// ============================================================================\n\nvec3 cloudLighting(float density, float heightInCloud, float sunAlt, float lightInt, float ambDark) {\n  // Overall daylight factor - dims everything as sun sets\n  float daylight = smoothstep(-0.12, 0.1, sunAlt);\n\n  // Night factor for transitioning to night colors\n  float nightFactor = 1.0 - smoothstep(-0.12, 0.02, sunAlt);\n\n  // Calculate warmth factor based on sun altitude\n  // Low sun = warm colors, high sun = neutral colors\n  float warmth = 1.0 - smoothstep(0.0, 0.4, sunAlt);\n  warmth = warmth * warmth; // Ease in for more dramatic effect at low angles\n\n  // Lit cloud color: white at midday, warm orange/gold at sunset, dark blue-grey at night\n  vec3 dayLitColor = vec3(1.0, 0.98, 0.96);\n  vec3 sunsetLitColor = vec3(1.0, 0.7, 0.45);\n  vec3 nightLitColor = vec3(0.12, 0.14, 0.2); // Faint moonlit highlight\n  vec3 litColor = mix(dayLitColor, sunsetLitColor, warmth);\n  litColor = mix(litColor, nightLitColor, nightFactor);\n\n  // Shadow color: cool blue-grey at midday, warm purple-brown at sunset, very dark at night\n  vec3 dayShadowColor = vec3(0.45, 0.5, 0.6);\n  vec3 sunsetShadowColor = vec3(0.35, 0.25, 0.3);\n  vec3 nightShadowColor = vec3(0.03, 0.04, 0.07); // Nearly black\n  vec3 shadowColor = mix(dayShadowColor, sunsetShadowColor, warmth);\n  shadowColor = mix(shadowColor, nightShadowColor, nightFactor);\n  shadowColor *= (1.0 - ambDark * 0.3);\n\n  // Lighting calculation\n  // Higher sun = lit from above, lower sun = more side/bottom lighting\n  float topLight = heightInCloud * max(0.0, sunAlt);\n  float sideLight = (1.0 - abs(heightInCloud - 0.5) * 2.0) * (1.0 - sunAlt * 0.5);\n\n  // Bottom illumination when sun is very low (light bouncing up from horizon)\n  float bottomLight = (1.0 - heightInCloud) * warmth * 0.5;\n\n  // Ambient light - much lower at night (just faint starlight/moonlight)\n  float ambientLight = mix(0.03, 0.2, daylight);\n\n  float lightAmount = (topLight * 0.5 + sideLight * 0.3 + bottomLight) * daylight + ambientLight;\n  lightAmount = clamp(lightAmount * lightInt, 0.0, 1.0);\n\n  // Mix shadow and lit colors\n  vec3 cloudColor = mix(shadowColor, litColor, lightAmount);\n\n  // Rim lighting / silver lining effect (more pronounced at sunset, very faint at night)\n  float rimLight = pow(density, 0.5) * (1.0 - density) * 4.0;\n  vec3 rimColor = mix(vec3(1.0, 1.0, 0.95), vec3(1.0, 0.8, 0.5), warmth);\n  rimColor = mix(rimColor, vec3(0.15, 0.18, 0.25), nightFactor); // Faint blue rim at night\n  float rimStrength = mix(0.1, 0.3, daylight); // Dim rim at night\n  cloudColor += rimColor * rimLight * rimStrength * lightInt;\n\n  // Hot highlight on very bright parts during sunset (not at night)\n  float hotSpot = pow(max(0.0, lightAmount - 0.6) * 2.5, 2.0) * warmth * daylight;\n  cloudColor += vec3(1.0, 0.5, 0.2) * hotSpot * 0.4;\n\n  return cloudColor;\n}\n\n// ============================================================================\n// MAIN CLOUD RENDERING\n// ============================================================================\n\nvec4 renderCloudLayer(vec2 uv, float layerIndex, float totalLayers) {\n  // Layer-specific variations\n  float layerOffset = layerIndex / max(1.0, totalLayers - 1.0);\n  float layerDepth = 1.0 - layerOffset * u_layerSpread;\n\n  // Apply horizon line offset to shift clouds vertically\n  // u_horizonLine 0.5 = centered, 0 = clouds at bottom, 1 = clouds at top\n  float horizonOffset = (u_horizonLine - 0.5) * 1.5;\n  float cloudY = uv.y - horizonOffset;\n\n  // Wind offset with parallax\n  vec2 windDir = vec2(cos(u_windAngle), sin(u_windAngle));\n  float speed = u_windSpeed * (0.5 + layerDepth * 0.5);\n  vec2 offset = windDir * u_time * speed * 0.1;\n\n  // Scale varies per layer (further = smaller features)\n  // u_cloudScale controls overall detail level\n  float scale = (2.0 + layerIndex * 0.5) * u_cloudScale;\n\n  // Get cumulus density (turbulence creates animated morphing/churning)\n  float density = cumulusDensity(uv, offset + vec2(layerIndex * 10.0), scale, u_time, u_turbulence);\n\n  // Height-based cloud distribution (clouds denser at bottom, dissipate toward top)\n  // This creates the effect of being at or above cloud level\n  float heightNoise = fbmValue(uv * 2.0 + offset * 0.5, 2, 2.0, 0.5) * 0.3;\n  float cloudCeiling = 0.65 + heightNoise; // Irregular top boundary\n  float heightFalloff = smoothstep(cloudCeiling, cloudCeiling - 0.4, cloudY);\n\n  // Additional dissipation - clouds thin out and become wispier near the top\n  float dissipation = smoothstep(0.3, 0.7, cloudY);\n  density = mix(density, density * density, dissipation * 0.5); // Thin out upper clouds\n\n  // Apply height-based density reduction\n  density *= heightFalloff;\n\n  // Apply coverage threshold\n  float coverageThreshold = 1.0 - u_coverage;\n  density = smoothstep(coverageThreshold - u_softness * 0.3, coverageThreshold + 0.1, density);\n\n  // Height in cloud for lighting - use vertical UV position\n  // Higher on screen = more lit by sun from above\n  float heightInCloud = cloudY * 0.6 + density * 0.4;\n\n  // Calculate lighting\n  vec3 color = cloudLighting(density, heightInCloud, u_sunAltitude, u_lightIntensity, u_ambientDarkness);\n\n  // === AERIAL PERSPECTIVE ===\n  // Distant layers (higher index) get hazier, more blue-tinted, lower contrast\n  float distance = layerOffset; // 0 = front, 1 = back\n\n  // Haze color - blue-ish during day, darker at night\n  float daylight = smoothstep(-0.1, 0.2, u_sunAltitude);\n  vec3 hazeColor = mix(vec3(0.05, 0.06, 0.1), vec3(0.6, 0.7, 0.85), daylight);\n\n  // Blend toward haze based on distance\n  float hazeAmount = distance * distance * 0.5; // Quadratic falloff\n  color = mix(color, hazeColor, hazeAmount);\n\n  // Reduce contrast for distant layers\n  float contrastReduction = 1.0 - distance * 0.3;\n  color = mix(vec3(0.5), color, contrastReduction);\n\n  // Apply density for alpha\n  float alpha = density * u_density * (0.6 + layerDepth * 0.4);\n\n  return vec4(color, alpha);\n}\n\nvoid main() {\n  vec2 uv = v_uv;\n  float aspect = u_resolution.x / u_resolution.y;\n  uv.x *= aspect;\n\n  // Transparent mode: render only clouds with alpha for compositing\n  if (u_transparentBackground) {\n    vec3 cloudColor = vec3(0.0);\n    float cloudAlpha = 0.0;\n\n    for (int i = u_numLayers - 1; i >= 0; i--) {\n      vec4 cloud = renderCloudLayer(uv, float(i), float(u_numLayers));\n      cloudColor = mix(cloudColor, cloud.rgb, cloud.a);\n      cloudAlpha = cloudAlpha + cloud.a * (1.0 - cloudAlpha);\n    }\n\n    fragColor = vec4(cloudColor, cloudAlpha);\n    return;\n  }\n\n  // Normal mode: full sky with stars and clouds\n  vec3 skyColor = atmosphericSky(v_uv, u_sunAltitude);\n\n  vec3 stars = renderStars(v_uv, u_time, u_sunAltitude, u_starDensity, u_starSize, u_starTwinkleSpeed, u_starTwinkleAmount);\n  skyColor += stars;\n\n  vec3 shootingStars = renderShootingStars(v_uv, u_time, u_sunAltitude);\n  skyColor += shootingStars;\n\n  vec3 color = skyColor;\n\n  for (int i = u_numLayers - 1; i >= 0; i--) {\n    vec4 cloud = renderCloudLayer(uv, float(i), float(u_numLayers));\n    color = mix(color, cloud.rgb, cloud.a);\n  }\n\n  if (u_debug) {\n    vec2 windDir = vec2(cos(u_windAngle), sin(u_windAngle));\n    vec2 offset = windDir * u_time * u_windSpeed * 0.1;\n    float rawDensity = cumulusDensity(uv, offset, 2.0, u_time, u_turbulence);\n    color = mix(color, vec3(rawDensity), 0.5);\n  }\n\n  fragColor = vec4(color, 1.0);\n}\n`;\n\nfunction createShader(\n  gl: WebGL2RenderingContext,\n  type: number,\n  source: string,\n): WebGLShader | null {\n  const shader = gl.createShader(type);\n  if (!shader) return null;\n\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n    console.error(\"Shader compile error:\", gl.getShaderInfoLog(shader));\n    gl.deleteShader(shader);\n    return null;\n  }\n\n  return shader;\n}\n\nfunction createProgram(\n  gl: WebGL2RenderingContext,\n  vertexShader: WebGLShader,\n  fragmentShader: WebGLShader,\n): WebGLProgram | null {\n  const program = gl.createProgram();\n  if (!program) return null;\n\n  gl.attachShader(program, vertexShader);\n  gl.attachShader(program, fragmentShader);\n  gl.linkProgram(program);\n\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n    console.error(\"Program link error:\", gl.getProgramInfoLog(program));\n    gl.deleteProgram(program);\n    return null;\n  }\n\n  return program;\n}\n\nexport function CloudCanvas({\n  className,\n  cloudScale = 1.0,\n  coverage = 0.5,\n  density = 0.8,\n  softness = 0.3,\n  windSpeed = 1.0,\n  windAngle = 0.2,\n  turbulence = 0.5,\n  sunAltitude = 0.3,\n  sunAzimuth = 0.0,\n  lightIntensity = 1.0,\n  ambientDarkness = 0.3,\n  numLayers = 3,\n  layerSpread = 0.3,\n  starDensity = 0.5,\n  starSize = 1.5,\n  starTwinkleSpeed = 1.0,\n  starTwinkleAmount = 0.5,\n  horizonLine = 0.5,\n  transparentBackground = false,\n  debug = false,\n}: CloudCanvasProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const glRef = useRef<WebGL2RenderingContext | null>(null);\n  const programRef = useRef<WebGLProgram | null>(null);\n  const uniformsRef = useRef<{\n    time: WebGLUniformLocation | null;\n    resolution: WebGLUniformLocation | null;\n    cloudScale: WebGLUniformLocation | null;\n    coverage: WebGLUniformLocation | null;\n    density: WebGLUniformLocation | null;\n    softness: WebGLUniformLocation | null;\n    windSpeed: WebGLUniformLocation | null;\n    windAngle: WebGLUniformLocation | null;\n    turbulence: WebGLUniformLocation | null;\n    sunAltitude: WebGLUniformLocation | null;\n    sunAzimuth: WebGLUniformLocation | null;\n    lightIntensity: WebGLUniformLocation | null;\n    ambientDarkness: WebGLUniformLocation | null;\n    numLayers: WebGLUniformLocation | null;\n    layerSpread: WebGLUniformLocation | null;\n    starDensity: WebGLUniformLocation | null;\n    starSize: WebGLUniformLocation | null;\n    starTwinkleSpeed: WebGLUniformLocation | null;\n    starTwinkleAmount: WebGLUniformLocation | null;\n    horizonLine: WebGLUniformLocation | null;\n    transparentBackground: WebGLUniformLocation | null;\n    debug: WebGLUniformLocation | null;\n  } | null>(null);\n  const animationFrameRef = useRef<number>(0);\n  const startTimeRef = useRef<number>(0);\n\n  const initGL = useCallback(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return false;\n\n    const gl = canvas.getContext(\"webgl2\", {\n      alpha: true,\n      premultipliedAlpha: false,\n    });\n    if (!gl) {\n      console.error(\"WebGL2 not supported\");\n      return false;\n    }\n    glRef.current = gl;\n\n    const vertexShader = createShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);\n    const fragmentShader = createShader(\n      gl,\n      gl.FRAGMENT_SHADER,\n      FRAGMENT_SHADER,\n    );\n    if (!vertexShader || !fragmentShader) return false;\n\n    const program = createProgram(gl, vertexShader, fragmentShader);\n    if (!program) return false;\n    programRef.current = program;\n\n    uniformsRef.current = {\n      time: gl.getUniformLocation(program, \"u_time\"),\n      resolution: gl.getUniformLocation(program, \"u_resolution\"),\n      cloudScale: gl.getUniformLocation(program, \"u_cloudScale\"),\n      coverage: gl.getUniformLocation(program, \"u_coverage\"),\n      density: gl.getUniformLocation(program, \"u_density\"),\n      softness: gl.getUniformLocation(program, \"u_softness\"),\n      windSpeed: gl.getUniformLocation(program, \"u_windSpeed\"),\n      windAngle: gl.getUniformLocation(program, \"u_windAngle\"),\n      turbulence: gl.getUniformLocation(program, \"u_turbulence\"),\n      sunAltitude: gl.getUniformLocation(program, \"u_sunAltitude\"),\n      sunAzimuth: gl.getUniformLocation(program, \"u_sunAzimuth\"),\n      lightIntensity: gl.getUniformLocation(program, \"u_lightIntensity\"),\n      ambientDarkness: gl.getUniformLocation(program, \"u_ambientDarkness\"),\n      numLayers: gl.getUniformLocation(program, \"u_numLayers\"),\n      layerSpread: gl.getUniformLocation(program, \"u_layerSpread\"),\n      starDensity: gl.getUniformLocation(program, \"u_starDensity\"),\n      starSize: gl.getUniformLocation(program, \"u_starSize\"),\n      starTwinkleSpeed: gl.getUniformLocation(program, \"u_starTwinkleSpeed\"),\n      starTwinkleAmount: gl.getUniformLocation(program, \"u_starTwinkleAmount\"),\n      horizonLine: gl.getUniformLocation(program, \"u_horizonLine\"),\n      transparentBackground: gl.getUniformLocation(\n        program,\n        \"u_transparentBackground\",\n      ),\n      debug: gl.getUniformLocation(program, \"u_debug\"),\n    };\n\n    const positions = new Float32Array([\n      -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1,\n    ]);\n\n    const buffer = gl.createBuffer();\n    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n    gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);\n\n    const positionLoc = gl.getAttribLocation(program, \"a_position\");\n    gl.enableVertexAttribArray(positionLoc);\n    gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);\n\n    startTimeRef.current = performance.now();\n    return true;\n  }, []);\n\n  const render = useCallback(() => {\n    const gl = glRef.current;\n    const program = programRef.current;\n    const uniforms = uniformsRef.current;\n    const canvas = canvasRef.current;\n\n    if (!gl || !program || !uniforms || !canvas) return;\n\n    const displayWidth = canvas.clientWidth * window.devicePixelRatio;\n    const displayHeight = canvas.clientHeight * window.devicePixelRatio;\n\n    if (canvas.width !== displayWidth || canvas.height !== displayHeight) {\n      canvas.width = displayWidth;\n      canvas.height = displayHeight;\n      gl.viewport(0, 0, canvas.width, canvas.height);\n    }\n\n    const time = (performance.now() - startTimeRef.current) / 1000;\n\n    gl.useProgram(program);\n    gl.uniform1f(uniforms.time, time);\n    gl.uniform2f(uniforms.resolution, canvas.width, canvas.height);\n    gl.uniform1f(uniforms.cloudScale, cloudScale);\n    gl.uniform1f(uniforms.coverage, coverage);\n    gl.uniform1f(uniforms.density, density);\n    gl.uniform1f(uniforms.softness, softness);\n    gl.uniform1f(uniforms.windSpeed, windSpeed);\n    gl.uniform1f(uniforms.windAngle, windAngle);\n    gl.uniform1f(uniforms.turbulence, turbulence);\n    gl.uniform1f(uniforms.sunAltitude, sunAltitude);\n    gl.uniform1f(uniforms.sunAzimuth, sunAzimuth);\n    gl.uniform1f(uniforms.lightIntensity, lightIntensity);\n    gl.uniform1f(uniforms.ambientDarkness, ambientDarkness);\n    gl.uniform1i(uniforms.numLayers, numLayers);\n    gl.uniform1f(uniforms.layerSpread, layerSpread);\n    gl.uniform1f(uniforms.starDensity, starDensity);\n    gl.uniform1f(uniforms.starSize, starSize);\n    gl.uniform1f(uniforms.starTwinkleSpeed, starTwinkleSpeed);\n    gl.uniform1f(uniforms.starTwinkleAmount, starTwinkleAmount);\n    gl.uniform1f(uniforms.horizonLine, horizonLine);\n    gl.uniform1i(uniforms.transparentBackground, transparentBackground ? 1 : 0);\n    gl.uniform1i(uniforms.debug, debug ? 1 : 0);\n\n    gl.drawArrays(gl.TRIANGLES, 0, 6);\n\n    animationFrameRef.current = requestAnimationFrame(render);\n  }, [\n    cloudScale,\n    coverage,\n    density,\n    softness,\n    windSpeed,\n    windAngle,\n    turbulence,\n    sunAltitude,\n    sunAzimuth,\n    lightIntensity,\n    ambientDarkness,\n    numLayers,\n    layerSpread,\n    starDensity,\n    starSize,\n    starTwinkleSpeed,\n    starTwinkleAmount,\n    horizonLine,\n    transparentBackground,\n    debug,\n  ]);\n\n  useEffect(() => {\n    if (initGL()) {\n      render();\n    }\n\n    return () => {\n      if (animationFrameRef.current) {\n        cancelAnimationFrame(animationFrameRef.current);\n      }\n    };\n  }, [initGL, render]);\n\n  return (\n    <canvas\n      ref={canvasRef}\n      className={className}\n      style={{ width: \"100%\", height: \"100%\" }}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/cloud-effect/page.tsx",
    "content": "\"use client\";\n\nimport { useControls, Leva } from \"leva\";\nimport { CloudCanvas } from \"./cloud-canvas\";\n\nexport default function CloudEffectSandbox() {\n  const cloudShape = useControls(\"Cloud Shape\", {\n    cloudScale: { value: 1.0, min: 0.3, max: 3, step: 0.05, label: \"Scale\" },\n    coverage: { value: 0.5, min: 0, max: 1, step: 0.01, label: \"Coverage\" },\n    density: { value: 0.85, min: 0, max: 1, step: 0.01, label: \"Density\" },\n    softness: { value: 0.3, min: 0, max: 1, step: 0.01, label: \"Softness\" },\n  });\n\n  const animation = useControls(\"Animation\", {\n    windSpeed: { value: 0.8, min: 0, max: 3, step: 0.05, label: \"Wind Speed\" },\n    windAngle: {\n      value: 0.1,\n      min: -Math.PI,\n      max: Math.PI,\n      step: 0.05,\n      label: \"Wind Angle\",\n    },\n    turbulence: { value: 0.5, min: 0, max: 2, step: 0.05, label: \"Turbulence\" },\n    numLayers: { value: 3, min: 1, max: 10, step: 1, label: \"Layers\" },\n    layerSpread: {\n      value: 0.3,\n      min: 0,\n      max: 2,\n      step: 0.05,\n      label: \"Layer Spread\",\n    },\n  });\n\n  const lighting = useControls(\"Lighting\", {\n    sunAltitude: {\n      value: 0.1,\n      min: -0.2,\n      max: 1,\n      step: 0.01,\n      label: \"Sun Position\",\n    },\n    lightIntensity: {\n      value: 1.0,\n      min: 0,\n      max: 2,\n      step: 0.05,\n      label: \"Intensity\",\n    },\n    ambientDarkness: {\n      value: 0.3,\n      min: 0,\n      max: 1,\n      step: 0.05,\n      label: \"Shadow Darkness\",\n    },\n  });\n\n  const stars = useControls(\"Stars\", {\n    starDensity: { value: 0.5, min: 0, max: 1, step: 0.05, label: \"Density\" },\n    starSize: { value: 1.5, min: 0.5, max: 4, step: 0.1, label: \"Size\" },\n    starTwinkleSpeed: {\n      value: 1.0,\n      min: 0,\n      max: 3,\n      step: 0.1,\n      label: \"Twinkle Speed\",\n    },\n    starTwinkleAmount: {\n      value: 0.6,\n      min: 0,\n      max: 1,\n      step: 0.05,\n      label: \"Twinkle Amount\",\n    },\n  });\n\n  const debug = useControls(\"Debug\", {\n    showDebug: { value: false, label: \"Show Debug\" },\n  });\n\n  return (\n    <div className=\"relative min-h-screen bg-black\">\n      <Leva\n        collapsed={false}\n        flat={false}\n        titleBar={{ title: \"Cloud Effect\" }}\n        theme={{\n          sizes: {\n            rootWidth: \"280px\",\n            controlWidth: \"140px\",\n          },\n        }}\n      />\n      <CloudCanvas\n        className=\"absolute inset-0\"\n        cloudScale={cloudShape.cloudScale}\n        coverage={cloudShape.coverage}\n        density={cloudShape.density}\n        softness={cloudShape.softness}\n        windSpeed={animation.windSpeed}\n        windAngle={animation.windAngle}\n        turbulence={animation.turbulence}\n        sunAltitude={lighting.sunAltitude}\n        lightIntensity={lighting.lightIntensity}\n        ambientDarkness={lighting.ambientDarkness}\n        numLayers={animation.numLayers}\n        layerSpread={animation.layerSpread}\n        starDensity={stars.starDensity}\n        starSize={stars.starSize}\n        starTwinkleSpeed={stars.starTwinkleSpeed}\n        starTwinkleAmount={stars.starTwinkleAmount}\n        debug={debug.showDebug}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/lightning-effect/lightning-canvas.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useRef, useCallback } from \"react\";\n\ninterface LightningCanvasProps {\n  className?: string;\n  branchDensity?: number;\n  displacement?: number;\n  glowIntensity?: number;\n  flashDuration?: number;\n  sceneIllumination?: number;\n  afterglowPersistence?: number;\n  triggerCount?: number;\n  autoMode?: boolean;\n  autoInterval?: number;\n  debug?: boolean;\n}\n\nconst VERTEX_SHADER = `#version 300 es\nin vec4 a_position;\nout vec2 v_uv;\n\nvoid main() {\n  gl_Position = a_position;\n  v_uv = a_position.xy * 0.5 + 0.5;\n}\n`;\n\nconst FRAGMENT_SHADER = `#version 300 es\nprecision highp float;\n\nin vec2 v_uv;\nout vec4 fragColor;\n\nuniform float u_time;\nuniform vec2 u_resolution;\nuniform float u_branchDensity;\nuniform float u_displacement;\nuniform float u_glowIntensity;\nuniform float u_flashDuration;\nuniform float u_sceneIllumination;\nuniform float u_afterglowPersistence;\nuniform float u_strikeTime;\nuniform float u_strikeSeed;\nuniform bool u_debug;\n\n#define MAX_SEGMENTS 32\n#define MAX_BRANCHES 16\n#define PI 3.14159265359\n\n// ============================================================================\n// EASING FUNCTIONS\n// ============================================================================\n\nfloat easeOutSine(float t) {\n  return sin(t * PI * 0.5);\n}\n\nfloat easeInSine(float t) {\n  return 1.0 - cos(t * PI * 0.5);\n}\n\nfloat easeInOutSine(float t) {\n  return -(cos(PI * t) - 1.0) * 0.5;\n}\n\nfloat easeOutQuad(float t) {\n  return 1.0 - (1.0 - t) * (1.0 - t);\n}\n\nfloat easeOutCubic(float t) {\n  float inv = 1.0 - t;\n  return 1.0 - inv * inv * inv;\n}\n\nfloat easeInOutCubic(float t) {\n  return t < 0.5\n    ? 4.0 * t * t * t\n    : 1.0 - pow(-2.0 * t + 2.0, 3.0) * 0.5;\n}\n\n// ============================================================================\n// NOISE FUNCTIONS\n// ============================================================================\n\nfloat hash11(float p) {\n  p = fract(p * 0.1031);\n  p *= p + 33.33;\n  p *= p + p;\n  return fract(p);\n}\n\nfloat hash12(vec2 p) {\n  vec3 p3 = fract(vec3(p.xyx) * 0.1031);\n  p3 += dot(p3, p3.yzx + 33.33);\n  return fract((p3.x + p3.y) * p3.z);\n}\n\nvec2 hash22(vec2 p) {\n  vec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973));\n  p3 += dot(p3, p3.yzx + 33.33);\n  return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfloat noise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  f = f * f * (3.0 - 2.0 * f);\n\n  float a = hash12(i);\n  float b = hash12(i + vec2(1.0, 0.0));\n  float c = hash12(i + vec2(0.0, 1.0));\n  float d = hash12(i + vec2(1.0, 1.0));\n\n  return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);\n}\n\nfloat fbm(vec2 p, int octaves) {\n  float value = 0.0;\n  float amplitude = 0.5;\n  for (int i = 0; i < 6; i++) {\n    if (i >= octaves) break;\n    value += amplitude * noise(p);\n    p *= 2.0;\n    amplitude *= 0.5;\n  }\n  return value;\n}\n\n// ============================================================================\n// BACKGROUND - Stormy night sky\n// ============================================================================\n\nvec3 stormySky(vec2 uv, float illumination) {\n  // Base dark sky\n  vec3 skyDark = vec3(0.02, 0.02, 0.04);\n  vec3 skyLight = vec3(0.15, 0.15, 0.2);\n\n  // Cloud layer using FBM\n  float clouds = fbm(uv * 3.0 + u_time * 0.01, 4);\n  clouds = smoothstep(0.3, 0.7, clouds);\n\n  vec3 color = mix(skyDark, skyLight, clouds * 0.3);\n\n  // When lightning strikes, illuminate clouds\n  color = mix(color, vec3(0.6, 0.65, 0.75), illumination * clouds);\n  color = mix(color, vec3(0.3, 0.32, 0.4), illumination * (1.0 - clouds) * 0.5);\n\n  return color;\n}\n\n// ============================================================================\n// LIGHTNING PATH GENERATION\n// ============================================================================\n\n// Distance from point to line segment\nfloat distToSegment(vec2 p, vec2 a, vec2 b) {\n  vec2 pa = p - a;\n  vec2 ba = b - a;\n  float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);\n  return length(pa - ba * h);\n}\n\n// Generate displaced point along path using midpoint displacement principle\nvec2 displacedPoint(vec2 start, vec2 end, float t, float seed, float displacementAmt) {\n  vec2 basePoint = mix(start, end, t);\n\n  // Direction perpendicular to path\n  vec2 dir = end - start;\n  vec2 perp = normalize(vec2(-dir.y, dir.x));\n\n  // Displacement amount varies: max at middle, zero at ends\n  float envelope = sin(t * PI);\n\n  // Multi-frequency noise for organic look\n  float n1 = noise(vec2(t * 8.0, seed * 100.0)) * 2.0 - 1.0;\n  float n2 = noise(vec2(t * 16.0, seed * 100.0 + 50.0)) * 2.0 - 1.0;\n  float n3 = noise(vec2(t * 32.0, seed * 100.0 + 100.0)) * 2.0 - 1.0;\n\n  float displacement = (n1 * 0.6 + n2 * 0.3 + n3 * 0.1) * envelope * displacementAmt;\n\n  // Bias toward target (DBM-inspired) - reduces displacement as we get closer to end\n  float targetBias = 1.0 - t * 0.3;\n  displacement *= targetBias;\n\n  return basePoint + perp * displacement * length(dir);\n}\n\n// Calculate distance to the main lightning bolt\nfloat mainBoltDistance(vec2 uv, vec2 start, vec2 end, float seed, float displacementAmt) {\n  float minDist = 999.0;\n\n  vec2 prevPoint = start;\n  for (int i = 1; i <= MAX_SEGMENTS; i++) {\n    float t = float(i) / float(MAX_SEGMENTS);\n    vec2 currPoint = displacedPoint(start, end, t, seed, displacementAmt);\n\n    float d = distToSegment(uv, prevPoint, currPoint);\n    minDist = min(minDist, d);\n\n    prevPoint = currPoint;\n  }\n\n  return minDist;\n}\n\n// Calculate distance to a branch\nfloat branchDistance(vec2 uv, vec2 branchStart, vec2 branchDir, float branchLen, float seed, float displacementAmt) {\n  vec2 branchEnd = branchStart + branchDir * branchLen;\n\n  float minDist = 999.0;\n  int segments = 12;\n\n  vec2 prevPoint = branchStart;\n  for (int i = 1; i <= 12; i++) {\n    float t = float(i) / float(segments);\n    vec2 currPoint = displacedPoint(branchStart, branchEnd, t, seed, displacementAmt * 0.7);\n\n    float d = distToSegment(uv, prevPoint, currPoint);\n    minDist = min(minDist, d);\n\n    prevPoint = currPoint;\n  }\n\n  return minDist;\n}\n\n// Generate all branches and return minimum distance with brightness\nvec2 branchesDistance(vec2 uv, vec2 start, vec2 end, float seed, float displacementAmt, float density) {\n  float minDist = 999.0;\n  float brightness = 0.0;\n\n  vec2 mainDir = normalize(end - start);\n  float mainLen = length(end - start);\n\n  // Generate branches along the main bolt\n  for (int i = 0; i < MAX_BRANCHES; i++) {\n    float idx = float(i);\n\n    // Position along main bolt where branch starts (avoid very start/end)\n    float branchT = 0.15 + hash11(seed + idx * 7.31) * 0.7;\n\n    // Branch probability decreases toward end (more branches near cloud)\n    float branchProb = (1.0 - branchT) * density;\n    if (hash11(seed + idx * 3.17) > branchProb) continue;\n\n    // Get branch start point on main bolt\n    vec2 branchStart = displacedPoint(start, end, branchT, seed, displacementAmt);\n\n    // Branch angle: 15-45 degrees from main direction\n    float angleOffset = (hash11(seed + idx * 11.13) * 2.0 - 1.0) * 0.6;\n    float side = hash11(seed + idx * 5.71) > 0.5 ? 1.0 : -1.0;\n    float angle = atan(mainDir.y, mainDir.x) + side * (0.3 + abs(angleOffset) * 0.5);\n    vec2 branchDir = vec2(cos(angle), sin(angle));\n\n    // Branch length: 15-40% of main bolt\n    float branchLen = mainLen * (0.15 + hash11(seed + idx * 13.37) * 0.25);\n\n    // Calculate distance to this branch\n    float d = branchDistance(uv, branchStart, branchDir, branchLen, seed + idx * 100.0, displacementAmt);\n\n    if (d < minDist) {\n      minDist = d;\n      // Brightness decreases with branch depth and distance from main\n      brightness = 0.5 - branchT * 0.2;\n    }\n\n    // Sub-branches (level 2)\n    if (density > 0.3 && hash11(seed + idx * 17.19) < density * 0.5) {\n      float subT = 0.3 + hash11(seed + idx * 19.23) * 0.4;\n      vec2 subStart = branchStart + branchDir * branchLen * subT;\n\n      float subAngle = angle + (hash11(seed + idx * 23.29) * 2.0 - 1.0) * 0.5;\n      vec2 subDir = vec2(cos(subAngle), sin(subAngle));\n      float subLen = branchLen * 0.4;\n\n      float subD = branchDistance(uv, subStart, subDir, subLen, seed + idx * 200.0, displacementAmt * 0.5);\n\n      if (subD < minDist) {\n        minDist = subD;\n        brightness = 0.25;\n      }\n    }\n  }\n\n  return vec2(minDist, brightness);\n}\n\n// ============================================================================\n// GLOW RENDERING\n// ============================================================================\n\nvec3 lightningGlow(float dist, float brightness, float intensity, float thickness) {\n  // Scale distance by inverse thickness - thinner bolt = larger effective distance\n  float scaledDist = dist / max(thickness, 0.1);\n\n  // Core: brilliant white, very sharp\n  float core = smoothstep(0.003, 0.0, scaledDist) * brightness;\n\n  // Inner glow: blue-white, exponential falloff\n  float innerGlow = exp(-scaledDist * 150.0) * brightness;\n\n  // Outer glow: purple-blue, softer falloff (fades slower, provides lingering aura)\n  float outerGlow = exp(-dist * dist * 3000.0) * brightness * thickness;\n\n  // Combine with colors\n  vec3 coreColor = vec3(1.0, 1.0, 1.0);\n  vec3 innerColor = vec3(0.7, 0.8, 1.0);\n  vec3 outerColor = vec3(0.5, 0.5, 0.9);\n\n  vec3 color = coreColor * core * 2.0;\n  color += innerColor * innerGlow * 0.8;\n  color += outerColor * outerGlow * 0.5;\n\n  return color * intensity;\n}\n\n// ============================================================================\n// TEMPORAL ANIMATION\n// ============================================================================\n\nfloat flashEnvelope(float timeSinceStrike, float duration) {\n  if (timeSinceStrike < 0.0 || timeSinceStrike > duration) return 0.0;\n\n  float t = timeSinceStrike / duration;\n\n  // Fast attack with sine easing (more natural ramp-up)\n  float attackT = clamp(t / 0.03, 0.0, 1.0);\n  float attack = easeOutCubic(attackT);\n\n  // Sustain with sine easing for smoother rolloff\n  float sustainT = clamp((t - 0.05) / 0.65, 0.0, 1.0);\n  float sustain = 1.0 - easeInOutSine(sustainT);\n\n  // Exponential decay with sine modulation for organic feel\n  float decay = exp(-t * 2.0);\n  decay = mix(decay, easeOutSine(1.0 - t), 0.3);\n\n  // Smooth fade to zero with sine easing (prevents hard cutoff)\n  float endT = clamp((t - 0.75) / 0.25, 0.0, 1.0);\n  float endFade = 1.0 - easeInSine(endT);\n\n  // Combine: sustain holds brightness longer, decay provides gentle tail\n  return attack * max(sustain, decay * 0.4) * endFade;\n}\n\n// Re-strike effect (multiple flashes)\nfloat restrikeEnvelope(float timeSinceStrike, float duration, float seed) {\n  // Main flash uses 70% of duration for gradual falloff\n  float env = flashEnvelope(timeSinceStrike, duration * 0.7);\n\n  // Possible re-strikes (less frequent, later in timeline)\n  if (hash11(seed * 7.7) > 0.7) {\n    float restrike1 = flashEnvelope(timeSinceStrike - duration * 0.5, duration * 0.3);\n    env = max(env, restrike1 * 0.6);\n  }\n\n  if (hash11(seed * 11.3) > 0.85) {\n    float restrike2 = flashEnvelope(timeSinceStrike - duration * 0.75, duration * 0.2);\n    env = max(env, restrike2 * 0.4);\n  }\n\n  return env;\n}\n\n// ============================================================================\n// MAIN\n// ============================================================================\n\nvoid main() {\n  vec2 uv = v_uv;\n  float aspect = u_resolution.x / u_resolution.y;\n  uv.x *= aspect;\n\n  // Time since last strike\n  float timeSinceStrike = u_time - u_strikeTime;\n  float durationSec = u_flashDuration / 1000.0;\n\n  // Flash envelope with re-strikes\n  float flash = restrikeEnvelope(timeSinceStrike, durationSec, u_strikeSeed);\n\n  // Afterimage has its own extended duration based on persistence\n  float afterimageDuration = durationSec * (1.0 + u_afterglowPersistence * 0.5);\n  float afterimageT = clamp(timeSinceStrike / afterimageDuration, 0.0, 1.0);\n  float afterimage = timeSinceStrike < 0.0 ? 0.0 : (1.0 - easeInSine(afterimageT));\n\n  // Scene illumination\n  float sceneFlash = flash * u_sceneIllumination;\n\n  // Background - also affected by afterimage for lingering illumination\n  float bgIllumination = max(sceneFlash, afterimage * u_sceneIllumination * 0.15);\n  vec3 color = stormySky(v_uv, bgIllumination);\n\n  // Render lightning (bright core needs flash, afterglow uses afterimage)\n  if (flash > 0.01 || afterimage > 0.01) {\n    // Lightning start and end points (randomized per strike)\n    vec2 strikeHash = hash22(vec2(u_strikeSeed * 123.456, u_strikeSeed * 789.012));\n\n    // Start: top of screen with horizontal variation\n    vec2 boltStart = vec2(\n      0.3 + strikeHash.x * 0.4,\n      1.05\n    );\n    boltStart.x *= aspect;\n\n    // End: bottom of screen with some horizontal drift\n    vec2 boltEnd = vec2(\n      boltStart.x + (strikeHash.x - 0.5) * 0.4,\n      -0.05\n    );\n\n    // Main bolt distance\n    float mainDist = mainBoltDistance(uv, boltStart, boltEnd, u_strikeSeed, u_displacement);\n\n    // Branch distances\n    vec2 branchResult = branchesDistance(uv, boltStart, boltEnd, u_strikeSeed, u_displacement, u_branchDensity);\n    float branchDist = branchResult.x;\n    float branchBrightness = branchResult.y;\n\n    // Thickness decay - bolt gets thinner as flash fades (sine easing for organic taper)\n    float mainThickness = mix(0.2, 1.0, easeOutSine(sqrt(max(flash, 0.0))));\n\n    // Afterglow color - slightly purple (retinal persistence)\n    vec3 afterglowColor = vec3(0.5, 0.45, 0.7);\n\n    // Main bolt: bright core with thickness decay + lingering afterglow\n    vec3 mainCore = lightningGlow(mainDist, easeOutQuad(max(flash, 0.0)), u_glowIntensity, mainThickness);\n    float mainAfterglowDist = mainDist * 0.6;  // Wider glow for afterimage\n    float mainAfterglowStrength = exp(-mainAfterglowDist * 50.0) * afterimage * 0.5;\n    vec3 mainAfterglow = afterglowColor * mainAfterglowStrength;\n    vec3 mainGlow = mainCore + mainAfterglow;\n\n    // Branches: bright core fades with flash, soft afterglow lingers\n    float branchThickness = mix(0.15, 1.0, easeOutSine(max(flash, 0.0)));\n    vec3 branchCore = lightningGlow(branchDist, branchBrightness * easeOutQuad(max(flash, 0.0)), u_glowIntensity, branchThickness);\n    float branchAfterglowDist = branchDist * 0.7;  // Wider glow\n    float branchAfterglowStrength = exp(-branchAfterglowDist * 80.0) * branchBrightness * afterimage * 0.4;\n    vec3 branchAfterglow = afterglowColor * branchAfterglowStrength;\n\n    // Bright cores - only visible during flash\n    vec3 brightCores = mainCore + branchCore;\n    color += brightCores * max(flash, 0.0);\n\n    // Afterglow - persists based on afterimage (independent of flash)\n    vec3 allAfterglow = mainAfterglow + branchAfterglow;\n    color += allAfterglow * afterimage;\n\n    // Add ambient glow from lightning source\n    float sourceGlow = exp(-length(uv - boltStart) * 3.0);\n    color += vec3(0.4, 0.45, 0.6) * sourceGlow * afterimage * 0.3;\n  }\n\n  // Debug visualization\n  if (u_debug) {\n    // Show strike timing\n    float debugFlash = mod(u_time, 1.0) < 0.5 ? 1.0 : 0.0;\n    color.r += flash * 0.3;\n\n    // Grid\n    vec2 grid = fract(uv * 10.0);\n    float gridLines = step(0.95, grid.x) + step(0.95, grid.y);\n    color = mix(color, vec3(0.3, 0.0, 0.0), gridLines * 0.3);\n  }\n\n  fragColor = vec4(color, 1.0);\n}\n`;\n\nfunction createShader(\n  gl: WebGL2RenderingContext,\n  type: number,\n  source: string,\n): WebGLShader | null {\n  const shader = gl.createShader(type);\n  if (!shader) return null;\n\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n    console.error(\"Shader compile error:\", gl.getShaderInfoLog(shader));\n    gl.deleteShader(shader);\n    return null;\n  }\n\n  return shader;\n}\n\nfunction createProgram(\n  gl: WebGL2RenderingContext,\n  vertexShader: WebGLShader,\n  fragmentShader: WebGLShader,\n): WebGLProgram | null {\n  const program = gl.createProgram();\n  if (!program) return null;\n\n  gl.attachShader(program, vertexShader);\n  gl.attachShader(program, fragmentShader);\n  gl.linkProgram(program);\n\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n    console.error(\"Program link error:\", gl.getProgramInfoLog(program));\n    gl.deleteProgram(program);\n    return null;\n  }\n\n  return program;\n}\n\nexport function LightningCanvas({\n  className,\n  branchDensity = 0.5,\n  displacement = 0.15,\n  glowIntensity = 1.0,\n  flashDuration = 150,\n  sceneIllumination = 0.8,\n  afterglowPersistence = 4.0,\n  triggerCount = 0,\n  autoMode = true,\n  autoInterval = 8,\n  debug = false,\n}: LightningCanvasProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const glRef = useRef<WebGL2RenderingContext | null>(null);\n  const programRef = useRef<WebGLProgram | null>(null);\n  const uniformsRef = useRef<{\n    time: WebGLUniformLocation | null;\n    resolution: WebGLUniformLocation | null;\n    branchDensity: WebGLUniformLocation | null;\n    displacement: WebGLUniformLocation | null;\n    glowIntensity: WebGLUniformLocation | null;\n    flashDuration: WebGLUniformLocation | null;\n    sceneIllumination: WebGLUniformLocation | null;\n    afterglowPersistence: WebGLUniformLocation | null;\n    strikeTime: WebGLUniformLocation | null;\n    strikeSeed: WebGLUniformLocation | null;\n    debug: WebGLUniformLocation | null;\n  } | null>(null);\n  const animationFrameRef = useRef<number>(0);\n  const startTimeRef = useRef<number>(0);\n  const strikeTimeRef = useRef<number>(-10);\n  const strikeSeedRef = useRef<number>(0);\n  const lastAutoStrikeRef = useRef<number>(0);\n  const prevTriggerCountRef = useRef<number>(0);\n\n  const initGL = useCallback(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return false;\n\n    const gl = canvas.getContext(\"webgl2\");\n    if (!gl) {\n      console.error(\"WebGL2 not supported\");\n      return false;\n    }\n    glRef.current = gl;\n\n    const vertexShader = createShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);\n    const fragmentShader = createShader(\n      gl,\n      gl.FRAGMENT_SHADER,\n      FRAGMENT_SHADER,\n    );\n    if (!vertexShader || !fragmentShader) return false;\n\n    const program = createProgram(gl, vertexShader, fragmentShader);\n    if (!program) return false;\n    programRef.current = program;\n\n    uniformsRef.current = {\n      time: gl.getUniformLocation(program, \"u_time\"),\n      resolution: gl.getUniformLocation(program, \"u_resolution\"),\n      branchDensity: gl.getUniformLocation(program, \"u_branchDensity\"),\n      displacement: gl.getUniformLocation(program, \"u_displacement\"),\n      glowIntensity: gl.getUniformLocation(program, \"u_glowIntensity\"),\n      flashDuration: gl.getUniformLocation(program, \"u_flashDuration\"),\n      sceneIllumination: gl.getUniformLocation(program, \"u_sceneIllumination\"),\n      afterglowPersistence: gl.getUniformLocation(\n        program,\n        \"u_afterglowPersistence\",\n      ),\n      strikeTime: gl.getUniformLocation(program, \"u_strikeTime\"),\n      strikeSeed: gl.getUniformLocation(program, \"u_strikeSeed\"),\n      debug: gl.getUniformLocation(program, \"u_debug\"),\n    };\n\n    const positions = new Float32Array([\n      -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1,\n    ]);\n\n    const positionBuffer = gl.createBuffer();\n    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);\n    gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);\n\n    const positionLoc = gl.getAttribLocation(program, \"a_position\");\n    gl.enableVertexAttribArray(positionLoc);\n    gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);\n\n    startTimeRef.current = performance.now();\n    return true;\n  }, []);\n\n  // Trigger strike function\n  const triggerStrike = useCallback(() => {\n    const time = (performance.now() - startTimeRef.current) / 1000;\n    strikeTimeRef.current = time;\n    strikeSeedRef.current = Math.random();\n  }, []);\n\n  // Handle manual triggers\n  useEffect(() => {\n    if (triggerCount > prevTriggerCountRef.current) {\n      triggerStrike();\n    }\n    prevTriggerCountRef.current = triggerCount;\n  }, [triggerCount, triggerStrike]);\n\n  const render = useCallback(() => {\n    const gl = glRef.current;\n    const program = programRef.current;\n    const uniforms = uniformsRef.current;\n    const canvas = canvasRef.current;\n\n    if (!gl || !program || !uniforms || !canvas) return;\n\n    const displayWidth = canvas.clientWidth * window.devicePixelRatio;\n    const displayHeight = canvas.clientHeight * window.devicePixelRatio;\n\n    if (canvas.width !== displayWidth || canvas.height !== displayHeight) {\n      canvas.width = displayWidth;\n      canvas.height = displayHeight;\n      gl.viewport(0, 0, canvas.width, canvas.height);\n    }\n\n    const time = (performance.now() - startTimeRef.current) / 1000;\n\n    // Auto-trigger logic\n    if (autoMode) {\n      const timeSinceLastStrike = time - lastAutoStrikeRef.current;\n      if (timeSinceLastStrike > autoInterval) {\n        strikeTimeRef.current = time;\n        strikeSeedRef.current = Math.random();\n        lastAutoStrikeRef.current = time;\n      }\n    }\n\n    gl.useProgram(program);\n    gl.uniform1f(uniforms.time, time);\n    gl.uniform2f(uniforms.resolution, canvas.width, canvas.height);\n    gl.uniform1f(uniforms.branchDensity, branchDensity);\n    gl.uniform1f(uniforms.displacement, displacement);\n    gl.uniform1f(uniforms.glowIntensity, glowIntensity);\n    gl.uniform1f(uniforms.flashDuration, flashDuration);\n    gl.uniform1f(uniforms.sceneIllumination, sceneIllumination);\n    gl.uniform1f(uniforms.afterglowPersistence, afterglowPersistence);\n    gl.uniform1f(uniforms.strikeTime, strikeTimeRef.current);\n    gl.uniform1f(uniforms.strikeSeed, strikeSeedRef.current);\n    gl.uniform1i(uniforms.debug, debug ? 1 : 0);\n\n    gl.drawArrays(gl.TRIANGLES, 0, 6);\n\n    animationFrameRef.current = requestAnimationFrame(render);\n  }, [\n    branchDensity,\n    displacement,\n    glowIntensity,\n    flashDuration,\n    sceneIllumination,\n    afterglowPersistence,\n    autoMode,\n    autoInterval,\n    debug,\n  ]);\n\n  useEffect(() => {\n    if (initGL()) {\n      render();\n    }\n\n    return () => {\n      if (animationFrameRef.current) {\n        cancelAnimationFrame(animationFrameRef.current);\n      }\n    };\n  }, [initGL, render]);\n\n  return (\n    <canvas\n      ref={canvasRef}\n      className={className}\n      style={{ width: \"100%\", height: \"100%\" }}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/lightning-effect/page.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { useControls, Leva, button } from \"leva\";\nimport { LightningCanvas } from \"./lightning-canvas\";\n\nexport default function LightningEffectSandbox() {\n  const [triggerCount, setTriggerCount] = useState(0);\n\n  const boltShape = useControls(\"Bolt Shape\", {\n    branchDensity: {\n      value: 0.5,\n      min: 0,\n      max: 1,\n      step: 0.05,\n      label: \"Branch Density\",\n    },\n    displacement: {\n      value: 0.15,\n      min: 0.01,\n      max: 0.4,\n      step: 0.01,\n      label: \"Displacement\",\n    },\n    glowIntensity: {\n      value: 1.0,\n      min: 0.1,\n      max: 3,\n      step: 0.1,\n      label: \"Glow Intensity\",\n    },\n  });\n\n  const timing = useControls(\"Timing\", {\n    flashDuration: {\n      value: 150,\n      min: 50,\n      max: 500,\n      step: 10,\n      label: \"Flash Duration (ms)\",\n    },\n    afterglowPersistence: {\n      value: 4.0,\n      min: 1,\n      max: 20,\n      step: 0.5,\n      label: \"Afterglow\",\n    },\n    sceneIllumination: {\n      value: 0.8,\n      min: 0,\n      max: 1,\n      step: 0.05,\n      label: \"Scene Illumination\",\n    },\n  });\n\n  const auto = useControls(\"Auto Mode\", {\n    autoMode: { value: true, label: \"Enabled\" },\n    autoInterval: { value: 8, min: 2, max: 20, step: 1, label: \"Interval (s)\" },\n  });\n\n  useControls(\"Actions\", {\n    \"Trigger Lightning\": button(() => setTriggerCount((c) => c + 1)),\n  });\n\n  const debug = useControls(\"Debug\", {\n    showDebug: { value: false, label: \"Show Debug\" },\n  });\n\n  return (\n    <div className=\"relative min-h-screen bg-black\">\n      <Leva\n        collapsed={false}\n        flat={false}\n        titleBar={{ title: \"Lightning Effect\" }}\n        theme={{\n          sizes: {\n            rootWidth: \"280px\",\n            controlWidth: \"140px\",\n          },\n        }}\n      />\n      <LightningCanvas\n        className=\"absolute inset-0\"\n        branchDensity={boltShape.branchDensity}\n        displacement={boltShape.displacement}\n        glowIntensity={boltShape.glowIntensity}\n        flashDuration={timing.flashDuration}\n        sceneIllumination={timing.sceneIllumination}\n        afterglowPersistence={timing.afterglowPersistence}\n        triggerCount={triggerCount}\n        autoMode={auto.autoMode}\n        autoInterval={auto.autoInterval}\n        debug={debug.showDebug}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/page.tsx",
    "content": "import Link from \"next/link\";\nimport {\n  Sun,\n  Cloud,\n  CloudRain,\n  CloudLightning,\n  Snowflake,\n  Layers,\n  SlidersHorizontal,\n  ThermometerSun,\n  Palette,\n} from \"lucide-react\";\n\nconst sandboxes = [\n  {\n    href: \"/sandbox/weather-tuning\",\n    title: \"Weather Tuning Studio\",\n    description:\n      \"Systematic condition tuning with checkpoints and sign-off workflow\",\n    icon: Palette,\n    featured: true,\n  },\n  {\n    href: \"/sandbox/weather-compositor\",\n    title: \"Weather Compositor\",\n    description:\n      \"Authoring-only custom compositor with condition presets and export/import\",\n    icon: SlidersHorizontal,\n    featured: true,\n  },\n  {\n    href: \"/sandbox/weather-effects\",\n    title: \"Weather Effects Canvas\",\n    description: \"Unified WebGL canvas with all effect layers\",\n    icon: Layers,\n    featured: true,\n  },\n  {\n    href: \"/sandbox/weather-widget\",\n    title: \"Weather Widget\",\n    description: \"Production runtime widget preview\",\n    icon: ThermometerSun,\n  },\n  {\n    href: \"/sandbox/weather-widget-stress\",\n    title: \"Weather Widget Stress Lab\",\n    description: \"Production runtime stress test (WebGL-safe)\",\n    icon: ThermometerSun,\n  },\n  {\n    href: \"/sandbox/celestial-effect\",\n    title: \"Celestial Effect\",\n    description: \"Sun, moon, stars, and sky gradients\",\n    icon: Sun,\n  },\n  {\n    href: \"/sandbox/cloud-effect\",\n    title: \"Cloud Effect\",\n    description: \"Volumetric clouds with lighting and wind\",\n    icon: Cloud,\n  },\n  {\n    href: \"/sandbox/rain-effect\",\n    title: \"Rain Effect\",\n    description: \"Glass drops and falling rain with refraction\",\n    icon: CloudRain,\n  },\n  {\n    href: \"/sandbox/lightning-effect\",\n    title: \"Lightning Effect\",\n    description: \"Procedural lightning bolts with branching\",\n    icon: CloudLightning,\n  },\n  {\n    href: \"/sandbox/snow-effect\",\n    title: \"Snow Effect\",\n    description: \"Layered snowflakes with wind and sparkle\",\n    icon: Snowflake,\n  },\n];\n\nexport default function SandboxIndex() {\n  const featured = sandboxes.filter((s) => s.featured);\n  const effects = sandboxes.filter((s) => !s.featured);\n\n  return (\n    <div className=\"min-h-screen bg-gradient-to-b from-zinc-900 to-black p-8\">\n      <div className=\"mx-auto max-w-3xl\">\n        <h1 className=\"mb-2 text-3xl font-semibold tracking-tight text-white\">\n          Weather Effects Sandbox\n        </h1>\n        <p className=\"mb-8 text-zinc-400\">\n          Development tools for tuning and testing weather effects\n        </p>\n\n        <section className=\"mb-8\">\n          <h2 className=\"mb-4 text-sm font-medium uppercase tracking-wider text-zinc-500\">\n            Compositors\n          </h2>\n          <div className=\"grid gap-3\">\n            {featured.map((sandbox) => (\n              <Link\n                key={sandbox.href}\n                href={sandbox.href}\n                className=\"group flex items-center gap-4 rounded-lg border border-zinc-800 bg-zinc-900/50 p-4 transition-colors hover:border-zinc-700 hover:bg-zinc-800/50\"\n              >\n                <div className=\"flex size-10 items-center justify-center rounded-md bg-blue-500/10 text-blue-400 transition-colors group-hover:bg-blue-500/20\">\n                  <sandbox.icon className=\"size-5\" />\n                </div>\n                <div>\n                  <h3 className=\"font-medium text-white\">{sandbox.title}</h3>\n                  <p className=\"text-sm text-zinc-400\">{sandbox.description}</p>\n                </div>\n              </Link>\n            ))}\n          </div>\n        </section>\n\n        <section>\n          <h2 className=\"mb-4 text-sm font-medium uppercase tracking-wider text-zinc-500\">\n            Individual Effects\n          </h2>\n          <div className=\"grid gap-3 sm:grid-cols-2\">\n            {effects.map((sandbox) => (\n              <Link\n                key={sandbox.href}\n                href={sandbox.href}\n                className=\"group flex items-center gap-3 rounded-lg border border-zinc-800 bg-zinc-900/50 p-3 transition-colors hover:border-zinc-700 hover:bg-zinc-800/50\"\n              >\n                <div className=\"flex size-9 items-center justify-center rounded-md bg-zinc-800 text-zinc-400 transition-colors group-hover:bg-zinc-700 group-hover:text-zinc-300\">\n                  <sandbox.icon className=\"size-4\" />\n                </div>\n                <div>\n                  <h3 className=\"text-sm font-medium text-white\">\n                    {sandbox.title}\n                  </h3>\n                  <p className=\"text-xs text-zinc-500\">{sandbox.description}</p>\n                </div>\n              </Link>\n            ))}\n          </div>\n        </section>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/rain-effect/page.tsx",
    "content": "\"use client\";\n\nimport { useControls, Leva } from \"leva\";\nimport { RainCanvas } from \"./rain-canvas\";\n\nexport default function RainEffectSandbox() {\n  const fallingRain = useControls(\"Falling Rain\", {\n    fallingIntensity: {\n      value: 0.6,\n      min: 0,\n      max: 1,\n      step: 0.01,\n      label: \"Intensity\",\n    },\n    fallingSpeed: { value: 1.0, min: 0.1, max: 5, step: 0.1, label: \"Speed\" },\n    fallingAngle: {\n      value: 0.1,\n      min: -1,\n      max: 1,\n      step: 0.01,\n      label: \"Wind Angle\",\n    },\n    fallingStreakLength: {\n      value: 1.0,\n      min: 0.1,\n      max: 5,\n      step: 0.1,\n      label: \"Streak Length\",\n    },\n    fallingLayers: { value: 4, min: 1, max: 6, step: 1, label: \"Depth Layers\" },\n  });\n\n  const rainAppearance = useControls(\"Rain Appearance\", {\n    fallingRefraction: {\n      value: 0.4,\n      min: 0,\n      max: 2,\n      step: 0.05,\n      label: \"Refraction\",\n    },\n    fallingWaviness: {\n      value: 1.0,\n      min: 0,\n      max: 3,\n      step: 0.1,\n      label: \"Waviness\",\n    },\n    fallingThicknessVar: {\n      value: 1.0,\n      min: 0,\n      max: 3,\n      step: 0.1,\n      label: \"Thickness Var\",\n    },\n  });\n\n  const glassDrops = useControls(\"Glass Drops\", {\n    glassIntensity: {\n      value: 0.5,\n      min: 0,\n      max: 1,\n      step: 0.01,\n      label: \"Intensity\",\n    },\n    zoom: { value: 1.0, min: 0.5, max: 3, step: 0.1, label: \"Zoom\" },\n  });\n\n  const debug = useControls(\"Debug\", {\n    showDebug: { value: false, label: \"Show Debug\" },\n  });\n\n  return (\n    <div className=\"relative min-h-screen bg-black\">\n      <Leva\n        collapsed={false}\n        flat={false}\n        titleBar={{ title: \"Rain Effect\" }}\n        theme={{\n          sizes: {\n            rootWidth: \"280px\",\n            controlWidth: \"140px\",\n          },\n        }}\n      />\n      <RainCanvas\n        className=\"absolute inset-0\"\n        glassIntensity={glassDrops.glassIntensity}\n        fallingIntensity={fallingRain.fallingIntensity}\n        fallingSpeed={fallingRain.fallingSpeed}\n        fallingAngle={fallingRain.fallingAngle}\n        fallingStreakLength={fallingRain.fallingStreakLength}\n        fallingLayers={fallingRain.fallingLayers}\n        fallingRefraction={rainAppearance.fallingRefraction}\n        fallingWaviness={rainAppearance.fallingWaviness}\n        fallingThicknessVar={rainAppearance.fallingThicknessVar}\n        zoom={glassDrops.zoom}\n        debug={debug.showDebug}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/rain-effect/rain-canvas.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useRef, useCallback } from \"react\";\n\ninterface RainCanvasProps {\n  className?: string;\n  // Glass drops (rain on window)\n  glassIntensity?: number;\n  zoom?: number;\n  // Falling rain (rain in the air)\n  fallingIntensity?: number;\n  fallingSpeed?: number;\n  fallingAngle?: number;\n  fallingStreakLength?: number;\n  fallingLayers?: number;\n  fallingRefraction?: number;\n  fallingWaviness?: number;\n  fallingThicknessVar?: number;\n  // Debug\n  debug?: boolean;\n}\n\nconst VERTEX_SHADER = `#version 300 es\nin vec4 a_position;\nout vec2 v_uv;\n\nvoid main() {\n  gl_Position = a_position;\n  v_uv = a_position.xy * 0.5 + 0.5;\n}\n`;\n\n// Inspired by \"Heartfelt\" by Martijn Steinrucken aka BigWings\n// Original: https://www.shadertoy.com/view/ltffzl\nconst FRAGMENT_SHADER = `#version 300 es\nprecision highp float;\n\nin vec2 v_uv;\nout vec4 fragColor;\n\nuniform float u_time;\nuniform vec2 u_resolution;\nuniform float u_glassIntensity;\nuniform float u_zoom;\nuniform float u_fallingIntensity;\nuniform float u_fallingSpeed;\nuniform float u_fallingAngle;\nuniform float u_fallingStreakLength;\nuniform int u_fallingLayers;\nuniform float u_fallingRefraction;\nuniform float u_fallingWaviness;\nuniform float u_fallingThicknessVar;\nuniform bool u_debug;\n\n#define S(a, b, t) smoothstep(a, b, t)\n\n// ============================================================================\n// NOISE FUNCTIONS\n// ============================================================================\n\n// 3D noise from 1D input\nvec3 N13(float p) {\n  vec3 p3 = fract(vec3(p) * vec3(0.1031, 0.11369, 0.13787));\n  p3 += dot(p3, p3.yzx + 19.19);\n  return fract(vec3((p3.x + p3.y) * p3.z, (p3.x + p3.z) * p3.y, (p3.y + p3.z) * p3.x));\n}\n\n// Simple 1D noise\nfloat N(float t) {\n  return fract(sin(t * 12345.564) * 7658.76);\n}\n\n// Saw wave - ramps up then down\nfloat Saw(float b, float t) {\n  return S(0.0, b, t) * S(1.0, b, t);\n}\n\n// 2D noise for background\nfloat noise2D(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  f = f * f * (3.0 - 2.0 * f);\n\n  float a = fract(sin(dot(i, vec2(127.1, 311.7))) * 43758.5453);\n  float b = fract(sin(dot(i + vec2(1.0, 0.0), vec2(127.1, 311.7))) * 43758.5453);\n  float c = fract(sin(dot(i + vec2(0.0, 1.0), vec2(127.1, 311.7))) * 43758.5453);\n  float d = fract(sin(dot(i + vec2(1.0, 1.0), vec2(127.1, 311.7))) * 43758.5453);\n\n  return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);\n}\n\n// FBM for richer textures\nfloat fbm(vec2 p) {\n  float f = 0.0;\n  float w = 0.5;\n  for (int i = 0; i < 4; i++) {\n    f += w * noise2D(p);\n    p *= 2.0;\n    w *= 0.5;\n  }\n  return f;\n}\n\n// ============================================================================\n// BACKGROUND - Rainy night city\n// ============================================================================\n\nvec3 cityLights(vec2 uv) {\n  vec3 color = vec3(0.0);\n\n  // Dark sky gradient\n  vec3 skyTop = vec3(0.02, 0.03, 0.06);\n  vec3 skyBottom = vec3(0.01, 0.015, 0.025);\n  color = mix(skyBottom, skyTop, uv.y);\n\n  // Horizon glow\n  float horizonGlow = exp(-pow((uv.y - 0.15) * 4.0, 2.0));\n  color += vec3(0.15, 0.08, 0.05) * horizonGlow * 0.5;\n\n  // Atmospheric haze\n  float fog = fbm(uv * 3.0 + u_time * 0.02) * 0.08;\n  color += vec3(0.04, 0.05, 0.07) * fog;\n\n  return color;\n}\n\n// ============================================================================\n// ANIMATED DROP LAYER (with trails)\n// ============================================================================\n\nvec2 DropLayer(vec2 uv, float t) {\n  vec2 UV = uv;\n\n  // Scroll down\n  uv.y += t * 0.75;\n\n  // Grid setup - wider cells horizontally\n  vec2 aspect = vec2(6.0, 1.0);\n  vec2 grid = aspect * 2.0;\n  vec2 id = floor(uv * grid);\n\n  // Shift columns for variation\n  float colShift = N(id.x);\n  uv.y += colShift;\n\n  // Recalculate ID after shift\n  id = floor(uv * grid);\n  vec3 n = N13(id.x * 35.2 + id.y * 2376.1);\n\n  // Position within cell\n  vec2 st = fract(uv * grid) - vec2(0.5, 0.0);\n\n  // Drop horizontal position with wiggle\n  float x = n.x - 0.5;\n  float y = UV.y * 20.0;\n\n  // Organic wiggle motion: sin(y + sin(y)) creates natural sway\n  float wiggle = sin(y + sin(y));\n  x += wiggle * (0.5 - abs(x)) * (n.z - 0.5);\n  x *= 0.7;\n\n  // Temporal animation - drop falls and resets\n  float ti = fract(t + n.z);\n  y = (Saw(0.85, ti) - 0.5) * 0.9 + 0.5;\n\n  vec2 p = vec2(x, y);\n\n  // Main drop shape\n  float d = length((st - p) * aspect.yx);\n  float mainDrop = S(0.4, 0.0, d);\n\n  // Trail behind the drop\n  float r = sqrt(S(1.0, y, st.y));\n  float cd = abs(st.x - x);\n  float trail = S(0.23 * r, 0.15 * r * r, cd);\n  float trailFront = S(-0.02, 0.02, st.y - y);\n  trail *= trailFront * r * r;\n\n  // Secondary droplets along the trail\n  float y2 = UV.y;\n  float trail2 = S(0.2 * r, 0.0, cd);\n  float droplets = max(0.0, (sin(y2 * (1.0 - y2) * 120.0) - st.y)) * trail2 * trailFront * n.z;\n\n  // Small drops along trail\n  y2 = fract(y2 * 10.0) + (st.y - 0.5);\n  float dd = length(st - vec2(x, y2));\n  droplets = S(0.3, 0.0, dd);\n\n  float m = mainDrop + droplets * r * trailFront;\n\n  return vec2(m, trail);\n}\n\n// ============================================================================\n// STATIC DROPS (small stationary drops on glass)\n// ============================================================================\n\nfloat StaticDrops(vec2 uv, float t) {\n  uv *= 40.0;\n\n  vec2 id = floor(uv);\n  uv = fract(uv) - 0.5;\n\n  vec3 n = N13(id.x * 107.45 + id.y * 3543.654);\n  vec2 p = (n.xy - 0.5) * 0.7;\n\n  float d = length(uv - p);\n\n  // Pulse in and out\n  float fade = Saw(0.025, fract(t + n.z));\n  float c = S(0.3, 0.0, d) * fract(n.z * 10.0) * fade;\n\n  return c;\n}\n\n// ============================================================================\n// COMBINED DROPS (Glass effect)\n// ============================================================================\n\nvec2 Drops(vec2 uv, float t, float l0, float l1, float l2) {\n  // Static drops\n  float s = StaticDrops(uv, t) * l0;\n\n  // Two animated layers at different scales\n  vec2 m1 = DropLayer(uv, t) * l1;\n  vec2 m2 = DropLayer(uv * 1.85, t) * l2;\n\n  // Combine\n  float c = s + m1.x + m2.x;\n  c = S(0.3, 1.0, c);\n\n  return vec2(c, max(m1.y * l0, m2.y * l1));\n}\n\n// ============================================================================\n// FALLING RAIN (Rain streaks in the air)\n// ============================================================================\n\n// Hash function for pseudo-random values\nfloat hash12(vec2 p) {\n  vec3 p3 = fract(vec3(p.xyx) * 0.1031);\n  p3 += dot(p3, p3.yzx + 33.33);\n  return fract((p3.x + p3.y) * p3.z);\n}\n\nvec2 hash22(vec2 p) {\n  vec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973));\n  p3 += dot(p3, p3.yzx + 33.33);\n  return fract((p3.xx + p3.yz) * p3.zy);\n}\n\n\n// Attempt a streak hit with organic shape\n// Returns: x = refraction offset, y = streak mask\nvec2 OrganicStreak(vec2 localUV, float streakH, float baseWidth, float seed, float seed2) {\n  vec2 d = localUV;\n\n  // Normalize position along streak (0 = top, 1 = bottom)\n  float t = (d.y + streakH) / (2.0 * streakH);\n  t = clamp(t, 0.0, 1.0);\n\n  // Early exit if clearly outside\n  if (abs(d.y) > streakH * 1.2) return vec2(0.0);\n\n  // === WAVY PATH ===\n  // Streak doesn't fall straight - has slight S-curve wiggle\n  float waveFreq = 2.0 + seed * 3.0;\n  float waveAmp = baseWidth * (1.0 + seed2 * 2.0) * u_fallingWaviness;\n  float wave = sin(t * waveFreq * 3.14159 + seed * 10.0) * waveAmp;\n  d.x -= wave;\n\n  // === VARIABLE THICKNESS ===\n  // Width varies along length: thicker near top, thinner at bottom, with noise\n  float taper = mix(1.3, 0.4, t * t); // Basic taper\n  float thicknessNoiseAmt = 0.2 * u_fallingThicknessVar;\n  float thicknessNoise = sin(t * 15.0 + seed * 20.0) * thicknessNoiseAmt + 1.0;\n  float width = baseWidth * taper * thicknessNoise;\n\n  // === CORE SHAPE ===\n  // Soft-edged streak\n  float coreDist = abs(d.x);\n  float core = S(width, width * 0.2, coreDist);\n\n  // Vertical bounds: soft fade at top and bottom\n  float vertFade = S(0.0, 0.1, t) * S(1.0, 0.85, t);\n\n  // === INTERNAL BRIGHTNESS VARIATION ===\n  // Simulates light refracting through oscillating raindrop\n  float oscillation = sin(t * 25.0 + seed * 30.0) * 0.25 + 0.75;\n  float speckle = sin(t * 60.0 + seed2 * 50.0) * 0.15 + 0.85;\n\n  // === BREAK INTO SEGMENTS ===\n  // Some streaks appear broken/dotted (especially longer ones)\n  float breakup = 1.0;\n  if (seed > 0.6) {\n    // This streak has gaps\n    float gapPattern = sin(t * 8.0 + seed * 15.0);\n    breakup = S(-0.3, 0.1, gapPattern);\n  }\n\n  // === BRIGHT HEAD ===\n  // The leading edge (top) of the drop catches more light\n  float headBrightness = S(0.15, 0.0, t) * 1.5;\n\n  // Combine all factors\n  float streak = core * vertFade * oscillation * speckle * breakup;\n  streak += headBrightness * core * S(0.2, 0.0, t);\n  streak = clamp(streak, 0.0, 1.0);\n\n  // Refraction direction based on position relative to wavy center\n  float refract = d.x * streak;\n\n  return vec2(refract, streak);\n}\n\n// Falling rain layer with organic streaks\nvec2 FallingRainLayer(vec2 uv, float t, float speed, float windAngle, float streakLen, float scale, float density) {\n  vec2 offset = vec2(0.0);\n\n  // Apply wind shear\n  vec2 p = uv;\n  p.x += p.y * windAngle;\n\n  // Scale and scroll\n  p *= scale;\n  p.y += t * speed;\n\n  // Grid\n  vec2 id = floor(p);\n  vec2 gv = fract(p) - 0.5;\n\n  // Check neighboring cells\n  for (int y = -1; y <= 1; y++) {\n    for (int x = -1; x <= 1; x++) {\n      vec2 offs = vec2(float(x), float(y));\n      vec2 cellId = id + offs;\n\n      // Random values for this cell\n      float n1 = hash12(cellId);\n      vec2 n2 = hash22(cellId * 17.23);\n      float n3 = hash12(cellId * 31.17);\n\n      // Skip based on density\n      if (n1 > density) continue;\n\n      // Drop position within cell\n      vec2 dropPos = offs + n2 - 0.5;\n\n      // Local coordinates relative to drop\n      vec2 localUV = gv - dropPos;\n\n      // Streak dimensions with variation\n      float streakW = 0.025 + n1 * 0.02;\n      float streakH = streakLen * (0.4 + n3 * 0.6);\n\n      // Get organic streak\n      vec2 result = OrganicStreak(localUV, streakH, streakW, n1, n3);\n\n      if (result.y > 0.001) {\n        offset.x += result.x * 0.5;\n        offset.y += (n1 - 0.5) * result.y * 0.1;\n      }\n    }\n  }\n\n  return offset;\n}\n\n// Returns refraction offset for falling rain (to distort background sampling)\nvec2 FallingRain(vec2 uv, float t) {\n  vec2 totalOffset = vec2(0.0);\n\n  if (u_fallingIntensity < 0.01) return totalOffset;\n\n  float speed = u_fallingSpeed * 5.0;\n  float windAngle = u_fallingAngle;\n  float streakLen = u_fallingStreakLength * 0.3;\n  float intensity = u_fallingIntensity;\n\n  int layers = u_fallingLayers;\n\n  // Multiple depth layers\n  for (int i = 0; i < 6; i++) {\n    if (i >= layers) break;\n\n    float layerIdx = float(i);\n    float depth = layerIdx / float(max(layers - 1, 1));\n\n    // Layer parameters: closer = larger streaks, faster, denser, more refraction\n    float layerScale = mix(6.0, 30.0, depth);\n    float layerSpeed = speed * mix(2.0, 0.5, depth);\n    float layerDensity = intensity * mix(0.8, 0.3, depth);\n    float layerStrength = mix(1.0, 0.15, depth);\n    float layerStreakLen = streakLen * mix(1.5, 0.4, depth);\n    float layerAngle = windAngle * mix(1.0, 0.6, depth);\n\n    // Unique offset per layer to prevent alignment\n    vec2 layerOffset = vec2(\n      sin(layerIdx * 73.156) * 3.0,\n      cos(layerIdx * 37.842) * 3.0\n    );\n\n    vec2 layer = FallingRainLayer(\n      uv + layerOffset,\n      t + layerIdx * 0.13,\n      layerSpeed,\n      layerAngle,\n      layerStreakLen,\n      layerScale,\n      layerDensity\n    );\n\n    totalOffset += layer * layerStrength;\n  }\n\n  // Scale refraction to screen space - controlled by uniform\n  return totalOffset * u_fallingRefraction;\n}\n\n// ============================================================================\n// MAIN\n// ============================================================================\n\nvoid main() {\n  // Normalized coordinates centered at origin\n  vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / u_resolution.y;\n  vec2 UV = gl_FragCoord.xy / u_resolution.xy;\n\n  // Apply zoom\n  uv *= u_zoom;\n\n  float t = u_time * 0.2;\n\n  // Glass drops intensity\n  float rainAmount = u_glassIntensity;\n\n  // Layer intensities based on rain amount\n  float staticDrops = S(-0.5, 1.0, rainAmount) * 2.0;\n  float layer1 = S(0.25, 0.75, rainAmount);\n  float layer2 = S(0.0, 0.5, rainAmount);\n\n  // Calculate glass drops\n  vec2 c = Drops(uv, t, staticDrops, layer1, layer2);\n\n  // Calculate normals by sampling at offsets (gradient-based) for glass drops\n  vec2 e = vec2(0.001, 0.0);\n  float cx = Drops(uv + e, t, staticDrops, layer1, layer2).x;\n  float cy = Drops(uv + e.yx, t, staticDrops, layer1, layer2).x;\n  vec2 glassNormal = vec2(cx - c.x, cy - c.x);\n\n  // Get falling rain refraction offset\n  vec2 fallingRainOffset = FallingRain(uv, u_time);\n\n  // Combine refractions: glass drops + falling rain\n  vec2 totalRefraction = glassNormal + fallingRainOffset;\n\n  // Sample background with combined refraction\n  vec2 refractedUV = UV + totalRefraction;\n  refractedUV = clamp(refractedUV, 0.0, 1.0);\n\n  vec3 color = cityLights(refractedUV);\n\n  // Falling rain creates subtle specular highlights where it refracts light\n  // This makes rain visible even against dark backgrounds\n  float rainMagnitude = length(fallingRainOffset);\n  if (rainMagnitude > 0.001) {\n    // Sample what's being refracted - if it's bright, the rain catches that light\n    vec3 refractedLight = cityLights(refractedUV);\n    float brightness = dot(refractedLight, vec3(0.299, 0.587, 0.114));\n\n    // Subtle specular highlight - brighter where refracting bright areas\n    float specular = rainMagnitude * 15.0 * (0.1 + brightness * 0.9);\n    color += vec3(0.8, 0.85, 0.95) * specular * 0.3;\n  }\n\n  // Add slight brightness to glass drops themselves\n  color += vec3(0.1, 0.12, 0.15) * c.x * 0.5;\n\n  // Debug mode\n  if (u_debug) {\n    // Show grid\n    vec2 grid = fract(uv * 12.0);\n    float gridLines = step(0.98, grid.x) + step(0.98, grid.y);\n    color = mix(color, vec3(1.0, 0.0, 0.0), gridLines * 0.3);\n\n    // Show normals (combined refraction)\n    color.rg += abs(totalRefraction) * 50.0;\n\n    // Show drop mask\n    color.b += c.x * 0.5;\n  }\n\n  fragColor = vec4(color, 1.0);\n}\n`;\n\nfunction createShader(\n  gl: WebGL2RenderingContext,\n  type: number,\n  source: string,\n): WebGLShader | null {\n  const shader = gl.createShader(type);\n  if (!shader) return null;\n\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n    console.error(\"Shader compile error:\", gl.getShaderInfoLog(shader));\n    gl.deleteShader(shader);\n    return null;\n  }\n\n  return shader;\n}\n\nfunction createProgram(\n  gl: WebGL2RenderingContext,\n  vertexShader: WebGLShader,\n  fragmentShader: WebGLShader,\n): WebGLProgram | null {\n  const program = gl.createProgram();\n  if (!program) return null;\n\n  gl.attachShader(program, vertexShader);\n  gl.attachShader(program, fragmentShader);\n  gl.linkProgram(program);\n\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n    console.error(\"Program link error:\", gl.getProgramInfoLog(program));\n    gl.deleteProgram(program);\n    return null;\n  }\n\n  return program;\n}\n\nexport function RainCanvas({\n  className,\n  glassIntensity = 0.5,\n  zoom = 1.0,\n  fallingIntensity = 0.6,\n  fallingSpeed = 1.0,\n  fallingAngle = 0.1,\n  fallingStreakLength = 1.0,\n  fallingLayers = 4,\n  fallingRefraction = 0.4,\n  fallingWaviness = 1.0,\n  fallingThicknessVar = 1.0,\n  debug = false,\n}: RainCanvasProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const glRef = useRef<WebGL2RenderingContext | null>(null);\n  const programRef = useRef<WebGLProgram | null>(null);\n  const uniformsRef = useRef<{\n    time: WebGLUniformLocation | null;\n    resolution: WebGLUniformLocation | null;\n    glassIntensity: WebGLUniformLocation | null;\n    zoom: WebGLUniformLocation | null;\n    fallingIntensity: WebGLUniformLocation | null;\n    fallingSpeed: WebGLUniformLocation | null;\n    fallingAngle: WebGLUniformLocation | null;\n    fallingStreakLength: WebGLUniformLocation | null;\n    fallingLayers: WebGLUniformLocation | null;\n    fallingRefraction: WebGLUniformLocation | null;\n    fallingWaviness: WebGLUniformLocation | null;\n    fallingThicknessVar: WebGLUniformLocation | null;\n    debug: WebGLUniformLocation | null;\n  } | null>(null);\n  const animationFrameRef = useRef<number>(0);\n  const startTimeRef = useRef<number>(0);\n\n  const initGL = useCallback(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return false;\n\n    const gl = canvas.getContext(\"webgl2\");\n    if (!gl) {\n      console.error(\"WebGL2 not supported\");\n      return false;\n    }\n    glRef.current = gl;\n\n    const vertexShader = createShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);\n    const fragmentShader = createShader(\n      gl,\n      gl.FRAGMENT_SHADER,\n      FRAGMENT_SHADER,\n    );\n    if (!vertexShader || !fragmentShader) return false;\n\n    const program = createProgram(gl, vertexShader, fragmentShader);\n    if (!program) return false;\n    programRef.current = program;\n\n    uniformsRef.current = {\n      time: gl.getUniformLocation(program, \"u_time\"),\n      resolution: gl.getUniformLocation(program, \"u_resolution\"),\n      glassIntensity: gl.getUniformLocation(program, \"u_glassIntensity\"),\n      zoom: gl.getUniformLocation(program, \"u_zoom\"),\n      fallingIntensity: gl.getUniformLocation(program, \"u_fallingIntensity\"),\n      fallingSpeed: gl.getUniformLocation(program, \"u_fallingSpeed\"),\n      fallingAngle: gl.getUniformLocation(program, \"u_fallingAngle\"),\n      fallingStreakLength: gl.getUniformLocation(\n        program,\n        \"u_fallingStreakLength\",\n      ),\n      fallingLayers: gl.getUniformLocation(program, \"u_fallingLayers\"),\n      fallingRefraction: gl.getUniformLocation(program, \"u_fallingRefraction\"),\n      fallingWaviness: gl.getUniformLocation(program, \"u_fallingWaviness\"),\n      fallingThicknessVar: gl.getUniformLocation(\n        program,\n        \"u_fallingThicknessVar\",\n      ),\n      debug: gl.getUniformLocation(program, \"u_debug\"),\n    };\n\n    const positions = new Float32Array([\n      -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1,\n    ]);\n\n    const positionBuffer = gl.createBuffer();\n    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);\n    gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);\n\n    const positionLoc = gl.getAttribLocation(program, \"a_position\");\n    gl.enableVertexAttribArray(positionLoc);\n    gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);\n\n    startTimeRef.current = performance.now();\n    return true;\n  }, []);\n\n  const render = useCallback(() => {\n    const gl = glRef.current;\n    const program = programRef.current;\n    const uniforms = uniformsRef.current;\n    const canvas = canvasRef.current;\n\n    if (!gl || !program || !uniforms || !canvas) return;\n\n    const displayWidth = canvas.clientWidth * window.devicePixelRatio;\n    const displayHeight = canvas.clientHeight * window.devicePixelRatio;\n\n    if (canvas.width !== displayWidth || canvas.height !== displayHeight) {\n      canvas.width = displayWidth;\n      canvas.height = displayHeight;\n      gl.viewport(0, 0, canvas.width, canvas.height);\n    }\n\n    const time = (performance.now() - startTimeRef.current) / 1000;\n\n    gl.useProgram(program);\n    gl.uniform1f(uniforms.time, time);\n    gl.uniform2f(uniforms.resolution, canvas.width, canvas.height);\n    gl.uniform1f(uniforms.glassIntensity, glassIntensity);\n    gl.uniform1f(uniforms.zoom, zoom);\n    gl.uniform1f(uniforms.fallingIntensity, fallingIntensity);\n    gl.uniform1f(uniforms.fallingSpeed, fallingSpeed);\n    gl.uniform1f(uniforms.fallingAngle, fallingAngle);\n    gl.uniform1f(uniforms.fallingStreakLength, fallingStreakLength);\n    gl.uniform1i(uniforms.fallingLayers, fallingLayers);\n    gl.uniform1f(uniforms.fallingRefraction, fallingRefraction);\n    gl.uniform1f(uniforms.fallingWaviness, fallingWaviness);\n    gl.uniform1f(uniforms.fallingThicknessVar, fallingThicknessVar);\n    gl.uniform1i(uniforms.debug, debug ? 1 : 0);\n\n    gl.drawArrays(gl.TRIANGLES, 0, 6);\n\n    animationFrameRef.current = requestAnimationFrame(render);\n  }, [\n    glassIntensity,\n    zoom,\n    fallingIntensity,\n    fallingSpeed,\n    fallingAngle,\n    fallingStreakLength,\n    fallingLayers,\n    fallingRefraction,\n    fallingWaviness,\n    fallingThicknessVar,\n    debug,\n  ]);\n\n  useEffect(() => {\n    if (initGL()) {\n      render();\n    }\n\n    return () => {\n      if (animationFrameRef.current) {\n        cancelAnimationFrame(animationFrameRef.current);\n      }\n    };\n  }, [initGL, render]);\n\n  return (\n    <canvas\n      ref={canvasRef}\n      className={className}\n      style={{ width: \"100%\", height: \"100%\" }}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/snow-effect/page.tsx",
    "content": "\"use client\";\n\nimport { useControls, Leva } from \"leva\";\nimport { SnowCanvas } from \"./snow-canvas\";\n\nexport default function SnowEffectSandbox() {\n  const density = useControls(\"Density\", {\n    intensity: { value: 0.5, min: 0.1, max: 1, step: 0.01, label: \"Intensity\" },\n    layers: { value: 4, min: 1, max: 6, step: 1, label: \"Depth Layers\" },\n  });\n\n  const motion = useControls(\"Motion\", {\n    fallSpeed: {\n      value: 0.6,\n      min: 0.2,\n      max: 2,\n      step: 0.05,\n      label: \"Fall Speed\",\n    },\n    windSpeed: { value: 0.3, min: 0, max: 3, step: 0.05, label: \"Wind Speed\" },\n    windAngle: { value: 0.2, min: -1, max: 1, step: 0.05, label: \"Wind Angle\" },\n    windShear: { value: 0.5, min: 0, max: 2, step: 0.05, label: \"Wind Shear\" },\n    turbulence: { value: 0.3, min: 0, max: 1, step: 0.05, label: \"Turbulence\" },\n    drift: { value: 0.5, min: 0, max: 2, step: 0.05, label: \"Drift\" },\n    flutter: { value: 0.5, min: 0, max: 1, step: 0.05, label: \"Flutter\" },\n  });\n\n  const appearance = useControls(\"Appearance\", {\n    flakeSize: { value: 1.0, min: 0.3, max: 3, step: 0.1, label: \"Flake Size\" },\n    sizeVariation: {\n      value: 0.5,\n      min: 0,\n      max: 1,\n      step: 0.05,\n      label: \"Size Variation\",\n    },\n    opacity: { value: 0.8, min: 0.2, max: 1, step: 0.05, label: \"Opacity\" },\n    glowAmount: { value: 0.5, min: 0, max: 1, step: 0.05, label: \"Glow\" },\n    sparkle: { value: 0.5, min: 0, max: 1, step: 0.05, label: \"Sparkle\" },\n  });\n\n  const blizzard = useControls(\"Blizzard\", {\n    visibility: {\n      value: 1.0,\n      min: 0.2,\n      max: 1,\n      step: 0.05,\n      label: \"Visibility\",\n    },\n  });\n\n  const debug = useControls(\"Debug\", {\n    showDebug: { value: false, label: \"Show Debug\" },\n  });\n\n  return (\n    <div className=\"relative min-h-screen bg-gradient-to-b from-slate-800 to-slate-900\">\n      <Leva\n        collapsed={false}\n        flat={false}\n        titleBar={{ title: \"Snow Effect\" }}\n        theme={{\n          sizes: {\n            rootWidth: \"280px\",\n            controlWidth: \"140px\",\n          },\n        }}\n      />\n      <SnowCanvas\n        className=\"absolute inset-0\"\n        intensity={density.intensity}\n        layers={density.layers}\n        fallSpeed={motion.fallSpeed}\n        windSpeed={motion.windSpeed}\n        windAngle={motion.windAngle}\n        windShear={motion.windShear}\n        turbulence={motion.turbulence}\n        drift={motion.drift}\n        flutter={motion.flutter}\n        flakeSize={appearance.flakeSize}\n        sizeVariation={appearance.sizeVariation}\n        opacity={appearance.opacity}\n        glowAmount={appearance.glowAmount}\n        sparkle={appearance.sparkle}\n        visibility={blizzard.visibility}\n        debug={debug.showDebug}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/snow-effect/snow-canvas.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useRef, useCallback } from \"react\";\n\ninterface SnowCanvasProps {\n  className?: string;\n  // Density\n  intensity?: number;\n  layers?: number;\n  // Motion\n  fallSpeed?: number;\n  windSpeed?: number;\n  windAngle?: number;\n  turbulence?: number;\n  drift?: number;\n  flutter?: number;\n  windShear?: number;\n  // Appearance\n  flakeSize?: number;\n  sizeVariation?: number;\n  opacity?: number;\n  glowAmount?: number;\n  sparkle?: number;\n  // Blizzard\n  visibility?: number;\n  // Debug\n  debug?: boolean;\n}\n\nconst VERTEX_SHADER = `#version 300 es\nin vec4 a_position;\nout vec2 v_uv;\n\nvoid main() {\n  gl_Position = a_position;\n  v_uv = a_position.xy * 0.5 + 0.5;\n}\n`;\n\nconst FRAGMENT_SHADER = `#version 300 es\nprecision highp float;\n\nin vec2 v_uv;\nout vec4 fragColor;\n\nuniform float u_time;\nuniform vec2 u_resolution;\nuniform float u_intensity;\nuniform int u_layers;\nuniform float u_fallSpeed;\nuniform float u_windSpeed;\nuniform float u_windAngle;\nuniform float u_turbulence;\nuniform float u_drift;\nuniform float u_flutter;\nuniform float u_windShear;\nuniform float u_flakeSize;\nuniform float u_sizeVariation;\nuniform float u_opacity;\nuniform float u_glowAmount;\nuniform float u_sparkle;\nuniform float u_visibility;\nuniform bool u_debug;\n\n#define PI 3.14159265359\n#define MAX_LAYERS 6\n\n// ============================================================================\n// NOISE FUNCTIONS\n// ============================================================================\n\nfloat hash12(vec2 p) {\n  vec3 p3 = fract(vec3(p.xyx) * 0.1031);\n  p3 += dot(p3, p3.yzx + 33.33);\n  return fract((p3.x + p3.y) * p3.z);\n}\n\nvec2 hash22(vec2 p) {\n  vec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973));\n  p3 += dot(p3, p3.yzx + 33.33);\n  return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfloat noise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  f = f * f * (3.0 - 2.0 * f);\n\n  float a = hash12(i);\n  float b = hash12(i + vec2(1.0, 0.0));\n  float c = hash12(i + vec2(0.0, 1.0));\n  float d = hash12(i + vec2(1.0, 1.0));\n\n  return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);\n}\n\nfloat fbm(vec2 p, int octaves) {\n  float value = 0.0;\n  float amplitude = 0.5;\n  for (int i = 0; i < 4; i++) {\n    if (i >= octaves) break;\n    value += amplitude * noise(p);\n    p *= 2.0;\n    amplitude *= 0.5;\n  }\n  return value;\n}\n\n// ============================================================================\n// 2D ROTATION\n// ============================================================================\n\nvec2 rotate2D(vec2 p, float angle) {\n  float c = cos(angle);\n  float s = sin(angle);\n  return vec2(p.x * c - p.y * s, p.x * s + p.y * c);\n}\n\n// ============================================================================\n// SNOWFLAKE SHAPE (with rotation)\n// ============================================================================\n\nfloat snowflakeShape(vec2 uv, float size, float seed, float rotation) {\n  // Apply rotation to UV\n  vec2 rotatedUV = rotate2D(uv, rotation);\n\n  float dist = length(rotatedUV);\n\n  // Base soft circle\n  float circle = smoothstep(size, size * 0.3, dist);\n\n  // Add subtle 6-fold symmetry for larger flakes\n  float angle = atan(rotatedUV.y, rotatedUV.x);\n  float hexPattern = 0.5 + 0.5 * cos(angle * 6.0);\n  hexPattern = pow(hexPattern, 2.0);\n\n  // Mix based on flake size (larger = more crystalline)\n  float crystalAmount = smoothstep(0.02, 0.05, size) * 0.3;\n  float shape = mix(circle, circle * (0.7 + hexPattern * 0.3), crystalAmount);\n\n  // Soft glow (uses non-rotated distance for circular glow)\n  float glow = exp(-dist * dist / (size * size * 3.0)) * u_glowAmount;\n\n  return shape + glow * 0.4;\n}\n\n// ============================================================================\n// WIND & TURBULENCE\n// ============================================================================\n\nvec2 getWind(vec2 uv, float time, float layerDepth, float flakeSize) {\n  // Base wind direction\n  vec2 baseWind = vec2(cos(u_windAngle), 0.0) * u_windSpeed;\n\n  // Turbulent gusts - vary by position and time\n  float gustFreq = 0.5 + u_turbulence;\n  float gust = noise(uv * gustFreq + time * 0.2) * 2.0 - 1.0;\n\n  // Add turbulent eddies\n  vec2 eddy = vec2(\n    noise(uv * 2.0 + time * 0.3 + vec2(0.0, 100.0)) - 0.5,\n    noise(uv * 2.0 + time * 0.3 + vec2(100.0, 0.0)) - 0.5\n  ) * u_turbulence * 0.5;\n\n  // Wind shear: wind increases with height (creates curved fall paths)\n  float heightFactor = 1.0 + uv.y * u_windShear;\n\n  // Deeper layers (background) respond less to wind\n  float windResponse = mix(0.3, 1.0, 1.0 - layerDepth);\n\n  // Size-dependent wind response:\n  // Larger flakes have more surface area = more air resistance = drift more\n  // Smaller flakes are denser relative to size = follow wind direction more directly\n  // This creates realistic differential motion\n  float sizeResponse = 0.7 + flakeSize * 0.6;  // Larger flakes drift more\n\n  return (baseWind * (0.7 + gust * 0.3) + eddy) * heightFactor * windResponse * sizeResponse;\n}\n\n// ============================================================================\n// SPARKLE EFFECT\n// ============================================================================\n\nfloat sparkle(vec2 cellId, float time, float seed) {\n  // Sparkle is a brief, bright flash when flake catches light\n  // Use high-frequency time-based noise with sharp threshold\n  float sparklePhase = hash12(cellId + vec2(seed * 100.0, 0.0)) * 100.0;\n  float sparkleFreq = 2.0 + hash12(cellId + vec2(0.0, seed * 100.0)) * 3.0;\n\n  // Create sharp spike using pow\n  float sparkleWave = sin(time * sparkleFreq + sparklePhase);\n  float sparkleIntensity = pow(max(0.0, sparkleWave), 16.0);  // Sharp spike\n\n  // Only some flakes sparkle at any given moment\n  float sparkleProbability = hash12(cellId + vec2(floor(time * 0.5), 0.0));\n  sparkleIntensity *= step(0.85, sparkleProbability);\n\n  return sparkleIntensity * u_sparkle;\n}\n\n// ============================================================================\n// SNOW LAYER\n// ============================================================================\n\nvec3 snowLayer(vec2 uv, float time, float layerIndex, float totalLayers) {\n  // Layer depth: 0 = foreground, 1 = background\n  float depth = layerIndex / max(1.0, totalLayers - 1.0);\n\n  // Layer-specific properties\n  float layerScale = mix(8.0, 40.0, depth);  // Closer = larger cells\n  float layerSpeed = u_fallSpeed * mix(1.2, 0.4, depth);  // Closer = faster\n  float layerDensity = u_intensity * mix(1.0, 0.5, depth);  // Closer = denser\n  float layerFlakeSize = u_flakeSize * mix(1.5, 0.3, depth);  // Closer = bigger\n  float layerOpacity = u_opacity * mix(1.0, 0.4, depth);  // Closer = more opaque\n\n  // Unique offset per layer to prevent alignment\n  vec2 layerOffset = vec2(\n    sin(layerIndex * 73.156) * 10.0,\n    cos(layerIndex * 37.842) * 10.0\n  );\n\n  // Apply motion - start with base position\n  vec2 p = (uv + layerOffset) * layerScale;\n\n  // Vertical fall\n  p.y += time * layerSpeed * 2.0;\n\n  // Wind shear creates curved paths: accumulate horizontal offset based on height\n  // As flake falls from top, it accumulates more horizontal drift\n  // This is calculated per-pixel to show the curve\n  float fallProgress = fract(p.y / layerScale);  // Where in fall cycle\n  float accumulatedShear = (1.0 - uv.y) * u_windShear * u_windSpeed * time * 0.3;\n  p.x += accumulatedShear * cos(u_windAngle);\n\n  // Get base wind (will be modified per-flake by size)\n  vec2 baseWind = getWind(uv, time, depth, 1.0);\n\n  // Base horizontal wind drift\n  p.x += time * baseWind.x * 0.3;\n\n  // Sinusoidal drift (gentle wandering)\n  float driftPhase = layerIndex * 1.7;\n  p.x += sin(time * 0.5 + p.y * 0.1 + driftPhase) * u_drift * 2.0;\n\n  // Grid-based flake placement\n  vec2 id = floor(p);\n  vec2 gv = fract(p) - 0.5;\n\n  float snow = 0.0;\n  float sparkleAccum = 0.0;\n\n  // Check neighboring cells for flakes\n  for (int y = -1; y <= 1; y++) {\n    for (int x = -1; x <= 1; x++) {\n      vec2 offs = vec2(float(x), float(y));\n      vec2 cellId = id + offs;\n\n      // Random values for this cell\n      float h1 = hash12(cellId);\n      vec2 h2 = hash22(cellId);\n      float h3 = hash12(cellId + vec2(127.0, 311.0));\n      float h4 = hash12(cellId + vec2(271.0, 183.0));\n\n      // Skip based on density\n      if (h1 > layerDensity) continue;\n\n      // Flake size with variation (calculate early for wind response)\n      float sizeVar = 1.0 + (h3 - 0.5) * u_sizeVariation;\n      float size = layerFlakeSize * sizeVar * 0.04;\n\n      // Size-dependent wind offset\n      // Larger flakes accumulate more horizontal drift\n      float sizeWindFactor = 0.7 + sizeVar * 0.6;\n      vec2 sizeWindOffset = baseWind * sizeWindFactor * time * 0.1;\n\n      // Random position within cell\n      vec2 flakePos = h2 * 0.8 - 0.4;\n\n      // Apply size-dependent wind to position\n      flakePos.x += sizeWindOffset.x * 0.5;\n\n      // Flutter - individual flake wobble\n      float flutterPhase = h3 * PI * 2.0;\n      float flutterAmp = u_flutter * 0.15 * (1.0 - depth);\n      flakePos.x += sin(time * 3.0 + flutterPhase) * flutterAmp;\n      flakePos.y += cos(time * 2.5 + flutterPhase * 1.3) * flutterAmp * 0.5;\n\n      // Local UV relative to flake center\n      vec2 localUV = gv - offs - flakePos;\n\n      // Rotation: tumbling animation\n      // Rotation speed varies by flake, larger flakes rotate slower\n      float rotationSpeed = (1.5 - sizeVar * 0.5) * (0.5 + h4 * 1.0);\n      float rotationPhase = h4 * PI * 2.0;\n      float rotation = time * rotationSpeed + rotationPhase;\n\n      // Get snowflake shape with rotation\n      float flake = snowflakeShape(localUV, size, h1, rotation);\n\n      // Calculate sparkle for this flake\n      float flakeSparkle = sparkle(cellId, time, h1) * flake;\n      sparkleAccum += flakeSparkle;\n\n      snow += flake * layerOpacity;\n    }\n  }\n\n  // Return snow intensity and sparkle separately\n  // x = snow, y = sparkle, z = depth (for color tinting)\n  return vec3(snow, sparkleAccum, depth);\n}\n\n// ============================================================================\n// BLIZZARD FOG\n// ============================================================================\n\nfloat blizzardFog(vec2 uv, float time) {\n  // Only active when visibility is reduced\n  float fogAmount = 1.0 - u_visibility;\n  if (fogAmount < 0.01) return 0.0;\n\n  // Animated noise-based fog\n  float fog = fbm(uv * 3.0 + time * 0.1 * u_windSpeed, 3);\n  fog = fog * 0.5 + 0.3;\n\n  // Stronger at top (snow coming from sky)\n  fog *= 0.5 + uv.y * 0.5;\n\n  return fog * fogAmount * 0.6;\n}\n\n// ============================================================================\n// MAIN\n// ============================================================================\n\nvoid main() {\n  vec2 uv = v_uv;\n  float aspect = u_resolution.x / u_resolution.y;\n  uv.x *= aspect;\n\n  float time = u_time;\n\n  // Accumulate snow from all layers (back to front)\n  float snow = 0.0;\n  float totalSparkle = 0.0;\n\n  for (int i = u_layers - 1; i >= 0; i--) {\n    vec3 layerResult = snowLayer(uv, time, float(i), float(u_layers));\n    snow += layerResult.x;\n    totalSparkle += layerResult.y;\n  }\n\n  // Clamp accumulated snow\n  snow = clamp(snow, 0.0, 1.0);\n  totalSparkle = clamp(totalSparkle, 0.0, 1.0);\n\n  // Add blizzard fog/whiteout effect\n  float fog = blizzardFog(uv, time);\n\n  // Final color - white snow on transparent background\n  // The slight blue tint is for distant flakes (aerial perspective)\n  vec3 snowColor = vec3(0.95, 0.97, 1.0);\n  vec3 sparkleColor = vec3(1.0, 1.0, 1.0);  // Pure white sparkle\n  vec3 fogColor = vec3(0.9, 0.92, 0.95);\n\n  vec3 color = snowColor * snow + fogColor * fog;\n\n  // Add sparkle as additive bright highlights\n  color += sparkleColor * totalSparkle * 2.0;\n\n  float alpha = snow + fog + totalSparkle;\n\n  // Debug visualization\n  if (u_debug) {\n    // Show wind field\n    vec2 wind = getWind(uv, time, 0.5, 1.0);\n    color.r += abs(wind.x) * 0.5;\n    color.g += abs(wind.y) * 0.5;\n\n    // Show sparkle intensity\n    color.b += totalSparkle;\n\n    // Grid\n    vec2 grid = fract(uv * 10.0);\n    float gridLines = step(0.95, grid.x) + step(0.95, grid.y);\n    color = mix(color, vec3(1.0, 0.0, 0.0), gridLines * 0.3);\n\n    alpha = max(alpha, 0.3);\n  }\n\n  // Output with premultiplied alpha for proper compositing\n  fragColor = vec4(color * alpha, alpha);\n}\n`;\n\nfunction createShader(\n  gl: WebGL2RenderingContext,\n  type: number,\n  source: string,\n): WebGLShader | null {\n  const shader = gl.createShader(type);\n  if (!shader) return null;\n\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n    console.error(\"Shader compile error:\", gl.getShaderInfoLog(shader));\n    gl.deleteShader(shader);\n    return null;\n  }\n\n  return shader;\n}\n\nfunction createProgram(\n  gl: WebGL2RenderingContext,\n  vertexShader: WebGLShader,\n  fragmentShader: WebGLShader,\n): WebGLProgram | null {\n  const program = gl.createProgram();\n  if (!program) return null;\n\n  gl.attachShader(program, vertexShader);\n  gl.attachShader(program, fragmentShader);\n  gl.linkProgram(program);\n\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n    console.error(\"Program link error:\", gl.getProgramInfoLog(program));\n    gl.deleteProgram(program);\n    return null;\n  }\n\n  return program;\n}\n\nexport function SnowCanvas({\n  className,\n  intensity = 0.5,\n  layers = 4,\n  fallSpeed = 0.6,\n  windSpeed = 0.3,\n  windAngle = 0.2,\n  turbulence = 0.3,\n  drift = 0.5,\n  flutter = 0.5,\n  windShear = 0.5,\n  flakeSize = 1.0,\n  sizeVariation = 0.5,\n  opacity = 0.8,\n  glowAmount = 0.5,\n  sparkle = 0.5,\n  visibility = 1.0,\n  debug = false,\n}: SnowCanvasProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const glRef = useRef<WebGL2RenderingContext | null>(null);\n  const programRef = useRef<WebGLProgram | null>(null);\n  const uniformsRef = useRef<{\n    time: WebGLUniformLocation | null;\n    resolution: WebGLUniformLocation | null;\n    intensity: WebGLUniformLocation | null;\n    layers: WebGLUniformLocation | null;\n    fallSpeed: WebGLUniformLocation | null;\n    windSpeed: WebGLUniformLocation | null;\n    windAngle: WebGLUniformLocation | null;\n    turbulence: WebGLUniformLocation | null;\n    drift: WebGLUniformLocation | null;\n    flutter: WebGLUniformLocation | null;\n    windShear: WebGLUniformLocation | null;\n    flakeSize: WebGLUniformLocation | null;\n    sizeVariation: WebGLUniformLocation | null;\n    opacity: WebGLUniformLocation | null;\n    glowAmount: WebGLUniformLocation | null;\n    sparkle: WebGLUniformLocation | null;\n    visibility: WebGLUniformLocation | null;\n    debug: WebGLUniformLocation | null;\n  } | null>(null);\n  const animationFrameRef = useRef<number>(0);\n  const startTimeRef = useRef<number>(0);\n\n  const initGL = useCallback(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return false;\n\n    const gl = canvas.getContext(\"webgl2\", {\n      alpha: true,\n      premultipliedAlpha: true,\n    });\n    if (!gl) {\n      console.error(\"WebGL2 not supported\");\n      return false;\n    }\n    glRef.current = gl;\n\n    const vertexShader = createShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);\n    const fragmentShader = createShader(\n      gl,\n      gl.FRAGMENT_SHADER,\n      FRAGMENT_SHADER,\n    );\n    if (!vertexShader || !fragmentShader) return false;\n\n    const program = createProgram(gl, vertexShader, fragmentShader);\n    if (!program) return false;\n    programRef.current = program;\n\n    uniformsRef.current = {\n      time: gl.getUniformLocation(program, \"u_time\"),\n      resolution: gl.getUniformLocation(program, \"u_resolution\"),\n      intensity: gl.getUniformLocation(program, \"u_intensity\"),\n      layers: gl.getUniformLocation(program, \"u_layers\"),\n      fallSpeed: gl.getUniformLocation(program, \"u_fallSpeed\"),\n      windSpeed: gl.getUniformLocation(program, \"u_windSpeed\"),\n      windAngle: gl.getUniformLocation(program, \"u_windAngle\"),\n      turbulence: gl.getUniformLocation(program, \"u_turbulence\"),\n      drift: gl.getUniformLocation(program, \"u_drift\"),\n      flutter: gl.getUniformLocation(program, \"u_flutter\"),\n      windShear: gl.getUniformLocation(program, \"u_windShear\"),\n      flakeSize: gl.getUniformLocation(program, \"u_flakeSize\"),\n      sizeVariation: gl.getUniformLocation(program, \"u_sizeVariation\"),\n      opacity: gl.getUniformLocation(program, \"u_opacity\"),\n      glowAmount: gl.getUniformLocation(program, \"u_glowAmount\"),\n      sparkle: gl.getUniformLocation(program, \"u_sparkle\"),\n      visibility: gl.getUniformLocation(program, \"u_visibility\"),\n      debug: gl.getUniformLocation(program, \"u_debug\"),\n    };\n\n    const positions = new Float32Array([\n      -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1,\n    ]);\n\n    const positionBuffer = gl.createBuffer();\n    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);\n    gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);\n\n    const positionLoc = gl.getAttribLocation(program, \"a_position\");\n    gl.enableVertexAttribArray(positionLoc);\n    gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);\n\n    // Enable blending for transparency\n    gl.enable(gl.BLEND);\n    gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);\n\n    startTimeRef.current = performance.now();\n    return true;\n  }, []);\n\n  const render = useCallback(() => {\n    const gl = glRef.current;\n    const program = programRef.current;\n    const uniforms = uniformsRef.current;\n    const canvas = canvasRef.current;\n\n    if (!gl || !program || !uniforms || !canvas) return;\n\n    const displayWidth = canvas.clientWidth * window.devicePixelRatio;\n    const displayHeight = canvas.clientHeight * window.devicePixelRatio;\n\n    if (canvas.width !== displayWidth || canvas.height !== displayHeight) {\n      canvas.width = displayWidth;\n      canvas.height = displayHeight;\n      gl.viewport(0, 0, canvas.width, canvas.height);\n    }\n\n    const time = (performance.now() - startTimeRef.current) / 1000;\n\n    gl.clearColor(0, 0, 0, 0);\n    gl.clear(gl.COLOR_BUFFER_BIT);\n\n    gl.useProgram(program);\n    gl.uniform1f(uniforms.time, time);\n    gl.uniform2f(uniforms.resolution, canvas.width, canvas.height);\n    gl.uniform1f(uniforms.intensity, intensity);\n    gl.uniform1i(uniforms.layers, layers);\n    gl.uniform1f(uniforms.fallSpeed, fallSpeed);\n    gl.uniform1f(uniforms.windSpeed, windSpeed);\n    gl.uniform1f(uniforms.windAngle, windAngle);\n    gl.uniform1f(uniforms.turbulence, turbulence);\n    gl.uniform1f(uniforms.drift, drift);\n    gl.uniform1f(uniforms.flutter, flutter);\n    gl.uniform1f(uniforms.windShear, windShear);\n    gl.uniform1f(uniforms.flakeSize, flakeSize);\n    gl.uniform1f(uniforms.sizeVariation, sizeVariation);\n    gl.uniform1f(uniforms.opacity, opacity);\n    gl.uniform1f(uniforms.glowAmount, glowAmount);\n    gl.uniform1f(uniforms.sparkle, sparkle);\n    gl.uniform1f(uniforms.visibility, visibility);\n    gl.uniform1i(uniforms.debug, debug ? 1 : 0);\n\n    gl.drawArrays(gl.TRIANGLES, 0, 6);\n\n    animationFrameRef.current = requestAnimationFrame(render);\n  }, [\n    intensity,\n    layers,\n    fallSpeed,\n    windSpeed,\n    windAngle,\n    turbulence,\n    drift,\n    flutter,\n    windShear,\n    flakeSize,\n    sizeVariation,\n    opacity,\n    glowAmount,\n    sparkle,\n    visibility,\n    debug,\n  ]);\n\n  useEffect(() => {\n    if (initGL()) {\n      render();\n    }\n\n    return () => {\n      if (animationFrameRef.current) {\n        cancelAnimationFrame(animationFrameRef.current);\n      }\n    };\n  }, [initGL, render]);\n\n  return (\n    <canvas\n      ref={canvasRef}\n      className={className}\n      style={{ width: \"100%\", height: \"100%\" }}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-compositor/celestial-canvas.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useRef, useCallback } from \"react\";\n\ninterface CelestialCanvasProps {\n  className?: string;\n  timeOfDay: number;\n  moonPhase?: number;\n  starDensity?: number;\n  celestialX?: number;\n  celestialY?: number;\n  sunSize?: number;\n  moonSize?: number;\n  sunGlowIntensity?: number;\n  sunGlowSize?: number;\n  sunRayCount?: number;\n  sunRayLength?: number;\n  sunRayIntensity?: number;\n  /**\n   * Scales subtle, noise-driven ray motion (shimmer + slow \"breathing\").\n   * 0 disables motion; 1 is the default subtlety; >1 increases visibility.\n   */\n  sunRayShimmer?: number;\n  /**\n   * Global speed multiplier for the ray shimmer/breath noise inputs.\n   * 1 is the default speed; >1 speeds up motion.\n   */\n  sunRayShimmerSpeed?: number;\n  moonGlowIntensity?: number;\n  moonGlowSize?: number;\n}\n\ninterface UniformLocations {\n  u_time: WebGLUniformLocation | null;\n  u_resolution: WebGLUniformLocation | null;\n  u_timeOfDay: WebGLUniformLocation | null;\n  u_moonPhase: WebGLUniformLocation | null;\n  u_starDensity: WebGLUniformLocation | null;\n  u_celestialPos: WebGLUniformLocation | null;\n  u_sunSize: WebGLUniformLocation | null;\n  u_moonSize: WebGLUniformLocation | null;\n  u_moonTexture: WebGLUniformLocation | null;\n  u_hasMoonTexture: WebGLUniformLocation | null;\n  u_sunGlowIntensity: WebGLUniformLocation | null;\n  u_sunGlowSize: WebGLUniformLocation | null;\n  u_sunRayCount: WebGLUniformLocation | null;\n  u_sunRayLength: WebGLUniformLocation | null;\n  u_sunRayIntensity: WebGLUniformLocation | null;\n  u_sunRayShimmer: WebGLUniformLocation | null;\n  u_sunRayShimmerSpeed: WebGLUniformLocation | null;\n  u_moonGlowIntensity: WebGLUniformLocation | null;\n  u_moonGlowSize: WebGLUniformLocation | null;\n}\n\nconst VERTEX_SHADER = /* glsl */ `#version 300 es\nin vec4 a_position;\nout vec2 v_uv;\n\nvoid main() {\n  gl_Position = a_position;\n  v_uv = a_position.xy * 0.5 + 0.5;\n}\n`;\n\nconst FRAGMENT_SHADER = /* glsl */ `#version 300 es\nprecision highp float;\n\nin vec2 v_uv;\nout vec4 fragColor;\n\n#define PI 3.14159265359\n#define TAU 6.28318530718\n\nuniform float u_time;\nuniform vec2 u_resolution;\nuniform float u_timeOfDay;\nuniform float u_moonPhase;\nuniform float u_starDensity;\nuniform vec2 u_celestialPos;\nuniform float u_sunSize;\nuniform float u_moonSize;\nuniform sampler2D u_moonTexture;\nuniform bool u_hasMoonTexture;\nuniform float u_sunGlowIntensity;\nuniform float u_sunGlowSize;\nuniform float u_sunRayCount;\nuniform float u_sunRayLength;\nuniform float u_sunRayIntensity;\nuniform float u_sunRayShimmer;\nuniform float u_sunRayShimmerSpeed;\nuniform float u_moonGlowIntensity;\nuniform float u_moonGlowSize;\n\nfloat hash(vec2 p) {\n  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nvec2 hash2(vec2 p) {\n  p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));\n  return fract(sin(p) * 43758.5453);\n}\n\nfloat noise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  f = f * f * (3.0 - 2.0 * f);\n  float a = hash(i);\n  float b = hash(i + vec2(1.0, 0.0));\n  float c = hash(i + vec2(0.0, 1.0));\n  float d = hash(i + vec2(1.0, 1.0));\n  return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);\n}\n\nfloat fbm(vec2 p, int octaves) {\n  float value = 0.0;\n  float amplitude = 0.5;\n  float frequency = 1.0;\n  for (int i = 0; i < 6; i++) {\n    if (i >= octaves) break;\n    value += amplitude * noise(p * frequency);\n    frequency *= 2.0;\n    amplitude *= 0.5;\n  }\n  return value;\n}\n\n// Calculate sun Y position based on time of day\n// Sun rises 0.18-0.32, visible during day, sets 0.68-0.82\n// Note: UV y=0 is bottom, y=1 is top, so below horizon means y < 0\nfloat getSunY(float timeOfDay, float baseY) {\n  float belowHorizon = -0.25;\n  float riseProgress = smoothstep(0.18, 0.32, timeOfDay);\n  float setProgress = smoothstep(0.68, 0.82, timeOfDay);\n  float visible = riseProgress * (1.0 - setProgress);\n  return mix(belowHorizon, baseY, visible);\n}\n\n// Calculate moon Y position based on time of day\n// Moon sets 0.12-0.26 (overlaps slightly with sun rise)\n// Moon rises 0.74-0.88 (overlaps slightly with sun set)\n// During overlap both are near horizon so both faded = subtle handoff\nfloat getMoonY(float timeOfDay, float baseY) {\n  float belowHorizon = -0.25;\n  float risingEvening = smoothstep(0.74, 0.88, timeOfDay);\n  float settingMorning = 1.0 - smoothstep(0.12, 0.26, timeOfDay);\n  float visible = max(risingEvening, settingMorning);\n  return mix(belowHorizon, baseY, visible);\n}\n\n// Fade opacity near horizon for smooth edge (bottom of screen)\n// Extended range so bodies visible earlier in their rise\nfloat getHorizonFade(float y) {\n  return smoothstep(-0.2, 0.0, y);\n}\n\nvec3 getSkyColor(vec2 uv, float timeOfDay) {\n  vec3 dayTop = vec3(0.4, 0.6, 0.9);\n  vec3 dayHorizon = vec3(0.7, 0.8, 0.95);\n  vec3 sunsetTop = vec3(0.2, 0.2, 0.4);\n  vec3 sunsetHorizon = vec3(0.9, 0.5, 0.2);\n  vec3 nightTop = vec3(0.02, 0.02, 0.05);\n  vec3 nightHorizon = vec3(0.05, 0.05, 0.1);\n\n  float dayAmount = smoothstep(0.25, 0.4, timeOfDay) * smoothstep(0.75, 0.6, timeOfDay);\n  float sunsetAmount = max(\n    smoothstep(0.2, 0.3, timeOfDay) * smoothstep(0.4, 0.3, timeOfDay),\n    smoothstep(0.6, 0.7, timeOfDay) * smoothstep(0.8, 0.7, timeOfDay)\n  );\n  float nightAmount = max(0.0, 1.0 - dayAmount - sunsetAmount);\n\n  float gradientFactor = pow(1.0 - uv.y, 1.0);\n\n  vec3 topColor = dayTop * dayAmount + sunsetTop * sunsetAmount + nightTop * nightAmount;\n  vec3 horizonColor = dayHorizon * dayAmount + sunsetHorizon * sunsetAmount + nightHorizon * nightAmount;\n\n  return mix(topColor, horizonColor, gradientFactor);\n}\n\nfloat drawStars(vec2 uv, float density, float time) {\n  float stars = 0.0;\n  for (int layer = 0; layer < 3; layer++) {\n    float layerScale = 100.0 + float(layer) * 50.0;\n    vec2 gridUV = uv * layerScale;\n    vec2 gridID = floor(gridUV);\n    vec2 gridFract = fract(gridUV);\n    vec2 starPos = hash2(gridID + float(layer) * 100.0);\n    float dist = length(gridFract - starPos);\n    float starPresent = step(1.0 - density * 0.3, hash(gridID * (float(layer) + 1.0)));\n    float starSize = 0.02 + hash(gridID.yx) * 0.03;\n    float twinkle = sin(time * (2.0 + hash(gridID) * 3.0) + hash(gridID.yx) * TAU) * 0.3 + 0.7;\n    float star = smoothstep(starSize, 0.0, dist) * starPresent * twinkle;\n    star *= 1.0 - float(layer) * 0.3;\n    stars += star;\n  }\n  return stars;\n}\n\nvec3 drawSun(vec2 uv, vec2 sunPos, float size) {\n  vec2 aspect = vec2(u_resolution.x / u_resolution.y, 1.0);\n  vec2 diff = (uv - sunPos) * aspect;\n  float dist = length(diff);\n  float angle = atan(diff.y, diff.x);\n\n  float disc = 1.0 - smoothstep(size * 0.9, size, dist);\n\n  vec3 sunCore = vec3(1.0, 1.0, 0.95);\n  vec3 sunEdge = vec3(1.0, 0.9, 0.4);\n  float edgeFactor = clamp(dist / size, 0.0, 1.0);\n  vec3 sunColor = mix(sunCore, sunEdge, edgeFactor);\n\n  float limbDarkening = 1.0 - pow(clamp(dist / size, 0.0, 1.0), 2.0) * 0.3;\n  sunColor *= limbDarkening;\n\n  float glowSize = max(0.1, u_sunGlowSize);\n  float scaledDist = dist / glowSize;\n  float glow1 = exp(-scaledDist * 8.0) * 0.5;\n  float glow2 = exp(-scaledDist * 3.0) * 0.3;\n  float glow3 = exp(-scaledDist * 1.5) * 0.15;\n\n  vec3 glowColor = vec3(1.0, 0.8, 0.4);\n  float glowTotal = (glow1 + glow2 + glow3) * u_sunGlowIntensity;\n\n  vec3 result = sunColor * disc * 2.0;\n  result += glowColor * glowTotal;\n\n  // ---------------------------------------------------------------------------\n  // Prismatic flare + rays\n  // ---------------------------------------------------------------------------\n  // Keep these effects subtle and mostly white — we want \"eye optics\" more than\n  // sci-fi neon. The rainbow shows up as a gentle chromatic fringe on very\n  // bright highlights.\n\n  // A thin, slightly prismatic halo ring around the sun.\n  float ringCenter = size * 1.15;\n  float ringWidth = max(size * 0.35, 0.001);\n  float ringMask = smoothstep(size * 0.85, size * 1.05, dist);\n  ringMask *= 1.0 - smoothstep(size * 5.0, size * 9.0, dist);\n\n  // Chromatic dispersion grows slightly with distance from the disc.\n  float chromaShift = size * (0.012 + u_sunRayIntensity * 0.06);\n  chromaShift *= smoothstep(size * 0.9, size * 2.4, dist);\n\n  float ringR = exp(-pow((dist - chromaShift - ringCenter) / ringWidth, 2.0));\n  float ringG = exp(-pow((dist - ringCenter) / ringWidth, 2.0));\n  float ringB = exp(-pow((dist + chromaShift - ringCenter) / ringWidth, 2.0));\n\n  // Desaturated spectrum-ish tint (mostly white).\n  float ringT = clamp((dist - size) / (size * 2.2), 0.0, 1.0);\n  vec3 ringSpectral = 0.55 + 0.45 * cos(TAU * (ringT + vec3(0.0, 0.33, 0.67)));\n  ringSpectral = clamp(ringSpectral, 0.0, 1.0);\n  vec3 ringColor = mix(vec3(1.0), ringSpectral, 0.45);\n\n  float ringIntensity = (ringR + ringG + ringB) / 3.0;\n  ringIntensity *= ringMask * u_sunGlowIntensity * 0.025;\n  result += ringColor * ringIntensity;\n\n  // Sun rays (diffraction spikes) with gentle shimmer/breath.\n  if (u_sunRayCount > 0.0 && u_sunRayIntensity > 0.0) {\n    // Rays are only visible close to the disc; bail early for perf.\n    if (dist < size * 3.6) {\n      float motion = clamp(u_sunRayShimmer, 0.0, 5.0);\n      float t = u_time * max(0.0, u_sunRayShimmerSpeed);\n\n      float rayPhase = angle * u_sunRayCount;\n      float rayIndex = floor(rayPhase / PI + 0.5);\n      float raySeed = hash(vec2(rayIndex, 19.17));\n\n      // Major rays + faint minor spikes (iris/eyelash diffraction).\n      float major = pow(abs(cos(rayPhase)), 10.0);\n      float minor = pow(abs(cos(rayPhase * 2.0 + raySeed * 2.3)), 22.0) * 0.18;\n      float rayShape = max(major, minor);\n\n      // Per-ray breathing (very slow) + along-ray shimmer (slightly faster).\n      float breathe =\n        1.0 +\n        (noise(vec2(t * 0.05, raySeed * 7.0)) - 0.5) * (0.08 * motion);\n      float shimmer =\n        1.0 +\n        (noise(vec2(dist * 12.0 - t * 0.25, raySeed * 23.0)) - 0.5) *\n          (0.12 * motion);\n      float micro =\n        1.0 +\n        (noise(vec2(t * 0.6, rayPhase * 0.8)) - 0.5) * (0.06 * motion);\n\n      float rayNoise =\n        0.72 +\n        0.28 * noise(vec2(rayPhase * 0.35, t * 0.12 + raySeed * 10.0));\n      float rayPattern = rayShape * rayNoise;\n\n      float rayStart = smoothstep(size * 0.75, size * 1.25, dist);\n      float rayEnd = smoothstep(size * (3.0 * breathe), size * 1.5, dist);\n\n      float rayLengthVar = 0.75 + raySeed * 0.55;\n      float maxRayDist = max(0.001, u_sunRayLength * 0.15);\n      float rayFalloff = exp(\n        -dist * dist / (maxRayDist * maxRayDist * rayLengthVar * breathe)\n      );\n\n      float rays = rayPattern * rayFalloff * rayStart * rayEnd * u_sunRayIntensity;\n      rays *= shimmer * micro;\n\n      // Chromatic fringe: compute a slightly different falloff per channel.\n      float prismMask = smoothstep(size * 1.05, size * 2.6, dist);\n      float rayChroma = size * (0.01 + u_sunRayIntensity * 0.05) * prismMask;\n\n      float distR = max(0.0, dist - rayChroma);\n      float distB = dist + rayChroma;\n\n      float falloffR = exp(\n        -distR * distR / (maxRayDist * maxRayDist * rayLengthVar * breathe)\n      );\n      float falloffG = rayFalloff;\n      float falloffB = exp(\n        -distB * distB / (maxRayDist * maxRayDist * rayLengthVar * breathe)\n      );\n\n      vec3 rayRGB = vec3(falloffR, falloffG, falloffB) * rayPattern * rayStart * rayEnd;\n      float rayAvg = (rayRGB.r + rayRGB.g + rayRGB.b) / 3.0;\n      vec3 rayChromaColor = rayRGB / max(rayAvg, 1e-4);\n\n      // Add a very subtle spectrum tint so the fringe reads as \"rainbow-like\",\n      // without turning into a colorful fantasy effect.\n      float rayT = clamp((dist - size) / (size * 2.6), 0.0, 1.0);\n      vec3 raySpectral = 0.55 + 0.45 * cos(TAU * (rayT + vec3(0.0, 0.33, 0.67)));\n      raySpectral = clamp(raySpectral, 0.0, 1.0);\n      raySpectral = mix(vec3(1.0), raySpectral, 0.28);\n\n      vec3 rayWarm = vec3(1.0, 0.92, 0.7);\n      float prismMix = clamp(0.08 + u_sunRayIntensity * 0.6, 0.0, 0.45) * prismMask;\n      vec3 rayColor = mix(rayWarm, rayChromaColor, prismMix);\n      rayColor = mix(rayColor, raySpectral, prismMix * 0.65);\n\n      result += rayColor * rays;\n    }\n  }\n\n  return result;\n}\n\nvec3 getSphereNormal(vec2 discUV) {\n  float r2 = dot(discUV, discUV);\n  if (r2 > 1.0) return vec3(0.0);\n  float z = sqrt(1.0 - r2);\n  return normalize(vec3(discUV.x, discUV.y, z));\n}\n\nvec2 sphereToEquirectangular(vec3 normal) {\n  float longitude = atan(normal.x, normal.z);\n  float u = longitude / TAU + 0.5;\n  float latitude = asin(clamp(normal.y, -1.0, 1.0));\n  float v = latitude / PI + 0.5;\n  return vec2(u, v);\n}\n\nvec3 getMoonSurfaceColor(vec3 normal, vec2 discUV) {\n  if (u_hasMoonTexture) {\n    vec2 texUV = sphereToEquirectangular(normal);\n    return texture(u_moonTexture, texUV).rgb;\n  }\n  float brightness = 0.7 + fbm(discUV * 5.0, 3) * 0.3;\n  return vec3(brightness * 0.85, brightness * 0.83, brightness * 0.8);\n}\n\nvec4 drawMoon(vec2 uv, vec2 moonPos, float size, float phase) {\n  vec2 aspect = vec2(u_resolution.x / u_resolution.y, 1.0);\n  vec2 diff = (uv - moonPos) * aspect;\n  float dist = length(diff);\n\n  vec2 discUV = diff / size;\n  float discDist = length(discUV);\n  float disc = 1.0 - smoothstep(0.95, 1.0, discDist);\n\n  float glowSize = max(0.1, u_moonGlowSize);\n  float glowIntensity = u_moonGlowIntensity;\n\n  if (disc < 0.001) {\n    float scaledDist = dist / glowSize;\n    float glow1 = exp(-scaledDist * 6.0) * 0.15;\n    float glow2 = exp(-scaledDist * 2.0) * 0.06;\n    vec3 glowColor = vec3(0.8, 0.85, 0.95);\n    float phaseAngle = phase * TAU;\n    vec3 sunDir = vec3(sin(phaseAngle), 0.0, -cos(phaseAngle));\n    float glowPhase = max(0.2, dot(normalize(vec3(discUV, 0.5)), sunDir) * 0.5 + 0.5);\n    return vec4(glowColor * (glow1 + glow2) * glowPhase * glowIntensity, 0.0);\n  }\n\n  vec3 normal = getSphereNormal(discUV);\n  float phaseAngle = phase * TAU;\n  vec3 sunDir = vec3(sin(phaseAngle), 0.0, -cos(phaseAngle));\n  float NdotL = dot(normal, sunDir);\n  float terminator = smoothstep(-0.02, 0.08, NdotL);\n\n  vec3 baseColor = getMoonSurfaceColor(normal, discUV) * 0.7;\n  vec3 ambient = baseColor * 0.02;\n  vec3 lit = baseColor * terminator * 0.85;\n  vec3 moonSurface = ambient + lit;\n\n  float limbDarkening = 1.0 - pow(discDist, 3.0) * 0.15;\n  moonSurface *= limbDarkening;\n\n  float rimLight = pow(1.0 - abs(NdotL), 4.0) * terminator * 0.1;\n  moonSurface += vec3(1.0, 0.98, 0.95) * rimLight;\n\n  float scaledDist = dist / glowSize;\n  float glow1 = exp(-scaledDist * 6.0) * 0.12;\n  float glow2 = exp(-scaledDist * 2.0) * 0.06;\n  vec3 glowColor = vec3(0.8, 0.85, 0.95);\n  float litAmount = max(0.1, terminator);\n  vec3 glow = glowColor * (glow1 + glow2) * litAmount * glowIntensity;\n\n  return vec4(moonSurface * disc + glow, disc);\n}\n\nvoid main() {\n  vec2 uv = v_uv;\n\n  vec3 color = getSkyColor(uv, u_timeOfDay);\n\n  // Calculate separate Y positions for sun and moon\n  float sunY = getSunY(u_timeOfDay, u_celestialPos.y);\n  float moonY = getMoonY(u_timeOfDay, u_celestialPos.y);\n  vec2 sunPos = vec2(u_celestialPos.x, sunY);\n  vec2 moonPos = vec2(u_celestialPos.x, moonY);\n\n  // Stars visible when moon is up (night time)\n  float moonFade = getHorizonFade(moonY);\n  if (moonFade > 0.01) {\n    float stars = drawStars(uv, u_starDensity, u_time);\n    color += vec3(stars) * moonFade;\n  }\n\n  // Draw sun with horizon fade\n  float sunFade = getHorizonFade(sunY);\n  if (sunFade > 0.01) {\n    vec3 sun = drawSun(uv, sunPos, u_sunSize);\n    color += sun * sunFade;\n  }\n\n  // Draw moon with horizon fade\n  if (moonFade > 0.01) {\n    vec4 moon = drawMoon(uv, moonPos, u_moonSize, u_moonPhase);\n    float alpha = moon.a * moonFade;\n    color = mix(color, moon.rgb, alpha) + moon.rgb * (1.0 - moon.a) * moonFade;\n  }\n\n  fragColor = vec4(color, 1.0);\n}\n`;\n\nfunction createShader(\n  gl: WebGL2RenderingContext,\n  type: GLenum,\n  source: string,\n): WebGLShader | null {\n  const shader = gl.createShader(type);\n  if (!shader) return null;\n\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n    console.error(\"Shader compile error:\", gl.getShaderInfoLog(shader));\n    gl.deleteShader(shader);\n    return null;\n  }\n\n  return shader;\n}\n\nfunction createProgram(\n  gl: WebGL2RenderingContext,\n  vertexShader: WebGLShader,\n  fragmentShader: WebGLShader,\n): WebGLProgram | null {\n  const program = gl.createProgram();\n  if (!program) return null;\n\n  gl.attachShader(program, vertexShader);\n  gl.attachShader(program, fragmentShader);\n  gl.linkProgram(program);\n\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n    console.error(\"Program link error:\", gl.getProgramInfoLog(program));\n    gl.deleteProgram(program);\n    return null;\n  }\n\n  return program;\n}\n\nfunction getUniformLocations(\n  gl: WebGL2RenderingContext,\n  program: WebGLProgram,\n): UniformLocations {\n  return {\n    u_time: gl.getUniformLocation(program, \"u_time\"),\n    u_resolution: gl.getUniformLocation(program, \"u_resolution\"),\n    u_timeOfDay: gl.getUniformLocation(program, \"u_timeOfDay\"),\n    u_moonPhase: gl.getUniformLocation(program, \"u_moonPhase\"),\n    u_starDensity: gl.getUniformLocation(program, \"u_starDensity\"),\n    u_celestialPos: gl.getUniformLocation(program, \"u_celestialPos\"),\n    u_sunSize: gl.getUniformLocation(program, \"u_sunSize\"),\n    u_moonSize: gl.getUniformLocation(program, \"u_moonSize\"),\n    u_moonTexture: gl.getUniformLocation(program, \"u_moonTexture\"),\n    u_hasMoonTexture: gl.getUniformLocation(program, \"u_hasMoonTexture\"),\n    u_sunGlowIntensity: gl.getUniformLocation(program, \"u_sunGlowIntensity\"),\n    u_sunGlowSize: gl.getUniformLocation(program, \"u_sunGlowSize\"),\n    u_sunRayCount: gl.getUniformLocation(program, \"u_sunRayCount\"),\n    u_sunRayLength: gl.getUniformLocation(program, \"u_sunRayLength\"),\n    u_sunRayIntensity: gl.getUniformLocation(program, \"u_sunRayIntensity\"),\n    u_sunRayShimmer: gl.getUniformLocation(program, \"u_sunRayShimmer\"),\n    u_sunRayShimmerSpeed: gl.getUniformLocation(\n      program,\n      \"u_sunRayShimmerSpeed\",\n    ),\n    u_moonGlowIntensity: gl.getUniformLocation(program, \"u_moonGlowIntensity\"),\n    u_moonGlowSize: gl.getUniformLocation(program, \"u_moonGlowSize\"),\n  };\n}\n\nfunction loadMoonTexture(\n  gl: WebGL2RenderingContext,\n  onLoad: () => void,\n): WebGLTexture | null {\n  const texture = gl.createTexture();\n  if (!texture) return null;\n\n  gl.bindTexture(gl.TEXTURE_2D, texture);\n  gl.texImage2D(\n    gl.TEXTURE_2D,\n    0,\n    gl.RGBA,\n    1,\n    1,\n    0,\n    gl.RGBA,\n    gl.UNSIGNED_BYTE,\n    new Uint8Array([128, 128, 128, 255]),\n  );\n\n  const image = new Image();\n  image.crossOrigin = \"anonymous\";\n  image.onload = () => {\n    gl.bindTexture(gl.TEXTURE_2D, texture);\n    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);\n    gl.generateMipmap(gl.TEXTURE_2D);\n    gl.texParameteri(\n      gl.TEXTURE_2D,\n      gl.TEXTURE_MIN_FILTER,\n      gl.LINEAR_MIPMAP_LINEAR,\n    );\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n    onLoad();\n  };\n  image.src = \"/assets/moon-texture.jpg\";\n\n  return texture;\n}\n\nexport function CelestialCanvas({\n  className,\n  timeOfDay,\n  moonPhase = 0.5,\n  starDensity = 0.5,\n  celestialX = 0.5,\n  celestialY = 0.72,\n  sunSize = 0.06,\n  moonSize = 0.05,\n  sunGlowIntensity = 1.0,\n  sunGlowSize = 0.3,\n  sunRayCount = 12,\n  sunRayLength = 0.5,\n  sunRayIntensity = 0.4,\n  sunRayShimmer = 1.0,\n  sunRayShimmerSpeed = 1.0,\n  moonGlowIntensity = 0.6,\n  moonGlowSize = 0.2,\n}: CelestialCanvasProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const glRef = useRef<WebGL2RenderingContext | null>(null);\n  const programRef = useRef<WebGLProgram | null>(null);\n  const uniformsRef = useRef<UniformLocations | null>(null);\n  const animationFrameRef = useRef<number>(0);\n  const startTimeRef = useRef<number>(0);\n  const moonTextureRef = useRef<WebGLTexture | null>(null);\n  const moonTextureLoadedRef = useRef<boolean>(false);\n\n  const propsRef = useRef({\n    timeOfDay,\n    moonPhase,\n    starDensity,\n    celestialX,\n    celestialY,\n    sunSize,\n    moonSize,\n    sunGlowIntensity,\n    sunGlowSize,\n    sunRayCount,\n    sunRayLength,\n    sunRayIntensity,\n    sunRayShimmer,\n    sunRayShimmerSpeed,\n    moonGlowIntensity,\n    moonGlowSize,\n  });\n  propsRef.current = {\n    timeOfDay,\n    moonPhase,\n    starDensity,\n    celestialX,\n    celestialY,\n    sunSize,\n    moonSize,\n    sunGlowIntensity,\n    sunGlowSize,\n    sunRayCount,\n    sunRayLength,\n    sunRayIntensity,\n    sunRayShimmer,\n    sunRayShimmerSpeed,\n    moonGlowIntensity,\n    moonGlowSize,\n  };\n\n  const initGL = useCallback(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return false;\n\n    const gl = canvas.getContext(\"webgl2\");\n    if (!gl) {\n      console.error(\"WebGL2 not supported\");\n      return false;\n    }\n    glRef.current = gl;\n\n    const vertexShader = createShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);\n    const fragmentShader = createShader(\n      gl,\n      gl.FRAGMENT_SHADER,\n      FRAGMENT_SHADER,\n    );\n    if (!vertexShader || !fragmentShader) return false;\n\n    const program = createProgram(gl, vertexShader, fragmentShader);\n    if (!program) return false;\n    programRef.current = program;\n\n    uniformsRef.current = getUniformLocations(gl, program);\n\n    moonTextureRef.current = loadMoonTexture(gl, () => {\n      moonTextureLoadedRef.current = true;\n    });\n\n    const positions = new Float32Array([\n      -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1,\n    ]);\n    const positionBuffer = gl.createBuffer();\n    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);\n    gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);\n\n    const positionLoc = gl.getAttribLocation(program, \"a_position\");\n    gl.enableVertexAttribArray(positionLoc);\n    gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);\n\n    startTimeRef.current = performance.now();\n    return true;\n  }, []);\n\n  const render = useCallback(() => {\n    const gl = glRef.current;\n    const program = programRef.current;\n    const uniforms = uniformsRef.current;\n    const canvas = canvasRef.current;\n    const props = propsRef.current;\n\n    if (!gl || !program || !uniforms || !canvas) return;\n\n    const displayWidth = canvas.clientWidth * window.devicePixelRatio;\n    const displayHeight = canvas.clientHeight * window.devicePixelRatio;\n\n    if (canvas.width !== displayWidth || canvas.height !== displayHeight) {\n      canvas.width = displayWidth;\n      canvas.height = displayHeight;\n      gl.viewport(0, 0, canvas.width, canvas.height);\n    }\n\n    const time = (performance.now() - startTimeRef.current) / 1000;\n\n    gl.useProgram(program);\n\n    gl.uniform1f(uniforms.u_time, time);\n    gl.uniform2f(uniforms.u_resolution, canvas.width, canvas.height);\n    gl.uniform1f(uniforms.u_timeOfDay, props.timeOfDay);\n    gl.uniform1f(uniforms.u_moonPhase, props.moonPhase);\n    gl.uniform1f(uniforms.u_starDensity, props.starDensity);\n    gl.uniform2f(uniforms.u_celestialPos, props.celestialX, props.celestialY);\n    gl.uniform1f(uniforms.u_sunSize, props.sunSize);\n    gl.uniform1f(uniforms.u_moonSize, props.moonSize);\n    gl.uniform1f(uniforms.u_sunGlowIntensity, props.sunGlowIntensity);\n    gl.uniform1f(uniforms.u_sunGlowSize, props.sunGlowSize);\n    gl.uniform1f(uniforms.u_sunRayCount, props.sunRayCount);\n    gl.uniform1f(uniforms.u_sunRayLength, props.sunRayLength);\n    gl.uniform1f(uniforms.u_sunRayIntensity, props.sunRayIntensity);\n    gl.uniform1f(uniforms.u_sunRayShimmer, props.sunRayShimmer);\n    gl.uniform1f(uniforms.u_sunRayShimmerSpeed, props.sunRayShimmerSpeed);\n    gl.uniform1f(uniforms.u_moonGlowIntensity, props.moonGlowIntensity);\n    gl.uniform1f(uniforms.u_moonGlowSize, props.moonGlowSize);\n\n    gl.activeTexture(gl.TEXTURE0);\n    gl.bindTexture(gl.TEXTURE_2D, moonTextureRef.current);\n    gl.uniform1i(uniforms.u_moonTexture, 0);\n    gl.uniform1i(\n      uniforms.u_hasMoonTexture,\n      moonTextureLoadedRef.current ? 1 : 0,\n    );\n\n    gl.drawArrays(gl.TRIANGLES, 0, 6);\n\n    animationFrameRef.current = requestAnimationFrame(render);\n  }, []);\n\n  useEffect(() => {\n    if (initGL()) {\n      render();\n    }\n\n    return () => {\n      if (animationFrameRef.current) {\n        cancelAnimationFrame(animationFrameRef.current);\n      }\n    };\n  }, [initGL, render]);\n\n  return (\n    <canvas\n      ref={canvasRef}\n      className={className}\n      style={{ width: \"100%\", height: \"100%\" }}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-compositor/interpolation.ts",
    "content": "import type { ConditionOverrides, FullCompositorParams } from \"./presets\";\nimport {\n  TIME_CHECKPOINTS,\n  TIME_CHECKPOINT_ORDER,\n  getNearestCheckpoint as getNearestCheckpointCore,\n  type TimeCheckpoint,\n} from \"@/lib/weather-authoring/weather-widget/effects/tuning\";\n\nexport interface CheckpointOverrides {\n  dawn: ConditionOverrides;\n  noon: ConditionOverrides;\n  dusk: ConditionOverrides;\n  midnight: ConditionOverrides;\n}\n\ntype BaseParamsGetter = (checkpoint: TimeCheckpoint) => FullCompositorParams;\n\ninterface SurroundingCheckpoints {\n  before: TimeCheckpoint;\n  after: TimeCheckpoint;\n  t: number;\n}\n\nexport function getSurroundingCheckpoints(\n  timeOfDay: number,\n): SurroundingCheckpoints {\n  const checkpointTimes = TIME_CHECKPOINT_ORDER.map((cp) => ({\n    checkpoint: cp,\n    value: TIME_CHECKPOINTS[cp],\n  })).sort((a, b) => a.value - b.value);\n\n  const normalized = ((timeOfDay % 1) + 1) % 1;\n\n  for (let i = 0; i < checkpointTimes.length; i++) {\n    const current = checkpointTimes[i];\n    const next = checkpointTimes[(i + 1) % checkpointTimes.length];\n\n    const start = current.value;\n    let end = next.value;\n\n    if (end < start) {\n      end += 1;\n    }\n\n    let queryTime = normalized;\n    if (queryTime < start && end > 1) {\n      queryTime += 1;\n    }\n\n    if (queryTime >= start && queryTime < end) {\n      const range = end - start;\n      const t = range > 0 ? (queryTime - start) / range : 0;\n\n      return {\n        before: current.checkpoint,\n        after: next.checkpoint,\n        t,\n      };\n    }\n  }\n\n  return {\n    before: \"midnight\",\n    after: \"dawn\",\n    t: 0,\n  };\n}\n\nfunction lerpNumber(a: number, b: number, t: number): number {\n  return a + (b - a) * t;\n}\n\nfunction interpolatePartialObject<T extends object>(\n  a: Partial<T> | undefined,\n  b: Partial<T> | undefined,\n  baseA: T | undefined,\n  baseB: T | undefined,\n  t: number,\n): Partial<T> | undefined {\n  if (!a && !b) return undefined;\n\n  const result: Partial<T> = {};\n  const allKeys = new Set([\n    ...(a ? Object.keys(a) : []),\n    ...(b ? Object.keys(b) : []),\n  ]) as Set<keyof T>;\n\n  for (const key of allKeys) {\n    const aVal = a?.[key];\n    const bVal = b?.[key];\n\n    if (aVal === undefined && bVal === undefined) {\n      continue;\n    }\n\n    let fromVal: T[keyof T] | undefined = aVal;\n    let toVal: T[keyof T] | undefined = bVal;\n\n    if (fromVal === undefined && baseA) {\n      fromVal = baseA[key];\n    }\n    if (toVal === undefined && baseB) {\n      toVal = baseB[key];\n    }\n\n    if (fromVal === undefined || toVal === undefined) {\n      result[key] = (fromVal ?? toVal) as T[keyof T];\n    } else if (typeof fromVal === \"number\" && typeof toVal === \"number\") {\n      result[key] = lerpNumber(fromVal, toVal, t) as T[keyof T];\n    } else if (typeof fromVal === \"boolean\") {\n      result[key] = (t < 0.5 ? fromVal : toVal) as T[keyof T];\n    } else {\n      result[key] = (t < 0.5 ? fromVal : toVal) as T[keyof T];\n    }\n  }\n\n  return Object.keys(result).length > 0 ? result : undefined;\n}\n\nexport function interpolateOverrides(\n  a: ConditionOverrides | undefined,\n  b: ConditionOverrides | undefined,\n  baseA: FullCompositorParams | undefined,\n  baseB: FullCompositorParams | undefined,\n  t: number,\n): ConditionOverrides | undefined {\n  if (!a && !b) return undefined;\n\n  const result: ConditionOverrides = {};\n\n  const layers = interpolatePartialObject(\n    a?.layers,\n    b?.layers,\n    baseA?.layers,\n    baseB?.layers,\n    t,\n  );\n  if (layers) result.layers = layers;\n\n  const celestial = interpolatePartialObject(\n    a?.celestial,\n    b?.celestial,\n    baseA?.celestial,\n    baseB?.celestial,\n    t,\n  );\n  if (celestial) result.celestial = celestial;\n\n  const cloud = interpolatePartialObject(\n    a?.cloud,\n    b?.cloud,\n    baseA?.cloud,\n    baseB?.cloud,\n    t,\n  );\n  if (cloud) result.cloud = cloud;\n\n  const rain = interpolatePartialObject(\n    a?.rain,\n    b?.rain,\n    baseA?.rain,\n    baseB?.rain,\n    t,\n  );\n  if (rain) result.rain = rain;\n\n  const lightning = interpolatePartialObject(\n    a?.lightning,\n    b?.lightning,\n    baseA?.lightning,\n    baseB?.lightning,\n    t,\n  );\n  if (lightning) result.lightning = lightning;\n\n  const snow = interpolatePartialObject(\n    a?.snow,\n    b?.snow,\n    baseA?.snow,\n    baseB?.snow,\n    t,\n  );\n  if (snow) result.snow = snow;\n\n  const glass = interpolatePartialObject(\n    a?.glass,\n    b?.glass,\n    baseA?.glass,\n    baseB?.glass,\n    t,\n  );\n  if (glass) result.glass = glass;\n\n  const post = interpolatePartialObject(\n    a?.post,\n    b?.post,\n    baseA?.post,\n    baseB?.post,\n    t,\n  );\n  if (post) result.post = post;\n\n  return Object.keys(result).length > 0 ? result : undefined;\n}\n\nexport function getInterpolatedOverrides(\n  checkpointOverrides: CheckpointOverrides | undefined,\n  timeOfDay: number,\n  getBaseForCheckpoint?: BaseParamsGetter,\n): ConditionOverrides | undefined {\n  if (!checkpointOverrides) return undefined;\n\n  const { before, after, t } = getSurroundingCheckpoints(timeOfDay);\n\n  const beforeOverrides = checkpointOverrides[before];\n  const afterOverrides = checkpointOverrides[after];\n\n  const baseA = getBaseForCheckpoint?.(before);\n  const baseB = getBaseForCheckpoint?.(after);\n\n  return interpolateOverrides(beforeOverrides, afterOverrides, baseA, baseB, t);\n}\n\nexport function createEmptyCheckpointOverrides(): CheckpointOverrides {\n  return {\n    dawn: {},\n    noon: {},\n    dusk: {},\n    midnight: {},\n  };\n}\n\nexport function isCheckpointOverridesEmpty(\n  checkpointOverrides: CheckpointOverrides,\n): boolean {\n  return (\n    Object.keys(checkpointOverrides.dawn).length === 0 &&\n    Object.keys(checkpointOverrides.noon).length === 0 &&\n    Object.keys(checkpointOverrides.dusk).length === 0 &&\n    Object.keys(checkpointOverrides.midnight).length === 0\n  );\n}\n\nexport function getCheckpointForTime(timeOfDay: number): TimeCheckpoint {\n  const { before, after, t } = getSurroundingCheckpoints(timeOfDay);\n  return t < 0.5 ? before : after;\n}\n\nexport function isNearCheckpoint(\n  timeOfDay: number,\n  checkpoint: TimeCheckpoint,\n  threshold = 0.02,\n): boolean {\n  const checkpointValue = TIME_CHECKPOINTS[checkpoint];\n  return Math.abs(timeOfDay - checkpointValue) < threshold;\n}\n\nexport const getNearestCheckpoint = getNearestCheckpointCore;\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-compositor/page.tsx",
    "content": "\"use client\";\n\nimport { useControls, Leva } from \"leva\";\nimport { useState, useEffect, useCallback, useRef, useMemo } from \"react\";\nimport { CloudCanvas } from \"@/app/sandbox/cloud-effect/cloud-canvas\";\nimport { RainCanvas } from \"@/app/sandbox/rain-effect/rain-canvas\";\nimport { LightningCanvas } from \"@/app/sandbox/lightning-effect/lightning-canvas\";\nimport { SnowCanvas } from \"@/app/sandbox/snow-effect/snow-canvas\";\nimport { CelestialCanvas } from \"./celestial-canvas\";\nimport {\n  Download,\n  Upload,\n  RotateCcw,\n  Eye,\n  Layers,\n  Sparkles,\n} from \"lucide-react\";\nimport { WeatherWidget as AuthoringWeatherWidget } from \"@/lib/weather-authoring/weather-widget\";\nimport { WeatherEffectsCanvas } from \"@/lib/weather-authoring/weather-widget/effects/weather-effects-canvas\";\nimport type { WeatherEffectLayer } from \"@/lib/weather-authoring/weather-widget/effects\";\nimport type { WeatherConditionCode } from \"@/lib/weather-authoring/weather-widget/schema\";\nimport {\n  WEATHER_CONDITIONS,\n  CONDITION_LABELS,\n  getBaseParamsForCondition,\n  mergeWithOverrides,\n  extractOverrides,\n  loadFromStorage,\n  saveToStorage,\n  exportToFile,\n  importFromFile,\n  type FullCompositorParams,\n  type CheckpointOverrides,\n  type GlobalSettings,\n} from \"./presets\";\nimport {\n  getInterpolatedOverrides,\n  getNearestCheckpoint,\n} from \"./interpolation\";\nimport { TIME_CHECKPOINTS } from \"../weather-tuning/lib/constants\";\nimport type { TimeCheckpoint } from \"../weather-tuning/types\";\n\nfunction formatTimeLabel(timeOfDay: number): string {\n  const totalMinutes = timeOfDay * 24 * 60;\n  const hours = Math.floor(totalMinutes / 60);\n  const minutes = Math.round(totalMinutes % 60);\n  const period = hours >= 12 ? \"PM\" : \"AM\";\n  const displayHour = hours % 12 || 12;\n  return `${displayHour}:${minutes.toString().padStart(2, \"0\")} ${period}`;\n}\n\ninterface ConditionPillProps {\n  conditionCode: WeatherConditionCode;\n  isActive: boolean;\n  hasOverrides: boolean;\n  onClick: () => void;\n}\n\nfunction ConditionPill({\n  conditionCode,\n  isActive,\n  hasOverrides,\n  onClick,\n}: ConditionPillProps) {\n  return (\n    <button\n      onClick={onClick}\n      className={`relative rounded-full px-3 py-1.5 text-xs font-medium whitespace-nowrap transition-all ${\n        isActive\n          ? \"bg-white/20 text-white ring-1 ring-white/30\"\n          : \"bg-white/10 text-white/60 hover:bg-white/15 hover:text-white/80\"\n      } `}\n    >\n      {CONDITION_LABELS[conditionCode]}\n      {hasOverrides && (\n        <span className=\"absolute -top-0.5 -right-0.5 size-2 rounded-full bg-blue-400\" />\n      )}\n    </button>\n  );\n}\n\nfunction useDebounce<T>(value: T, delay: number): T {\n  const [debouncedValue, setDebouncedValue] = useState(value);\n\n  useEffect(() => {\n    const timer = setTimeout(() => setDebouncedValue(value), delay);\n    return () => clearTimeout(timer);\n  }, [value, delay]);\n\n  return debouncedValue;\n}\n\nconst DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {\n  timeOfDay: 0.5,\n};\n\nfunction createEmptyCheckpointOverrides(): CheckpointOverrides {\n  return { dawn: {}, noon: {}, dusk: {}, midnight: {} };\n}\n\nexport default function WeatherCompositorSandbox() {\n  const [isMounted, setIsMounted] = useState(false);\n  const [activeCondition, setActiveCondition] =\n    useState<WeatherConditionCode>(\"clear\");\n  const [globalSettings, setGlobalSettings] = useState<GlobalSettings>(\n    DEFAULT_GLOBAL_SETTINGS,\n  );\n  const [checkpointOverrides, setCheckpointOverrides] = useState<\n    Partial<Record<WeatherConditionCode, CheckpointOverrides>>\n  >({});\n  const [previewMode, setPreviewMode] = useState<\n    \"layers\" | \"widget\" | \"unified\"\n  >(\"layers\");\n  const isInitializing = useRef(true);\n  const fileInputRef = useRef<HTMLInputElement>(null);\n\n  useEffect(() => {\n    setIsMounted(true);\n    const stored = loadFromStorage();\n    if (stored) {\n      setActiveCondition(stored.activeCondition);\n      setGlobalSettings(stored.globalSettings ?? DEFAULT_GLOBAL_SETTINGS);\n      setCheckpointOverrides(stored.checkpointOverrides);\n    }\n    setTimeout(() => {\n      isInitializing.current = false;\n    }, 100);\n  }, []);\n\n  const getBaseForCheckpoint = useCallback(\n    (checkpoint: TimeCheckpoint) => {\n      const checkpointTime = TIME_CHECKPOINTS[checkpoint].value;\n      return getBaseParamsForCondition(\n        activeCondition,\n        new Date(Date.now())\n          .toISOString()\n          .replace(\n            /T\\d{2}:\\d{2}/,\n            `T${String(Math.floor(checkpointTime * 24)).padStart(2, \"0\")}:${String(Math.floor(((checkpointTime * 24) % 1) * 60)).padStart(2, \"0\")}`,\n          ),\n      );\n    },\n    [activeCondition],\n  );\n\n  const baseParams = useMemo(\n    () => getBaseParamsForCondition(activeCondition),\n    [activeCondition],\n  );\n  const currentConditionCheckpoints = checkpointOverrides[activeCondition];\n  const currentOverrides = useMemo(\n    () =>\n      getInterpolatedOverrides(\n        currentConditionCheckpoints,\n        globalSettings.timeOfDay,\n        getBaseForCheckpoint,\n      ),\n    [\n      currentConditionCheckpoints,\n      globalSettings.timeOfDay,\n      getBaseForCheckpoint,\n    ],\n  );\n  const mergedParams = useMemo(\n    () => mergeWithOverrides(baseParams, currentOverrides),\n    [baseParams, currentOverrides],\n  );\n\n  // Global controls (not persisted per-condition)\n  const [global, setGlobal] = useControls(\n    \"Global\",\n    () => ({\n      timeOfDay: {\n        value: globalSettings.timeOfDay,\n        min: 0,\n        max: 1,\n        step: 0.01,\n        label: \"Time of Day\",\n      },\n    }),\n    [],\n  );\n\n  // Sync global controls with state\n  useEffect(() => {\n    if (isInitializing.current) return;\n    setGlobalSettings({ timeOfDay: global.timeOfDay });\n  }, [global.timeOfDay]);\n\n  const [layers, setLayers] = useControls(\n    \"Layers\",\n    () => ({\n      celestial: { value: mergedParams.layers.celestial, label: \"Celestial\" },\n      clouds: { value: mergedParams.layers.clouds, label: \"Clouds\" },\n      rain: { value: mergedParams.layers.rain, label: \"Rain\" },\n      lightning: { value: mergedParams.layers.lightning, label: \"Lightning\" },\n      snow: { value: mergedParams.layers.snow, label: \"Snow\" },\n    }),\n    [activeCondition],\n  );\n\n  const enabledLayers = useMemo(\n    () =>\n      (Object.entries(layers) as Array<[WeatherEffectLayer, boolean]>)\n        .filter(([, enabled]) => enabled)\n        .map(([layer]) => layer),\n    [layers],\n  );\n\n  const [celestial, setCelestial] = useControls(\n    \"Celestial\",\n    () => ({\n      moonPhase: {\n        value: mergedParams.celestial.moonPhase,\n        min: 0,\n        max: 1,\n        step: 0.01,\n        label: \"Moon Phase\",\n      },\n      starDensity: {\n        value: mergedParams.celestial.starDensity,\n        min: 0,\n        max: 2,\n        step: 0.05,\n        label: \"Star Density\",\n      },\n      celestialX: {\n        value: mergedParams.celestial.celestialX,\n        min: 0,\n        max: 1,\n        step: 0.01,\n        label: \"Position X\",\n      },\n      celestialY: {\n        value: mergedParams.celestial.celestialY,\n        min: 0,\n        max: 1,\n        step: 0.01,\n        label: \"Position Y\",\n      },\n      sunSize: {\n        value: mergedParams.celestial.sunSize,\n        min: 0.01,\n        max: 0.8,\n        step: 0.005,\n        label: \"Sun Size\",\n      },\n      moonSize: {\n        value: mergedParams.celestial.moonSize,\n        min: 0.01,\n        max: 0.6,\n        step: 0.005,\n        label: \"Moon Size\",\n      },\n      sunGlowIntensity: {\n        value: mergedParams.celestial.sunGlowIntensity,\n        min: 0,\n        max: 5,\n        step: 0.05,\n        label: \"Sun Glow\",\n      },\n      sunGlowSize: {\n        value: mergedParams.celestial.sunGlowSize,\n        min: 0.05,\n        max: 2,\n        step: 0.05,\n        label: \"Sun Glow Size\",\n      },\n      sunRayCount: {\n        value: mergedParams.celestial.sunRayCount,\n        min: 0,\n        max: 48,\n        step: 1,\n        label: \"Sun Ray Count\",\n      },\n      sunRayLength: {\n        value: mergedParams.celestial.sunRayLength,\n        min: 0,\n        max: 3,\n        step: 0.05,\n        label: \"Sun Ray Length\",\n      },\n      sunRayIntensity: {\n        value: mergedParams.celestial.sunRayIntensity,\n        min: 0,\n        max: 3,\n        step: 0.05,\n        label: \"Sun Ray Intensity\",\n      },\n      sunRayShimmer: {\n        value: mergedParams.celestial.sunRayShimmer,\n        min: 0,\n        max: 5,\n        step: 0.05,\n        label: \"Sun Ray Shimmer\",\n      },\n      sunRayShimmerSpeed: {\n        value: mergedParams.celestial.sunRayShimmerSpeed,\n        min: 0,\n        max: 5,\n        step: 0.05,\n        label: \"Sun Ray Shimmer Speed\",\n      },\n      moonGlowIntensity: {\n        value: mergedParams.celestial.moonGlowIntensity,\n        min: 0,\n        max: 5,\n        step: 0.05,\n        label: \"Moon Glow\",\n      },\n      moonGlowSize: {\n        value: mergedParams.celestial.moonGlowSize,\n        min: 0.05,\n        max: 1.5,\n        step: 0.02,\n        label: \"Moon Glow Size\",\n      },\n      skyBrightness: {\n        value: mergedParams.celestial.skyBrightness,\n        min: 0,\n        max: 2,\n        step: 0.05,\n        label: \"Sky Brightness\",\n      },\n      skySaturation: {\n        value: mergedParams.celestial.skySaturation,\n        min: 0,\n        max: 2,\n        step: 0.05,\n        label: \"Sky Saturation\",\n      },\n      skyContrast: {\n        value: mergedParams.celestial.skyContrast,\n        min: 0,\n        max: 1,\n        step: 0.05,\n        label: \"Sky Contrast\",\n      },\n    }),\n    [activeCondition],\n  );\n\n  const [cloud, setCloud] = useControls(\n    \"Clouds\",\n    () => ({\n      cloudScale: {\n        value: mergedParams.cloud.cloudScale,\n        min: 0.1,\n        max: 10,\n        step: 0.1,\n        label: \"Scale\",\n      },\n      coverage: {\n        value: mergedParams.cloud.coverage,\n        min: 0,\n        max: 1,\n        step: 0.01,\n        label: \"Coverage\",\n      },\n      density: {\n        value: mergedParams.cloud.density,\n        min: 0,\n        max: 2,\n        step: 0.01,\n        label: \"Density\",\n      },\n      softness: {\n        value: mergedParams.cloud.softness,\n        min: 0,\n        max: 2,\n        step: 0.01,\n        label: \"Softness\",\n      },\n      windSpeed: {\n        value: mergedParams.cloud.windSpeed,\n        min: 0,\n        max: 5,\n        step: 0.05,\n        label: \"Wind Speed\",\n      },\n      windAngle: {\n        value: mergedParams.cloud.windAngle,\n        min: -Math.PI,\n        max: Math.PI,\n        step: 0.1,\n        label: \"Wind Angle\",\n      },\n      turbulence: {\n        value: mergedParams.cloud.turbulence,\n        min: 0,\n        max: 5,\n        step: 0.05,\n        label: \"Turbulence\",\n      },\n      lightIntensity: {\n        value: mergedParams.cloud.lightIntensity,\n        min: 0,\n        max: 5,\n        step: 0.05,\n        label: \"Light Intensity\",\n      },\n      ambientDarkness: {\n        value: mergedParams.cloud.ambientDarkness,\n        min: 0,\n        max: 1,\n        step: 0.05,\n        label: \"Darkness\",\n      },\n      backlightIntensity: {\n        value: mergedParams.cloud.backlightIntensity ?? 0.5,\n        min: 0,\n        max: 2,\n        step: 0.05,\n        label: \"Backlight\",\n      },\n      numLayers: {\n        value: mergedParams.cloud.numLayers,\n        min: 1,\n        max: 10,\n        step: 1,\n        label: \"Layers\",\n      },\n      // Legacy params for original CloudCanvas only:\n      sunAzimuth: {\n        value: mergedParams.cloud.sunAzimuth,\n        min: -Math.PI,\n        max: Math.PI,\n        step: 0.1,\n        label: \"Sun Azimuth (legacy)\",\n      },\n      layerSpread: {\n        value: mergedParams.cloud.layerSpread,\n        min: 0,\n        max: 2,\n        step: 0.05,\n        label: \"Layer Spread (legacy)\",\n      },\n      starSize: {\n        value: mergedParams.cloud.starSize,\n        min: 0.1,\n        max: 5,\n        step: 0.1,\n        label: \"Star Size (legacy)\",\n      },\n      starTwinkleSpeed: {\n        value: mergedParams.cloud.starTwinkleSpeed,\n        min: 0,\n        max: 10,\n        step: 0.1,\n        label: \"Twinkle Speed (legacy)\",\n      },\n      starTwinkleAmount: {\n        value: mergedParams.cloud.starTwinkleAmount,\n        min: 0,\n        max: 2,\n        step: 0.05,\n        label: \"Twinkle Amount (legacy)\",\n      },\n      horizonLine: {\n        value: mergedParams.cloud.horizonLine,\n        min: 0,\n        max: 1,\n        step: 0.01,\n        label: \"Horizon (legacy)\",\n      },\n    }),\n    [activeCondition],\n  );\n\n  const [rain, setRain] = useControls(\n    \"Rain\",\n    () => ({\n      glassIntensity: {\n        value: mergedParams.rain.glassIntensity,\n        min: 0,\n        max: 2,\n        step: 0.05,\n        label: \"Glass Drops\",\n      },\n      zoom: {\n        value: mergedParams.rain.zoom,\n        min: 0.1,\n        max: 5,\n        step: 0.05,\n        label: \"Zoom\",\n      },\n      fallingIntensity: {\n        value: mergedParams.rain.fallingIntensity,\n        min: 0,\n        max: 2,\n        step: 0.05,\n        label: \"Falling Rain\",\n      },\n      fallingSpeed: {\n        value: mergedParams.rain.fallingSpeed,\n        min: 0.05,\n        max: 10,\n        step: 0.1,\n        label: \"Fall Speed\",\n      },\n      fallingAngle: {\n        value: mergedParams.rain.fallingAngle,\n        min: -1.5,\n        max: 1.5,\n        step: 0.02,\n        label: \"Angle\",\n      },\n      fallingStreakLength: {\n        value: mergedParams.rain.fallingStreakLength,\n        min: 0.1,\n        max: 5,\n        step: 0.05,\n        label: \"Streak Length\",\n      },\n      fallingLayers: {\n        value: mergedParams.rain.fallingLayers,\n        min: 1,\n        max: 10,\n        step: 1,\n        label: \"Layers\",\n      },\n      fallingRefraction: {\n        value: mergedParams.rain.fallingRefraction,\n        min: 0,\n        max: 2,\n        step: 0.05,\n        label: \"Refraction\",\n      },\n      fallingWaviness: {\n        value: mergedParams.rain.fallingWaviness,\n        min: 0,\n        max: 2,\n        step: 0.02,\n        label: \"Waviness\",\n      },\n      fallingThicknessVar: {\n        value: mergedParams.rain.fallingThicknessVar,\n        min: 0,\n        max: 2,\n        step: 0.05,\n        label: \"Thickness Var\",\n      },\n    }),\n    [activeCondition],\n  );\n\n  const [lightning, setLightning] = useControls(\n    \"Lightning\",\n    () => ({\n      branchDensity: {\n        value: mergedParams.lightning.branchDensity,\n        min: 0,\n        max: 2,\n        step: 0.05,\n        label: \"Branch Density\",\n      },\n      displacement: {\n        value: mergedParams.lightning.displacement,\n        min: 0,\n        max: 1,\n        step: 0.01,\n        label: \"Displacement\",\n      },\n      glowIntensity: {\n        value: mergedParams.lightning.glowIntensity,\n        min: 0,\n        max: 5,\n        step: 0.05,\n        label: \"Glow Intensity\",\n      },\n      flashDuration: {\n        value: mergedParams.lightning.flashDuration,\n        min: 0.01,\n        max: 2,\n        step: 0.01,\n        label: \"Flash Duration\",\n      },\n      sceneIllumination: {\n        value: mergedParams.lightning.sceneIllumination,\n        min: 0,\n        max: 2,\n        step: 0.05,\n        label: \"Scene Light\",\n      },\n      afterglowPersistence: {\n        value: mergedParams.lightning.afterglowPersistence,\n        min: 0,\n        max: 2,\n        step: 0.05,\n        label: \"Afterglow\",\n      },\n      autoMode: {\n        value: mergedParams.lightning.autoMode,\n        label: \"Auto Trigger\",\n      },\n      autoInterval: {\n        value: mergedParams.lightning.autoInterval,\n        min: 0.5,\n        max: 60,\n        step: 0.5,\n        label: \"Interval (s)\",\n      },\n    }),\n    [activeCondition],\n  );\n\n  const [snow, setSnow] = useControls(\n    \"Snow\",\n    () => ({\n      intensity: {\n        value: mergedParams.snow.intensity,\n        min: 0,\n        max: 2,\n        step: 0.05,\n        label: \"Intensity\",\n      },\n      layers: {\n        value: mergedParams.snow.layers,\n        min: 1,\n        max: 12,\n        step: 1,\n        label: \"Layers\",\n      },\n      fallSpeed: {\n        value: mergedParams.snow.fallSpeed,\n        min: 0.01,\n        max: 5,\n        step: 0.05,\n        label: \"Fall Speed\",\n      },\n      windSpeed: {\n        value: mergedParams.snow.windSpeed,\n        min: 0,\n        max: 3,\n        step: 0.05,\n        label: \"Wind Speed\",\n      },\n      windAngle: {\n        value: mergedParams.snow.windAngle,\n        min: -Math.PI,\n        max: Math.PI,\n        step: 0.1,\n        label: \"Wind Angle\",\n      },\n      turbulence: {\n        value: mergedParams.snow.turbulence,\n        min: 0,\n        max: 3,\n        step: 0.05,\n        label: \"Turbulence\",\n      },\n      drift: {\n        value: mergedParams.snow.drift,\n        min: 0,\n        max: 3,\n        step: 0.05,\n        label: \"Drift\",\n      },\n      flutter: {\n        value: mergedParams.snow.flutter,\n        min: 0,\n        max: 3,\n        step: 0.05,\n        label: \"Flutter\",\n      },\n      windShear: {\n        value: mergedParams.snow.windShear,\n        min: 0,\n        max: 3,\n        step: 0.05,\n        label: \"Wind Shear\",\n      },\n      flakeSize: {\n        value: mergedParams.snow.flakeSize,\n        min: 0.1,\n        max: 5,\n        step: 0.05,\n        label: \"Flake Size\",\n      },\n      sizeVariation: {\n        value: mergedParams.snow.sizeVariation,\n        min: 0,\n        max: 2,\n        step: 0.05,\n        label: \"Size Variation\",\n      },\n      opacity: {\n        value: mergedParams.snow.opacity,\n        min: 0,\n        max: 2,\n        step: 0.05,\n        label: \"Opacity\",\n      },\n      glowAmount: {\n        value: mergedParams.snow.glowAmount,\n        min: 0,\n        max: 3,\n        step: 0.05,\n        label: \"Glow\",\n      },\n      sparkle: {\n        value: mergedParams.snow.sparkle,\n        min: 0,\n        max: 3,\n        step: 0.05,\n        label: \"Sparkle\",\n      },\n      visibility: {\n        value: mergedParams.snow.visibility,\n        min: 0,\n        max: 2,\n        step: 0.05,\n        label: \"Visibility\",\n      },\n    }),\n    [activeCondition],\n  );\n\n  const [interactions] = useControls(\n    \"Interactions (Unified)\",\n    () => ({\n      rainRefractionStrength: {\n        value: 1.0,\n        min: 0,\n        max: 3,\n        step: 0.05,\n        label: \"Rain Refraction\",\n      },\n      lightningSceneIllumination: {\n        value: 0.6,\n        min: 0,\n        max: 2,\n        step: 0.05,\n        label: \"Lightning Illumination\",\n      },\n    }),\n    [],\n  );\n\n  const [_glassPanel] = useControls(\n    \"Glass Panel (Unified)\",\n    () => ({\n      enabled: { value: true, label: \"Enabled\" },\n      regionX: { value: 0.05, min: 0, max: 1, step: 0.01, label: \"Region X\" },\n      regionY: { value: 0.65, min: 0, max: 1, step: 0.01, label: \"Region Y\" },\n      regionW: {\n        value: 0.9,\n        min: 0,\n        max: 1,\n        step: 0.01,\n        label: \"Region Width\",\n      },\n      regionH: {\n        value: 0.25,\n        min: 0,\n        max: 1,\n        step: 0.01,\n        label: \"Region Height\",\n      },\n      refractionScale: {\n        value: 30,\n        min: 10,\n        max: 80,\n        step: 1,\n        label: \"Refraction Scale\",\n      },\n      edgeWidth: {\n        value: 0.15,\n        min: 0.05,\n        max: 0.5,\n        step: 0.01,\n        label: \"Edge Width\",\n      },\n      chromaticAberration: {\n        value: 1.2,\n        min: 1.0,\n        max: 2.0,\n        step: 0.05,\n        label: \"Chromatic Aberration\",\n      },\n      specularIntensity: {\n        value: 1.5,\n        min: 0,\n        max: 3,\n        step: 0.1,\n        label: \"Specular Intensity\",\n      },\n      lightAngle: {\n        value: 225,\n        min: 0,\n        max: 360,\n        step: 5,\n        label: \"Light Angle (°)\",\n      },\n    }),\n    [],\n  );\n\n  // Combine Leva values with global timeOfDay for full params\n  const currentParams: FullCompositorParams = {\n    layers,\n    celestial: { ...celestial, timeOfDay: global.timeOfDay },\n    cloud,\n    rain,\n    lightning,\n    snow,\n    glass: {\n      enabled: true,\n      depth: 3,\n      strength: 75,\n      chromaticAberration: 6,\n      blur: 1.5,\n      brightness: 0.8,\n      saturation: 1.3,\n    },\n    post: {\n      enabled: true,\n      haze: 0,\n      hazeHorizon: 0.5,\n      hazeDesaturation: 0.3,\n      hazeContrast: 0.2,\n      bloomIntensity: 0,\n      bloomThreshold: 0.8,\n      bloomKnee: 0.5,\n      bloomRadius: 8,\n      bloomTapScale: 1,\n      exposureIntensity: 0,\n      exposureDesaturation: 0.3,\n      exposureRecovery: 2,\n      godRayIntensity: 0,\n      godRayDecay: 0.96,\n      godRayDensity: 0.5,\n      godRayWeight: 0.3,\n      godRaySamples: 60,\n    },\n  };\n\n  const debouncedParams = useDebounce(currentParams, 300);\n  const debouncedCondition = useDebounce(activeCondition, 300);\n\n  useEffect(() => {\n    if (isInitializing.current || !isMounted) return;\n\n    // Guard against race conditionCode: only save if the debounced condition\n    // matches the current active condition. This prevents params from\n    // condition A being saved to condition B when switching quickly.\n    if (debouncedCondition !== activeCondition) return;\n\n    // Use checkpoint-specific base for extracting overrides to ensure\n    // we compute deltas against the correct defaults\n    const activeCheckpoint = getNearestCheckpoint(globalSettings.timeOfDay);\n    const checkpointBase = getBaseForCheckpoint(activeCheckpoint);\n    const newOverrides = extractOverrides(debouncedParams, checkpointBase);\n    const hasChanges = Object.keys(newOverrides).length > 0;\n\n    setCheckpointOverrides((prev) => {\n      const existing =\n        prev[debouncedCondition] ?? createEmptyCheckpointOverrides();\n      if (hasChanges) {\n        return {\n          ...prev,\n          [debouncedCondition]: {\n            ...existing,\n            [activeCheckpoint]: newOverrides,\n          },\n        };\n      } else {\n        const updated = {\n          ...existing,\n          [activeCheckpoint]: {},\n        };\n        const allEmpty = Object.values(updated).every(\n          (o) => Object.keys(o).length === 0,\n        );\n        if (allEmpty) {\n          const next = { ...prev };\n          delete next[debouncedCondition];\n          return next;\n        }\n        return { ...prev, [debouncedCondition]: updated };\n      }\n    });\n  }, [\n    debouncedParams,\n    debouncedCondition,\n    activeCondition,\n    getBaseForCheckpoint,\n    isMounted,\n    globalSettings.timeOfDay,\n  ]);\n\n  const stateToSave = useDebounce(\n    { activeCondition, globalSettings, checkpointOverrides },\n    500,\n  );\n  useEffect(() => {\n    if (!isMounted || isInitializing.current) return;\n    saveToStorage({ version: 4, ...stateToSave });\n  }, [stateToSave, isMounted]);\n\n  const handleConditionChange = useCallback(\n    (conditionCode: WeatherConditionCode) => {\n      setActiveCondition(conditionCode);\n      const newBase = getBaseParamsForCondition(conditionCode);\n      const existingCheckpoints = checkpointOverrides[conditionCode];\n      const getBaseForNewCondition = (checkpoint: TimeCheckpoint) => {\n        const checkpointTime = TIME_CHECKPOINTS[checkpoint].value;\n        return getBaseParamsForCondition(\n          conditionCode,\n          new Date(Date.now())\n            .toISOString()\n            .replace(\n              /T\\d{2}:\\d{2}/,\n              `T${String(Math.floor(checkpointTime * 24)).padStart(2, \"0\")}:${String(Math.floor(((checkpointTime * 24) % 1) * 60)).padStart(2, \"0\")}`,\n            ),\n        );\n      };\n      const interpolatedOverrides = getInterpolatedOverrides(\n        existingCheckpoints,\n        globalSettings.timeOfDay,\n        getBaseForNewCondition,\n      );\n      const merged = mergeWithOverrides(newBase, interpolatedOverrides);\n\n      setLayers(merged.layers);\n      setCelestial({\n        moonPhase: merged.celestial.moonPhase,\n        starDensity: merged.celestial.starDensity,\n        celestialX: merged.celestial.celestialX,\n        celestialY: merged.celestial.celestialY,\n        sunSize: merged.celestial.sunSize,\n        moonSize: merged.celestial.moonSize,\n        sunGlowIntensity: merged.celestial.sunGlowIntensity,\n        sunGlowSize: merged.celestial.sunGlowSize,\n        sunRayCount: merged.celestial.sunRayCount,\n        sunRayLength: merged.celestial.sunRayLength,\n        sunRayIntensity: merged.celestial.sunRayIntensity,\n        sunRayShimmer: merged.celestial.sunRayShimmer,\n        sunRayShimmerSpeed: merged.celestial.sunRayShimmerSpeed,\n        moonGlowIntensity: merged.celestial.moonGlowIntensity,\n        moonGlowSize: merged.celestial.moonGlowSize,\n      });\n      setCloud(merged.cloud);\n      setRain(merged.rain);\n      setLightning(merged.lightning);\n      setSnow(merged.snow);\n    },\n    [\n      checkpointOverrides,\n      globalSettings.timeOfDay,\n      setLayers,\n      setCelestial,\n      setCloud,\n      setRain,\n      setLightning,\n      setSnow,\n    ],\n  );\n\n  const handleExport = useCallback(() => {\n    exportToFile({\n      version: 4,\n      activeCondition,\n      globalSettings,\n      checkpointOverrides,\n    });\n  }, [activeCondition, globalSettings, checkpointOverrides]);\n\n  const handleImport = useCallback(\n    async (e: React.ChangeEvent<HTMLInputElement>) => {\n      const file = e.target.files?.[0];\n      if (!file) return;\n\n      try {\n        const imported = await importFromFile(file);\n        setActiveCondition(imported.activeCondition);\n        setGlobalSettings(imported.globalSettings ?? DEFAULT_GLOBAL_SETTINGS);\n        setCheckpointOverrides(imported.checkpointOverrides);\n        setGlobal({ timeOfDay: imported.globalSettings?.timeOfDay ?? 0.5 });\n        handleConditionChange(imported.activeCondition);\n      } catch (err) {\n        console.error(\"Failed to import presets:\", err);\n      }\n\n      if (fileInputRef.current) {\n        fileInputRef.current.value = \"\";\n      }\n    },\n    [handleConditionChange, setGlobal],\n  );\n\n  const handleReset = useCallback(() => {\n    setCheckpointOverrides((prev) => {\n      const updated = { ...prev };\n      delete updated[activeCondition];\n      return updated;\n    });\n    const base = getBaseParamsForCondition(activeCondition);\n    setLayers(base.layers);\n    setCelestial({\n      moonPhase: base.celestial.moonPhase,\n      starDensity: base.celestial.starDensity,\n      celestialX: base.celestial.celestialX,\n      celestialY: base.celestial.celestialY,\n      sunSize: base.celestial.sunSize,\n      moonSize: base.celestial.moonSize,\n      sunGlowIntensity: base.celestial.sunGlowIntensity,\n      sunGlowSize: base.celestial.sunGlowSize,\n      sunRayCount: base.celestial.sunRayCount,\n      sunRayLength: base.celestial.sunRayLength,\n      sunRayIntensity: base.celestial.sunRayIntensity,\n      sunRayShimmer: base.celestial.sunRayShimmer,\n      sunRayShimmerSpeed: base.celestial.sunRayShimmerSpeed,\n      moonGlowIntensity: base.celestial.moonGlowIntensity,\n      moonGlowSize: base.celestial.moonGlowSize,\n    });\n    setCloud(base.cloud);\n    setRain(base.rain);\n    setLightning(base.lightning);\n    setSnow(base.snow);\n  }, [\n    activeCondition,\n    setLayers,\n    setCelestial,\n    setCloud,\n    setRain,\n    setLightning,\n    setSnow,\n  ]);\n\n  if (!isMounted) {\n    return null;\n  }\n\n  const timeOfDay = global.timeOfDay;\n\n  return (\n    <div className=\"relative min-h-screen bg-black\">\n      <Leva\n        collapsed={false}\n        flat={false}\n        titleBar={{ title: \"Weather Compositor\" }}\n        theme={{\n          sizes: {\n            rootWidth: \"280px\",\n            controlWidth: \"140px\",\n          },\n        }}\n      />\n\n      <div className=\"absolute inset-0 flex flex-col items-center justify-center gap-4 p-8\">\n        <div className=\"flex w-full max-w-[600px] flex-wrap items-center justify-center gap-2\">\n          {WEATHER_CONDITIONS.map((condition) => (\n            <ConditionPill\n              key={condition}\n              conditionCode={condition}\n              isActive={condition === activeCondition}\n              hasOverrides={checkpointOverrides[condition] !== undefined}\n              onClick={() => handleConditionChange(condition)}\n            />\n          ))}\n          <div className=\"ml-2 flex items-center gap-1 border-l border-white/20 pl-2\">\n            <button\n              onClick={handleExport}\n              className=\"rounded p-1.5 text-white/60 transition-colors hover:bg-white/10 hover:text-white\"\n              title=\"Export presets\"\n            >\n              <Download className=\"size-4\" />\n            </button>\n            <button\n              onClick={() => fileInputRef.current?.click()}\n              className=\"rounded p-1.5 text-white/60 transition-colors hover:bg-white/10 hover:text-white\"\n              title=\"Import presets\"\n            >\n              <Upload className=\"size-4\" />\n            </button>\n            <button\n              onClick={handleReset}\n              className=\"rounded p-1.5 text-white/60 transition-colors hover:bg-white/10 hover:text-white\"\n              title=\"Reset current condition to defaults\"\n            >\n              <RotateCcw className=\"size-4\" />\n            </button>\n            <button\n              onClick={() => setPreviewMode(\"layers\")}\n              className={`rounded p-1.5 transition-colors ${\n                previewMode === \"layers\"\n                  ? \"bg-blue-500/30 text-blue-300\"\n                  : \"text-white/60 hover:bg-white/10 hover:text-white\"\n              }`}\n              title=\"Show separate effect layers\"\n            >\n              <Layers className=\"size-4\" />\n            </button>\n            <button\n              onClick={() => setPreviewMode(\"widget\")}\n              className={`rounded p-1.5 transition-colors ${\n                previewMode === \"widget\"\n                  ? \"bg-blue-500/30 text-blue-300\"\n                  : \"text-white/60 hover:bg-white/10 hover:text-white\"\n              }`}\n              title=\"Show authoring widget with custom compositor\"\n            >\n              <Eye className=\"size-4\" />\n            </button>\n            <button\n              onClick={() => setPreviewMode(\"unified\")}\n              className={`rounded p-1.5 transition-colors ${\n                previewMode === \"unified\"\n                  ? \"bg-purple-500/30 text-purple-300\"\n                  : \"text-white/60 hover:bg-white/10 hover:text-white\"\n              }`}\n              title=\"Show production-style unified runtime layering\"\n            >\n              <Sparkles className=\"size-4\" />\n            </button>\n            <input\n              ref={fileInputRef}\n              type=\"file\"\n              accept=\".json\"\n              onChange={handleImport}\n              className=\"hidden\"\n            />\n          </div>\n        </div>\n\n        <div\n          className=\"relative overflow-hidden rounded-xl border border-white/10\"\n          style={{\n            width: previewMode !== \"layers\" ? \"480px\" : \"400px\",\n            height: previewMode !== \"layers\" ? \"340px\" : \"280px\",\n          }}\n        >\n          {previewMode === \"unified\" ? (\n            <div className=\"relative h-full w-full\">\n              <WeatherEffectsCanvas\n                className=\"absolute inset-0\"\n                layers={{\n                  celestial: layers.celestial,\n                  clouds: layers.clouds,\n                  rain: layers.rain,\n                  lightning: layers.lightning,\n                  snow: layers.snow,\n                }}\n                celestial={{\n                  timeOfDay,\n                  moonPhase: celestial.moonPhase,\n                  starDensity: celestial.starDensity,\n                  celestialX: celestial.celestialX,\n                  celestialY: celestial.celestialY,\n                  sunSize: celestial.sunSize,\n                  moonSize: celestial.moonSize,\n                  sunGlowIntensity: celestial.sunGlowIntensity,\n                  sunGlowSize: celestial.sunGlowSize,\n                  sunRayCount: celestial.sunRayCount,\n                  sunRayLength: celestial.sunRayLength,\n                  sunRayIntensity: celestial.sunRayIntensity,\n                  sunRayShimmer: celestial.sunRayShimmer,\n                  sunRayShimmerSpeed: celestial.sunRayShimmerSpeed,\n                  moonGlowIntensity: celestial.moonGlowIntensity,\n                  moonGlowSize: celestial.moonGlowSize,\n                }}\n                cloud={{\n                  coverage: cloud.coverage,\n                  density: cloud.density,\n                  softness: cloud.softness,\n                  cloudScale: cloud.cloudScale,\n                  windSpeed: cloud.windSpeed,\n                  windAngle: cloud.windAngle,\n                  turbulence: cloud.turbulence,\n                  lightIntensity: cloud.lightIntensity,\n                  ambientDarkness: cloud.ambientDarkness,\n                  backlightIntensity: cloud.backlightIntensity,\n                  numLayers: cloud.numLayers,\n                }}\n                rain={{\n                  glassIntensity: rain.glassIntensity,\n                  glassZoom: rain.zoom,\n                  fallingIntensity: rain.fallingIntensity,\n                  fallingSpeed: rain.fallingSpeed,\n                  fallingAngle: rain.fallingAngle,\n                  fallingStreakLength: rain.fallingStreakLength,\n                  fallingLayers: rain.fallingLayers,\n                }}\n                lightning={{\n                  enabled: true,\n                  autoMode: lightning.autoMode,\n                  autoInterval: lightning.autoInterval,\n                  flashIntensity: lightning.glowIntensity,\n                  branchDensity: lightning.branchDensity,\n                }}\n                snow={{\n                  intensity: snow.intensity,\n                  layers: snow.layers,\n                  fallSpeed: snow.fallSpeed,\n                  windSpeed: snow.windSpeed,\n                  drift: snow.drift,\n                  flakeSize: snow.flakeSize,\n                }}\n                interactions={{\n                  rainRefractionStrength: interactions.rainRefractionStrength,\n                  lightningSceneIllumination:\n                    interactions.lightningSceneIllumination,\n                }}\n              />\n              <div className=\"relative z-10 h-full w-full [&_[data-slot=card]]:h-full [&_[data-slot=card]]:border-0 [&_[data-slot=card]]:bg-transparent [&_[data-slot=card]]:shadow-none [&_[data-slot=weather-widget]]:h-full [&_[data-slot=weather-widget]]:max-w-none [&_article]:h-full\">\n                <AuthoringWeatherWidget\n                  version=\"3.1\"\n                  id=\"unified-preview\"\n                  location={{ name: \"San Francisco, CA\" }}\n                  units={{ temperature: \"fahrenheit\" }}\n                  current={{\n                    temperature: 72,\n                    tempMin: 65,\n                    tempMax: 78,\n                    conditionCode: activeCondition,\n                  }}\n                  forecast={[\n                    {\n                      label: \"Today\",\n                      tempMin: 65,\n                      tempMax: 78,\n                      conditionCode: activeCondition,\n                    },\n                    {\n                      label: \"Tue\",\n                      tempMin: 64,\n                      tempMax: 77,\n                      conditionCode: \"partly-cloudy\",\n                    },\n                    {\n                      label: \"Wed\",\n                      tempMin: 61,\n                      tempMax: 73,\n                      conditionCode: \"cloudy\",\n                    },\n                    {\n                      label: \"Thu\",\n                      tempMin: 58,\n                      tempMax: 68,\n                      conditionCode: \"rain\",\n                    },\n                    {\n                      label: \"Fri\",\n                      tempMin: 55,\n                      tempMax: 65,\n                      conditionCode: \"drizzle\",\n                    },\n                  ]}\n                  updatedAt={(() => {\n                    const date = new Date();\n                    // Keep this aligned with `getTimeOfDay`, which interprets timestamps in UTC.\n                    date.setUTCHours(\n                      Math.floor(timeOfDay * 24),\n                      Math.floor(((timeOfDay * 24) % 1) * 60),\n                      0,\n                      0,\n                    );\n                    return date.toISOString();\n                  })()}\n                  time={{ localTimeOfDay: timeOfDay }}\n                  effects={{ enabled: false }}\n                />\n              </div>\n            </div>\n          ) : previewMode === \"widget\" ? (\n            <AuthoringWeatherWidget\n              version=\"3.1\"\n              id=\"compositor-preview\"\n              location={{ name: \"San Francisco, CA\" }}\n              units={{ temperature: \"fahrenheit\" }}\n              current={{\n                temperature: 72,\n                tempMin: 65,\n                tempMax: 78,\n                conditionCode: activeCondition,\n              }}\n              forecast={[\n                {\n                  label: \"Today\",\n                  tempMin: 65,\n                  tempMax: 78,\n                  conditionCode: activeCondition,\n                },\n                {\n                  label: \"Tue\",\n                  tempMin: 64,\n                  tempMax: 77,\n                  conditionCode: \"partly-cloudy\",\n                },\n                {\n                  label: \"Wed\",\n                  tempMin: 61,\n                  tempMax: 73,\n                  conditionCode: \"cloudy\",\n                },\n                {\n                  label: \"Thu\",\n                  tempMin: 58,\n                  tempMax: 68,\n                  conditionCode: \"rain\",\n                },\n                {\n                  label: \"Fri\",\n                  tempMin: 55,\n                  tempMax: 65,\n                  conditionCode: \"drizzle\",\n                },\n                {\n                  label: \"Sat\",\n                  tempMin: 60,\n                  tempMax: 72,\n                  conditionCode: \"partly-cloudy\",\n                },\n                {\n                  label: \"Sun\",\n                  tempMin: 63,\n                  tempMax: 75,\n                  conditionCode: \"clear\",\n                },\n              ]}\n              updatedAt={(() => {\n                const date = new Date();\n                // Keep this aligned with `getTimeOfDay`, which interprets timestamps in UTC.\n                date.setUTCHours(\n                  Math.floor(timeOfDay * 24),\n                  Math.floor(((timeOfDay * 24) % 1) * 60),\n                  0,\n                  0,\n                );\n                return date.toISOString();\n              })()}\n              time={{ localTimeOfDay: timeOfDay }}\n              effects={{ enabled: true }}\n              customEffectProps={{\n                enabledLayers,\n                celestial: layers.celestial\n                  ? {\n                      timeOfDay,\n                      moonPhase: celestial.moonPhase,\n                      starDensity: celestial.starDensity,\n                      celestialX: celestial.celestialX,\n                      celestialY: celestial.celestialY,\n                      sunSize: celestial.sunSize,\n                      moonSize: celestial.moonSize,\n                      sunGlowIntensity: celestial.sunGlowIntensity,\n                      sunGlowSize: celestial.sunGlowSize,\n                      sunRayCount: celestial.sunRayCount,\n                      sunRayLength: celestial.sunRayLength,\n                      sunRayIntensity: celestial.sunRayIntensity,\n                      sunRayShimmer: celestial.sunRayShimmer,\n                      sunRayShimmerSpeed: celestial.sunRayShimmerSpeed,\n                      moonGlowIntensity: celestial.moonGlowIntensity,\n                      moonGlowSize: celestial.moonGlowSize,\n                    }\n                  : undefined,\n                cloud: layers.clouds\n                  ? {\n                      cloudScale: cloud.cloudScale,\n                      coverage: cloud.coverage,\n                      density: cloud.density,\n                      softness: cloud.softness,\n                      windSpeed: cloud.windSpeed,\n                      windAngle: cloud.windAngle,\n                      turbulence: cloud.turbulence,\n                      sunAltitude:\n                        timeOfDay < 0.5 ? timeOfDay * 2 : 2 - timeOfDay * 2,\n                      sunAzimuth: cloud.sunAzimuth,\n                      lightIntensity: cloud.lightIntensity,\n                      ambientDarkness: cloud.ambientDarkness,\n                      numLayers: cloud.numLayers,\n                      layerSpread: cloud.layerSpread,\n                      starDensity: 0,\n                      starSize: cloud.starSize,\n                      starTwinkleSpeed: cloud.starTwinkleSpeed,\n                      starTwinkleAmount: cloud.starTwinkleAmount,\n                      horizonLine: cloud.horizonLine,\n                    }\n                  : undefined,\n                rain: layers.rain\n                  ? {\n                      glassIntensity: rain.glassIntensity,\n                      zoom: rain.zoom,\n                      fallingIntensity: rain.fallingIntensity,\n                      fallingSpeed: rain.fallingSpeed,\n                      fallingAngle: rain.fallingAngle,\n                      fallingStreakLength: rain.fallingStreakLength,\n                      fallingLayers: rain.fallingLayers,\n                      fallingRefraction: rain.fallingRefraction,\n                      fallingWaviness: rain.fallingWaviness,\n                      fallingThicknessVar: rain.fallingThicknessVar,\n                    }\n                  : undefined,\n                lightning: layers.lightning\n                  ? {\n                      branchDensity: lightning.branchDensity,\n                      displacement: lightning.displacement,\n                      glowIntensity: lightning.glowIntensity,\n                      flashDuration: lightning.flashDuration,\n                      sceneIllumination: lightning.sceneIllumination,\n                      afterglowPersistence: lightning.afterglowPersistence,\n                      autoMode: lightning.autoMode,\n                      autoInterval: lightning.autoInterval,\n                    }\n                  : undefined,\n                snow: layers.snow\n                  ? {\n                      intensity: snow.intensity,\n                      layers: snow.layers,\n                      fallSpeed: snow.fallSpeed,\n                      windSpeed: snow.windSpeed,\n                      windAngle: snow.windAngle,\n                      turbulence: snow.turbulence,\n                      drift: snow.drift,\n                      flutter: snow.flutter,\n                      windShear: snow.windShear,\n                      flakeSize: snow.flakeSize,\n                      sizeVariation: snow.sizeVariation,\n                      opacity: snow.opacity,\n                      glowAmount: snow.glowAmount,\n                      sparkle: snow.sparkle,\n                      visibility: snow.visibility,\n                    }\n                  : undefined,\n              }}\n            />\n          ) : (\n            <>\n              {layers.celestial && (\n                <CelestialCanvas\n                  className=\"absolute inset-0\"\n                  timeOfDay={timeOfDay}\n                  moonPhase={celestial.moonPhase}\n                  starDensity={celestial.starDensity}\n                  celestialX={celestial.celestialX}\n                  celestialY={celestial.celestialY}\n                  sunSize={celestial.sunSize}\n                  moonSize={celestial.moonSize}\n                  sunGlowIntensity={celestial.sunGlowIntensity}\n                  sunGlowSize={celestial.sunGlowSize}\n                  sunRayCount={celestial.sunRayCount}\n                  sunRayLength={celestial.sunRayLength}\n                  sunRayIntensity={celestial.sunRayIntensity}\n                  moonGlowIntensity={celestial.moonGlowIntensity}\n                  moonGlowSize={celestial.moonGlowSize}\n                />\n              )}\n\n              {layers.clouds && (\n                <CloudCanvas\n                  className=\"absolute inset-0\"\n                  cloudScale={cloud.cloudScale}\n                  coverage={cloud.coverage}\n                  density={cloud.density}\n                  softness={cloud.softness}\n                  windSpeed={cloud.windSpeed}\n                  windAngle={cloud.windAngle}\n                  turbulence={cloud.turbulence}\n                  sunAltitude={\n                    timeOfDay < 0.5 ? timeOfDay * 2 : 2 - timeOfDay * 2\n                  }\n                  sunAzimuth={cloud.sunAzimuth}\n                  lightIntensity={cloud.lightIntensity}\n                  ambientDarkness={cloud.ambientDarkness}\n                  numLayers={cloud.numLayers}\n                  layerSpread={cloud.layerSpread}\n                  starDensity={0}\n                  starSize={cloud.starSize}\n                  starTwinkleSpeed={cloud.starTwinkleSpeed}\n                  starTwinkleAmount={cloud.starTwinkleAmount}\n                  horizonLine={cloud.horizonLine}\n                  transparentBackground={true}\n                />\n              )}\n\n              {layers.rain && (\n                <div\n                  className=\"absolute inset-0\"\n                  style={{ mixBlendMode: \"screen\" }}\n                >\n                  <RainCanvas\n                    className=\"absolute inset-0\"\n                    glassIntensity={rain.glassIntensity}\n                    zoom={rain.zoom}\n                    fallingIntensity={rain.fallingIntensity}\n                    fallingSpeed={rain.fallingSpeed}\n                    fallingAngle={rain.fallingAngle}\n                    fallingStreakLength={rain.fallingStreakLength}\n                    fallingLayers={rain.fallingLayers}\n                    fallingRefraction={rain.fallingRefraction}\n                    fallingWaviness={rain.fallingWaviness}\n                    fallingThicknessVar={rain.fallingThicknessVar}\n                  />\n                </div>\n              )}\n\n              {layers.lightning && (\n                <div\n                  className=\"absolute inset-0\"\n                  style={{ mixBlendMode: \"screen\" }}\n                >\n                  <LightningCanvas\n                    className=\"absolute inset-0\"\n                    branchDensity={lightning.branchDensity}\n                    displacement={lightning.displacement}\n                    glowIntensity={lightning.glowIntensity}\n                    flashDuration={lightning.flashDuration}\n                    sceneIllumination={lightning.sceneIllumination}\n                    afterglowPersistence={lightning.afterglowPersistence}\n                    autoMode={lightning.autoMode}\n                    autoInterval={lightning.autoInterval}\n                  />\n                </div>\n              )}\n\n              {layers.snow && (\n                <div\n                  className=\"absolute inset-0\"\n                  style={{ mixBlendMode: \"plus-lighter\" }}\n                >\n                  <SnowCanvas\n                    className=\"absolute inset-0\"\n                    intensity={snow.intensity}\n                    layers={snow.layers}\n                    fallSpeed={snow.fallSpeed}\n                    windSpeed={snow.windSpeed}\n                    windAngle={snow.windAngle}\n                    turbulence={snow.turbulence}\n                    drift={snow.drift}\n                    flutter={snow.flutter}\n                    windShear={snow.windShear}\n                    flakeSize={snow.flakeSize}\n                    sizeVariation={snow.sizeVariation}\n                    opacity={snow.opacity}\n                    glowAmount={snow.glowAmount}\n                    sparkle={snow.sparkle}\n                    visibility={snow.visibility}\n                  />\n                </div>\n              )}\n            </>\n          )}\n\n          <div className=\"pointer-events-none absolute bottom-4 left-4 rounded bg-black/50 px-2 py-1 text-sm text-white/80 backdrop-blur-sm\">\n            {formatTimeLabel(timeOfDay)}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-compositor/presets.ts",
    "content": "import type { WeatherConditionCode } from \"@/lib/weather-authoring/weather-widget/schema\";\nimport {\n  mapWeatherToEffects,\n  getTimeOfDay,\n  getNearestCheckpoint,\n} from \"@/lib/weather-authoring/weather-widget/effects\";\n\n// Weather conditions grouped by category for intuitive navigation\nexport interface ConditionGroup {\n  name: string;\n  conditions: WeatherConditionCode[];\n}\n\nexport const CONDITION_GROUPS: ConditionGroup[] = [\n  {\n    name: \"Sky\",\n    conditions: [\"clear\", \"partly-cloudy\", \"cloudy\", \"overcast\", \"fog\"],\n  },\n  {\n    name: \"Rain\",\n    conditions: [\"drizzle\", \"rain\", \"heavy-rain\", \"thunderstorm\"],\n  },\n  {\n    name: \"Winter\",\n    conditions: [\"snow\", \"sleet\", \"hail\"],\n  },\n  {\n    name: \"Wind\",\n    conditions: [\"windy\"],\n  },\n];\n\n// Flat list for iteration (derived from groups)\nexport const WEATHER_CONDITIONS: WeatherConditionCode[] =\n  CONDITION_GROUPS.flatMap((group) => group.conditions);\n\nexport const CONDITION_LABELS: Record<WeatherConditionCode, string> = {\n  clear: \"Clear\",\n  \"partly-cloudy\": \"Partly Cloudy\",\n  cloudy: \"Cloudy\",\n  overcast: \"Overcast\",\n  fog: \"Fog\",\n  drizzle: \"Drizzle\",\n  rain: \"Rain\",\n  \"heavy-rain\": \"Heavy Rain\",\n  thunderstorm: \"Thunderstorm\",\n  snow: \"Snow\",\n  sleet: \"Sleet\",\n  hail: \"Hail\",\n  windy: \"Windy\",\n};\n\nexport interface LayerToggles {\n  celestial: boolean;\n  clouds: boolean;\n  rain: boolean;\n  lightning: boolean;\n  snow: boolean;\n}\n\nexport interface CelestialParams {\n  timeOfDay: number;\n  moonPhase: number;\n  starDensity: number;\n  celestialX: number;\n  celestialY: number;\n  sunSize: number;\n  moonSize: number;\n  sunGlowIntensity: number;\n  sunGlowSize: number;\n  sunRayCount: number;\n  sunRayLength: number;\n  sunRayIntensity: number;\n  /**\n   * Scales subtle, noise-driven ray motion (shimmer + slow \"breathing\").\n   * 0 disables motion; 1 is the default subtlety; >1 increases visibility.\n   */\n  sunRayShimmer: number;\n  /**\n   * Global speed multiplier for the ray shimmer/breath noise inputs.\n   * 1 is the default speed; >1 speeds up motion.\n   */\n  sunRayShimmerSpeed: number;\n  moonGlowIntensity: number;\n  moonGlowSize: number;\n  skyBrightness: number;\n  skySaturation: number;\n  skyContrast: number;\n}\n\nexport interface CloudParams {\n  cloudScale: number;\n  coverage: number;\n  density: number;\n  softness: number;\n  windSpeed: number;\n  windAngle: number;\n  turbulence: number;\n  sunAzimuth: number;\n  lightIntensity: number;\n  ambientDarkness: number;\n  backlightIntensity: number;\n  numLayers: number;\n  layerSpread: number;\n  starSize: number;\n  starTwinkleSpeed: number;\n  starTwinkleAmount: number;\n  horizonLine: number;\n}\n\nexport interface RainParams {\n  glassIntensity: number;\n  zoom: number;\n  fallingIntensity: number;\n  fallingSpeed: number;\n  fallingAngle: number;\n  fallingStreakLength: number;\n  fallingLayers: number;\n  fallingRefraction: number;\n  fallingWaviness: number;\n  fallingThicknessVar: number;\n}\n\nexport interface LightningParams {\n  branchDensity: number;\n  displacement: number;\n  glowIntensity: number;\n  flashDuration: number;\n  sceneIllumination: number;\n  afterglowPersistence: number;\n  autoMode: boolean;\n  autoInterval: number;\n}\n\nexport interface SnowParams {\n  intensity: number;\n  layers: number;\n  fallSpeed: number;\n  windSpeed: number;\n  windAngle: number;\n  turbulence: number;\n  drift: number;\n  flutter: number;\n  windShear: number;\n  flakeSize: number;\n  sizeVariation: number;\n  opacity: number;\n  glowAmount: number;\n  sparkle: number;\n  visibility: number;\n}\n\nexport interface PostParams {\n  enabled: boolean;\n  haze: number;\n  hazeHorizon: number;\n  hazeDesaturation: number;\n  hazeContrast: number;\n  bloomIntensity: number;\n  bloomThreshold: number;\n  bloomKnee: number;\n  bloomRadius: number;\n  bloomTapScale: number;\n  exposureIntensity: number;\n  exposureDesaturation: number;\n  exposureRecovery: number;\n  godRayIntensity: number;\n  godRayDecay: number;\n  godRayDensity: number;\n  godRayWeight: number;\n  godRaySamples: number;\n}\n\nexport interface GlassParams {\n  enabled: boolean;\n  depth: number;\n  strength: number;\n  chromaticAberration: number;\n  blur: number;\n  brightness: number;\n  saturation: number;\n}\n\nexport interface ConditionOverrides {\n  layers?: Partial<LayerToggles>;\n  celestial?: Partial<CelestialParams>;\n  cloud?: Partial<CloudParams>;\n  rain?: Partial<RainParams>;\n  lightning?: Partial<LightningParams>;\n  snow?: Partial<SnowParams>;\n  glass?: Partial<GlassParams>;\n  post?: Partial<PostParams>;\n}\n\nexport interface GlobalSettings {\n  timeOfDay: number;\n}\n\nexport interface CheckpointOverrides {\n  dawn: ConditionOverrides;\n  noon: ConditionOverrides;\n  dusk: ConditionOverrides;\n  midnight: ConditionOverrides;\n}\n\nexport interface CompositorStateV4 {\n  version: 4;\n  activeCondition: WeatherConditionCode;\n  globalSettings: GlobalSettings;\n  checkpointOverrides: Partial<\n    Record<WeatherConditionCode, CheckpointOverrides>\n  >;\n}\n\nexport type CompositorState = CompositorStateV4;\n\nexport interface FullCompositorParams {\n  layers: LayerToggles;\n  celestial: CelestialParams;\n  cloud: CloudParams;\n  rain: RainParams;\n  lightning: LightningParams;\n  snow: SnowParams;\n  glass: GlassParams;\n  post: PostParams;\n}\n\nfunction buildBaseParamsForCondition(\n  condition: WeatherConditionCode,\n  timestamp?: string,\n): FullCompositorParams {\n  const effectConfig = mapWeatherToEffects({\n    conditionCode: condition,\n    timestamp,\n  });\n\n  const timeOfDay = timestamp ? getTimeOfDay(timestamp) : 0.5;\n\n  const hasCloud = effectConfig.cloud !== undefined;\n  const hasRain = effectConfig.rain !== undefined;\n  const hasLightning = effectConfig.lightning !== undefined;\n  const hasSnow = effectConfig.snow !== undefined;\n\n  const lightningIntervalMin = effectConfig.lightning?.intervalMin ?? 4;\n  const lightningIntervalMax = effectConfig.lightning?.intervalMax ?? 12;\n\n  return {\n    layers: {\n      celestial: true,\n      clouds: hasCloud,\n      rain: hasRain,\n      lightning: hasLightning,\n      snow: hasSnow,\n    },\n    celestial: {\n      timeOfDay,\n      moonPhase: effectConfig.celestial?.moonPhase ?? 0.5,\n      starDensity: effectConfig.celestial?.starDensity ?? 0.5,\n      celestialX: effectConfig.celestial?.celestialX ?? 0.5,\n      celestialY: effectConfig.celestial?.celestialY ?? 0.72,\n      sunSize: effectConfig.celestial?.sunSize ?? 0.06,\n      moonSize: effectConfig.celestial?.moonSize ?? 0.05,\n      sunGlowIntensity: effectConfig.celestial?.sunGlowIntensity ?? 1.0,\n      sunGlowSize: effectConfig.celestial?.sunGlowSize ?? 0.3,\n      sunRayCount: effectConfig.celestial?.sunRayCount ?? 12,\n      sunRayLength: effectConfig.celestial?.sunRayLength ?? 0.5,\n      sunRayIntensity: effectConfig.celestial?.sunRayIntensity ?? 0.4,\n      sunRayShimmer: 1.0,\n      sunRayShimmerSpeed: 1.0,\n      moonGlowIntensity: effectConfig.celestial?.moonGlowIntensity ?? 1.0,\n      moonGlowSize: effectConfig.celestial?.moonGlowSize ?? 0.2,\n      skyBrightness: 1.0,\n      skySaturation: 1.0,\n      skyContrast: 1.0,\n    },\n    cloud: {\n      cloudScale: 1.5,\n      coverage: effectConfig.cloud?.coverage ?? 0.5,\n      density: 0.7,\n      softness: 0.3,\n      windSpeed: effectConfig.cloud?.speed ?? 0.5,\n      windAngle: 0,\n      turbulence: effectConfig.cloud?.turbulence ?? 0.5,\n      sunAzimuth: 0,\n      lightIntensity: 1.0,\n      ambientDarkness: effectConfig.cloud?.darkness ?? 0.3,\n      backlightIntensity: 0.5,\n      numLayers: 3,\n      layerSpread: 0.3,\n      starSize: 1.0,\n      starTwinkleSpeed: 1.0,\n      starTwinkleAmount: 0.5,\n      horizonLine: 0.5,\n    },\n    rain: {\n      glassIntensity: hasRain ? (effectConfig.rain?.intensity ?? 0.5) * 0.7 : 0,\n      zoom: 1.0,\n      fallingIntensity: hasRain ? (effectConfig.rain?.intensity ?? 0.6) : 0,\n      fallingSpeed: 1.0,\n      fallingAngle: hasRain ? (effectConfig.rain?.angle ?? 5) * 0.02 : 0.1,\n      fallingStreakLength: 0.8,\n      fallingLayers: 3,\n      fallingRefraction: 0.3,\n      fallingWaviness: 0.15,\n      fallingThicknessVar: 0.5,\n    },\n    lightning: {\n      branchDensity: 0.6,\n      displacement: 0.08,\n      glowIntensity: 0.8,\n      flashDuration: 0.15,\n      sceneIllumination: 0.6,\n      afterglowPersistence: 0.3,\n      autoMode: hasLightning\n        ? (effectConfig.lightning?.autoTrigger ?? true)\n        : false,\n      autoInterval: hasLightning\n        ? (lightningIntervalMin + lightningIntervalMax) / 2\n        : 8,\n    },\n    snow: {\n      intensity: hasSnow ? (effectConfig.snow?.intensity ?? 0.7) : 0,\n      layers: 4,\n      fallSpeed: 0.5,\n      windSpeed: hasSnow ? (effectConfig.snow?.windDrift ?? 0.3) : 0.3,\n      windAngle: 0,\n      turbulence: 0.3,\n      drift: hasSnow ? (effectConfig.snow?.windDrift ?? 0.3) : 0.3,\n      flutter: 0.5,\n      windShear: 0.2,\n      flakeSize: 1.0,\n      sizeVariation: 0.5,\n      opacity: 0.8,\n      glowAmount: 0.3,\n      sparkle: 0.2,\n      visibility: 1.0,\n    },\n    glass: {\n      enabled: true,\n      depth: 3,\n      strength: 75,\n      chromaticAberration: 6,\n      blur: 1.5,\n      brightness: 0.8,\n      saturation: 1.3,\n    },\n    post: {\n      enabled: true,\n      haze: 0,\n      hazeHorizon: 0.5,\n      hazeDesaturation: 0.3,\n      hazeContrast: 0.2,\n      bloomIntensity: 0,\n      bloomThreshold: 0.8,\n      bloomKnee: 0.5,\n      bloomRadius: 8,\n      bloomTapScale: 1,\n      exposureIntensity: 0,\n      exposureDesaturation: 0.3,\n      exposureRecovery: 2,\n      godRayIntensity: 0,\n      godRayDecay: 0.96,\n      godRayDensity: 0.5,\n      godRayWeight: 0.3,\n      godRaySamples: 60,\n    },\n  };\n}\n\nexport function getRawBaseParamsForCondition(\n  condition: WeatherConditionCode,\n  timestamp?: string,\n): FullCompositorParams {\n  return buildBaseParamsForCondition(condition, timestamp);\n}\n\nexport function getBaseParamsForCondition(\n  condition: WeatherConditionCode,\n  timestamp?: string,\n): FullCompositorParams {\n  const timeOfDay = timestamp ? getTimeOfDay(timestamp) : 0.5;\n  const nearestCheckpoint = getNearestCheckpoint(timeOfDay);\n  const checkpointDefaults =\n    DEFAULT_CHECKPOINT_OVERRIDES[condition]?.[nearestCheckpoint];\n\n  const base = buildBaseParamsForCondition(condition, timestamp);\n\n  // Apply tuned defaults for this condition/checkpoint as part of the base\n  if (checkpointDefaults) {\n    return mergeWithOverrides(base, checkpointDefaults);\n  }\n  return base;\n}\n\nexport function mergeWithOverrides(\n  base: FullCompositorParams,\n  overrides?: ConditionOverrides,\n): FullCompositorParams {\n  if (!overrides) return base;\n\n  return {\n    layers: { ...base.layers, ...overrides.layers },\n    celestial: { ...base.celestial, ...overrides.celestial },\n    cloud: { ...base.cloud, ...overrides.cloud },\n    rain: { ...base.rain, ...overrides.rain },\n    lightning: { ...base.lightning, ...overrides.lightning },\n    snow: { ...base.snow, ...overrides.snow },\n    glass: { ...base.glass, ...overrides.glass },\n    post: { ...base.post, ...overrides.post },\n  };\n}\n\nexport function extractOverrides(\n  current: FullCompositorParams,\n  base: FullCompositorParams,\n): ConditionOverrides {\n  const overrides: ConditionOverrides = {};\n\n  const layerDiff = diffObjects(current.layers, base.layers);\n  if (Object.keys(layerDiff).length > 0) overrides.layers = layerDiff;\n\n  // Exclude timeOfDay from celestial comparison (it's a global setting)\n  const celestialDiff = diffObjects(current.celestial, base.celestial, [\n    \"timeOfDay\",\n  ]);\n  if (Object.keys(celestialDiff).length > 0)\n    overrides.celestial = celestialDiff;\n\n  const cloudDiff = diffObjects(current.cloud, base.cloud);\n  if (Object.keys(cloudDiff).length > 0) overrides.cloud = cloudDiff;\n\n  const rainDiff = diffObjects(current.rain, base.rain);\n  if (Object.keys(rainDiff).length > 0) overrides.rain = rainDiff;\n\n  const lightningDiff = diffObjects(current.lightning, base.lightning);\n  if (Object.keys(lightningDiff).length > 0)\n    overrides.lightning = lightningDiff;\n\n  const snowDiff = diffObjects(current.snow, base.snow);\n  if (Object.keys(snowDiff).length > 0) overrides.snow = snowDiff;\n\n  const glassDiff = diffObjects(current.glass, base.glass);\n  if (Object.keys(glassDiff).length > 0) overrides.glass = glassDiff;\n\n  const postDiff = diffObjects(current.post, base.post);\n  if (Object.keys(postDiff).length > 0) overrides.post = postDiff;\n\n  return overrides;\n}\n\nfunction diffObjects<T extends object>(\n  current: T,\n  base: T,\n  exclude: string[] = [],\n): Partial<T> {\n  const diff: Partial<T> = {};\n  for (const key of Object.keys(current) as (keyof T)[]) {\n    if (exclude.includes(key as string)) continue;\n    if (current[key] !== base[key]) {\n      diff[key] = current[key];\n    }\n  }\n  return diff;\n}\n\nconst STORAGE_KEY = \"weather-compositor-state\";\n\n// Tuned default overrides for each condition and checkpoint\nexport const DEFAULT_CHECKPOINT_OVERRIDES: Partial<\n  Record<WeatherConditionCode, CheckpointOverrides>\n> = {\n  clear: {\n    dawn: {\n      celestial: {\n        celestialY: 0.74,\n        sunGlowIntensity: 3.7,\n        sunGlowSize: 0.36,\n        moonGlowIntensity: 2.45,\n        moonGlowSize: 0.96,\n        skyBrightness: 1.04,\n        skySaturation: 1.31,\n        skyContrast: 0.61,\n      },\n    },\n    noon: {\n      celestial: {\n        celestialY: 0.74,\n        sunGlowIntensity: 2.68,\n        sunGlowSize: 0.37,\n        sunRayIntensity: 0.11,\n        moonGlowIntensity: 2.45,\n        moonGlowSize: 0.96,\n        skyBrightness: 0.91,\n        skySaturation: 1.53,\n      },\n    },\n    dusk: {\n      celestial: {\n        celestialY: 0.74,\n        sunGlowSize: 0.47,\n        sunRayIntensity: 0.04,\n        moonGlowIntensity: 2.45,\n        moonGlowSize: 0.96,\n        skyBrightness: 1.04,\n        skySaturation: 1.31,\n        skyContrast: 0.61,\n      },\n    },\n    midnight: {\n      celestial: {\n        celestialY: 0.74,\n        moonGlowIntensity: 2.45,\n        moonGlowSize: 0.96,\n        skyBrightness: 1.04,\n        skySaturation: 1.31,\n        skyContrast: 0.61,\n      },\n    },\n  },\n  \"partly-cloudy\": {\n    dawn: {\n      cloud: {\n        coverage: 0.43,\n        density: 0.32,\n        softness: 0.34,\n        lightIntensity: 1.2,\n        backlightIntensity: 0.45,\n      },\n    },\n    noon: {\n      cloud: {\n        coverage: 0.43,\n        density: 0.32,\n        softness: 0.34,\n        lightIntensity: 1.2,\n        backlightIntensity: 0.45,\n      },\n    },\n    dusk: {\n      cloud: {\n        coverage: 0.43,\n        density: 0.32,\n        softness: 0.34,\n        lightIntensity: 1.2,\n        backlightIntensity: 0.45,\n      },\n    },\n    midnight: {\n      cloud: {\n        coverage: 0.38,\n        density: 1.36,\n        softness: 0.34,\n        lightIntensity: 0.47,\n        backlightIntensity: 0.61,\n      },\n    },\n  },\n  cloudy: {\n    dawn: {\n      celestial: { skyBrightness: 0.91, skySaturation: 1.16 },\n      cloud: {\n        softness: 0.45,\n        windSpeed: 0.09,\n        lightIntensity: 0.81,\n        backlightIntensity: 0.39,\n      },\n    },\n    noon: {\n      celestial: { skyBrightness: 0.91, skySaturation: 1.16 },\n      cloud: {\n        softness: 0.45,\n        windSpeed: 0.09,\n        lightIntensity: 0.81,\n        backlightIntensity: 0.39,\n      },\n    },\n    dusk: {\n      celestial: { skyBrightness: 0.91, skySaturation: 1.16 },\n      cloud: {\n        coverage: 0.58,\n        softness: 0.29,\n        windSpeed: 0.09,\n        lightIntensity: 1.26,\n        backlightIntensity: 0.55,\n      },\n    },\n    midnight: {\n      cloud: {\n        coverage: 0.76,\n        density: 1.25,\n        softness: 0.4,\n        lightIntensity: 0.92,\n        ambientDarkness: 1,\n        backlightIntensity: 0.43,\n        numLayers: 1,\n      },\n    },\n  },\n  overcast: {\n    dawn: {\n      celestial: {\n        sunGlowIntensity: 2.08,\n        sunGlowSize: 0.22,\n        sunRayCount: 0,\n        sunRayLength: 0,\n        sunRayIntensity: 0,\n        skyBrightness: 1.05,\n      },\n      cloud: {\n        cloudScale: 0.98,\n        coverage: 1,\n        density: 0.87,\n        softness: 1,\n        windSpeed: 0.04,\n        lightIntensity: 1.1,\n        backlightIntensity: 0.53,\n        numLayers: 1,\n      },\n    },\n    noon: {\n      celestial: {\n        sunGlowIntensity: 1.73,\n        sunGlowSize: 0.48,\n        sunRayCount: 0,\n        sunRayLength: 0,\n        sunRayIntensity: 0,\n        skyBrightness: 0.68,\n        skySaturation: 0.84,\n      },\n      cloud: {\n        cloudScale: 0.98,\n        coverage: 1,\n        density: 0.87,\n        softness: 1,\n        windSpeed: 0.04,\n        lightIntensity: 1.1,\n        backlightIntensity: 0,\n        numLayers: 1,\n      },\n    },\n    dusk: {\n      celestial: {\n        sunGlowIntensity: 1.73,\n        sunGlowSize: 0.22,\n        sunRayCount: 0,\n        sunRayLength: 0,\n        sunRayIntensity: 0,\n        skyBrightness: 0.81,\n        skySaturation: 0.79,\n      },\n      cloud: {\n        cloudScale: 0.98,\n        coverage: 1,\n        density: 0.87,\n        softness: 1,\n        windSpeed: 0.04,\n        lightIntensity: 1.1,\n        backlightIntensity: 0,\n        numLayers: 1,\n      },\n    },\n    midnight: {\n      celestial: {\n        sunGlowIntensity: 1.73,\n        sunGlowSize: 0.22,\n        sunRayCount: 0,\n        sunRayLength: 0,\n        sunRayIntensity: 0,\n        skyBrightness: 0.64,\n        skySaturation: 1.46,\n      },\n      cloud: {\n        cloudScale: 0.98,\n        coverage: 1,\n        density: 0.97,\n        softness: 0.95,\n        windSpeed: 0.04,\n        lightIntensity: 1.1,\n        backlightIntensity: 0.22,\n        numLayers: 1,\n      },\n    },\n  },\n  fog: {\n    dawn: {},\n    noon: {},\n    dusk: {},\n    midnight: { celestial: { celestialY: 0.74 } },\n  },\n  rain: {\n    dawn: {},\n    noon: {},\n    dusk: {},\n    midnight: { cloud: { windSpeed: 0.19 } },\n  },\n  \"heavy-rain\": {\n    dawn: {\n      cloud: { coverage: 0.64, density: 1.2, windSpeed: 0.1, numLayers: 1 },\n    },\n    noon: {\n      celestial: {\n        sunGlowIntensity: 3.38,\n        skyBrightness: 0.88,\n        skySaturation: 0.97,\n      },\n      cloud: {\n        coverage: 0.64,\n        density: 1.27,\n        windSpeed: 0.1,\n        lightIntensity: 0.19,\n        ambientDarkness: 1,\n        backlightIntensity: 0.47,\n        numLayers: 2,\n      },\n      rain: {\n        glassIntensity: 0.88,\n        zoom: 1.18,\n        fallingSpeed: 3,\n        fallingStreakLength: 2,\n        fallingLayers: 6,\n      },\n    },\n    dusk: {\n      cloud: { coverage: 0.64, density: 1.2, windSpeed: 0.1, numLayers: 1 },\n    },\n    midnight: {\n      cloud: { coverage: 0.64, density: 1.2, windSpeed: 0.1, numLayers: 1 },\n    },\n  },\n  thunderstorm: {\n    dawn: {\n      lightning: {\n        branchDensity: 0.83,\n        glowIntensity: 0.85,\n        flashDuration: 0.44,\n        sceneIllumination: 0.77,\n      },\n    },\n    noon: {\n      lightning: {\n        branchDensity: 0.83,\n        glowIntensity: 0.85,\n        flashDuration: 0.44,\n        sceneIllumination: 0.77,\n      },\n    },\n    dusk: {\n      lightning: {\n        branchDensity: 0.83,\n        glowIntensity: 0.85,\n        flashDuration: 0.44,\n        sceneIllumination: 0.77,\n      },\n    },\n    midnight: {\n      cloud: {\n        windSpeed: 0.12,\n        turbulence: 0.63,\n        lightIntensity: 0.73,\n        ambientDarkness: 1,\n        backlightIntensity: 0.62,\n      },\n      lightning: {\n        branchDensity: 0.72,\n        glowIntensity: 1.72,\n        flashDuration: 0.5,\n        sceneIllumination: 0.19,\n        autoInterval: 7.5,\n      },\n    },\n  },\n  snow: {\n    dawn: {\n      cloud: { lightIntensity: 0.64 },\n      snow: { intensity: 0.12 },\n    },\n    noon: {\n      cloud: { lightIntensity: 0.64 },\n      snow: { intensity: 0.23 },\n    },\n    dusk: {\n      cloud: { lightIntensity: 0.64 },\n      snow: { intensity: 0.15 },\n    },\n    midnight: {\n      cloud: { lightIntensity: 0.64 },\n    },\n  },\n  sleet: {\n    dawn: {\n      rain: {\n        glassIntensity: 0.3,\n        zoom: 0.83,\n        fallingSpeed: 3,\n        fallingStreakLength: 0.42,\n      },\n      snow: {\n        intensity: 0.08,\n        layers: 6,\n        fallSpeed: 0.76,\n        drift: 0.28,\n        flakeSize: 1.87,\n      },\n    },\n    noon: {\n      rain: {\n        glassIntensity: 0.3,\n        zoom: 0.83,\n        fallingSpeed: 3,\n        fallingStreakLength: 0.42,\n      },\n      snow: {\n        intensity: 0.08,\n        layers: 6,\n        fallSpeed: 0.76,\n        drift: 0.28,\n        flakeSize: 1.87,\n      },\n    },\n    dusk: {\n      rain: {\n        glassIntensity: 0.3,\n        zoom: 0.83,\n        fallingSpeed: 3,\n        fallingStreakLength: 0.42,\n      },\n      snow: {\n        intensity: 0.08,\n        layers: 6,\n        fallSpeed: 0.76,\n        drift: 0.28,\n        flakeSize: 1.87,\n      },\n    },\n    midnight: {\n      celestial: { celestialY: 0.74 },\n      rain: {\n        glassIntensity: 0.3,\n        zoom: 0.83,\n        fallingSpeed: 3,\n        fallingStreakLength: 0.42,\n      },\n      snow: {\n        intensity: 0.08,\n        layers: 6,\n        fallSpeed: 0.76,\n        drift: 0.28,\n        flakeSize: 1.87,\n      },\n    },\n  },\n  hail: {\n    dawn: { cloud: { windSpeed: 0.16 } },\n    noon: { cloud: { windSpeed: 0.16 } },\n    dusk: { cloud: { windSpeed: 0.16 } },\n    midnight: { celestial: { celestialY: 0.74 }, cloud: { windSpeed: 0.16 } },\n  },\n  windy: {\n    dawn: {\n      celestial: { celestialY: 0.74 },\n      cloud: {\n        cloudScale: 1.84,\n        coverage: 0.49,\n        density: 0.67,\n        windSpeed: 0.26,\n        turbulence: 0.77,\n        lightIntensity: 0.63,\n        ambientDarkness: 0.37,\n        backlightIntensity: 0.39,\n      },\n    },\n    noon: {\n      celestial: { celestialY: 0.74 },\n      cloud: {\n        cloudScale: 1.84,\n        coverage: 0.49,\n        density: 0.67,\n        windSpeed: 0.26,\n        turbulence: 0.77,\n        lightIntensity: 0.63,\n        ambientDarkness: 0.37,\n        backlightIntensity: 0.39,\n      },\n    },\n    dusk: {\n      celestial: { celestialY: 0.74 },\n      cloud: {\n        cloudScale: 1.84,\n        coverage: 0.49,\n        density: 0.67,\n        windSpeed: 0.26,\n        turbulence: 0.77,\n        lightIntensity: 0.63,\n        ambientDarkness: 0.37,\n        backlightIntensity: 0.39,\n      },\n    },\n    midnight: {\n      celestial: { celestialY: 0.74 },\n      cloud: {\n        cloudScale: 1.84,\n        coverage: 0.49,\n        density: 0.67,\n        windSpeed: 0.26,\n        turbulence: 0.77,\n        lightIntensity: 0.63,\n        ambientDarkness: 0.37,\n        backlightIntensity: 0.39,\n      },\n    },\n  },\n};\n\nfunction createEmptyCheckpointOverrides(): CheckpointOverrides {\n  return {\n    dawn: {},\n    noon: {},\n    dusk: {},\n    midnight: {},\n  };\n}\n\nfunction isV4State(state: unknown): state is CompositorStateV4 {\n  if (!state || typeof state !== \"object\") return false;\n  const s = state as Record<string, unknown>;\n  return s.version === 4 && \"checkpointOverrides\" in s;\n}\n\nexport function loadFromStorage(): CompositorState | null {\n  if (typeof window === \"undefined\") return null;\n  try {\n    const stored = localStorage.getItem(STORAGE_KEY);\n    if (!stored) return null;\n\n    const parsed = JSON.parse(stored);\n\n    if (isV4State(parsed)) {\n      return parsed;\n    }\n\n    return null;\n  } catch {\n    return null;\n  }\n}\n\nexport function saveToStorage(state: CompositorState): void {\n  if (typeof window === \"undefined\") return;\n  try {\n    localStorage.setItem(STORAGE_KEY, JSON.stringify(state));\n  } catch {\n    console.warn(\"Failed to save compositor state to localStorage\");\n  }\n}\n\nexport function getCheckpointOverridesForCondition(\n  state: CompositorState,\n  condition: WeatherConditionCode,\n): CheckpointOverrides {\n  // User overrides only - defaults are now baked into getBaseParamsForCondition\n  return (\n    state.checkpointOverrides[condition] ?? createEmptyCheckpointOverrides()\n  );\n}\n\nexport function exportToFile(state: CompositorState): void {\n  const json = JSON.stringify(state, null, 2);\n  const blob = new Blob([json], { type: \"application/json\" });\n  const url = URL.createObjectURL(blob);\n  const a = document.createElement(\"a\");\n  a.href = url;\n  a.download = \"weather-compositor-presets.json\";\n  a.click();\n  URL.revokeObjectURL(url);\n}\n\nexport function importFromFile(file: File): Promise<CompositorState> {\n  return new Promise((resolve, reject) => {\n    const reader = new FileReader();\n    reader.onload = (e) => {\n      try {\n        const parsed = JSON.parse(e.target?.result as string);\n\n        if (isV4State(parsed)) {\n          resolve(parsed);\n          return;\n        }\n\n        reject(new Error(\"Invalid file format\"));\n      } catch {\n        reject(new Error(\"Invalid JSON file\"));\n      }\n    };\n    reader.onerror = () => reject(new Error(\"Failed to read file\"));\n    reader.readAsText(file);\n  });\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-compositor/tuned-presets.json",
    "content": "{\n  \"activeCondition\": \"clear\",\n  \"globalSettings\": {\n    \"timeOfDay\": 0.5\n  },\n  \"overrides\": {\n    \"clear\": {\n      \"layers\": {\n        \"clouds\": true\n      },\n      \"cloud\": {\n        \"cloudScale\": 2.2,\n        \"coverage\": 0.08,\n        \"density\": 0.35,\n        \"softness\": 0.85,\n        \"windSpeed\": 0.15,\n        \"windAngle\": 0.1,\n        \"turbulence\": 0.15,\n        \"ambientDarkness\": 0.0,\n        \"lightIntensity\": 1.15,\n        \"numLayers\": 2,\n        \"layerSpread\": 0.6,\n        \"starTwinkleAmount\": 0.75,\n        \"starTwinkleSpeed\": 0.8,\n        \"horizonLine\": 0.3\n      },\n      \"celestial\": {\n        \"celestialX\": 0.7,\n        \"celestialY\": 0.8,\n        \"sunSize\": 0.052,\n        \"moonSize\": 0.044,\n        \"starDensity\": 0.85\n      }\n    },\n\n    \"partly-cloudy\": {\n      \"cloud\": {\n        \"cloudScale\": 1.6,\n        \"coverage\": 0.38,\n        \"density\": 0.62,\n        \"softness\": 0.48,\n        \"windSpeed\": 0.32,\n        \"windAngle\": 0.15,\n        \"turbulence\": 0.35,\n        \"ambientDarkness\": 0.12,\n        \"lightIntensity\": 1.0,\n        \"numLayers\": 4,\n        \"layerSpread\": 0.42,\n        \"horizonLine\": 0.42\n      },\n      \"celestial\": {\n        \"celestialX\": 0.65,\n        \"celestialY\": 0.74,\n        \"sunSize\": 0.058,\n        \"moonSize\": 0.048,\n        \"starDensity\": 0.45\n      }\n    },\n\n    \"cloudy\": {\n      \"cloud\": {\n        \"cloudScale\": 1.35,\n        \"coverage\": 0.68,\n        \"density\": 0.72,\n        \"softness\": 0.42,\n        \"windSpeed\": 0.28,\n        \"windAngle\": 0.08,\n        \"turbulence\": 0.3,\n        \"ambientDarkness\": 0.22,\n        \"lightIntensity\": 0.88,\n        \"numLayers\": 4,\n        \"layerSpread\": 0.35,\n        \"horizonLine\": 0.48\n      },\n      \"celestial\": {\n        \"celestialX\": 0.62,\n        \"celestialY\": 0.68,\n        \"sunSize\": 0.072,\n        \"moonSize\": 0.06,\n        \"starDensity\": 0.15\n      }\n    },\n\n    \"overcast\": {\n      \"cloud\": {\n        \"cloudScale\": 1.15,\n        \"coverage\": 0.96,\n        \"density\": 0.92,\n        \"softness\": 0.28,\n        \"windSpeed\": 0.18,\n        \"windAngle\": 0.05,\n        \"turbulence\": 0.18,\n        \"ambientDarkness\": 0.48,\n        \"lightIntensity\": 0.62,\n        \"numLayers\": 5,\n        \"layerSpread\": 0.22,\n        \"horizonLine\": 0.58\n      },\n      \"celestial\": {\n        \"celestialX\": 0.58,\n        \"celestialY\": 0.65,\n        \"sunSize\": 0.085,\n        \"moonSize\": 0.07,\n        \"starDensity\": 0.0\n      }\n    },\n\n    \"fog\": {\n      \"cloud\": {\n        \"cloudScale\": 0.75,\n        \"coverage\": 0.88,\n        \"density\": 0.98,\n        \"softness\": 0.95,\n        \"windSpeed\": 0.08,\n        \"windAngle\": 0.02,\n        \"turbulence\": 0.05,\n        \"ambientDarkness\": 0.18,\n        \"lightIntensity\": 0.72,\n        \"numLayers\": 5,\n        \"layerSpread\": 0.12,\n        \"horizonLine\": 0.25\n      },\n      \"celestial\": {\n        \"celestialX\": 0.65,\n        \"celestialY\": 0.85,\n        \"sunSize\": 0.12,\n        \"moonSize\": 0.095,\n        \"starDensity\": 0.0\n      }\n    },\n\n    \"drizzle\": {\n      \"cloud\": {\n        \"cloudScale\": 1.25,\n        \"coverage\": 0.72,\n        \"density\": 0.68,\n        \"softness\": 0.55,\n        \"windSpeed\": 0.25,\n        \"windAngle\": 0.12,\n        \"turbulence\": 0.28,\n        \"ambientDarkness\": 0.32,\n        \"lightIntensity\": 0.78,\n        \"numLayers\": 4,\n        \"layerSpread\": 0.32,\n        \"horizonLine\": 0.46\n      },\n      \"rain\": {\n        \"glassIntensity\": 0.28,\n        \"zoom\": 1.15,\n        \"fallingIntensity\": 0.22,\n        \"fallingSpeed\": 0.65,\n        \"fallingAngle\": 0.05,\n        \"fallingStreakLength\": 0.45,\n        \"fallingLayers\": 2,\n        \"fallingRefraction\": 0.22,\n        \"fallingWaviness\": 0.12,\n        \"fallingThicknessVar\": 0.35\n      },\n      \"celestial\": {\n        \"celestialX\": 0.62,\n        \"celestialY\": 0.7,\n        \"sunSize\": 0.068,\n        \"moonSize\": 0.055,\n        \"starDensity\": 0.08\n      }\n    },\n\n    \"rain\": {\n      \"cloud\": {\n        \"cloudScale\": 1.18,\n        \"coverage\": 0.82,\n        \"density\": 0.78,\n        \"softness\": 0.38,\n        \"windSpeed\": 0.45,\n        \"windAngle\": 0.18,\n        \"turbulence\": 0.42,\n        \"ambientDarkness\": 0.42,\n        \"lightIntensity\": 0.72,\n        \"numLayers\": 4,\n        \"layerSpread\": 0.28,\n        \"horizonLine\": 0.52\n      },\n      \"rain\": {\n        \"glassIntensity\": 0.52,\n        \"zoom\": 1.0,\n        \"fallingIntensity\": 0.58,\n        \"fallingSpeed\": 1.15,\n        \"fallingAngle\": 0.1,\n        \"fallingStreakLength\": 0.72,\n        \"fallingLayers\": 3,\n        \"fallingRefraction\": 0.38,\n        \"fallingWaviness\": 0.1,\n        \"fallingThicknessVar\": 0.48\n      },\n      \"celestial\": {\n        \"celestialX\": 0.6,\n        \"celestialY\": 0.66,\n        \"sunSize\": 0.065,\n        \"moonSize\": 0.052,\n        \"starDensity\": 0.0\n      }\n    },\n\n    \"heavy-rain\": {\n      \"cloud\": {\n        \"cloudScale\": 0.95,\n        \"coverage\": 0.98,\n        \"density\": 0.95,\n        \"softness\": 0.22,\n        \"windSpeed\": 0.68,\n        \"windAngle\": 0.25,\n        \"turbulence\": 0.58,\n        \"ambientDarkness\": 0.62,\n        \"lightIntensity\": 0.48,\n        \"numLayers\": 5,\n        \"layerSpread\": 0.18,\n        \"horizonLine\": 0.58\n      },\n      \"rain\": {\n        \"glassIntensity\": 0.88,\n        \"zoom\": 0.88,\n        \"fallingIntensity\": 0.92,\n        \"fallingSpeed\": 1.65,\n        \"fallingAngle\": 0.18,\n        \"fallingStreakLength\": 1.05,\n        \"fallingLayers\": 4,\n        \"fallingRefraction\": 0.55,\n        \"fallingWaviness\": 0.18,\n        \"fallingThicknessVar\": 0.62\n      },\n      \"celestial\": {\n        \"celestialX\": 0.55,\n        \"celestialY\": 0.62,\n        \"sunSize\": 0.06,\n        \"moonSize\": 0.048,\n        \"starDensity\": 0.0\n      }\n    },\n\n    \"thunderstorm\": {\n      \"cloud\": {\n        \"cloudScale\": 0.85,\n        \"coverage\": 1.0,\n        \"density\": 1.0,\n        \"softness\": 0.15,\n        \"windSpeed\": 0.85,\n        \"windAngle\": 0.32,\n        \"turbulence\": 0.82,\n        \"ambientDarkness\": 0.78,\n        \"lightIntensity\": 0.32,\n        \"numLayers\": 5,\n        \"layerSpread\": 0.12,\n        \"horizonLine\": 0.62\n      },\n      \"rain\": {\n        \"glassIntensity\": 0.92,\n        \"zoom\": 0.82,\n        \"fallingIntensity\": 1.0,\n        \"fallingSpeed\": 1.85,\n        \"fallingAngle\": 0.28,\n        \"fallingStreakLength\": 1.15,\n        \"fallingLayers\": 5,\n        \"fallingRefraction\": 0.65,\n        \"fallingWaviness\": 0.22,\n        \"fallingThicknessVar\": 0.72\n      },\n      \"lightning\": {\n        \"branchDensity\": 0.72,\n        \"displacement\": 0.095,\n        \"glowIntensity\": 1.2,\n        \"flashDuration\": 0.16,\n        \"sceneIllumination\": 0.78,\n        \"afterglowPersistence\": 0.42,\n        \"autoInterval\": 5.5\n      },\n      \"celestial\": {\n        \"celestialX\": 0.52,\n        \"celestialY\": 0.58,\n        \"sunSize\": 0.055,\n        \"moonSize\": 0.045,\n        \"starDensity\": 0.0\n      }\n    },\n\n    \"snow\": {\n      \"cloud\": {\n        \"cloudScale\": 1.55,\n        \"coverage\": 0.58,\n        \"density\": 0.62,\n        \"softness\": 0.62,\n        \"windSpeed\": 0.18,\n        \"windAngle\": 0.12,\n        \"turbulence\": 0.12,\n        \"ambientDarkness\": 0.12,\n        \"lightIntensity\": 1.08,\n        \"numLayers\": 3,\n        \"layerSpread\": 0.45,\n        \"horizonLine\": 0.42\n      },\n      \"snow\": {\n        \"intensity\": 0.58,\n        \"layers\": 5,\n        \"fallSpeed\": 0.38,\n        \"windSpeed\": 0.22,\n        \"windAngle\": 0.18,\n        \"turbulence\": 0.32,\n        \"drift\": 0.42,\n        \"flutter\": 0.65,\n        \"windShear\": 0.12,\n        \"flakeSize\": 1.08,\n        \"sizeVariation\": 0.58,\n        \"opacity\": 0.88,\n        \"glowAmount\": 0.45,\n        \"sparkle\": 0.38,\n        \"visibility\": 0.92\n      },\n      \"celestial\": {\n        \"celestialX\": 0.68,\n        \"celestialY\": 0.76,\n        \"sunSize\": 0.062,\n        \"moonSize\": 0.052,\n        \"starDensity\": 0.35\n      }\n    },\n\n    \"sleet\": {\n      \"cloud\": {\n        \"cloudScale\": 1.22,\n        \"coverage\": 0.78,\n        \"density\": 0.75,\n        \"softness\": 0.38,\n        \"windSpeed\": 0.48,\n        \"windAngle\": 0.22,\n        \"turbulence\": 0.42,\n        \"ambientDarkness\": 0.38,\n        \"lightIntensity\": 0.72,\n        \"numLayers\": 4,\n        \"layerSpread\": 0.28,\n        \"horizonLine\": 0.5\n      },\n      \"rain\": {\n        \"glassIntensity\": 0.42,\n        \"zoom\": 0.95,\n        \"fallingIntensity\": 0.48,\n        \"fallingSpeed\": 1.35,\n        \"fallingAngle\": 0.2,\n        \"fallingStreakLength\": 0.55,\n        \"fallingLayers\": 3,\n        \"fallingRefraction\": 0.32,\n        \"fallingWaviness\": 0.06,\n        \"fallingThicknessVar\": 0.42\n      },\n      \"snow\": {\n        \"intensity\": 0.32,\n        \"layers\": 3,\n        \"fallSpeed\": 0.55,\n        \"windSpeed\": 0.42,\n        \"windAngle\": 0.28,\n        \"turbulence\": 0.22,\n        \"drift\": 0.48,\n        \"flutter\": 0.32,\n        \"windShear\": 0.22,\n        \"flakeSize\": 0.82,\n        \"sizeVariation\": 0.48,\n        \"opacity\": 0.72,\n        \"glowAmount\": 0.18,\n        \"sparkle\": 0.12,\n        \"visibility\": 0.88\n      },\n      \"celestial\": {\n        \"celestialX\": 0.58,\n        \"celestialY\": 0.65,\n        \"sunSize\": 0.058,\n        \"moonSize\": 0.048,\n        \"starDensity\": 0.0\n      }\n    },\n\n    \"hail\": {\n      \"cloud\": {\n        \"cloudScale\": 1.05,\n        \"coverage\": 0.92,\n        \"density\": 0.88,\n        \"softness\": 0.22,\n        \"windSpeed\": 0.62,\n        \"windAngle\": 0.18,\n        \"turbulence\": 0.52,\n        \"ambientDarkness\": 0.55,\n        \"lightIntensity\": 0.55,\n        \"numLayers\": 5,\n        \"layerSpread\": 0.18,\n        \"horizonLine\": 0.55\n      },\n      \"rain\": {\n        \"glassIntensity\": 0.78,\n        \"zoom\": 0.92,\n        \"fallingIntensity\": 0.82,\n        \"fallingSpeed\": 2.2,\n        \"fallingAngle\": 0.12,\n        \"fallingStreakLength\": 0.35,\n        \"fallingLayers\": 4,\n        \"fallingRefraction\": 0.48,\n        \"fallingWaviness\": 0.04,\n        \"fallingThicknessVar\": 0.85\n      },\n      \"celestial\": {\n        \"celestialX\": 0.55,\n        \"celestialY\": 0.62,\n        \"sunSize\": 0.058,\n        \"moonSize\": 0.048,\n        \"starDensity\": 0.0\n      }\n    },\n\n    \"windy\": {\n      \"cloud\": {\n        \"cloudScale\": 1.35,\n        \"coverage\": 0.42,\n        \"density\": 0.52,\n        \"softness\": 0.42,\n        \"windSpeed\": 1.65,\n        \"windAngle\": 0.35,\n        \"turbulence\": 0.95,\n        \"ambientDarkness\": 0.12,\n        \"lightIntensity\": 1.0,\n        \"numLayers\": 4,\n        \"layerSpread\": 0.55,\n        \"horizonLine\": 0.38\n      },\n      \"celestial\": {\n        \"celestialX\": 0.78,\n        \"celestialY\": 0.74,\n        \"sunSize\": 0.055,\n        \"moonSize\": 0.046,\n        \"starDensity\": 0.42\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-effects/page.tsx",
    "content": "\"use client\";\n\nimport { useControls, button } from \"leva\";\nimport {\n  WeatherEffectsCanvas,\n  type CelestialParams,\n  type CloudParams,\n  type RainParams,\n  type LightningParams,\n  type SnowParams,\n  type InteractionParams,\n  type LayerToggles,\n} from \"@/lib/weather-authoring/weather-widget/effects/weather-effects-canvas\";\n\nexport default function WeatherEffectsSandbox() {\n  const layers = useControls(\"Layers\", {\n    celestial: true,\n    clouds: true,\n    rain: false,\n    lightning: false,\n    snow: false,\n  }) as LayerToggles;\n\n  const celestial = useControls(\"Celestial\", {\n    timeOfDay: { value: 0.5, min: 0, max: 1, step: 0.01 },\n    moonPhase: { value: 0.54, min: 0, max: 1, step: 0.01 },\n    starDensity: { value: 2.0, min: 0, max: 5, step: 0.1 },\n    celestialX: { value: 0.74, min: 0, max: 1, step: 0.01 },\n    celestialY: { value: 0.78, min: 0, max: 1, step: 0.01 },\n    sunSize: { value: 0.14, min: 0.01, max: 0.5, step: 0.01 },\n    moonSize: { value: 0.17, min: 0.01, max: 0.5, step: 0.01 },\n    sunGlowIntensity: { value: 3.05, min: 0, max: 10, step: 0.05 },\n    sunGlowSize: { value: 0.3, min: 0, max: 2, step: 0.01 },\n    sunRayCount: { value: 6, min: 0, max: 24, step: 1 },\n    sunRayLength: { value: 3.0, min: 0, max: 10, step: 0.1 },\n    sunRayIntensity: { value: 0.1, min: 0, max: 1, step: 0.01 },\n    sunRayShimmer: {\n      value: 1.0,\n      min: 0,\n      max: 5,\n      step: 0.05,\n      label: \"Ray Shimmer\",\n    },\n    sunRayShimmerSpeed: {\n      value: 1.0,\n      min: 0,\n      max: 5,\n      step: 0.05,\n      label: \"Ray Shimmer Speed\",\n    },\n    moonGlowIntensity: { value: 3.45, min: 0, max: 10, step: 0.05 },\n    moonGlowSize: { value: 0.94, min: 0, max: 2, step: 0.01 },\n  }) as CelestialParams;\n\n  const cloud = useControls(\"Clouds\", {\n    coverage: { value: 0.5, min: 0, max: 1, step: 0.01 },\n    density: { value: 0.7, min: 0, max: 1, step: 0.01 },\n    softness: { value: 0.5, min: 0, max: 1, step: 0.01 },\n    cloudScale: { value: 1.0, min: 0.1, max: 5, step: 0.1 },\n    windSpeed: { value: 0.3, min: 0, max: 2, step: 0.01 },\n    windAngle: { value: 0, min: -Math.PI, max: Math.PI, step: 0.01 },\n    turbulence: { value: 0.3, min: 0, max: 1, step: 0.01 },\n    lightIntensity: { value: 1.0, min: 0, max: 2, step: 0.01 },\n    ambientDarkness: { value: 0.2, min: 0, max: 1, step: 0.01 },\n    backlightIntensity: { value: 0.5, min: 0, max: 2, step: 0.01 },\n    numLayers: { value: 3, min: 1, max: 5, step: 1 },\n  }) as CloudParams;\n\n  const rain = useControls(\"Rain\", {\n    glassIntensity: { value: 0.5, min: 0, max: 1, step: 0.01 },\n    glassZoom: { value: 1.0, min: 0.5, max: 3, step: 0.01 },\n    fallingIntensity: { value: 0.6, min: 0, max: 1, step: 0.01 },\n    fallingSpeed: { value: 1.0, min: 0.1, max: 3, step: 0.01 },\n    fallingAngle: { value: 0.1, min: -0.5, max: 0.5, step: 0.01 },\n    fallingStreakLength: { value: 1.0, min: 0.1, max: 3, step: 0.01 },\n    fallingLayers: { value: 4, min: 1, max: 6, step: 1 },\n  }) as RainParams;\n\n  const lightning = useControls(\"Lightning\", {\n    enabled: true,\n    autoMode: true,\n    autoInterval: { value: 8, min: 2, max: 20, step: 0.5 },\n    flashIntensity: { value: 1.0, min: 0, max: 2, step: 0.01 },\n    branchDensity: { value: 0.5, min: 0, max: 1, step: 0.01 },\n  }) as LightningParams;\n\n  const snow = useControls(\"Snow\", {\n    intensity: { value: 0.5, min: 0, max: 1, step: 0.01 },\n    layers: { value: 4, min: 1, max: 8, step: 1 },\n    fallSpeed: { value: 1.0, min: 0.1, max: 3, step: 0.01 },\n    windSpeed: { value: 0.3, min: 0, max: 1, step: 0.01 },\n    drift: { value: 0.3, min: 0, max: 1, step: 0.01 },\n    flakeSize: { value: 1.0, min: 0.1, max: 3, step: 0.01 },\n  }) as SnowParams;\n\n  const interactions = useControls(\"Interactions\", {\n    rainRefractionStrength: { value: 1.0, min: 0, max: 3, step: 0.01 },\n    lightningSceneIllumination: { value: 0.6, min: 0, max: 2, step: 0.01 },\n  }) as InteractionParams;\n\n  useControls(\"Presets\", {\n    \"Clear Day\": button(() => {\n      // Would need setters for this - just a placeholder\n      console.log(\"Preset: Clear Day\");\n    }),\n    \"Rainy Night\": button(() => {\n      console.log(\"Preset: Rainy Night\");\n    }),\n    Thunderstorm: button(() => {\n      console.log(\"Preset: Thunderstorm\");\n    }),\n    Snowy: button(() => {\n      console.log(\"Preset: Snowy\");\n    }),\n  });\n\n  return (\n    <div className=\"h-screen w-screen bg-black\">\n      <WeatherEffectsCanvas\n        className=\"h-full w-full\"\n        layers={layers}\n        celestial={celestial}\n        cloud={cloud}\n        rain={rain}\n        lightning={lightning}\n        snow={snow}\n        interactions={interactions}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/components/checkpoint-dots.tsx",
    "content": "\"use client\";\n\nimport { cn } from \"@/lib/ui/cn\";\nimport type { ConditionCheckpoints } from \"../types\";\nimport { TIME_CHECKPOINT_ORDER } from \"../lib/constants\";\n\ninterface CheckpointDotsProps {\n  checkpoints?: ConditionCheckpoints;\n  className?: string;\n  size?: \"sm\" | \"md\";\n}\n\nconst DEFAULT_CHECKPOINTS: ConditionCheckpoints = {\n  dawn: \"pending\",\n  noon: \"pending\",\n  dusk: \"pending\",\n  midnight: \"pending\",\n};\n\nexport function CheckpointDots({\n  checkpoints = DEFAULT_CHECKPOINTS,\n  className,\n  size = \"md\",\n}: CheckpointDotsProps) {\n  return (\n    <div className={cn(\"flex gap-1\", className)}>\n      {TIME_CHECKPOINT_ORDER.map((checkpoint) => {\n        const status = checkpoints[checkpoint] ?? \"pending\";\n        return (\n          <div\n            key={checkpoint}\n            className={cn(\n              \"rounded-full transition-colors\",\n              size === \"sm\" ? \"size-1\" : \"size-1.5\",\n              status === \"reviewed\"\n                ? \"bg-green-500 dark:bg-green-400\"\n                : \"bg-muted-foreground/30\",\n            )}\n            title={`${checkpoint}: ${status}`}\n          />\n        );\n      })}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/components/condition-sidebar.tsx",
    "content": "\"use client\";\n\nimport { cn } from \"@/lib/ui/cn\";\nimport {\n  Check,\n  Cloud,\n  CloudRain,\n  CloudSnow,\n  Sun,\n  Wind,\n  Zap,\n  CloudFog,\n  CloudHail,\n  Cloudy,\n  CloudDrizzle,\n  Snowflake,\n} from \"lucide-react\";\nimport type { WeatherConditionCode } from \"@/lib/weather-authoring/weather-widget/schema\";\nimport {\n  CONDITION_GROUPS,\n  CONDITION_LABELS,\n} from \"../../weather-compositor/presets\";\nimport { CheckpointDots } from \"./checkpoint-dots\";\nimport type { ConditionCheckpoints } from \"../types\";\n\ninterface ConditionSidebarProps {\n  selectedCondition: WeatherConditionCode | null;\n  signedOff: Set<WeatherConditionCode>;\n  checkpoints: Partial<Record<WeatherConditionCode, ConditionCheckpoints>>;\n  getOverrideCount: (condition: WeatherConditionCode) => number;\n  onSelectCondition: (condition: WeatherConditionCode) => void;\n}\n\nconst CONDITION_ICONS: Record<WeatherConditionCode, typeof Sun> = {\n  clear: Sun,\n  \"partly-cloudy\": Cloudy,\n  cloudy: Cloud,\n  overcast: Cloud,\n  fog: CloudFog,\n  drizzle: CloudDrizzle,\n  rain: CloudRain,\n  \"heavy-rain\": CloudRain,\n  thunderstorm: Zap,\n  snow: CloudSnow,\n  sleet: Snowflake,\n  hail: CloudHail,\n  windy: Wind,\n};\n\nconst CONDITION_COLORS: Record<WeatherConditionCode, string> = {\n  clear: \"from-amber-500 to-orange-500\",\n  \"partly-cloudy\": \"from-sky-400 to-slate-400\",\n  cloudy: \"from-slate-400 to-slate-500\",\n  overcast: \"from-slate-500 to-slate-600\",\n  fog: \"from-slate-400 to-slate-500\",\n  drizzle: \"from-blue-400 to-cyan-400\",\n  rain: \"from-blue-500 to-cyan-500\",\n  \"heavy-rain\": \"from-blue-600 to-indigo-600\",\n  thunderstorm: \"from-purple-500 to-indigo-600\",\n  snow: \"from-slate-200 to-blue-200\",\n  sleet: \"from-cyan-300 to-slate-400\",\n  hail: \"from-cyan-400 to-slate-400\",\n  windy: \"from-teal-400 to-cyan-500\",\n};\n\nexport function ConditionSidebar({\n  selectedCondition,\n  signedOff,\n  checkpoints,\n  getOverrideCount,\n  onSelectCondition,\n}: ConditionSidebarProps) {\n  return (\n    <aside className=\"flex w-44 shrink-0 flex-col border-r border-border/40\">\n      <div className=\"border-b border-border/40 px-3 py-2\">\n        <h2 className=\"text-[10px] font-medium uppercase tracking-wider text-muted-foreground/50\">\n          Conditions\n        </h2>\n      </div>\n\n      <div className=\"scrollbar-subtle flex-1 overflow-y-auto\">\n        <nav className=\"flex flex-col\">\n          {CONDITION_GROUPS.map((group) => (\n            <div key={group.name}>\n              <div className=\"sticky top-0 z-[5] border-b border-border/30 bg-background/95 px-3 py-1.5 backdrop-blur-sm\">\n                <span className=\"text-[9px] font-medium uppercase tracking-wider text-muted-foreground/40\">\n                  {group.name}\n                </span>\n              </div>\n              <div className=\"flex flex-col gap-px px-1.5 py-1\">\n                {group.conditions.map((condition) => {\n                  const Icon = CONDITION_ICONS[condition];\n                  const label = CONDITION_LABELS[condition];\n                  const isSelected = selectedCondition === condition;\n                  const isSignedOff = signedOff.has(condition);\n                  const overrideCount = getOverrideCount(condition);\n                  const colorGradient = CONDITION_COLORS[condition];\n\n                  return (\n                    <button\n                      key={condition}\n                      onClick={() => onSelectCondition(condition)}\n                      className={cn(\n                        \"group relative flex items-center gap-2.5 rounded-md px-2 py-1.5 text-left transition-all\",\n                        isSelected ? \"bg-accent/80\" : \"hover:bg-accent/40\",\n                      )}\n                    >\n                      {isSelected && (\n                        <div className=\"absolute inset-y-1 left-0 w-0.5 rounded-full bg-foreground/40\" />\n                      )}\n\n                      <div\n                        className={cn(\n                          \"flex size-6 shrink-0 items-center justify-center rounded transition-all\",\n                          isSelected\n                            ? `bg-gradient-to-br ${colorGradient}`\n                            : \"bg-muted/50\",\n                        )}\n                      >\n                        <Icon\n                          className={cn(\n                            \"size-3.5 transition-colors\",\n                            isSelected\n                              ? \"text-white\"\n                              : \"text-muted-foreground/60\",\n                          )}\n                        />\n                      </div>\n\n                      <div className=\"min-w-0 flex-1\">\n                        <div className=\"flex items-center gap-1\">\n                          <span\n                            className={cn(\n                              \"truncate text-xs transition-colors\",\n                              isSelected\n                                ? \"text-foreground\"\n                                : \"text-muted-foreground\",\n                            )}\n                          >\n                            {label}\n                          </span>\n                          {isSignedOff && (\n                            <Check className=\"size-3 text-green-600/70 dark:text-green-400/70\" />\n                          )}\n                          {overrideCount > 0 && (\n                            <span className=\"rounded bg-amber-500/10 px-1 py-0.5 font-mono text-[9px] text-amber-600/70 dark:text-amber-400/70\">\n                              {overrideCount}\n                            </span>\n                          )}\n                        </div>\n                        <div className=\"mt-0.5\">\n                          <CheckpointDots\n                            checkpoints={checkpoints[condition]}\n                            size=\"sm\"\n                          />\n                        </div>\n                      </div>\n                    </button>\n                  );\n                })}\n              </div>\n            </div>\n          ))}\n        </nav>\n      </div>\n\n      <div className=\"border-t border-border/40 px-3 py-2\">\n        <div className=\"flex items-center gap-1.5 text-[10px] text-muted-foreground/40\">\n          <kbd className=\"rounded border border-border/50 bg-muted/30 px-1 py-0.5 font-mono\">\n            ↑↓\n          </kbd>\n          <span>navigate</span>\n        </div>\n      </div>\n    </aside>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/components/detail-editor.tsx",
    "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { RotateCcw, Eye, EyeOff, CheckCircle2, Copy } from \"lucide-react\";\nimport { cn } from \"@/lib/ui/cn\";\nimport type { WeatherConditionCode } from \"@/lib/weather-authoring/weather-widget/schema\";\nimport {\n  WeatherEffectsCanvas,\n  type WeatherEffectsCanvasProps,\n} from \"@/lib/weather-authoring/weather-widget/effects\";\nimport {\n  WeatherDataOverlay,\n  createWeatherOverlayStubData,\n} from \"./weather-data-overlay\";\nimport { GlassControls } from \"./glass-controls\";\nimport { CONDITION_LABELS } from \"../../weather-compositor/presets\";\nimport type {\n  FullCompositorParams,\n  GlassParams,\n} from \"../../weather-compositor/presets\";\nimport { ParameterPanel } from \"./parameter-panel\";\nimport { TIME_CHECKPOINT_ORDER, TIME_CHECKPOINTS } from \"../lib/constants\";\nimport type { ConditionCheckpoints, TimeCheckpoint } from \"../types\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\n\ntype LayerKey =\n  | \"layers\"\n  | \"celestial\"\n  | \"cloud\"\n  | \"rain\"\n  | \"lightning\"\n  | \"snow\"\n  | \"glass\"\n  | \"post\";\n\ninterface DetailEditorProps {\n  condition: WeatherConditionCode;\n  params: FullCompositorParams;\n  canvasProps: WeatherEffectsCanvasProps;\n  baseParams: FullCompositorParams;\n  checkpoints?: ConditionCheckpoints;\n  activeEditCheckpoint: TimeCheckpoint;\n  isPreviewing: boolean;\n  isSignedOff: boolean;\n  expandedGroups: Set<string>;\n  currentTime: number;\n  showWidgetOverlay: boolean;\n  onParamsChange: (params: FullCompositorParams) => void;\n  onToggleGroup: (group: string) => void;\n  onReset: () => void;\n  onSignOff: () => void;\n  onCheckpointClick: (checkpoint: TimeCheckpoint) => void;\n  onToggleWidgetOverlay: () => void;\n  onCopyLayer?: (\n    targetCondition: WeatherConditionCode,\n    layerKey: LayerKey,\n  ) => void;\n  onCopyLayerToAll?: (layerKey: LayerKey) => void;\n  onCopyCheckpoint?: (targetCheckpoints: TimeCheckpoint[]) => void;\n}\n\nexport function DetailEditor({\n  condition,\n  params,\n  canvasProps,\n  baseParams,\n  checkpoints,\n  activeEditCheckpoint,\n  isPreviewing,\n  isSignedOff,\n  expandedGroups,\n  currentTime,\n  showWidgetOverlay,\n  onParamsChange,\n  onToggleGroup,\n  onReset,\n  onSignOff,\n  onCheckpointClick,\n  onToggleWidgetOverlay,\n  onCopyLayer,\n  onCopyLayerToAll,\n  onCopyCheckpoint,\n}: DetailEditorProps) {\n  const overlayData = useMemo(\n    () => createWeatherOverlayStubData(condition),\n    [condition],\n  );\n  const label = CONDITION_LABELS[condition];\n\n  const allCheckpointsReviewed = checkpoints\n    ? TIME_CHECKPOINT_ORDER.every((cp) => checkpoints[cp] === \"reviewed\")\n    : false;\n\n  const reviewedCount = checkpoints\n    ? TIME_CHECKPOINT_ORDER.filter((cp) => checkpoints[cp] === \"reviewed\")\n        .length\n    : 0;\n\n  return (\n    <div className=\"flex h-full gap-5\">\n      <div className=\"flex w-[420px] shrink-0 flex-col gap-3\">\n        <div className=\"group/widget border-border relative aspect-4/3 overflow-hidden rounded-xl border shadow-xl @container/weather [container-type:size]\">\n          <div className=\"absolute inset-0 bg-black\">\n            <WeatherEffectsCanvas\n              className=\"absolute inset-0\"\n              {...canvasProps}\n            />\n          </div>\n\n          {showWidgetOverlay && (\n            <div className=\"absolute inset-0 z-20\">\n              <WeatherDataOverlay\n                glassParams={params.glass}\n                timeOfDay={params.celestial.timeOfDay}\n                {...overlayData}\n              />\n            </div>\n          )}\n\n          <div className=\"absolute bottom-2.5 left-2.5 z-20 opacity-0 transition-opacity group-hover/widget:opacity-100\">\n            <button\n              onClick={onToggleWidgetOverlay}\n              className=\"flex items-center gap-1 rounded bg-black/50 px-2 py-1 text-[10px] text-white/70 backdrop-blur-sm transition-all hover:bg-black/70 hover:text-white\"\n            >\n              {showWidgetOverlay ? (\n                <EyeOff className=\"size-3\" />\n              ) : (\n                <Eye className=\"size-3\" />\n              )}\n              {showWidgetOverlay ? \"Hide\" : \"Show\"}\n            </button>\n          </div>\n\n          {!showWidgetOverlay && (\n            <div className=\"absolute top-2.5 left-2.5 z-20\">\n              <div className=\"rounded bg-black/50 px-2 py-1 backdrop-blur-sm\">\n                <h2 className=\"text-xs font-medium text-white\">{label}</h2>\n              </div>\n            </div>\n          )}\n        </div>\n\n        <div className=\"border-border/40 bg-card/50 rounded-lg border p-3\">\n          <div className=\"mb-2 flex items-center justify-between\">\n            <span className=\"text-muted-foreground/50 text-[10px] font-medium tracking-wider uppercase\">\n              Checkpoints\n            </span>\n            <div className=\"flex items-center gap-2\">\n              {onCopyCheckpoint && !isPreviewing && (\n                <DropdownMenu>\n                  <DropdownMenuTrigger asChild>\n                    <button\n                      className=\"text-muted-foreground/50 hover:bg-accent/50 hover:text-muted-foreground flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] transition-colors\"\n                      title={`Copy ${TIME_CHECKPOINTS[activeEditCheckpoint].label} to other checkpoints`}\n                    >\n                      <Copy className=\"size-2.5\" />\n                      Copy to...\n                    </button>\n                  </DropdownMenuTrigger>\n                  <DropdownMenuContent align=\"end\" className=\"min-w-[140px]\">\n                    <DropdownMenuItem\n                      onClick={() => {\n                        const targets = TIME_CHECKPOINT_ORDER.filter(\n                          (cp) => cp !== activeEditCheckpoint,\n                        ) as TimeCheckpoint[];\n                        onCopyCheckpoint(targets);\n                      }}\n                      className=\"text-xs font-medium\"\n                    >\n                      All checkpoints\n                    </DropdownMenuItem>\n                    <DropdownMenuSeparator />\n                    {TIME_CHECKPOINT_ORDER.filter(\n                      (cp) => cp !== activeEditCheckpoint,\n                    ).map((checkpoint) => (\n                      <DropdownMenuItem\n                        key={checkpoint}\n                        onClick={() =>\n                          onCopyCheckpoint([checkpoint as TimeCheckpoint])\n                        }\n                        className=\"text-xs\"\n                      >\n                        {TIME_CHECKPOINTS[checkpoint].label}\n                      </DropdownMenuItem>\n                    ))}\n                  </DropdownMenuContent>\n                </DropdownMenu>\n              )}\n              <span className=\"text-muted-foreground/40 font-mono text-[10px]\">\n                {reviewedCount}/4\n              </span>\n            </div>\n          </div>\n\n          <div className=\"grid grid-cols-4 gap-1\">\n            {TIME_CHECKPOINT_ORDER.map((checkpoint) => {\n              const { value, label } = TIME_CHECKPOINTS[checkpoint];\n              const status = checkpoints?.[checkpoint] ?? \"pending\";\n              const isActive = Math.abs(currentTime - value) < 0.02;\n              const isReviewed = status === \"reviewed\";\n              const isEditing = checkpoint === activeEditCheckpoint;\n\n              return (\n                <button\n                  key={checkpoint}\n                  onClick={() => onCheckpointClick(checkpoint)}\n                  className={cn(\n                    \"relative py-2 text-[11px] font-medium tracking-wide uppercase transition-colors\",\n                    isEditing\n                      ? \"bg-muted-foreground/20 text-foreground\"\n                      : isActive\n                        ? \"text-foreground\"\n                        : isReviewed\n                          ? \"text-foreground/60\"\n                          : \"text-muted-foreground/40 hover:text-muted-foreground\",\n                  )}\n                  title={`${label} (${status})${isEditing ? \" - editing\" : \"\"}`}\n                >\n                  {label}\n                  {isReviewed && !isEditing && (\n                    <span className=\"absolute top-1 right-1 text-[8px]\">*</span>\n                  )}\n                </button>\n              );\n            })}\n          </div>\n        </div>\n\n        <div className=\"flex gap-1.5\">\n          <button\n            onClick={onReset}\n            className=\"border-border/40 bg-card/30 text-muted-foreground/60 hover:bg-accent/50 hover:text-muted-foreground flex flex-1 items-center justify-center gap-1.5 rounded-md border px-3 py-1.5 text-xs transition-all\"\n          >\n            <RotateCcw className=\"size-3\" />\n            Reset\n          </button>\n          <button\n            onClick={onSignOff}\n            disabled={!allCheckpointsReviewed && !isSignedOff}\n            className={cn(\n              \"flex flex-1 items-center justify-center gap-1.5 rounded-md px-3 py-1.5 text-xs transition-all\",\n              isSignedOff\n                ? \"border border-green-500/20 bg-green-500/10 text-green-600/70 dark:text-green-400/70\"\n                : allCheckpointsReviewed\n                  ? \"bg-foreground/10 text-foreground/70 hover:bg-foreground/15\"\n                  : \"bg-muted/20 text-muted-foreground/30 cursor-not-allowed\",\n            )}\n            title={\n              !allCheckpointsReviewed && !isSignedOff\n                ? \"Review all checkpoints first\"\n                : undefined\n            }\n          >\n            {isSignedOff ? (\n              <>\n                <CheckCircle2 className=\"size-3\" />\n                Done\n              </>\n            ) : (\n              \"Sign Off\"\n            )}\n          </button>\n        </div>\n      </div>\n\n      <div className=\"flex min-h-0 min-w-0 flex-1 flex-col gap-3\">\n        <GlassControls\n          params={params.glass}\n          onChange={(next: GlassParams) =>\n            onParamsChange({ ...params, glass: next })\n          }\n        />\n\n        <div className=\"border-border/40 bg-card/30 min-h-0 flex-1 overflow-hidden rounded-lg border\">\n          <div className=\"scrollbar-subtle h-full overflow-y-auto\">\n            <ParameterPanel\n              params={params}\n              baseParams={baseParams}\n              onParamsChange={onParamsChange}\n              expandedGroups={expandedGroups}\n              onToggleGroup={onToggleGroup}\n              activeEditCheckpoint={activeEditCheckpoint}\n              isPreviewing={isPreviewing}\n              currentCondition={condition}\n              onCopyLayer={onCopyLayer}\n              onCopyLayerToAll={onCopyLayerToAll}\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/components/export-panel.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Check, FileCode } from \"lucide-react\";\nimport type { WeatherConditionCode } from \"@/lib/weather-authoring/weather-widget/schema\";\nimport type { CheckpointOverrides } from \"../../weather-compositor/presets\";\nimport { hasAnyTuningDelta } from \"../lib/has-any-tuning-delta\";\nimport { listUpdatedParams } from \"../lib/list-updated-params\";\n\ninterface ExportPanelProps {\n  checkpointOverrides: Partial<\n    Record<WeatherConditionCode, CheckpointOverrides>\n  >;\n  signedOff: Set<WeatherConditionCode>;\n  onApplied?: (\n    checkpointOverrides: Partial<\n      Record<WeatherConditionCode, CheckpointOverrides>\n    >,\n  ) => void;\n  onRecovered?: (\n    checkpointOverrides: Partial<\n      Record<WeatherConditionCode, CheckpointOverrides>\n    >,\n  ) => void;\n}\n\ntype ToastState = {\n  tone: \"success\" | \"error\" | \"info\";\n  title: string;\n  detail?: string;\n} | null;\n\nexport function ExportPanel({\n  checkpointOverrides,\n  signedOff,\n  onApplied,\n  onRecovered,\n}: ExportPanelProps) {\n  const [applyStatus, setApplyStatus] = useState<\n    \"idle\" | \"saving\" | \"success\" | \"error\"\n  >(\"idle\");\n  const [applyError, setApplyError] = useState<string | null>(null);\n  const [toast, setToast] = useState<ToastState>(null);\n  const canApply = hasAnyTuningDelta(checkpointOverrides);\n\n  const handleRecover = async () => {\n    setApplyError(null);\n    try {\n      const response = await fetch(\"/api/weather-tuning/recover\");\n      if (!response.ok) {\n        const message = await response.text();\n        setApplyError(message || \"Failed to recover tuning.\");\n        return;\n      }\n      const payload = (await response.json()) as {\n        checkpointOverrides?: Partial<\n          Record<WeatherConditionCode, CheckpointOverrides>\n        >;\n      };\n      if (!payload?.checkpointOverrides) {\n        setApplyError(\"No recovered presets returned.\");\n        return;\n      }\n      onRecovered?.(payload.checkpointOverrides);\n      setToast({\n        tone: \"info\",\n        title: \"Recovered tuning from repo presets\",\n      });\n    } catch (error) {\n      console.error(\"Failed to recover tuning.\", error);\n      setApplyError(\"Failed to recover tuning.\");\n    }\n  };\n\n  const handleApply = async () => {\n    if (!canApply) return;\n\n    setApplyStatus(\"saving\");\n    setApplyError(null);\n    try {\n      const response = await fetch(\"/api/weather-tuning/apply\", {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({\n          checkpointOverrides,\n          signedOff: Array.from(signedOff),\n        }),\n      });\n\n      if (!response.ok) {\n        const message = await response.text();\n        setApplyStatus(\"error\");\n        setApplyError(message || \"Failed to apply export.\");\n        return;\n      }\n\n      const payload = (await response.json()) as {\n        path?: string;\n        checkpointOverrides?: Partial<\n          Record<WeatherConditionCode, CheckpointOverrides>\n        >;\n      };\n      const filePath =\n        typeof payload?.path === \"string\"\n          ? payload.path\n          : \"lib/weather-authoring/presets/tuned-presets.json\";\n\n      const updatedParams = listUpdatedParams(checkpointOverrides);\n      const detail =\n        updatedParams.length > 0\n          ? `Updated params (${updatedParams.length}): ${updatedParams.join(\", \")}`\n          : \"Updated params: none\";\n\n      setToast({\n        tone: \"success\",\n        title: `Applied tuning → ${filePath}`,\n        detail,\n      });\n      setApplyStatus(\"success\");\n      try {\n        if (payload?.checkpointOverrides) {\n          onApplied?.(payload.checkpointOverrides);\n        } else {\n          onApplied?.({});\n        }\n      } catch (error) {\n        console.error(\"onApplied() failed after apply.\", error);\n      }\n      setTimeout(() => setApplyStatus(\"idle\"), 2000);\n    } catch (error) {\n      console.error(\"Failed to apply export.\", error);\n      setApplyStatus(\"error\");\n      setApplyError(\"Failed to apply export.\");\n      setToast({\n        tone: \"error\",\n        title: \"Failed to apply tuning\",\n      });\n    }\n  };\n\n  useEffect(() => {\n    if (!toast) return;\n    const timer = window.setTimeout(() => setToast(null), 7000);\n    return () => window.clearTimeout(timer);\n  }, [toast]);\n\n  return (\n    <>\n      <div className=\"flex items-center gap-2\">\n        <Button\n          variant=\"outline\"\n          size=\"sm\"\n          className=\"gap-2\"\n          onClick={handleRecover}\n          disabled={applyStatus === \"saving\"}\n        >\n          <FileCode className=\"size-4\" />\n          Recover from repo\n        </Button>\n        <Button\n          variant=\"outline\"\n          size=\"sm\"\n          className=\"gap-2\"\n          onClick={handleApply}\n          disabled={applyStatus === \"saving\" || !canApply}\n        >\n          <FileCode className=\"size-4\" />\n          {applyStatus === \"saving\"\n            ? \"Applying…\"\n            : applyStatus === \"success\"\n              ? \"Applied\"\n              : \"Apply to repo\"}\n          {applyStatus === \"success\" && (\n            <Check className=\"size-4 text-emerald-400\" />\n          )}\n        </Button>\n      </div>\n      {applyError && (\n        <span className=\"ml-2 text-xs text-red-500/80\">{applyError}</span>\n      )}\n      {toast && (\n        <div\n          style={{ zIndex: 2147483647 }}\n          className={`fixed bottom-6 right-6 z-50 max-h-56 max-w-[44rem] overflow-auto rounded-md border px-3 py-2 text-xs shadow-lg ${\n            toast.tone === \"success\"\n              ? \"border-emerald-500/60 bg-emerald-950/95 text-emerald-50\"\n              : toast.tone === \"error\"\n                ? \"border-rose-500/60 bg-rose-950/95 text-rose-50\"\n                : \"border-slate-500/60 bg-slate-900/95 text-slate-50\"\n          }`}\n        >\n          <div className=\"font-medium\">{toast.title}</div>\n          {toast.detail && (\n            <div className=\"mt-1 whitespace-pre-wrap opacity-95\">\n              {toast.detail}\n            </div>\n          )}\n        </div>\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/components/glass-controls.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { ChevronDown, Sparkles } from \"lucide-react\";\nimport { cn } from \"@/lib/ui/cn\";\nimport type { GlassParams } from \"../../weather-compositor/presets\";\n\ninterface GlassControlsProps {\n  params: GlassParams;\n  onChange: (params: GlassParams) => void;\n}\n\ninterface SliderRowProps {\n  label: string;\n  value: number;\n  min: number;\n  max: number;\n  step: number;\n  onChange: (value: number) => void;\n  disabled?: boolean;\n}\n\nfunction SliderRow({\n  label,\n  value,\n  min,\n  max,\n  step,\n  onChange,\n  disabled,\n}: SliderRowProps) {\n  return (\n    <div className=\"flex items-center gap-3\">\n      <span\n        className={cn(\n          \"w-24 text-[11px]\",\n          disabled ? \"text-muted-foreground/30\" : \"text-muted-foreground/70\",\n        )}\n      >\n        {label}\n      </span>\n      <input\n        type=\"range\"\n        min={min}\n        max={max}\n        step={step}\n        value={value}\n        onChange={(e) => onChange(parseFloat(e.target.value))}\n        disabled={disabled}\n        className=\"h-1 flex-1 cursor-pointer appearance-none rounded-full bg-muted/50 disabled:cursor-not-allowed disabled:opacity-30 [&::-webkit-slider-thumb]:size-2.5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-foreground/60\"\n      />\n      <span\n        className={cn(\n          \"w-8 text-right font-mono text-[10px]\",\n          disabled ? \"text-muted-foreground/30\" : \"text-muted-foreground/50\",\n        )}\n      >\n        {value}\n      </span>\n    </div>\n  );\n}\n\nexport function GlassControls({ params, onChange }: GlassControlsProps) {\n  const [isExpanded, setIsExpanded] = useState(false);\n\n  const updateParam = <K extends keyof GlassParams>(\n    key: K,\n    value: GlassParams[K],\n  ) => {\n    onChange({ ...params, [key]: value });\n  };\n\n  return (\n    <div className=\"rounded-lg border border-border/40 bg-card/30\">\n      <button\n        onClick={() => setIsExpanded(!isExpanded)}\n        className=\"flex w-full items-center justify-between px-3 py-2 text-left\"\n      >\n        <div className=\"flex items-center gap-2\">\n          <Sparkles\n            className={cn(\n              \"size-3.5 transition-colors\",\n              params.enabled\n                ? \"text-foreground/60\"\n                : \"text-muted-foreground/30\",\n            )}\n          />\n          <span className=\"text-[11px] font-medium text-foreground/70\">\n            Glass Effect\n          </span>\n          {!params.enabled && (\n            <span className=\"rounded bg-muted/50 px-1.5 py-0.5 text-[9px] text-muted-foreground/50\">\n              OFF\n            </span>\n          )}\n        </div>\n        <ChevronDown\n          className={cn(\n            \"size-3.5 text-muted-foreground/40 transition-transform\",\n            isExpanded && \"rotate-180\",\n          )}\n        />\n      </button>\n\n      {isExpanded && (\n        <div className=\"border-t border-border/30 px-3 py-2.5 space-y-2.5\">\n          <div className=\"flex items-center gap-2\">\n            <label className=\"flex cursor-pointer items-center gap-2\">\n              <input\n                type=\"checkbox\"\n                checked={params.enabled}\n                onChange={(e) => updateParam(\"enabled\", e.target.checked)}\n                className=\"size-3 rounded border-border accent-foreground\"\n              />\n              <span className=\"text-[11px] text-muted-foreground/70\">\n                Enable SVG refraction\n              </span>\n            </label>\n            <span className=\"text-[9px] text-muted-foreground/40\">\n              (disable to simulate unsupported browsers)\n            </span>\n          </div>\n\n          <div className=\"space-y-1.5\">\n            <SliderRow\n              label=\"Depth\"\n              value={params.depth}\n              min={2}\n              max={20}\n              step={1}\n              onChange={(v) => updateParam(\"depth\", v)}\n              disabled={!params.enabled}\n            />\n            <SliderRow\n              label=\"Strength\"\n              value={params.strength}\n              min={10}\n              max={120}\n              step={5}\n              onChange={(v) => updateParam(\"strength\", v)}\n              disabled={!params.enabled}\n            />\n            <SliderRow\n              label=\"Chromatic\"\n              value={params.chromaticAberration}\n              min={0}\n              max={20}\n              step={1}\n              onChange={(v) => updateParam(\"chromaticAberration\", v)}\n              disabled={!params.enabled}\n            />\n            <SliderRow\n              label=\"Blur\"\n              value={params.blur}\n              min={0}\n              max={6}\n              step={0.5}\n              onChange={(v) => updateParam(\"blur\", v)}\n              disabled={!params.enabled}\n            />\n            <SliderRow\n              label=\"Brightness\"\n              value={params.brightness}\n              min={0.8}\n              max={1.4}\n              step={0.05}\n              onChange={(v) => updateParam(\"brightness\", v)}\n              disabled={!params.enabled}\n            />\n            <SliderRow\n              label=\"Saturation\"\n              value={params.saturation}\n              min={0.5}\n              max={2.0}\n              step={0.1}\n              onChange={(v) => updateParam(\"saturation\", v)}\n              disabled={!params.enabled}\n            />\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/components/parameter-definitions.ts",
    "content": "import type { LayerKey } from \"../hooks/use-tuning-state\";\nimport { RAIN_PARAM_LIMITS, SNOW_FALL_SPEED_MAX } from \"../lib/constants\";\n\nexport type TunableLayerKey = Exclude<LayerKey, \"layers\">;\n\nexport interface ParameterDef {\n  key: string;\n  label: string;\n  min: number;\n  max: number;\n  step: number;\n}\n\nexport interface ParameterGroup {\n  name: string;\n  layer: TunableLayerKey;\n  params: ParameterDef[];\n}\n\nexport const PARAMETER_GROUPS: ParameterGroup[] = [\n  {\n    name: \"Sky\",\n    layer: \"celestial\",\n    params: [\n      {\n        key: \"celestialY\",\n        label: \"Sun/Moon Height\",\n        min: 0,\n        max: 1,\n        step: 0.01,\n      },\n      {\n        key: \"celestialX\",\n        label: \"Sun/Moon Position\",\n        min: 0,\n        max: 1,\n        step: 0.01,\n      },\n      { key: \"sunSize\", label: \"Sun Size\", min: 0.01, max: 0.2, step: 0.005 },\n      { key: \"moonSize\", label: \"Moon Size\", min: 0.01, max: 0.2, step: 0.005 },\n      { key: \"moonPhase\", label: \"Moon Phase\", min: 0, max: 1, step: 0.01 },\n      { key: \"starDensity\", label: \"Star Density\", min: 0, max: 2, step: 0.01 },\n      {\n        key: \"sunGlowIntensity\",\n        label: \"Sun Glow Intensity\",\n        min: 0,\n        max: 6,\n        step: 0.05,\n      },\n      {\n        key: \"sunGlowSize\",\n        label: \"Sun Glow Size\",\n        min: 0,\n        max: 2,\n        step: 0.01,\n      },\n      {\n        key: \"moonGlowIntensity\",\n        label: \"Moon Glow Intensity\",\n        min: 0,\n        max: 6,\n        step: 0.05,\n      },\n      {\n        key: \"moonGlowSize\",\n        label: \"Moon Glow Size\",\n        min: 0,\n        max: 2,\n        step: 0.01,\n      },\n      {\n        key: \"skyBrightness\",\n        label: \"Sky Brightness\",\n        min: 0,\n        max: 2,\n        step: 0.01,\n      },\n      {\n        key: \"skySaturation\",\n        label: \"Sky Saturation\",\n        min: 0,\n        max: 2,\n        step: 0.01,\n      },\n      { key: \"skyContrast\", label: \"Sky Contrast\", min: 0, max: 2, step: 0.01 },\n    ],\n  },\n  {\n    name: \"Sun Rays\",\n    layer: \"celestial\",\n    params: [\n      {\n        key: \"sunRayCount\",\n        label: \"Ray Count\",\n        min: 0,\n        max: 24,\n        step: 1,\n      },\n      {\n        key: \"sunRayLength\",\n        label: \"Ray Length\",\n        min: 0,\n        max: 2,\n        step: 0.01,\n      },\n      {\n        key: \"sunRayIntensity\",\n        label: \"Ray Intensity\",\n        min: 0,\n        max: 3,\n        step: 0.01,\n      },\n      {\n        key: \"sunRayShimmer\",\n        label: \"Ray Shimmer\",\n        min: 0,\n        max: 5,\n        step: 0.05,\n      },\n      {\n        key: \"sunRayShimmerSpeed\",\n        label: \"Ray Shimmer Speed\",\n        min: 0,\n        max: 5,\n        step: 0.05,\n      },\n    ],\n  },\n  {\n    name: \"Clouds\",\n    layer: \"cloud\",\n    params: [\n      { key: \"cloudScale\", label: \"Scale\", min: 0.5, max: 3, step: 0.01 },\n      { key: \"coverage\", label: \"Coverage\", min: 0, max: 1, step: 0.01 },\n      { key: \"density\", label: \"Density\", min: 0, max: 2, step: 0.01 },\n      { key: \"softness\", label: \"Softness\", min: 0, max: 1, step: 0.01 },\n      {\n        key: \"lightIntensity\",\n        label: \"Light Intensity\",\n        min: 0,\n        max: 2,\n        step: 0.01,\n      },\n      {\n        key: \"ambientDarkness\",\n        label: \"Ambient Darkness\",\n        min: 0,\n        max: 1,\n        step: 0.01,\n      },\n      {\n        key: \"backlightIntensity\",\n        label: \"Backlight Intensity\",\n        min: 0,\n        max: 2,\n        step: 0.01,\n      },\n      { key: \"windSpeed\", label: \"Wind Speed\", min: 0, max: 2, step: 0.01 },\n      {\n        key: \"windAngle\",\n        label: \"Wind Angle\",\n        min: -3.14,\n        max: 3.14,\n        step: 0.01,\n      },\n      { key: \"turbulence\", label: \"Turbulence\", min: 0, max: 1, step: 0.01 },\n      {\n        key: \"sunAzimuth\",\n        label: \"Sun Azimuth\",\n        min: 0,\n        max: 6.28,\n        step: 0.01,\n      },\n      { key: \"numLayers\", label: \"Layers\", min: 1, max: 8, step: 1 },\n      { key: \"layerSpread\", label: \"Layer Spread\", min: 0, max: 1, step: 0.01 },\n      { key: \"horizonLine\", label: \"Horizon Line\", min: 0, max: 1, step: 0.01 },\n      { key: \"starSize\", label: \"Star Size\", min: 0, max: 2, step: 0.01 },\n      {\n        key: \"starTwinkleSpeed\",\n        label: \"Star Twinkle Speed\",\n        min: 0,\n        max: 3,\n        step: 0.01,\n      },\n      {\n        key: \"starTwinkleAmount\",\n        label: \"Star Twinkle Amount\",\n        min: 0,\n        max: 1,\n        step: 0.01,\n      },\n    ],\n  },\n  {\n    name: \"Rain\",\n    layer: \"rain\",\n    params: [\n      {\n        key: \"zoom\",\n        label: \"Zoom\",\n        min: RAIN_PARAM_LIMITS.zoom.min,\n        max: RAIN_PARAM_LIMITS.zoom.max,\n        step: 0.01,\n      },\n      {\n        key: \"fallingIntensity\",\n        label: \"Falling Intensity\",\n        min: RAIN_PARAM_LIMITS.fallingIntensity.min,\n        max: RAIN_PARAM_LIMITS.fallingIntensity.max,\n        step: 0.01,\n      },\n      {\n        key: \"fallingSpeed\",\n        label: \"Falling Speed\",\n        min: RAIN_PARAM_LIMITS.fallingSpeed.min,\n        max: RAIN_PARAM_LIMITS.fallingSpeed.max,\n        step: 0.1,\n      },\n      {\n        key: \"fallingAngle\",\n        label: \"Falling Angle\",\n        min: RAIN_PARAM_LIMITS.fallingAngle.min,\n        max: RAIN_PARAM_LIMITS.fallingAngle.max,\n        step: 0.01,\n      },\n      {\n        key: \"fallingStreakLength\",\n        label: \"Streak Length\",\n        min: RAIN_PARAM_LIMITS.fallingStreakLength.min,\n        max: RAIN_PARAM_LIMITS.fallingStreakLength.max,\n        step: 0.01,\n      },\n      {\n        key: \"fallingLayers\",\n        label: \"Layers\",\n        min: RAIN_PARAM_LIMITS.fallingLayers.min,\n        max: RAIN_PARAM_LIMITS.fallingLayers.max,\n        step: 1,\n      },\n      {\n        key: \"fallingRefraction\",\n        label: \"Refraction\",\n        min: RAIN_PARAM_LIMITS.fallingRefraction.min,\n        max: RAIN_PARAM_LIMITS.fallingRefraction.max,\n        step: 0.01,\n      },\n      {\n        key: \"fallingWaviness\",\n        label: \"Waviness\",\n        min: RAIN_PARAM_LIMITS.fallingWaviness.min,\n        max: RAIN_PARAM_LIMITS.fallingWaviness.max,\n        step: 0.01,\n      },\n      {\n        key: \"fallingThicknessVar\",\n        label: \"Thickness Var\",\n        min: RAIN_PARAM_LIMITS.fallingThicknessVar.min,\n        max: RAIN_PARAM_LIMITS.fallingThicknessVar.max,\n        step: 0.01,\n      },\n      {\n        key: \"glassIntensity\",\n        label: \"Glass Droplets\",\n        min: RAIN_PARAM_LIMITS.glassIntensity.min,\n        max: RAIN_PARAM_LIMITS.glassIntensity.max,\n        step: 0.01,\n      },\n    ],\n  },\n  {\n    name: \"Snow\",\n    layer: \"snow\",\n    params: [\n      { key: \"intensity\", label: \"Intensity\", min: 0, max: 1, step: 0.01 },\n      { key: \"layers\", label: \"Layers\", min: 1, max: 8, step: 1 },\n      {\n        key: \"fallSpeed\",\n        label: \"Fall Speed\",\n        min: 0.1,\n        max: SNOW_FALL_SPEED_MAX,\n        step: 0.1,\n      },\n      { key: \"windSpeed\", label: \"Wind\", min: 0, max: 2, step: 0.01 },\n      {\n        key: \"windAngle\",\n        label: \"Wind Angle\",\n        min: -3.14,\n        max: 3.14,\n        step: 0.01,\n      },\n      {\n        key: \"windShear\",\n        label: \"Wind Shear\",\n        min: 0,\n        max: 1,\n        step: 0.01,\n      },\n      { key: \"turbulence\", label: \"Turbulence\", min: 0, max: 1, step: 0.01 },\n      { key: \"drift\", label: \"Drift\", min: 0, max: 1, step: 0.01 },\n      { key: \"flutter\", label: \"Flutter\", min: 0, max: 1, step: 0.01 },\n      { key: \"flakeSize\", label: \"Flake Size\", min: 0.2, max: 2, step: 0.01 },\n      {\n        key: \"sizeVariation\",\n        label: \"Size Variation\",\n        min: 0,\n        max: 1,\n        step: 0.01,\n      },\n      { key: \"opacity\", label: \"Opacity\", min: 0, max: 1, step: 0.01 },\n      { key: \"glowAmount\", label: \"Glow\", min: 0, max: 1, step: 0.01 },\n      { key: \"sparkle\", label: \"Sparkle\", min: 0, max: 1, step: 0.01 },\n      {\n        key: \"visibility\",\n        label: \"Visibility\",\n        min: 0,\n        max: 1,\n        step: 0.01,\n      },\n    ],\n  },\n  {\n    name: \"Lightning\",\n    layer: \"lightning\",\n    params: [\n      {\n        key: \"glowIntensity\",\n        label: \"Flash Intensity\",\n        min: 0,\n        max: 2,\n        step: 0.01,\n      },\n      {\n        key: \"autoInterval\",\n        label: \"Auto Interval\",\n        min: 1,\n        max: 20,\n        step: 0.25,\n      },\n      {\n        key: \"branchDensity\",\n        label: \"Branch Density\",\n        min: 0,\n        max: 1,\n        step: 0.01,\n      },\n      {\n        key: \"displacement\",\n        label: \"Displacement\",\n        min: 0,\n        max: 0.2,\n        step: 0.005,\n      },\n      {\n        key: \"flashDuration\",\n        label: \"Flash Duration\",\n        min: 0.05,\n        max: 0.5,\n        step: 0.01,\n      },\n      {\n        key: \"sceneIllumination\",\n        label: \"Scene Illumination\",\n        min: 0,\n        max: 1,\n        step: 0.01,\n      },\n      {\n        key: \"afterglowPersistence\",\n        label: \"Afterglow\",\n        min: 0,\n        max: 1,\n        step: 0.01,\n      },\n    ],\n  },\n  {\n    name: \"Glass\",\n    layer: \"glass\",\n    params: [\n      { key: \"depth\", label: \"Depth\", min: 2, max: 20, step: 1 },\n      { key: \"strength\", label: \"Strength\", min: 10, max: 120, step: 5 },\n      {\n        key: \"chromaticAberration\",\n        label: \"Chromatic\",\n        min: 0,\n        max: 20,\n        step: 1,\n      },\n      { key: \"blur\", label: \"Blur\", min: 0, max: 6, step: 0.5 },\n      {\n        key: \"brightness\",\n        label: \"Brightness\",\n        min: 0.8,\n        max: 1.4,\n        step: 0.05,\n      },\n      { key: \"saturation\", label: \"Saturation\", min: 0.5, max: 2.0, step: 0.1 },\n    ],\n  },\n  {\n    name: \"Post\",\n    layer: \"post\",\n    params: [\n      { key: \"haze\", label: \"Haze\", min: 0, max: 1, step: 0.01 },\n      {\n        key: \"hazeHorizon\",\n        label: \"Haze Horizon\",\n        min: 0,\n        max: 1,\n        step: 0.01,\n      },\n      {\n        key: \"hazeDesaturation\",\n        label: \"Haze Desaturation\",\n        min: 0,\n        max: 1,\n        step: 0.01,\n      },\n      {\n        key: \"hazeContrast\",\n        label: \"Haze Contrast\",\n        min: 0,\n        max: 1,\n        step: 0.01,\n      },\n      {\n        key: \"bloomIntensity\",\n        label: \"Bloom Intensity\",\n        min: 0,\n        max: 2,\n        step: 0.01,\n      },\n      {\n        key: \"bloomThreshold\",\n        label: \"Bloom Threshold\",\n        min: 0,\n        max: 1,\n        step: 0.01,\n      },\n      { key: \"bloomKnee\", label: \"Bloom Knee\", min: 0, max: 1, step: 0.01 },\n      { key: \"bloomRadius\", label: \"Bloom Radius\", min: 0, max: 20, step: 0.5 },\n      {\n        key: \"bloomTapScale\",\n        label: \"Bloom Tap Scale\",\n        min: 0.5,\n        max: 2,\n        step: 0.01,\n      },\n      {\n        key: \"exposureIntensity\",\n        label: \"Exposure Intensity\",\n        min: 0,\n        max: 2,\n        step: 0.01,\n      },\n      {\n        key: \"exposureDesaturation\",\n        label: \"Exposure Desaturation\",\n        min: 0,\n        max: 1,\n        step: 0.01,\n      },\n      {\n        key: \"exposureRecovery\",\n        label: \"Exposure Recovery\",\n        min: 0,\n        max: 5,\n        step: 0.05,\n      },\n      {\n        key: \"godRayIntensity\",\n        label: \"God Rays Intensity\",\n        min: 0,\n        max: 2,\n        step: 0.01,\n      },\n      {\n        key: \"godRayDecay\",\n        label: \"God Rays Decay\",\n        min: 0.8,\n        max: 1,\n        step: 0.001,\n      },\n      {\n        key: \"godRayDensity\",\n        label: \"God Rays Density\",\n        min: 0,\n        max: 1,\n        step: 0.01,\n      },\n      {\n        key: \"godRayWeight\",\n        label: \"God Rays Weight\",\n        min: 0,\n        max: 1,\n        step: 0.01,\n      },\n      {\n        key: \"godRaySamples\",\n        label: \"God Rays Samples\",\n        min: 0,\n        max: 200,\n        step: 1,\n      },\n    ],\n  },\n];\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/components/parameter-matrix-view.tsx",
    "content": "\"use client\";\n\nimport { useMemo, useState, useCallback, useEffect } from \"react\";\nimport { cn } from \"@/lib/ui/cn\";\nimport {\n  WeatherEffectsCanvas,\n  getMaxConcurrentWeatherWebglCanvases,\n  setMaxConcurrentWeatherWebglCanvases,\n} from \"@/lib/weather-authoring/weather-widget/effects/weather-effects-canvas\";\nimport type { WeatherConditionCode } from \"@/lib/weather-authoring/weather-widget/schema\";\nimport {\n  WEATHER_CONDITIONS,\n  CONDITION_LABELS,\n} from \"../../weather-compositor/presets\";\nimport { TIME_CHECKPOINTS, TIME_CHECKPOINT_ORDER } from \"../lib/constants\";\nimport { mapCompositorParamsToCanvasProps } from \"../lib/map-to-canvas-props\";\nimport type { TimeCheckpoint } from \"../types\";\nimport type { TuningStateReturn } from \"../hooks/use-tuning-state\";\nimport { Slider } from \"@/components/ui/slider\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport {\n  ChevronDown,\n  ChevronRight,\n  Copy,\n  Layers,\n  Clock,\n  Globe,\n} from \"lucide-react\";\nimport {\n  PARAMETER_GROUPS,\n  type ParameterDef,\n  type TunableLayerKey,\n} from \"./parameter-definitions\";\nimport {\n  WeatherDataOverlay,\n  createWeatherOverlayStubData,\n} from \"./weather-data-overlay\";\n\n// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\ninterface ParameterMatrixViewProps {\n  tuningState: TuningStateReturn;\n}\n\n// -----------------------------------------------------------------------------\n// Hooks\n// -----------------------------------------------------------------------------\n\n/**\n * Hook for reading/writing a single parameter value across conditions.\n * Encapsulates the logic for getting values from merged params and bulk updates.\n */\nfunction useParameterAccessor(\n  tuningState: TuningStateReturn,\n  layer: TunableLayerKey,\n  paramKey: string,\n  checkpoint: TimeCheckpoint,\n) {\n  const getValue = useCallback(\n    (condition: WeatherConditionCode): number | undefined => {\n      const params = tuningState.getFullParamsForCheckpoint(\n        condition,\n        checkpoint,\n      );\n      const layerParams = params[layer];\n      if (!layerParams || typeof layerParams !== \"object\") return undefined;\n      // Layer params are typed interfaces but we need dynamic key access\n      const value = (layerParams as unknown as Record<string, unknown>)[\n        paramKey\n      ];\n      return typeof value === \"number\" ? value : undefined;\n    },\n    [tuningState, layer, paramKey, checkpoint],\n  );\n\n  const setValue = useCallback(\n    (condition: WeatherConditionCode, value: number) => {\n      tuningState.updateParameterAtCheckpoint(\n        condition,\n        checkpoint,\n        layer,\n        paramKey,\n        value,\n      );\n    },\n    [tuningState, checkpoint, layer, paramKey],\n  );\n\n  const applyToAllConditions = useCallback(\n    (sourceCondition: WeatherConditionCode) => {\n      const value = getValue(sourceCondition);\n      if (value === undefined) return;\n\n      tuningState.bulkUpdateParameter(\n        WEATHER_CONDITIONS.filter((c) => c !== sourceCondition),\n        [checkpoint],\n        layer,\n        paramKey,\n        value,\n      );\n    },\n    [getValue, tuningState, checkpoint, layer, paramKey],\n  );\n\n  const applyToAllCheckpoints = useCallback(\n    (condition: WeatherConditionCode) => {\n      const value = getValue(condition);\n      if (value === undefined) return;\n\n      tuningState.bulkUpdateParameter(\n        [condition],\n        TIME_CHECKPOINT_ORDER,\n        layer,\n        paramKey,\n        value,\n      );\n    },\n    [getValue, tuningState, layer, paramKey],\n  );\n\n  const applyEverywhere = useCallback(\n    (sourceCondition: WeatherConditionCode) => {\n      const value = getValue(sourceCondition);\n      if (value === undefined) return;\n\n      tuningState.bulkUpdateParameter(\n        WEATHER_CONDITIONS,\n        TIME_CHECKPOINT_ORDER,\n        layer,\n        paramKey,\n        value,\n      );\n    },\n    [getValue, tuningState, layer, paramKey],\n  );\n\n  return {\n    getValue,\n    setValue,\n    applyToAllConditions,\n    applyToAllCheckpoints,\n    applyEverywhere,\n  };\n}\n\n// -----------------------------------------------------------------------------\n// Components\n// -----------------------------------------------------------------------------\n\n/** Preview tile showing weather effects for a single condition */\nfunction ConditionPreview({\n  condition,\n  tuningState,\n  checkpoint,\n}: {\n  condition: WeatherConditionCode;\n  tuningState: TuningStateReturn;\n  checkpoint: TimeCheckpoint;\n}) {\n  const params = useMemo(\n    () => tuningState.getFullParamsForCheckpoint(condition, checkpoint),\n    [condition, tuningState, checkpoint],\n  );\n  const canvasProps = useMemo(\n    () => mapCompositorParamsToCanvasProps(params),\n    [params],\n  );\n  const overlayData = useMemo(\n    () => createWeatherOverlayStubData(condition),\n    [condition],\n  );\n\n  return (\n    <div className=\"border-border/50 relative aspect-4/3 w-full overflow-hidden rounded-md border bg-black @container/weather [container-type:size]\">\n      <WeatherEffectsCanvas className=\"absolute inset-0\" {...canvasProps} />\n      <div className=\"absolute inset-0 z-10\">\n        <WeatherDataOverlay\n          glassParams={params.glass}\n          location={overlayData.location}\n          conditionCode={condition}\n          temperature={overlayData.temperature}\n          tempHigh={overlayData.tempHigh}\n          tempLow={overlayData.tempLow}\n          forecast={overlayData.forecast}\n          unit={overlayData.unit}\n          timeOfDay={params.celestial.timeOfDay}\n        />\n      </div>\n    </div>\n  );\n}\n\n/** Dropdown menu for bulk-applying a parameter value */\nfunction BulkApplyMenu({\n  onApplyToAllConditions,\n  onApplyToAllCheckpoints,\n  onApplyEverywhere,\n}: {\n  onApplyToAllConditions: () => void;\n  onApplyToAllCheckpoints: () => void;\n  onApplyEverywhere: () => void;\n}) {\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <button\n          className=\"text-muted-foreground/50 hover:bg-muted hover:text-muted-foreground rounded p-1\"\n          title=\"Apply value to...\"\n        >\n          <Copy className=\"size-3\" />\n        </button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\" className=\"min-w-[180px]\">\n        <DropdownMenuItem\n          onClick={onApplyToAllConditions}\n          className=\"gap-2 text-xs\"\n        >\n          <Layers className=\"size-3.5\" />\n          All conditions\n          <span className=\"text-muted-foreground ml-auto text-[10px]\">\n            this time\n          </span>\n        </DropdownMenuItem>\n        <DropdownMenuItem\n          onClick={onApplyToAllCheckpoints}\n          className=\"gap-2 text-xs\"\n        >\n          <Clock className=\"size-3.5\" />\n          All times\n          <span className=\"text-muted-foreground ml-auto text-[10px]\">\n            this condition\n          </span>\n        </DropdownMenuItem>\n        <DropdownMenuSeparator />\n        <DropdownMenuItem\n          onClick={onApplyEverywhere}\n          className=\"gap-2 text-xs font-medium\"\n        >\n          <Globe className=\"size-3.5\" />\n          Everywhere\n          <span className=\"text-muted-foreground ml-auto text-[10px]\">\n            all × all\n          </span>\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n\n/** Single condition row with slider and bulk-apply menu */\nfunction ConditionSlider({\n  condition,\n  param,\n  layer,\n  tuningState,\n  checkpoint,\n}: {\n  condition: WeatherConditionCode;\n  param: ParameterDef;\n  layer: TunableLayerKey;\n  tuningState: TuningStateReturn;\n  checkpoint: TimeCheckpoint;\n}) {\n  const {\n    getValue,\n    setValue,\n    applyToAllConditions,\n    applyToAllCheckpoints,\n    applyEverywhere,\n  } = useParameterAccessor(tuningState, layer, param.key, checkpoint);\n\n  const value = getValue(condition);\n  if (value === undefined) return null;\n\n  return (\n    <div className=\"flex items-center gap-3\">\n      <span className=\"text-muted-foreground w-24 truncate text-[10px]\">\n        {CONDITION_LABELS[condition]}\n      </span>\n      <Slider\n        value={[value]}\n        min={param.min}\n        max={param.max}\n        step={param.step}\n        onValueChange={([v]) => setValue(condition, v)}\n        className=\"flex-1\"\n      />\n      <span className=\"text-muted-foreground w-12 text-right font-mono text-[10px]\">\n        {value.toFixed(2)}\n      </span>\n      <BulkApplyMenu\n        onApplyToAllConditions={() => applyToAllConditions(condition)}\n        onApplyToAllCheckpoints={() => applyToAllCheckpoints(condition)}\n        onApplyEverywhere={() => applyEverywhere(condition)}\n      />\n    </div>\n  );\n}\n\n/** Expandable row for a single parameter showing sliders for all conditions */\nfunction ParameterRow({\n  param,\n  layer,\n  tuningState,\n  selectedCheckpoint,\n}: {\n  param: ParameterDef;\n  layer: TunableLayerKey;\n  tuningState: TuningStateReturn;\n  selectedCheckpoint: TimeCheckpoint;\n}) {\n  const [expanded, setExpanded] = useState(false);\n\n  const ChevronIcon = expanded ? ChevronDown : ChevronRight;\n\n  return (\n    <div className=\"border-border/30 border-b\">\n      <button\n        onClick={() => setExpanded(!expanded)}\n        className=\"hover:bg-muted/30 flex w-full items-center gap-2 px-3 py-2 text-left\"\n      >\n        <ChevronIcon className=\"text-muted-foreground size-3.5\" />\n        <span className=\"text-xs font-medium\">{param.label}</span>\n      </button>\n\n      {expanded && (\n        <div className=\"space-y-2 px-3 pb-3\">\n          {WEATHER_CONDITIONS.map((condition) => (\n            <ConditionSlider\n              key={condition}\n              condition={condition}\n              param={param}\n              layer={layer}\n              tuningState={tuningState}\n              checkpoint={selectedCheckpoint}\n            />\n          ))}\n        </div>\n      )}\n    </div>\n  );\n}\n\n/** Header with checkpoint selector tabs */\nfunction ParameterListHeader({\n  selectedCheckpoint,\n  onSelectCheckpoint,\n}: {\n  selectedCheckpoint: TimeCheckpoint;\n  onSelectCheckpoint: (checkpoint: TimeCheckpoint) => void;\n}) {\n  return (\n    <div className=\"border-border/50 bg-background sticky top-0 z-10 border-b p-3\">\n      <h2 className=\"text-sm font-medium\">Parameters</h2>\n      <p className=\"text-muted-foreground mt-1 text-[10px]\">\n        Edit parameters across all conditions\n      </p>\n\n      <div className=\"mt-3 flex gap-1\">\n        {TIME_CHECKPOINT_ORDER.map((checkpoint) => (\n          <button\n            key={checkpoint}\n            onClick={() => onSelectCheckpoint(checkpoint)}\n            className={cn(\n              \"flex-1 rounded px-2 py-1.5 text-[10px] font-medium transition-all\",\n              selectedCheckpoint === checkpoint\n                ? \"bg-foreground text-background\"\n                : \"bg-muted/50 text-muted-foreground hover:bg-muted\",\n            )}\n          >\n            {TIME_CHECKPOINTS[checkpoint].label}\n          </button>\n        ))}\n      </div>\n    </div>\n  );\n}\n\n/** Sticky group header for parameter sections */\nfunction GroupHeader({ name }: { name: string }) {\n  return (\n    <div className=\"border-border/30 bg-muted/50 sticky top-[88px] z-5 border-t border-b px-3 py-1.5\">\n      <span className=\"text-muted-foreground text-[10px] font-medium tracking-wider uppercase\">\n        {name}\n      </span>\n    </div>\n  );\n}\n\n// -----------------------------------------------------------------------------\n// Main Component\n// -----------------------------------------------------------------------------\n\nexport function ParameterMatrixView({ tuningState }: ParameterMatrixViewProps) {\n  const [selectedCheckpoint, setSelectedCheckpoint] =\n    useState<TimeCheckpoint>(\"noon\");\n\n  // The Parameter view can render many previews at once; temporarily increase the\n  // WebGL context budget so all tiles can initialize on capable machines.\n  useEffect(() => {\n    const prev = getMaxConcurrentWeatherWebglCanvases();\n    setMaxConcurrentWeatherWebglCanvases(13);\n    return () => setMaxConcurrentWeatherWebglCanvases(prev);\n  }, []);\n\n  return (\n    <div className=\"flex h-full\">\n      {/* Left: Parameter list */}\n      <div className=\"border-border/50 w-80 shrink-0 overflow-y-auto border-r\">\n        <ParameterListHeader\n          selectedCheckpoint={selectedCheckpoint}\n          onSelectCheckpoint={setSelectedCheckpoint}\n        />\n\n        <div>\n          {PARAMETER_GROUPS.map((group) => (\n            <div key={group.name}>\n              <GroupHeader name={group.name} />\n              <div className=\"divide-border/30 divide-y\">\n                {group.params.map((param) => (\n                  <ParameterRow\n                    key={`${group.layer}-${param.key}`}\n                    param={param}\n                    layer={group.layer}\n                    tuningState={tuningState}\n                    selectedCheckpoint={selectedCheckpoint}\n                  />\n                ))}\n              </div>\n            </div>\n          ))}\n        </div>\n      </div>\n\n      {/* Right: Condition grid preview */}\n      <div className=\"flex-1 overflow-y-auto p-4\">\n        <div className=\"mb-4\">\n          <h2 className=\"text-sm font-medium\">Preview Grid</h2>\n          <p className=\"text-muted-foreground mt-1 text-[10px]\">\n            All conditions at{\" \"}\n            {TIME_CHECKPOINTS[selectedCheckpoint].label.toLowerCase()}\n          </p>\n        </div>\n\n        <div className=\"grid grid-cols-4 gap-3\">\n          {WEATHER_CONDITIONS.map((condition) => (\n            <ConditionPreview\n              key={condition}\n              condition={condition}\n              tuningState={tuningState}\n              checkpoint={selectedCheckpoint}\n            />\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/components/parameter-panel.tsx",
    "content": "\"use client\";\n\nimport {\n  Accordion,\n  AccordionItem,\n  AccordionTrigger,\n  AccordionContent,\n} from \"@/components/ui/accordion\";\nimport { cn } from \"@/lib/ui/cn\";\nimport {\n  Sun,\n  Cloud,\n  CloudRain,\n  Zap,\n  Snowflake,\n  Sparkles,\n  Pencil,\n  Copy,\n  Eye,\n} from \"lucide-react\";\nimport type { FullCompositorParams } from \"../../weather-compositor/presets\";\nimport {\n  WEATHER_CONDITIONS,\n  CONDITION_LABELS,\n} from \"../../weather-compositor/presets\";\nimport { ParameterRow, ParameterToggleRow } from \"./parameter-row\";\nimport type { TimeCheckpoint } from \"../types\";\nimport type { WeatherConditionCode } from \"@/lib/weather-authoring/weather-widget/schema\";\nimport {\n  RAIN_PARAM_LIMITS,\n  SNOW_FALL_SPEED_MAX,\n  TIME_CHECKPOINTS,\n} from \"../lib/constants\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\n\ntype LayerKey =\n  | \"layers\"\n  | \"celestial\"\n  | \"cloud\"\n  | \"rain\"\n  | \"lightning\"\n  | \"snow\"\n  | \"glass\"\n  | \"post\";\n\ninterface ParameterPanelProps {\n  params: FullCompositorParams;\n  baseParams: FullCompositorParams;\n  onParamsChange: (params: FullCompositorParams) => void;\n  expandedGroups: Set<string>;\n  onToggleGroup: (group: string) => void;\n  activeEditCheckpoint: TimeCheckpoint;\n  isPreviewing: boolean;\n  currentCondition?: WeatherConditionCode;\n  onCopyLayer?: (\n    targetCondition: WeatherConditionCode,\n    layerKey: LayerKey,\n  ) => void;\n  onCopyLayerToAll?: (layerKey: LayerKey) => void;\n}\n\nfunction countChanges<T extends object>(current: T, base: T): number {\n  let count = 0;\n  for (const key of Object.keys(current) as (keyof T)[]) {\n    const currentVal = current[key];\n    const baseVal = base[key];\n    if (typeof currentVal === \"number\" && typeof baseVal === \"number\") {\n      if (Math.abs(currentVal - baseVal) > 0.001) count++;\n    } else if (currentVal !== baseVal) {\n      count++;\n    }\n  }\n  return count;\n}\n\nfunction DeltaBadge({ count }: { count: number }) {\n  if (count === 0) return null;\n  return (\n    <span className=\"rounded bg-amber-500/10 px-1 py-0.5 font-mono text-[9px] text-amber-600/60 dark:text-amber-400/60\">\n      {count}\n    </span>\n  );\n}\n\nconst LAYER_CONFIG = {\n  celestial: {\n    icon: Sun,\n    label: \"Celestial\",\n    color: \"from-amber-500 to-orange-500\",\n  },\n  clouds: {\n    icon: Cloud,\n    label: \"Clouds\",\n    color: \"from-slate-400 to-slate-500\",\n  },\n  rain: { icon: CloudRain, label: \"Rain\", color: \"from-blue-500 to-cyan-500\" },\n  lightning: {\n    icon: Zap,\n    label: \"Lightning\",\n    color: \"from-purple-500 to-indigo-500\",\n  },\n  snow: { icon: Snowflake, label: \"Snow\", color: \"from-slate-200 to-blue-300\" },\n} as const;\n\nfunction CopyToDropdown({\n  currentCondition,\n  layerLabel,\n  onCopy,\n  onCopyToAll,\n}: {\n  layerKey: LayerKey;\n  layerLabel: string;\n  currentCondition: WeatherConditionCode;\n  onCopy: (targetCondition: WeatherConditionCode) => void;\n  onCopyToAll?: () => void;\n}) {\n  const otherConditions = WEATHER_CONDITIONS.filter(\n    (c) => c !== currentCondition,\n  );\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <div\n          role=\"button\"\n          tabIndex={0}\n          className=\"text-muted-foreground/40 hover:bg-accent/50 hover:text-muted-foreground cursor-pointer rounded p-1 transition-colors\"\n          title={`Copy ${layerLabel} settings to another condition`}\n          onClick={(e) => e.stopPropagation()}\n          onKeyDown={(e) => {\n            if (e.key === \"Enter\" || e.key === \" \") {\n              e.stopPropagation();\n            }\n          }}\n        >\n          <Copy className=\"size-3\" />\n        </div>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\" className=\"min-w-[160px]\">\n        <div className=\"text-muted-foreground/60 px-2 py-1.5 text-[10px] font-medium tracking-wider uppercase\">\n          Copy {layerLabel} to…\n        </div>\n        {onCopyToAll && (\n          <>\n            <DropdownMenuItem\n              onClick={onCopyToAll}\n              className=\"text-xs font-medium\"\n            >\n              All Conditions\n            </DropdownMenuItem>\n            <DropdownMenuSeparator />\n          </>\n        )}\n        {otherConditions.map((condition) => (\n          <DropdownMenuItem\n            key={condition}\n            onClick={() => onCopy(condition)}\n            className=\"text-xs\"\n          >\n            {CONDITION_LABELS[condition]}\n          </DropdownMenuItem>\n        ))}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n\nexport function ParameterPanel({\n  params,\n  baseParams,\n  onParamsChange,\n  expandedGroups,\n  onToggleGroup,\n  activeEditCheckpoint,\n  isPreviewing,\n  currentCondition,\n  onCopyLayer,\n  onCopyLayerToAll,\n}: ParameterPanelProps) {\n  const checkpointInfo = TIME_CHECKPOINTS[activeEditCheckpoint];\n  const updateLayer = (key: keyof typeof params.layers, value: boolean) => {\n    onParamsChange({\n      ...params,\n      layers: { ...params.layers, [key]: value },\n    });\n  };\n\n  const updateCelestial = (\n    key: keyof typeof params.celestial,\n    value: number,\n  ) => {\n    onParamsChange({\n      ...params,\n      celestial: { ...params.celestial, [key]: value },\n    });\n  };\n\n  const updateCloud = (key: keyof typeof params.cloud, value: number) => {\n    onParamsChange({\n      ...params,\n      cloud: { ...params.cloud, [key]: value },\n    });\n  };\n\n  const updateRain = (key: keyof typeof params.rain, value: number) => {\n    onParamsChange({\n      ...params,\n      rain: { ...params.rain, [key]: value },\n    });\n  };\n\n  const updateLightning = (\n    key: keyof typeof params.lightning,\n    value: number | boolean,\n  ) => {\n    onParamsChange({\n      ...params,\n      lightning: { ...params.lightning, [key]: value },\n    });\n  };\n\n  const updateSnow = (key: keyof typeof params.snow, value: number) => {\n    onParamsChange({\n      ...params,\n      snow: { ...params.snow, [key]: value },\n    });\n  };\n\n  const updatePost = (\n    key: keyof typeof params.post,\n    value: number | boolean,\n  ) => {\n    onParamsChange({\n      ...params,\n      post: { ...params.post, [key]: value },\n    });\n  };\n\n  const expandedArray = Array.from(expandedGroups);\n\n  return (\n    <div className=\"flex h-full flex-col\">\n      <div className=\"border-border/30 bg-card/80 sticky top-0 z-10 border-b px-3 py-2.5 backdrop-blur-xl\">\n        <div className=\"mb-2 flex items-center justify-between\">\n          <span className=\"text-muted-foreground/40 text-[10px] font-medium tracking-wider uppercase\">\n            Layers\n          </span>\n          {isPreviewing ? (\n            <div className=\"flex items-center gap-1.5 rounded bg-amber-500/10 px-1.5 py-0.5\">\n              <Eye className=\"size-2.5 text-amber-500/70\" />\n              <span className=\"text-[9px] font-medium text-amber-600/70 dark:text-amber-400/70\">\n                Preview Mode\n              </span>\n            </div>\n          ) : (\n            <div className=\"flex items-center gap-1.5 rounded bg-blue-500/10 px-1.5 py-0.5\">\n              <Pencil className=\"size-2.5 text-blue-500/70\" />\n              <span className=\"text-[9px] font-medium text-blue-600/70 dark:text-blue-400/70\">\n                Editing {checkpointInfo.label}\n              </span>\n            </div>\n          )}\n        </div>\n        <div className=\"flex flex-wrap gap-1\">\n          {([\"celestial\", \"clouds\", \"rain\", \"lightning\", \"snow\"] as const).map(\n            (layer) => {\n              const config = LAYER_CONFIG[layer];\n              const Icon = config.icon;\n              const isEnabled = params.layers[layer];\n              const baseEnabled = baseParams.layers[layer];\n              const isChanged = isEnabled !== baseEnabled;\n\n              return (\n                <button\n                  key={layer}\n                  onClick={() => updateLayer(layer, !isEnabled)}\n                  className={cn(\n                    \"group flex items-center gap-1 rounded px-2 py-1 text-[10px] transition-all\",\n                    isEnabled\n                      ? \"bg-accent/60 text-foreground/80\"\n                      : \"bg-muted/30 text-muted-foreground/50 hover:bg-muted/50 hover:text-muted-foreground\",\n                    isChanged && \"ring-1 ring-amber-500/30\",\n                  )}\n                >\n                  <div\n                    className={cn(\n                      \"flex size-3.5 items-center justify-center rounded transition-all\",\n                      isEnabled\n                        ? \"bg-linear-to-br \" + config.color\n                        : \"bg-muted/50\",\n                    )}\n                  >\n                    <Icon\n                      className={cn(\n                        \"size-2\",\n                        isEnabled ? \"text-white\" : \"text-muted-foreground/50\",\n                      )}\n                    />\n                  </div>\n                  {config.label}\n                </button>\n              );\n            },\n          )}\n        </div>\n      </div>\n\n      <Accordion\n        type=\"multiple\"\n        value={expandedArray}\n        onValueChange={(value) => {\n          const newExpanded = new Set(value);\n          for (const group of expandedArray) {\n            if (!newExpanded.has(group)) {\n              onToggleGroup(group);\n            }\n          }\n          for (const group of value) {\n            if (!expandedGroups.has(group)) {\n              onToggleGroup(group);\n            }\n          }\n        }}\n        className=\"flex-1 px-3 pb-3\"\n      >\n        {params.layers.celestial && (\n          <AccordionItem value=\"celestial\" className=\"border-border/30\">\n            <AccordionTrigger className=\"text-muted-foreground hover:text-foreground py-2.5 text-xs hover:no-underline [&[data-state=open]>svg]:rotate-180\">\n              <div className=\"flex flex-1 items-center justify-between\">\n                <div className=\"flex items-center gap-2\">\n                  <div className=\"flex size-5 items-center justify-center rounded bg-linear-to-br from-amber-500 to-orange-500\">\n                    <Sun className=\"size-3 text-white\" />\n                  </div>\n                  <span>Celestial</span>\n                  <DeltaBadge\n                    count={countChanges(params.celestial, baseParams.celestial)}\n                  />\n                </div>\n                {currentCondition && onCopyLayer && (\n                  <CopyToDropdown\n                    layerKey=\"celestial\"\n                    layerLabel=\"Celestial\"\n                    currentCondition={currentCondition}\n                    onCopy={(target) => onCopyLayer(target, \"celestial\")}\n                    onCopyToAll={\n                      onCopyLayerToAll\n                        ? () => onCopyLayerToAll(\"celestial\")\n                        : undefined\n                    }\n                  />\n                )}\n              </div>\n            </AccordionTrigger>\n            <AccordionContent>\n              <div className=\"space-y-1\">\n                <div\n                  className=\"text-muted-foreground/50 flex items-center gap-2.5 rounded px-1.5 py-1.5 text-[10px]\"\n                  title=\"Time of day is controlled by the dial above.\"\n                >\n                  <div className=\"w-28 shrink-0\">\n                    <span className=\"text-muted-foreground/60 text-[11px]\">\n                      Time of Day\n                    </span>\n                  </div>\n                  <div className=\"flex-1 truncate\">\n                    Controlled by the dial above\n                  </div>\n                  <div className=\"w-16 shrink-0 text-right font-mono tabular-nums\">\n                    {params.celestial.timeOfDay.toFixed(2)}\n                  </div>\n                </div>\n                <ParameterRow\n                  label=\"Moon Phase\"\n                  value={params.celestial.moonPhase}\n                  baseValue={baseParams.celestial.moonPhase}\n                  min={0}\n                  max={1}\n                  onChange={(v) => updateCelestial(\"moonPhase\", v)}\n                  onReset={() =>\n                    updateCelestial(\"moonPhase\", baseParams.celestial.moonPhase)\n                  }\n                />\n                <ParameterRow\n                  label=\"Star Density\"\n                  value={params.celestial.starDensity}\n                  baseValue={baseParams.celestial.starDensity}\n                  min={0}\n                  max={2}\n                  onChange={(v) => updateCelestial(\"starDensity\", v)}\n                  onReset={() =>\n                    updateCelestial(\n                      \"starDensity\",\n                      baseParams.celestial.starDensity,\n                    )\n                  }\n                />\n                <ParameterRow\n                  label=\"Celestial X\"\n                  value={params.celestial.celestialX}\n                  baseValue={baseParams.celestial.celestialX}\n                  min={0}\n                  max={1}\n                  onChange={(v) => updateCelestial(\"celestialX\", v)}\n                  onReset={() =>\n                    updateCelestial(\n                      \"celestialX\",\n                      baseParams.celestial.celestialX,\n                    )\n                  }\n                />\n                <ParameterRow\n                  label=\"Celestial Y\"\n                  value={params.celestial.celestialY}\n                  baseValue={baseParams.celestial.celestialY}\n                  min={0}\n                  max={1}\n                  onChange={(v) => updateCelestial(\"celestialY\", v)}\n                  onReset={() =>\n                    updateCelestial(\n                      \"celestialY\",\n                      baseParams.celestial.celestialY,\n                    )\n                  }\n                />\n                <ParameterRow\n                  label=\"Sun Size\"\n                  value={params.celestial.sunSize}\n                  baseValue={baseParams.celestial.sunSize}\n                  min={0.01}\n                  max={0.8}\n                  onChange={(v) => updateCelestial(\"sunSize\", v)}\n                  onReset={() =>\n                    updateCelestial(\"sunSize\", baseParams.celestial.sunSize)\n                  }\n                />\n                <ParameterRow\n                  label=\"Moon Size\"\n                  value={params.celestial.moonSize}\n                  baseValue={baseParams.celestial.moonSize}\n                  min={0.01}\n                  max={0.6}\n                  onChange={(v) => updateCelestial(\"moonSize\", v)}\n                  onReset={() =>\n                    updateCelestial(\"moonSize\", baseParams.celestial.moonSize)\n                  }\n                />\n                <ParameterRow\n                  label=\"Sun Glow Intensity\"\n                  value={params.celestial.sunGlowIntensity}\n                  baseValue={baseParams.celestial.sunGlowIntensity}\n                  min={0}\n                  max={5}\n                  onChange={(v) => updateCelestial(\"sunGlowIntensity\", v)}\n                  onReset={() =>\n                    updateCelestial(\n                      \"sunGlowIntensity\",\n                      baseParams.celestial.sunGlowIntensity,\n                    )\n                  }\n                />\n                <ParameterRow\n                  label=\"Sun Glow Size\"\n                  value={params.celestial.sunGlowSize}\n                  baseValue={baseParams.celestial.sunGlowSize}\n                  min={0.05}\n                  max={2}\n                  onChange={(v) => updateCelestial(\"sunGlowSize\", v)}\n                  onReset={() =>\n                    updateCelestial(\n                      \"sunGlowSize\",\n                      baseParams.celestial.sunGlowSize,\n                    )\n                  }\n                />\n                <ParameterRow\n                  label=\"Sun Ray Count\"\n                  value={params.celestial.sunRayCount}\n                  baseValue={baseParams.celestial.sunRayCount}\n                  min={0}\n                  max={48}\n                  step={1}\n                  onChange={(v) => updateCelestial(\"sunRayCount\", v)}\n                  onReset={() =>\n                    updateCelestial(\n                      \"sunRayCount\",\n                      baseParams.celestial.sunRayCount,\n                    )\n                  }\n                />\n                <ParameterRow\n                  label=\"Sun Ray Length\"\n                  value={params.celestial.sunRayLength}\n                  baseValue={baseParams.celestial.sunRayLength}\n                  min={0}\n                  max={3}\n                  onChange={(v) => updateCelestial(\"sunRayLength\", v)}\n                  onReset={() =>\n                    updateCelestial(\n                      \"sunRayLength\",\n                      baseParams.celestial.sunRayLength,\n                    )\n                  }\n                />\n                <ParameterRow\n                  label=\"Sun Ray Intensity\"\n                  value={params.celestial.sunRayIntensity}\n                  baseValue={baseParams.celestial.sunRayIntensity}\n                  min={0}\n                  max={3}\n                  onChange={(v) => updateCelestial(\"sunRayIntensity\", v)}\n                  onReset={() =>\n                    updateCelestial(\n                      \"sunRayIntensity\",\n                      baseParams.celestial.sunRayIntensity,\n                    )\n                  }\n                />\n                <ParameterRow\n                  label=\"Sun Ray Shimmer\"\n                  value={params.celestial.sunRayShimmer}\n                  baseValue={baseParams.celestial.sunRayShimmer}\n                  min={0}\n                  max={5}\n                  step={0.05}\n                  onChange={(v) => updateCelestial(\"sunRayShimmer\", v)}\n                  onReset={() =>\n                    updateCelestial(\n                      \"sunRayShimmer\",\n                      baseParams.celestial.sunRayShimmer,\n                    )\n                  }\n                />\n                <ParameterRow\n                  label=\"Sun Ray Shimmer Speed\"\n                  value={params.celestial.sunRayShimmerSpeed}\n                  baseValue={baseParams.celestial.sunRayShimmerSpeed}\n                  min={0}\n                  max={5}\n                  step={0.05}\n                  onChange={(v) => updateCelestial(\"sunRayShimmerSpeed\", v)}\n                  onReset={() =>\n                    updateCelestial(\n                      \"sunRayShimmerSpeed\",\n                      baseParams.celestial.sunRayShimmerSpeed,\n                    )\n                  }\n                />\n                <ParameterRow\n                  label=\"Moon Glow Intensity\"\n                  value={params.celestial.moonGlowIntensity}\n                  baseValue={baseParams.celestial.moonGlowIntensity}\n                  min={0}\n                  max={5}\n                  onChange={(v) => updateCelestial(\"moonGlowIntensity\", v)}\n                  onReset={() =>\n                    updateCelestial(\n                      \"moonGlowIntensity\",\n                      baseParams.celestial.moonGlowIntensity,\n                    )\n                  }\n                />\n                <ParameterRow\n                  label=\"Moon Glow Size\"\n                  value={params.celestial.moonGlowSize}\n                  baseValue={baseParams.celestial.moonGlowSize}\n                  min={0.05}\n                  max={1.5}\n                  onChange={(v) => updateCelestial(\"moonGlowSize\", v)}\n                  onReset={() =>\n                    updateCelestial(\n                      \"moonGlowSize\",\n                      baseParams.celestial.moonGlowSize,\n                    )\n                  }\n                />\n                <ParameterRow\n                  label=\"Sky Brightness\"\n                  value={params.celestial.skyBrightness}\n                  baseValue={baseParams.celestial.skyBrightness}\n                  min={0}\n                  max={2}\n                  onChange={(v) => updateCelestial(\"skyBrightness\", v)}\n                  onReset={() =>\n                    updateCelestial(\n                      \"skyBrightness\",\n                      baseParams.celestial.skyBrightness,\n                    )\n                  }\n                />\n                <ParameterRow\n                  label=\"Sky Saturation\"\n                  value={params.celestial.skySaturation}\n                  baseValue={baseParams.celestial.skySaturation}\n                  min={0}\n                  max={2}\n                  onChange={(v) => updateCelestial(\"skySaturation\", v)}\n                  onReset={() =>\n                    updateCelestial(\n                      \"skySaturation\",\n                      baseParams.celestial.skySaturation,\n                    )\n                  }\n                />\n                <ParameterRow\n                  label=\"Sky Contrast\"\n                  value={params.celestial.skyContrast}\n                  baseValue={baseParams.celestial.skyContrast}\n                  min={0}\n                  max={1}\n                  onChange={(v) => updateCelestial(\"skyContrast\", v)}\n                  onReset={() =>\n                    updateCelestial(\n                      \"skyContrast\",\n                      baseParams.celestial.skyContrast,\n                    )\n                  }\n                />\n              </div>\n            </AccordionContent>\n          </AccordionItem>\n        )}\n\n        {params.layers.clouds && (\n          <AccordionItem value=\"cloud\" className=\"border-border/30\">\n            <AccordionTrigger className=\"text-muted-foreground hover:text-foreground py-2.5 text-xs hover:no-underline [&[data-state=open]>svg]:rotate-180\">\n              <div className=\"flex flex-1 items-center justify-between\">\n                <div className=\"flex items-center gap-2\">\n                  <div className=\"flex size-5 items-center justify-center rounded bg-linear-to-br from-slate-400 to-slate-500\">\n                    <Cloud className=\"size-3 text-white\" />\n                  </div>\n                  <span>Clouds</span>\n                  <DeltaBadge\n                    count={countChanges(params.cloud, baseParams.cloud)}\n                  />\n                </div>\n                {currentCondition && onCopyLayer && (\n                  <CopyToDropdown\n                    layerKey=\"cloud\"\n                    layerLabel=\"Cloud\"\n                    currentCondition={currentCondition}\n                    onCopy={(target) => onCopyLayer(target, \"cloud\")}\n                    onCopyToAll={\n                      onCopyLayerToAll\n                        ? () => onCopyLayerToAll(\"cloud\")\n                        : undefined\n                    }\n                  />\n                )}\n              </div>\n            </AccordionTrigger>\n            <AccordionContent>\n              <div className=\"space-y-1\">\n                <ParameterRow\n                  label=\"Coverage\"\n                  value={params.cloud.coverage}\n                  baseValue={baseParams.cloud.coverage}\n                  min={0}\n                  max={1}\n                  onChange={(v) => updateCloud(\"coverage\", v)}\n                  onReset={() =>\n                    updateCloud(\"coverage\", baseParams.cloud.coverage)\n                  }\n                />\n                <ParameterRow\n                  label=\"Density\"\n                  value={params.cloud.density}\n                  baseValue={baseParams.cloud.density}\n                  min={0}\n                  max={1.5}\n                  onChange={(v) => updateCloud(\"density\", v)}\n                  onReset={() =>\n                    updateCloud(\"density\", baseParams.cloud.density)\n                  }\n                />\n                <ParameterRow\n                  label=\"Softness\"\n                  value={params.cloud.softness}\n                  baseValue={baseParams.cloud.softness}\n                  min={0}\n                  max={1}\n                  onChange={(v) => updateCloud(\"softness\", v)}\n                  onReset={() =>\n                    updateCloud(\"softness\", baseParams.cloud.softness)\n                  }\n                />\n                <ParameterRow\n                  label=\"Cloud Scale\"\n                  value={params.cloud.cloudScale}\n                  baseValue={baseParams.cloud.cloudScale}\n                  min={0.5}\n                  max={5}\n                  onChange={(v) => updateCloud(\"cloudScale\", v)}\n                  onReset={() =>\n                    updateCloud(\"cloudScale\", baseParams.cloud.cloudScale)\n                  }\n                />\n                <ParameterRow\n                  label=\"Wind Speed\"\n                  value={params.cloud.windSpeed}\n                  baseValue={baseParams.cloud.windSpeed}\n                  min={0}\n                  max={2}\n                  onChange={(v) => updateCloud(\"windSpeed\", v)}\n                  onReset={() =>\n                    updateCloud(\"windSpeed\", baseParams.cloud.windSpeed)\n                  }\n                />\n                <ParameterRow\n                  label=\"Wind Angle\"\n                  value={params.cloud.windAngle}\n                  baseValue={baseParams.cloud.windAngle}\n                  min={-Math.PI}\n                  max={Math.PI}\n                  step={0.1}\n                  onChange={(v) => updateCloud(\"windAngle\", v)}\n                  onReset={() =>\n                    updateCloud(\"windAngle\", baseParams.cloud.windAngle)\n                  }\n                />\n                <ParameterRow\n                  label=\"Turbulence\"\n                  value={params.cloud.turbulence}\n                  baseValue={baseParams.cloud.turbulence}\n                  min={0}\n                  max={1}\n                  onChange={(v) => updateCloud(\"turbulence\", v)}\n                  onReset={() =>\n                    updateCloud(\"turbulence\", baseParams.cloud.turbulence)\n                  }\n                />\n                <ParameterRow\n                  label=\"Light Intensity\"\n                  value={params.cloud.lightIntensity}\n                  baseValue={baseParams.cloud.lightIntensity}\n                  min={0}\n                  max={2}\n                  onChange={(v) => updateCloud(\"lightIntensity\", v)}\n                  onReset={() =>\n                    updateCloud(\n                      \"lightIntensity\",\n                      baseParams.cloud.lightIntensity,\n                    )\n                  }\n                />\n                <ParameterRow\n                  label=\"Ambient Darkness\"\n                  value={params.cloud.ambientDarkness}\n                  baseValue={baseParams.cloud.ambientDarkness}\n                  min={0}\n                  max={1}\n                  onChange={(v) => updateCloud(\"ambientDarkness\", v)}\n                  onReset={() =>\n                    updateCloud(\n                      \"ambientDarkness\",\n                      baseParams.cloud.ambientDarkness,\n                    )\n                  }\n                />\n                <ParameterRow\n                  label=\"Backlight Intensity\"\n                  value={params.cloud.backlightIntensity}\n                  baseValue={baseParams.cloud.backlightIntensity}\n                  min={0}\n                  max={2}\n                  onChange={(v) => updateCloud(\"backlightIntensity\", v)}\n                  onReset={() =>\n                    updateCloud(\n                      \"backlightIntensity\",\n                      baseParams.cloud.backlightIntensity,\n                    )\n                  }\n                />\n                <ParameterRow\n                  label=\"Num Layers\"\n                  value={params.cloud.numLayers}\n                  baseValue={baseParams.cloud.numLayers}\n                  min={1}\n                  max={6}\n                  step={1}\n                  onChange={(v) => updateCloud(\"numLayers\", v)}\n                  onReset={() =>\n                    updateCloud(\"numLayers\", baseParams.cloud.numLayers)\n                  }\n                />\n              </div>\n            </AccordionContent>\n          </AccordionItem>\n        )}\n\n        {params.layers.rain && (\n          <AccordionItem value=\"rain\" className=\"border-border/30\">\n            <AccordionTrigger className=\"text-muted-foreground hover:text-foreground py-2.5 text-xs hover:no-underline [&[data-state=open]>svg]:rotate-180\">\n              <div className=\"flex flex-1 items-center justify-between\">\n                <div className=\"flex items-center gap-2\">\n                  <div className=\"flex size-5 items-center justify-center rounded bg-linear-to-br from-blue-500 to-cyan-500\">\n                    <CloudRain className=\"size-3 text-white\" />\n                  </div>\n                  <span>Rain</span>\n                  <DeltaBadge\n                    count={countChanges(params.rain, baseParams.rain)}\n                  />\n                </div>\n                {currentCondition && onCopyLayer && (\n                  <CopyToDropdown\n                    layerKey=\"rain\"\n                    layerLabel=\"Rain\"\n                    currentCondition={currentCondition}\n                    onCopy={(target) => onCopyLayer(target, \"rain\")}\n                    onCopyToAll={\n                      onCopyLayerToAll\n                        ? () => onCopyLayerToAll(\"rain\")\n                        : undefined\n                    }\n                  />\n                )}\n              </div>\n            </AccordionTrigger>\n            <AccordionContent>\n              <div className=\"space-y-1\">\n                <ParameterRow\n                  label=\"Glass Intensity\"\n                  value={params.rain.glassIntensity}\n                  baseValue={baseParams.rain.glassIntensity}\n                  min={RAIN_PARAM_LIMITS.glassIntensity.min}\n                  max={RAIN_PARAM_LIMITS.glassIntensity.max}\n                  onChange={(v) => updateRain(\"glassIntensity\", v)}\n                  onReset={() =>\n                    updateRain(\"glassIntensity\", baseParams.rain.glassIntensity)\n                  }\n                />\n                <ParameterRow\n                  label=\"Zoom\"\n                  value={params.rain.zoom}\n                  baseValue={baseParams.rain.zoom}\n                  min={RAIN_PARAM_LIMITS.zoom.min}\n                  max={RAIN_PARAM_LIMITS.zoom.max}\n                  onChange={(v) => updateRain(\"zoom\", v)}\n                  onReset={() => updateRain(\"zoom\", baseParams.rain.zoom)}\n                />\n                <ParameterRow\n                  label=\"Falling Intensity\"\n                  value={params.rain.fallingIntensity}\n                  baseValue={baseParams.rain.fallingIntensity}\n                  min={RAIN_PARAM_LIMITS.fallingIntensity.min}\n                  max={RAIN_PARAM_LIMITS.fallingIntensity.max}\n                  onChange={(v) => updateRain(\"fallingIntensity\", v)}\n                  onReset={() =>\n                    updateRain(\n                      \"fallingIntensity\",\n                      baseParams.rain.fallingIntensity,\n                    )\n                  }\n                />\n                <ParameterRow\n                  label=\"Falling Speed\"\n                  value={params.rain.fallingSpeed}\n                  baseValue={baseParams.rain.fallingSpeed}\n                  min={RAIN_PARAM_LIMITS.fallingSpeed.min}\n                  max={RAIN_PARAM_LIMITS.fallingSpeed.max}\n                  onChange={(v) => updateRain(\"fallingSpeed\", v)}\n                  onReset={() =>\n                    updateRain(\"fallingSpeed\", baseParams.rain.fallingSpeed)\n                  }\n                />\n                <ParameterRow\n                  label=\"Falling Angle\"\n                  value={params.rain.fallingAngle}\n                  baseValue={baseParams.rain.fallingAngle}\n                  min={RAIN_PARAM_LIMITS.fallingAngle.min}\n                  max={RAIN_PARAM_LIMITS.fallingAngle.max}\n                  onChange={(v) => updateRain(\"fallingAngle\", v)}\n                  onReset={() =>\n                    updateRain(\"fallingAngle\", baseParams.rain.fallingAngle)\n                  }\n                />\n                <ParameterRow\n                  label=\"Streak Length\"\n                  value={params.rain.fallingStreakLength}\n                  baseValue={baseParams.rain.fallingStreakLength}\n                  min={RAIN_PARAM_LIMITS.fallingStreakLength.min}\n                  max={RAIN_PARAM_LIMITS.fallingStreakLength.max}\n                  onChange={(v) => updateRain(\"fallingStreakLength\", v)}\n                  onReset={() =>\n                    updateRain(\n                      \"fallingStreakLength\",\n                      baseParams.rain.fallingStreakLength,\n                    )\n                  }\n                />\n                <ParameterRow\n                  label=\"Falling Layers\"\n                  value={params.rain.fallingLayers}\n                  baseValue={baseParams.rain.fallingLayers}\n                  min={RAIN_PARAM_LIMITS.fallingLayers.min}\n                  max={RAIN_PARAM_LIMITS.fallingLayers.max}\n                  step={1}\n                  onChange={(v) => updateRain(\"fallingLayers\", v)}\n                  onReset={() =>\n                    updateRain(\"fallingLayers\", baseParams.rain.fallingLayers)\n                  }\n                />\n              </div>\n            </AccordionContent>\n          </AccordionItem>\n        )}\n\n        {params.layers.lightning && (\n          <AccordionItem value=\"lightning\" className=\"border-border/30\">\n            <AccordionTrigger className=\"text-muted-foreground hover:text-foreground py-2.5 text-xs hover:no-underline [&[data-state=open]>svg]:rotate-180\">\n              <div className=\"flex flex-1 items-center justify-between\">\n                <div className=\"flex items-center gap-2\">\n                  <div className=\"flex size-5 items-center justify-center rounded bg-linear-to-br from-purple-500 to-indigo-500\">\n                    <Zap className=\"size-3 text-white\" />\n                  </div>\n                  <span>Lightning</span>\n                  <DeltaBadge\n                    count={countChanges(params.lightning, baseParams.lightning)}\n                  />\n                </div>\n                {currentCondition && onCopyLayer && (\n                  <CopyToDropdown\n                    layerKey=\"lightning\"\n                    layerLabel=\"Lightning\"\n                    currentCondition={currentCondition}\n                    onCopy={(target) => onCopyLayer(target, \"lightning\")}\n                    onCopyToAll={\n                      onCopyLayerToAll\n                        ? () => onCopyLayerToAll(\"lightning\")\n                        : undefined\n                    }\n                  />\n                )}\n              </div>\n            </AccordionTrigger>\n            <AccordionContent>\n              <div className=\"space-y-1\">\n                <ParameterToggleRow\n                  label=\"Auto Mode\"\n                  value={params.lightning.autoMode}\n                  baseValue={baseParams.lightning.autoMode}\n                  onChange={(v) => updateLightning(\"autoMode\", v)}\n                  onReset={() =>\n                    updateLightning(\"autoMode\", baseParams.lightning.autoMode)\n                  }\n                />\n                <ParameterRow\n                  label=\"Auto Interval\"\n                  value={params.lightning.autoInterval}\n                  baseValue={baseParams.lightning.autoInterval}\n                  min={1}\n                  max={30}\n                  step={0.5}\n                  onChange={(v) => updateLightning(\"autoInterval\", v)}\n                  onReset={() =>\n                    updateLightning(\n                      \"autoInterval\",\n                      baseParams.lightning.autoInterval,\n                    )\n                  }\n                />\n                <ParameterRow\n                  label=\"Branch Density\"\n                  value={params.lightning.branchDensity}\n                  baseValue={baseParams.lightning.branchDensity}\n                  min={0}\n                  max={1}\n                  onChange={(v) => updateLightning(\"branchDensity\", v)}\n                  onReset={() =>\n                    updateLightning(\n                      \"branchDensity\",\n                      baseParams.lightning.branchDensity,\n                    )\n                  }\n                />\n                <ParameterRow\n                  label=\"Glow Intensity\"\n                  value={params.lightning.glowIntensity}\n                  baseValue={baseParams.lightning.glowIntensity}\n                  min={0}\n                  max={2}\n                  onChange={(v) => updateLightning(\"glowIntensity\", v)}\n                  onReset={() =>\n                    updateLightning(\n                      \"glowIntensity\",\n                      baseParams.lightning.glowIntensity,\n                    )\n                  }\n                />\n                <ParameterRow\n                  label=\"Flash Duration\"\n                  value={params.lightning.flashDuration}\n                  baseValue={baseParams.lightning.flashDuration}\n                  min={0.05}\n                  max={0.5}\n                  onChange={(v) => updateLightning(\"flashDuration\", v)}\n                  onReset={() =>\n                    updateLightning(\n                      \"flashDuration\",\n                      baseParams.lightning.flashDuration,\n                    )\n                  }\n                />\n                <ParameterRow\n                  label=\"Scene Illumination\"\n                  value={params.lightning.sceneIllumination}\n                  baseValue={baseParams.lightning.sceneIllumination}\n                  min={0}\n                  max={1}\n                  onChange={(v) => updateLightning(\"sceneIllumination\", v)}\n                  onReset={() =>\n                    updateLightning(\n                      \"sceneIllumination\",\n                      baseParams.lightning.sceneIllumination,\n                    )\n                  }\n                />\n              </div>\n            </AccordionContent>\n          </AccordionItem>\n        )}\n\n        {params.layers.snow && (\n          <AccordionItem value=\"snow\" className=\"border-border/30\">\n            <AccordionTrigger className=\"text-muted-foreground hover:text-foreground py-2.5 text-xs hover:no-underline [&[data-state=open]>svg]:rotate-180\">\n              <div className=\"flex flex-1 items-center justify-between\">\n                <div className=\"flex items-center gap-2\">\n                  <div className=\"flex size-5 items-center justify-center rounded bg-linear-to-br from-slate-200 to-blue-300\">\n                    <Snowflake className=\"size-3 text-slate-700\" />\n                  </div>\n                  <span>Snow</span>\n                  <DeltaBadge\n                    count={countChanges(params.snow, baseParams.snow)}\n                  />\n                </div>\n                {currentCondition && onCopyLayer && (\n                  <CopyToDropdown\n                    layerKey=\"snow\"\n                    layerLabel=\"Snow\"\n                    currentCondition={currentCondition}\n                    onCopy={(target) => onCopyLayer(target, \"snow\")}\n                    onCopyToAll={\n                      onCopyLayerToAll\n                        ? () => onCopyLayerToAll(\"snow\")\n                        : undefined\n                    }\n                  />\n                )}\n              </div>\n            </AccordionTrigger>\n            <AccordionContent>\n              <div className=\"space-y-1\">\n                <ParameterRow\n                  label=\"Intensity\"\n                  value={params.snow.intensity}\n                  baseValue={baseParams.snow.intensity}\n                  min={0}\n                  max={1}\n                  onChange={(v) => updateSnow(\"intensity\", v)}\n                  onReset={() =>\n                    updateSnow(\"intensity\", baseParams.snow.intensity)\n                  }\n                />\n                <ParameterRow\n                  label=\"Layers\"\n                  value={params.snow.layers}\n                  baseValue={baseParams.snow.layers}\n                  min={1}\n                  max={8}\n                  step={1}\n                  onChange={(v) => updateSnow(\"layers\", v)}\n                  onReset={() => updateSnow(\"layers\", baseParams.snow.layers)}\n                />\n                <ParameterRow\n                  label=\"Fall Speed\"\n                  value={params.snow.fallSpeed}\n                  baseValue={baseParams.snow.fallSpeed}\n                  min={0.1}\n                  max={SNOW_FALL_SPEED_MAX}\n                  onChange={(v) => updateSnow(\"fallSpeed\", v)}\n                  onReset={() =>\n                    updateSnow(\"fallSpeed\", baseParams.snow.fallSpeed)\n                  }\n                />\n                <ParameterRow\n                  label=\"Wind Speed\"\n                  value={params.snow.windSpeed}\n                  baseValue={baseParams.snow.windSpeed}\n                  min={0}\n                  max={2}\n                  onChange={(v) => updateSnow(\"windSpeed\", v)}\n                  onReset={() =>\n                    updateSnow(\"windSpeed\", baseParams.snow.windSpeed)\n                  }\n                />\n                <ParameterRow\n                  label=\"Drift\"\n                  value={params.snow.drift}\n                  baseValue={baseParams.snow.drift}\n                  min={0}\n                  max={1}\n                  onChange={(v) => updateSnow(\"drift\", v)}\n                  onReset={() => updateSnow(\"drift\", baseParams.snow.drift)}\n                />\n                <ParameterRow\n                  label=\"Flake Size\"\n                  value={params.snow.flakeSize}\n                  baseValue={baseParams.snow.flakeSize}\n                  min={0.5}\n                  max={3}\n                  onChange={(v) => updateSnow(\"flakeSize\", v)}\n                  onReset={() =>\n                    updateSnow(\"flakeSize\", baseParams.snow.flakeSize)\n                  }\n                />\n              </div>\n            </AccordionContent>\n          </AccordionItem>\n        )}\n\n        <AccordionItem value=\"post\" className=\"border-border/30\">\n          <AccordionTrigger className=\"text-muted-foreground hover:text-foreground py-2.5 text-xs hover:no-underline [&[data-state=open]>svg]:rotate-180\">\n            <div className=\"flex flex-1 items-center justify-between\">\n              <div className=\"flex items-center gap-2\">\n                <div className=\"flex size-5 items-center justify-center rounded bg-linear-to-br from-violet-500 to-fuchsia-500\">\n                  <Sparkles className=\"size-3 text-white\" />\n                </div>\n                <span>Atmosphere</span>\n                <DeltaBadge\n                  count={countChanges(params.post, baseParams.post)}\n                />\n              </div>\n              {currentCondition && onCopyLayer && (\n                <CopyToDropdown\n                  layerKey=\"post\"\n                  layerLabel=\"Atmosphere\"\n                  currentCondition={currentCondition}\n                  onCopy={(target) => onCopyLayer(target, \"post\")}\n                  onCopyToAll={\n                    onCopyLayerToAll\n                      ? () => onCopyLayerToAll(\"post\")\n                      : undefined\n                  }\n                />\n              )}\n            </div>\n          </AccordionTrigger>\n          <AccordionContent>\n            <div className=\"space-y-1\">\n              <ParameterToggleRow\n                label=\"Enabled\"\n                value={params.post.enabled}\n                baseValue={baseParams.post.enabled}\n                onChange={(v) => updatePost(\"enabled\", v)}\n                onReset={() => updatePost(\"enabled\", baseParams.post.enabled)}\n              />\n\n              <ParameterRow\n                label=\"Haze\"\n                value={params.post.haze}\n                baseValue={baseParams.post.haze}\n                min={0}\n                max={1}\n                onChange={(v) => updatePost(\"haze\", v)}\n                onReset={() => updatePost(\"haze\", baseParams.post.haze)}\n              />\n              <ParameterRow\n                label=\"Haze Horizon\"\n                value={params.post.hazeHorizon}\n                baseValue={baseParams.post.hazeHorizon}\n                min={0}\n                max={1}\n                onChange={(v) => updatePost(\"hazeHorizon\", v)}\n                onReset={() =>\n                  updatePost(\"hazeHorizon\", baseParams.post.hazeHorizon)\n                }\n              />\n              <ParameterRow\n                label=\"Haze Desaturation\"\n                value={params.post.hazeDesaturation}\n                baseValue={baseParams.post.hazeDesaturation}\n                min={0}\n                max={1}\n                onChange={(v) => updatePost(\"hazeDesaturation\", v)}\n                onReset={() =>\n                  updatePost(\n                    \"hazeDesaturation\",\n                    baseParams.post.hazeDesaturation,\n                  )\n                }\n              />\n              <ParameterRow\n                label=\"Haze Contrast\"\n                value={params.post.hazeContrast}\n                baseValue={baseParams.post.hazeContrast}\n                min={0}\n                max={1}\n                onChange={(v) => updatePost(\"hazeContrast\", v)}\n                onReset={() =>\n                  updatePost(\"hazeContrast\", baseParams.post.hazeContrast)\n                }\n              />\n\n              <div className=\"border-border/30 my-2 border-t\" />\n\n              <ParameterRow\n                label=\"Bloom Intensity\"\n                value={params.post.bloomIntensity}\n                baseValue={baseParams.post.bloomIntensity}\n                min={0}\n                max={1}\n                onChange={(v) => updatePost(\"bloomIntensity\", v)}\n                onReset={() =>\n                  updatePost(\"bloomIntensity\", baseParams.post.bloomIntensity)\n                }\n              />\n              <ParameterRow\n                label=\"Bloom Threshold\"\n                value={params.post.bloomThreshold}\n                baseValue={baseParams.post.bloomThreshold}\n                min={0}\n                max={1}\n                onChange={(v) => updatePost(\"bloomThreshold\", v)}\n                onReset={() =>\n                  updatePost(\"bloomThreshold\", baseParams.post.bloomThreshold)\n                }\n              />\n              <ParameterRow\n                label=\"Bloom Knee\"\n                value={params.post.bloomKnee}\n                baseValue={baseParams.post.bloomKnee}\n                min={0}\n                max={1}\n                onChange={(v) => updatePost(\"bloomKnee\", v)}\n                onReset={() =>\n                  updatePost(\"bloomKnee\", baseParams.post.bloomKnee)\n                }\n              />\n              <ParameterRow\n                label=\"Bloom Radius\"\n                value={params.post.bloomRadius}\n                baseValue={baseParams.post.bloomRadius}\n                min={0}\n                max={6}\n                step={0.05}\n                onChange={(v) => updatePost(\"bloomRadius\", v)}\n                onReset={() =>\n                  updatePost(\"bloomRadius\", baseParams.post.bloomRadius)\n                }\n              />\n              <ParameterRow\n                label=\"Bloom Tap Scale\"\n                value={params.post.bloomTapScale}\n                baseValue={baseParams.post.bloomTapScale}\n                min={0.25}\n                max={3}\n                step={0.05}\n                onChange={(v) => updatePost(\"bloomTapScale\", v)}\n                onReset={() =>\n                  updatePost(\"bloomTapScale\", baseParams.post.bloomTapScale)\n                }\n              />\n\n              <div className=\"border-border/30 my-2 border-t\" />\n\n              <p className=\"text-muted-foreground/40 px-1.5 py-1 text-[10px] italic\">\n                Lightning flash response — requires lightning enabled\n              </p>\n              <ParameterRow\n                label=\"Exposure Intensity\"\n                value={params.post.exposureIntensity}\n                baseValue={baseParams.post.exposureIntensity}\n                min={0}\n                max={2}\n                onChange={(v) => updatePost(\"exposureIntensity\", v)}\n                onReset={() =>\n                  updatePost(\n                    \"exposureIntensity\",\n                    baseParams.post.exposureIntensity,\n                  )\n                }\n              />\n              <ParameterRow\n                label=\"Exposure Desaturation\"\n                value={params.post.exposureDesaturation}\n                baseValue={baseParams.post.exposureDesaturation}\n                min={0}\n                max={1}\n                onChange={(v) => updatePost(\"exposureDesaturation\", v)}\n                onReset={() =>\n                  updatePost(\n                    \"exposureDesaturation\",\n                    baseParams.post.exposureDesaturation,\n                  )\n                }\n              />\n              <ParameterRow\n                label=\"Exposure Recovery\"\n                value={params.post.exposureRecovery}\n                baseValue={baseParams.post.exposureRecovery}\n                min={0.1}\n                max={3}\n                step={0.05}\n                onChange={(v) => updatePost(\"exposureRecovery\", v)}\n                onReset={() =>\n                  updatePost(\n                    \"exposureRecovery\",\n                    baseParams.post.exposureRecovery,\n                  )\n                }\n              />\n\n              <div className=\"border-border/30 my-2 border-t\" />\n\n              <p className=\"text-muted-foreground/40 px-1.5 py-1 text-[10px] italic\">\n                Crepuscular rays — only visible at dawn &amp; dusk\n              </p>\n              <ParameterRow\n                label=\"God Rays Intensity\"\n                value={params.post.godRayIntensity}\n                baseValue={baseParams.post.godRayIntensity}\n                min={0}\n                max={2}\n                onChange={(v) => updatePost(\"godRayIntensity\", v)}\n                onReset={() =>\n                  updatePost(\"godRayIntensity\", baseParams.post.godRayIntensity)\n                }\n              />\n              <ParameterRow\n                label=\"God Rays Decay\"\n                value={params.post.godRayDecay}\n                baseValue={baseParams.post.godRayDecay}\n                min={0.9}\n                max={0.999}\n                step={0.001}\n                onChange={(v) => updatePost(\"godRayDecay\", v)}\n                onReset={() =>\n                  updatePost(\"godRayDecay\", baseParams.post.godRayDecay)\n                }\n              />\n              <ParameterRow\n                label=\"God Rays Density\"\n                value={params.post.godRayDensity}\n                baseValue={baseParams.post.godRayDensity}\n                min={0.2}\n                max={1.4}\n                step={0.01}\n                onChange={(v) => updatePost(\"godRayDensity\", v)}\n                onReset={() =>\n                  updatePost(\"godRayDensity\", baseParams.post.godRayDensity)\n                }\n              />\n              <ParameterRow\n                label=\"God Rays Weight\"\n                value={params.post.godRayWeight}\n                baseValue={baseParams.post.godRayWeight}\n                min={0}\n                max={1}\n                step={0.01}\n                onChange={(v) => updatePost(\"godRayWeight\", v)}\n                onReset={() =>\n                  updatePost(\"godRayWeight\", baseParams.post.godRayWeight)\n                }\n              />\n              <ParameterRow\n                label=\"God Rays Samples\"\n                value={params.post.godRaySamples}\n                baseValue={baseParams.post.godRaySamples}\n                min={0}\n                max={32}\n                step={1}\n                onChange={(v) => updatePost(\"godRaySamples\", v)}\n                onReset={() =>\n                  updatePost(\"godRaySamples\", baseParams.post.godRaySamples)\n                }\n              />\n            </div>\n          </AccordionContent>\n        </AccordionItem>\n      </Accordion>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/components/parameter-row.tsx",
    "content": "\"use client\";\n\nimport { useState, useRef, useEffect } from \"react\";\nimport { RotateCcw } from \"lucide-react\";\nimport { Slider } from \"@/components/ui/slider\";\nimport { cn } from \"@/lib/ui/cn\";\n\ninterface ParameterRowProps {\n  label: string;\n  value: number;\n  baseValue: number;\n  min: number;\n  max: number;\n  step?: number;\n  onChange: (value: number) => void;\n  onReset: () => void;\n}\n\nexport function ParameterRow({\n  label,\n  value,\n  baseValue,\n  min,\n  max,\n  step = 0.01,\n  onChange,\n  onReset,\n}: ParameterRowProps) {\n  const [isEditing, setIsEditing] = useState(false);\n  const [editValue, setEditValue] = useState(\"\");\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  const isChanged = Math.abs(value - baseValue) > 0.001;\n  const displayValue = value.toFixed(2);\n\n  useEffect(() => {\n    if (isEditing && inputRef.current) {\n      inputRef.current.focus();\n      inputRef.current.select();\n    }\n  }, [isEditing]);\n\n  const startEditing = () => {\n    setEditValue(displayValue);\n    setIsEditing(true);\n  };\n\n  const commitEdit = () => {\n    const parsed = parseFloat(editValue);\n    if (!isNaN(parsed)) {\n      const clamped = Math.max(min, Math.min(max, parsed));\n      onChange(clamped);\n    }\n    setIsEditing(false);\n  };\n\n  const cancelEdit = () => {\n    setIsEditing(false);\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n    if (e.key === \"Enter\") {\n      e.preventDefault();\n      commitEdit();\n    } else if (e.key === \"Escape\") {\n      e.preventDefault();\n      cancelEdit();\n    } else if (e.key === \"ArrowUp\") {\n      e.preventDefault();\n      const increment = e.shiftKey ? step * 10 : step;\n      const newValue = Math.min(max, parseFloat(editValue) + increment);\n      setEditValue(newValue.toFixed(2));\n    } else if (e.key === \"ArrowDown\") {\n      e.preventDefault();\n      const decrement = e.shiftKey ? step * 10 : step;\n      const newValue = Math.max(min, parseFloat(editValue) - decrement);\n      setEditValue(newValue.toFixed(2));\n    }\n  };\n\n  return (\n    <div className=\"group flex items-center gap-2.5 rounded px-1.5 py-1.5 transition-colors hover:bg-accent/30\">\n      <div className=\"w-28 shrink-0\">\n        <span\n          className={cn(\n            \"text-[11px] transition-colors\",\n            isChanged ? \"text-foreground/80\" : \"text-muted-foreground/60\",\n          )}\n        >\n          {label}\n        </span>\n      </div>\n\n      <div className=\"relative flex-1\">\n        {isChanged && (\n          <div\n            className=\"absolute top-1/2 h-1 w-1 -translate-y-1/2 rounded-full bg-muted-foreground/30\"\n            style={{\n              left: `${((baseValue - min) / (max - min)) * 100}%`,\n            }}\n            title={`Base: ${baseValue.toFixed(2)}`}\n          />\n        )}\n        <Slider\n          value={[value]}\n          onValueChange={([v]) => onChange(v)}\n          min={min}\n          max={max}\n          step={step}\n          className=\"relative w-full [&_[data-slot=slider-track]]:h-0.5 [&_[data-slot=slider-range]]:bg-foreground/20\"\n        />\n      </div>\n\n      <div className=\"flex w-16 shrink-0 items-center justify-end gap-1\">\n        {isEditing ? (\n          <input\n            ref={inputRef}\n            type=\"text\"\n            inputMode=\"decimal\"\n            value={editValue}\n            onChange={(e) => setEditValue(e.target.value)}\n            onKeyDown={handleKeyDown}\n            onBlur={commitEdit}\n            aria-label={`${label} value`}\n            className={cn(\n              \"w-12 rounded border border-blue-500/50 bg-blue-500/10 px-1 py-0.5\",\n              \"font-mono text-[10px] tabular-nums text-blue-600 dark:text-blue-400\",\n              \"outline-none ring-1 ring-blue-500/30\",\n            )}\n          />\n        ) : (\n          <button\n            type=\"button\"\n            onClick={startEditing}\n            className={cn(\n              \"rounded px-1 py-0.5 font-mono text-[10px] tabular-nums transition-colors\",\n              \"hover:bg-accent hover:ring-1 hover:ring-border\",\n              \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n              isChanged\n                ? \"bg-amber-500/10 text-amber-600/80 dark:text-amber-400/80\"\n                : \"text-muted-foreground/40\",\n            )}\n            aria-label={`Edit ${label}: ${displayValue}`}\n          >\n            {displayValue}\n          </button>\n        )}\n        <button\n          type=\"button\"\n          onClick={onReset}\n          disabled={!isChanged}\n          className={cn(\n            \"flex size-4 items-center justify-center rounded transition-all\",\n            \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n            isChanged\n              ? \"text-muted-foreground/50 opacity-100 hover:bg-accent hover:text-muted-foreground\"\n              : \"pointer-events-none opacity-0\",\n          )}\n          aria-label={`Reset ${label} to ${baseValue.toFixed(2)}`}\n        >\n          <RotateCcw className=\"size-2.5\" aria-hidden=\"true\" />\n        </button>\n      </div>\n    </div>\n  );\n}\n\ninterface ParameterToggleRowProps {\n  label: string;\n  value: boolean;\n  baseValue: boolean;\n  onChange: (value: boolean) => void;\n  onReset: () => void;\n}\n\nexport function ParameterToggleRow({\n  label,\n  value,\n  baseValue,\n  onChange,\n  onReset,\n}: ParameterToggleRowProps) {\n  const isChanged = value !== baseValue;\n\n  return (\n    <div className=\"group flex items-center gap-2.5 rounded px-1.5 py-1.5 transition-colors hover:bg-accent/30\">\n      <div className=\"w-28 shrink-0\">\n        <span\n          className={cn(\n            \"text-[11px] transition-colors\",\n            isChanged ? \"text-foreground/80\" : \"text-muted-foreground/60\",\n          )}\n        >\n          {label}\n        </span>\n      </div>\n\n      <div className=\"flex-1\">\n        <button\n          type=\"button\"\n          role=\"switch\"\n          aria-checked={value}\n          aria-label={label}\n          onClick={() => onChange(!value)}\n          className={cn(\n            \"relative flex h-4 w-7 items-center rounded-full transition-colors\",\n            \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n            value ? \"bg-foreground/30\" : \"bg-muted/50\",\n          )}\n        >\n          <span\n            aria-hidden=\"true\"\n            className={cn(\n              \"absolute size-3 rounded-full bg-white shadow-sm transition-transform\",\n              value ? \"translate-x-3.5\" : \"translate-x-0.5\",\n            )}\n          />\n        </button>\n      </div>\n\n      <div className=\"flex w-16 shrink-0 items-center justify-end gap-1\">\n        <span\n          className={cn(\n            \"rounded px-1 py-0.5 text-[10px] transition-colors\",\n            value ? \"text-foreground/60\" : \"text-muted-foreground/40\",\n          )}\n        >\n          {value ? \"On\" : \"Off\"}\n        </span>\n        <button\n          type=\"button\"\n          onClick={onReset}\n          disabled={!isChanged}\n          className={cn(\n            \"flex size-4 items-center justify-center rounded transition-all\",\n            \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n            isChanged\n              ? \"text-muted-foreground/50 opacity-100 hover:bg-accent hover:text-muted-foreground\"\n              : \"pointer-events-none opacity-0\",\n          )}\n          aria-label={`Reset ${label} to ${baseValue ? \"On\" : \"Off\"}`}\n        >\n          <RotateCcw className=\"size-2.5\" aria-hidden=\"true\" />\n        </button>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/components/time-dial.tsx",
    "content": "\"use client\";\n\nimport { useRef, useCallback } from \"react\";\nimport { cn } from \"@/lib/ui/cn\";\nimport { TIME_CHECKPOINTS, TIME_CHECKPOINT_ORDER } from \"../lib/constants\";\nimport type { TimeCheckpoint } from \"../types\";\n\ninterface TimeDialProps {\n  value: number;\n  isPreviewing: boolean;\n  activeEditCheckpoint: TimeCheckpoint;\n  onScrub: (value: number) => void;\n  onCheckpointClick: (checkpoint: TimeCheckpoint) => void;\n  onExitPreview: () => void;\n}\n\nexport function TimeDial({\n  value,\n  isPreviewing,\n  activeEditCheckpoint,\n  onScrub,\n  onCheckpointClick,\n  onExitPreview,\n}: TimeDialProps) {\n  const dialRef = useRef<HTMLDivElement>(null);\n  const isDragging = useRef(false);\n\n  const formatTime = (timeOfDay: number): string => {\n    const hours = Math.floor(timeOfDay * 24);\n    const minutes = Math.floor((timeOfDay * 24 - hours) * 60);\n    const period = hours >= 12 ? \"PM\" : \"AM\";\n    const displayHours = hours % 12 || 12;\n    return `${displayHours}:${minutes.toString().padStart(2, \"0\")} ${period}`;\n  };\n\n  const getAngleFromValue = (v: number) => {\n    return v * 360 - 90;\n  };\n\n  const getValueFromAngle = (angle: number) => {\n    let normalized = (angle + 90) / 360;\n    if (normalized < 0) normalized += 1;\n    if (normalized >= 1) normalized -= 1;\n    return Math.max(0, Math.min(1, normalized));\n  };\n\n  const handleDial = useCallback(\n    (clientX: number, clientY: number) => {\n      if (!dialRef.current) return;\n\n      const rect = dialRef.current.getBoundingClientRect();\n      const centerX = rect.left + rect.width / 2;\n      const centerY = rect.top + rect.height / 2;\n\n      const angle =\n        Math.atan2(clientY - centerY, clientX - centerX) * (180 / Math.PI);\n      const newValue = getValueFromAngle(angle);\n      onScrub(newValue);\n    },\n    [onScrub],\n  );\n\n  const handleMouseDown = (e: React.MouseEvent) => {\n    isDragging.current = true;\n    handleDial(e.clientX, e.clientY);\n\n    const handleMouseMove = (e: MouseEvent) => {\n      if (isDragging.current) {\n        handleDial(e.clientX, e.clientY);\n      }\n    };\n\n    const handleMouseUp = () => {\n      isDragging.current = false;\n      window.removeEventListener(\"mousemove\", handleMouseMove);\n      window.removeEventListener(\"mouseup\", handleMouseUp);\n    };\n\n    window.addEventListener(\"mousemove\", handleMouseMove);\n    window.addEventListener(\"mouseup\", handleMouseUp);\n  };\n\n  const angle = getAngleFromValue(value);\n  const editCheckpointInfo = TIME_CHECKPOINTS[activeEditCheckpoint];\n\n  return (\n    <div className=\"flex items-center gap-6\">\n      <div className=\"relative\">\n        <div\n          ref={dialRef}\n          onMouseDown={handleMouseDown}\n          className=\"relative flex size-14 cursor-pointer items-center justify-center\"\n        >\n          <div className=\"absolute inset-0 rounded-full border border-border/40\" />\n\n          {TIME_CHECKPOINT_ORDER.map((checkpoint) => {\n            const { value: cpValue, label } = TIME_CHECKPOINTS[checkpoint];\n            const cpAngle = getAngleFromValue(cpValue);\n            const radians = (cpAngle * Math.PI) / 180;\n            const radius = 22;\n            const x = Math.cos(radians) * radius;\n            const y = Math.sin(radians) * radius;\n            const isViewingHere = Math.abs(value - cpValue) < 0.04;\n            const isEditingHere = checkpoint === activeEditCheckpoint;\n\n            return (\n              <button\n                key={checkpoint}\n                onClick={(e) => {\n                  e.stopPropagation();\n                  onCheckpointClick(checkpoint);\n                }}\n                className={cn(\n                  \"absolute flex size-2 items-center justify-center rounded-full transition-all\",\n                  isEditingHere\n                    ? \"scale-150 bg-foreground\"\n                    : isViewingHere\n                      ? \"scale-125 bg-foreground/60\"\n                      : \"bg-foreground/20 hover:bg-foreground/40\",\n                )}\n                style={{\n                  transform: `translate(${x}px, ${y}px)`,\n                }}\n                title={`${label}${isEditingHere ? \" (editing)\" : \"\"}`}\n              />\n            );\n          })}\n\n          <div\n            className=\"absolute left-1/2 top-1/2 h-px w-4 origin-left bg-foreground/50\"\n            style={{\n              transform: `translate(0, -50%) rotate(${angle}deg)`,\n            }}\n          />\n          <div className=\"absolute left-1/2 top-1/2 size-1 -translate-x-1/2 -translate-y-1/2 rounded-full bg-foreground\" />\n        </div>\n      </div>\n\n      <div className=\"flex flex-col items-start gap-0.5\">\n        <span className=\"font-mono text-sm tabular-nums text-foreground\">\n          {formatTime(value)}\n        </span>\n        {isPreviewing && (\n          <button\n            onClick={onExitPreview}\n            className=\"text-[10px] uppercase tracking-wide text-muted-foreground/60 hover:text-foreground\"\n          >\n            Preview / Click to edit {editCheckpointInfo.label}\n          </button>\n        )}\n      </div>\n\n      <div className=\"h-6 w-px bg-border/20\" />\n\n      <div className=\"flex gap-1\">\n        {TIME_CHECKPOINT_ORDER.map((checkpoint) => {\n          const { value: cpValue, label } = TIME_CHECKPOINTS[checkpoint];\n          const isViewingHere = Math.abs(value - cpValue) < 0.04;\n          const isEditingHere = checkpoint === activeEditCheckpoint;\n\n          return (\n            <button\n              key={checkpoint}\n              onClick={() => onCheckpointClick(checkpoint)}\n              className={cn(\n                \"px-2 py-1 text-[11px] font-medium uppercase tracking-wide transition-colors\",\n                isEditingHere\n                  ? \"bg-muted-foreground/20 text-foreground\"\n                  : isViewingHere\n                    ? \"text-foreground\"\n                    : \"text-muted-foreground/40 hover:text-muted-foreground\",\n              )}\n              title={`${label}${isEditingHere ? \" (editing)\" : \"\"}`}\n            >\n              {label}\n            </button>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/components/time-matrix-view.tsx",
    "content": "\"use client\";\n\nimport { useMemo, useState } from \"react\";\nimport { cn } from \"@/lib/ui/cn\";\nimport { Slider } from \"@/components/ui/slider\";\nimport { WeatherEffectsCanvas } from \"@/lib/weather-authoring/weather-widget/effects/weather-effects-canvas\";\nimport type { LucideIcon } from \"lucide-react\";\nimport {\n  Cloud,\n  CloudLightning,\n  CloudRain,\n  CloudSnow,\n  Sparkles,\n  SlidersHorizontal,\n  SunDim,\n  SunMedium,\n} from \"lucide-react\";\nimport type { WeatherConditionCode } from \"@/lib/weather-authoring/weather-widget/schema\";\nimport type { TimeCheckpoint } from \"../types\";\nimport type { TuningStateReturn } from \"../hooks/use-tuning-state\";\nimport { TIME_CHECKPOINT_ORDER } from \"../lib/constants\";\nimport { mapCompositorParamsToCanvasProps } from \"../lib/map-to-canvas-props\";\nimport {\n  WeatherDataOverlay,\n  createWeatherOverlayStubData,\n} from \"./weather-data-overlay\";\nimport {\n  PARAMETER_GROUPS,\n  type ParameterDef,\n  type TunableLayerKey,\n} from \"./parameter-definitions\";\n\nconst PARAMETER_GROUP_ICONS: Record<string, LucideIcon> = {\n  Sky: SunDim,\n  \"Sun Rays\": SunMedium,\n  Clouds: Cloud,\n  Rain: CloudRain,\n  Snow: CloudSnow,\n  Lightning: CloudLightning,\n  Glass: Sparkles,\n  Post: SlidersHorizontal,\n};\n\nconst PARAMETER_GROUP_COLORS: Record<string, { dot: string; text: string }> = {\n  Sky: { dot: \"text-sky-500/80\", text: \"text-sky-500/80\" },\n  \"Sun Rays\": { dot: \"text-amber-500/80\", text: \"text-amber-500/80\" },\n  Clouds: { dot: \"text-slate-400/80\", text: \"text-slate-400/80\" },\n  Rain: { dot: \"text-blue-500/80\", text: \"text-blue-500/80\" },\n  Snow: { dot: \"text-cyan-400/80\", text: \"text-cyan-400/80\" },\n  Lightning: { dot: \"text-violet-400/80\", text: \"text-violet-400/80\" },\n  Glass: { dot: \"text-teal-400/80\", text: \"text-teal-400/80\" },\n  Post: { dot: \"text-pink-500/80\", text: \"text-pink-500/80\" },\n};\n\ninterface TimeMatrixViewProps {\n  tuningState: TuningStateReturn;\n  condition: WeatherConditionCode;\n}\n\nfunction getNumericValue(\n  params: Record<string, unknown>,\n  key: string,\n): number | undefined {\n  const value = params[key];\n  return typeof value === \"number\" ? value : undefined;\n}\n\nfunction CheckpointSlider({\n  value,\n  baseValue,\n  min,\n  max,\n  step,\n  onChange,\n  label,\n}: {\n  value: number;\n  baseValue: number;\n  min: number;\n  max: number;\n  step: number;\n  onChange: (value: number, opts: { bulkAcrossTimes: boolean }) => void;\n  label: string;\n}) {\n  const isChanged = Math.abs(value - baseValue) > 0.001;\n  const displayValue = value.toFixed(2);\n  const [bulkAcrossTimes, setBulkAcrossTimes] = useState(false);\n\n  return (\n    <div className=\"flex flex-col gap-1\">\n      <div\n        className=\"relative\"\n        onPointerDownCapture={(e) => setBulkAcrossTimes(e.metaKey)}\n        onPointerUpCapture={() => setBulkAcrossTimes(false)}\n        onPointerCancel={() => setBulkAcrossTimes(false)}\n      >\n        {isChanged && (\n          <div\n            className=\"absolute top-1/2 h-1 w-1 -translate-y-1/2 rounded-full bg-muted-foreground/40\"\n            style={{\n              left: `${((baseValue - min) / (max - min)) * 100}%`,\n            }}\n            title={`Base: ${baseValue.toFixed(2)}`}\n          />\n        )}\n        <Slider\n          aria-label={`${label} ${displayValue}`}\n          value={[value]}\n          min={min}\n          max={max}\n          step={step}\n          onValueChange={([next]) => onChange(next, { bulkAcrossTimes })}\n          className=\"relative w-full [&_[data-slot=slider-track]]:h-0.5 [&_[data-slot=slider-range]]:bg-foreground/20\"\n        />\n      </div>\n      <span\n        className={cn(\n          \"font-mono text-xs tabular-nums\",\n          isChanged\n            ? \"text-amber-600/80 dark:text-amber-400/80\"\n            : \"text-muted-foreground/50\",\n        )}\n      >\n        {displayValue}\n      </span>\n    </div>\n  );\n}\n\nfunction ParameterTimeRow({\n  param,\n  layer,\n  tuningState,\n  condition,\n}: {\n  param: ParameterDef;\n  layer: TunableLayerKey;\n  tuningState: TuningStateReturn;\n  condition: WeatherConditionCode;\n}) {\n  const values = useMemo(() => {\n    return TIME_CHECKPOINT_ORDER.map((checkpoint) => {\n      const full = tuningState.getFullParamsForCheckpoint(\n        condition,\n        checkpoint,\n      );\n      const base = tuningState.getBaseParamsForCheckpoint(\n        condition,\n        checkpoint,\n      );\n\n      const layerParams = full[layer] as unknown as Record<string, unknown>;\n      const baseParams = base[layer] as unknown as Record<string, unknown>;\n\n      const value = getNumericValue(layerParams, param.key);\n      const baseValue = getNumericValue(baseParams, param.key);\n\n      return {\n        checkpoint,\n        value,\n        baseValue,\n      };\n    });\n  }, [condition, layer, param.key, tuningState]);\n\n  const hasValue = values.some((entry) => typeof entry.value === \"number\");\n  if (!hasValue) return null;\n\n  return (\n    <div className=\"border-b border-border/20 py-2\">\n      <div className=\"grid grid-cols-[160px_repeat(4,minmax(0,1fr))] items-start gap-3\">\n        <div className=\"flex items-center gap-2 text-xs text-muted-foreground/70\">\n          <span>{param.label}</span>\n        </div>\n        {values.map(({ checkpoint, value, baseValue }) => {\n          if (typeof value !== \"number\" || typeof baseValue !== \"number\") {\n            return (\n              <div\n                key={checkpoint}\n                className=\"text-xs text-muted-foreground/40\"\n              >\n                —\n              </div>\n            );\n          }\n\n          return (\n            <CheckpointSlider\n              key={checkpoint}\n              value={value}\n              baseValue={baseValue}\n              min={param.min}\n              max={param.max}\n              step={param.step}\n              label={`${param.label} ${checkpoint}`}\n              onChange={(next, { bulkAcrossTimes }) => {\n                if (bulkAcrossTimes) {\n                  tuningState.bulkUpdateParameterAcrossCheckpoints(\n                    condition,\n                    TIME_CHECKPOINT_ORDER,\n                    layer,\n                    param.key,\n                    next,\n                  );\n                  return;\n                }\n                tuningState.updateParameterAtCheckpoint(\n                  condition,\n                  checkpoint,\n                  layer,\n                  param.key,\n                  next,\n                );\n              }}\n            />\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n\nfunction CheckpointPreview({\n  condition,\n  checkpoint,\n  tuningState,\n}: {\n  condition: WeatherConditionCode;\n  checkpoint: TimeCheckpoint;\n  tuningState: TuningStateReturn;\n}) {\n  const params = useMemo(\n    () => tuningState.getFullParamsForCheckpoint(condition, checkpoint),\n    [condition, checkpoint, tuningState],\n  );\n  const canvasProps = useMemo(\n    () => mapCompositorParamsToCanvasProps(params),\n    [params],\n  );\n  const overlayData = useMemo(\n    () => createWeatherOverlayStubData(condition),\n    [condition],\n  );\n\n  return (\n    <div className=\"border-border/50 relative aspect-4/3 w-full overflow-hidden rounded-md border bg-black @container/weather [container-type:size]\">\n      <WeatherEffectsCanvas className=\"absolute inset-0\" {...canvasProps} />\n      <div className=\"absolute inset-0 z-10\">\n        <WeatherDataOverlay\n          glassParams={params.glass}\n          location={overlayData.location}\n          conditionCode={condition}\n          temperature={overlayData.temperature}\n          tempHigh={overlayData.tempHigh}\n          tempLow={overlayData.tempLow}\n          forecast={overlayData.forecast}\n          unit={overlayData.unit}\n          timeOfDay={params.celestial.timeOfDay}\n        />\n      </div>\n    </div>\n  );\n}\n\nexport function TimeMatrixView({\n  tuningState,\n  condition,\n}: TimeMatrixViewProps) {\n  return (\n    <div className=\"flex h-full flex-col\">\n      {/* Keep the widgets always visible while scrolling parameters */}\n      <div className=\"border-border/30 bg-background/95 shrink-0 border-b px-4 py-4 backdrop-blur\">\n        <div className=\"grid grid-cols-4 gap-3\">\n          {TIME_CHECKPOINT_ORDER.map((checkpoint) => (\n            <CheckpointPreview\n              key={checkpoint}\n              condition={condition}\n              checkpoint={checkpoint}\n              tuningState={tuningState}\n            />\n          ))}\n        </div>\n      </div>\n\n      <div className=\"min-h-0 flex-1 overflow-y-auto px-4 pb-6\">\n        {PARAMETER_GROUPS.map((group) => {\n          const Icon = PARAMETER_GROUP_ICONS[group.name];\n          const color = PARAMETER_GROUP_COLORS[group.name] ?? {\n            dot: \"text-muted-foreground/60\",\n            text: \"text-muted-foreground/60\",\n          };\n\n          return (\n            <div key={group.name} className=\"mt-6\">\n              <div className=\"sticky top-0 z-10 -mx-4 border-b border-border/30 bg-background/95 px-4 py-2 backdrop-blur\">\n                <div className=\"flex items-center gap-2 text-xs font-medium tracking-wider uppercase\">\n                  {Icon ? (\n                    <div\n                      className={cn(\n                        \"flex size-4 items-center justify-center rounded-full border border-border/40 bg-muted/40\",\n                        color.dot,\n                      )}\n                    >\n                      <Icon className=\"size-2.5\" />\n                    </div>\n                  ) : null}\n                  <span className={color.text}>{group.name}</span>\n                </div>\n              </div>\n              <div className=\"mt-2\">\n                {group.params.map((param) => (\n                  <ParameterTimeRow\n                    key={`${group.layer}.${param.key}`}\n                    param={param}\n                    layer={group.layer}\n                    tuningState={tuningState}\n                    condition={condition}\n                  />\n                ))}\n              </div>\n            </div>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/components/view-mode-toggle.tsx",
    "content": "\"use client\";\n\nimport { cn } from \"@/lib/ui/cn\";\nimport { LayoutGrid, Sliders, Clock } from \"lucide-react\";\n\nexport type ViewMode = \"condition\" | \"parameter\" | \"time\";\n\ninterface ViewModeToggleProps {\n  value: ViewMode;\n  onChange: (mode: ViewMode) => void;\n}\n\nexport function ViewModeToggle({ value, onChange }: ViewModeToggleProps) {\n  return (\n    <div className=\"flex items-center gap-1 rounded-lg border border-border/50 bg-muted/30 p-1\">\n      <button\n        onClick={() => onChange(\"condition\")}\n        className={cn(\n          \"flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-medium transition-all\",\n          value === \"condition\"\n            ? \"bg-background text-foreground shadow-sm\"\n            : \"text-muted-foreground hover:text-foreground\",\n        )}\n      >\n        <Sliders className=\"size-3.5\" />\n        <span>Condition</span>\n      </button>\n      <button\n        onClick={() => onChange(\"parameter\")}\n        className={cn(\n          \"flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-medium transition-all\",\n          value === \"parameter\"\n            ? \"bg-background text-foreground shadow-sm\"\n            : \"text-muted-foreground hover:text-foreground\",\n        )}\n      >\n        <LayoutGrid className=\"size-3.5\" />\n        <span>Parameter</span>\n      </button>\n      <button\n        onClick={() => onChange(\"time\")}\n        className={cn(\n          \"flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-medium transition-all\",\n          value === \"time\"\n            ? \"bg-background text-foreground shadow-sm\"\n            : \"text-muted-foreground hover:text-foreground\",\n        )}\n      >\n        <Clock className=\"size-3.5\" />\n        <span>Time</span>\n      </button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/components/weather-data-overlay.tsx",
    "content": "\"use client\";\n\n// IMPORTANT:\n// The tuning studio must use the exact same overlay as the production widget,\n// otherwise you end up tuning against a different composition.\nexport {\n  WeatherDataOverlay,\n  type WeatherDataOverlayProps,\n  type GlassEffectParams,\n} from \"@/components/tool-ui/weather-widget/weather-data-overlay\";\n\nimport type {\n  ForecastDay,\n  TemperatureUnit,\n  WeatherConditionCode,\n} from \"@/components/tool-ui/weather-widget/runtime\";\n\nexport interface WeatherOverlayStubData {\n  location: string;\n  conditionCode: WeatherConditionCode;\n  temperature: number;\n  tempHigh: number;\n  tempLow: number;\n  forecast: ForecastDay[];\n  unit: TemperatureUnit;\n}\n\nexport function createWeatherOverlayStubData(\n  conditionCode: WeatherConditionCode,\n): WeatherOverlayStubData {\n  return {\n    location: \"San Francisco, CA\",\n    conditionCode,\n    temperature: 72,\n    tempHigh: 78,\n    tempLow: 65,\n    forecast: [\n      { label: \"Today\", tempMin: 65, tempMax: 78, conditionCode },\n      {\n        label: \"Tue\",\n        tempMin: 64,\n        tempMax: 77,\n        conditionCode: \"partly-cloudy\",\n      },\n      { label: \"Wed\", tempMin: 62, tempMax: 75, conditionCode: \"cloudy\" },\n      { label: \"Thu\", tempMin: 60, tempMax: 73, conditionCode: \"rain\" },\n      { label: \"Fri\", tempMin: 63, tempMax: 76, conditionCode: \"clear\" },\n    ],\n    unit: \"fahrenheit\",\n  };\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/hooks/use-tuning-state.ts",
    "content": "\"use client\";\n\nimport { useState, useCallback, useEffect, useMemo } from \"react\";\nimport type { WeatherConditionCode } from \"@/lib/weather-authoring/weather-widget/schema\";\nimport { TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES } from \"@/lib/weather-authoring/weather-widget/effects/tuned-presets\";\nimport {\n  resolveWeatherEffectsCanvasProps,\n  type WeatherEffectsCanvasProps,\n  type WeatherEffectsCheckpointMode,\n} from \"@/lib/weather-authoring/weather-widget/effects\";\nimport type {\n  ConditionOverrides,\n  FullCompositorParams,\n  CheckpointOverrides,\n} from \"../../weather-compositor/presets\";\nimport {\n  getRawBaseParamsForCondition,\n  mergeWithOverrides,\n  extractOverrides,\n  loadFromStorage,\n  saveToStorage,\n  WEATHER_CONDITIONS,\n  type CompositorState,\n} from \"../../weather-compositor/presets\";\nimport {\n  getNearestCheckpoint,\n  getInterpolatedOverrides,\n} from \"../../weather-compositor/interpolation\";\nimport type {\n  ConditionCheckpoints,\n  CompareMode,\n  TimeCheckpoint,\n} from \"../types\";\nimport {\n  DEFAULT_TIME_OF_DAY,\n  TIME_CHECKPOINTS,\n  TIME_CHECKPOINT_ORDER,\n} from \"../lib/constants\";\nimport { mapToolUiPresetsToCompositor } from \"../lib/tool-ui-import\";\nimport { resolveCompositorParamsAtTime } from \"../lib/resolve-params\";\nimport { recoverRepoCheckpointOverrides } from \"../lib/recover-repo-overrides\";\nimport { createStudioTimestamp } from \"../lib/studio-timestamp\";\nimport { mergeTunedPresets, toToolUiDelta } from \"../lib/tool-ui-export\";\nimport { loadWorkflowState, saveWorkflowState } from \"../lib/workflow-state\";\n\nexport type LayerKey =\n  | \"layers\"\n  | \"celestial\"\n  | \"cloud\"\n  | \"rain\"\n  | \"lightning\"\n  | \"snow\"\n  | \"glass\"\n  | \"post\";\n\nconst STUDIO_PREVIEW_CHECKPOINT_MODE: WeatherEffectsCheckpointMode = \"nearest\";\n\nfunction createEmptyCheckpointOverrides(): CheckpointOverrides {\n  return {\n    dawn: {},\n    noon: {},\n    dusk: {},\n    midnight: {},\n  };\n}\n\nexport function useTuningState() {\n  const [checkpointOverrides, setCheckpointOverrides] = useState<\n    Partial<Record<WeatherConditionCode, CheckpointOverrides>>\n  >({});\n  const [repoCheckpointOverrides, setRepoCheckpointOverrides] = useState<\n    Partial<Record<WeatherConditionCode, CheckpointOverrides>>\n  >(() =>\n    mapToolUiPresetsToCompositor(TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES),\n  );\n  const [globalTimeOfDay, setGlobalTimeOfDay] = useState(DEFAULT_TIME_OF_DAY);\n  const [activeEditCheckpoint, setActiveEditCheckpoint] =\n    useState<TimeCheckpoint>(() => getNearestCheckpoint(DEFAULT_TIME_OF_DAY));\n  const [selectedCondition, setSelectedCondition] =\n    useState<WeatherConditionCode | null>(\"clear\");\n  const [expandedGroups, setExpandedGroups] = useState<Set<string>>(\n    () => new Set(),\n  );\n  const [compareMode, setCompareMode] = useState<CompareMode>(\"off\");\n  const [compareTarget, setCompareTarget] =\n    useState<WeatherConditionCode | null>(null);\n  const [showWidgetOverlay, setShowWidgetOverlay] = useState(false);\n  const [isPreviewing, setIsPreviewing] = useState(false);\n  const [checkpoints, setCheckpoints] = useState<\n    Partial<Record<WeatherConditionCode, ConditionCheckpoints>>\n  >({});\n  const [signedOff, setSignedOff] = useState<Set<WeatherConditionCode>>(\n    () => new Set(),\n  );\n  const [isHydrated, setIsHydrated] = useState(false);\n  useEffect(() => {\n    const compositorState = loadFromStorage() as CompositorState | null;\n    if (compositorState) {\n      setCheckpointOverrides(compositorState.checkpointOverrides ?? {});\n\n      const storedTime = compositorState.globalSettings.timeOfDay;\n      const nearest = getNearestCheckpoint(storedTime);\n      const snappedTime = TIME_CHECKPOINTS[nearest].value;\n      setGlobalTimeOfDay(snappedTime);\n      setActiveEditCheckpoint(nearest);\n    }\n\n    const workflowState = loadWorkflowState();\n    if (workflowState) {\n      setCheckpoints(workflowState.checkpoints);\n      setSignedOff(new Set(workflowState.signedOff));\n      if (workflowState.repoCheckpointOverrides) {\n        setRepoCheckpointOverrides(workflowState.repoCheckpointOverrides);\n      }\n    }\n\n    setIsHydrated(true);\n  }, []);\n\n  useEffect(() => {\n    if (process.env.NODE_ENV === \"production\") return;\n\n    let cancelled = false;\n    const recover = async () => {\n      const recovered = await recoverRepoCheckpointOverrides();\n      if (!cancelled && recovered) {\n        setRepoCheckpointOverrides(recovered);\n      }\n    };\n\n    void recover();\n    return () => {\n      cancelled = true;\n    };\n  }, []);\n\n  useEffect(() => {\n    if (!isHydrated) return;\n\n    saveToStorage({\n      version: 4,\n      activeCondition: selectedCondition ?? \"clear\",\n      globalSettings: { timeOfDay: globalTimeOfDay },\n      checkpointOverrides,\n    });\n  }, [checkpointOverrides, globalTimeOfDay, selectedCondition, isHydrated]);\n\n  useEffect(() => {\n    if (!isHydrated) return;\n    saveWorkflowState({\n      checkpoints,\n      signedOff: Array.from(signedOff),\n      repoCheckpointOverrides,\n    });\n  }, [checkpoints, signedOff, repoCheckpointOverrides, isHydrated]);\n\n  // Auto-expand parameter groups for active layers when condition changes\n  useEffect(() => {\n    if (!selectedCondition) return;\n\n    const params = getParamsForCondition(selectedCondition);\n\n    const groups = new Set<string>();\n    if (params.layers.celestial) groups.add(\"celestial\");\n    if (params.layers.clouds) groups.add(\"cloud\");\n    if (params.layers.rain) groups.add(\"rain\");\n    if (params.layers.lightning) groups.add(\"lightning\");\n    if (params.layers.snow) groups.add(\"snow\");\n\n    setExpandedGroups(groups);\n    // Only re-run when condition changes, not on every override change\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [selectedCondition]);\n\n  const getTimestamp = useCallback((timeOfDay: number) => {\n    // Important: our production helpers (`getTimeOfDay`, `getSunAltitude`) use UTC.\n    // If we generate timestamps in local time and then read them as UTC, the\n    // tuning studio silently shifts the day cycle (noon becomes evening, etc).\n    // This presents as \"bleeding\" or wildly inconsistent cross-condition tuning.\n    return createStudioTimestamp(timeOfDay);\n  }, []);\n\n  const getRawBaseParamsForCheckpoint = useCallback(\n    (\n      condition: WeatherConditionCode,\n      checkpoint: TimeCheckpoint,\n    ): FullCompositorParams => {\n      const checkpointTime = TIME_CHECKPOINTS[checkpoint].value;\n      const timestamp = getTimestamp(checkpointTime);\n      const base = getRawBaseParamsForCondition(condition, timestamp);\n      base.celestial.timeOfDay = checkpointTime;\n      return base;\n    },\n    [getTimestamp],\n  );\n\n  const getBaseParamsForCheckpoint = useCallback(\n    (\n      condition: WeatherConditionCode,\n      checkpoint: TimeCheckpoint,\n    ): FullCompositorParams => {\n      const base = getRawBaseParamsForCheckpoint(condition, checkpoint);\n      const repoDefaults = repoCheckpointOverrides[condition]?.[checkpoint];\n      return repoDefaults ? mergeWithOverrides(base, repoDefaults) : base;\n    },\n    [getRawBaseParamsForCheckpoint, repoCheckpointOverrides],\n  );\n\n  const resolveParamsAtTime = useCallback(\n    (\n      condition: WeatherConditionCode,\n      timeOfDay: number,\n    ): FullCompositorParams => {\n      const timestamp = getTimestamp(timeOfDay);\n      const rawBase = getRawBaseParamsForCondition(condition, timestamp);\n      rawBase.celestial.timeOfDay = timeOfDay;\n\n      return resolveCompositorParamsAtTime({\n        timeOfDay,\n        rawBaseAtTime: rawBase,\n        getRawBaseForCheckpoint: (checkpoint) =>\n          getRawBaseParamsForCheckpoint(condition, checkpoint),\n        repoCheckpointOverrides: repoCheckpointOverrides[condition],\n        getRepoBaseForCheckpoint: (checkpoint) =>\n          getBaseParamsForCheckpoint(condition, checkpoint),\n        userCheckpointOverrides: checkpointOverrides[condition],\n      });\n    },\n    [\n      checkpointOverrides,\n      getBaseParamsForCheckpoint,\n      getRawBaseParamsForCheckpoint,\n      getTimestamp,\n      repoCheckpointOverrides,\n    ],\n  );\n\n  // Get full params including user overrides for a specific checkpoint\n  const getFullParamsForCheckpoint = useCallback(\n    (\n      condition: WeatherConditionCode,\n      checkpoint: TimeCheckpoint,\n    ): FullCompositorParams => {\n      const checkpointTime = TIME_CHECKPOINTS[checkpoint].value;\n      return resolveParamsAtTime(condition, checkpointTime);\n    },\n    [resolveParamsAtTime],\n  );\n\n  const getParamsForCondition = useCallback(\n    (condition: WeatherConditionCode): FullCompositorParams => {\n      return resolveParamsAtTime(condition, globalTimeOfDay);\n    },\n    [globalTimeOfDay, resolveParamsAtTime],\n  );\n\n  const getBaseParams = useCallback(\n    (condition: WeatherConditionCode): FullCompositorParams => {\n      const timestamp = getTimestamp(globalTimeOfDay);\n      const rawBase = getRawBaseParamsForCondition(condition, timestamp);\n      rawBase.celestial.timeOfDay = globalTimeOfDay;\n\n      const repoOverridesForCondition = repoCheckpointOverrides[condition];\n      const repoInterpolated = getInterpolatedOverrides(\n        repoOverridesForCondition,\n        globalTimeOfDay,\n        (checkpoint) => getRawBaseParamsForCheckpoint(condition, checkpoint),\n      );\n\n      return repoInterpolated\n        ? mergeWithOverrides(rawBase, repoInterpolated)\n        : rawBase;\n    },\n    [\n      getRawBaseParamsForCheckpoint,\n      getTimestamp,\n      globalTimeOfDay,\n      repoCheckpointOverrides,\n    ],\n  );\n\n  const previewTunedPresets = useMemo(\n    () =>\n      mergeTunedPresets(\n        toToolUiDelta(repoCheckpointOverrides),\n        toToolUiDelta(checkpointOverrides),\n      ),\n    [checkpointOverrides, repoCheckpointOverrides],\n  );\n\n  const getCanvasPropsForCondition = useCallback(\n    (\n      condition: WeatherConditionCode,\n      timeOfDay: number,\n      checkpointMode: WeatherEffectsCheckpointMode = STUDIO_PREVIEW_CHECKPOINT_MODE,\n    ): WeatherEffectsCanvasProps => {\n      const timestamp = getTimestamp(timeOfDay);\n      return resolveWeatherEffectsCanvasProps({\n        conditionCode: condition,\n        timestamp,\n        timeOfDay,\n        tunedPresets: previewTunedPresets,\n        checkpointMode,\n      });\n    },\n    [getTimestamp, previewTunedPresets],\n  );\n\n  const withCheckpointOverrides = useCallback(\n    (\n      updater: (\n        current: Partial<Record<WeatherConditionCode, CheckpointOverrides>>,\n      ) => Partial<Record<WeatherConditionCode, CheckpointOverrides>>,\n    ) => {\n      setCheckpointOverrides((prev) => updater(prev));\n    },\n    [],\n  );\n\n  const clearStudioDeltas = useCallback(() => {\n    // Clear pending user edits. This is used after applying an export so\n    // subsequent applies only include new changes, while keeping the current\n    // workflow state (review/sign-off).\n    setCheckpointOverrides({});\n    setIsPreviewing(false);\n  }, []);\n\n  const updateCheckpointOverrides = useCallback(\n    (\n      condition: WeatherConditionCode,\n      checkpoint: TimeCheckpoint,\n      newOverrides: ConditionOverrides,\n    ) => {\n      withCheckpointOverrides((prev) => {\n        const existing = prev[condition] ?? createEmptyCheckpointOverrides();\n        return {\n          ...prev,\n          [condition]: {\n            ...existing,\n            [checkpoint]: newOverrides,\n          },\n        };\n      });\n    },\n    [withCheckpointOverrides],\n  );\n\n  const updateParameterAtCheckpoint = useCallback(\n    (\n      condition: WeatherConditionCode,\n      checkpoint: TimeCheckpoint,\n      layer: LayerKey,\n      parameter: string,\n      value: number | boolean,\n    ) => {\n      const base = getBaseParamsForCheckpoint(condition, checkpoint);\n      const full = getFullParamsForCheckpoint(condition, checkpoint);\n      const nextGroup = {\n        ...(full[layer] as unknown as Record<string, number | boolean>),\n        [parameter]: value,\n      };\n      const nextFull = {\n        ...full,\n        [layer]: nextGroup,\n      } as FullCompositorParams;\n      const newOverrides = extractOverrides(nextFull, base);\n      updateCheckpointOverrides(condition, checkpoint, newOverrides);\n    },\n    [\n      getBaseParamsForCheckpoint,\n      getFullParamsForCheckpoint,\n      updateCheckpointOverrides,\n    ],\n  );\n\n  const updateParams = useCallback(\n    (condition: WeatherConditionCode, newParams: FullCompositorParams) => {\n      let checkpointToEdit = activeEditCheckpoint;\n      if (isPreviewing) {\n        // If we're previewing, snap edits back to the nearest checkpoint\n        // so overrides are stored against a single, deterministic checkpoint.\n        const previewParams = getParamsForCondition(condition);\n\n        checkpointToEdit = getNearestCheckpoint(globalTimeOfDay);\n        setActiveEditCheckpoint(checkpointToEdit);\n        setGlobalTimeOfDay(TIME_CHECKPOINTS[checkpointToEdit].value);\n        setIsPreviewing(false);\n\n        const delta = extractOverrides(newParams, previewParams);\n\n        const checkpointBase = getBaseParamsForCheckpoint(\n          condition,\n          checkpointToEdit,\n        );\n        const checkpointCurrent = getFullParamsForCheckpoint(\n          condition,\n          checkpointToEdit,\n        );\n        const checkpointNext = mergeWithOverrides(checkpointCurrent, delta);\n        const newOverrides = extractOverrides(checkpointNext, checkpointBase);\n        updateCheckpointOverrides(condition, checkpointToEdit, newOverrides);\n        return;\n      }\n\n      const base = getBaseParamsForCheckpoint(condition, checkpointToEdit);\n      const newOverrides = extractOverrides(newParams, base);\n      updateCheckpointOverrides(condition, checkpointToEdit, newOverrides);\n    },\n    [\n      getBaseParamsForCheckpoint,\n      getFullParamsForCheckpoint,\n      getParamsForCondition,\n      activeEditCheckpoint,\n      updateCheckpointOverrides,\n      isPreviewing,\n      globalTimeOfDay,\n    ],\n  );\n\n  const resetCondition = useCallback(\n    (condition: WeatherConditionCode) => {\n      withCheckpointOverrides((prev) => {\n        const next = { ...prev };\n        delete next[condition];\n        return next;\n      });\n      setCheckpoints((prev) => {\n        const next = { ...prev };\n        delete next[condition];\n        return next;\n      });\n      setSignedOff((prev) => {\n        const next = new Set(prev);\n        next.delete(condition);\n        return next;\n      });\n    },\n    [withCheckpointOverrides],\n  );\n\n  const copyLayerFromCondition = useCallback(\n    (\n      sourceCondition: WeatherConditionCode,\n      targetCondition: WeatherConditionCode,\n      layerKey: LayerKey,\n    ) => {\n      withCheckpointOverrides((prev) => {\n        const existing =\n          prev[targetCondition] ?? createEmptyCheckpointOverrides();\n        const updated = { ...existing };\n\n        for (const checkpoint of TIME_CHECKPOINT_ORDER) {\n          // Get the FULL merged params for the source (base + defaults + user overrides)\n          const sourceBase = getBaseParamsForCheckpoint(\n            sourceCondition,\n            checkpoint,\n          );\n          const sourceUserOverrides = prev[sourceCondition]?.[checkpoint];\n          const sourceFull = sourceUserOverrides\n            ? mergeWithOverrides(sourceBase, sourceUserOverrides)\n            : sourceBase;\n\n          // Get just the layer data from the full params\n          const sourceLayerData = sourceFull[layerKey];\n\n          if (\n            sourceLayerData &&\n            typeof sourceLayerData === \"object\" &&\n            Object.keys(sourceLayerData).length > 0\n          ) {\n            // Apply the full layer data as overrides to the target\n            updated[checkpoint] = {\n              ...updated[checkpoint],\n              [layerKey]: JSON.parse(JSON.stringify(sourceLayerData)),\n            };\n          } else {\n            // Clear target's layer if source has no data\n            const currentCheckpoint = updated[checkpoint];\n            if (currentCheckpoint && layerKey in currentCheckpoint) {\n              const { [layerKey]: _, ...rest } = currentCheckpoint;\n              updated[checkpoint] = rest;\n            }\n          }\n        }\n\n        return {\n          ...prev,\n          [targetCondition]: updated,\n        };\n      });\n    },\n    [getBaseParamsForCheckpoint, withCheckpointOverrides],\n  );\n\n  const copyLayerToAllConditions = useCallback(\n    (sourceCondition: WeatherConditionCode, layerKey: LayerKey) => {\n      const otherConditions = WEATHER_CONDITIONS.filter(\n        (c) => c !== sourceCondition,\n      );\n      for (const target of otherConditions) {\n        copyLayerFromCondition(sourceCondition, target, layerKey);\n      }\n    },\n    [copyLayerFromCondition],\n  );\n\n  const toggleGroup = useCallback((group: string) => {\n    setExpandedGroups((prev) => {\n      const next = new Set(prev);\n      if (next.has(group)) {\n        next.delete(group);\n      } else {\n        next.add(group);\n      }\n      return next;\n    });\n  }, []);\n\n  const signedOffCount = useMemo(() => signedOff.size, [signedOff]);\n\n  const getOverrideCount = useCallback(\n    (condition: WeatherConditionCode): number => {\n      const conditionCheckpointOverrides = checkpointOverrides[condition];\n      if (!conditionCheckpointOverrides) return 0;\n\n      let count = 0;\n      for (const checkpoint of TIME_CHECKPOINT_ORDER) {\n        const checkpointData = conditionCheckpointOverrides[checkpoint];\n        if (!checkpointData) continue;\n        for (const group of Object.values(checkpointData)) {\n          if (group && typeof group === \"object\") {\n            count += Object.keys(group).length;\n          }\n        }\n      }\n      return count;\n    },\n    [checkpointOverrides],\n  );\n\n  const markCheckpointReviewed = useCallback(\n    (condition: WeatherConditionCode, checkpoint: TimeCheckpoint) => {\n      setCheckpoints((prev) => {\n        const current = prev[condition] ?? {\n          dawn: \"pending\",\n          noon: \"pending\",\n          dusk: \"pending\",\n          midnight: \"pending\",\n        };\n        return {\n          ...prev,\n          [condition]: {\n            ...current,\n            [checkpoint]: \"reviewed\",\n          },\n        };\n      });\n    },\n    [],\n  );\n\n  const copyCheckpointToCheckpoints = useCallback(\n    (\n      condition: WeatherConditionCode,\n      sourceCheckpoint: TimeCheckpoint,\n      targetCheckpoints: TimeCheckpoint[],\n    ) => {\n      withCheckpointOverrides((prev) => {\n        const existing = prev[condition] ?? createEmptyCheckpointOverrides();\n        const updated = { ...existing };\n\n        const sourceFull = getFullParamsForCheckpoint(\n          condition,\n          sourceCheckpoint,\n        );\n\n        for (const target of targetCheckpoints) {\n          if (target !== sourceCheckpoint) {\n            const targetBase = getBaseParamsForCheckpoint(condition, target);\n            const rebased = extractOverrides(sourceFull, targetBase);\n            updated[target] = JSON.parse(JSON.stringify(rebased));\n          }\n        }\n\n        return {\n          ...prev,\n          [condition]: updated,\n        };\n      });\n\n      for (const target of targetCheckpoints) {\n        if (target !== sourceCheckpoint) {\n          markCheckpointReviewed(condition, target);\n        }\n      }\n    },\n    [\n      getBaseParamsForCheckpoint,\n      getFullParamsForCheckpoint,\n      markCheckpointReviewed,\n      withCheckpointOverrides,\n    ],\n  );\n\n  const bulkUpdateParameter = useCallback(\n    (\n      conditions: WeatherConditionCode[],\n      checkpoints: TimeCheckpoint[],\n      layer: LayerKey,\n      parameter: string,\n      value: number | boolean,\n    ) => {\n      for (const condition of conditions) {\n        for (const checkpoint of checkpoints) {\n          updateParameterAtCheckpoint(\n            condition,\n            checkpoint,\n            layer,\n            parameter,\n            value,\n          );\n        }\n      }\n    },\n    [updateParameterAtCheckpoint],\n  );\n\n  const bulkUpdateParameterAcrossCheckpoints = useCallback(\n    (\n      condition: WeatherConditionCode,\n      checkpoints: TimeCheckpoint[],\n      layer: LayerKey,\n      parameter: string,\n      value: number | boolean,\n    ) => {\n      withCheckpointOverrides((prev) => {\n        const existing = prev[condition] ?? createEmptyCheckpointOverrides();\n        const updated = { ...existing };\n\n        for (const checkpoint of checkpoints) {\n          const base = getBaseParamsForCheckpoint(condition, checkpoint);\n          const userOverrides = existing[checkpoint];\n          const full = userOverrides\n            ? mergeWithOverrides(base, userOverrides)\n            : base;\n\n          const nextGroup = {\n            ...(full[layer] as unknown as Record<string, number | boolean>),\n            [parameter]: value,\n          };\n          const nextFull = {\n            ...full,\n            [layer]: nextGroup,\n          } as FullCompositorParams;\n\n          const newOverrides = extractOverrides(nextFull, base);\n          updated[checkpoint] = newOverrides;\n        }\n\n        return {\n          ...prev,\n          [condition]: updated,\n        };\n      });\n    },\n    [getBaseParamsForCheckpoint, withCheckpointOverrides],\n  );\n\n  const goToCheckpoint = useCallback(\n    (condition: WeatherConditionCode, checkpoint: TimeCheckpoint) => {\n      const { value } = TIME_CHECKPOINTS[checkpoint];\n      setGlobalTimeOfDay(value);\n      setActiveEditCheckpoint(checkpoint);\n      setIsPreviewing(false);\n      markCheckpointReviewed(condition, checkpoint);\n    },\n    [markCheckpointReviewed],\n  );\n\n  const scrubTime = useCallback((time: number) => {\n    const nearest = getNearestCheckpoint(time);\n    setGlobalTimeOfDay(TIME_CHECKPOINTS[nearest].value);\n    setActiveEditCheckpoint(nearest);\n    setIsPreviewing(true);\n  }, []);\n\n  const exitPreview = useCallback(() => {\n    setIsPreviewing(false);\n    const nearest = getNearestCheckpoint(globalTimeOfDay);\n    setActiveEditCheckpoint(nearest);\n  }, [globalTimeOfDay]);\n\n  const toggleSignOff = useCallback((condition: WeatherConditionCode) => {\n    setSignedOff((prev) => {\n      const next = new Set(prev);\n      if (next.has(condition)) {\n        next.delete(condition);\n      } else {\n        next.add(condition);\n      }\n      return next;\n    });\n  }, []);\n\n  const getConditionCheckpoints = useCallback(\n    (condition: WeatherConditionCode): ConditionCheckpoints => {\n      return (\n        checkpoints[condition] ?? {\n          dawn: \"pending\",\n          noon: \"pending\",\n          dusk: \"pending\",\n          midnight: \"pending\",\n        }\n      );\n    },\n    [checkpoints],\n  );\n\n  const allCheckpointsReviewed = useCallback(\n    (condition: WeatherConditionCode): boolean => {\n      const cp = getConditionCheckpoints(condition);\n      return TIME_CHECKPOINT_ORDER.every((key) => cp[key] === \"reviewed\");\n    },\n    [getConditionCheckpoints],\n  );\n\n  return {\n    checkpointOverrides,\n    setCheckpointOverrides,\n    repoCheckpointOverrides,\n    setRepoCheckpointOverrides,\n    globalTimeOfDay,\n    setGlobalTimeOfDay,\n    activeEditCheckpoint,\n    setActiveEditCheckpoint,\n    selectedCondition,\n    setSelectedCondition,\n    expandedGroups,\n    toggleGroup,\n    compareMode,\n    setCompareMode,\n    compareTarget,\n    setCompareTarget,\n    showWidgetOverlay,\n    setShowWidgetOverlay,\n    isPreviewing,\n    setIsPreviewing,\n    checkpoints,\n    setCheckpoints,\n    signedOff,\n    setSignedOff,\n    signedOffCount,\n    isHydrated,\n    clearStudioDeltas,\n    getParamsForCondition,\n    getCanvasPropsForCondition,\n    getBaseParams,\n    getBaseParamsForCheckpoint,\n    getFullParamsForCheckpoint,\n    updateCheckpointOverrides,\n    updateParams,\n    updateParameterAtCheckpoint,\n    bulkUpdateParameter,\n    bulkUpdateParameterAcrossCheckpoints,\n    resetCondition,\n    copyLayerFromCondition,\n    copyLayerToAllConditions,\n    copyCheckpointToCheckpoints,\n    getOverrideCount,\n    markCheckpointReviewed,\n    goToCheckpoint,\n    scrubTime,\n    exitPreview,\n    toggleSignOff,\n    getConditionCheckpoints,\n    allCheckpointsReviewed,\n  };\n}\n\nexport type TuningStateReturn = ReturnType<typeof useTuningState>;\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/lib/constants.ts",
    "content": "import {\n  TIME_CHECKPOINTS as TOOL_UI_TIME_CHECKPOINTS,\n  TIME_CHECKPOINT_ORDER as TOOL_UI_TIME_CHECKPOINT_ORDER,\n  type TimeCheckpoint,\n} from \"@/lib/weather-authoring/weather-widget/effects/tuning\";\n\nexport const TIME_CHECKPOINTS: Record<\n  TimeCheckpoint,\n  { value: number; label: string }\n> = {\n  dawn: { value: TOOL_UI_TIME_CHECKPOINTS.dawn, label: \"Dawn\" },\n  noon: { value: TOOL_UI_TIME_CHECKPOINTS.noon, label: \"Noon\" },\n  dusk: { value: TOOL_UI_TIME_CHECKPOINTS.dusk, label: \"Dusk\" },\n  midnight: { value: TOOL_UI_TIME_CHECKPOINTS.midnight, label: \"Night\" },\n};\n\nexport const TIME_CHECKPOINT_ORDER: TimeCheckpoint[] =\n  TOOL_UI_TIME_CHECKPOINT_ORDER;\n\nexport const STORAGE_KEY = \"weather-tuning-state\";\nexport const SESSION_KEY = \"weather-tuning-workflow\";\n\nexport const DEFAULT_TIME_OF_DAY = 0.5;\nexport const SNOW_FALL_SPEED_MAX = 8;\n\nexport const RAIN_PARAM_LIMITS = {\n  glassIntensity: { min: 0, max: 2 },\n  zoom: { min: 0.25, max: 4 },\n  fallingIntensity: { min: 0, max: 2 },\n  fallingSpeed: { min: 0.1, max: 8 },\n  fallingAngle: { min: -1.25, max: 1.25 },\n  fallingStreakLength: { min: 0.1, max: 4 },\n  fallingLayers: { min: 1, max: 12 },\n  fallingRefraction: { min: 0, max: 2 },\n  fallingWaviness: { min: 0, max: 2 },\n  fallingThicknessVar: { min: 0, max: 2 },\n} as const;\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/lib/has-any-tuning-delta.ts",
    "content": "import type { WeatherConditionCode } from \"@/lib/weather-authoring/weather-widget/schema\";\nimport type { CheckpointOverrides } from \"../../weather-compositor/presets\";\n\nexport function hasAnyTuningDelta(\n  checkpointOverrides: Partial<\n    Record<WeatherConditionCode, CheckpointOverrides>\n  >,\n): boolean {\n  return Object.values(checkpointOverrides).some((byCheckpoint) => {\n    if (!byCheckpoint) return false;\n    return Object.values(byCheckpoint).some((checkpointData) => {\n      return Boolean(checkpointData && Object.keys(checkpointData).length > 0);\n    });\n  });\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/lib/list-updated-params.ts",
    "content": "import type { WeatherConditionCode } from \"@/lib/weather-authoring/weather-widget/schema\";\nimport type { CheckpointOverrides } from \"../../weather-compositor/presets\";\n\ntype CheckpointKey = keyof CheckpointOverrides;\n\nconst CHECKPOINTS: CheckpointKey[] = [\"dawn\", \"noon\", \"dusk\", \"midnight\"];\n\nexport function listUpdatedParams(\n  checkpointOverrides: Partial<\n    Record<WeatherConditionCode, CheckpointOverrides>\n  >,\n): string[] {\n  const out: string[] = [];\n\n  for (const condition of Object.keys(\n    checkpointOverrides,\n  ) as WeatherConditionCode[]) {\n    const byCheckpoint = checkpointOverrides[condition];\n    if (!byCheckpoint) continue;\n\n    for (const checkpoint of CHECKPOINTS) {\n      const overrideGroups = byCheckpoint[checkpoint];\n      if (!overrideGroups) continue;\n\n      for (const [groupKey, groupValue] of Object.entries(overrideGroups)) {\n        if (!groupValue || typeof groupValue !== \"object\") continue;\n        for (const paramKey of Object.keys(groupValue)) {\n          out.push(`${condition}.${checkpoint}.${groupKey}.${paramKey}`);\n        }\n      }\n    }\n  }\n\n  return out;\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/lib/map-to-canvas-props.ts",
    "content": "import {\n  mapWeatherCompositorParamsToCanvasProps,\n  type WeatherStudioCompositorParams,\n  type WeatherEffectsCanvasProps,\n} from \"@/lib/weather-authoring/weather-widget/effects\";\nimport type { FullCompositorParams } from \"../../weather-compositor/presets\";\n\n/**\n * Convert tuning-studio compositor params (the superset used by the studio)\n * into the exact prop shape the shipping `WeatherEffectsCanvas` understands.\n *\n * Keeping this mapping centralized prevents \"looks wrong / looks like bleed\"\n * issues caused by inconsistent field names (e.g. `zoom` vs `glassZoom`) or\n * missing enable flags (e.g. lightning `enabled`).\n */\nexport function mapCompositorParamsToCanvasProps(\n  params: FullCompositorParams,\n): WeatherEffectsCanvasProps {\n  return mapWeatherCompositorParamsToCanvasProps(\n    params as WeatherStudioCompositorParams,\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/lib/recover-repo-overrides.ts",
    "content": "import type { WeatherConditionCode } from \"@/lib/weather-authoring/weather-widget/schema\";\nimport type { CheckpointOverrides } from \"../../weather-compositor/presets\";\n\ntype RecoverPayload = {\n  checkpointOverrides?: Partial<\n    Record<WeatherConditionCode, CheckpointOverrides>\n  >;\n};\n\nexport async function recoverRepoCheckpointOverrides(\n  fetchImpl: typeof fetch = fetch,\n): Promise<Partial<Record<WeatherConditionCode, CheckpointOverrides>> | null> {\n  try {\n    const response = await fetchImpl(\"/api/weather-tuning/recover\", {\n      cache: \"no-store\",\n    });\n    if (!response.ok) return null;\n\n    const payload = (await response.json()) as RecoverPayload;\n    if (!payload?.checkpointOverrides) return null;\n    return payload.checkpointOverrides;\n  } catch {\n    return null;\n  }\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/lib/resolve-params.ts",
    "content": "import type { FullCompositorParams } from \"../../weather-compositor/presets\";\nimport { mergeWithOverrides } from \"../../weather-compositor/presets\";\nimport { getInterpolatedOverrides } from \"../../weather-compositor/interpolation\";\nimport type { CheckpointOverrides } from \"../../weather-compositor/interpolation\";\nimport type { TimeCheckpoint } from \"../types\";\n\ntype BaseGetter = (checkpoint: TimeCheckpoint) => FullCompositorParams;\n\n/**\n * Resolve the effective compositor params at a given time-of-day.\n *\n * We treat \"repo\" presets as the baseline defaults, and \"user\" overrides as\n * transient deltas layered on top. Both layers can interpolate across time.\n */\nexport function resolveCompositorParamsAtTime(opts: {\n  timeOfDay: number;\n  rawBaseAtTime: FullCompositorParams;\n  getRawBaseForCheckpoint: BaseGetter;\n  repoCheckpointOverrides: CheckpointOverrides | undefined;\n  getRepoBaseForCheckpoint: BaseGetter;\n  userCheckpointOverrides: CheckpointOverrides | undefined;\n}): FullCompositorParams {\n  const repoInterpolated = getInterpolatedOverrides(\n    opts.repoCheckpointOverrides,\n    opts.timeOfDay,\n    opts.getRawBaseForCheckpoint,\n  );\n  const baseWithRepo = repoInterpolated\n    ? mergeWithOverrides(opts.rawBaseAtTime, repoInterpolated)\n    : opts.rawBaseAtTime;\n\n  const userInterpolated = getInterpolatedOverrides(\n    opts.userCheckpointOverrides,\n    opts.timeOfDay,\n    opts.getRepoBaseForCheckpoint,\n  );\n\n  return userInterpolated\n    ? mergeWithOverrides(baseWithRepo, userInterpolated)\n    : baseWithRepo;\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/lib/studio-timestamp.ts",
    "content": "export function createStudioTimestamp(\n  timeOfDay: number,\n  referenceDate: Date = new Date(),\n): string {\n  const date = new Date(referenceDate);\n  date.setUTCFullYear(2000, 0, 1);\n  const hours = Math.floor(timeOfDay * 24);\n  const minutes = Math.floor((timeOfDay * 24 - hours) * 60);\n  date.setUTCHours(hours, minutes, 0, 0);\n  return date.toISOString();\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/lib/tool-ui-export.ts",
    "content": "import type { WeatherConditionCode } from \"@/lib/weather-authoring/weather-widget/schema\";\nimport {\n  TIME_CHECKPOINTS,\n  type TimeCheckpoint,\n  type WeatherEffectsCheckpointOverrides,\n  type WeatherEffectsOverrides,\n  type WeatherEffectsTunedPresets,\n} from \"@/lib/weather-authoring/weather-widget/effects/tuning\";\nimport type {\n  CheckpointOverrides,\n  ConditionOverrides,\n} from \"../../weather-compositor/presets\";\nimport {\n  extractOverrides,\n  getRawBaseParamsForCondition,\n  mergeWithOverrides,\n} from \"../../weather-compositor/presets\";\nimport { createStudioTimestamp } from \"./studio-timestamp\";\n\nconst CHECKPOINTS: TimeCheckpoint[] = [\"dawn\", \"noon\", \"dusk\", \"midnight\"];\n\nexport const WEATHER_CONDITION_ORDER: WeatherConditionCode[] = [\n  \"clear\",\n  \"partly-cloudy\",\n  \"cloudy\",\n  \"overcast\",\n  \"fog\",\n  \"drizzle\",\n  \"rain\",\n  \"heavy-rain\",\n  \"thunderstorm\",\n  \"snow\",\n  \"sleet\",\n  \"hail\",\n  \"windy\",\n];\n\nfunction isObjectEmpty(value: unknown): boolean {\n  return (\n    value !== null &&\n    typeof value === \"object\" &&\n    Object.keys(value as Record<string, unknown>).length === 0\n  );\n}\n\nexport function mapConditionOverridesToToolUi(\n  input: ConditionOverrides,\n): WeatherEffectsOverrides {\n  const out: WeatherEffectsOverrides = {};\n\n  if (input.layers) {\n    out.layers = input.layers;\n  }\n\n  if (input.celestial) {\n    // Avoid exporting timeOfDay (it’s derived from timestamp in production).\n    const { timeOfDay: _timeOfDay, ...rest } = input.celestial;\n    if (!isObjectEmpty(rest)) {\n      out.celestial = rest;\n    }\n  }\n\n  if (input.cloud) {\n    const {\n      cloudScale,\n      coverage,\n      density,\n      softness,\n      windSpeed,\n      windAngle,\n      turbulence,\n      lightIntensity,\n      ambientDarkness,\n      backlightIntensity,\n      numLayers,\n    } = input.cloud;\n\n    const cloud: Record<string, unknown> = {\n      ...(cloudScale !== undefined ? { cloudScale } : {}),\n      ...(coverage !== undefined ? { coverage } : {}),\n      ...(density !== undefined ? { density } : {}),\n      ...(softness !== undefined ? { softness } : {}),\n      ...(windSpeed !== undefined ? { windSpeed } : {}),\n      ...(windAngle !== undefined ? { windAngle } : {}),\n      ...(turbulence !== undefined ? { turbulence } : {}),\n      ...(lightIntensity !== undefined ? { lightIntensity } : {}),\n      ...(ambientDarkness !== undefined ? { ambientDarkness } : {}),\n      ...(backlightIntensity !== undefined ? { backlightIntensity } : {}),\n      ...(numLayers !== undefined ? { numLayers } : {}),\n    };\n\n    if (!isObjectEmpty(cloud)) {\n      out.cloud = cloud as WeatherEffectsOverrides[\"cloud\"];\n    }\n  }\n\n  const interactions: Record<string, unknown> = {};\n\n  if (input.rain) {\n    const {\n      glassIntensity,\n      zoom,\n      fallingIntensity,\n      fallingSpeed,\n      fallingAngle,\n      fallingStreakLength,\n      fallingLayers,\n      fallingRefraction,\n    } = input.rain;\n\n    const rain: Record<string, unknown> = {\n      ...(glassIntensity !== undefined ? { glassIntensity } : {}),\n      ...(zoom !== undefined ? { glassZoom: zoom } : {}),\n      ...(fallingIntensity !== undefined ? { fallingIntensity } : {}),\n      ...(fallingSpeed !== undefined ? { fallingSpeed } : {}),\n      ...(fallingAngle !== undefined ? { fallingAngle } : {}),\n      ...(fallingStreakLength !== undefined ? { fallingStreakLength } : {}),\n      ...(fallingLayers !== undefined ? { fallingLayers } : {}),\n    };\n\n    if (!isObjectEmpty(rain)) {\n      out.rain = rain as WeatherEffectsOverrides[\"rain\"];\n    }\n\n    if (fallingRefraction !== undefined) {\n      interactions.rainRefractionStrength = fallingRefraction;\n    }\n  }\n\n  if (input.lightning || input.layers?.lightning === true) {\n    const {\n      branchDensity,\n      glowIntensity,\n      autoMode,\n      autoInterval,\n      sceneIllumination,\n    } = input.lightning ?? {};\n\n    const lightning: Record<string, unknown> = {\n      enabled: true,\n      ...(autoMode !== undefined ? { autoMode } : {}),\n      ...(autoInterval !== undefined ? { autoInterval } : {}),\n      ...(branchDensity !== undefined ? { branchDensity } : {}),\n      ...(glowIntensity !== undefined ? { flashIntensity: glowIntensity } : {}),\n    };\n\n    if (!isObjectEmpty(lightning)) {\n      out.lightning = lightning as WeatherEffectsOverrides[\"lightning\"];\n    }\n\n    if (sceneIllumination !== undefined) {\n      interactions.lightningSceneIllumination = sceneIllumination;\n    }\n  }\n\n  if (input.snow) {\n    const {\n      intensity,\n      layers,\n      fallSpeed,\n      windSpeed,\n      windAngle,\n      turbulence,\n      drift,\n      flutter,\n      windShear,\n      flakeSize,\n      sizeVariation,\n      opacity,\n      glowAmount,\n      sparkle,\n    } = input.snow;\n\n    const snow: Record<string, unknown> = {\n      ...(intensity !== undefined ? { intensity } : {}),\n      ...(layers !== undefined ? { layers } : {}),\n      ...(fallSpeed !== undefined ? { fallSpeed } : {}),\n      ...(windSpeed !== undefined ? { windSpeed } : {}),\n      ...(windAngle !== undefined ? { windAngle } : {}),\n      ...(turbulence !== undefined ? { turbulence } : {}),\n      ...(drift !== undefined ? { drift } : {}),\n      ...(flutter !== undefined ? { flutter } : {}),\n      ...(windShear !== undefined ? { windShear } : {}),\n      ...(flakeSize !== undefined ? { flakeSize } : {}),\n      ...(sizeVariation !== undefined ? { sizeVariation } : {}),\n      ...(opacity !== undefined ? { opacity } : {}),\n      ...(glowAmount !== undefined ? { glowAmount } : {}),\n      ...(sparkle !== undefined ? { sparkle } : {}),\n    };\n\n    if (!isObjectEmpty(snow)) {\n      out.snow = snow as WeatherEffectsOverrides[\"snow\"];\n    }\n  }\n\n  if (input.glass) {\n    const {\n      enabled,\n      depth,\n      strength,\n      chromaticAberration,\n      blur,\n      brightness,\n      saturation,\n    } = input.glass;\n\n    const glass: Record<string, unknown> = {\n      ...(enabled !== undefined ? { enabled } : {}),\n      ...(depth !== undefined ? { depth } : {}),\n      ...(strength !== undefined ? { strength } : {}),\n      ...(chromaticAberration !== undefined ? { chromaticAberration } : {}),\n      ...(blur !== undefined ? { blur } : {}),\n      ...(brightness !== undefined ? { brightness } : {}),\n      ...(saturation !== undefined ? { saturation } : {}),\n    };\n\n    if (!isObjectEmpty(glass)) {\n      out.glass = glass as WeatherEffectsOverrides[\"glass\"];\n    }\n  }\n\n  if (!isObjectEmpty(interactions)) {\n    out.interactions = interactions as WeatherEffectsOverrides[\"interactions\"];\n  }\n\n  if (input.post) {\n    out.post = input.post as WeatherEffectsOverrides[\"post\"];\n  }\n\n  return out;\n}\n\nfunction mergeGroup<T extends object>(\n  base: Partial<T> | undefined,\n  delta: Partial<T> | undefined,\n): Partial<T> | undefined {\n  if (!base && !delta) return undefined;\n  return { ...base, ...delta };\n}\n\nexport function mergeWeatherEffectsOverrides(\n  base: WeatherEffectsOverrides | undefined,\n  delta: WeatherEffectsOverrides | undefined,\n): WeatherEffectsOverrides {\n  return {\n    layers: mergeGroup(base?.layers, delta?.layers),\n    celestial: mergeGroup(base?.celestial, delta?.celestial),\n    cloud: mergeGroup(base?.cloud, delta?.cloud),\n    rain: mergeGroup(base?.rain, delta?.rain),\n    lightning: mergeGroup(base?.lightning, delta?.lightning),\n    snow: mergeGroup(base?.snow, delta?.snow),\n    glass: mergeGroup(base?.glass, delta?.glass),\n    interactions: mergeGroup(base?.interactions, delta?.interactions),\n    post: mergeGroup(base?.post, delta?.post),\n  };\n}\n\nexport function mergeTunedPresets(\n  base: WeatherEffectsTunedPresets,\n  delta: WeatherEffectsTunedPresets,\n): WeatherEffectsTunedPresets {\n  const out: WeatherEffectsTunedPresets = { ...base };\n  const conditions = new Set<WeatherConditionCode>([\n    ...(Object.keys(base) as WeatherConditionCode[]),\n    ...(Object.keys(delta) as WeatherConditionCode[]),\n  ]);\n\n  for (const condition of conditions) {\n    const baseCheckpoints = base[condition];\n    const deltaCheckpoints = delta[condition];\n    if (!baseCheckpoints && !deltaCheckpoints) continue;\n\n    const merged: WeatherEffectsCheckpointOverrides = {\n      dawn: mergeWeatherEffectsOverrides(\n        baseCheckpoints?.dawn,\n        deltaCheckpoints?.dawn,\n      ),\n      noon: mergeWeatherEffectsOverrides(\n        baseCheckpoints?.noon,\n        deltaCheckpoints?.noon,\n      ),\n      dusk: mergeWeatherEffectsOverrides(\n        baseCheckpoints?.dusk,\n        deltaCheckpoints?.dusk,\n      ),\n      midnight: mergeWeatherEffectsOverrides(\n        baseCheckpoints?.midnight,\n        deltaCheckpoints?.midnight,\n      ),\n    };\n\n    out[condition] = merged;\n  }\n\n  return out;\n}\n\nfunction hasAnyOverrideGroups(\n  checkpoints: WeatherEffectsCheckpointOverrides,\n): boolean {\n  return CHECKPOINTS.some((checkpoint) => {\n    const groups = checkpoints[checkpoint];\n    return Object.keys(groups).some((key) => {\n      const value = (groups as Record<string, unknown>)[key];\n      return value && !isObjectEmpty(value);\n    });\n  });\n}\n\nexport function buildCanonicalToolUiPresetsForEditedConditions(\n  editedCheckpointOverrides: Partial<\n    Record<WeatherConditionCode, CheckpointOverrides>\n  >,\n  repoCheckpointOverrides: Partial<\n    Record<WeatherConditionCode, CheckpointOverrides>\n  >,\n): WeatherEffectsTunedPresets {\n  const out: WeatherEffectsTunedPresets = {};\n\n  for (const condition of Object.keys(\n    editedCheckpointOverrides,\n  ) as WeatherConditionCode[]) {\n    const edited = editedCheckpointOverrides[condition];\n    if (!edited) continue;\n\n    const mapped: WeatherEffectsCheckpointOverrides = {\n      dawn: {},\n      noon: {},\n      dusk: {},\n      midnight: {},\n    };\n\n    for (const checkpoint of CHECKPOINTS) {\n      const timeOfDay = TIME_CHECKPOINTS[checkpoint];\n      const timestamp = createStudioTimestamp(timeOfDay);\n      const rawBase = getRawBaseParamsForCondition(condition, timestamp);\n      rawBase.celestial.timeOfDay = timeOfDay;\n\n      const withRepo = mergeWithOverrides(\n        rawBase,\n        repoCheckpointOverrides[condition]?.[checkpoint],\n      );\n      const effective = mergeWithOverrides(withRepo, edited[checkpoint]);\n      const canonicalOverrides = extractOverrides(effective, rawBase);\n      mapped[checkpoint] = mapConditionOverridesToToolUi(canonicalOverrides);\n    }\n\n    out[condition] = mapped;\n  }\n\n  return out;\n}\n\nexport function replaceEditedConditions(\n  base: WeatherEffectsTunedPresets,\n  editedConditionPresets: WeatherEffectsTunedPresets,\n): WeatherEffectsTunedPresets {\n  const out: WeatherEffectsTunedPresets = { ...base };\n\n  for (const condition of Object.keys(\n    editedConditionPresets,\n  ) as WeatherConditionCode[]) {\n    const nextCondition = editedConditionPresets[condition];\n    if (nextCondition && hasAnyOverrideGroups(nextCondition)) {\n      out[condition] = nextCondition;\n    } else {\n      delete out[condition];\n    }\n  }\n\n  return out;\n}\n\nexport function toToolUiDelta(\n  checkpointOverrides: Partial<\n    Record<WeatherConditionCode, CheckpointOverrides>\n  >,\n): WeatherEffectsTunedPresets {\n  const out: WeatherEffectsTunedPresets = {};\n\n  for (const condition of Object.keys(\n    checkpointOverrides,\n  ) as WeatherConditionCode[]) {\n    const byCheckpoint = checkpointOverrides[condition];\n    if (!byCheckpoint) continue;\n\n    const mapped: Partial<WeatherEffectsCheckpointOverrides> = {};\n\n    for (const checkpoint of CHECKPOINTS) {\n      const conditionOverrides = byCheckpoint[checkpoint];\n      if (!conditionOverrides) continue;\n      const toolUi = mapConditionOverridesToToolUi(conditionOverrides);\n      if (Object.keys(toolUi).length === 0) continue;\n      mapped[checkpoint] = toolUi;\n    }\n\n    if (Object.keys(mapped).length > 0) {\n      out[condition] = {\n        dawn: mapped.dawn ?? {},\n        noon: mapped.noon ?? {},\n        dusk: mapped.dusk ?? {},\n        midnight: mapped.midnight ?? {},\n      };\n    }\n  }\n\n  return out;\n}\n\nexport function generateToolUiTypeScript(\n  presets: WeatherEffectsTunedPresets,\n  signedOff: Set<WeatherConditionCode>,\n  exportedAt: string = new Date().toISOString(),\n): string {\n  const lines: string[] = [\n    \"// Generated by Weather Tuning Studio\",\n    `// Exported at: ${exportedAt}`,\n    \"\",\n    'import type { WeatherConditionCode } from \"../schema\";',\n    'import type { WeatherEffectsCheckpointOverrides } from \"./tuning\";',\n    \"\",\n    \"export const TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES: Partial<Record<WeatherConditionCode, WeatherEffectsCheckpointOverrides>> = {\",\n  ];\n\n  for (const condition of WEATHER_CONDITION_ORDER) {\n    const conditionCheckpoints = presets[condition];\n    if (!conditionCheckpoints) continue;\n\n    const isSigned = signedOff.has(condition);\n    lines.push(`  // ${condition}${isSigned ? \" ✓ signed off\" : \"\"}`);\n    lines.push(`  \"${condition}\": {`);\n\n    for (const checkpoint of CHECKPOINTS) {\n      const checkpointData = conditionCheckpoints[checkpoint] ?? {};\n      const hasAnyGroups = Object.keys(checkpointData).some((key) => {\n        const value = (checkpointData as Record<string, unknown>)[key];\n        return value && !isObjectEmpty(value);\n      });\n\n      if (!hasAnyGroups) {\n        lines.push(`    ${checkpoint}: {},`);\n        continue;\n      }\n\n      lines.push(`    ${checkpoint}: {`);\n\n      const writeGroup = (key: keyof WeatherEffectsOverrides) => {\n        const value = checkpointData[key];\n        if (!value || isObjectEmpty(value)) return;\n        lines.push(`      ${key}: {`);\n        for (const [k, v] of Object.entries(value)) {\n          lines.push(\n            `        ${k}: ${typeof v === \"number\" ? v.toFixed(4) : JSON.stringify(v)},`,\n          );\n        }\n        lines.push(\"      },\");\n      };\n\n      writeGroup(\"layers\");\n      writeGroup(\"celestial\");\n      writeGroup(\"cloud\");\n      writeGroup(\"rain\");\n      writeGroup(\"lightning\");\n      writeGroup(\"snow\");\n      writeGroup(\"glass\");\n      writeGroup(\"interactions\");\n      writeGroup(\"post\");\n\n      lines.push(\"    },\");\n    }\n\n    lines.push(\"  },\");\n  }\n\n  lines.push(\"};\");\n  lines.push(\"\");\n\n  return lines.join(\"\\n\");\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/lib/tool-ui-import.ts",
    "content": "import type { WeatherConditionCode } from \"@/lib/weather-authoring/weather-widget/schema\";\nimport type {\n  WeatherEffectsCheckpointOverrides,\n  WeatherEffectsOverrides,\n  WeatherEffectsTunedPresets,\n} from \"@/lib/weather-authoring/weather-widget/effects/tuning\";\nimport type {\n  CheckpointOverrides,\n  ConditionOverrides,\n} from \"../../weather-compositor/presets\";\n\nfunction isEmptyObject(value: unknown): boolean {\n  return (\n    value !== null &&\n    typeof value === \"object\" &&\n    Object.keys(value as Record<string, unknown>).length === 0\n  );\n}\n\nexport function mapToolUiOverridesToCompositor(\n  input: WeatherEffectsOverrides,\n): ConditionOverrides {\n  const out: ConditionOverrides = {};\n\n  if (input.layers && !isEmptyObject(input.layers)) {\n    out.layers = input.layers as ConditionOverrides[\"layers\"];\n  }\n\n  if (input.celestial && !isEmptyObject(input.celestial)) {\n    out.celestial = input.celestial as ConditionOverrides[\"celestial\"];\n  }\n\n  if (input.cloud && !isEmptyObject(input.cloud)) {\n    out.cloud = input.cloud as ConditionOverrides[\"cloud\"];\n  }\n\n  if (input.rain && !isEmptyObject(input.rain)) {\n    const rain: Record<string, unknown> = { ...(input.rain as object) };\n    if (\"glassZoom\" in rain) {\n      const zoom = rain.glassZoom as number | undefined;\n      delete rain.glassZoom;\n      if (zoom !== undefined) {\n        rain.zoom = zoom;\n      }\n    }\n    out.rain = rain as ConditionOverrides[\"rain\"];\n  }\n\n  if (input.lightning && !isEmptyObject(input.lightning)) {\n    const lightning: Record<string, unknown> = {\n      ...(input.lightning as object),\n    };\n    if (\"flashIntensity\" in lightning) {\n      const glowIntensity = lightning.flashIntensity as number | undefined;\n      delete lightning.flashIntensity;\n      if (glowIntensity !== undefined) {\n        lightning.glowIntensity = glowIntensity;\n      }\n    }\n    // The studio doesn't need the explicit enabled flag (layers controls visibility).\n    delete lightning.enabled;\n    if (!isEmptyObject(lightning)) {\n      out.lightning = lightning as ConditionOverrides[\"lightning\"];\n    }\n  }\n\n  if (input.snow && !isEmptyObject(input.snow)) {\n    out.snow = input.snow as ConditionOverrides[\"snow\"];\n  }\n\n  if (input.glass && !isEmptyObject(input.glass)) {\n    out.glass = input.glass as ConditionOverrides[\"glass\"];\n  }\n\n  if (input.post && !isEmptyObject(input.post)) {\n    out.post = input.post as ConditionOverrides[\"post\"];\n  }\n\n  if (input.interactions && !isEmptyObject(input.interactions)) {\n    const interactions = input.interactions as Record<string, unknown>;\n\n    if (typeof interactions.rainRefractionStrength === \"number\") {\n      out.rain = {\n        ...out.rain,\n        fallingRefraction: interactions.rainRefractionStrength,\n      } as ConditionOverrides[\"rain\"];\n    }\n\n    if (typeof interactions.lightningSceneIllumination === \"number\") {\n      out.lightning = {\n        ...out.lightning,\n        sceneIllumination: interactions.lightningSceneIllumination,\n      } as ConditionOverrides[\"lightning\"];\n    }\n  }\n\n  return out;\n}\n\nexport function mapToolUiCheckpointsToCompositor(\n  input: WeatherEffectsCheckpointOverrides,\n): CheckpointOverrides {\n  return {\n    dawn: mapToolUiOverridesToCompositor(input.dawn ?? {}),\n    noon: mapToolUiOverridesToCompositor(input.noon ?? {}),\n    dusk: mapToolUiOverridesToCompositor(input.dusk ?? {}),\n    midnight: mapToolUiOverridesToCompositor(input.midnight ?? {}),\n  };\n}\n\nexport function mapToolUiPresetsToCompositor(\n  presets: WeatherEffectsTunedPresets,\n): Partial<Record<WeatherConditionCode, CheckpointOverrides>> {\n  const out: Partial<Record<WeatherConditionCode, CheckpointOverrides>> = {};\n  for (const condition of Object.keys(presets) as WeatherConditionCode[]) {\n    const byCheckpoint = presets[condition];\n    if (!byCheckpoint) continue;\n    out[condition] = mapToolUiCheckpointsToCompositor(byCheckpoint);\n  }\n  return out;\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/lib/workflow-state.ts",
    "content": "import type { WeatherConditionCode } from \"@/lib/weather-authoring/weather-widget/schema\";\nimport type { CheckpointOverrides } from \"../../weather-compositor/presets\";\nimport type { ConditionCheckpoints } from \"../types\";\nimport { SESSION_KEY as LEGACY_WORKFLOW_STATE_STORAGE_KEY } from \"./constants\";\n\nexport const WORKFLOW_STATE_STORAGE_KEY = \"weather-tuning-studio-session\";\nexport const STORAGE_KEY = WORKFLOW_STATE_STORAGE_KEY;\n\nexport interface WorkflowState {\n  checkpoints: Partial<Record<WeatherConditionCode, ConditionCheckpoints>>;\n  signedOff: WeatherConditionCode[];\n  repoCheckpointOverrides?: Partial<\n    Record<WeatherConditionCode, CheckpointOverrides>\n  >;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction parseWorkflowState(parsed: unknown): WorkflowState | null {\n  if (!isRecord(parsed)) return null;\n\n  const checkpoints = isRecord(parsed.checkpoints)\n    ? (parsed.checkpoints as Partial<\n        Record<WeatherConditionCode, ConditionCheckpoints>\n      >)\n    : {};\n\n  const signedOff = Array.isArray(parsed.signedOff)\n    ? parsed.signedOff.filter(\n        (condition): condition is WeatherConditionCode =>\n          typeof condition === \"string\",\n      )\n    : [];\n\n  const repoCheckpointOverrides = isRecord(parsed.repoCheckpointOverrides)\n    ? (parsed.repoCheckpointOverrides as Partial<\n        Record<WeatherConditionCode, CheckpointOverrides>\n      >)\n    : undefined;\n\n  if (repoCheckpointOverrides) {\n    return {\n      checkpoints,\n      signedOff,\n      repoCheckpointOverrides,\n    };\n  }\n\n  return {\n    checkpoints,\n    signedOff,\n  };\n}\n\nexport function loadWorkflowState(): WorkflowState | null {\n  if (typeof window === \"undefined\") return null;\n  try {\n    const stored =\n      window.localStorage.getItem(WORKFLOW_STATE_STORAGE_KEY) ??\n      window.localStorage.getItem(LEGACY_WORKFLOW_STATE_STORAGE_KEY);\n\n    if (!stored) return null;\n    return parseWorkflowState(JSON.parse(stored));\n  } catch {\n    return null;\n  }\n}\n\nexport function saveWorkflowState(state: WorkflowState): void {\n  if (typeof window === \"undefined\") return;\n  try {\n    window.localStorage.setItem(\n      WORKFLOW_STATE_STORAGE_KEY,\n      JSON.stringify(state),\n    );\n  } catch {\n    console.warn(\"Failed to save workflow state to localStorage\");\n  }\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/page.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport Link from \"next/link\";\nimport { ArrowLeft, Download } from \"lucide-react\";\nimport { useTuningState } from \"./hooks/use-tuning-state\";\nimport { TimeDial } from \"./components/time-dial\";\nimport { ConditionSidebar } from \"./components/condition-sidebar\";\nimport { DetailEditor } from \"./components/detail-editor\";\nimport { ExportPanel } from \"./components/export-panel\";\nimport { ViewModeToggle, type ViewMode } from \"./components/view-mode-toggle\";\nimport { ParameterMatrixView } from \"./components/parameter-matrix-view\";\nimport { TimeMatrixView } from \"./components/time-matrix-view\";\nimport { TIME_CHECKPOINT_ORDER } from \"./lib/constants\";\nimport { WEATHER_CONDITIONS } from \"../weather-compositor/presets\";\nimport type { TimeCheckpoint } from \"./types\";\n\nexport default function WeatherTuningPage() {\n  const state = useTuningState();\n  const [viewMode, setViewMode] = useState<ViewMode>(\"time\");\n  const { selectedCondition, setSelectedCondition, goToCheckpoint } = state;\n\n  useEffect(() => {\n    function handleKeyDown(e: KeyboardEvent) {\n      if (\n        e.target instanceof HTMLInputElement ||\n        e.target instanceof HTMLTextAreaElement\n      )\n        return;\n\n      const key = e.key;\n\n      if (key === \"ArrowUp\" || key === \"ArrowDown\") {\n        e.preventDefault();\n        const currentIndex = selectedCondition\n          ? WEATHER_CONDITIONS.indexOf(selectedCondition)\n          : -1;\n\n        let newIndex: number;\n        if (key === \"ArrowUp\") {\n          newIndex =\n            currentIndex <= 0\n              ? WEATHER_CONDITIONS.length - 1\n              : currentIndex - 1;\n        } else {\n          newIndex =\n            currentIndex >= WEATHER_CONDITIONS.length - 1\n              ? 0\n              : currentIndex + 1;\n        }\n\n        setSelectedCondition(WEATHER_CONDITIONS[newIndex]);\n        return;\n      }\n\n      if (!selectedCondition) return;\n\n      if (key >= \"1\" && key <= \"4\") {\n        e.preventDefault();\n        const index = parseInt(key) - 1;\n        const checkpoint = TIME_CHECKPOINT_ORDER[index];\n        if (checkpoint) {\n          goToCheckpoint(selectedCondition, checkpoint);\n        }\n      }\n    }\n\n    window.addEventListener(\"keydown\", handleKeyDown);\n    return () => window.removeEventListener(\"keydown\", handleKeyDown);\n  }, [selectedCondition, setSelectedCondition, goToCheckpoint]);\n\n  const selectedParams = state.selectedCondition\n    ? state.getParamsForCondition(state.selectedCondition)\n    : null;\n  const selectedCanvasProps = state.selectedCondition\n    ? state.getCanvasPropsForCondition(\n        state.selectedCondition,\n        state.globalTimeOfDay,\n      )\n    : null;\n\n  const selectedBaseParams = state.selectedCondition\n    ? state.getBaseParams(state.selectedCondition)\n    : null;\n\n  const progressPercent = Math.round((state.signedOffCount / 13) * 100);\n\n  return (\n    <div className=\"relative flex h-screen flex-col overflow-hidden bg-background\">\n      <header className=\"relative z-10 flex items-center justify-between border-b border-border/50 px-5 py-2\">\n        <div className=\"flex items-center gap-4\">\n          <Link\n            href=\"/sandbox\"\n            className=\"group flex items-center gap-1.5 text-xs text-muted-foreground/70 transition-colors hover:text-foreground\"\n          >\n            <ArrowLeft className=\"size-3.5 transition-transform group-hover:-translate-x-0.5\" />\n            <span>Exit</span>\n          </Link>\n\n          <div className=\"h-4 w-px bg-border/50\" />\n\n          <div className=\"flex items-center gap-3\">\n            <h1 className=\"text-sm font-medium text-foreground/80\">\n              Weather Tuning Studio\n            </h1>\n            <div className=\"flex items-center gap-2\">\n              <div className=\"h-1 w-20 overflow-hidden rounded-full bg-muted/50\">\n                <div\n                  className=\"h-full rounded-full bg-foreground/20 transition-all duration-500\"\n                  style={{ width: `${progressPercent}%` }}\n                />\n              </div>\n              <span className=\"font-mono text-[10px] text-muted-foreground/60\">\n                {state.signedOffCount}/13\n              </span>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"flex items-center gap-4\">\n          <ViewModeToggle value={viewMode} onChange={setViewMode} />\n          <ExportPanel\n            checkpointOverrides={state.checkpointOverrides}\n            signedOff={state.signedOff}\n            onApplied={(checkpointOverrides) => {\n              // Saving means \"these values are the new defaults\".\n              // Adopt the repo presets as the new baseline and clear user deltas,\n              // keeping the current visual output stable.\n              state.setRepoCheckpointOverrides(checkpointOverrides);\n              state.clearStudioDeltas();\n            }}\n            onRecovered={(checkpointOverrides) => {\n              state.setRepoCheckpointOverrides(checkpointOverrides);\n              state.clearStudioDeltas();\n            }}\n          />\n        </div>\n      </header>\n\n      <div className=\"relative z-10 flex min-h-0 flex-1\">\n        {(viewMode === \"condition\" || viewMode === \"time\") && (\n          <ConditionSidebar\n            selectedCondition={state.selectedCondition}\n            signedOff={state.signedOff}\n            checkpoints={state.checkpoints}\n            getOverrideCount={state.getOverrideCount}\n            onSelectCondition={state.setSelectedCondition}\n          />\n        )}\n\n        <main className=\"flex min-h-0 min-w-0 flex-1 flex-col\">\n          {viewMode === \"condition\" && (\n            <div className=\"flex items-center justify-center border-b border-border/40 px-6 py-3\">\n              <TimeDial\n                value={state.globalTimeOfDay}\n                isPreviewing={state.isPreviewing}\n                activeEditCheckpoint={state.activeEditCheckpoint}\n                onScrub={state.scrubTime}\n                onCheckpointClick={(checkpoint: TimeCheckpoint) => {\n                  if (state.selectedCondition) {\n                    state.goToCheckpoint(state.selectedCondition, checkpoint);\n                  }\n                }}\n                onExitPreview={state.exitPreview}\n              />\n            </div>\n          )}\n\n          {viewMode === \"parameter\" ? (\n            <div className=\"min-h-0 flex-1 overflow-hidden\">\n              <ParameterMatrixView tuningState={state} />\n            </div>\n          ) : viewMode === \"time\" ? (\n            <div className=\"min-h-0 flex-1 overflow-hidden\">\n              {state.selectedCondition ? (\n                <TimeMatrixView\n                  tuningState={state}\n                  condition={state.selectedCondition}\n                />\n              ) : (\n                <div className=\"flex h-full items-center justify-center\">\n                  <div className=\"text-center\">\n                    <div className=\"mx-auto mb-4 flex size-16 items-center justify-center rounded-2xl border border-border bg-muted\">\n                      <Download className=\"size-7 text-muted-foreground\" />\n                    </div>\n                    <p className=\"text-sm text-muted-foreground\">\n                      Select a condition from the sidebar to begin tuning\n                    </p>\n                  </div>\n                </div>\n              )}\n            </div>\n          ) : (\n            <div className=\"min-h-0 flex-1 overflow-hidden p-6\">\n              {state.selectedCondition &&\n              selectedParams &&\n              selectedCanvasProps &&\n              selectedBaseParams ? (\n                <DetailEditor\n                  condition={state.selectedCondition}\n                  params={selectedParams}\n                  canvasProps={selectedCanvasProps}\n                  baseParams={selectedBaseParams}\n                  checkpoints={state.getConditionCheckpoints(\n                    state.selectedCondition,\n                  )}\n                  activeEditCheckpoint={state.activeEditCheckpoint}\n                  isPreviewing={state.isPreviewing}\n                  isSignedOff={state.signedOff.has(state.selectedCondition)}\n                  expandedGroups={state.expandedGroups}\n                  currentTime={state.globalTimeOfDay}\n                  showWidgetOverlay={state.showWidgetOverlay}\n                  onParamsChange={(params) =>\n                    state.updateParams(state.selectedCondition!, params)\n                  }\n                  onToggleGroup={state.toggleGroup}\n                  onReset={() => state.resetCondition(state.selectedCondition!)}\n                  onSignOff={() =>\n                    state.toggleSignOff(state.selectedCondition!)\n                  }\n                  onCheckpointClick={(checkpoint) =>\n                    state.goToCheckpoint(state.selectedCondition!, checkpoint)\n                  }\n                  onToggleWidgetOverlay={() =>\n                    state.setShowWidgetOverlay(!state.showWidgetOverlay)\n                  }\n                  onCopyLayer={(targetCondition, layerKey) =>\n                    state.copyLayerFromCondition(\n                      state.selectedCondition!,\n                      targetCondition,\n                      layerKey,\n                    )\n                  }\n                  onCopyLayerToAll={(layerKey) =>\n                    state.copyLayerToAllConditions(\n                      state.selectedCondition!,\n                      layerKey,\n                    )\n                  }\n                  onCopyCheckpoint={(targetCheckpoints) =>\n                    state.copyCheckpointToCheckpoints(\n                      state.selectedCondition!,\n                      state.activeEditCheckpoint,\n                      targetCheckpoints,\n                    )\n                  }\n                />\n              ) : (\n                <div className=\"flex h-full items-center justify-center\">\n                  <div className=\"text-center\">\n                    <div className=\"mx-auto mb-4 flex size-16 items-center justify-center rounded-2xl border border-border bg-muted\">\n                      <Download className=\"size-7 text-muted-foreground\" />\n                    </div>\n                    <p className=\"text-sm text-muted-foreground\">\n                      Select a condition from the sidebar to begin tuning\n                    </p>\n                  </div>\n                </div>\n              )}\n            </div>\n          )}\n        </main>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-tuning/types.ts",
    "content": "import type { WeatherConditionCode } from \"@/lib/weather-authoring/weather-widget/schema\";\nimport type { ConditionOverrides } from \"../weather-compositor/presets\";\n\nexport type CheckpointStatus = \"pending\" | \"reviewed\";\n\nexport interface ConditionCheckpoints {\n  dawn: CheckpointStatus;\n  noon: CheckpointStatus;\n  dusk: CheckpointStatus;\n  midnight: CheckpointStatus;\n}\n\nexport type CompareMode = \"off\" | \"ab\" | \"side-by-side\";\n\nexport interface TuningState {\n  overrides: Partial<Record<WeatherConditionCode, ConditionOverrides>>;\n  globalTimeOfDay: number;\n\n  selectedCondition: WeatherConditionCode | null;\n  expandedGroups: Set<string>;\n  compareMode: CompareMode;\n  compareTarget: WeatherConditionCode | null;\n  showWidgetOverlay: boolean;\n\n  checkpoints: Partial<Record<WeatherConditionCode, ConditionCheckpoints>>;\n  signedOff: Set<WeatherConditionCode>;\n}\n\nexport type { TimeCheckpoint } from \"@/lib/weather-authoring/weather-widget/effects/tuning\";\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-widget/page.tsx",
    "content": "\"use client\";\n\nimport { useState, useMemo } from \"react\";\nimport { useControls, Leva } from \"leva\";\nimport {\n  WeatherWidget,\n  type TemperatureUnit,\n  type WeatherWidgetCurrent,\n  type WeatherWidgetPayload,\n} from \"@/components/tool-ui/weather-widget/runtime\";\n\ninterface LocationPreset {\n  name: string;\n  description: string;\n  location: string;\n  current: WeatherWidgetCurrent;\n  forecast: WeatherWidgetPayload[\"forecast\"];\n  unit: TemperatureUnit;\n}\n\nconst LOCATION_PRESETS: LocationPreset[] = [\n  {\n    name: \"san-diego\",\n    description: \"Clear & Sunny\",\n    location: \"San Diego, CA\",\n    current: {\n      temperature: 76,\n      tempMin: 68,\n      tempMax: 79,\n      conditionCode: \"clear\",\n    },\n    forecast: [\n      { label: \"Tue\", tempMin: 65, tempMax: 78, conditionCode: \"clear\" },\n      { label: \"Wed\", tempMin: 66, tempMax: 81, conditionCode: \"clear\" },\n      {\n        label: \"Thu\",\n        tempMin: 67,\n        tempMax: 82,\n        conditionCode: \"partly-cloudy\",\n      },\n      { label: \"Fri\", tempMin: 68, tempMax: 80, conditionCode: \"clear\" },\n      {\n        label: \"Sat\",\n        tempMin: 64,\n        tempMax: 77,\n        conditionCode: \"partly-cloudy\",\n      },\n    ],\n    unit: \"fahrenheit\",\n  },\n  {\n    name: \"seattle\",\n    description: \"Rainy Week\",\n    location: \"Seattle, WA\",\n    current: {\n      temperature: 52,\n      tempMin: 48,\n      tempMax: 55,\n      conditionCode: \"rain\",\n    },\n    forecast: [\n      { label: \"Mon\", tempMin: 46, tempMax: 54, conditionCode: \"drizzle\" },\n      { label: \"Tue\", tempMin: 47, tempMax: 53, conditionCode: \"rain\" },\n      { label: \"Wed\", tempMin: 45, tempMax: 52, conditionCode: \"heavy-rain\" },\n      { label: \"Thu\", tempMin: 44, tempMax: 51, conditionCode: \"rain\" },\n      { label: \"Fri\", tempMin: 46, tempMax: 55, conditionCode: \"cloudy\" },\n    ],\n    unit: \"fahrenheit\",\n  },\n  {\n    name: \"london\",\n    description: \"Overcast & Foggy\",\n    location: \"London, UK\",\n    current: { temperature: 8, tempMin: 5, tempMax: 10, conditionCode: \"fog\" },\n    forecast: [\n      { label: \"Mon\", tempMin: 4, tempMax: 9, conditionCode: \"fog\" },\n      { label: \"Tue\", tempMin: 6, tempMax: 11, conditionCode: \"overcast\" },\n      { label: \"Wed\", tempMin: 5, tempMax: 10, conditionCode: \"cloudy\" },\n      { label: \"Thu\", tempMin: 7, tempMax: 12, conditionCode: \"drizzle\" },\n      { label: \"Fri\", tempMin: 4, tempMax: 8, conditionCode: \"fog\" },\n    ],\n    unit: \"celsius\",\n  },\n  {\n    name: \"minneapolis\",\n    description: \"Heavy Snow\",\n    location: \"Minneapolis, MN\",\n    current: {\n      temperature: 18,\n      tempMin: 8,\n      tempMax: 22,\n      conditionCode: \"snow\",\n    },\n    forecast: [\n      { label: \"Tue\", tempMin: 5, tempMax: 19, conditionCode: \"snow\" },\n      { label: \"Wed\", tempMin: -2, tempMax: 12, conditionCode: \"snow\" },\n      { label: \"Thu\", tempMin: -8, tempMax: 6, conditionCode: \"cloudy\" },\n      {\n        label: \"Fri\",\n        tempMin: -5,\n        tempMax: 10,\n        conditionCode: \"partly-cloudy\",\n      },\n      { label: \"Sat\", tempMin: 2, tempMax: 18, conditionCode: \"clear\" },\n    ],\n    unit: \"fahrenheit\",\n  },\n  {\n    name: \"kansas-city\",\n    description: \"Thunderstorm\",\n    location: \"Kansas City, MO\",\n    current: {\n      temperature: 72,\n      tempMin: 65,\n      tempMax: 78,\n      conditionCode: \"thunderstorm\",\n    },\n    forecast: [\n      { label: \"Tue\", tempMin: 62, tempMax: 75, conditionCode: \"heavy-rain\" },\n      { label: \"Wed\", tempMin: 58, tempMax: 70, conditionCode: \"rain\" },\n      { label: \"Thu\", tempMin: 55, tempMax: 68, conditionCode: \"cloudy\" },\n      {\n        label: \"Fri\",\n        tempMin: 52,\n        tempMax: 72,\n        conditionCode: \"partly-cloudy\",\n      },\n      { label: \"Sat\", tempMin: 58, tempMax: 76, conditionCode: \"clear\" },\n    ],\n    unit: \"fahrenheit\",\n  },\n  {\n    name: \"chicago\",\n    description: \"Windy City\",\n    location: \"Chicago, IL\",\n    current: {\n      temperature: 45,\n      tempMin: 38,\n      tempMax: 52,\n      conditionCode: \"windy\",\n    },\n    forecast: [\n      { label: \"Tue\", tempMin: 35, tempMax: 48, conditionCode: \"windy\" },\n      {\n        label: \"Wed\",\n        tempMin: 32,\n        tempMax: 45,\n        conditionCode: \"partly-cloudy\",\n      },\n      { label: \"Thu\", tempMin: 30, tempMax: 42, conditionCode: \"cloudy\" },\n      { label: \"Fri\", tempMin: 28, tempMax: 40, conditionCode: \"snow\" },\n      { label: \"Sat\", tempMin: 25, tempMax: 38, conditionCode: \"clear\" },\n    ],\n    unit: \"fahrenheit\",\n  },\n  {\n    name: \"sedona\",\n    description: \"Desert Night\",\n    location: \"Sedona, AZ\",\n    current: {\n      temperature: 45,\n      tempMin: 38,\n      tempMax: 62,\n      conditionCode: \"clear\",\n    },\n    forecast: [\n      { label: \"Tue\", tempMin: 36, tempMax: 64, conditionCode: \"clear\" },\n      { label: \"Wed\", tempMin: 38, tempMax: 66, conditionCode: \"clear\" },\n      {\n        label: \"Thu\",\n        tempMin: 40,\n        tempMax: 68,\n        conditionCode: \"partly-cloudy\",\n      },\n      { label: \"Fri\", tempMin: 42, tempMax: 70, conditionCode: \"clear\" },\n      { label: \"Sat\", tempMin: 39, tempMax: 65, conditionCode: \"clear\" },\n    ],\n    unit: \"fahrenheit\",\n  },\n  {\n    name: \"reykjavik\",\n    description: \"Sleet & Hail\",\n    location: \"Reykjavik, Iceland\",\n    current: {\n      temperature: 2,\n      tempMin: -1,\n      tempMax: 4,\n      conditionCode: \"sleet\",\n    },\n    forecast: [\n      { label: \"Mon\", tempMin: -2, tempMax: 3, conditionCode: \"sleet\" },\n      { label: \"Tue\", tempMin: -3, tempMax: 2, conditionCode: \"hail\" },\n      { label: \"Wed\", tempMin: -1, tempMax: 4, conditionCode: \"snow\" },\n      { label: \"Thu\", tempMin: 0, tempMax: 5, conditionCode: \"overcast\" },\n      { label: \"Fri\", tempMin: 1, tempMax: 6, conditionCode: \"windy\" },\n    ],\n    unit: \"celsius\",\n  },\n  {\n    name: \"bangkok\",\n    description: \"Heavy Rain\",\n    location: \"Bangkok, Thailand\",\n    current: {\n      temperature: 29,\n      tempMin: 26,\n      tempMax: 32,\n      conditionCode: \"heavy-rain\",\n    },\n    forecast: [\n      { label: \"Mon\", tempMin: 25, tempMax: 31, conditionCode: \"heavy-rain\" },\n      { label: \"Tue\", tempMin: 26, tempMax: 33, conditionCode: \"thunderstorm\" },\n      { label: \"Wed\", tempMin: 27, tempMax: 34, conditionCode: \"rain\" },\n      { label: \"Thu\", tempMin: 26, tempMax: 32, conditionCode: \"drizzle\" },\n      {\n        label: \"Fri\",\n        tempMin: 28,\n        tempMax: 35,\n        conditionCode: \"partly-cloudy\",\n      },\n    ],\n    unit: \"celsius\",\n  },\n  {\n    name: \"portland\",\n    description: \"Drizzle\",\n    location: \"Portland, OR\",\n    current: {\n      temperature: 48,\n      tempMin: 42,\n      tempMax: 52,\n      conditionCode: \"drizzle\",\n    },\n    forecast: [\n      { label: \"Mon\", tempMin: 40, tempMax: 50, conditionCode: \"drizzle\" },\n      { label: \"Tue\", tempMin: 42, tempMax: 52, conditionCode: \"cloudy\" },\n      { label: \"Wed\", tempMin: 44, tempMax: 54, conditionCode: \"drizzle\" },\n      { label: \"Thu\", tempMin: 43, tempMax: 53, conditionCode: \"rain\" },\n      {\n        label: \"Fri\",\n        tempMin: 45,\n        tempMax: 55,\n        conditionCode: \"partly-cloudy\",\n      },\n    ],\n    unit: \"fahrenheit\",\n  },\n  {\n    name: \"denver\",\n    description: \"Partly Cloudy\",\n    location: \"Denver, CO\",\n    current: {\n      temperature: 58,\n      tempMin: 45,\n      tempMax: 65,\n      conditionCode: \"partly-cloudy\",\n    },\n    forecast: [\n      {\n        label: \"Mon\",\n        tempMin: 42,\n        tempMax: 62,\n        conditionCode: \"partly-cloudy\",\n      },\n      { label: \"Tue\", tempMin: 40, tempMax: 60, conditionCode: \"clear\" },\n      { label: \"Wed\", tempMin: 38, tempMax: 58, conditionCode: \"cloudy\" },\n      { label: \"Thu\", tempMin: 35, tempMax: 55, conditionCode: \"snow\" },\n      { label: \"Fri\", tempMin: 30, tempMax: 50, conditionCode: \"clear\" },\n    ],\n    unit: \"fahrenheit\",\n  },\n  {\n    name: \"pittsburgh\",\n    description: \"Overcast\",\n    location: \"Pittsburgh, PA\",\n    current: {\n      temperature: 42,\n      tempMin: 36,\n      tempMax: 48,\n      conditionCode: \"overcast\",\n    },\n    forecast: [\n      { label: \"Mon\", tempMin: 34, tempMax: 46, conditionCode: \"overcast\" },\n      { label: \"Tue\", tempMin: 32, tempMax: 44, conditionCode: \"cloudy\" },\n      { label: \"Wed\", tempMin: 30, tempMax: 42, conditionCode: \"rain\" },\n      { label: \"Thu\", tempMin: 28, tempMax: 40, conditionCode: \"overcast\" },\n      {\n        label: \"Fri\",\n        tempMin: 32,\n        tempMax: 45,\n        conditionCode: \"partly-cloudy\",\n      },\n    ],\n    unit: \"fahrenheit\",\n  },\n  {\n    name: \"sf\",\n    description: \"Cloudy\",\n    location: \"San Francisco, CA\",\n    current: {\n      temperature: 58,\n      tempMin: 52,\n      tempMax: 62,\n      conditionCode: \"cloudy\",\n    },\n    forecast: [\n      { label: \"Mon\", tempMin: 50, tempMax: 60, conditionCode: \"cloudy\" },\n      { label: \"Tue\", tempMin: 52, tempMax: 62, conditionCode: \"fog\" },\n      {\n        label: \"Wed\",\n        tempMin: 54,\n        tempMax: 64,\n        conditionCode: \"partly-cloudy\",\n      },\n      { label: \"Thu\", tempMin: 55, tempMax: 65, conditionCode: \"clear\" },\n      { label: \"Fri\", tempMin: 53, tempMax: 63, conditionCode: \"cloudy\" },\n    ],\n    unit: \"fahrenheit\",\n  },\n];\n\nfunction formatTimeLabel(timeOfDay: number): string {\n  const totalMinutes = timeOfDay * 24 * 60;\n  const hours = Math.floor(totalMinutes / 60);\n  const minutes = Math.round(totalMinutes % 60);\n  const period = hours >= 12 ? \"PM\" : \"AM\";\n  const displayHour = hours % 12 || 12;\n  return `${displayHour}:${minutes.toString().padStart(2, \"0\")} ${period}`;\n}\n\nfunction timeToISOString(timeOfDay: number): string {\n  const totalMinutes = timeOfDay * 24 * 60;\n  const hours = Math.floor(totalMinutes / 60);\n  const minutes = Math.round(totalMinutes % 60);\n  const now = new Date();\n  // Keep this aligned with `getTimeOfDay`, which interprets timestamps in UTC.\n  now.setUTCHours(hours, minutes, 0, 0);\n  return now.toISOString();\n}\n\ninterface LocationPillProps {\n  preset: LocationPreset;\n  isActive: boolean;\n  onClick: () => void;\n}\n\nfunction LocationPill({ preset, isActive, onClick }: LocationPillProps) {\n  return (\n    <button\n      onClick={onClick}\n      className={`relative rounded-full px-3 py-1.5 text-xs font-medium whitespace-nowrap transition-all ${\n        isActive\n          ? \"bg-white/20 text-white ring-1 ring-white/30\"\n          : \"bg-white/10 text-white/60 hover:bg-white/15 hover:text-white/80\"\n      } `}\n    >\n      <span className=\"block\">{preset.location.split(\",\")[0]}</span>\n      <span className=\"block text-[10px] opacity-60\">{preset.description}</span>\n    </button>\n  );\n}\n\nexport default function WeatherWidgetSandbox() {\n  const [activePresetIndex, setActivePresetIndex] = useState(0);\n  const activePreset = LOCATION_PRESETS[activePresetIndex];\n\n  const [{ timeOfDay, effectsEnabled, quality }] = useControls(\n    \"Settings\",\n    () => ({\n      timeOfDay: {\n        value: 0.5,\n        min: 0,\n        max: 1,\n        step: 0.01,\n        label: \"Time of Day\",\n      },\n      effectsEnabled: { value: true, label: \"Effects\" },\n      quality: {\n        value: \"high\" as const,\n        options: [\"low\", \"medium\", \"high\", \"auto\"] as const,\n        label: \"Quality\",\n      },\n    }),\n  );\n\n  const timestamp = useMemo(() => timeToISOString(timeOfDay), [timeOfDay]);\n\n  const widgetData: WeatherWidgetPayload = {\n    version: \"3.1\",\n    id: `weather-widget-${activePreset.name}`,\n    location: { name: activePreset.location },\n    units: { temperature: activePreset.unit },\n    current: activePreset.current,\n    forecast: activePreset.forecast,\n    time: { localTimeOfDay: timeOfDay },\n    updatedAt: timestamp,\n  };\n\n  return (\n    <div className=\"relative min-h-screen bg-linear-to-b from-slate-900 to-slate-950\">\n      <Leva\n        collapsed={false}\n        flat={false}\n        titleBar={{ title: \"Weather Widget\" }}\n        theme={{\n          sizes: {\n            rootWidth: \"240px\",\n            controlWidth: \"120px\",\n          },\n        }}\n      />\n\n      <div className=\"absolute inset-0 flex flex-col items-center justify-center gap-6 p-8\">\n        <div className=\"flex w-full max-w-[800px] flex-wrap items-center justify-center gap-2\">\n          {LOCATION_PRESETS.map((preset, index) => (\n            <LocationPill\n              key={preset.name}\n              preset={preset}\n              isActive={index === activePresetIndex}\n              onClick={() => setActivePresetIndex(index)}\n            />\n          ))}\n        </div>\n\n        <div className=\"relative\">\n          <WeatherWidget\n            {...widgetData}\n            effects={{\n              enabled: effectsEnabled,\n              quality,\n            }}\n          />\n        </div>\n\n        <div className=\"rounded bg-black/50 px-3 py-1.5 text-sm text-white/80 backdrop-blur-sm\">\n          {formatTimeLabel(timeOfDay)} · {activePreset.current.conditionCode}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-widget-production/page.tsx",
    "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { Leva, useControls } from \"leva\";\n\nimport {\n  WeatherWidget,\n  type PrecipitationLevel,\n  type TemperatureUnit,\n  type WeatherConditionCode,\n  type WeatherWidgetPayload,\n} from \"@/components/tool-ui/weather-widget/runtime\";\nimport type { EffectQuality } from \"@/components/tool-ui/weather-widget/schema-runtime\";\nimport {\n  getSceneBrightnessFromTimeOfDay,\n  getWeatherTheme,\n} from \"@/components/tool-ui/weather-widget/generated/weather-runtime-core.generated\";\nimport { WeatherDataOverlay } from \"@/components/tool-ui/weather-widget/weather-data-overlay\";\nimport { cn } from \"@/lib/utils\";\nimport { resolveWeatherEffectsCanvasRuntimeProps as resolveBaseCanvasProps } from \"@/lib/weather-authoring/weather-widget/effects/canvas-resolver-runtime\";\nimport {\n  resolveEffectCanvasDpr,\n  resolveEffectQuality,\n} from \"@/lib/weather-authoring/weather-widget/effects/effect-compositor-quality\";\nimport { TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES } from \"@/lib/weather-authoring/weather-widget/effects/generated/tuned-presets.generated\";\nimport { getNearestCheckpoint } from \"@/lib/weather-authoring/weather-widget/effects/tuning\";\nimport { WeatherEffectsCanvas } from \"@/lib/weather-authoring/weather-widget/effects/weather-effects-canvas\";\nimport type { WeatherEffectsCanvasProps } from \"@/lib/weather-authoring/weather-widget/effects/weather-effects-types\";\nimport { resolveWeatherEffectsCanvasRuntimeProps as resolveRuntimeDefaults } from \"@/lib/weather-authoring/weather-widget/effects/weather-effects-props\";\nimport { createProductionHarnessRuntimeInput } from \"./runtime-input\";\n\nconst CONDITION_OPTIONS: WeatherConditionCode[] = [\n  \"clear\",\n  \"partly-cloudy\",\n  \"cloudy\",\n  \"overcast\",\n  \"fog\",\n  \"drizzle\",\n  \"rain\",\n  \"heavy-rain\",\n  \"thunderstorm\",\n  \"snow\",\n  \"sleet\",\n  \"hail\",\n  \"windy\",\n];\n\nconst PRECIPITATION_OPTIONS: PrecipitationLevel[] = [\n  \"none\",\n  \"light\",\n  \"moderate\",\n  \"heavy\",\n];\n\nconst TEMPERATURE_UNIT_OPTIONS: TemperatureUnit[] = [\"celsius\", \"fahrenheit\"];\nconst QUALITY_OPTIONS: EffectQuality[] = [\"low\", \"medium\", \"high\", \"auto\"];\n\nfunction timeToISOString(timeOfDay: number): string {\n  const now = new Date();\n  const totalMinutes = timeOfDay * 24 * 60;\n  const hours = Math.floor(totalMinutes / 60);\n  const minutes = Math.round(totalMinutes % 60);\n  now.setUTCHours(hours, minutes, 0, 0);\n  return now.toISOString();\n}\n\nfunction formatValue(value: number): string {\n  return value.toFixed(3);\n}\n\nfunction formatDelta(next: number, prev: number): string {\n  const delta = next - prev;\n  const sign = delta > 0 ? \"+\" : \"\";\n  return `${sign}${delta.toFixed(3)}`;\n}\n\ninterface UntunedPreviewProps {\n  payload: WeatherWidgetPayload;\n  effectsEnabled: boolean;\n  quality: EffectQuality;\n  canvasProps: WeatherEffectsCanvasProps;\n  timeOfDay: number;\n}\n\nfunction UntunedPreview({\n  payload,\n  effectsEnabled,\n  quality,\n  canvasProps,\n  timeOfDay,\n}: UntunedPreviewProps) {\n  const weatherTheme = getWeatherTheme(\n    getSceneBrightnessFromTimeOfDay(timeOfDay, payload.current.conditionCode),\n    undefined,\n  );\n  const isDarkTheme = weatherTheme === \"dark\";\n  const qualityTier = resolveEffectQuality(quality);\n  const dpr = resolveEffectCanvasDpr(qualityTier);\n\n  return (\n    <article data-slot=\"weather-widget\" className=\"isolate w-full max-w-md\">\n      <div\n        data-slot=\"card\"\n        className={cn(\n          \"@container/weather [container-type:size] relative aspect-[4/3] overflow-clip rounded-2xl border-0 p-0 shadow-none\",\n          isDarkTheme\n            ? \"bg-gradient-to-b from-zinc-950 via-zinc-900/70 to-zinc-950\"\n            : \"bg-gradient-to-b from-sky-50 via-sky-100/70 to-white\",\n        )}\n      >\n        {effectsEnabled ? (\n          <div\n            className=\"absolute inset-0 overflow-hidden\"\n            style={{ pointerEvents: \"none\", borderRadius: \"inherit\" }}\n            aria-hidden=\"true\"\n          >\n            <WeatherEffectsCanvas\n              className=\"absolute inset-0\"\n              dpr={dpr}\n              {...canvasProps}\n            />\n          </div>\n        ) : null}\n\n        <WeatherDataOverlay\n          location={payload.location.name}\n          conditionCode={payload.current.conditionCode}\n          temperature={payload.current.temperature}\n          tempHigh={payload.current.tempMax}\n          tempLow={payload.current.tempMin}\n          forecast={payload.forecast}\n          unit={payload.units.temperature}\n          theme={weatherTheme}\n          timeOfDay={timeOfDay}\n          timestamp={payload.updatedAt}\n          reducedMotion={false}\n        />\n      </div>\n    </article>\n  );\n}\n\nexport default function WeatherWidgetProductionHarnessPage() {\n  const {\n    locationName,\n    conditionCode,\n    temperatureUnit,\n    temperature,\n    tempMin,\n    tempMax,\n    windSpeed,\n    precipitationLevel,\n    visibility,\n    timeOfDay,\n    effectsEnabled,\n    quality,\n  } = useControls(\"Production Payload\", {\n    locationName: { value: \"San Francisco, CA\" },\n    conditionCode: { value: \"overcast\", options: CONDITION_OPTIONS },\n    temperatureUnit: { value: \"celsius\", options: TEMPERATURE_UNIT_OPTIONS },\n    temperature: { value: 22, min: -30, max: 55, step: 0.1 },\n    tempMin: { value: 20, min: -40, max: 45, step: 0.1 },\n    tempMax: { value: 24, min: -30, max: 60, step: 0.1 },\n    windSpeed: { value: 3.3, min: 0, max: 25, step: 0.1 },\n    precipitationLevel: { value: \"none\", options: PRECIPITATION_OPTIONS },\n    visibility: { value: 10000, min: 100, max: 20000, step: 100 },\n    timeOfDay: { value: 0.5, min: 0, max: 1, step: 0.01 },\n    effectsEnabled: { value: true },\n    quality: { value: \"high\", options: QUALITY_OPTIONS },\n  });\n\n  const updatedAt = useMemo(() => timeToISOString(timeOfDay), [timeOfDay]);\n\n  const payload = useMemo<WeatherWidgetPayload>(\n    () => ({\n      version: \"3.1\",\n      id: \"weather-widget-production-harness\",\n      location: { name: locationName },\n      units: { temperature: temperatureUnit as TemperatureUnit },\n      current: {\n        conditionCode: conditionCode as WeatherConditionCode,\n        temperature,\n        tempMin,\n        tempMax,\n        windSpeed,\n        precipitationLevel: precipitationLevel as PrecipitationLevel,\n        visibility,\n      },\n      forecast: [\n        {\n          label: \"Tue\",\n          tempMin: tempMin - 1,\n          tempMax: tempMax,\n          conditionCode: conditionCode as WeatherConditionCode,\n        },\n        {\n          label: \"Wed\",\n          tempMin,\n          tempMax: tempMax + 1,\n          conditionCode: conditionCode as WeatherConditionCode,\n        },\n        {\n          label: \"Thu\",\n          tempMin: tempMin - 2,\n          tempMax: tempMax + 2,\n          conditionCode: conditionCode as WeatherConditionCode,\n        },\n        {\n          label: \"Fri\",\n          tempMin: tempMin - 1,\n          tempMax: tempMax + 1,\n          conditionCode: conditionCode as WeatherConditionCode,\n        },\n        {\n          label: \"Sat\",\n          tempMin,\n          tempMax,\n          conditionCode: conditionCode as WeatherConditionCode,\n        },\n      ],\n      time: { localTimeOfDay: timeOfDay },\n      updatedAt,\n    }),\n    [\n      conditionCode,\n      locationName,\n      precipitationLevel,\n      temperature,\n      temperatureUnit,\n      tempMax,\n      tempMin,\n      timeOfDay,\n      updatedAt,\n      visibility,\n      windSpeed,\n    ],\n  );\n\n  const { current, updatedAt: payloadUpdatedAt } = payload;\n  const runtimeInput = useMemo(\n    () =>\n      createProductionHarnessRuntimeInput({\n        conditionCode: current.conditionCode,\n        windSpeed: current.windSpeed ?? 0,\n        precipitationLevel: current.precipitationLevel ?? \"none\",\n        visibility: current.visibility ?? 10000,\n        timestamp: payloadUpdatedAt ?? updatedAt,\n        timeOfDay,\n      }),\n    [current, payloadUpdatedAt, timeOfDay, updatedAt],\n  );\n\n  const untunedCanvasProps = useMemo(\n    () => resolveBaseCanvasProps(runtimeInput),\n    [runtimeInput],\n  );\n  const tunedCanvasProps = useMemo(\n    () =>\n      resolveBaseCanvasProps({\n        ...runtimeInput,\n        tunedPresets: TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES,\n      }),\n    [runtimeInput],\n  );\n\n  const untunedRuntime = useMemo(\n    () => resolveRuntimeDefaults(untunedCanvasProps),\n    [untunedCanvasProps],\n  );\n  const tunedRuntime = useMemo(\n    () => resolveRuntimeDefaults(tunedCanvasProps),\n    [tunedCanvasProps],\n  );\n\n  const checkpoint = getNearestCheckpoint(runtimeInput.timeOfDay);\n  const metrics = [\n    {\n      label: \"post.bloomIntensity\",\n      untuned: untunedRuntime.post.bloomIntensity,\n      tuned: tunedRuntime.post.bloomIntensity,\n    },\n    {\n      label: \"post.bloomThreshold\",\n      untuned: untunedRuntime.post.bloomThreshold,\n      tuned: tunedRuntime.post.bloomThreshold,\n    },\n    {\n      label: \"post.exposureIntensity\",\n      untuned: untunedRuntime.post.exposureIntensity,\n      tuned: tunedRuntime.post.exposureIntensity,\n    },\n    {\n      label: \"post.godRayIntensity\",\n      untuned: untunedRuntime.post.godRayIntensity,\n      tuned: tunedRuntime.post.godRayIntensity,\n    },\n    {\n      label: \"post.haze\",\n      untuned: untunedRuntime.post.haze,\n      tuned: tunedRuntime.post.haze,\n    },\n    {\n      label: \"cloud.ambientDarkness\",\n      untuned: untunedRuntime.cloud.ambientDarkness,\n      tuned: tunedRuntime.cloud.ambientDarkness,\n    },\n  ];\n\n  return (\n    <div className=\"relative min-h-screen bg-linear-to-b from-slate-900 to-slate-950 px-6 py-8 text-white\">\n      <Leva\n        collapsed={false}\n        titleBar={{ title: \"Production Harness Controls\" }}\n        theme={{\n          sizes: {\n            rootWidth: \"320px\",\n            controlWidth: \"140px\",\n          },\n        }}\n      />\n\n      <div className=\"mx-auto flex w-full max-w-6xl flex-col gap-6\">\n        <header className=\"space-y-2\">\n          <h1 className=\"text-xl font-semibold tracking-tight\">\n            Weather Widget Production Harness\n          </h1>\n          <p className=\"max-w-3xl text-sm text-slate-300\">\n            Left card is the production component with tuned checkpoint\n            overrides. Right card uses the same payload but skips tuned\n            overrides to expose baseline behavior.\n          </p>\n          <p className=\"text-xs text-slate-400\">\n            Active checkpoint:{\" \"}\n            <span className=\"font-semibold text-slate-200\">{checkpoint}</span> ·\n            condition:{\" \"}\n            <span className=\"font-semibold text-slate-200\">\n              {conditionCode}\n            </span>\n          </p>\n        </header>\n\n        <section className=\"grid grid-cols-1 gap-6 xl:grid-cols-2\">\n          <div className=\"space-y-3\">\n            <h2 className=\"text-sm font-medium text-slate-200\">\n              Production (tuned overrides on)\n            </h2>\n            <WeatherWidget\n              {...payload}\n              effects={{\n                enabled: effectsEnabled,\n                quality: quality as EffectQuality,\n              }}\n            />\n          </div>\n\n          <div className=\"space-y-3\">\n            <h2 className=\"text-sm font-medium text-slate-200\">\n              Untuned baseline (same payload)\n            </h2>\n            <UntunedPreview\n              payload={payload}\n              effectsEnabled={effectsEnabled}\n              quality={quality as EffectQuality}\n              canvasProps={untunedCanvasProps}\n              timeOfDay={runtimeInput.timeOfDay}\n            />\n          </div>\n        </section>\n\n        <section className=\"overflow-hidden rounded-xl border border-white/10 bg-black/30 backdrop-blur\">\n          <div className=\"border-b border-white/10 px-4 py-3 text-sm font-medium text-slate-100\">\n            Runtime post-process diagnostics (untuned vs tuned)\n          </div>\n          <table className=\"w-full text-left text-xs\">\n            <thead className=\"bg-white/5 text-slate-300\">\n              <tr>\n                <th className=\"px-4 py-2 font-medium\">Metric</th>\n                <th className=\"px-4 py-2 font-medium\">Untuned</th>\n                <th className=\"px-4 py-2 font-medium\">Tuned</th>\n                <th className=\"px-4 py-2 font-medium\">Delta</th>\n              </tr>\n            </thead>\n            <tbody className=\"divide-y divide-white/10\">\n              {metrics.map((metric) => (\n                <tr key={metric.label}>\n                  <td className=\"px-4 py-2 font-mono text-slate-200\">\n                    {metric.label}\n                  </td>\n                  <td className=\"px-4 py-2 font-mono text-slate-300\">\n                    {formatValue(metric.untuned)}\n                  </td>\n                  <td className=\"px-4 py-2 font-mono text-slate-100\">\n                    {formatValue(metric.tuned)}\n                  </td>\n                  <td className=\"px-4 py-2 font-mono text-amber-200\">\n                    {formatDelta(metric.tuned, metric.untuned)}\n                  </td>\n                </tr>\n              ))}\n            </tbody>\n          </table>\n        </section>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-widget-production/runtime-input.ts",
    "content": "import type {\n  PrecipitationLevel,\n  WeatherConditionCode,\n} from \"@/components/tool-ui/weather-widget/runtime\";\nimport { snapTimeOfDayToNearestCheckpoint } from \"@/components/tool-ui/weather-widget/generated/weather-runtime-core.generated\";\n\nexport interface ProductionHarnessRuntimeInput {\n  conditionCode: WeatherConditionCode;\n  windSpeed: number;\n  precipitationLevel: PrecipitationLevel;\n  visibility: number;\n  timestamp: string;\n  timeOfDay: number;\n}\n\nexport function createProductionHarnessRuntimeInput(\n  input: ProductionHarnessRuntimeInput,\n): ProductionHarnessRuntimeInput {\n  return {\n    ...input,\n    timeOfDay: snapTimeOfDayToNearestCheckpoint(input.timeOfDay),\n  };\n}\n"
  },
  {
    "path": "apps/www/app/sandbox/weather-widget-stress/page.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useMemo, useRef, useState, type ReactNode } from \"react\";\nimport { Leva, useControls, button } from \"leva\";\nimport {\n  WeatherWidget,\n  ForecastDay,\n  PrecipitationLevel,\n  TemperatureUnit,\n  WeatherConditionCode,\n} from \"@/components/tool-ui/weather-widget/runtime\";\nimport { cn } from \"@/lib/ui/cn\";\n\nconst CONDITIONS: WeatherConditionCode[] = [\n  \"clear\",\n  \"partly-cloudy\",\n  \"cloudy\",\n  \"overcast\",\n  \"fog\",\n  \"drizzle\",\n  \"rain\",\n  \"heavy-rain\",\n  \"thunderstorm\",\n  \"snow\",\n  \"sleet\",\n  \"hail\",\n  \"windy\",\n];\n\nconst WEBGL_SAFE_WIDGET_COUNT = 8;\n\n// A representative subset that exercises the major effect pathways:\n// - clouds, fog, rain, heavy rain, lightning, snow\nconst CONDITION_SPECTRUM: WeatherConditionCode[] = [\n  \"clear\",\n  \"partly-cloudy\",\n  \"overcast\",\n  \"fog\",\n  \"rain\",\n  \"heavy-rain\",\n  \"thunderstorm\",\n  \"snow\",\n];\n\nconst CITIES = [\n  { location: \"San Diego, CA\", conditionCode: \"clear\" as const },\n  { location: \"Seattle, WA\", conditionCode: \"rain\" as const },\n  { location: \"London, UK\", conditionCode: \"fog\" as const },\n  { location: \"Minneapolis, MN\", conditionCode: \"snow\" as const },\n  { location: \"Kansas City, MO\", conditionCode: \"thunderstorm\" as const },\n  { location: \"Chicago, IL\", conditionCode: \"windy\" as const },\n  { location: \"Bangkok, Thailand\", conditionCode: \"heavy-rain\" as const },\n  { location: \"Reykjavik, Iceland\", conditionCode: \"sleet\" as const },\n  { location: \"Denver, CO\", conditionCode: \"partly-cloudy\" as const },\n  { location: \"San Francisco, CA\", conditionCode: \"cloudy\" as const },\n];\n\nconst WEEKDAYS = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"] as const;\n\nfunction clamp(value: number, min: number, max: number): number {\n  return Math.max(min, Math.min(max, value));\n}\n\nfunction mulberry32(seed: number): () => number {\n  let a = seed >>> 0;\n  return () => {\n    a += 0x6d2b79f5;\n    let t = a;\n    t = Math.imul(t ^ (t >>> 15), t | 1);\n    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nfunction pick<T>(rand: () => number, items: readonly T[]): T {\n  return items[Math.floor(rand() * items.length)]!;\n}\n\nfunction timeToISOString(timeOfDay: number): string {\n  const totalMinutes = timeOfDay * 24 * 60;\n  const hours = Math.floor(totalMinutes / 60);\n  const minutes = Math.round(totalMinutes % 60);\n  const now = new Date();\n  // Keep this aligned with `getTimeOfDay`, which interprets timestamps in UTC.\n  now.setUTCHours(hours, minutes, 0, 0);\n  return now.toISOString();\n}\n\nfunction baseTempForCondition(\n  conditionCode: WeatherConditionCode,\n  unit: TemperatureUnit,\n): number {\n  const f: Record<WeatherConditionCode, number> = {\n    clear: 78,\n    \"partly-cloudy\": 70,\n    cloudy: 62,\n    overcast: 58,\n    fog: 54,\n    drizzle: 52,\n    rain: 50,\n    \"heavy-rain\": 47,\n    thunderstorm: 72,\n    snow: 18,\n    sleet: 34,\n    hail: 38,\n    windy: 45,\n  };\n  const tempF = f[conditionCode] ?? 65;\n  if (unit === \"fahrenheit\") return tempF;\n  return Math.round(((tempF - 32) * 5) / 9);\n}\n\nfunction precipitationForCondition(\n  conditionCode: WeatherConditionCode,\n): PrecipitationLevel | undefined {\n  switch (conditionCode) {\n    case \"drizzle\":\n      return \"light\";\n    case \"rain\":\n      return \"moderate\";\n    case \"heavy-rain\":\n    case \"thunderstorm\":\n      return \"heavy\";\n    case \"snow\":\n    case \"sleet\":\n    case \"hail\":\n      return \"moderate\";\n    default:\n      return \"none\";\n  }\n}\n\nfunction buildForecast(\n  rand: () => number,\n  conditionCode: WeatherConditionCode,\n  unit: TemperatureUnit,\n): ForecastDay[] {\n  const base = baseTempForCondition(conditionCode, unit);\n  const start = Math.floor(rand() * WEEKDAYS.length);\n\n  return Array.from({ length: 7 })\n    .slice(0, 7)\n    .map((_, i) => {\n      const day = WEEKDAYS[(start + i) % WEEKDAYS.length]!;\n      const hi = base + Math.round((rand() - 0.4) * 10);\n      const lo = hi - (3 + Math.round(rand() * 8));\n      const maybeCondition = i === 0 ? conditionCode : pick(rand, CONDITIONS);\n      return {\n        label: day,\n        tempMin: lo,\n        tempMax: hi,\n        conditionCode: maybeCondition,\n      };\n    });\n}\n\nexport default function WeatherWidgetStressPage() {\n  const [remountSeed, setRemountSeed] = useState(0);\n\n  const [stress] = useControls(\"Stress\", () => ({\n    count: {\n      value: WEBGL_SAFE_WIDGET_COUNT,\n      min: 1,\n      max: WEBGL_SAFE_WIDGET_COUNT,\n      step: 1,\n      label: \"WebGL widgets\",\n    },\n    columns: { value: 4, min: 1, max: 6, step: 1 },\n    fillCells: { value: true, label: \"Fill grid cells\" },\n    remountAll: button(() => setRemountSeed((s) => s + 1)),\n    autoRemount: { value: false, label: \"Auto-remount\" },\n    autoRemountSeconds: {\n      value: 5,\n      min: 1,\n      max: 30,\n      step: 1,\n      label: \"Auto-remount (s)\",\n    },\n  }));\n\n  const [timeControls, setTimeControls] = useControls(\"Time\", () => ({\n    timeOfDay: { value: 0.5, min: 0, max: 1, step: 0.001 },\n    animate: { value: false, label: \"Animate time\" },\n    speed: {\n      value: 0.03,\n      min: 0.001,\n      max: 0.2,\n      step: 0.001,\n      label: \"Speed (Δ/second)\",\n    },\n  }));\n\n  const [effectsControls] = useControls(\"Effects\", () => ({\n    enabled: { value: true },\n    reducedMotion: { value: false, label: \"Reduced motion\" },\n    quality: {\n      value: \"auto\" as const,\n      options: [\"low\", \"medium\", \"high\", \"auto\"] as const,\n    },\n  }));\n\n  const [dataControls] = useControls(\"Data\", () => ({\n    includeUpdatedAt: { value: true },\n    includeExtras: { value: true },\n    unit: {\n      value: \"fahrenheit\" as const,\n      options: [\"fahrenheit\", \"celsius\"] as const,\n    },\n  }));\n\n  const timeOfDayRef = useRef(timeControls.timeOfDay);\n  useEffect(() => {\n    timeOfDayRef.current = timeControls.timeOfDay;\n  }, [timeControls.timeOfDay]);\n\n  useEffect(() => {\n    if (!timeControls.animate) return;\n\n    const intervalMs = 120;\n    const id = window.setInterval(() => {\n      const next =\n        (timeOfDayRef.current + timeControls.speed * (intervalMs / 1000)) % 1;\n      timeOfDayRef.current = next;\n      setTimeControls({ timeOfDay: next });\n    }, intervalMs);\n\n    return () => window.clearInterval(id);\n  }, [setTimeControls, timeControls.animate, timeControls.speed]);\n\n  useEffect(() => {\n    if (!stress.autoRemount) return;\n    const id = window.setInterval(\n      () => setRemountSeed((s) => s + 1),\n      stress.autoRemountSeconds * 1000,\n    );\n    return () => window.clearInterval(id);\n  }, [stress.autoRemount, stress.autoRemountSeconds]);\n\n  const timestamp = useMemo(\n    () => timeToISOString(timeControls.timeOfDay),\n    [timeControls.timeOfDay],\n  );\n\n  const globalEffects = useMemo(() => {\n    return {\n      enabled: effectsControls.enabled,\n      reducedMotion: effectsControls.reducedMotion,\n      quality: effectsControls.quality,\n    } as const;\n  }, [\n    effectsControls.enabled,\n    effectsControls.quality,\n    effectsControls.reducedMotion,\n  ]);\n\n  const gridItems = useMemo(() => {\n    const items: Array<{\n      key: string;\n      widget: ReactNode;\n      label: string;\n    }> = [];\n\n    for (let i = 0; i < stress.count; i++) {\n      const rand = mulberry32(remountSeed * 100_000 + i * 9973 + 42);\n\n      const city = CITIES[i % CITIES.length]!;\n      const unit = dataControls.unit;\n\n      const condition = CONDITION_SPECTRUM[i % CONDITION_SPECTRUM.length]!;\n      const baseTemp = baseTempForCondition(condition, unit);\n      const includeUpdatedAt = dataControls.includeUpdatedAt;\n      const includeExtras = dataControls.includeExtras;\n      const location = city.location;\n\n      const currentTemp = baseTemp + Math.round((rand() - 0.5) * 8);\n      const tempMax = currentTemp + (2 + Math.round(rand() * 6));\n      const tempMin = currentTemp - (2 + Math.round(rand() * 8));\n\n      const precipitation = precipitationForCondition(condition);\n\n      const current = {\n        temperature: currentTemp,\n        tempMin,\n        tempMax,\n        conditionCode: condition,\n        ...(includeExtras\n          ? {\n              windSpeed:\n                condition === \"windy\"\n                  ? Math.round(15 + rand() * 25)\n                  : Math.round(rand() * 20),\n              precipitationLevel: precipitation,\n              visibility:\n                condition === \"fog\"\n                  ? clamp(0.5 + rand() * 2.5, 0.1, 10)\n                  : clamp(3 + rand() * 10, 0.1, 15),\n            }\n          : {}),\n      };\n\n      const forecast = buildForecast(rand, condition, unit).slice(0, 7);\n\n      const widget = (\n        <WeatherWidget\n          version=\"3.1\"\n          id={`weather-widget-stress-${remountSeed}-${i}`}\n          location={{ name: location }}\n          units={{ temperature: unit }}\n          current={current}\n          forecast={forecast}\n          time={{ localTimeOfDay: timeControls.timeOfDay }}\n          updatedAt={includeUpdatedAt ? timestamp : undefined}\n          effects={globalEffects}\n          className={stress.fillCells ? \"w-full max-w-none\" : undefined}\n        />\n      );\n\n      items.push({\n        key: `${remountSeed}-${i}`,\n        widget,\n        label: condition,\n      });\n    }\n\n    return items;\n  }, [\n    dataControls.includeExtras,\n    dataControls.includeUpdatedAt,\n    dataControls.unit,\n    globalEffects,\n    remountSeed,\n    stress.count,\n    stress.fillCells,\n    timeControls.timeOfDay,\n    timestamp,\n  ]);\n\n  return (\n    <div className=\"fixed inset-0 overflow-y-auto bg-gradient-to-b from-zinc-950 via-zinc-950 to-black\">\n      <Leva\n        collapsed={false}\n        flat={false}\n        titleBar={{ title: \"Weather Widget Stress Lab\" }}\n        theme={{\n          sizes: {\n            rootWidth: \"300px\",\n            controlWidth: \"140px\",\n          },\n        }}\n      />\n\n      <div className=\"mx-auto flex max-w-6xl flex-col gap-6 p-8 pb-24\">\n        <header className=\"space-y-2\">\n          <h1 className=\"text-2xl font-semibold tracking-tight text-white\">\n            Weather Widget Stress Lab\n          </h1>\n          <p className=\"max-w-3xl text-sm text-zinc-400\">\n            WebGL-safe sampler for the shipping weather effects. It renders a\n            small set of representative conditions so you can scrub time of day\n            globally and spot visual/correctness issues quickly.\n          </p>\n          <div className=\"max-w-3xl rounded-lg border border-zinc-800 bg-zinc-950/40 p-3 text-xs text-zinc-400\">\n            <span className=\"font-medium text-zinc-200\">Note:</span> Effects use{\" \"}\n            <span className=\"font-medium text-zinc-200\">WebGL2</span>, and\n            browsers cap how many WebGL contexts can be active. This page\n            intentionally caps the sampler grid to{\" \"}\n            <span className=\"font-mono text-zinc-200\">\n              {WEBGL_SAFE_WIDGET_COUNT}\n            </span>{\" \"}\n            widgets so every card can render effects without falling back.\n          </div>\n          <div className=\"flex flex-wrap items-center gap-2 text-xs text-zinc-400\">\n            <span className=\"rounded border border-zinc-800 bg-zinc-900/40 px-2 py-1\">\n              Widgets:{\" \"}\n              <span className=\"font-mono text-zinc-200\">{stress.count}</span>\n            </span>\n            <span className=\"rounded border border-zinc-800 bg-zinc-900/40 px-2 py-1\">\n              Columns:{\" \"}\n              <span className=\"font-mono text-zinc-200\">{stress.columns}</span>\n            </span>\n            <span className=\"rounded border border-zinc-800 bg-zinc-900/40 px-2 py-1\">\n              Time:{\" \"}\n              <span className=\"font-mono text-zinc-200\">\n                {timeControls.timeOfDay.toFixed(3)}\n              </span>\n            </span>\n            <span className=\"rounded border border-zinc-800 bg-zinc-900/40 px-2 py-1\">\n              Effects:{\" \"}\n              <span className=\"font-mono text-zinc-200\">\n                {effectsControls.enabled ? \"on\" : \"off\"}\n              </span>\n            </span>\n            <span className=\"rounded border border-zinc-800 bg-zinc-900/40 px-2 py-1\">\n              Reduced motion:{\" \"}\n              <span className=\"font-mono text-zinc-200\">\n                {effectsControls.reducedMotion ? \"on\" : \"off\"}\n              </span>\n            </span>\n            <span className=\"rounded border border-zinc-800 bg-zinc-900/40 px-2 py-1\">\n              Quality:{\" \"}\n              <span className=\"font-mono text-zinc-200\">\n                {effectsControls.quality}\n              </span>\n            </span>\n          </div>\n        </header>\n\n        <section className=\"space-y-3\">\n          <div className=\"flex items-center justify-between gap-3\">\n            <h2 className=\"text-sm font-medium tracking-wider text-zinc-500 uppercase\">\n              WebGL-safe condition sampler\n            </h2>\n            <div className=\"flex items-center gap-2 text-xs text-zinc-500\">\n              <span className=\"hidden sm:inline\">Tip:</span>\n              <span className=\"hidden sm:inline\">\n                Adjust time of day to preview all conditions at the same moment.\n              </span>\n            </div>\n          </div>\n\n          <div\n            className={cn(\"grid gap-4\", stress.fillCells && \"items-stretch\")}\n            style={{\n              gridTemplateColumns: `repeat(${stress.columns}, minmax(0, 1fr))`,\n            }}\n          >\n            {gridItems.map((item) => (\n              <div key={item.key} className=\"space-y-1\">\n                <div className=\"text-[10px] tracking-wider text-zinc-600 uppercase\">\n                  {item.label}\n                </div>\n                {item.widget}\n              </div>\n            ))}\n          </div>\n        </section>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/staging/_components/rounded-rect-overlay.tsx",
    "content": "\"use client\";\n\nimport { cn } from \"@/lib/ui/cn\";\n\ninterface RoundedRectOverlayProps {\n  rect: DOMRect;\n  containerRect: DOMRect;\n  padding?: number;\n  paddingOuter?: number; // Padding on outer-facing side (overrides padding on that side)\n  color: \"blue\" | \"green\" | \"orange\";\n  showMargin?: boolean;\n  marginSize?: number;\n  marginSizeOuter?: number; // Margin on outer-facing side (overrides marginSize on that side)\n  label?: string;\n  outerEdgeRadiusFactor?: number; // Factor to reduce radius on outer-facing edge (0-1)\n  isLeftAligned?: boolean; // If true, left side is outer-facing; if false, right side is outer-facing\n}\n\nconst COLOR_STYLES = {\n  blue: {\n    border: \"border-blue-500\",\n    bg: \"bg-blue-500/10\",\n    marginBorder: \"border-blue-400/50\",\n    marginBg: \"bg-blue-400/5\",\n    text: \"text-blue-600 dark:text-blue-400\",\n  },\n  green: {\n    border: \"border-emerald-500\",\n    bg: \"bg-emerald-500/10\",\n    marginBorder: \"border-emerald-400/50\",\n    marginBg: \"bg-emerald-400/5\",\n    text: \"text-emerald-600 dark:text-emerald-400\",\n  },\n  orange: {\n    border: \"border-orange-500\",\n    bg: \"bg-orange-500/10\",\n    marginBorder: \"border-orange-400/50\",\n    marginBg: \"bg-orange-400/5\",\n    text: \"text-orange-600 dark:text-orange-400\",\n  },\n};\n\nexport function RoundedRectOverlay({\n  rect,\n  containerRect,\n  padding = 6,\n  paddingOuter,\n  color,\n  showMargin = false,\n  marginSize = 16,\n  marginSizeOuter,\n  label,\n  outerEdgeRadiusFactor = 1,\n  isLeftAligned = true,\n}: RoundedRectOverlayProps) {\n  const styles = COLOR_STYLES[color];\n\n  // Asymmetric padding: outer-facing side uses paddingOuter if provided\n  const paddingLeft = isLeftAligned ? (paddingOuter ?? padding) : padding;\n  const paddingRight = isLeftAligned ? padding : (paddingOuter ?? padding);\n\n  // Calculate position relative to container\n  const left = rect.left - containerRect.left - paddingLeft;\n  const top = rect.top - containerRect.top - padding;\n  const width = rect.width + paddingLeft + paddingRight;\n  const height = rect.height + padding * 2;\n  const fullRadius = height / 2;\n  const reducedRadius = fullRadius * outerEdgeRadiusFactor;\n\n  // Asymmetric radii: smaller on outer-facing side\n  const radiusLeft = isLeftAligned ? reducedRadius : fullRadius;\n  const radiusRight = isLeftAligned ? fullRadius : reducedRadius;\n  const borderRadius = `${radiusLeft}px ${radiusRight}px ${radiusRight}px ${radiusLeft}px`;\n\n  // Asymmetric margin: outer-facing side uses marginSizeOuter if provided\n  const marginLeft_ = isLeftAligned\n    ? (marginSizeOuter ?? marginSize)\n    : marginSize;\n  const marginRight_ = isLeftAligned\n    ? marginSize\n    : (marginSizeOuter ?? marginSize);\n\n  // Margin overlay dimensions\n  const marginLeftPos = left - marginLeft_;\n  const marginTop = top - marginSize;\n  const marginWidth = width + marginLeft_ + marginRight_;\n  const marginHeight = height + marginSize * 2;\n  const marginFullRadius = marginHeight / 2;\n  const marginReducedRadius = marginFullRadius * outerEdgeRadiusFactor;\n  const marginRadiusLeft = isLeftAligned\n    ? marginReducedRadius\n    : marginFullRadius;\n  const marginRadiusRight = isLeftAligned\n    ? marginFullRadius\n    : marginReducedRadius;\n  const marginBorderRadius = `${marginRadiusLeft}px ${marginRadiusRight}px ${marginRadiusRight}px ${marginRadiusLeft}px`;\n\n  return (\n    <>\n      {showMargin && (\n        <div\n          className={cn(\n            \"absolute border border-dashed\",\n            styles.marginBorder,\n            styles.marginBg,\n          )}\n          style={{\n            left: marginLeftPos,\n            top: marginTop,\n            width: marginWidth,\n            height: marginHeight,\n            borderRadius: marginBorderRadius,\n          }}\n        />\n      )}\n      <div\n        className={cn(\"absolute border\", styles.border, styles.bg)}\n        style={{\n          left,\n          top,\n          width,\n          height,\n          borderRadius,\n        }}\n      >\n        {label && (\n          <span\n            className={cn(\n              \"absolute -top-5 left-1/2 -translate-x-1/2 whitespace-nowrap text-[10px] font-medium\",\n              styles.text,\n            )}\n          >\n            {label}\n          </span>\n        )}\n      </div>\n    </>\n  );\n}\n\ninterface ThumbIndicatorProps {\n  rect: DOMRect;\n  containerRect: DOMRect;\n}\n\nexport function ThumbIndicator({ rect, containerRect }: ThumbIndicatorProps) {\n  const left = rect.left - containerRect.left;\n  const top = rect.top - containerRect.top;\n\n  return (\n    <div\n      className=\"absolute border border-orange-500 bg-orange-500/20\"\n      style={{\n        left: left - 2,\n        top: top - 2,\n        width: rect.width + 4,\n        height: rect.height + 4,\n        borderRadius: 4,\n      }}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/www/app/staging/_components/staging-canvas.tsx",
    "content": "\"use client\";\n\nimport { useRef, useState, useEffect, useCallback } from \"react\";\nimport { previewConfigs, type ComponentId } from \"@/lib/docs/preview-config\";\nimport { getStagingConfig } from \"@/lib/staging/staging-config\";\nimport type { DebugLevel } from \"@/lib/staging/types\";\nimport { ChatContextPreview } from \"@/app/docs/_components/chat-context-preview\";\n\ninterface StagingCanvasProps {\n  componentId: ComponentId;\n  presetName: string;\n  debugLevel: DebugLevel;\n}\n\nexport function StagingCanvas({\n  componentId,\n  presetName,\n  debugLevel,\n}: StagingCanvasProps) {\n  const componentRef = useRef<HTMLDivElement>(null);\n  const [, forceUpdate] = useState(0);\n  const previewConfig = previewConfigs[componentId];\n  const stagingConfig = getStagingConfig(componentId);\n\n  const preset = previewConfig?.presets?.[presetName];\n  const Wrapper = previewConfig?.wrapper;\n\n  // Force re-render overlay on pointer events and animation frames during interaction\n  const scheduleUpdate = useCallback(() => {\n    forceUpdate((n) => n + 1);\n  }, []);\n\n  useEffect(() => {\n    if (debugLevel === \"off\") return;\n\n    const container = componentRef.current;\n    if (!container) return;\n\n    let rafId: number | null = null;\n    let isInteracting = false;\n\n    const updateLoop = () => {\n      if (isInteracting) {\n        scheduleUpdate();\n        rafId = requestAnimationFrame(updateLoop);\n      }\n    };\n\n    const handlePointerDown = () => {\n      isInteracting = true;\n      rafId = requestAnimationFrame(updateLoop);\n    };\n\n    const handlePointerUp = () => {\n      isInteracting = false;\n      if (rafId) {\n        cancelAnimationFrame(rafId);\n        rafId = null;\n      }\n      // One final update after release\n      scheduleUpdate();\n    };\n\n    container.addEventListener(\"pointerdown\", handlePointerDown);\n    window.addEventListener(\"pointerup\", handlePointerUp);\n\n    return () => {\n      container.removeEventListener(\"pointerdown\", handlePointerDown);\n      window.removeEventListener(\"pointerup\", handlePointerUp);\n      if (rafId) cancelAnimationFrame(rafId);\n    };\n  }, [debugLevel, scheduleUpdate]);\n\n  if (!previewConfig || !preset) {\n    return (\n      <div className=\"text-muted-foreground text-sm\">\n        Component or preset not found\n      </div>\n    );\n  }\n\n  // If staging config has a tuning panel, render it instead of the normal component\n  if (stagingConfig?.renderTuningPanel) {\n    return (\n      <div className=\"relative w-full\">\n        {stagingConfig.renderTuningPanel({\n          data: preset.data as Record<string, unknown>,\n        })}\n      </div>\n    );\n  }\n\n  const component = previewConfig.renderComponent({\n    data: preset.data,\n    presetName,\n    state: {},\n    setState: () => {},\n  });\n\n  const wrappedComponent = Wrapper ? <Wrapper>{component}</Wrapper> : component;\n\n  const hasDebugOverlay = stagingConfig?.renderDebugOverlay != null;\n  const shouldShowOverlay = hasDebugOverlay && debugLevel !== \"off\";\n\n  return (\n    <div className=\"relative w-full max-w-3xl\">\n      <ChatContextPreview\n        userMessage={previewConfig.chatContext.userMessage}\n        preamble={previewConfig.chatContext.preamble}\n      >\n        <div\n          ref={componentRef}\n          className=\"relative min-w-[500px] [&_article]:max-w-none\"\n        >\n          {wrappedComponent}\n\n          {debugLevel !== \"off\" && (\n            <div className=\"pointer-events-none absolute inset-0\">\n              {!hasDebugOverlay && (\n                <div className=\"absolute top-0 left-0 bg-orange-500 px-2 py-1 text-xs text-white rounded\">\n                  No debug overlay for {componentId}\n                </div>\n              )}\n              {shouldShowOverlay &&\n                stagingConfig?.renderDebugOverlay?.({\n                  level: debugLevel,\n                  componentRef,\n                })}\n            </div>\n          )}\n        </div>\n      </ChatContextPreview>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/staging/_components/staging-showcase.tsx",
    "content": "\"use client\";\n\nimport {\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  type ReactNode,\n} from \"react\";\nimport { AnimatePresence, motion, type Transition } from \"motion/react\";\nimport { cn } from \"@/lib/ui/cn\";\nimport { previewConfigs, type ComponentId } from \"@/lib/docs/preview-config\";\nimport { getStagingConfig } from \"@/lib/staging/staging-config\";\nimport type { DebugLevel } from \"@/lib/staging/types\";\n\nconst TIMING = {\n  durations: {\n    userIn: 500,\n    preambleIn: 280,\n    toolIn: 600,\n  },\n  beats: {\n    afterUser: 700,\n    beforeContent: 500,\n    afterPreamble: 200,\n  },\n} as const;\n\nconst SPRINGS = {\n  gentle: {\n    type: \"spring\",\n    damping: 28,\n    stiffness: 180,\n    mass: 0.8,\n  },\n  smooth: {\n    type: \"spring\",\n    damping: 24,\n    stiffness: 260,\n    mass: 0.8,\n  },\n  standard: {\n    type: \"spring\",\n    damping: 26,\n    stiffness: 220,\n    mass: 0.7,\n  },\n} as const satisfies Record<string, Transition>;\n\ninterface ChatBubbleProps {\n  role: \"user\" | \"assistant\";\n  children: ReactNode;\n  className?: string;\n}\n\nfunction ChatBubble({ role, children, className }: ChatBubbleProps) {\n  const isUser = role === \"user\";\n\n  return (\n    <div\n      className={cn(\n        \"flex w-full\",\n        isUser ? \"justify-end pb-3\" : \"justify-start\",\n      )}\n    >\n      <div\n        className={cn(\n          \"relative max-w-[min(720px,100%)] text-base leading-relaxed\",\n          isUser &&\n            \"rounded-2xl bg-blue-600 px-4 py-2.5 text-white dark:bg-blue-700\",\n          !isUser && \"text-foreground\",\n          className,\n        )}\n      >\n        {children}\n      </div>\n    </div>\n  );\n}\n\nfunction TypingIndicator() {\n  return (\n    <motion.span\n      className=\"bg-foreground/50 block size-3 rounded-full\"\n      animate={{ opacity: [0.4, 1, 0.4] }}\n      transition={{\n        duration: 1.4,\n        repeat: Infinity,\n        ease: \"easeInOut\",\n      }}\n      aria-label=\"Assistant is typing\"\n    />\n  );\n}\n\nfunction StreamingChar({ char, delay }: { char: string; delay: number }) {\n  return (\n    <motion.span\n      initial={{ opacity: 0 }}\n      animate={{ opacity: 1 }}\n      transition={{\n        duration: 0.4,\n        delay,\n        ease: [0.25, 0.1, 0.25, 1],\n      }}\n    >\n      {char}\n    </motion.span>\n  );\n}\n\ninterface PreambleBubbleProps {\n  text: string;\n  msPerChar?: number;\n  onComplete?: () => void;\n}\n\nfunction PreambleBubble({\n  text,\n  msPerChar = 28,\n  onComplete,\n}: PreambleBubbleProps) {\n  const [isVisible, setIsVisible] = useState(false);\n  const hasCalledComplete = useRef(false);\n\n  useEffect(() => {\n    const timeoutId = window.setTimeout(() => setIsVisible(true), 100);\n    return () => window.clearTimeout(timeoutId);\n  }, []);\n\n  useEffect(() => {\n    if (hasCalledComplete.current || !isVisible) return;\n\n    const totalDuration = text.length * msPerChar + 400;\n    const timeoutId = window.setTimeout(() => {\n      if (!hasCalledComplete.current) {\n        hasCalledComplete.current = true;\n        onComplete?.();\n      }\n    }, totalDuration);\n\n    return () => window.clearTimeout(timeoutId);\n  }, [msPerChar, text.length, onComplete, isVisible]);\n\n  const characters = useMemo(() => {\n    return text.split(\"\").map((char, index) => ({\n      char,\n      delay: index * (msPerChar / 1000),\n    }));\n  }, [text, msPerChar]);\n\n  return (\n    <motion.div\n      initial={{ opacity: 0 }}\n      animate={{ opacity: isVisible ? 1 : 0 }}\n      transition={SPRINGS.smooth}\n    >\n      <ChatBubble role=\"assistant\">\n        <span>\n          {isVisible &&\n            characters.map(({ char, delay }, index) => (\n              <StreamingChar key={index} char={char} delay={delay} />\n            ))}\n        </span>\n      </ChatBubble>\n    </motion.div>\n  );\n}\n\nfunction ToolReveal({ children }: { children: ReactNode }) {\n  return (\n    <motion.div\n      initial={{ opacity: 0, y: 30, filter: \"blur(10px)\" }}\n      animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n      transition={SPRINGS.gentle}\n    >\n      {children}\n    </motion.div>\n  );\n}\n\ntype ShowcasePhase = \"user\" | \"typing\" | \"preamble\" | \"tool\" | \"complete\";\n\ninterface StagingShowcaseProps {\n  componentId: ComponentId;\n  presetName: string;\n  debugLevel: DebugLevel;\n}\n\nexport function StagingShowcase({\n  componentId,\n  presetName,\n  debugLevel,\n}: StagingShowcaseProps) {\n  const [phase, setPhase] = useState<ShowcasePhase>(\"user\");\n  const [key, setKey] = useState(0);\n  const componentRef = useRef<HTMLDivElement>(null);\n\n  const previewConfig = previewConfigs[componentId];\n  const stagingConfig = getStagingConfig(componentId);\n  const preset = previewConfig?.presets?.[presetName];\n  const Wrapper = previewConfig?.wrapper;\n\n  const userMessage = previewConfig?.chatContext?.userMessage ?? \"\";\n  const preamble = previewConfig?.chatContext?.preamble ?? \"\";\n\n  const startSequence = useCallback(() => {\n    setKey((k) => k + 1);\n    setPhase(\"user\");\n\n    // User message appears, then show typing\n    setTimeout(() => {\n      setPhase(\"typing\");\n    }, TIMING.durations.userIn);\n\n    // Typing indicator, then show preamble\n    setTimeout(() => {\n      setPhase(\"preamble\");\n    }, TIMING.durations.userIn + TIMING.beats.afterUser);\n  }, []);\n\n  const handlePreambleComplete = useCallback(() => {\n    setPhase(\"tool\");\n    setTimeout(() => {\n      setPhase(\"complete\");\n    }, TIMING.durations.toolIn);\n  }, []);\n\n  // Auto-start sequence on mount and when component/preset changes\n  useEffect(() => {\n    startSequence();\n  }, [componentId, presetName, startSequence]);\n\n  if (!previewConfig || !preset) {\n    return (\n      <div className=\"text-muted-foreground text-sm\">\n        Component or preset not found\n      </div>\n    );\n  }\n\n  const component = previewConfig.renderComponent({\n    data: preset.data,\n    presetName,\n    state: {},\n    setState: () => {},\n  });\n\n  const wrappedComponent = Wrapper ? <Wrapper>{component}</Wrapper> : component;\n\n  const showTyping = phase === \"typing\";\n  const showPreamble =\n    phase === \"preamble\" || phase === \"tool\" || phase === \"complete\";\n  const showTool = phase === \"tool\" || phase === \"complete\";\n\n  return (\n    <div className=\"relative h-full w-full max-w-3xl\">\n      <div className=\"absolute inset-0 flex flex-col pt-8\">\n        <AnimatePresence mode=\"wait\">\n          <motion.div\n            key={`user-${key}`}\n            initial={{ opacity: 0, y: 16 }}\n            animate={{ opacity: 1, y: 0 }}\n            transition={SPRINGS.standard}\n            className=\"mb-4\"\n          >\n            <ChatBubble role=\"user\">{userMessage}</ChatBubble>\n          </motion.div>\n        </AnimatePresence>\n\n        <div className=\"relative min-h-8\">\n          <AnimatePresence mode=\"wait\">\n            {showTyping && (\n              <motion.div\n                key={`typing-${key}`}\n                initial={{ opacity: 0, scale: 0.5 }}\n                animate={{ opacity: 1, scale: 1 }}\n                exit={{ opacity: 0 }}\n                transition={SPRINGS.smooth}\n                className=\"absolute top-0 left-0\"\n              >\n                <TypingIndicator />\n              </motion.div>\n            )}\n          </AnimatePresence>\n\n          <AnimatePresence>\n            {showPreamble && (\n              <motion.div\n                key={`preamble-${key}`}\n                initial={{ opacity: 0 }}\n                animate={{ opacity: 1 }}\n              >\n                <PreambleBubble\n                  text={preamble}\n                  onComplete={handlePreambleComplete}\n                />\n              </motion.div>\n            )}\n          </AnimatePresence>\n        </div>\n\n        <AnimatePresence>\n          {showTool && (\n            <motion.div\n              key={`tool-${key}`}\n              className=\"mt-4 flex w-full justify-start\"\n            >\n              <div\n                ref={componentRef}\n                className=\"relative min-w-[500px] [&_article]:max-w-none\"\n              >\n                <ToolReveal>{wrappedComponent}</ToolReveal>\n\n                {debugLevel !== \"off\" && stagingConfig?.renderDebugOverlay && (\n                  <div className=\"pointer-events-none absolute inset-0\">\n                    {stagingConfig.renderDebugOverlay({\n                      level: debugLevel,\n                      componentRef,\n                    })}\n                  </div>\n                )}\n              </div>\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/staging/_components/staging-toolbar.tsx",
    "content": "\"use client\";\n\nimport { Settings, Sun, Moon, Monitor, Play, Eye } from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\nimport { cn } from \"@/lib/ui/cn\";\nimport { previewConfigs, type ComponentId } from \"@/lib/docs/preview-config\";\nimport { getStagingConfig } from \"@/lib/staging/staging-config\";\nimport { useStagingStore, usePresetNames } from \"./use-staging-state\";\nimport type { DebugLevel } from \"@/lib/staging/types\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Button } from \"@/components/ui/button\";\nimport { Label } from \"@/components/ui/label\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\n\nconst COMPONENT_IDS = Object.keys(previewConfigs) as ComponentId[];\n\nconst DEBUG_LEVEL_LABELS: Record<DebugLevel, string> = {\n  off: \"Off\",\n  boundaries: \"Boundaries\",\n  margins: \"Margins\",\n  full: \"Full\",\n};\n\nfunction ComponentSelector() {\n  const { componentId, setComponent } = useStagingStore();\n\n  return (\n    <Select\n      value={componentId}\n      onValueChange={(v) => setComponent(v as ComponentId)}\n    >\n      <SelectTrigger className=\"h-8 w-[180px] border-none bg-transparent text-sm font-medium shadow-none focus:ring-0\">\n        <SelectValue />\n      </SelectTrigger>\n      <SelectContent>\n        {COMPONENT_IDS.map((id) => (\n          <SelectItem key={id} value={id} className=\"text-sm\">\n            {formatComponentName(id)}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  );\n}\n\nfunction formatComponentName(id: string): string {\n  return id\n    .split(\"-\")\n    .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n    .join(\" \");\n}\n\nfunction PresetPills() {\n  const { presetName, setPreset } = useStagingStore();\n  const presetNames = usePresetNames();\n\n  return (\n    <div className=\"flex items-center gap-1\">\n      {presetNames.map((name, index) => (\n        <button\n          key={name}\n          onClick={() => setPreset(name)}\n          className={cn(\n            \"rounded-full px-3 py-1 text-xs font-medium transition-colors\",\n            presetName === name\n              ? \"bg-foreground text-background\"\n              : \"text-muted-foreground hover:text-foreground hover:bg-muted\",\n          )}\n        >\n          <span className=\"text-muted-foreground mr-1.5 font-mono opacity-60\">\n            {index + 1}\n          </span>\n          {formatPresetName(name)}\n        </button>\n      ))}\n    </div>\n  );\n}\n\nfunction formatPresetName(name: string): string {\n  return name\n    .split(\"-\")\n    .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n    .join(\" \");\n}\n\nfunction SettingsPopover() {\n  const { componentId, debugLevel, setDebugLevel } = useStagingStore();\n  const { theme, setTheme } = useTheme();\n\n  const stagingConfig = getStagingConfig(componentId);\n  const hasDebugOverlays =\n    (stagingConfig?.supportedDebugLevels?.length ?? 0) > 0;\n\n  return (\n    <Popover>\n      <PopoverTrigger asChild>\n        <Button\n          variant=\"ghost\"\n          size=\"icon\"\n          className=\"h-8 w-8 text-muted-foreground hover:text-foreground\"\n        >\n          <Settings className=\"h-4 w-4\" />\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent align=\"end\" className=\"w-72\">\n        <div className=\"space-y-4\">\n          {hasDebugOverlays && (\n            <div className=\"space-y-2\">\n              <div className=\"flex items-center justify-between\">\n                <Label className=\"text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n                  Debug Overlay\n                </Label>\n                <kbd className=\"rounded bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground\">\n                  D\n                </kbd>\n              </div>\n              <RadioGroup\n                value={debugLevel}\n                onValueChange={(v) => setDebugLevel(v as DebugLevel)}\n                className=\"grid grid-cols-2 gap-2\"\n              >\n                {(stagingConfig?.supportedDebugLevels ?? []).map((level) => (\n                  <div key={level} className=\"flex items-center space-x-2\">\n                    <RadioGroupItem value={level} id={`debug-${level}`} />\n                    <Label\n                      htmlFor={`debug-${level}`}\n                      className=\"text-sm cursor-pointer\"\n                    >\n                      {DEBUG_LEVEL_LABELS[level]}\n                    </Label>\n                  </div>\n                ))}\n              </RadioGroup>\n            </div>\n          )}\n\n          <div className=\"space-y-2\">\n            <Label className=\"text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n              Theme\n            </Label>\n            <div className=\"flex gap-1\">\n              <Button\n                variant={theme === \"light\" ? \"default\" : \"ghost\"}\n                size=\"sm\"\n                className=\"flex-1\"\n                onClick={() => setTheme(\"light\")}\n              >\n                <Sun className=\"mr-1.5 h-3.5 w-3.5\" />\n                Light\n              </Button>\n              <Button\n                variant={theme === \"dark\" ? \"default\" : \"ghost\"}\n                size=\"sm\"\n                className=\"flex-1\"\n                onClick={() => setTheme(\"dark\")}\n              >\n                <Moon className=\"mr-1.5 h-3.5 w-3.5\" />\n                Dark\n              </Button>\n              <Button\n                variant={theme === \"system\" ? \"default\" : \"ghost\"}\n                size=\"sm\"\n                className=\"flex-1\"\n                onClick={() => setTheme(\"system\")}\n              >\n                <Monitor className=\"mr-1.5 h-3.5 w-3.5\" />\n              </Button>\n            </div>\n          </div>\n\n          <div className=\"border-t pt-3\">\n            <p className=\"text-[10px] text-muted-foreground\">\n              Press{\" \"}\n              <kbd className=\"rounded bg-muted px-1 py-0.5 font-mono\">1</kbd>-\n              <kbd className=\"rounded bg-muted px-1 py-0.5 font-mono\">9</kbd> to\n              switch presets\n            </p>\n          </div>\n        </div>\n      </PopoverContent>\n    </Popover>\n  );\n}\n\nfunction ViewModeToggle() {\n  const { viewMode, setViewMode } = useStagingStore();\n\n  return (\n    <div className=\"flex items-center rounded-lg border p-0.5\">\n      <button\n        onClick={() => setViewMode(\"static\")}\n        className={cn(\n          \"flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium transition-colors\",\n          viewMode === \"static\"\n            ? \"bg-foreground text-background\"\n            : \"text-muted-foreground hover:text-foreground\",\n        )}\n        title=\"Static view\"\n      >\n        <Eye className=\"h-3 w-3\" />\n        Static\n      </button>\n      <button\n        onClick={() => setViewMode(\"showcase\")}\n        className={cn(\n          \"flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium transition-colors\",\n          viewMode === \"showcase\"\n            ? \"bg-foreground text-background\"\n            : \"text-muted-foreground hover:text-foreground\",\n        )}\n        title=\"Showcase animation\"\n      >\n        <Play className=\"h-3 w-3\" />\n        Showcase\n      </button>\n    </div>\n  );\n}\n\nfunction DebugLevelIndicator() {\n  const { debugLevel, cycleDebugLevel } = useStagingStore();\n\n  if (debugLevel === \"off\") return null;\n\n  const colors: Record<DebugLevel, string> = {\n    off: \"\",\n    boundaries: \"bg-blue-500\",\n    margins: \"bg-emerald-500\",\n    full: \"bg-orange-500\",\n  };\n\n  return (\n    <button\n      onClick={cycleDebugLevel}\n      className=\"flex items-center gap-1.5 rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-muted/80\"\n      title=\"Click or press D to cycle\"\n    >\n      <span className={cn(\"h-2 w-2 rounded-full\", colors[debugLevel])} />\n      <span className=\"font-medium\">{DEBUG_LEVEL_LABELS[debugLevel]}</span>\n    </button>\n  );\n}\n\nexport function StagingToolbar() {\n  return (\n    <header className=\"border-b bg-background/80 backdrop-blur-sm\">\n      <div className=\"flex h-12 items-center justify-between px-4\">\n        <div className=\"flex items-center gap-2\">\n          <ComponentSelector />\n          <div className=\"bg-border h-4 w-px\" />\n          <PresetPills />\n        </div>\n\n        <div className=\"flex items-center gap-2\">\n          <ViewModeToggle />\n          <div className=\"bg-border h-4 w-px\" />\n          <DebugLevelIndicator />\n          <SettingsPopover />\n        </div>\n      </div>\n    </header>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/staging/_components/use-keyboard-shortcuts.ts",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { useStagingStore, usePresetNames } from \"./use-staging-state\";\n\nexport function useKeyboardShortcuts() {\n  const { setPreset, cycleDebugLevel } = useStagingStore();\n  const presetNames = usePresetNames();\n\n  useEffect(() => {\n    function handleKeyDown(event: KeyboardEvent) {\n      // Ignore if typing in an input\n      if (\n        event.target instanceof HTMLInputElement ||\n        event.target instanceof HTMLTextAreaElement\n      ) {\n        return;\n      }\n\n      // Number keys 1-9 for preset selection\n      if (event.key >= \"1\" && event.key <= \"9\") {\n        const index = parseInt(event.key, 10) - 1;\n        if (index < presetNames.length) {\n          setPreset(presetNames[index]);\n        }\n        return;\n      }\n\n      // D to cycle debug levels\n      if (event.key.toLowerCase() === \"d\") {\n        cycleDebugLevel();\n        return;\n      }\n    }\n\n    window.addEventListener(\"keydown\", handleKeyDown);\n    return () => window.removeEventListener(\"keydown\", handleKeyDown);\n  }, [presetNames, setPreset, cycleDebugLevel]);\n}\n"
  },
  {
    "path": "apps/www/app/staging/_components/use-staging-state.ts",
    "content": "\"use client\";\n\nimport { create } from \"zustand\";\nimport { useSearchParams, useRouter, usePathname } from \"next/navigation\";\nimport { useEffect, useCallback } from \"react\";\nimport { previewConfigs, type ComponentId } from \"@/lib/docs/preview-config\";\nimport type { DebugLevel } from \"@/lib/staging/types\";\n\ntype ViewMode = \"static\" | \"showcase\";\n\ninterface StagingState {\n  componentId: ComponentId;\n  presetName: string;\n  debugLevel: DebugLevel;\n  viewMode: ViewMode;\n  setComponent: (id: ComponentId) => void;\n  setPreset: (name: string) => void;\n  setDebugLevel: (level: DebugLevel) => void;\n  cycleDebugLevel: () => void;\n  setViewMode: (mode: ViewMode) => void;\n}\n\nconst DEBUG_LEVELS: DebugLevel[] = [\"off\", \"boundaries\", \"margins\", \"full\"];\n\nexport const useStagingStore = create<StagingState>((set, get) => ({\n  componentId: \"parameter-slider\",\n  presetName: \"photo-adjustments\",\n  debugLevel: \"off\",\n  viewMode: \"static\",\n\n  setComponent: (id) => {\n    const config = previewConfigs[id];\n    set({\n      componentId: id,\n      presetName:\n        config?.defaultPreset ?? Object.keys(config?.presets ?? {})[0] ?? \"\",\n      debugLevel: \"off\",\n    });\n  },\n\n  setPreset: (name) => set({ presetName: name }),\n\n  setDebugLevel: (level) => set({ debugLevel: level }),\n\n  cycleDebugLevel: () => {\n    const current = get().debugLevel;\n    const currentIndex = DEBUG_LEVELS.indexOf(current);\n    const nextIndex = (currentIndex + 1) % DEBUG_LEVELS.length;\n    set({ debugLevel: DEBUG_LEVELS[nextIndex] });\n  },\n\n  setViewMode: (mode) => set({ viewMode: mode }),\n}));\n\nexport function useStagingState() {\n  const store = useStagingStore();\n  const searchParams = useSearchParams();\n  const router = useRouter();\n  const pathname = usePathname();\n\n  // Sync URL params to store on mount\n  useEffect(() => {\n    const componentParam = searchParams.get(\"component\") as ComponentId | null;\n    const presetParam = searchParams.get(\"preset\");\n    const debugParam = searchParams.get(\"debug\") as DebugLevel | null;\n\n    if (componentParam && componentParam in previewConfigs) {\n      const config = previewConfigs[componentParam];\n      const presetName =\n        presetParam && presetParam in (config?.presets ?? {})\n          ? presetParam\n          : (config?.defaultPreset ?? \"\");\n\n      const debugLevel =\n        debugParam && DEBUG_LEVELS.includes(debugParam) ? debugParam : \"off\";\n\n      // Only update store if values have changed to prevent infinite loop\n      const currentState = useStagingStore.getState();\n      if (\n        currentState.componentId !== componentParam ||\n        currentState.presetName !== presetName ||\n        currentState.debugLevel !== debugLevel\n      ) {\n        useStagingStore.setState({\n          componentId: componentParam,\n          presetName,\n          debugLevel,\n        });\n      }\n    }\n  }, [searchParams]);\n\n  // Sync store to URL params\n  const syncToUrl = useCallback(() => {\n    const state = useStagingStore.getState();\n    const params = new URLSearchParams();\n    params.set(\"component\", state.componentId);\n    params.set(\"preset\", state.presetName);\n    if (state.debugLevel !== \"off\") {\n      params.set(\"debug\", state.debugLevel);\n    }\n    router.replace(`${pathname}?${params.toString()}`, { scroll: false });\n  }, [router, pathname]);\n\n  // Subscribe to store changes\n  useEffect(() => {\n    const unsubscribe = useStagingStore.subscribe(() => {\n      syncToUrl();\n    });\n    return unsubscribe;\n  }, [syncToUrl]);\n\n  return store;\n}\n\nexport function usePresetNames(): string[] {\n  const { componentId } = useStagingStore();\n  const config = previewConfigs[componentId];\n  return Object.keys(config?.presets ?? {});\n}\n"
  },
  {
    "path": "apps/www/app/staging/layout.tsx",
    "content": "import type { ReactNode } from \"react\";\nimport { ThemeProvider } from \"next-themes\";\n\nexport default function StagingLayout({ children }: { children: ReactNode }) {\n  return (\n    <ThemeProvider\n      attribute=\"class\"\n      defaultTheme=\"system\"\n      enableSystem\n      disableTransitionOnChange\n    >\n      <div className=\"bg-background text-foreground min-h-screen\">\n        {children}\n      </div>\n    </ThemeProvider>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/staging/page.tsx",
    "content": "\"use client\";\n\nimport { Suspense } from \"react\";\nimport { StagingToolbar } from \"./_components/staging-toolbar\";\nimport { StagingCanvas } from \"./_components/staging-canvas\";\nimport { StagingShowcase } from \"./_components/staging-showcase\";\nimport { useStagingState } from \"./_components/use-staging-state\";\nimport { useKeyboardShortcuts } from \"./_components/use-keyboard-shortcuts\";\n\nfunction StagingContent() {\n  const { componentId, presetName, debugLevel, viewMode } = useStagingState();\n  useKeyboardShortcuts();\n\n  return (\n    <div className=\"flex h-screen flex-col\">\n      <StagingToolbar />\n      <main className=\"relative flex flex-1 items-start justify-center overflow-auto p-8\">\n        {viewMode === \"static\" ? (\n          <StagingCanvas\n            componentId={componentId}\n            presetName={presetName}\n            debugLevel={debugLevel}\n          />\n        ) : (\n          <StagingShowcase\n            componentId={componentId}\n            presetName={presetName}\n            debugLevel={debugLevel}\n          />\n        )}\n      </main>\n    </div>\n  );\n}\n\nexport default function StagingPage() {\n  return (\n    <Suspense\n      fallback={\n        <div className=\"flex h-screen items-center justify-center\">\n          <div className=\"text-muted-foreground text-sm\">Loading...</div>\n        </div>\n      }\n    >\n      <StagingContent />\n    </Suspense>\n  );\n}\n"
  },
  {
    "path": "apps/www/app/styles/builder-loader.css",
    "content": ".loader {\n  width: 48px;\n  height: 48px;\n  border: 5px solid;\n  border-color: #000 transparent;\n  border-radius: 50%;\n  display: inline-block;\n  box-sizing: border-box;\n  animation: rotation 1s linear infinite;\n  margin: 20px auto;\n}\n\n@keyframes rotation {\n  0% {\n    transform: rotate(0deg);\n  }\n  100% {\n    transform: rotate(360deg);\n  }\n}\n\n@media (prefers-color-scheme: dark) {\n  .loader {\n    border-color: #fff transparent;\n  }\n}\n"
  },
  {
    "path": "apps/www/app/styles/custom-utilities.css",
    "content": ":root {\n  --cubic-ease-in-out: cubic-bezier(0.62, -0.05, 0.71, 1.15);\n}\n\n@keyframes accordion-down {\n  from {\n    height: 0;\n  }\n  to {\n    height: var(--radix-accordion-content-height);\n  }\n}\n\n@keyframes accordion-up {\n  from {\n    height: var(--radix-accordion-content-height);\n  }\n  to {\n    height: 0;\n  }\n}\n\n@keyframes collapsible-down {\n  from {\n    height: 0;\n  }\n  to {\n    height: var(--radix-collapsible-content-height);\n  }\n}\n\n@keyframes collapsible-up {\n  from {\n    height: var(--radix-collapsible-content-height);\n  }\n  to {\n    height: 0;\n  }\n}\n\n@keyframes scale-in {\n  from {\n    transform: scale(0.8);\n    opacity: 0;\n  }\n  to {\n    transform: scale(1);\n    opacity: 1;\n  }\n}\n\n@keyframes spring-bounce {\n  0% {\n    transform: scale(0.8);\n    opacity: 0;\n  }\n  50% {\n    transform: scale(1.1);\n  }\n  100% {\n    transform: scale(1);\n    opacity: 1;\n  }\n}\n\n@keyframes check-draw {\n  0% {\n    stroke-dasharray: var(--check-path-length, 24);\n    stroke-dashoffset: var(--check-path-length, 24);\n    opacity: 0;\n  }\n  50% {\n    opacity: 1;\n  }\n  100% {\n    stroke-dasharray: var(--check-path-length, 24);\n    stroke-dashoffset: 0;\n    opacity: 1;\n  }\n}\n\n@keyframes fade-up {\n  from {\n    opacity: 0;\n    transform: translateY(8px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes fade-blur-in {\n  from {\n    opacity: 0;\n    transform: translateY(4px);\n    filter: blur(4px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n    filter: blur(0);\n  }\n}\n\n@keyframes progress-pulse {\n  0%,\n  100% {\n    filter: brightness(1);\n  }\n  50% {\n    filter: brightness(1.3);\n  }\n}\n\n@keyframes shimmer-sweep {\n  0% {\n    transform: translateX(-100%);\n  }\n  100% {\n    transform: translateX(200%);\n  }\n}\n\n@keyframes glow-pulse {\n  0%,\n  100% {\n    opacity: 0;\n  }\n  50% {\n    opacity: 1;\n  }\n}\n\n@keyframes fade-in-stagger {\n  from {\n    opacity: 0;\n    transform: translateY(-4px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes fade-out-stagger {\n  from {\n    opacity: 1;\n    transform: translateY(0);\n  }\n  to {\n    opacity: 0;\n    transform: translateY(-4px);\n  }\n}\n\n@keyframes navbar-fade-in {\n  from {\n    opacity: 0;\n    transform: translateY(-12px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@layer utilities {\n  .animate-accordion-down {\n    animation: accordion-down 200ms ease-out;\n  }\n\n  .animate-accordion-up {\n    animation: accordion-up 200ms ease-out;\n  }\n\n  .animate-collapsible-down {\n    animation: collapsible-down 150ms ease-out;\n  }\n\n  .animate-collapsible-up {\n    animation: collapsible-up 150ms ease-out;\n  }\n\n  .animate-fade-blur-in {\n    animation: fade-blur-in 300ms cubic-bezier(0.16, 1, 0.3, 1) both;\n  }\n\n  .animate-navbar-fade-in {\n    opacity: 0;\n    animation: navbar-fade-in 1.2s cubic-bezier(0.22, 1, 0.36, 1) 0.3s both;\n  }\n\n  .bg-wash {\n    background-color: hsl(var(--wash));\n  }\n  .bg-dot-grid {\n    --dot-grid-color: var(--border);\n    background-image: radial-gradient(\n      circle,\n      var(--dot-grid-color) 1px,\n      transparent 1px\n    );\n    background-position: 6px 6px;\n    background-size: 16px 16px;\n  }\n\n  .dark .bg-dot-grid {\n    --dot-grid-color: color-mix(in oklch, var(--border) 80%, transparent);\n  }\n\n  .shadow-crisp-edge {\n    position: relative;\n    box-sizing: padding-box;\n    box-shadow:\n      0px 1px 0px -1px oklch(0 0 0 / 0.1),\n      0px 1px 1px -1px oklch(0 0 0 / 0.1),\n      0px 1px 2px -1px oklch(0 0 0 / 0.1),\n      0px 2px 4px -2px oklch(0 0 0 / 0.1),\n      0px 3px 6px -3px oklch(0 0 0 / 0.1);\n\n    &::after {\n      content: \"\";\n      pointer-events: none;\n      position: absolute;\n      inset: 0;\n      border-radius: inherit;\n      box-shadow:\n        inset 0px 0px 0px 1px oklch(1 0 0 / 0.5),\n        inset 0px 1px 0px oklch(1 0 0 / 0.5);\n    }\n  }\n\n  .dark .shadow-crisp-edge {\n    box-shadow:\n      0px 1px 0px -1px oklch(0 0 0 / 0.1),\n      0px 1px 1px -1px oklch(0 0 0 / 0.1),\n      0px 1px 2px -1px oklch(0 0 0 / 0.1),\n      0px 2px 4px -2px oklch(0 0 0 / 0.1),\n      0px 3px 6px -3px oklch(0 0 0 / 0.1);\n\n    &::after {\n      box-shadow:\n        inset 0px 0px 0px 1px oklch(1 0 0 / 0.05),\n        inset 0px 1px 0px oklch(1 0 0 / 0.05);\n    }\n  }\n\n  .scrollbar-subtle {\n    scrollbar-width: thin;\n    scrollbar-color: oklch(0 0 0 / 0.2) transparent;\n  }\n\n  .scrollbar-subtle::-webkit-scrollbar {\n    width: 20px;\n    height: 20px;\n  }\n\n  .scrollbar-subtle::-webkit-scrollbar-track {\n    background: transparent;\n    /* Add padding to prevent thumb from being clipped by rounded corners */\n    margin: 0.25rem;\n  }\n\n  .scrollbar-subtle::-webkit-scrollbar-thumb {\n    background-color: oklch(58.88% 0.23563 28.674 / 0.2);\n    border-radius: 9999px;\n    /* Ensure thumb doesn't extend to edges */\n    border: 0 solid transparent;\n  }\n\n  .scrollbar-subtle:hover::-webkit-scrollbar-thumb {\n    background-color: oklch(0 0 0 / 0.3);\n  }\n\n  .dark .scrollbar-subtle {\n    scrollbar-color: var(--color-neutral-500) transparent;\n  }\n\n  .dark .scrollbar-subtle::-webkit-scrollbar-thumb {\n    background-color: oklch(52.771% 0.21252 28.808 / 0.25);\n  }\n\n  .dark .scrollbar-subtle:hover::-webkit-scrollbar-thumb {\n    background-color: oklch(58.661% 0.23514 28.709 / 0.55);\n  }\n\n  .border-gradient-glow {\n    --glow-gradient: rgba(0, 0, 0, 0.12);\n    --glow-surface-from: var(--background);\n    --glow-surface-to: #f7f7f7;\n    background:\n      linear-gradient(\n          to bottom,\n          var(--glow-surface-from),\n          var(--glow-surface-to)\n        )\n        padding-box,\n      var(--glow-gradient) border-box;\n    border: 1px solid transparent;\n  }\n\n  .dark .border-gradient-glow {\n    --glow-surface-from: var(--background);\n    --glow-surface-to: var(--background);\n    --glow-gradient: radial-gradient(\n      ellipse 300% 214% at 0% 72%,\n      rgba(255, 255, 255, 0.41) 0%,\n      rgba(255, 255, 255, 0.12) 19%,\n      rgba(255, 255, 255, 0.02) 49%,\n      rgba(255, 255, 255, 0) 100%\n    );\n  }\n\n  .border-gradient-glow-composer {\n    --glow-gradient: rgba(0, 0, 0, 0.12);\n    --composer-bg: rgba(247, 247, 247, 0.8);\n    background:\n      linear-gradient(var(--composer-bg), var(--composer-bg)) padding-box,\n      var(--glow-gradient) border-box;\n    border: 1px solid transparent;\n    backdrop-filter: blur(24px);\n    -webkit-backdrop-filter: blur(24px);\n  }\n\n  .dark .border-gradient-glow-composer {\n    --glow-gradient: radial-gradient(\n      ellipse 167% 263% at 0% 41%,\n      rgba(255, 255, 255, 0.41) 0%,\n      rgba(255, 255, 255, 0.12) 19%,\n      rgba(255, 255, 255, 0.02) 49%,\n      rgba(255, 255, 255, 0) 100%\n    );\n    --composer-bg: color-mix(in srgb, var(--background) 85%, transparent);\n  }\n\n  .border-gradient-glow-dot {\n    --glow-gradient: rgba(0, 0, 0, 0.12);\n  }\n\n  .dark .border-gradient-glow-dot {\n    --glow-gradient: radial-gradient(\n      ellipse 140% 200% at 25% 100%,\n      rgba(255, 255, 255, 0.4) 0%,\n      rgba(255, 255, 255, 0) 50%,\n      rgba(255, 255, 255, 0.01) 46%\n    );\n  }\n\n  .border-gradient-glow-notch {\n    --glow-gradient: rgba(0, 0, 0, 0.12);\n    background: var(--glow-gradient);\n    border: 1px solid transparent;\n    mask:\n      linear-gradient(#fff 0 0) padding-box,\n      linear-gradient(#fff 0 0);\n    mask-composite: exclude;\n    -webkit-mask-composite: xor;\n  }\n\n  .dark .border-gradient-glow-notch {\n    --glow-gradient: radial-gradient(\n      ellipse 120% 180% at 20% 80%,\n      rgba(255, 255, 255, 0.12) 0%,\n      rgba(255, 255, 255, 0.04) 30%,\n      rgba(255, 255, 255, 0.01) 60%,\n      rgba(255, 255, 255, 0) 100%\n    );\n  }\n\n  .gradient-line-header {\n    background: linear-gradient(\n      to right,\n      rgba(0, 0, 0, 0.07) 0%,\n      rgba(0, 0, 0, 0) 140%\n    );\n  }\n\n  .dark .gradient-line-header {\n    background: linear-gradient(\n      to right,\n      rgba(255, 255, 255, 0.1) 0%,\n      rgba(255, 255, 255, 0) 100%\n    );\n  }\n}\n"
  },
  {
    "path": "apps/www/app/styles/fumadocs-overrides.css",
    "content": "/* =============================================================================\n   Fumadocs UI Overrides\n   Scoped to docs/MDX surfaces for consistent styling with shadcn theme\n   ============================================================================= */\n\n/* -----------------------------------------------------------------------------\n   Debug / Development Variables\n   TODO: Remove or replace these placeholder values before production\n   ----------------------------------------------------------------------------- */\n\n:root {\n  --shiki-light-bg: var(--color-neutral-100);\n  --shiki-dark-bg: var(--color-neutral-900);\n\n  .dark {\n    --color-fd-foreground: hsl(0 0% 98%);\n  }\n}\n\n/* -----------------------------------------------------------------------------\n   Prose Content Overrides\n   Styling adjustments for MDX content within .prose containers\n   ----------------------------------------------------------------------------- */\n\n@layer utilities {\n  /* Step component badges (<Steps>/<Step>) */\n  .prose :where(.fd-step)::before {\n    @apply bg-muted text-primary size-8 text-lg;\n  }\n\n  /* Remove decorative quote marks from blockquotes */\n  .prose blockquote::before,\n  .prose blockquote::after,\n  .prose blockquote p::before,\n  .prose blockquote p::after {\n    content: none !important;\n  }\n}\n\n/* -----------------------------------------------------------------------------\n   Tab Panel Layout\n   Component preview and code block positioning within tab panels\n   ----------------------------------------------------------------------------- */\n\n/* Center component previews */\n[data-state][role=\"tabpanel\"] > .not-prose {\n  margin-inline: auto;\n}\n\n/* Full-width code blocks (negative margin compensates for panel padding) */\n[data-state][role=\"tabpanel\"] > figure:only-child {\n  margin: -1rem !important;\n}\n\n/* -----------------------------------------------------------------------------\n   Shiki Code Block Constraints\n   Prevents overflow from long unbreakable strings (URLs, tokens, etc.)\n   Fumadocs Pre uses w-max which causes horizontal overflow\n   ----------------------------------------------------------------------------- */\n\nfigure.shiki {\n  position: relative;\n  background-color: var(--color-neutral-100);\n  corner-shape: squircle;\n  border: none;\n  border-radius: calc(var(--radius) * var(--border-radius-multiplier));\n}\n\n.dark figure.shiki {\n  background-color: var(--color-neutral-900);\n}\n\nfigure.shiki pre {\n  width: 100% !important;\n  min-width: 0 !important;\n  max-width: 100% !important;\n}\n\nfigure.shiki pre code {\n  white-space: pre-wrap;\n  word-break: break-all;\n  overflow-wrap: anywhere;\n}\n\n/* -----------------------------------------------------------------------------\n   Full-Bleed Code Panel\n   Expanded code view with internal padding for view mode toggle\n   ----------------------------------------------------------------------------- */\n\n.code-panel-fullbleed {\n  overflow-y: scroll;\n  height: 100%;\n\n  & figure.shiki {\n    display: flex;\n    flex: 1;\n    flex-direction: column;\n    min-height: 0;\n    margin: 0;\n    border: none;\n    border-radius: 0;\n    height: 100%;\n    background: transparent;\n  }\n\n  & figure.shiki pre {\n    flex: 1;\n    margin: 0;\n    padding-top: 4rem; /* Space for toggle button */\n    border-radius: 0;\n  }\n\n  /* Fill available height instead of fumadocs default max-height */\n  & .fd-scroll-container {\n    height: 100%;\n    max-height: none !important;\n    padding-bottom: 6rem; /* Extra scroll room at bottom */\n  }\n\n  /* Hide fumadocs copy button (we provide our own) */\n  & figure.shiki > div.absolute {\n    display: none;\n  }\n\n  /* Restore squircle corners on larger screens */\n  @media (min-width: 1024px) {\n    & figure.shiki {\n      corner-shape: squircle;\n      border-radius: calc(var(--radius) * var(--border-radius-multiplier));\n    }\n  }\n}\n\n/* -----------------------------------------------------------------------------\n   Fumadocs Theme Tokens\n   NOTE: fumadocs-ui/css/shadcn.css automatically maps --color-fd-* to shadcn\n   variables (--background, --foreground, etc). No manual mapping needed here.\n   To customize fumadocs colors, edit shadcn-theme.css instead.\n   ----------------------------------------------------------------------------- */\n"
  },
  {
    "path": "apps/www/app/styles/globals.css",
    "content": "@import \"tailwindcss\";\n@import \"tw-animate-css\";\n@import \"tw-shimmer\";\n\n/* Fumadocs UI styles */\n@import \"fumadocs-ui/css/preset.css\";\n@import \"fumadocs-ui/css/shadcn.css\";\n\n/* Silk Sheet UI styles */\n@import \"@silk-hq/components/layered-styles\";\n\n/* Shadcn theme */\n@import \"./shadcn-theme.css\";\n\n/* Lib overrides */\n@import \"./leaflet-overrides.css\";\n@import \"./fumadocs-overrides.css\";\n\n/* Custom styles */\n@import \"./prose.css\";\n@import \"./squircle.css\";\n@import \"./custom-utilities.css\";\n\n/* View Transitions */\n@import \"./theme-transition.css\";\n\n@layer base {\n  * {\n    @apply border-border outline-ring/50;\n  }\n  html {\n    overflow: hidden;\n    overscroll-behavior: none;\n  }\n  body {\n    @apply bg-background text-foreground;\n    overflow: hidden;\n  }\n}\n"
  },
  {
    "path": "apps/www/app/styles/leaflet-overrides.css",
    "content": "/* Leaflet overrides for theme integration */\n\n.leaflet-container {\n  background: var(--muted);\n  /* Isolate Leaflet's high z-indices to prevent them from escaping the container */\n  isolation: isolate;\n}\n\n[data-theme=\"dark\"] .leaflet-container {\n  background: hsl(240 0 10%);\n}\n\n.leaflet-control-attribution {\n  background: oklch(from var(--background) l c h / 0.8) !important;\n  color: var(--muted-foreground);\n}\n\n[data-theme=\"dark\"] .leaflet-control-attribution {\n  background: hsl(240 10% 3.9% / 0.8) !important;\n  color: hsl(240 5% 64.9%);\n}\n\n.leaflet-control-attribution a {\n  color: var(--muted-foreground);\n}\n\n[data-theme=\"dark\"] .leaflet-control-attribution a {\n  color: hsl(240 5% 64.9%);\n}\n\n.leaflet-control-attribution a:hover {\n  color: var(--foreground);\n}\n\n[data-theme=\"dark\"] .leaflet-control-attribution a:hover {\n  color: hsl(0 0% 98%);\n}\n"
  },
  {
    "path": "apps/www/app/styles/nav-sheet.css",
    "content": ".MobileNavSheet-trigger {\n  position: fixed;\n  right: calc(1rem + env(safe-area-inset-right, 0px));\n  bottom: calc(2.75rem + env(safe-area-inset-bottom, 0px));\n}\n\n.MobileNavSheet-view {\n  height: var(--silk-100-lvh-dvh-pct);\n  z-index: 50;\n  position: fixed;\n  inset: 0;\n}\n\n:root {\n  --overlay-offset: 0px;\n}\n/* Broad fallback: difference between legacy vh and dynamic dvh */\n@supports (height: 100dvh) {\n  :root {\n    --overlay-offset: max(0px, calc(100vh - 100dvh));\n  }\n}\n/* Preferred: largest vs dynamic viewport (captures iOS Safari toolbar) */\n@supports (height: 100lvh) and (height: 100dvh) {\n  :root {\n    --overlay-offset: max(0px, calc(100lvh - 100dvh));\n  }\n}\n\n.MobileNavSheet-content {\n  box-sizing: border-box;\n  max-width: 700px;\n  max-height: 100vh;\n  /* Extend height to include safe area; prefer dynamic viewport units */\n  height: calc(92vh + env(safe-area-inset-bottom, 0px)); /* fallback */\n  height: calc(92svh + env(safe-area-inset-bottom, 0px)); /* small viewport */\n  height: calc(92dvh + env(safe-area-inset-bottom, 0px)); /* dynamic viewport */\n  display: flex;\n  flex-direction: column;\n  position: relative;\n  z-index: 52;\n  overflow: hidden;\n}\n\n.MobileNavSheet-content > div {\n  -webkit-overflow-scrolling: touch;\n  scroll-behavior: smooth;\n}\n\n.MobileNavSheet-backdrop {\n  background-color: rgb(0 0 0);\n  position: fixed;\n  inset: 0;\n  z-index: 51;\n}\n\n.MobileNavSheet-bleedingBackground {\n  overflow: hidden;\n  border-radius: 16px 16px 0 0;\n  background-color: var(--background);\n  box-shadow:\n    0 -4px 6px -1px rgb(0 0 0 / 0.1),\n    0 -2px 4px -2px rgb(0 0 0 / 0.1);\n}\n\n/* Bottom gradient that sits above the footer and hints scrolling */\n.MobileNavSheet-bottomGradient {\n  pointer-events: none;\n  position: absolute;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  height: 5rem; /* match visual density */\n  z-index: 15; /* stays below the footer */\n  background: linear-gradient(to top, var(--background), transparent);\n}\n\n/* Footer with safe-area padding, outside the scroll area */\n.MobileNavSheet-footer {\n  flex-shrink: 0;\n  background-color: var(--background);\n  position: relative;\n  z-index: 20; /* above gradient */\n  /* Older iOS fallback */\n  padding: 0.75rem 1rem calc(0.75rem + constant(safe-area-inset-bottom));\n  /* Modern: use the larger of safe-area or toolbar overlay (not both) */\n  padding: 0.75rem 1rem\n    calc(0.75rem + max(env(safe-area-inset-bottom, 0px), var(--overlay-offset)));\n}\n\n.MobileNavSheet-footerInner {\n  max-width: 700px;\n  margin: 0 auto;\n  display: flex;\n  gap: 0.5rem;\n}\n"
  },
  {
    "path": "apps/www/app/styles/prose.css",
    "content": "@layer utilities {\n  .prose {\n    @apply text-foreground;\n  }\n\n  .prose :where(h1):not(:where([class~=\"not-prose\"], [class~=\"not-prose\"] *)) {\n    @apply mt-0 mb-2 flex flex-wrap items-center gap-3 text-4xl font-medium tracking-normal;\n  }\n\n  .prose :where(h2):not(:where([class~=\"not-prose\"], [class~=\"not-prose\"] *)) {\n    @apply mb-4 text-2xl font-medium;\n  }\n\n  .prose\n    :where(h2:not(:first-child)):not(\n      :where([class~=\"not-prose\"], [class~=\"not-prose\"] *)\n    ) {\n    @apply mt-16;\n  }\n\n  .prose :where(h3):not(:where([class~=\"not-prose\"], [class~=\"not-prose\"] *)) {\n    @apply mt-8 mb-3 text-xl font-medium;\n  }\n\n  .prose :where(h4):not(:where([class~=\"not-prose\"], [class~=\"not-prose\"] *)) {\n    @apply mt-6 mb-2 text-lg;\n  }\n\n  .prose :where(p):not(:where([class~=\"not-prose\"], [class~=\"not-prose\"] *)) {\n    @apply mb-4;\n  }\n\n  .prose\n    :where(.lead):not(:where([class~=\"not-prose\"], [class~=\"not-prose\"] *)) {\n    @apply text-muted-foreground mb-6 text-xl leading-relaxed;\n  }\n\n  .prose :where(a):not(:where([class~=\"not-prose\"], [class~=\"not-prose\"] *)) {\n    @apply text-primary hover:text-primary/80 font-medium decoration-1 transition-colors;\n  }\n\n  .prose\n    :where(strong):not(:where([class~=\"not-prose\"], [class~=\"not-prose\"] *)) {\n    @apply text-foreground font-semibold;\n  }\n\n  .prose\n    :where(ul, ol):not(:where([class~=\"not-prose\"], [class~=\"not-prose\"] *)) {\n    /* Reduce default left indent for lists */\n    @apply my-4 ml-4 space-y-2;\n  }\n\n  /* Reduce indent for nested lists as well */\n  .prose\n    :where(li > ul, li > ol):not(\n      :where([class~=\"not-prose\"], [class~=\"not-prose\"] *)\n    ) {\n    @apply ml-4;\n  }\n\n  .prose :where(ul):not(:where([class~=\"not-prose\"], [class~=\"not-prose\"] *)) {\n    @apply list-disc;\n  }\n\n  .prose :where(ol):not(:where([class~=\"not-prose\"], [class~=\"not-prose\"] *)) {\n    @apply list-decimal;\n  }\n\n  .prose :where(li):not(:where([class~=\"not-prose\"], [class~=\"not-prose\"] *)) {\n    @apply leading-7;\n  }\n\n  .prose\n    :where(li > p):not(:where([class~=\"not-prose\"], [class~=\"not-prose\"] *)) {\n    @apply my-2;\n  }\n\n  .prose\n    :where(code):not(:where([class~=\"not-prose\"], [class~=\"not-prose\"] *)) {\n    @apply bg-primary/10 dark:bg-primary/15 rounded-md border-none px-1.5 py-1 font-mono;\n  }\n\n  /* Remove Tailwind Typography's visible backtick quotes around inline code */\n  .prose :where(:not(pre) > code)::before,\n  .prose :where(:not(pre) > code)::after {\n    content: none !important;\n  }\n\n  .prose :where(pre):not(:where([class~=\"not-prose\"], [class~=\"not-prose\"] *)) {\n    @apply my-6 overflow-x-auto rounded-lg bg-muted p-4;\n  }\n\n  .prose\n    :where(pre code):not(:where([class~=\"not-prose\"], [class~=\"not-prose\"] *)) {\n    @apply bg-transparent p-0;\n  }\n\n  .prose\n    :where(blockquote):not(\n      :where([class~=\"not-prose\"], [class~=\"not-prose\"] *)\n    ) {\n    @apply border-primary text-muted-foreground my-6 border-l-4 pl-4 font-normal italic;\n  }\n\n  /* Remove quote marks around blockquoted text */\n  .prose :where(blockquote)::before,\n  .prose :where(blockquote)::after {\n    content: none !important;\n  }\n\n  .prose :where(hr):not(:where([class~=\"not-prose\"], [class~=\"not-prose\"] *)) {\n    @apply border-border my-8;\n  }\n\n  .prose\n    :where(table):not(:where([class~=\"not-prose\"], [class~=\"not-prose\"] *)) {\n    @apply my-6 w-full border-collapse;\n  }\n\n  .prose\n    :where(thead):not(:where([class~=\"not-prose\"], [class~=\"not-prose\"] *)) {\n    @apply border-border border-b;\n  }\n\n  .prose :where(th):not(:where([class~=\"not-prose\"], [class~=\"not-prose\"] *)) {\n    @apply p-2 text-left font-semibold;\n  }\n\n  .prose :where(td):not(:where([class~=\"not-prose\"], [class~=\"not-prose\"] *)) {\n    @apply border-border border-t p-2;\n  }\n}\n"
  },
  {
    "path": "apps/www/app/styles/shadcn-theme.css",
    "content": "/* Tailwind v4 theme configuration */\n@custom-variant dark (&:is(.dark *, [data-theme=\"dark\"], [data-theme=\"dark\"] *));\n\n@import \"tailwindcss\";\n@import \"tw-animate-css\";\n\n@custom-variant dark (&:is(.dark *));\n\n@theme inline {\n  --radius-sm: calc(var(--radius) - 4px);\n  --radius-md: calc(var(--radius) - 2px);\n  --radius-lg: var(--radius);\n  --radius-xl: calc(var(--radius) + 4px);\n  --radius-2xl: calc(var(--radius) + 8px);\n  --radius-3xl: calc(var(--radius) + 12px);\n  --radius-4xl: calc(var(--radius) + 16px);\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --color-card: var(--card);\n  --color-card-foreground: var(--card-foreground);\n  --color-popover: var(--popover);\n  --color-popover-foreground: var(--popover-foreground);\n  --color-primary: var(--primary);\n  --color-primary-foreground: var(--primary-foreground);\n  --color-secondary: var(--secondary);\n  --color-secondary-foreground: var(--secondary-foreground);\n  --color-muted: var(--muted);\n  --color-muted-foreground: var(--muted-foreground);\n  --color-accent: var(--accent);\n  --color-accent-foreground: var(--accent-foreground);\n  --color-destructive: var(--destructive);\n  --color-border: var(--border);\n  --color-input: var(--input);\n  --color-ring: var(--ring);\n  --color-chart-1: var(--chart-1);\n  --color-chart-2: var(--chart-2);\n  --color-chart-3: var(--chart-3);\n  --color-chart-4: var(--chart-4);\n  --color-chart-5: var(--chart-5);\n  --color-sidebar: var(--sidebar);\n  --color-sidebar-foreground: var(--sidebar-foreground);\n  --color-sidebar-primary: var(--sidebar-primary);\n  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n  --color-sidebar-accent: var(--sidebar-accent);\n  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n  --color-sidebar-border: var(--sidebar-border);\n  --color-sidebar-ring: var(--sidebar-ring);\n}\n\n:root {\n  color-scheme: light;\n  --radius: 0.625rem;\n  --background: oklch(1 0 0);\n  --foreground: oklch(0.145 0 0);\n  --card: oklch(1 0 0);\n  --card-foreground: oklch(0.145 0 0);\n  --popover: oklch(1 0 0);\n  --popover-foreground: oklch(0.145 0 0);\n  --primary: oklch(0.205 0 0);\n  --primary-foreground: oklch(0.985 0 0);\n  --secondary: oklch(0.97 0 0);\n  --secondary-foreground: oklch(0.205 0 0);\n  --muted: oklch(0.97 0 0);\n  --muted-foreground: oklch(0.556 0 0);\n  --accent: oklch(0.97 0 0);\n  --accent-foreground: oklch(0.205 0 0);\n  --destructive: oklch(0.577 0.245 27.325);\n  --border: oklch(0.922 0 0);\n  --input: oklch(0.922 0 0);\n  --ring: oklch(0.708 0 0);\n  --chart-1: var(--color-green-500);\n  --chart-2: var(--color-red-500);\n  --chart-3: var(--color-blue-500);\n  --chart-4: var(--color-purple-500);\n  --chart-5: var(--color-yellow-500);\n  --sidebar: oklch(0.985 0 0);\n  --sidebar-foreground: oklch(0.145 0 0);\n  --sidebar-primary: oklch(0.205 0 0);\n  --sidebar-primary-foreground: oklch(0.985 0 0);\n  --sidebar-accent: oklch(0.97 0 0);\n  --sidebar-accent-foreground: oklch(0.205 0 0);\n  --sidebar-border: oklch(0.922 0 0);\n  --sidebar-ring: oklch(0.708 0 0);\n}\n\n.dark {\n  color-scheme: dark;\n  --background: oklch(0.145 0 0);\n  --foreground: oklch(0.985 0 0);\n  --card: oklch(0.205 0 0);\n  --card-foreground: oklch(0.985 0 0);\n  --popover: oklch(0.205 0 0);\n  --popover-foreground: oklch(0.985 0 0);\n  --primary: oklch(0.922 0 0);\n  --primary-foreground: oklch(0.205 0 0);\n  --secondary: oklch(0.269 0 0);\n  --secondary-foreground: oklch(0.985 0 0);\n  --muted: oklch(0.269 0 0);\n  --muted-foreground: oklch(0.708 0 0);\n  --accent: oklch(0.269 0 0);\n  --accent-foreground: oklch(0.985 0 0);\n  --destructive: oklch(0.704 0.191 22.216);\n  --border: oklch(1 0 0 / 10%);\n  --input: oklch(1 0 0 / 15%);\n  --ring: oklch(0.556 0 0);\n  --chart-1: var(--color-green-400);\n  --chart-2: var(--color-red-400);\n  --chart-3: var(--color-blue-400);\n  --chart-4: var(--color-purple-400);\n  --chart-5: var(--color-yellow-400);\n  --sidebar: oklch(0.205 0 0);\n  --sidebar-foreground: oklch(0.985 0 0);\n  --sidebar-primary: oklch(0.488 0.243 264.376);\n  --sidebar-primary-foreground: oklch(0.985 0 0);\n  --sidebar-accent: oklch(0.269 0 0);\n  --sidebar-accent-foreground: oklch(0.985 0 0);\n  --sidebar-border: oklch(1 0 0 / 10%);\n  --sidebar-ring: oklch(0.556 0 0);\n}\n\n/* Isolated theme scopes for docs preview */\n/* Double attribute selector increases specificity to override .dark from ancestors */\n[data-theme=\"light\"][data-theme] {\n  color-scheme: light;\n  --wash: oklch(0.95 0 0);\n  --background: oklch(1 0 0);\n  --foreground: oklch(0.145 0 0);\n  --card: oklch(1 0 0);\n  --card-foreground: oklch(0.145 0 0);\n  --popover: oklch(1 0 0);\n  --popover-foreground: oklch(0.145 0 0);\n  --primary: oklch(0.205 0 0);\n  --primary-foreground: oklch(0.985 0 0);\n  --secondary: oklch(0.97 0 0);\n  --secondary-foreground: oklch(0.205 0 0);\n  --muted: oklch(0.97 0 0);\n  --muted-foreground: oklch(0.556 0 0);\n  --accent: oklch(0.97 0 0);\n  --accent-foreground: oklch(0.205 0 0);\n  --destructive: oklch(0.577 0.245 27.325);\n  --destructive-foreground: oklch(0.985 0 0);\n  --border: oklch(0.922 0 0);\n  --input: oklch(0.922 0 0);\n  --ring: oklch(0.708 0 0);\n  --chart-1: var(--color-green-500);\n  --chart-2: var(--color-red-500);\n  --chart-3: var(--color-blue-500);\n  --chart-4: var(--color-purple-500);\n  --chart-5: var(--color-yellow-500);\n}\n\n[data-theme=\"dark\"][data-theme] {\n  color-scheme: dark;\n  --wash: oklch(0.15 0 0);\n  --background: oklch(0.145 0 0);\n  --foreground: oklch(0.985 0 0);\n  --card: oklch(0.205 0 0);\n  --card-foreground: oklch(0.985 0 0);\n  --popover: oklch(0.205 0 0);\n  --popover-foreground: oklch(0.985 0 0);\n  --primary: oklch(0.922 0 0);\n  --primary-foreground: oklch(0.205 0 0);\n  --secondary: oklch(0.269 0 0);\n  --secondary-foreground: oklch(0.985 0 0);\n  --muted: oklch(0.269 0 0);\n  --muted-foreground: oklch(0.708 0 0);\n  --accent: oklch(0.269 0 0);\n  --accent-foreground: oklch(0.985 0 0);\n  --destructive: oklch(0.704 0.191 22.216);\n  --destructive-foreground: oklch(0.985 0 0);\n  --border: oklch(1 0 0 / 10%);\n  --input: oklch(1 0 0 / 15%);\n  --ring: oklch(0.556 0 0);\n  --chart-1: var(--color-green-400);\n  --chart-2: var(--color-red-400);\n  --chart-3: var(--color-blue-400);\n  --chart-4: var(--color-purple-400);\n  --chart-5: var(--color-yellow-400);\n}\n"
  },
  {
    "path": "apps/www/app/styles/squircle.css",
    "content": "/* Squircle corners - progressively enhanced / only visible if supported */\n:root {\n  --border-radius-multiplier: 1;\n}\n\n@supports (corner-shape: squircle) {\n  :root {\n    --border-radius-multiplier: 2;\n  }\n}\n\n.squircle {\n  corner-shape: squircle;\n  border-radius: 30px !important;\n  border-radius: calc(var(--radius) * var(--border-radius-multiplier));\n}\n"
  },
  {
    "path": "apps/www/app/styles/theme-transition.css",
    "content": "/*\n  Theme transition styles adapted from https://github.com/rudrodip/theme-toggle-effect\n  Scoped to [data-theme-transition] to avoid conflicts with other view transitions.\n*/\n\n@layer base {\n  @keyframes theme-scale {\n    to {\n      mask-size: 350vmax;\n    }\n  }\n\n  [data-theme-transition]::view-transition-old(root),\n  [data-theme-transition]::view-transition-new(root) {\n    animation: none;\n    mix-blend-mode: normal;\n  }\n\n  [data-theme-transition]::view-transition-new(root) {\n    mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3Cfilter id='blur' x='-100%25' y='-100%25' width='400%25' height='400%25'%3E%3CfeGaussianBlur stdDeviation='4'/%3E%3C/filter%3E%3C/defs%3E%3Ccircle cx='100' cy='0' r='40' fill='white' filter='url(%23blur)'/%3E%3C/svg%3E\")\n      calc(100% + 10px) top / 0% no-repeat;\n    mask-origin: content-box;\n    animation: theme-scale 0.6s ease-in-out;\n    animation-fill-mode: forwards;\n  }\n\n  [data-theme-transition]::view-transition-old(root) {\n    z-index: -1;\n  }\n\n  [data-theme-transition].dark::view-transition-new(root) {\n    z-index: 1;\n  }\n}\n"
  },
  {
    "path": "apps/www/components/assistant-ui/assistant-modal.tsx",
    "content": "\"use client\";\n\nimport { BotIcon, ChevronDownIcon } from \"lucide-react\";\n\nimport { type FC, forwardRef } from \"react\";\nimport { AssistantModalPrimitive } from \"@assistant-ui/react\";\n\nimport { Thread } from \"@/components/assistant-ui/thread\";\nimport { TooltipIconButton } from \"@/components/assistant-ui/tooltip-icon-button\";\n\nexport const AssistantModal: FC = () => {\n  return (\n    <AssistantModalPrimitive.Root>\n      <AssistantModalPrimitive.Anchor className=\"aui-root aui-modal-anchor fixed right-4 bottom-4 size-11\">\n        <AssistantModalPrimitive.Trigger asChild>\n          <AssistantModalButton />\n        </AssistantModalPrimitive.Trigger>\n      </AssistantModalPrimitive.Anchor>\n      <AssistantModalPrimitive.Content\n        sideOffset={16}\n        className=\"aui-root aui-modal-content data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-bottom-1/2 data-[state=closed]:slide-out-to-right-1/2 data-[state=closed]:zoom-out data-[state=open]:fade-in-0 data-[state=open]:slide-in-from-bottom-1/2 data-[state=open]:slide-in-from-right-1/2 data-[state=open]:zoom-in z-50 h-[500px] w-[400px] overflow-clip overscroll-contain rounded-xl border bg-popover p-0 text-popover-foreground shadow-md outline-none data-[state=closed]:animate-out data-[state=open]:animate-in [&>.aui-thread-root]:bg-inherit\"\n      >\n        <Thread />\n      </AssistantModalPrimitive.Content>\n    </AssistantModalPrimitive.Root>\n  );\n};\n\ntype AssistantModalButtonProps = { \"data-state\"?: \"open\" | \"closed\" };\n\nconst AssistantModalButton = forwardRef<\n  HTMLButtonElement,\n  AssistantModalButtonProps\n>(({ \"data-state\": state, ...rest }, ref) => {\n  const tooltip = state === \"open\" ? \"Close Assistant\" : \"Open Assistant\";\n\n  return (\n    <TooltipIconButton\n      variant=\"default\"\n      tooltip={tooltip}\n      side=\"left\"\n      {...rest}\n      className=\"aui-modal-button size-full rounded-full shadow transition-transform hover:scale-110 active:scale-90\"\n      ref={ref}\n    >\n      <BotIcon\n        data-state={state}\n        className=\"aui-modal-button-closed-icon absolute size-6 transition-all data-[state=closed]:rotate-0 data-[state=open]:rotate-90 data-[state=closed]:scale-100 data-[state=open]:scale-0\"\n      />\n\n      <ChevronDownIcon\n        data-state={state}\n        className=\"aui-modal-button-open-icon absolute size-6 transition-all data-[state=closed]:-rotate-90 data-[state=open]:rotate-0 data-[state=closed]:scale-0 data-[state=open]:scale-100\"\n      />\n      <span className=\"aui-sr-only sr-only\">{tooltip}</span>\n    </TooltipIconButton>\n  );\n});\n\nAssistantModalButton.displayName = \"AssistantModalButton\";\n"
  },
  {
    "path": "apps/www/components/assistant-ui/attachment.tsx",
    "content": "\"use client\";\n\nimport { PropsWithChildren, useEffect, useState, type FC } from \"react\";\nimport Image from \"next/image\";\nimport { XIcon, PlusIcon, FileText } from \"lucide-react\";\nimport {\n  AttachmentPrimitive,\n  ComposerPrimitive,\n  MessagePrimitive,\n  useAuiState,\n  useAui,\n} from \"@assistant-ui/react\";\nimport { useShallow } from \"zustand/shallow\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport {\n  Dialog,\n  DialogTitle,\n  DialogContent,\n  DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"@/components/ui/avatar\";\nimport { TooltipIconButton } from \"@/components/assistant-ui/tooltip-icon-button\";\nimport { cn } from \"@/lib/ui/cn\";\n\nconst useFileSrc = (file: File | undefined) => {\n  const [src, setSrc] = useState<string | undefined>(undefined);\n\n  useEffect(() => {\n    if (!file) {\n      setSrc(undefined);\n      return;\n    }\n\n    const objectUrl = URL.createObjectURL(file);\n    setSrc(objectUrl);\n\n    return () => {\n      URL.revokeObjectURL(objectUrl);\n    };\n  }, [file]);\n\n  return src;\n};\n\nconst useAttachmentSrc = () => {\n  const { file, src } = useAuiState(\n    useShallow(({ attachment }): { file?: File; src?: string } => {\n      if (attachment.type !== \"image\") return {};\n      if (attachment.file) return { file: attachment.file };\n      const src = attachment.content?.filter((c) => c.type === \"image\")[0]\n        ?.image;\n      if (!src) return {};\n      return { src };\n    }),\n  );\n\n  return useFileSrc(file) ?? src;\n};\n\ntype AttachmentPreviewProps = {\n  src: string;\n};\n\nconst AttachmentPreview: FC<AttachmentPreviewProps> = ({ src }) => {\n  const [isLoaded, setIsLoaded] = useState(false);\n  return (\n    <Image\n      src={src}\n      alt=\"Image Preview\"\n      width={1}\n      height={1}\n      className={\n        isLoaded\n          ? \"aui-attachment-preview-image-loaded block h-auto max-h-[80vh] w-auto max-w-full object-contain\"\n          : \"aui-attachment-preview-image-loading hidden\"\n      }\n      onLoadingComplete={() => setIsLoaded(true)}\n      priority={false}\n    />\n  );\n};\n\nconst AttachmentPreviewDialog: FC<PropsWithChildren> = ({ children }) => {\n  const src = useAttachmentSrc();\n\n  if (!src) return children;\n\n  return (\n    <Dialog>\n      <DialogTrigger\n        className=\"aui-attachment-preview-trigger cursor-pointer transition-colors hover:bg-accent/50\"\n        asChild\n      >\n        {children}\n      </DialogTrigger>\n      <DialogContent className=\"aui-attachment-preview-dialog-content p-2 sm:max-w-3xl [&>button]:rounded-full [&>button]:bg-foreground/60 [&>button]:p-1 [&>button]:opacity-100 [&>button]:ring-0! [&_svg]:text-background [&>button]:hover:[&_svg]:text-destructive\">\n        <DialogTitle className=\"aui-sr-only sr-only\">\n          Image Attachment Preview\n        </DialogTitle>\n        <div className=\"aui-attachment-preview relative mx-auto flex max-h-[80dvh] w-full items-center justify-center overflow-hidden bg-background\">\n          <AttachmentPreview src={src} />\n        </div>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nconst AttachmentThumb: FC = () => {\n  const isImage = useAuiState(({ attachment }) => attachment.type === \"image\");\n  const src = useAttachmentSrc();\n\n  return (\n    <Avatar className=\"aui-attachment-tile-avatar h-full w-full rounded-none\">\n      <AvatarImage\n        src={src}\n        alt=\"Attachment preview\"\n        className=\"aui-attachment-tile-image object-cover\"\n      />\n      <AvatarFallback delayMs={isImage ? 200 : 0}>\n        <FileText className=\"aui-attachment-tile-fallback-icon size-8 text-muted-foreground\" />\n      </AvatarFallback>\n    </Avatar>\n  );\n};\n\nconst AttachmentUI: FC = () => {\n  const aui = useAui();\n  const isComposer = aui.attachment.source === \"composer\";\n\n  const isImage = useAuiState(({ attachment }) => attachment.type === \"image\");\n  const typeLabel = useAuiState(({ attachment }) => {\n    const type = attachment.type;\n    switch (type) {\n      case \"image\":\n        return \"Image\";\n      case \"document\":\n        return \"Document\";\n      case \"file\":\n        return \"File\";\n      default:\n        const _exhaustiveCheck: never = type;\n        throw new Error(`Unknown attachment type: ${_exhaustiveCheck}`);\n    }\n  });\n\n  return (\n    <Tooltip>\n      <AttachmentPrimitive.Root\n        className={cn(\n          \"aui-attachment-root relative\",\n          isImage &&\n            \"aui-attachment-root-composer only:[&>#attachment-tile]:size-24\",\n        )}\n      >\n        <AttachmentPreviewDialog>\n          <TooltipTrigger asChild>\n            <div\n              className={cn(\n                \"aui-attachment-tile size-14 cursor-pointer overflow-hidden rounded-[14px] border bg-muted transition-opacity hover:opacity-75\",\n                isComposer &&\n                  \"aui-attachment-tile-composer border-foreground/20\",\n              )}\n              role=\"button\"\n              id=\"attachment-tile\"\n              aria-label={`${typeLabel} attachment`}\n            >\n              <AttachmentThumb />\n            </div>\n          </TooltipTrigger>\n        </AttachmentPreviewDialog>\n        {isComposer && <AttachmentRemove />}\n      </AttachmentPrimitive.Root>\n      <TooltipContent side=\"top\">\n        <AttachmentPrimitive.Name />\n      </TooltipContent>\n    </Tooltip>\n  );\n};\n\nconst AttachmentRemove: FC = () => {\n  return (\n    <AttachmentPrimitive.Remove asChild>\n      <TooltipIconButton\n        tooltip=\"Remove file\"\n        className=\"aui-attachment-tile-remove absolute top-1.5 right-1.5 size-3.5 rounded-full bg-white text-muted-foreground opacity-100 shadow-sm hover:bg-white! [&_svg]:text-black hover:[&_svg]:text-destructive\"\n        side=\"top\"\n      >\n        <XIcon className=\"aui-attachment-remove-icon size-3 dark:stroke-[2.5px]\" />\n      </TooltipIconButton>\n    </AttachmentPrimitive.Remove>\n  );\n};\n\nexport const UserMessageAttachments: FC = () => {\n  return (\n    <div className=\"aui-user-message-attachments-end col-span-full col-start-1 row-start-1 flex w-full flex-row justify-end gap-2\">\n      <MessagePrimitive.Attachments components={{ Attachment: AttachmentUI }} />\n    </div>\n  );\n};\n\nexport const ComposerAttachments: FC = () => {\n  return (\n    <div className=\"aui-composer-attachments mb-2 flex w-full flex-row items-center gap-2 overflow-x-auto px-1.5 pt-0.5 pb-1 empty:hidden\">\n      <ComposerPrimitive.Attachments\n        components={{ Attachment: AttachmentUI }}\n      />\n    </div>\n  );\n};\n\nexport const ComposerAddAttachment: FC = () => {\n  return (\n    <ComposerPrimitive.AddAttachment asChild>\n      <TooltipIconButton\n        tooltip=\"Add Attachment\"\n        side=\"bottom\"\n        variant=\"ghost\"\n        size=\"icon\"\n        className=\"aui-composer-add-attachment size-[34px] rounded-full p-1 font-semibold text-xs hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30\"\n        aria-label=\"Add Attachment\"\n      >\n        <PlusIcon className=\"aui-attachment-add-icon size-5 stroke-[1.5px]\" />\n      </TooltipIconButton>\n    </ComposerPrimitive.AddAttachment>\n  );\n};\n"
  },
  {
    "path": "apps/www/components/assistant-ui/markdown-text.tsx",
    "content": "\"use client\";\n\nimport \"@assistant-ui/react-markdown/styles/dot.css\";\n\nimport {\n  type CodeHeaderProps,\n  MarkdownTextPrimitive,\n  unstable_memoizeMarkdownComponents as memoizeMarkdownComponents,\n  useIsMarkdownCodeBlock,\n} from \"@assistant-ui/react-markdown\";\nimport remarkGfm from \"remark-gfm\";\nimport { type FC, memo, useState } from \"react\";\nimport { CheckIcon, CopyIcon } from \"lucide-react\";\n\nimport { TooltipIconButton } from \"@/components/assistant-ui/tooltip-icon-button\";\nimport { cn } from \"@/lib/ui/cn\";\n\nconst MarkdownTextImpl = () => {\n  return (\n    <MarkdownTextPrimitive\n      remarkPlugins={[remarkGfm]}\n      className=\"aui-md\"\n      components={defaultComponents}\n    />\n  );\n};\n\nexport const MarkdownText = memo(MarkdownTextImpl);\n\nconst CodeHeader: FC<CodeHeaderProps> = ({ language, code }) => {\n  const { isCopied, copyToClipboard } = useCopyToClipboard();\n  const onCopy = () => {\n    if (!code || isCopied) return;\n    copyToClipboard(code);\n  };\n\n  return (\n    <div className=\"aui-code-header-root bg-muted-foreground/15 text-foreground dark:bg-muted-foreground/20 mt-4 flex items-center justify-between gap-4 rounded-t-lg px-4 py-2 text-sm font-semibold\">\n      <span className=\"aui-code-header-language lowercase [&>span]:text-xs\">\n        {language}\n      </span>\n      <TooltipIconButton tooltip=\"Copy\" onClick={onCopy}>\n        {!isCopied && <CopyIcon />}\n        {isCopied && <CheckIcon />}\n      </TooltipIconButton>\n    </div>\n  );\n};\n\nconst useCopyToClipboard = ({\n  copiedDuration = 3000,\n}: {\n  copiedDuration?: number;\n} = {}) => {\n  const [isCopied, setIsCopied] = useState<boolean>(false);\n\n  const copyToClipboard = (value: string) => {\n    if (!value) return;\n\n    navigator.clipboard.writeText(value).then(() => {\n      setIsCopied(true);\n      setTimeout(() => setIsCopied(false), copiedDuration);\n    });\n  };\n\n  return { isCopied, copyToClipboard };\n};\n\nconst defaultComponents = memoizeMarkdownComponents({\n  h1: ({ className, ...props }) => (\n    <h1\n      className={cn(\n        \"aui-md-h1 mb-8 scroll-m-20 text-4xl font-extrabold tracking-tight last:mb-0\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n  h2: ({ className, ...props }) => (\n    <h2\n      className={cn(\n        \"aui-md-h2 mt-8 mb-4 scroll-m-20 text-3xl font-semibold tracking-tight first:mt-0 last:mb-0\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n  h3: ({ className, ...props }) => (\n    <h3\n      className={cn(\n        \"aui-md-h3 mt-6 mb-4 scroll-m-20 text-2xl font-semibold tracking-tight first:mt-0 last:mb-0\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n  h4: ({ className, ...props }) => (\n    <h4\n      className={cn(\n        \"aui-md-h4 mt-6 mb-4 scroll-m-20 text-xl font-semibold tracking-tight first:mt-0 last:mb-0\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n  h5: ({ className, ...props }) => (\n    <h5\n      className={cn(\n        \"aui-md-h5 my-4 text-lg font-semibold first:mt-0 last:mb-0\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n  h6: ({ className, ...props }) => (\n    <h6\n      className={cn(\n        \"aui-md-h6 my-4 font-semibold first:mt-0 last:mb-0\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n  p: ({ className, ...props }) => (\n    <p\n      className={cn(\"aui-md-p mt-5 mb-5 first:mt-0 last:mb-0\", className)}\n      {...props}\n    />\n  ),\n  a: ({ className, ...props }) => (\n    <a\n      className={cn(\n        \"aui-md-a text-primary font-medium underline underline-offset-4\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n  blockquote: ({ className, ...props }) => (\n    <blockquote\n      className={cn(\"aui-md-blockquote border-l-2 pl-6 italic\", className)}\n      {...props}\n    />\n  ),\n  ul: ({ className, ...props }) => (\n    <ul\n      className={cn(\"aui-md-ul my-5 ml-6 list-disc [&>li]:mt-2\", className)}\n      {...props}\n    />\n  ),\n  ol: ({ className, ...props }) => (\n    <ol\n      className={cn(\"aui-md-ol my-5 ml-6 list-decimal [&>li]:mt-2\", className)}\n      {...props}\n    />\n  ),\n  hr: ({ className, ...props }) => (\n    <hr className={cn(\"aui-md-hr my-5 border-b\", className)} {...props} />\n  ),\n  table: ({ className, ...props }) => (\n    <table\n      className={cn(\n        \"aui-md-table my-5 w-full border-separate border-spacing-0 overflow-y-auto\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n  th: ({ className, ...props }) => (\n    <th\n      className={cn(\n        \"aui-md-th bg-muted px-4 py-2 text-left font-bold first:rounded-tl-lg last:rounded-tr-lg [[align=center]]:text-center [[align=right]]:text-right\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n  td: ({ className, ...props }) => (\n    <td\n      className={cn(\n        \"aui-md-td border-b border-l px-4 py-2 text-left last:border-r [[align=center]]:text-center [[align=right]]:text-right\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n  tr: ({ className, ...props }) => (\n    <tr\n      className={cn(\n        \"aui-md-tr m-0 border-b p-0 first:border-t [&:last-child>td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n  sup: ({ className, ...props }) => (\n    <sup\n      className={cn(\"aui-md-sup [&>a]:text-xs [&>a]:no-underline\", className)}\n      {...props}\n    />\n  ),\n  pre: ({ className, ...props }) => (\n    <pre\n      className={cn(\n        \"aui-md-pre overflow-x-auto rounded-t-none! rounded-b-lg bg-black p-4 text-white\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n  code: function Code({ className, ...props }) {\n    const isCodeBlock = useIsMarkdownCodeBlock();\n    return (\n      <code\n        className={cn(\n          !isCodeBlock &&\n            \"aui-md-inline-code bg-muted rounded border font-semibold\",\n          className,\n        )}\n        {...props}\n      />\n    );\n  },\n  CodeHeader,\n});\n"
  },
  {
    "path": "apps/www/components/assistant-ui/reasoning.tsx",
    "content": "\"use client\";\n\nimport { BrainIcon, ChevronDownIcon } from \"lucide-react\";\nimport {\n  memo,\n  useCallback,\n  useRef,\n  useState,\n  type FC,\n  type PropsWithChildren,\n} from \"react\";\n\nimport {\n  useScrollLock,\n  useAuiState,\n  type ReasoningMessagePartComponent,\n  type ReasoningGroupComponent,\n} from \"@assistant-ui/react\";\n\nimport { MarkdownText } from \"@/components/assistant-ui/markdown-text\";\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@/components/ui/collapsible\";\nimport { cn } from \"@/lib/ui/cn\";\n\nconst ANIMATION_DURATION = 200;\nconst SHIMMER_DURATION = 1000;\n\n/**\n * Root collapsible container that manages open/closed state and scroll lock.\n * Provides animation timing via CSS variable and prevents scroll jumps on collapse.\n */\nconst ReasoningRoot: FC<\n  PropsWithChildren<{\n    className?: string;\n  }>\n> = ({ className, children }) => {\n  const collapsibleRef = useRef<HTMLDivElement>(null);\n  const [isOpen, setIsOpen] = useState(false);\n  const lockScroll = useScrollLock(collapsibleRef, ANIMATION_DURATION);\n\n  const handleOpenChange = useCallback(\n    (open: boolean) => {\n      if (!open) {\n        lockScroll();\n      }\n      setIsOpen(open);\n    },\n    [lockScroll],\n  );\n\n  return (\n    <Collapsible\n      ref={collapsibleRef}\n      open={isOpen}\n      onOpenChange={handleOpenChange}\n      className={cn(\"aui-reasoning-root mb-4 w-full\", className)}\n      style={\n        {\n          \"--animation-duration\": `${ANIMATION_DURATION}ms`,\n          \"--shimmer-duration\": `${SHIMMER_DURATION}ms`,\n        } as React.CSSProperties\n      }\n    >\n      {children}\n    </Collapsible>\n  );\n};\n\nReasoningRoot.displayName = \"ReasoningRoot\";\n\n/**\n * Gradient overlay that softens the bottom edge during expand/collapse animations.\n * Animation: Fades out with delay when opening and fades back in when closing.\n */\nconst GradientFade: FC<{ className?: string }> = ({ className }) => (\n  <div\n    className={cn(\n      \"aui-reasoning-fade pointer-events-none absolute inset-x-0 bottom-0 z-10 h-16\",\n      \"bg-[linear-gradient(to_top,var(--color-background),transparent)]\",\n      \"animate-in fade-in-0\",\n      \"group-data-[state=open]/collapsible-content:animate-out\",\n      \"group-data-[state=open]/collapsible-content:fade-out-0\",\n      \"group-data-[state=open]/collapsible-content:delay-[calc(var(--animation-duration)*0.75)]\", // calc for timing the delay\n      \"group-data-[state=open]/collapsible-content:fill-mode-forwards\",\n      \"duration-(--animation-duration)\",\n      \"group-data-[state=open]/collapsible-content:duration-(--animation-duration)\",\n      className,\n    )}\n  />\n);\n\n/**\n * Trigger button for the Reasoning collapsible.\n * Composed of icons, label, and text shimmer animation when reasoning is being streamed.\n */\nconst ReasoningTrigger: FC<{ active: boolean; className?: string }> = ({\n  active,\n  className,\n}) => (\n  <CollapsibleTrigger\n    className={cn(\n      \"aui-reasoning-trigger group/trigger -mb-2 flex max-w-[75%] items-center gap-2 py-2 text-sm text-muted-foreground transition-colors hover:text-foreground\",\n      className,\n    )}\n  >\n    <BrainIcon className=\"aui-reasoning-trigger-icon size-4 shrink-0\" />\n    <span className=\"aui-reasoning-trigger-label-wrapper relative inline-block leading-none\">\n      <span>Reasoning</span>\n      {active ? (\n        <span\n          aria-hidden\n          className={cn(\n            \"aui-reasoning-trigger-shimmer pointer-events-none absolute inset-0 bg-clip-text bg-no-repeat text-transparent motion-reduce:animate-none\",\n            \"animate-shimmer will-change-[background-position]\",\n            \"bg-size-[200%_100%]\",\n            \"bg-[linear-gradient(90deg,transparent_0%,transparent_40%,color-mix(in_oklch,var(--foreground)_75%,transparent)_56%,transparent_80%,transparent_100%)]\",\n          )}\n        >\n          Reasoning\n        </span>\n      ) : null}\n    </span>\n    <ChevronDownIcon\n      className={cn(\n        \"aui-reasoning-trigger-chevron mt-0.5 size-4 shrink-0\",\n        \"transition-transform duration-(--animation-duration) ease-out\",\n        \"group-data-[state=closed]/trigger:-rotate-90\",\n        \"group-data-[state=open]/trigger:rotate-0\",\n      )}\n    />\n  </CollapsibleTrigger>\n);\n\n/**\n * Collapsible content wrapper that handles height expand/collapse animation.\n * Animation: Height animates up (collapse) and down (expand).\n * Also provides group context for child animations via data-state attributes.\n */\nconst ReasoningContent: FC<\n  PropsWithChildren<{\n    className?: string;\n    \"aria-busy\"?: boolean;\n  }>\n> = ({ className, children, \"aria-busy\": ariaBusy }) => (\n  <CollapsibleContent\n    className={cn(\n      \"aui-reasoning-content relative overflow-hidden text-sm text-muted-foreground outline-none\",\n      \"group/collapsible-content ease-out\",\n      \"data-[state=closed]:animate-collapsible-up\",\n      \"data-[state=open]:animate-collapsible-down\",\n      \"data-[state=closed]:fill-mode-forwards\",\n      \"data-[state=closed]:pointer-events-none\",\n      \"data-[state=open]:duration-(--animation-duration)\",\n      \"data-[state=closed]:duration-(--animation-duration)\",\n      className,\n    )}\n    aria-busy={ariaBusy}\n  >\n    {children}\n    <GradientFade />\n  </CollapsibleContent>\n);\n\nReasoningContent.displayName = \"ReasoningContent\";\n\n/**\n * Text content wrapper that animates the reasoning text visibility.\n * Animation: Slides in from top + fades in when opening, reverses when closing.\n * Reacts to parent ReasoningContent's data-state via Radix group selectors.\n */\nconst ReasoningText: FC<\n  PropsWithChildren<{\n    className?: string;\n  }>\n> = ({ className, children }) => (\n  <div\n    className={cn(\n      \"aui-reasoning-text relative z-0 space-y-4 pt-4 pl-6 leading-relaxed\",\n      \"transform-gpu transition-[transform,opacity]\",\n      \"group-data-[state=open]/collapsible-content:animate-in\",\n      \"group-data-[state=closed]/collapsible-content:animate-out\",\n      \"group-data-[state=open]/collapsible-content:fade-in-0\",\n      \"group-data-[state=closed]/collapsible-content:fade-out-0\",\n      \"group-data-[state=open]/collapsible-content:slide-in-from-top-4\",\n      \"group-data-[state=closed]/collapsible-content:slide-out-to-top-4\",\n      \"group-data-[state=open]/collapsible-content:duration-(--animation-duration)\",\n      \"group-data-[state=closed]/collapsible-content:duration-(--animation-duration)\",\n      \"[&_p]:-mb-2\",\n      className,\n    )}\n  >\n    {children}\n  </div>\n);\n\nReasoningText.displayName = \"ReasoningText\";\n\n/**\n * Renders a single reasoning part's text with markdown support.\n * Consecutive reasoning parts are automatically grouped by ReasoningGroup.\n *\n * Pass Reasoning to MessagePrimitive.Parts in thread.tsx\n *\n * @example:\n * ```tsx\n * <MessagePrimitive.Parts\n *   components={{\n *     Reasoning: Reasoning,\n *     ReasoningGroup: ReasoningGroup,\n *   }}\n * />\n * ```\n */\nconst ReasoningImpl: ReasoningMessagePartComponent = () => <MarkdownText />;\n\n/**\n * Collapsible wrapper that groups consecutive reasoning parts together.\n *  Includes scroll lock to prevent page jumps during collapse animation.\n *\n *  Pass ReasoningGroup to MessagePrimitive.Parts in thread.tsx\n *\n * @example:\n * ```tsx\n * <MessagePrimitive.Parts\n *   components={{\n *     Reasoning: Reasoning,\n *     ReasoningGroup: ReasoningGroup,\n *   }}\n * />\n * ```\n */\nconst ReasoningGroupImpl: ReasoningGroupComponent = ({\n  children,\n  startIndex,\n  endIndex,\n}) => {\n  /**\n   * Detects if reasoning is currently streaming within this group's range.\n   */\n  const isReasoningStreaming = useAuiState(({ message }) => {\n    if (message.status?.type !== \"running\") return false;\n    const lastIndex = message.parts.length - 1;\n    if (lastIndex < 0) return false;\n    const lastType = message.parts[lastIndex]?.type;\n    if (lastType !== \"reasoning\") return false;\n    return lastIndex >= startIndex && lastIndex <= endIndex;\n  });\n\n  return (\n    <ReasoningRoot>\n      <ReasoningTrigger active={isReasoningStreaming} />\n\n      <ReasoningContent aria-busy={isReasoningStreaming}>\n        <ReasoningText>{children}</ReasoningText>\n      </ReasoningContent>\n    </ReasoningRoot>\n  );\n};\n\nexport const Reasoning = memo(ReasoningImpl);\nReasoning.displayName = \"Reasoning\";\n\nexport const ReasoningGroup = memo(ReasoningGroupImpl);\nReasoningGroup.displayName = \"ReasoningGroup\";\n"
  },
  {
    "path": "apps/www/components/assistant-ui/shiki-highlighter.tsx",
    "content": "\"use client\";\n\nimport { FC } from \"react\";\nimport ShikiHighlighter, { type ShikiHighlighterProps } from \"react-shiki\";\nimport type { SyntaxHighlighterProps as AUIProps } from \"@assistant-ui/react-markdown\";\nimport { cn } from \"@/lib/ui/cn\";\n\n/**\n * Props for the SyntaxHighlighter component\n */\nexport type HighlighterProps = Omit<\n  ShikiHighlighterProps,\n  \"children\" | \"theme\"\n> & {\n  theme?: ShikiHighlighterProps[\"theme\"];\n} & Pick<AUIProps, \"node\" | \"components\" | \"language\" | \"code\">;\n\n/**\n * SyntaxHighlighter component, using react-shiki\n * Use it by passing to `defaultComponents` in `markdown-text.tsx`\n *\n * @example\n * const defaultComponents = memoizeMarkdownComponents({\n *   SyntaxHighlighter,\n *   h1: //...\n *   //...other elements...\n * });\n */\nexport const SyntaxHighlighter: FC<HighlighterProps> = ({\n  code,\n  language,\n  theme = { dark: \"kanagawa-wave\", light: \"kanagawa-lotus\" },\n  className,\n  addDefaultStyles = false, // assistant-ui requires custom base styles\n  showLanguage = false, // assistant-ui/react-markdown handles language labels\n  node: _node,\n  components: _components,\n  ...props\n}) => {\n  return (\n    <ShikiHighlighter\n      {...props}\n      language={language}\n      theme={theme}\n      addDefaultStyles={addDefaultStyles}\n      showLanguage={showLanguage}\n      defaultColor=\"light-dark()\"\n      className={cn(\n        \"aui-shiki-base [&_pre]:overflow-x-auto [&_pre]:rounded-b-lg [&_pre]:!bg-muted/75 [&_pre]:p-4\",\n        className,\n      )}\n    >\n      {code.trim()}\n    </ShikiHighlighter>\n  );\n};\n\nSyntaxHighlighter.displayName = \"SyntaxHighlighter\";\n"
  },
  {
    "path": "apps/www/components/assistant-ui/thread-list.tsx",
    "content": "import type { FC } from \"react\";\nimport {\n  ThreadListItemPrimitive,\n  ThreadListPrimitive,\n  useAuiState,\n} from \"@assistant-ui/react\";\nimport { ArchiveIcon, PlusIcon } from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { TooltipIconButton } from \"@/components/assistant-ui/tooltip-icon-button\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\n\nexport const ThreadList: FC = () => {\n  return (\n    <ThreadListPrimitive.Root className=\"aui-root aui-thread-list-root flex flex-col items-stretch gap-1.5\">\n      <ThreadListNew />\n      <ThreadListItems />\n    </ThreadListPrimitive.Root>\n  );\n};\n\nconst ThreadListNew: FC = () => {\n  return (\n    <ThreadListPrimitive.New asChild>\n      <Button\n        className=\"aui-thread-list-new flex items-center justify-start gap-1 rounded-lg px-2.5 py-2 text-start hover:bg-muted data-active:bg-muted\"\n        variant=\"ghost\"\n      >\n        <PlusIcon />\n        New Thread\n      </Button>\n    </ThreadListPrimitive.New>\n  );\n};\n\nconst ThreadListItems: FC = () => {\n  const isLoading = useAuiState(({ threads }) => threads.isLoading);\n\n  if (isLoading) {\n    return <ThreadListSkeleton />;\n  }\n\n  return <ThreadListPrimitive.Items components={{ ThreadListItem }} />;\n};\n\nconst ThreadListSkeleton: FC = () => {\n  return (\n    <>\n      {Array.from({ length: 5 }, (_, i) => (\n        <div\n          key={i}\n          role=\"status\"\n          aria-label=\"Loading threads\"\n          aria-live=\"polite\"\n          className=\"aui-thread-list-skeleton-wrapper flex items-center gap-2 rounded-md px-3 py-2\"\n        >\n          <Skeleton className=\"aui-thread-list-skeleton h-[22px] flex-grow\" />\n        </div>\n      ))}\n    </>\n  );\n};\n\nconst ThreadListItem: FC = () => {\n  return (\n    <ThreadListItemPrimitive.Root className=\"aui-thread-list-item flex items-center gap-2 rounded-lg transition-all hover:bg-muted focus-visible:bg-muted focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none data-active:bg-muted\">\n      <ThreadListItemPrimitive.Trigger className=\"aui-thread-list-item-trigger flex-grow px-3 py-2 text-start\">\n        <ThreadListItemTitle />\n      </ThreadListItemPrimitive.Trigger>\n      <ThreadListItemArchive />\n    </ThreadListItemPrimitive.Root>\n  );\n};\n\nconst ThreadListItemTitle: FC = () => {\n  return (\n    <span className=\"aui-thread-list-item-title text-sm\">\n      <ThreadListItemPrimitive.Title fallback=\"New Chat\" />\n    </span>\n  );\n};\n\nconst ThreadListItemArchive: FC = () => {\n  return (\n    <ThreadListItemPrimitive.Archive asChild>\n      <TooltipIconButton\n        className=\"aui-thread-list-item-archive mr-3 ml-auto size-4 p-0 text-foreground hover:text-primary\"\n        variant=\"ghost\"\n        tooltip=\"Archive thread\"\n      >\n        <ArchiveIcon />\n      </TooltipIconButton>\n    </ThreadListItemPrimitive.Archive>\n  );\n};\n"
  },
  {
    "path": "apps/www/components/assistant-ui/thread.tsx",
    "content": "import {\n  ComposerAddAttachment,\n  ComposerAttachments,\n  UserMessageAttachments,\n} from \"@/components/assistant-ui/attachment\";\nimport { MarkdownText } from \"@/components/assistant-ui/markdown-text\";\nimport { ToolFallback } from \"@/components/assistant-ui/tool-fallback\";\nimport { TooltipIconButton } from \"@/components/assistant-ui/tooltip-icon-button\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/ui/cn\";\nimport {\n  ActionBarPrimitive,\n  AssistantIf,\n  BranchPickerPrimitive,\n  ComposerPrimitive,\n  ErrorPrimitive,\n  MessagePrimitive,\n  ThreadPrimitive,\n} from \"@assistant-ui/react\";\nimport {\n  ArrowDownIcon,\n  ArrowUpIcon,\n  CheckIcon,\n  ChevronLeftIcon,\n  ChevronRightIcon,\n  CopyIcon,\n  DownloadIcon,\n  PencilIcon,\n  RefreshCwIcon,\n  SquareIcon,\n} from \"lucide-react\";\nimport type { FC } from \"react\";\n\nexport const Thread: FC = () => {\n  return (\n    <ThreadPrimitive.Root\n      className=\"aui-root aui-thread-root @container flex h-full flex-col bg-background\"\n      style={{\n        [\"--thread-max-width\" as string]: \"44rem\",\n      }}\n    >\n      <ThreadPrimitive.Viewport\n        turnAnchor=\"top\"\n        className=\"aui-thread-viewport relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll scroll-smooth px-4 pt-4\"\n      >\n        <AssistantIf condition={({ thread }) => thread.isEmpty}>\n          <ThreadWelcome />\n        </AssistantIf>\n\n        <ThreadPrimitive.Messages\n          components={{\n            UserMessage,\n            EditComposer,\n            AssistantMessage,\n          }}\n        />\n\n        <ThreadPrimitive.ViewportFooter className=\"aui-thread-viewport-footer sticky bottom-0 mx-auto mt-auto flex w-full max-w-(--thread-max-width) flex-col gap-4 overflow-visible rounded-t-3xl bg-background pb-4 md:pb-6\">\n          <ThreadScrollToBottom />\n          <Composer />\n        </ThreadPrimitive.ViewportFooter>\n      </ThreadPrimitive.Viewport>\n    </ThreadPrimitive.Root>\n  );\n};\n\nconst ThreadScrollToBottom: FC = () => {\n  return (\n    <ThreadPrimitive.ScrollToBottom asChild>\n      <TooltipIconButton\n        tooltip=\"Scroll to bottom\"\n        variant=\"outline\"\n        className=\"aui-thread-scroll-to-bottom absolute -top-12 z-10 self-center rounded-full p-4 disabled:invisible dark:bg-background dark:hover:bg-accent\"\n      >\n        <ArrowDownIcon />\n      </TooltipIconButton>\n    </ThreadPrimitive.ScrollToBottom>\n  );\n};\n\nconst ThreadWelcome: FC = () => {\n  return (\n    <div className=\"aui-thread-welcome-root mx-auto my-auto flex w-full max-w-(--thread-max-width) grow flex-col\">\n      <div className=\"aui-thread-welcome-center flex w-full grow flex-col items-center justify-center\">\n        <div className=\"aui-thread-welcome-message flex size-full flex-col justify-center px-4\">\n          <h1 className=\"aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in font-semibold text-2xl duration-200\">\n            Hello there!\n          </h1>\n          <p className=\"aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in text-muted-foreground text-xl delay-75 duration-200\">\n            How can I help you today?\n          </p>\n        </div>\n      </div>\n      <ThreadSuggestions />\n    </div>\n  );\n};\n\nconst SUGGESTIONS = [\n  {\n    title: \"What's the weather\",\n    label: \"in San Francisco?\",\n    prompt: \"What's the weather in San Francisco?\",\n  },\n  {\n    title: \"Explain React hooks\",\n    label: \"like useState and useEffect\",\n    prompt: \"Explain React hooks like useState and useEffect\",\n  },\n] as const;\n\nconst ThreadSuggestions: FC = () => {\n  return (\n    <div className=\"aui-thread-welcome-suggestions grid w-full @md:grid-cols-2 gap-2 pb-4\">\n      {SUGGESTIONS.map((suggestion, index) => (\n        <div\n          key={suggestion.prompt}\n          className=\"aui-thread-welcome-suggestion-display fade-in slide-in-from-bottom-2 @md:nth-[n+3]:block nth-[n+3]:hidden animate-in fill-mode-both duration-200\"\n          style={{ animationDelay: `${100 + index * 50}ms` }}\n        >\n          <ThreadPrimitive.Suggestion prompt={suggestion.prompt} send asChild>\n            <Button\n              variant=\"ghost\"\n              className=\"aui-thread-welcome-suggestion h-auto w-full @md:flex-col flex-wrap items-start justify-start gap-1 rounded-2xl border px-4 py-3 text-left text-sm transition-colors hover:bg-muted\"\n              aria-label={suggestion.prompt}\n            >\n              <span className=\"aui-thread-welcome-suggestion-text-1 font-medium\">\n                {suggestion.title}\n              </span>\n              <span className=\"aui-thread-welcome-suggestion-text-2 text-muted-foreground\">\n                {suggestion.label}\n              </span>\n            </Button>\n          </ThreadPrimitive.Suggestion>\n        </div>\n      ))}\n    </div>\n  );\n};\n\nconst Composer: FC = () => {\n  return (\n    <ComposerPrimitive.Root className=\"aui-composer-root relative flex w-full flex-col\">\n      <ComposerPrimitive.AttachmentDropzone className=\"aui-composer-attachment-dropzone flex w-full flex-col rounded-2xl border border-input bg-background px-1 pt-2 outline-none transition-shadow has-[textarea:focus-visible]:border-ring has-[textarea:focus-visible]:ring-2 has-[textarea:focus-visible]:ring-ring/20 data-[dragging=true]:border-ring data-[dragging=true]:border-dashed data-[dragging=true]:bg-accent/50\">\n        <ComposerAttachments />\n        <ComposerPrimitive.Input\n          placeholder=\"Send a message...\"\n          className=\"aui-composer-input mb-1 max-h-32 min-h-14 w-full resize-none bg-transparent px-4 pt-2 pb-3 text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-0\"\n          rows={1}\n          autoFocus\n          aria-label=\"Message input\"\n        />\n        <ComposerAction />\n      </ComposerPrimitive.AttachmentDropzone>\n    </ComposerPrimitive.Root>\n  );\n};\n\nconst ComposerAction: FC = () => {\n  return (\n    <div className=\"aui-composer-action-wrapper relative mx-2 mb-2 flex items-center justify-between\">\n      <ComposerAddAttachment />\n\n      <AssistantIf condition={({ thread }) => !thread.isRunning}>\n        <ComposerPrimitive.Send asChild>\n          <TooltipIconButton\n            tooltip=\"Send message\"\n            side=\"bottom\"\n            type=\"submit\"\n            variant=\"default\"\n            size=\"icon\"\n            className=\"aui-composer-send size-8 rounded-full\"\n            aria-label=\"Send message\"\n          >\n            <ArrowUpIcon className=\"aui-composer-send-icon size-4\" />\n          </TooltipIconButton>\n        </ComposerPrimitive.Send>\n      </AssistantIf>\n\n      <AssistantIf condition={({ thread }) => thread.isRunning}>\n        <ComposerPrimitive.Cancel asChild>\n          <Button\n            type=\"button\"\n            variant=\"default\"\n            size=\"icon\"\n            className=\"aui-composer-cancel size-8 rounded-full\"\n            aria-label=\"Stop generating\"\n          >\n            <SquareIcon className=\"aui-composer-cancel-icon size-3 fill-current\" />\n          </Button>\n        </ComposerPrimitive.Cancel>\n      </AssistantIf>\n    </div>\n  );\n};\n\nconst MessageError: FC = () => {\n  return (\n    <MessagePrimitive.Error>\n      <ErrorPrimitive.Root className=\"aui-message-error-root mt-2 rounded-md border border-destructive bg-destructive/10 p-3 text-destructive text-sm dark:bg-destructive/5 dark:text-red-200\">\n        <ErrorPrimitive.Message className=\"aui-message-error-message line-clamp-2\" />\n      </ErrorPrimitive.Root>\n    </MessagePrimitive.Error>\n  );\n};\n\nconst AssistantMessage: FC = () => {\n  return (\n    <MessagePrimitive.Root\n      className=\"aui-assistant-message-root fade-in slide-in-from-bottom-1 relative mx-auto w-full max-w-(--thread-max-width) animate-in py-3 duration-150\"\n      data-role=\"assistant\"\n    >\n      <div className=\"aui-assistant-message-content wrap-break-word px-2 text-foreground leading-relaxed\">\n        <MessagePrimitive.Parts\n          components={{\n            Text: MarkdownText,\n            tools: { Fallback: ToolFallback },\n          }}\n        />\n        <MessageError />\n      </div>\n\n      <div className=\"aui-assistant-message-footer mt-1 ml-2 flex\">\n        <BranchPicker />\n        <AssistantActionBar />\n      </div>\n    </MessagePrimitive.Root>\n  );\n};\n\nconst AssistantActionBar: FC = () => {\n  return (\n    <ActionBarPrimitive.Root\n      hideWhenRunning\n      autohide=\"not-last\"\n      autohideFloat=\"single-branch\"\n      className=\"aui-assistant-action-bar-root col-start-3 row-start-2 -ml-1 flex gap-1 text-muted-foreground data-floating:absolute data-floating:rounded-md data-floating:border data-floating:bg-background data-floating:p-1 data-floating:shadow-sm\"\n    >\n      <ActionBarPrimitive.Copy asChild>\n        <TooltipIconButton tooltip=\"Copy\">\n          <AssistantIf condition={({ message }) => message.isCopied}>\n            <CheckIcon />\n          </AssistantIf>\n          <AssistantIf condition={({ message }) => !message.isCopied}>\n            <CopyIcon />\n          </AssistantIf>\n        </TooltipIconButton>\n      </ActionBarPrimitive.Copy>\n      <ActionBarPrimitive.ExportMarkdown asChild>\n        <TooltipIconButton tooltip=\"Export as Markdown\">\n          <DownloadIcon />\n        </TooltipIconButton>\n      </ActionBarPrimitive.ExportMarkdown>\n      <ActionBarPrimitive.Reload asChild>\n        <TooltipIconButton tooltip=\"Refresh\">\n          <RefreshCwIcon />\n        </TooltipIconButton>\n      </ActionBarPrimitive.Reload>\n    </ActionBarPrimitive.Root>\n  );\n};\n\nconst UserMessage: FC = () => {\n  return (\n    <MessagePrimitive.Root\n      className=\"aui-user-message-root fade-in slide-in-from-bottom-1 mx-auto grid w-full max-w-(--thread-max-width) animate-in auto-rows-auto grid-cols-[minmax(72px,1fr)_auto] content-start gap-y-2 px-2 py-3 duration-150 [&:where(>*)]:col-start-2\"\n      data-role=\"user\"\n    >\n      <UserMessageAttachments />\n\n      <div className=\"aui-user-message-content-wrapper relative col-start-2 min-w-0\">\n        <div className=\"aui-user-message-content wrap-break-word rounded-2xl bg-muted px-4 py-2.5 text-foreground\">\n          <MessagePrimitive.Parts />\n        </div>\n        <div className=\"aui-user-action-bar-wrapper absolute top-1/2 left-0 -translate-x-full -translate-y-1/2 pr-2\">\n          <UserActionBar />\n        </div>\n      </div>\n\n      <BranchPicker className=\"aui-user-branch-picker col-span-full col-start-1 row-start-3 -mr-1 justify-end\" />\n    </MessagePrimitive.Root>\n  );\n};\n\nconst UserActionBar: FC = () => {\n  return (\n    <ActionBarPrimitive.Root\n      hideWhenRunning\n      autohide=\"not-last\"\n      className=\"aui-user-action-bar-root flex flex-col items-end\"\n    >\n      <ActionBarPrimitive.Edit asChild>\n        <TooltipIconButton tooltip=\"Edit\" className=\"aui-user-action-edit p-4\">\n          <PencilIcon />\n        </TooltipIconButton>\n      </ActionBarPrimitive.Edit>\n    </ActionBarPrimitive.Root>\n  );\n};\n\nconst EditComposer: FC = () => {\n  return (\n    <MessagePrimitive.Root className=\"aui-edit-composer-wrapper mx-auto flex w-full max-w-(--thread-max-width) flex-col px-2 py-3\">\n      <ComposerPrimitive.Root className=\"aui-edit-composer-root ml-auto flex w-full max-w-[85%] flex-col rounded-2xl bg-muted\">\n        <ComposerPrimitive.Input\n          className=\"aui-edit-composer-input min-h-14 w-full resize-none bg-transparent p-4 text-foreground text-sm outline-none\"\n          autoFocus\n        />\n        <div className=\"aui-edit-composer-footer mx-3 mb-3 flex items-center gap-2 self-end\">\n          <ComposerPrimitive.Cancel asChild>\n            <Button variant=\"ghost\" size=\"sm\">\n              Cancel\n            </Button>\n          </ComposerPrimitive.Cancel>\n          <ComposerPrimitive.Send asChild>\n            <Button size=\"sm\">Update</Button>\n          </ComposerPrimitive.Send>\n        </div>\n      </ComposerPrimitive.Root>\n    </MessagePrimitive.Root>\n  );\n};\n\nconst BranchPicker: FC<BranchPickerPrimitive.Root.Props> = ({\n  className,\n  ...rest\n}) => {\n  return (\n    <BranchPickerPrimitive.Root\n      hideWhenSingleBranch\n      className={cn(\n        \"aui-branch-picker-root mr-2 -ml-2 inline-flex items-center text-muted-foreground text-xs\",\n        className,\n      )}\n      {...rest}\n    >\n      <BranchPickerPrimitive.Previous asChild>\n        <TooltipIconButton tooltip=\"Previous\">\n          <ChevronLeftIcon />\n        </TooltipIconButton>\n      </BranchPickerPrimitive.Previous>\n      <span className=\"aui-branch-picker-state font-medium\">\n        <BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />\n      </span>\n      <BranchPickerPrimitive.Next asChild>\n        <TooltipIconButton tooltip=\"Next\">\n          <ChevronRightIcon />\n        </TooltipIconButton>\n      </BranchPickerPrimitive.Next>\n    </BranchPickerPrimitive.Root>\n  );\n};\n"
  },
  {
    "path": "apps/www/components/assistant-ui/threadlist-sidebar.tsx",
    "content": "import * as React from \"react\";\nimport { Github, MessagesSquare } from \"lucide-react\";\nimport Link from \"next/link\";\nimport {\n  Sidebar,\n  SidebarContent,\n  SidebarFooter,\n  SidebarHeader,\n  SidebarMenu,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  SidebarRail,\n} from \"@/components/ui/sidebar\";\nimport { ThreadList } from \"@/components/assistant-ui/thread-list\";\n\nexport function ThreadListSidebar({\n  ...props\n}: React.ComponentProps<typeof Sidebar>) {\n  return (\n    <Sidebar {...props}>\n      <SidebarHeader className=\"aui-sidebar-header mb-2 border-b\">\n        <div className=\"aui-sidebar-header-content flex items-center justify-between\">\n          <SidebarMenu>\n            <SidebarMenuItem>\n              <SidebarMenuButton size=\"lg\" asChild>\n                <Link\n                  href=\"https://assistant-ui.com\"\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                >\n                  <div className=\"aui-sidebar-header-icon-wrapper flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground\">\n                    <MessagesSquare className=\"aui-sidebar-header-icon size-4\" />\n                  </div>\n                  <div className=\"aui-sidebar-header-heading mr-6 flex flex-col gap-0.5 leading-none\">\n                    <span className=\"aui-sidebar-header-title font-semibold\">\n                      assistant-ui\n                    </span>\n                  </div>\n                </Link>\n              </SidebarMenuButton>\n            </SidebarMenuItem>\n          </SidebarMenu>\n        </div>\n      </SidebarHeader>\n      <SidebarContent className=\"aui-sidebar-content px-2\">\n        <ThreadList />\n      </SidebarContent>\n      <SidebarRail />\n      <SidebarFooter className=\"aui-sidebar-footer border-t\">\n        <SidebarMenu>\n          <SidebarMenuItem>\n            <SidebarMenuButton size=\"lg\" asChild>\n              <Link\n                href=\"https://github.com/assistant-ui/assistant-ui\"\n                target=\"_blank\"\n              >\n                <div className=\"aui-sidebar-footer-icon-wrapper flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground\">\n                  <Github className=\"aui-sidebar-footer-icon size-4\" />\n                </div>\n                <div className=\"aui-sidebar-footer-heading flex flex-col gap-0.5 leading-none\">\n                  <span className=\"aui-sidebar-footer-title font-semibold\">\n                    GitHub\n                  </span>\n                  <span>View Source</span>\n                </div>\n              </Link>\n            </SidebarMenuButton>\n          </SidebarMenuItem>\n        </SidebarMenu>\n      </SidebarFooter>\n    </Sidebar>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/assistant-ui/tool-fallback.tsx",
    "content": "import type { ToolCallMessagePartComponent } from \"@assistant-ui/react\";\nimport {\n  CheckIcon,\n  ChevronDownIcon,\n  ChevronUpIcon,\n  XCircleIcon,\n} from \"lucide-react\";\nimport { useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/ui/cn\";\n\nexport const ToolFallback: ToolCallMessagePartComponent = ({\n  toolName,\n  argsText,\n  result,\n  status,\n}) => {\n  const [isCollapsed, setIsCollapsed] = useState(true);\n\n  const isCancelled =\n    status?.type === \"incomplete\" && status.reason === \"cancelled\";\n  const cancelledReason =\n    isCancelled && status.error\n      ? typeof status.error === \"string\"\n        ? status.error\n        : JSON.stringify(status.error)\n      : null;\n\n  return (\n    <div\n      className={cn(\n        \"aui-tool-fallback-root mb-4 flex w-full flex-col gap-3 rounded-lg border py-3\",\n        isCancelled && \"border-muted-foreground/30 bg-muted/30\",\n      )}\n    >\n      <div className=\"aui-tool-fallback-header flex items-center gap-2 px-4\">\n        {isCancelled ? (\n          <XCircleIcon className=\"aui-tool-fallback-icon size-4 text-muted-foreground\" />\n        ) : (\n          <CheckIcon className=\"aui-tool-fallback-icon size-4\" />\n        )}\n        <p\n          className={cn(\n            \"aui-tool-fallback-title grow\",\n            isCancelled && \"text-muted-foreground line-through\",\n          )}\n        >\n          {isCancelled ? \"Cancelled tool: \" : \"Used tool: \"}\n          <b>{toolName}</b>\n        </p>\n        <Button onClick={() => setIsCollapsed(!isCollapsed)}>\n          {isCollapsed ? <ChevronUpIcon /> : <ChevronDownIcon />}\n        </Button>\n      </div>\n      {!isCollapsed && (\n        <div className=\"aui-tool-fallback-content flex flex-col gap-2 border-t pt-2\">\n          {cancelledReason && (\n            <div className=\"aui-tool-fallback-cancelled-root px-4\">\n              <p className=\"aui-tool-fallback-cancelled-header font-semibold text-muted-foreground\">\n                Cancelled reason:\n              </p>\n              <p className=\"aui-tool-fallback-cancelled-reason text-muted-foreground\">\n                {cancelledReason}\n              </p>\n            </div>\n          )}\n          <div\n            className={cn(\n              \"aui-tool-fallback-args-root px-4\",\n              isCancelled && \"opacity-60\",\n            )}\n          >\n            <pre className=\"aui-tool-fallback-args-value whitespace-pre-wrap\">\n              {argsText}\n            </pre>\n          </div>\n          {!isCancelled && result !== undefined && (\n            <div className=\"aui-tool-fallback-result-root border-t border-dashed px-4 pt-2\">\n              <p className=\"aui-tool-fallback-result-header font-semibold\">\n                Result:\n              </p>\n              <pre className=\"aui-tool-fallback-result-content whitespace-pre-wrap\">\n                {typeof result === \"string\"\n                  ? result\n                  : JSON.stringify(result, null, 2)}\n              </pre>\n            </div>\n          )}\n        </div>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/www/components/assistant-ui/tooltip-icon-button.tsx",
    "content": "\"use client\";\n\nimport { ComponentPropsWithRef, forwardRef } from \"react\";\nimport { Slottable } from \"@radix-ui/react-slot\";\n\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/ui/cn\";\n\nexport type TooltipIconButtonProps = ComponentPropsWithRef<typeof Button> & {\n  tooltip: string;\n  side?: \"top\" | \"bottom\" | \"left\" | \"right\";\n};\n\nexport const TooltipIconButton = forwardRef<\n  HTMLButtonElement,\n  TooltipIconButtonProps\n>(({ children, tooltip, side = \"bottom\", className, ...rest }, ref) => {\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>\n        <Button\n          variant=\"ghost\"\n          size=\"icon\"\n          {...rest}\n          className={cn(\"aui-button-icon size-6 p-1\", className)}\n          ref={ref}\n        >\n          <Slottable>{children}</Slottable>\n          <span className=\"aui-sr-only sr-only\">{tooltip}</span>\n        </Button>\n      </TooltipTrigger>\n      <TooltipContent side={side}>{tooltip}</TooltipContent>\n    </Tooltip>\n  );\n});\n\nTooltipIconButton.displayName = \"TooltipIconButton\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/approval-card/README.md",
    "content": "# Approval Card\n\nImplementation for the \"approval-card\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/approval-card/index.tsx\n- serializable schema + parse helpers: components/tool-ui/approval-card/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/approval-card/content.mdx\n- Preset payload: lib/presets/approval-card.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/approval-card/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n */\n\nexport { cn } from \"@/lib/utils\";\nexport { Separator } from \"@/components/ui/separator\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/approval-card/approval-card.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn, Separator } from \"./_adapter\";\nimport type { ApprovalCardProps, ApprovalDecision } from \"./schema\";\nimport { ActionButtons } from \"../shared/action-buttons\";\nimport { type Action } from \"../shared/schema\";\n\nimport { icons, Check, X } from \"lucide-react\";\n\ntype LucideIcon = React.ComponentType<{ className?: string }>;\n\nfunction getLucideIcon(name: string): LucideIcon | null {\n  const pascalName = name\n    .split(\"-\")\n    .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n    .join(\"\");\n\n  const Icon = icons[pascalName as keyof typeof icons];\n  return Icon ?? null;\n}\n\ninterface ApprovalCardReceiptProps {\n  id: string;\n  title: string;\n  choice: ApprovalDecision;\n  actionLabel?: string;\n  className?: string;\n}\n\nfunction ApprovalCardReceipt({\n  id,\n  title,\n  choice,\n  actionLabel,\n  className,\n}: ApprovalCardReceiptProps) {\n  const isApproved = choice === \"approved\";\n  const displayLabel = actionLabel ?? (isApproved ? \"Approved\" : \"Denied\");\n\n  return (\n    <div\n      className={cn(\n        \"flex w-full min-w-64 max-w-md flex-col\",\n        \"text-foreground\",\n        \"motion-safe:animate-in motion-safe:fade-in motion-safe:blur-in-sm motion-safe:zoom-in-95 motion-safe:duration-300 motion-safe:ease-[cubic-bezier(0.16,1,0.3,1)] motion-safe:fill-mode-both\",\n        className,\n      )}\n      data-slot=\"approval-card\"\n      data-tool-ui-id={id}\n      data-receipt=\"true\"\n      role=\"status\"\n      aria-label={displayLabel}\n    >\n      <div\n        className={cn(\n          \"bg-card/60 flex w-full items-center gap-3 rounded-2xl border px-4 py-3 shadow-xs\",\n        )}\n      >\n        <span\n          className={cn(\n            \"flex size-8 shrink-0 items-center justify-center rounded-full bg-muted\",\n            isApproved ? \"text-primary\" : \"text-muted-foreground\",\n          )}\n        >\n          {isApproved ? <Check className=\"size-4\" /> : <X className=\"size-4\" />}\n        </span>\n        <div className=\"flex flex-col\">\n          <span className=\"text-sm font-medium\">{displayLabel}</span>\n          <span className=\"text-muted-foreground text-sm\">{title}</span>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport function ApprovalCard({\n  id,\n  title,\n  description,\n  icon,\n  metadata,\n  variant,\n  confirmLabel,\n  cancelLabel,\n  className,\n  choice,\n  onConfirm,\n  onCancel,\n}: ApprovalCardProps) {\n  const resolvedVariant = variant ?? \"default\";\n  const resolvedConfirmLabel = confirmLabel ?? \"Approve\";\n  const resolvedCancelLabel = cancelLabel ?? \"Deny\";\n  const Icon = icon ? getLucideIcon(icon) : null;\n\n  const handleAction = React.useCallback(\n    async (actionId: string) => {\n      if (actionId === \"confirm\") {\n        await onConfirm?.();\n      } else if (actionId === \"cancel\") {\n        await onCancel?.();\n      }\n    },\n    [onConfirm, onCancel],\n  );\n\n  const handleKeyDown = React.useCallback(\n    (event: React.KeyboardEvent) => {\n      if (event.key === \"Escape\") {\n        event.preventDefault();\n        onCancel?.();\n      }\n    },\n    [onCancel],\n  );\n\n  const isDestructive = resolvedVariant === \"destructive\";\n\n  const actions: Action[] = [\n    {\n      id: \"cancel\",\n      label: resolvedCancelLabel,\n      variant: \"ghost\",\n    },\n    {\n      id: \"confirm\",\n      label: resolvedConfirmLabel,\n      variant: isDestructive ? \"destructive\" : \"default\",\n    },\n  ];\n\n  const viewKey = choice ? `receipt-${choice}` : \"interactive\";\n\n  return (\n    <div key={viewKey} className=\"contents\">\n      {choice ? (\n        <ApprovalCardReceipt\n          id={id}\n          title={title}\n          choice={choice}\n          className={className}\n        />\n      ) : (\n        <article\n          className={cn(\n            \"flex w-full min-w-64 max-w-md flex-col gap-3\",\n            \"text-foreground\",\n            className,\n          )}\n          data-slot=\"approval-card\"\n          data-tool-ui-id={id}\n          role=\"dialog\"\n          aria-labelledby={`${id}-title`}\n          aria-describedby={description ? `${id}-description` : undefined}\n          onKeyDown={handleKeyDown}\n        >\n          <div className=\"bg-card flex w-full flex-col gap-4 rounded-2xl border p-5 shadow-xs\">\n            <div className=\"flex items-start gap-3\">\n              {Icon && (\n                <span\n                  className={cn(\n                    \"flex size-10 shrink-0 items-center justify-center rounded-xl\",\n                    isDestructive\n                      ? \"bg-destructive/10 text-destructive\"\n                      : \"bg-primary/10 text-primary\",\n                  )}\n                >\n                  <Icon className=\"size-5\" />\n                </span>\n              )}\n              <div className=\"flex flex-1 flex-col gap-1\">\n                <h2\n                  id={`${id}-title`}\n                  className=\"text-base font-semibold leading-tight\"\n                >\n                  {title}\n                </h2>\n                {description && (\n                  <p\n                    id={`${id}-description`}\n                    className=\"text-muted-foreground text-sm\"\n                  >\n                    {description}\n                  </p>\n                )}\n              </div>\n            </div>\n\n            {metadata && metadata.length > 0 && (\n              <>\n                <Separator />\n                <dl className=\"flex flex-col gap-2 text-sm\">\n                  {metadata.map((item, index) => (\n                    <div key={index} className=\"flex justify-between gap-4\">\n                      <dt className=\"text-muted-foreground shrink-0\">\n                        {item.key}\n                      </dt>\n                      <dd className=\"min-w-0 truncate\">{item.value}</dd>\n                    </div>\n                  ))}\n                </dl>\n              </>\n            )}\n          </div>\n          <div className=\"@container/actions\">\n            <ActionButtons actions={actions} onAction={handleAction} />\n          </div>\n        </article>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/approval-card/index.tsx",
    "content": "export { ApprovalCard } from \"./approval-card\";\nexport {\n  type SerializableApprovalCard,\n  type ApprovalCardProps,\n  type ApprovalDecision,\n  type MetadataItem,\n} from \"./schema\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/approval-card/schema.ts",
    "content": "import { z } from \"zod\";\nimport { ToolUIIdSchema, ToolUIRoleSchema } from \"../shared/schema\";\nimport { defineToolUiContract } from \"../shared/contract\";\n\nexport const MetadataItemSchema = z.object({\n  key: z.string().min(1),\n  value: z.string(),\n});\n\nexport type MetadataItem = z.infer<typeof MetadataItemSchema>;\n\nexport const ApprovalDecisionSchema = z.enum([\"approved\", \"denied\"]);\n\nexport type ApprovalDecision = z.infer<typeof ApprovalDecisionSchema>;\n\nexport const SerializableApprovalCardSchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n\n  title: z.string().min(1),\n  description: z.string().optional(),\n  icon: z.string().optional(),\n  metadata: z.array(MetadataItemSchema).optional(),\n\n  variant: z.enum([\"default\", \"destructive\"]).optional(),\n\n  confirmLabel: z.string().optional(),\n  cancelLabel: z.string().optional(),\n\n  choice: ApprovalDecisionSchema.optional(),\n});\n\nexport type SerializableApprovalCard = z.infer<\n  typeof SerializableApprovalCardSchema\n>;\n\nconst SerializableApprovalCardSchemaContract = defineToolUiContract(\n  \"ApprovalCard\",\n  SerializableApprovalCardSchema,\n);\n\nexport const parseSerializableApprovalCard: (\n  input: unknown,\n) => SerializableApprovalCard = SerializableApprovalCardSchemaContract.parse;\n\nexport const safeParseSerializableApprovalCard: (\n  input: unknown,\n) => SerializableApprovalCard | null =\n  SerializableApprovalCardSchemaContract.safeParse;\nexport interface ApprovalCardProps extends SerializableApprovalCard {\n  className?: string;\n  onConfirm?: () => void | Promise<void>;\n  onCancel?: () => void | Promise<void>;\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/audio/README.md",
    "content": "# Audio\n\nImplementation for the \"audio\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/audio/index.ts\n- serializable schema + parse helpers: components/tool-ui/audio/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/audio/content.mdx\n- Preset payload: lib/presets/audio.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/audio/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n */\n\"use client\";\n\nexport { cn } from \"@/lib/utils\";\nexport { Button } from \"@/components/ui/button\";\nexport { Slider } from \"@/components/ui/slider\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/audio/audio.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Pause, Play } from \"lucide-react\";\nimport { cn, Button, Slider } from \"./_adapter\";\n\nimport { AudioProvider, useAudio } from \"./context\";\nimport type { SerializableAudio, AudioVariant } from \"./schema\";\n\nconst FALLBACK_LOCALE = \"en-US\";\n\nfunction formatTime(seconds: number): string {\n  if (!Number.isFinite(seconds)) return \"0:00\";\n  const mins = Math.floor(seconds / 60);\n  const secs = Math.floor(seconds % 60);\n  return `${mins}:${secs.toString().padStart(2, \"0\")}`;\n}\n\nexport interface AudioProps extends SerializableAudio {\n  variant?: AudioVariant;\n  className?: string;\n  onMediaEvent?: (type: \"play\" | \"pause\" | \"mute\" | \"unmute\") => void;\n}\n\nexport function Audio(props: AudioProps) {\n  return (\n    <AudioProvider>\n      <AudioInner {...props} />\n    </AudioProvider>\n  );\n}\n\ninterface PlayerControls {\n  isPlaying: boolean;\n  currentTime: number;\n  duration: number;\n  onPlayPause: () => void;\n  onSeek: (value: number[]) => void;\n  onSeekStart: () => void;\n  onSeekEnd: () => void;\n}\n\ninterface FullPlayerProps {\n  artwork?: string;\n  title?: string;\n  description?: string;\n  controls: PlayerControls;\n}\n\nfunction FullPlayer({\n  artwork,\n  title,\n  description,\n  controls,\n}: FullPlayerProps) {\n  return (\n    <div className=\"flex w-full flex-col\">\n      {artwork && (\n        <div className=\"bg-muted relative aspect-[4/3] w-full overflow-hidden\">\n          <img\n            src={artwork}\n            alt=\"\"\n            aria-hidden=\"true\"\n            loading=\"lazy\"\n            decoding=\"async\"\n            className=\"absolute inset-0 h-full w-full object-cover\"\n          />\n        </div>\n      )}\n      <div className=\"flex flex-col gap-5 p-4\">\n        {(title || description) && (\n          <div className=\"space-y-0.5\">\n            {title && (\n              <div className=\"text-foreground line-clamp-2 font-semibold leading-snug\">\n                {title}\n              </div>\n            )}\n            {description && (\n              <div className=\"text-muted-foreground line-clamp-2 text-sm leading-snug\">\n                {description}\n              </div>\n            )}\n          </div>\n        )}\n        <div className=\"flex items-start gap-3\">\n          <div className=\"flex flex-1 flex-col gap-2\">\n            <Slider\n              value={[controls.currentTime]}\n              max={controls.duration || 100}\n              step={0.1}\n              onValueChange={controls.onSeek}\n              onPointerDown={controls.onSeekStart}\n              onPointerUp={controls.onSeekEnd}\n              className=\"cursor-pointer [&_[data-slot=range]]:bg-foreground [&_[data-slot=thumb]]:size-3 [&_[data-slot=thumb]]:border-2 [&_[data-slot=thumb]]:border-background [&_[data-slot=thumb]]:bg-foreground\"\n              aria-label=\"Audio progress\"\n            />\n            <div className=\"text-muted-foreground flex items-center justify-between text-xs tabular-nums\">\n              <span>{formatTime(controls.currentTime)}</span>\n              <span>{formatTime(controls.duration)}</span>\n            </div>\n          </div>\n          <Button\n            variant=\"default\"\n            size=\"icon\"\n            onClick={controls.onPlayPause}\n            className=\"-mt-4 size-10 shrink-0 rounded-full\"\n            aria-label={controls.isPlaying ? \"Pause\" : \"Play\"}\n          >\n            {controls.isPlaying ? (\n              <Pause className=\"size-4\" fill=\"currentColor\" />\n            ) : (\n              <Play className=\"size-4 ml-0.5\" fill=\"currentColor\" />\n            )}\n          </Button>\n        </div>\n      </div>\n    </div>\n  );\n}\n\ninterface CompactPlayerProps {\n  artwork?: string;\n  title?: string;\n  description?: string;\n  controls: PlayerControls;\n}\n\nfunction CompactPlayer({\n  artwork,\n  title,\n  description,\n  controls,\n}: CompactPlayerProps) {\n  const progress =\n    controls.duration > 0\n      ? (controls.currentTime / controls.duration) * 100\n      : 0;\n\n  return (\n    <div className=\"relative flex w-full items-center gap-3 overflow-hidden p-3\">\n      {artwork && (\n        <>\n          <img\n            src={artwork}\n            alt=\"\"\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute -left-1/4 top-1/2 h-[200%] w-auto -translate-y-1/2 object-cover opacity-40 blur-2xl saturate-150\"\n          />\n          <div className=\"from-card/60 to-card/90 pointer-events-none absolute inset-0 bg-gradient-to-r\" />\n        </>\n      )}\n      {artwork && (\n        <div className=\"ring-background/20 relative size-12 shrink-0 overflow-hidden rounded-lg shadow-lg ring-1\">\n          <img\n            src={artwork}\n            alt=\"\"\n            aria-hidden=\"true\"\n            loading=\"lazy\"\n            decoding=\"async\"\n            className=\"absolute inset-0 h-full w-full object-cover\"\n          />\n        </div>\n      )}\n      <div className=\"relative flex min-w-0 flex-1 flex-col justify-center\">\n        {title && (\n          <div className=\"text-foreground truncate text-sm font-semibold leading-tight\">\n            {title}\n          </div>\n        )}\n        {description && (\n          <div className=\"text-muted-foreground mt-0.5 truncate text-xs leading-tight\">\n            {description}\n          </div>\n        )}\n        {controls.duration > 0 && (\n          <div className=\"mt-1 flex items-center gap-2\">\n            <div className=\"bg-foreground/20 relative h-1 flex-1 overflow-hidden rounded-full\">\n              <div\n                className=\"bg-foreground absolute inset-y-0 left-0 rounded-full transition-all duration-150\"\n                style={{ width: `${progress}%` }}\n              />\n            </div>\n            <span className=\"text-muted-foreground text-xs tabular-nums\">\n              {formatTime(controls.currentTime)}\n            </span>\n          </div>\n        )}\n      </div>\n      <Button\n        variant=\"default\"\n        size=\"icon\"\n        onClick={controls.onPlayPause}\n        className=\"relative size-10 shrink-0 rounded-full shadow-md\"\n        aria-label={controls.isPlaying ? \"Pause\" : \"Play\"}\n      >\n        {controls.isPlaying ? (\n          <Pause className=\"size-4\" fill=\"currentColor\" />\n        ) : (\n          <Play className=\"size-4 ml-0.5\" fill=\"currentColor\" />\n        )}\n      </Button>\n    </div>\n  );\n}\n\nfunction AudioInner(props: AudioProps) {\n  const { variant = \"full\", className, onMediaEvent, ...serializable } = props;\n\n  const {\n    id,\n    src,\n    title,\n    description,\n    artwork,\n    locale: providedLocale,\n  } = serializable;\n\n  const locale = providedLocale ?? FALLBACK_LOCALE;\n\n  const { state, setState, setAudioElement } = useAudio();\n  const audioRef = React.useRef<HTMLAudioElement | null>(null);\n  const [currentTime, setCurrentTime] = React.useState(0);\n  const [duration, setDuration] = React.useState(0);\n  const [isSeeking, setIsSeeking] = React.useState(false);\n\n  React.useEffect(() => {\n    setAudioElement(audioRef.current);\n    return () => setAudioElement(null);\n  }, [setAudioElement]);\n\n  React.useEffect(() => {\n    const audio = audioRef.current;\n    if (!audio) return;\n    if (state.playing && audio.paused) {\n      void audio.play().catch(() => undefined);\n    } else if (!state.playing && !audio.paused) {\n      audio.pause();\n    }\n  }, [state.playing]);\n\n  const handlePlayPause = () => {\n    const audio = audioRef.current;\n    if (!audio) return;\n    if (audio.paused) {\n      void audio.play().catch(() => undefined);\n    } else {\n      audio.pause();\n    }\n  };\n\n  const handleSeek = (value: number[]) => {\n    const audio = audioRef.current;\n    if (!audio) return;\n    const newTime = value[0];\n    audio.currentTime = newTime;\n    setCurrentTime(newTime);\n  };\n\n  const handleSeekStart = () => {\n    setIsSeeking(true);\n  };\n\n  const handleSeekEnd = () => {\n    setIsSeeking(false);\n  };\n\n  const controls: PlayerControls = {\n    isPlaying: state.playing,\n    currentTime,\n    duration,\n    onPlayPause: handlePlayPause,\n    onSeek: handleSeek,\n    onSeekStart: handleSeekStart,\n    onSeekEnd: handleSeekEnd,\n  };\n\n  const isCompact = variant === \"compact\";\n\n  return (\n    <article\n      className={cn(\n        \"@container/actions relative w-full\",\n        isCompact ? \"min-w-72 max-w-md\" : \"min-w-52 max-w-sm\",\n        className,\n      )}\n      lang={locale}\n      data-tool-ui-id={id}\n      data-slot=\"audio\"\n    >\n      <div\n        className={cn(\n          \"group @container relative isolate flex w-full min-w-0 flex-col overflow-hidden\",\n          \"border-border bg-card border text-sm shadow-xs\",\n          \"rounded-xl\",\n        )}\n      >\n        {isCompact ? (\n          <CompactPlayer\n            artwork={artwork}\n            title={title}\n            description={description}\n            controls={controls}\n          />\n        ) : (\n          <FullPlayer\n            artwork={artwork}\n            title={title}\n            description={description}\n            controls={controls}\n          />\n        )}\n\n        <audio\n          ref={audioRef}\n          src={src}\n          preload=\"metadata\"\n          className=\"hidden\"\n          onPlay={() => {\n            setState({ playing: true });\n            onMediaEvent?.(\"play\");\n          }}\n          onPause={() => {\n            setState({ playing: false });\n            onMediaEvent?.(\"pause\");\n          }}\n          onTimeUpdate={(event) => {\n            if (!isSeeking) {\n              setCurrentTime(event.currentTarget.currentTime);\n            }\n          }}\n          onLoadedMetadata={(event) => {\n            setDuration(event.currentTarget.duration);\n          }}\n          onDurationChange={(event) => {\n            setDuration(event.currentTarget.duration);\n          }}\n        />\n      </div>\n    </article>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/audio/context.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\n\nexport interface AudioPlaybackState {\n  playing: boolean;\n  muted: boolean;\n}\n\nexport interface AudioContextValue {\n  state: AudioPlaybackState;\n  setState: (patch: Partial<AudioPlaybackState>) => void;\n  audioElement: HTMLAudioElement | null;\n  setAudioElement: (node: HTMLAudioElement | null) => void;\n}\n\nconst AudioContext = React.createContext<AudioContextValue | null>(null);\n\nexport function useAudio() {\n  const ctx = React.use(AudioContext);\n  if (!ctx) {\n    throw new Error(\"useAudio must be used within an <AudioProvider />\");\n  }\n  return ctx;\n}\n\nexport interface AudioProviderProps {\n  children: React.ReactNode;\n  defaultState?: Partial<AudioPlaybackState>;\n}\n\nexport function AudioProvider({ children, defaultState }: AudioProviderProps) {\n  const [state, setStateInternal] = React.useState<AudioPlaybackState>({\n    playing: defaultState?.playing ?? false,\n    muted: defaultState?.muted ?? false,\n  });\n\n  const [audioElement, setAudioElement] =\n    React.useState<HTMLAudioElement | null>(null);\n\n  const setState = React.useCallback((patch: Partial<AudioPlaybackState>) => {\n    setStateInternal((prev) => ({ ...prev, ...patch }));\n  }, []);\n\n  const value = React.useMemo(\n    () => ({ state, setState, audioElement, setAudioElement }),\n    [state, setState, audioElement],\n  );\n\n  return (\n    <AudioContext.Provider value={value}>{children}</AudioContext.Provider>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/audio/index.ts",
    "content": "export { Audio } from \"./audio\";\nexport type { AudioProps } from \"./audio\";\nexport { AudioProvider, useAudio } from \"./context\";\nexport type { AudioPlaybackState, AudioContextValue } from \"./context\";\nexport type { SerializableAudio, Source, AudioVariant } from \"./schema\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/audio/schema.ts",
    "content": "import { z } from \"zod\";\nimport { defineToolUiContract } from \"../shared/contract\";\nimport {\n  ToolUIIdSchema,\n  ToolUIReceiptSchema,\n  ToolUIRoleSchema,\n} from \"../shared/schema\";\n\nexport const SourceSchema = z.object({\n  label: z.string(),\n  iconUrl: z.url().optional(),\n  url: z.url().optional(),\n});\n\nexport type Source = z.infer<typeof SourceSchema>;\n\nexport const SerializableAudioSchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  receipt: ToolUIReceiptSchema.optional(),\n  assetId: z.string(),\n  src: z.url(),\n  title: z.string().optional(),\n  description: z.string().optional(),\n  artwork: z.url().optional(),\n  durationMs: z.number().int().positive().optional(),\n  fileSizeBytes: z.number().int().positive().optional(),\n  createdAt: z.string().datetime().optional(),\n  locale: z.string().optional(),\n  source: SourceSchema.optional(),\n});\n\nexport type SerializableAudio = z.infer<typeof SerializableAudioSchema>;\n\nconst SerializableAudioSchemaContract = defineToolUiContract(\n  \"Audio\",\n  SerializableAudioSchema,\n);\n\nexport const parseSerializableAudio: (input: unknown) => SerializableAudio =\n  SerializableAudioSchemaContract.parse;\n\nexport const safeParseSerializableAudio: (\n  input: unknown,\n) => SerializableAudio | null = SerializableAudioSchemaContract.safeParse;\nexport type AudioVariant = \"full\" | \"compact\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/chart/README.md",
    "content": "# Chart\n\nImplementation for the \"chart\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/chart/index.tsx\n- serializable schema + parse helpers: components/tool-ui/chart/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/chart/content.mdx\n- Preset payload: lib/presets/chart.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/chart/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn    → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Chart → shadcn/ui Chart (recharts wrapper)\n *   Card  → shadcn/ui Card\n */\n\nexport { cn } from \"@/lib/utils\";\nexport {\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent,\n  ChartLegend,\n  ChartLegendContent,\n  type ChartConfig,\n} from \"@/components/ui/chart\";\nexport {\n  Card,\n  CardHeader,\n  CardTitle,\n  CardDescription,\n  CardContent,\n} from \"@/components/ui/card\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/chart/chart.tsx",
    "content": "\"use client\";\n\nimport { useMemo, useCallback, memo } from \"react\";\nimport {\n  BarChart,\n  LineChart,\n  Bar,\n  Line,\n  XAxis,\n  YAxis,\n  CartesianGrid,\n} from \"recharts\";\n\nimport {\n  cn,\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent,\n  ChartLegend,\n  ChartLegendContent,\n  Card,\n  CardHeader,\n  CardTitle,\n  CardDescription,\n  CardContent,\n  type ChartConfig,\n} from \"./_adapter\";\nimport type { ChartProps } from \"./schema\";\n\nconst DEFAULT_COLORS = [\n  \"var(--chart-1)\",\n  \"var(--chart-2)\",\n  \"var(--chart-3)\",\n  \"var(--chart-4)\",\n  \"var(--chart-5)\",\n];\n\nexport const Chart = memo(function Chart({\n  id,\n  type,\n  title,\n  description,\n  data,\n  xKey,\n  series,\n  colors,\n  showLegend = false,\n  showGrid = true,\n  className,\n  onDataPointClick,\n}: ChartProps) {\n  const palette = colors?.length ? colors : DEFAULT_COLORS;\n\n  const seriesColors = useMemo(\n    () =>\n      series.map(\n        (seriesItem, index) =>\n          seriesItem.color ?? palette[index % palette.length],\n      ),\n    [series, palette],\n  );\n\n  const chartConfig: ChartConfig = useMemo(\n    () =>\n      Object.fromEntries(\n        series.map((seriesItem, index) => [\n          seriesItem.key,\n          {\n            label: seriesItem.label,\n            color: seriesColors[index],\n          },\n        ]),\n      ),\n    [series, seriesColors],\n  );\n\n  const handleDataPointClick = useCallback(\n    (\n      seriesKey: string,\n      seriesLabel: string,\n      payload: Record<string, unknown>,\n      index: number,\n    ) => {\n      onDataPointClick?.({\n        seriesKey,\n        seriesLabel,\n        xValue: payload[xKey],\n        yValue: payload[seriesKey],\n        index,\n        payload,\n      });\n    },\n    [onDataPointClick, xKey],\n  );\n\n  const ChartComponent = type === \"bar\" ? BarChart : LineChart;\n\n  const chartContent = (\n    <ChartContainer\n      config={chartConfig}\n      className=\"min-h-[200px] w-full\"\n      data-tool-ui-id={id}\n    >\n      <ChartComponent data={data} accessibilityLayer>\n        {showGrid && <CartesianGrid vertical={false} />}\n        <XAxis\n          dataKey={xKey}\n          tickLine={false}\n          tickMargin={10}\n          axisLine={false}\n        />\n        <YAxis tickLine={false} axisLine={false} tickMargin={10} />\n        <ChartTooltip content={<ChartTooltipContent />} />\n        {showLegend && <ChartLegend content={<ChartLegendContent />} />}\n\n        {type === \"bar\" &&\n          series.map((s, i) => (\n            <Bar\n              key={s.key}\n              dataKey={s.key}\n              fill={seriesColors[i]}\n              radius={4}\n              onClick={(data) =>\n                handleDataPointClick(s.key, s.label, data.payload, data.index)\n              }\n              cursor={onDataPointClick ? \"pointer\" : undefined}\n            />\n          ))}\n\n        {type === \"line\" &&\n          series.map((s, i) => (\n            <Line\n              key={s.key}\n              dataKey={s.key}\n              type=\"monotone\"\n              stroke={seriesColors[i]}\n              strokeWidth={2}\n              dot={{ r: 4, cursor: onDataPointClick ? \"pointer\" : undefined }}\n              activeDot={{\n                r: 6,\n                cursor: onDataPointClick ? \"pointer\" : undefined,\n                // Recharts types are incorrect - onClick receives (event, dotData) at runtime\n                onClick: ((\n                  _: unknown,\n                  dotData: { payload: Record<string, unknown>; index: number },\n                ) => {\n                  handleDataPointClick(\n                    s.key,\n                    s.label,\n                    dotData.payload,\n                    dotData.index,\n                  );\n                }) as unknown as React.MouseEventHandler,\n              }}\n            />\n          ))}\n      </ChartComponent>\n    </ChartContainer>\n  );\n\n  return (\n    <Card\n      className={cn(\"w-full min-w-80\", className)}\n      data-tool-ui-id={id}\n      data-slot=\"chart\"\n    >\n      {(title || description) && (\n        <CardHeader>\n          {title && <CardTitle className=\"text-pretty\">{title}</CardTitle>}\n          {description && (\n            <CardDescription className=\"text-pretty\">\n              {description}\n            </CardDescription>\n          )}\n        </CardHeader>\n      )}\n      <CardContent>{chartContent}</CardContent>\n    </Card>\n  );\n});\n"
  },
  {
    "path": "apps/www/components/tool-ui/chart/index.tsx",
    "content": "export { Chart } from \"./chart\";\nexport {\n  type ChartProps,\n  type ChartSeries,\n  type ChartDataPoint,\n  type ChartClientProps,\n  type SerializableChart,\n} from \"./schema\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/chart/schema.ts",
    "content": "import { z } from \"zod\";\nimport { defineToolUiContract } from \"../shared/contract\";\nimport {\n  ToolUIIdSchema,\n  ToolUIReceiptSchema,\n  ToolUIRoleSchema,\n} from \"../shared/schema\";\n\nexport const ChartSeriesSchema = z.object({\n  key: z.string().min(1),\n  label: z.string().min(1),\n  color: z.string().optional(),\n});\n\nexport type ChartSeries = z.infer<typeof ChartSeriesSchema>;\n\nexport const ChartPropsSchema = z\n  .object({\n    id: ToolUIIdSchema,\n    role: ToolUIRoleSchema.optional(),\n    receipt: ToolUIReceiptSchema.optional(),\n    type: z.enum([\"bar\", \"line\"]),\n    title: z.string().optional(),\n    description: z.string().optional(),\n    data: z.array(z.record(z.string(), z.unknown())).min(1),\n    xKey: z.string().min(1),\n    series: z.array(ChartSeriesSchema).min(1),\n    /** Color palette applied to series in order. Individual series.color takes precedence. */\n    colors: z.array(z.string().min(1)).min(1).optional(),\n    showLegend: z.boolean().optional(),\n    showGrid: z.boolean().optional(),\n  })\n  .superRefine((value, ctx) => {\n    const seenSeriesKeys = new Set<string>();\n    value.series.forEach((series, index) => {\n      if (seenSeriesKeys.has(series.key)) {\n        ctx.addIssue({\n          code: \"custom\",\n          path: [\"series\", index, \"key\"],\n          message: `Duplicate series key \"${series.key}\".`,\n        });\n        return;\n      }\n      seenSeriesKeys.add(series.key);\n    });\n\n    value.data.forEach((row, rowIndex) => {\n      if (!(value.xKey in row)) {\n        ctx.addIssue({\n          code: \"custom\",\n          path: [\"data\", rowIndex, value.xKey],\n          message: `Missing xKey \"${value.xKey}\" in data row.`,\n        });\n      } else {\n        const xVal = row[value.xKey];\n        const isValidX = typeof xVal === \"string\" || typeof xVal === \"number\";\n        if (!isValidX) {\n          ctx.addIssue({\n            code: \"custom\",\n            path: [\"data\", rowIndex, value.xKey],\n            message: `Expected \"${value.xKey}\" to be a string or number.`,\n          });\n        }\n      }\n\n      value.series.forEach((series) => {\n        if (!(series.key in row)) {\n          ctx.addIssue({\n            code: \"custom\",\n            path: [\"data\", rowIndex, series.key],\n            message: `Missing series key \"${series.key}\" in data row.`,\n          });\n          return;\n        }\n\n        const yVal = row[series.key];\n        if (yVal === null) {\n          return;\n        }\n        if (typeof yVal !== \"number\" || !Number.isFinite(yVal)) {\n          ctx.addIssue({\n            code: \"custom\",\n            path: [\"data\", rowIndex, series.key],\n            message: `Expected \"${series.key}\" to be a finite number (or null).`,\n          });\n        }\n      });\n    });\n  });\n\nexport type ChartDataPoint = {\n  seriesKey: string;\n  seriesLabel: string;\n  xValue: unknown;\n  yValue: unknown;\n  index: number;\n  payload: Record<string, unknown>;\n};\n\nexport type ChartClientProps = {\n  className?: string;\n  onDataPointClick?: (point: ChartDataPoint) => void;\n};\n\nexport type ChartProps = z.infer<typeof ChartPropsSchema> & ChartClientProps;\n\nexport const SerializableChartSchema = ChartPropsSchema;\n\nexport type SerializableChart = z.infer<typeof SerializableChartSchema>;\n\nconst SerializableChartSchemaContract = defineToolUiContract(\n  \"Chart\",\n  SerializableChartSchema,\n);\n\nexport const parseSerializableChart: (input: unknown) => SerializableChart =\n  SerializableChartSchemaContract.parse;\n\nexport const safeParseSerializableChart: (\n  input: unknown,\n) => SerializableChart | null = SerializableChartSchemaContract.safeParse;\n"
  },
  {
    "path": "apps/www/components/tool-ui/citation/README.md",
    "content": "# Citation\n\nImplementation for the \"citation\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/citation/index.ts\n- serializable schema + parse helpers: components/tool-ui/citation/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/citation/content.mdx\n- Preset payload: lib/presets/citation.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/citation/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn      → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Tooltip → shadcn/ui Tooltip (only needed for variant=\"inline\")\n *   Popover → shadcn/ui Popover (only needed for CitationList)\n */\n\"use client\";\n\nexport { cn } from \"@/lib/utils\";\nexport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/citation/citation-list.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport {\n  FileText,\n  Globe,\n  Code2,\n  Newspaper,\n  Database,\n  File,\n  ExternalLink,\n} from \"lucide-react\";\nimport { cn, Popover, PopoverContent, PopoverTrigger } from \"./_adapter\";\nimport { Citation } from \"./citation\";\nimport type {\n  SerializableCitation,\n  CitationType,\n  CitationVariant,\n} from \"./schema\";\nimport {\n  openSafeNavigationHref,\n  resolveSafeNavigationHref,\n} from \"../shared/media\";\n\nconst TYPE_ICONS: Record<CitationType, LucideIcon> = {\n  webpage: Globe,\n  document: FileText,\n  article: Newspaper,\n  api: Database,\n  code: Code2,\n  other: File,\n};\n\nfunction useHoverPopover(delay = 100) {\n  const [open, setOpen] = React.useState(false);\n  const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n  const containerRef = React.useRef<HTMLDivElement>(null);\n\n  const handleMouseEnter = React.useCallback(() => {\n    if (timeoutRef.current) clearTimeout(timeoutRef.current);\n    timeoutRef.current = setTimeout(() => setOpen(true), delay);\n  }, [delay]);\n\n  const handleMouseLeave = React.useCallback(() => {\n    if (timeoutRef.current) clearTimeout(timeoutRef.current);\n    timeoutRef.current = setTimeout(() => setOpen(false), delay);\n  }, [delay]);\n\n  const handleFocus = React.useCallback(() => {\n    if (timeoutRef.current) clearTimeout(timeoutRef.current);\n    setOpen(true);\n  }, []);\n\n  const handleBlur = React.useCallback(\n    (e: React.FocusEvent) => {\n      const relatedTarget = e.relatedTarget as HTMLElement | null;\n      if (containerRef.current?.contains(relatedTarget)) {\n        return;\n      }\n      if (relatedTarget?.closest(\"[data-radix-popper-content-wrapper]\")) {\n        return;\n      }\n      if (timeoutRef.current) clearTimeout(timeoutRef.current);\n      timeoutRef.current = setTimeout(() => setOpen(false), delay);\n    },\n    [delay],\n  );\n\n  React.useEffect(() => {\n    return () => {\n      if (timeoutRef.current) clearTimeout(timeoutRef.current);\n    };\n  }, []);\n\n  return {\n    open,\n    setOpen,\n    containerRef,\n    handleMouseEnter,\n    handleMouseLeave,\n    handleFocus,\n    handleBlur,\n  };\n}\n\nexport interface CitationListProps {\n  id: string;\n  citations: SerializableCitation[];\n  variant?: CitationVariant;\n  maxVisible?: number;\n  className?: string;\n  onNavigate?: (href: string, citation: SerializableCitation) => void;\n}\n\nexport function CitationList(props: CitationListProps) {\n  const {\n    id,\n    citations,\n    variant = \"default\",\n    maxVisible,\n    className,\n    onNavigate,\n  } = props;\n\n  const shouldTruncate =\n    maxVisible !== undefined && citations.length > maxVisible;\n  const visibleCitations = shouldTruncate\n    ? citations.slice(0, maxVisible)\n    : citations;\n  const overflowCitations = shouldTruncate ? citations.slice(maxVisible) : [];\n  const overflowCount = overflowCitations.length;\n\n  const wrapperClass =\n    variant === \"inline\"\n      ? \"flex flex-wrap items-center gap-1.5\"\n      : \"flex flex-col gap-2\";\n\n  // Stacked variant: overlapping favicons with popover\n  if (variant === \"stacked\") {\n    return (\n      <StackedCitations\n        id={id}\n        citations={citations}\n        className={className}\n        onNavigate={onNavigate}\n      />\n    );\n  }\n\n  if (variant === \"default\") {\n    return (\n      <div\n        className={cn(\"isolate flex flex-col gap-4\", className)}\n        data-tool-ui-id={id}\n        data-slot=\"citation-list\"\n      >\n        {visibleCitations.map((citation) => (\n          <Citation\n            key={citation.id}\n            {...citation}\n            variant=\"default\"\n            onNavigate={onNavigate}\n          />\n        ))}\n        {shouldTruncate && (\n          <OverflowIndicator\n            citations={overflowCitations}\n            count={overflowCount}\n            variant=\"default\"\n            onNavigate={onNavigate}\n          />\n        )}\n      </div>\n    );\n  }\n\n  return (\n    <div\n      className={cn(\"isolate\", wrapperClass, className)}\n      data-tool-ui-id={id}\n      data-slot=\"citation-list\"\n    >\n      {visibleCitations.map((citation) => (\n        <Citation\n          key={citation.id}\n          {...citation}\n          variant={variant}\n          onNavigate={onNavigate}\n        />\n      ))}\n      {shouldTruncate && (\n        <OverflowIndicator\n          citations={overflowCitations}\n          count={overflowCount}\n          variant={variant}\n          onNavigate={onNavigate}\n        />\n      )}\n    </div>\n  );\n}\n\ninterface OverflowIndicatorProps {\n  citations: SerializableCitation[];\n  count: number;\n  variant: CitationVariant;\n  onNavigate?: (href: string, citation: SerializableCitation) => void;\n}\n\nfunction OverflowIndicator({\n  citations,\n  count,\n  variant,\n  onNavigate,\n}: OverflowIndicatorProps) {\n  const { open, handleMouseEnter, handleMouseLeave } = useHoverPopover();\n\n  const handleClick = (citation: SerializableCitation) => {\n    const href = resolveSafeNavigationHref(citation.href);\n    if (!href) return;\n    if (onNavigate) {\n      onNavigate(href, citation);\n    } else {\n      openSafeNavigationHref(href);\n    }\n  };\n\n  const popoverContent = (\n    <div className=\"flex max-h-72 flex-col overflow-y-auto\">\n      {citations.map((citation) => (\n        <OverflowItem\n          key={citation.id}\n          citation={citation}\n          onClick={() => handleClick(citation)}\n        />\n      ))}\n    </div>\n  );\n\n  if (variant === \"inline\") {\n    return (\n      <Popover open={open}>\n        <PopoverTrigger asChild>\n          <button\n            type=\"button\"\n            onMouseEnter={handleMouseEnter}\n            onMouseLeave={handleMouseLeave}\n            className={cn(\n              \"inline-flex items-center gap-1 rounded-md px-2 py-1\",\n              \"bg-muted/60 text-sm tabular-nums\",\n              \"transition-colors duration-150\",\n              \"hover:bg-muted\",\n              \"focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none\",\n            )}\n          >\n            <span className=\"text-muted-foreground\">+{count} more</span>\n          </button>\n        </PopoverTrigger>\n        <PopoverContent\n          side=\"top\"\n          align=\"start\"\n          className=\"w-80 p-1\"\n          onMouseEnter={handleMouseEnter}\n          onMouseLeave={handleMouseLeave}\n          onOpenAutoFocus={(e) => e.preventDefault()}\n        >\n          {popoverContent}\n        </PopoverContent>\n      </Popover>\n    );\n  }\n\n  // Default variant\n  return (\n    <Popover open={open}>\n      <PopoverTrigger asChild>\n        <button\n          type=\"button\"\n          onMouseEnter={handleMouseEnter}\n          onMouseLeave={handleMouseLeave}\n          className={cn(\n            \"flex items-center justify-center rounded-xl px-4 py-3\",\n            \"border-border bg-card border border-dashed\",\n            \"transition-colors duration-150\",\n            \"hover:border-foreground/25 hover:bg-muted/50\",\n            \"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none\",\n          )}\n        >\n          <span className=\"text-muted-foreground text-sm tabular-nums\">\n            +{count} more sources\n          </span>\n        </button>\n      </PopoverTrigger>\n      <PopoverContent\n        side=\"bottom\"\n        align=\"start\"\n        className=\"w-80 p-1\"\n        onMouseEnter={handleMouseEnter}\n        onMouseLeave={handleMouseLeave}\n        onOpenAutoFocus={(e) => e.preventDefault()}\n      >\n        {popoverContent}\n      </PopoverContent>\n    </Popover>\n  );\n}\n\ninterface OverflowItemProps {\n  citation: SerializableCitation;\n  onClick: () => void;\n}\n\nfunction OverflowItem({ citation, onClick }: OverflowItemProps) {\n  const TypeIcon = TYPE_ICONS[citation.type ?? \"webpage\"] ?? Globe;\n\n  return (\n    <button\n      type=\"button\"\n      onClick={onClick}\n      className=\"group hover:bg-muted focus-visible:bg-muted flex w-full cursor-pointer items-center gap-2.5 rounded-md px-2 py-2 text-left transition-colors focus-visible:outline-none\"\n    >\n      {citation.favicon ? (\n        <img\n          src={citation.favicon}\n          alt=\"\"\n          aria-hidden=\"true\"\n          width={16}\n          height={16}\n          className=\"bg-muted size-4 shrink-0 rounded object-cover\"\n        />\n      ) : (\n        <TypeIcon\n          className=\"text-muted-foreground size-4 shrink-0\"\n          aria-hidden=\"true\"\n        />\n      )}\n      <div className=\"min-w-0 flex-1\">\n        <p className=\"group-hover:decoration-foreground/30 truncate text-sm font-medium group-hover:underline group-hover:underline-offset-2\">\n          {citation.title}\n        </p>\n        <p className=\"text-muted-foreground truncate text-xs\">\n          {citation.domain}\n        </p>\n      </div>\n      <ExternalLink className=\"text-muted-foreground mt-0.5 size-3.5 shrink-0 self-start opacity-0 transition-opacity group-hover:opacity-100\" />\n    </button>\n  );\n}\n\ninterface StackedCitationsProps {\n  id: string;\n  citations: SerializableCitation[];\n  className?: string;\n  onNavigate?: (href: string, citation: SerializableCitation) => void;\n}\n\nfunction StackedCitations({\n  id,\n  citations,\n  className,\n  onNavigate,\n}: StackedCitationsProps) {\n  const {\n    open,\n    setOpen,\n    containerRef,\n    handleMouseEnter,\n    handleMouseLeave,\n    handleBlur,\n  } = useHoverPopover();\n  const maxIcons = 4;\n  const visibleCitations = citations.slice(0, maxIcons);\n  const remainingCount = Math.max(0, citations.length - maxIcons);\n\n  const handleClick = (citation: SerializableCitation) => {\n    const href = resolveSafeNavigationHref(citation.href);\n    if (!href) return;\n    if (onNavigate) {\n      onNavigate(href, citation);\n    } else {\n      openSafeNavigationHref(href);\n    }\n  };\n\n  return (\n    <div ref={containerRef} onBlur={handleBlur} className=\"inline-flex\">\n      <Popover open={open}>\n        <PopoverTrigger asChild>\n          <button\n            type=\"button\"\n            data-tool-ui-id={id}\n            data-slot=\"citation-list\"\n            onMouseEnter={handleMouseEnter}\n            onMouseLeave={handleMouseLeave}\n            onKeyDown={(e) => {\n              if (e.key === \"Enter\" || e.key === \" \") {\n                e.preventDefault();\n                setOpen(true);\n              }\n            }}\n            className={cn(\n              \"isolate inline-flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2\",\n              \"bg-muted/40 outline-none\",\n              \"transition-colors duration-150\",\n              \"hover:bg-muted/70\",\n              \"focus-visible:ring-ring focus-visible:ring-2\",\n              className,\n            )}\n          >\n            <div className=\"flex items-center\">\n              {visibleCitations.map((citation, index) => {\n                const TypeIcon =\n                  TYPE_ICONS[citation.type ?? \"webpage\"] ?? Globe;\n                return (\n                  <div\n                    key={citation.id}\n                    className={cn(\n                      \"border-border bg-background dark:border-foreground/20 relative flex size-6 items-center justify-center rounded-full border shadow-xs\",\n                      index > 0 && \"-ml-2\",\n                    )}\n                    style={{ zIndex: maxIcons - index }}\n                  >\n                    {citation.favicon ? (\n                      <img\n                        src={citation.favicon}\n                        alt=\"\"\n                        aria-hidden=\"true\"\n                        width={18}\n                        height={18}\n                        className=\"size-4.5 rounded-full object-cover\"\n                      />\n                    ) : (\n                      <TypeIcon\n                        className=\"text-muted-foreground size-3\"\n                        aria-hidden=\"true\"\n                      />\n                    )}\n                  </div>\n                );\n              })}\n              {remainingCount > 0 && (\n                <div\n                  className=\"border-border bg-background dark:border-foreground/20 relative -ml-2 flex size-6 items-center justify-center rounded-full border shadow-xs\"\n                  style={{ zIndex: 0 }}\n                >\n                  <span className=\"text-muted-foreground text-[10px] font-medium tracking-tight\">\n                    •••\n                  </span>\n                </div>\n              )}\n            </div>\n            <span className=\"text-muted-foreground text-sm tabular-nums\">\n              {citations.length} source{citations.length !== 1 && \"s\"}\n            </span>\n          </button>\n        </PopoverTrigger>\n        <PopoverContent\n          side=\"bottom\"\n          align=\"start\"\n          className=\"w-80 p-1\"\n          onMouseEnter={handleMouseEnter}\n          onMouseLeave={handleMouseLeave}\n          onBlur={handleBlur}\n          onEscapeKeyDown={() => setOpen(false)}\n        >\n          <div className=\"flex max-h-72 flex-col overflow-y-auto\">\n            {citations.map((citation) => (\n              <OverflowItem\n                key={citation.id}\n                citation={citation}\n                onClick={() => handleClick(citation)}\n              />\n            ))}\n          </div>\n        </PopoverContent>\n      </Popover>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/citation/citation.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport {\n  FileText,\n  Globe,\n  Code2,\n  Newspaper,\n  Database,\n  File,\n  ExternalLink,\n} from \"lucide-react\";\nimport { cn, Popover, PopoverContent, PopoverTrigger } from \"./_adapter\";\n\nimport { openSafeNavigationHref, sanitizeHref } from \"../shared/media\";\nimport type {\n  SerializableCitation,\n  CitationType,\n  CitationVariant,\n} from \"./schema\";\n\nconst FALLBACK_LOCALE = \"en-US\";\n\nconst TYPE_ICONS: Record<CitationType, LucideIcon> = {\n  webpage: Globe,\n  document: FileText,\n  article: Newspaper,\n  api: Database,\n  code: Code2,\n  other: File,\n};\n\nfunction extractDomain(url: string): string | undefined {\n  try {\n    const urlObj = new URL(url);\n    return urlObj.hostname.replace(/^www\\./, \"\");\n  } catch {\n    return undefined;\n  }\n}\n\nfunction formatDate(isoString: string, locale: string): string {\n  try {\n    const date = new Date(isoString);\n    return date.toLocaleDateString(locale, {\n      year: \"numeric\",\n      month: \"short\",\n    });\n  } catch {\n    return isoString;\n  }\n}\n\nfunction useHoverPopover(delay = 100) {\n  const [open, setOpen] = React.useState(false);\n  const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  const handleMouseEnter = React.useCallback(() => {\n    if (timeoutRef.current) clearTimeout(timeoutRef.current);\n    timeoutRef.current = setTimeout(() => setOpen(true), delay);\n  }, [delay]);\n\n  const handleMouseLeave = React.useCallback(() => {\n    if (timeoutRef.current) clearTimeout(timeoutRef.current);\n    timeoutRef.current = setTimeout(() => setOpen(false), delay);\n  }, [delay]);\n\n  React.useEffect(() => {\n    return () => {\n      if (timeoutRef.current) clearTimeout(timeoutRef.current);\n    };\n  }, []);\n\n  return { open, setOpen, handleMouseEnter, handleMouseLeave };\n}\n\nexport interface CitationProps extends SerializableCitation {\n  variant?: CitationVariant;\n  className?: string;\n  onNavigate?: (href: string, citation: SerializableCitation) => void;\n}\n\nexport function Citation(props: CitationProps) {\n  const { variant = \"default\", className, onNavigate, ...serializable } = props;\n\n  const {\n    id,\n    href: rawHref,\n    title,\n    snippet,\n    domain: providedDomain,\n    favicon,\n    author,\n    publishedAt,\n    type = \"webpage\",\n    locale: providedLocale,\n  } = serializable;\n\n  const locale = providedLocale ?? FALLBACK_LOCALE;\n  const sanitizedHref = sanitizeHref(rawHref);\n  const domain = providedDomain ?? extractDomain(rawHref);\n\n  const citationData: SerializableCitation = {\n    ...serializable,\n    href: sanitizedHref ?? rawHref,\n    domain,\n    locale,\n  };\n\n  const TypeIcon = TYPE_ICONS[type] ?? Globe;\n\n  const handleClick = () => {\n    if (!sanitizedHref) return;\n    if (onNavigate) {\n      onNavigate(sanitizedHref, citationData);\n    } else {\n      openSafeNavigationHref(sanitizedHref);\n    }\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent) => {\n    if (sanitizedHref && (e.key === \"Enter\" || e.key === \" \")) {\n      e.preventDefault();\n      handleClick();\n    }\n  };\n\n  const iconElement = favicon ? (\n    <img\n      src={favicon}\n      alt=\"\"\n      aria-hidden=\"true\"\n      width={14}\n      height={14}\n      className=\"bg-muted size-3.5 shrink-0 rounded object-cover\"\n    />\n  ) : (\n    <TypeIcon className=\"size-3.5 shrink-0 opacity-60\" aria-hidden=\"true\" />\n  );\n\n  const { open, handleMouseEnter, handleMouseLeave } = useHoverPopover();\n\n  // Inline variant: compact chip with hover popover\n  if (variant === \"inline\") {\n    return (\n      <Popover open={open}>\n        <PopoverTrigger asChild>\n          <button\n            type=\"button\"\n            aria-label={title}\n            data-tool-ui-id={id}\n            data-slot=\"citation\"\n            onClick={handleClick}\n            onMouseEnter={handleMouseEnter}\n            onMouseLeave={handleMouseLeave}\n            className={cn(\n              \"inline-flex cursor-pointer items-center gap-1.5 rounded-md px-2 py-1\",\n              \"bg-muted/60 text-sm outline-none\",\n              \"transition-colors duration-150\",\n              \"hover:bg-muted\",\n              \"focus-visible:ring-ring focus-visible:ring-2\",\n              className,\n            )}\n          >\n            {iconElement}\n            <span className=\"text-muted-foreground\">{domain}</span>\n          </button>\n        </PopoverTrigger>\n        <PopoverContent\n          side=\"top\"\n          align=\"start\"\n          className=\"w-72 cursor-pointer p-0\"\n          onMouseEnter={handleMouseEnter}\n          onMouseLeave={handleMouseLeave}\n          onOpenAutoFocus={(e) => e.preventDefault()}\n          onCloseAutoFocus={(e) => e.preventDefault()}\n          onClick={handleClick}\n        >\n          <div className=\"hover:bg-muted/50 flex flex-col gap-2 p-3 transition-colors\">\n            <div className=\"flex items-start gap-2\">\n              {iconElement}\n              <span className=\"text-muted-foreground text-xs\">{domain}</span>\n            </div>\n            <p className=\"text-sm leading-snug font-medium\">{title}</p>\n            {snippet && (\n              <p className=\"text-muted-foreground line-clamp-2 text-xs leading-relaxed\">\n                {snippet}\n              </p>\n            )}\n          </div>\n        </PopoverContent>\n      </Popover>\n    );\n  }\n\n  // Default variant: full card\n  return (\n    <article\n      className={cn(\"relative w-full max-w-md min-w-72\", className)}\n      lang={locale}\n      data-tool-ui-id={id}\n      data-slot=\"citation\"\n    >\n      <div\n        className={cn(\n          \"group @container relative isolate flex w-full min-w-0 flex-col overflow-hidden rounded-xl\",\n          \"border-border bg-card border text-sm shadow-xs\",\n          \"transition-colors duration-150\",\n          sanitizedHref && [\n            \"cursor-pointer\",\n            \"hover:border-foreground/25\",\n            \"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none\",\n          ],\n        )}\n        onClick={sanitizedHref ? handleClick : undefined}\n        role={sanitizedHref ? \"link\" : undefined}\n        tabIndex={sanitizedHref ? 0 : undefined}\n        onKeyDown={handleKeyDown}\n      >\n        <div className=\"flex flex-col gap-2 p-4\">\n          <div className=\"text-muted-foreground flex min-w-0 items-center justify-between gap-1.5 text-xs\">\n            <div className=\"flex min-w-0 items-center gap-1.5\">\n              {iconElement}\n              <span className=\"truncate font-medium\">{domain}</span>\n              {(author || publishedAt) && (\n                <span className=\"opacity-70\">\n                  <span className=\"opacity-60\"> — </span>\n                  {author}\n                  {author && publishedAt && \", \"}\n                  {publishedAt && (\n                    <time dateTime={publishedAt} className=\"tabular-nums\">\n                      {formatDate(publishedAt, locale)}\n                    </time>\n                  )}\n                </span>\n              )}\n            </div>\n            {sanitizedHref && (\n              <ExternalLink className=\"size-3.5 shrink-0 opacity-0 transition-opacity group-hover:opacity-100\" />\n            )}\n          </div>\n\n          <h3 className=\"text-foreground text-[15px] leading-snug font-medium text-pretty\">\n            <span className=\"group-hover:decoration-foreground/30 line-clamp-2 group-hover:underline group-hover:underline-offset-2\">\n              {title}\n            </span>\n          </h3>\n\n          {snippet && (\n            <p className=\"text-muted-foreground text-[13px] leading-relaxed text-pretty\">\n              <span className=\"line-clamp-3\">{snippet}</span>\n            </p>\n          )}\n        </div>\n      </div>\n    </article>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/citation/index.ts",
    "content": "export { Citation } from \"./citation\";\nexport type { CitationProps } from \"./citation\";\nexport { CitationList } from \"./citation-list\";\nexport type { CitationListProps } from \"./citation-list\";\nexport type {\n  SerializableCitation,\n  CitationType,\n  CitationVariant,\n} from \"./schema\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/citation/schema.ts",
    "content": "import { z } from \"zod\";\nimport { defineToolUiContract } from \"../shared/contract\";\nimport {\n  ToolUIIdSchema,\n  ToolUIReceiptSchema,\n  ToolUIRoleSchema,\n} from \"../shared/schema\";\n\nexport const CitationTypeSchema = z.enum([\n  \"webpage\",\n  \"document\",\n  \"article\",\n  \"api\",\n  \"code\",\n  \"other\",\n]);\n\nexport type CitationType = z.infer<typeof CitationTypeSchema>;\n\nexport const CitationVariantSchema = z.enum([\"default\", \"inline\", \"stacked\"]);\n\nexport type CitationVariant = z.infer<typeof CitationVariantSchema>;\n\nexport const SerializableCitationSchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  receipt: ToolUIReceiptSchema.optional(),\n  href: z.string().url(),\n  title: z.string(),\n  snippet: z.string().optional(),\n  domain: z.string().optional(),\n  favicon: z.string().url().optional(),\n  author: z.string().optional(),\n  publishedAt: z.string().datetime().optional(),\n  type: CitationTypeSchema.optional(),\n  locale: z.string().optional(),\n});\n\nexport type SerializableCitation = z.infer<typeof SerializableCitationSchema>;\n\nconst SerializableCitationSchemaContract = defineToolUiContract(\n  \"Citation\",\n  SerializableCitationSchema,\n);\n\nexport const parseSerializableCitation: (\n  input: unknown,\n) => SerializableCitation = SerializableCitationSchemaContract.parse;\n\nexport const safeParseSerializableCitation: (\n  input: unknown,\n) => SerializableCitation | null = SerializableCitationSchemaContract.safeParse;\n"
  },
  {
    "path": "apps/www/components/tool-ui/code-block/README.md",
    "content": "# Code Block\n\nImplementation for the \"code-block\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/code-block/index.tsx\n- serializable schema + parse helpers: components/tool-ui/code-block/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/code-block/content.mdx\n- Preset payload: lib/presets/code-block.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/code-block/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn          → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Button      → shadcn/ui Button\n *   Collapsible → shadcn/ui Collapsible\n */\n\nexport { cn } from \"@/lib/utils\";\nexport { Button } from \"@/components/ui/button\";\nexport { Collapsible, CollapsibleTrigger } from \"@/components/ui/collapsible\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/code-block/code-block.tsx",
    "content": "\"use client\";\n\nimport {\n  useState,\n  useCallback,\n  useEffect,\n  createContext,\n  use,\n  type ReactNode,\n} from \"react\";\nimport {\n  createHighlighter,\n  createJavaScriptRegexEngine,\n  type Highlighter,\n} from \"shiki\";\nimport { Copy, Check, ChevronDown, ChevronUp } from \"lucide-react\";\nimport pierreDarkTheme from \"../shared/pierre-dark-theme.js\";\nimport pierreLightTheme from \"../shared/pierre-light-theme.js\";\nimport type { CodeBlockLineNumbersMode, CodeBlockProps } from \"./schema\";\nimport { useCopyToClipboard } from \"../shared/use-copy-to-clipboard\";\n\nimport { Button, cn, Collapsible, CollapsibleTrigger } from \"./_adapter\";\n\nconst COPY_ID = \"codeblock-code\";\nconst MAX_HTML_CACHE_ENTRIES = 64;\n\nlet highlighterPromise: Promise<Highlighter> | null = null;\n\nfunction getHighlighter(): Promise<Highlighter> {\n  if (!highlighterPromise) {\n    highlighterPromise = createHighlighter({\n      themes: [pierreDarkTheme as never, pierreLightTheme as never],\n      langs: [],\n      engine: createJavaScriptRegexEngine(),\n    });\n  }\n  return highlighterPromise;\n}\n\nconst htmlCache = new Map<string, string>();\n\nfunction getCacheKey(\n  code: string,\n  language: string,\n  theme: string,\n  lineNumbers: CodeBlockLineNumbersMode,\n  highlightLines?: number[],\n): string {\n  return JSON.stringify({\n    code,\n    language,\n    theme,\n    lineNumbers,\n    highlightLines: highlightLines ?? null,\n  });\n}\n\nfunction setCachedHtml(cacheKey: string, html: string): void {\n  if (htmlCache.has(cacheKey)) {\n    htmlCache.set(cacheKey, html);\n    return;\n  }\n\n  if (htmlCache.size >= MAX_HTML_CACHE_ENTRIES) {\n    const oldestKey = htmlCache.keys().next().value;\n    if (typeof oldestKey === \"string\") {\n      htmlCache.delete(oldestKey);\n    }\n  }\n\n  htmlCache.set(cacheKey, html);\n}\n\nconst LANGUAGE_DISPLAY_NAMES: Record<string, string> = {\n  typescript: \"TypeScript\",\n  javascript: \"JavaScript\",\n  python: \"Python\",\n  tsx: \"TSX\",\n  jsx: \"JSX\",\n  json: \"JSON\",\n  bash: \"Bash\",\n  shell: \"Shell\",\n  css: \"CSS\",\n  html: \"HTML\",\n  markdown: \"Markdown\",\n  sql: \"SQL\",\n  yaml: \"YAML\",\n  go: \"Go\",\n  rust: \"Rust\",\n  text: \"Plain Text\",\n};\n\nfunction getLanguageDisplayName(lang: string): string {\n  return LANGUAGE_DISPLAY_NAMES[lang.toLowerCase()] || lang.toUpperCase();\n}\n\nfunction getSystemTheme(): \"light\" | \"dark\" {\n  if (typeof window === \"undefined\") return \"light\";\n  return window.matchMedia?.(\"(prefers-color-scheme: dark)\").matches\n    ? \"dark\"\n    : \"light\";\n}\n\nfunction getDocumentTheme(): \"light\" | \"dark\" | null {\n  if (typeof document === \"undefined\") return null;\n  const root = document.documentElement;\n  const dataTheme = root.getAttribute(\"data-theme\")?.toLowerCase();\n  if (dataTheme === \"dark\") return \"dark\";\n  if (dataTheme === \"light\") return \"light\";\n  if (root.classList.contains(\"dark\")) return \"dark\";\n  if (root.classList.contains(\"light\")) return \"light\";\n  return null;\n}\n\nfunction useResolvedTheme(): \"light\" | \"dark\" {\n  const [theme, setTheme] = useState<\"light\" | \"dark\">(() => {\n    return getDocumentTheme() ?? getSystemTheme();\n  });\n\n  useEffect(() => {\n    if (typeof window === \"undefined\" || typeof document === \"undefined\") {\n      return;\n    }\n\n    const update = () => setTheme(getDocumentTheme() ?? getSystemTheme());\n\n    const mql = window.matchMedia?.(\"(prefers-color-scheme: dark)\");\n    mql?.addEventListener(\"change\", update);\n\n    const observer = new MutationObserver(update);\n    observer.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\", \"data-theme\"],\n    });\n\n    return () => {\n      mql?.removeEventListener(\"change\", update);\n      observer.disconnect();\n    };\n  }, []);\n\n  return theme;\n}\n\nexport type CodeBlockRootProps = CodeBlockProps & {\n  children: ReactNode;\n  expanded?: boolean;\n  defaultExpanded?: boolean;\n  onExpandedChange?: (expanded: boolean) => void;\n};\n\ntype CodeBlockSharedState = {\n  id: string;\n  code: string;\n  language: string;\n  filename?: string;\n  highlightedHtml: string | null;\n  isCopied: boolean;\n  copyCode: () => void;\n  lineCount: number;\n  isCollapsed: boolean;\n  shouldCollapse: boolean;\n  toggleExpanded: () => void;\n};\n\nconst CodeBlockContext = createContext<CodeBlockSharedState | null>(null);\n\nfunction useCodeBlock(): CodeBlockSharedState {\n  const context = use(CodeBlockContext);\n  if (!context) {\n    throw new Error(\n      \"CodeBlock subcomponents must be used within <CodeBlock.Root>.\",\n    );\n  }\n  return context;\n}\n\nfunction CodeBlockRoot({\n  id,\n  code,\n  language = \"text\",\n  lineNumbers = \"visible\",\n  filename,\n  highlightLines,\n  maxCollapsedLines,\n  className,\n  children,\n  expanded: expandedProp,\n  defaultExpanded = false,\n  onExpandedChange,\n}: CodeBlockRootProps) {\n  const resolvedTheme = useResolvedTheme();\n  const [expandedState, setExpandedState] = useState(defaultExpanded);\n  const { copiedId, copy } = useCopyToClipboard();\n  const isCopied = copiedId === COPY_ID;\n\n  const expanded = expandedProp ?? expandedState;\n  const setExpanded = useCallback(\n    (nextExpanded: boolean) => {\n      if (expandedProp === undefined) {\n        setExpandedState(nextExpanded);\n      }\n      onExpandedChange?.(nextExpanded);\n    },\n    [expandedProp, onExpandedChange],\n  );\n\n  const theme = resolvedTheme === \"dark\" ? \"pierre-dark\" : \"pierre-light\";\n  const cacheKey = getCacheKey(\n    code,\n    language,\n    theme,\n    lineNumbers,\n    highlightLines,\n  );\n\n  const [highlightedHtml, setHighlightedHtml] = useState<string | null>(\n    () => htmlCache.get(cacheKey) ?? null,\n  );\n\n  useEffect(() => {\n    const cached = htmlCache.get(cacheKey);\n    if (cached) {\n      setHighlightedHtml(cached);\n      return;\n    }\n\n    let cancelled = false;\n    const showLineNumbers = lineNumbers === \"visible\";\n\n    async function highlight() {\n      if (!code) {\n        if (!cancelled) setHighlightedHtml(\"\");\n        return;\n      }\n\n      try {\n        const highlighter = await getHighlighter();\n        const loadedLangs = highlighter.getLoadedLanguages();\n\n        if (!loadedLangs.includes(language)) {\n          await highlighter.loadLanguage(\n            language as Parameters<Highlighter[\"loadLanguage\"]>[0],\n          );\n        }\n\n        const lineCount = code.split(\"\\n\").length;\n        const lineNumberWidth = `${String(lineCount).length + 0.5}ch`;\n\n        const html = highlighter.codeToHtml(code, {\n          lang: language,\n          theme,\n          transformers: [\n            {\n              line(node, line) {\n                node.properties[\"data-line\"] = line;\n                if (highlightLines?.includes(line)) {\n                  const highlightBg =\n                    resolvedTheme === \"dark\"\n                      ? \"rgba(255,255,255,0.1)\"\n                      : \"rgba(0,0,0,0.05)\";\n                  node.properties.style = `background:${highlightBg};`;\n                }\n                if (showLineNumbers) {\n                  node.children.unshift({\n                    type: \"element\",\n                    tagName: \"span\",\n                    properties: {\n                      style: `display:inline-block;width:${lineNumberWidth};text-align:right;margin-right:1.5em;user-select:none;opacity:0.5;`,\n                      \"aria-hidden\": \"true\",\n                    },\n                    children: [{ type: \"text\", value: String(line) }],\n                  });\n                }\n              },\n            },\n          ],\n        });\n        if (!cancelled) {\n          setCachedHtml(cacheKey, html);\n          setHighlightedHtml(html);\n        }\n      } catch {\n        const escaped = code\n          .replace(/&/g, \"&amp;\")\n          .replace(/</g, \"&lt;\")\n          .replace(/>/g, \"&gt;\");\n        if (!cancelled) {\n          setHighlightedHtml(`<pre><code>${escaped}</code></pre>`);\n        }\n      }\n    }\n    void highlight();\n    return () => {\n      cancelled = true;\n    };\n  }, [\n    cacheKey,\n    code,\n    language,\n    lineNumbers,\n    theme,\n    highlightLines,\n    resolvedTheme,\n  ]);\n\n  const lineCount = code.split(\"\\n\").length;\n  const shouldCollapse = !!maxCollapsedLines && lineCount > maxCollapsedLines;\n  const isCollapsed = shouldCollapse && !expanded;\n\n  const copyCode = useCallback(() => {\n    void copy(code, COPY_ID);\n  }, [code, copy]);\n\n  const toggleExpanded = useCallback(() => {\n    setExpanded(!expanded);\n  }, [expanded, setExpanded]);\n\n  const state: CodeBlockSharedState = {\n    id,\n    code,\n    language,\n    filename,\n    highlightedHtml,\n    isCopied,\n    copyCode,\n    lineCount,\n    shouldCollapse,\n    isCollapsed,\n    toggleExpanded,\n  };\n\n  return (\n    <CodeBlockContext.Provider value={state}>\n      <div\n        className={cn(\n          \"@container flex w-full min-w-80 flex-col gap-3\",\n          className,\n        )}\n        data-tool-ui-id={id}\n        data-slot=\"code-block\"\n      >\n        <div className=\"border-border bg-card overflow-hidden rounded-lg border shadow-xs\">\n          <Collapsible open={!isCollapsed}>{children}</Collapsible>\n        </div>\n      </div>\n    </CodeBlockContext.Provider>\n  );\n}\n\nexport type CodeBlockSectionProps = {\n  className?: string;\n};\n\nfunction CodeBlockHeader({ className }: CodeBlockSectionProps) {\n  const { language, filename, isCopied, copyCode } = useCodeBlock();\n  return (\n    <div\n      className={cn(\n        \"bg-card flex items-center justify-between border-b px-4 py-2\",\n        className,\n      )}\n    >\n      <div className=\"flex items-center gap-1\">\n        <span className=\"text-muted-foreground text-sm\">\n          {getLanguageDisplayName(language)}\n        </span>\n        {filename && (\n          <>\n            <span className=\"text-muted-foreground/50\">•</span>\n            <span className=\"text-foreground text-sm font-medium\">\n              {filename}\n            </span>\n          </>\n        )}\n      </div>\n      <Button\n        variant=\"ghost\"\n        size=\"sm\"\n        onClick={copyCode}\n        className=\"h-7 w-7 p-0\"\n        aria-label={isCopied ? \"Copied\" : \"Copy code\"}\n      >\n        {isCopied ? (\n          <Check className=\"h-4 w-4 text-green-700 dark:text-green-400\" />\n        ) : (\n          <Copy className=\"text-muted-foreground h-4 w-4\" />\n        )}\n      </Button>\n    </div>\n  );\n}\n\nfunction CodeBlockContent({ className }: CodeBlockSectionProps) {\n  const { highlightedHtml, isCollapsed } = useCodeBlock();\n  return (\n    <div\n      className={cn(\n        \"overflow-x-auto overflow-y-clip text-[13px] leading-[1.4] [&_pre]:bg-transparent [&_pre]:py-4\",\n        isCollapsed && \"max-h-[200px]\",\n        className,\n      )}\n    >\n      {highlightedHtml && (\n        <div dangerouslySetInnerHTML={{ __html: highlightedHtml }} />\n      )}\n    </div>\n  );\n}\n\nfunction CodeBlockCollapseToggle({ className }: CodeBlockSectionProps) {\n  const { shouldCollapse, isCollapsed, toggleExpanded, lineCount } =\n    useCodeBlock();\n\n  if (!shouldCollapse) return null;\n\n  return (\n    <CollapsibleTrigger asChild>\n      <Button\n        variant=\"ghost\"\n        onClick={toggleExpanded}\n        className={cn(\n          \"text-muted-foreground w-full rounded-none border-t font-normal\",\n          className,\n        )}\n      >\n        {isCollapsed ? (\n          <>\n            <ChevronDown className=\"mr-1 size-4\" />\n            Show all {lineCount} lines\n          </>\n        ) : (\n          <>\n            <ChevronUp className=\"mr-2 h-4 w-4\" />\n            Collapse\n          </>\n        )}\n      </Button>\n    </CollapsibleTrigger>\n  );\n}\n\nexport type CodeBlockComposedProps = Omit<CodeBlockRootProps, \"children\">;\n\nfunction CodeBlockComposed(props: CodeBlockComposedProps) {\n  return (\n    <CodeBlockRoot {...props}>\n      <CodeBlockHeader />\n      <CodeBlockContent />\n      <CodeBlockCollapseToggle />\n    </CodeBlockRoot>\n  );\n}\n\ntype CodeBlockComponent = typeof CodeBlockComposed & {\n  Root: typeof CodeBlockRoot;\n  Header: typeof CodeBlockHeader;\n  Content: typeof CodeBlockContent;\n  CollapseToggle: typeof CodeBlockCollapseToggle;\n};\n\nexport const CodeBlock = Object.assign(CodeBlockComposed, {\n  Root: CodeBlockRoot,\n  Header: CodeBlockHeader,\n  Content: CodeBlockContent,\n  CollapseToggle: CodeBlockCollapseToggle,\n}) as CodeBlockComponent;\n"
  },
  {
    "path": "apps/www/components/tool-ui/code-block/index.tsx",
    "content": "export { CodeBlock } from \"./code-block\";\nexport type {\n  CodeBlockRootProps,\n  CodeBlockComposedProps,\n  CodeBlockSectionProps,\n} from \"./code-block\";\nexport type {\n  CodeBlockProps,\n  CodeBlockLineNumbersMode,\n  SerializableCodeBlock,\n} from \"./schema\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/code-block/schema.ts",
    "content": "import { z } from \"zod\";\nimport { defineToolUiContract } from \"../shared/contract\";\nimport {\n  ToolUIIdSchema,\n  ToolUIReceiptSchema,\n  ToolUIRoleSchema,\n} from \"../shared/schema\";\n\nexport const CodeBlockPropsSchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  receipt: ToolUIReceiptSchema.optional(),\n  code: z.string(),\n  language: z.string().trim().min(1).default(\"text\"),\n  lineNumbers: z.enum([\"visible\", \"hidden\"]).default(\"visible\"),\n  filename: z.string().optional(),\n  highlightLines: z.array(z.number().int().positive()).optional(),\n  maxCollapsedLines: z.number().min(1).optional(),\n  className: z.string().optional(),\n});\n\nexport type CodeBlockProps = z.infer<typeof CodeBlockPropsSchema>;\nexport type CodeBlockLineNumbersMode = CodeBlockProps[\"lineNumbers\"];\n\nexport const SerializableCodeBlockSchema = CodeBlockPropsSchema.omit({\n  className: true,\n});\n\nexport type SerializableCodeBlock = z.infer<typeof SerializableCodeBlockSchema>;\n\nconst SerializableCodeBlockSchemaContract = defineToolUiContract(\n  \"CodeBlock\",\n  SerializableCodeBlockSchema,\n);\n\nexport const parseSerializableCodeBlock: (\n  input: unknown,\n) => SerializableCodeBlock = SerializableCodeBlockSchemaContract.parse;\n\nexport const safeParseSerializableCodeBlock: (\n  input: unknown,\n) => SerializableCodeBlock | null =\n  SerializableCodeBlockSchemaContract.safeParse;\n"
  },
  {
    "path": "apps/www/components/tool-ui/code-diff/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn          -> Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Button      -> shadcn/ui Button\n *   Collapsible -> shadcn/ui Collapsible\n */\n\nexport { cn } from \"@/lib/utils\";\nexport { Button } from \"@/components/ui/button\";\nexport { Collapsible, CollapsibleTrigger } from \"@/components/ui/collapsible\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/code-diff/code-diff.tsx",
    "content": "\"use client\";\n\nimport {\n  useState,\n  useCallback,\n  useEffect,\n  useMemo,\n  createContext,\n  use,\n  type ReactNode,\n} from \"react\";\nimport {\n  FileDiff as PierreFileDiff,\n  PatchDiff as PierrePatchDiff,\n} from \"@pierre/diffs/react\";\nimport { parseDiffFromFile, RegisteredCustomThemes } from \"@pierre/diffs\";\nimport type { FileDiffMetadata, ThemesType } from \"@pierre/diffs\";\nimport { Copy, Check, ChevronDown, ChevronUp } from \"lucide-react\";\nimport type { CodeDiffProps } from \"./schema\";\nimport { useCopyToClipboard } from \"../shared/use-copy-to-clipboard\";\nimport { Button, cn, Collapsible, CollapsibleTrigger } from \"./_adapter\";\n\n/*\n * Pierre's shared_highlighter registers custom themes with dynamic imports\n * (`import(\"../themes/pierre-dark.js\")`) that fail under Turbopack because the\n * package `exports` field doesn't include those subpaths. We override the\n * RegisteredCustomThemes map entries with loaders that point to local vendored\n * theme files in `components/tool-ui/shared`, which Turbopack can resolve.\n */\nRegisteredCustomThemes.set(\"pierre-dark\", () =>\n  import(\"../shared/pierre-dark-theme.js\").then((m) => m.default as never),\n);\nRegisteredCustomThemes.set(\"pierre-light\", () =>\n  import(\"../shared/pierre-light-theme.js\").then((m) => m.default as never),\n);\n\nconst COPY_ID = \"codediff-code\";\n\n/* ── Theme detection (mirrors CodeBlock) ────────────────────────── */\n\nfunction getSystemTheme(): \"light\" | \"dark\" {\n  if (typeof window === \"undefined\") return \"light\";\n  return window.matchMedia?.(\"(prefers-color-scheme: dark)\").matches\n    ? \"dark\"\n    : \"light\";\n}\n\nfunction getDocumentTheme(): \"light\" | \"dark\" | null {\n  if (typeof document === \"undefined\") return null;\n  const root = document.documentElement;\n  const dataTheme = root.getAttribute(\"data-theme\")?.toLowerCase();\n  if (dataTheme === \"dark\") return \"dark\";\n  if (dataTheme === \"light\") return \"light\";\n  if (root.classList.contains(\"dark\")) return \"dark\";\n  if (root.classList.contains(\"light\")) return \"light\";\n  return null;\n}\n\nfunction useResolvedTheme(): \"light\" | \"dark\" {\n  const [theme, setTheme] = useState<\"light\" | \"dark\">(() => {\n    return getDocumentTheme() ?? getSystemTheme();\n  });\n\n  useEffect(() => {\n    if (typeof window === \"undefined\" || typeof document === \"undefined\") {\n      return;\n    }\n\n    const update = () => setTheme(getDocumentTheme() ?? getSystemTheme());\n\n    const mql = window.matchMedia?.(\"(prefers-color-scheme: dark)\");\n    mql?.addEventListener(\"change\", update);\n\n    const observer = new MutationObserver(update);\n    observer.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\", \"data-theme\"],\n    });\n\n    return () => {\n      mql?.removeEventListener(\"change\", update);\n      observer.disconnect();\n    };\n  }, []);\n\n  return theme;\n}\n\n/* ── Language display names (mirrors CodeBlock) ─────────────────── */\n\nconst LANGUAGE_DISPLAY_NAMES: Record<string, string> = {\n  typescript: \"TypeScript\",\n  javascript: \"JavaScript\",\n  python: \"Python\",\n  tsx: \"TSX\",\n  jsx: \"JSX\",\n  json: \"JSON\",\n  bash: \"Bash\",\n  shell: \"Shell\",\n  css: \"CSS\",\n  html: \"HTML\",\n  markdown: \"Markdown\",\n  sql: \"SQL\",\n  yaml: \"YAML\",\n  go: \"Go\",\n  rust: \"Rust\",\n  text: \"Plain Text\",\n};\n\nfunction getLanguageDisplayName(lang: string): string {\n  return LANGUAGE_DISPLAY_NAMES[lang.toLowerCase()] || lang.toUpperCase();\n}\n\n/* ── Shared context ─────────────────────────────────────────────── */\n\ntype CodeDiffSharedState = {\n  id: string;\n  isPatchMode: boolean;\n  language: string;\n  lineNumbers: \"visible\" | \"hidden\";\n  filename?: string;\n  diffStyle: \"unified\" | \"split\";\n  copyableCode: string;\n  isCopied: boolean;\n  copyCode: () => void;\n  isCollapsed: boolean;\n  shouldCollapse: boolean;\n  toggleExpanded: () => void;\n  resolvedTheme: \"light\" | \"dark\";\n  pierreThemes: ThemesType;\n  fileDiffMetadata: FileDiffMetadata | null;\n  patch: string | null;\n  additions: number;\n  deletions: number;\n};\n\nconst CodeDiffContext = createContext<CodeDiffSharedState | null>(null);\n\nfunction useCodeDiff(): CodeDiffSharedState {\n  const context = use(CodeDiffContext);\n  if (!context) {\n    throw new Error(\n      \"CodeDiff subcomponents must be used within <CodeDiff.Root>.\",\n    );\n  }\n  return context;\n}\n\n/* ── Subcomponents ──────────────────────────────────────────────── */\n\nexport type CodeDiffRootProps = CodeDiffProps & {\n  children: ReactNode;\n  expanded?: boolean;\n  defaultExpanded?: boolean;\n  onExpandedChange?: (expanded: boolean) => void;\n};\n\nfunction CodeDiffRoot({\n  id,\n  oldCode,\n  newCode,\n  patch,\n  language = \"text\",\n  filename,\n  lineNumbers = \"visible\",\n  diffStyle = \"unified\",\n  maxCollapsedLines,\n  className,\n  children,\n  expanded: expandedProp,\n  defaultExpanded = false,\n  onExpandedChange,\n}: CodeDiffRootProps) {\n  const resolvedTheme = useResolvedTheme();\n  const [expandedState, setExpandedState] = useState(defaultExpanded);\n  const { copiedId, copy } = useCopyToClipboard();\n  const isCopied = copiedId === COPY_ID;\n\n  const expanded = expandedProp ?? expandedState;\n  const setExpanded = useCallback(\n    (nextExpanded: boolean) => {\n      if (expandedProp === undefined) {\n        setExpandedState(nextExpanded);\n      }\n      onExpandedChange?.(nextExpanded);\n    },\n    [expandedProp, onExpandedChange],\n  );\n\n  const pierreThemes: ThemesType = {\n    dark: \"pierre-dark\",\n    light: \"pierre-light\",\n  };\n\n  // Auto-detect mode: if `patch` is provided, use patch mode; otherwise files mode\n  const isPatchMode = !!patch;\n\n  const fileDiffMetadata = useMemo(() => {\n    if (isPatchMode) return null;\n    return parseDiffFromFile(\n      {\n        name: filename ?? \"file\",\n        contents: oldCode ?? \"\",\n        lang: language as never,\n      },\n      {\n        name: filename ?? \"file\",\n        contents: newCode ?? \"\",\n        lang: language as never,\n      },\n    );\n  }, [isPatchMode, oldCode, newCode, filename, language]);\n\n  const copyableCode = isPatchMode ? (patch ?? \"\") : (newCode ?? oldCode ?? \"\");\n\n  const lineCount = useMemo(() => {\n    if (isPatchMode) {\n      return (patch ?? \"\").split(\"\\n\").length;\n    }\n    if (fileDiffMetadata) {\n      return fileDiffMetadata.unifiedLineCount;\n    }\n    return 0;\n  }, [isPatchMode, patch, fileDiffMetadata]);\n\n  const { additions, deletions } = useMemo(() => {\n    if (!isPatchMode && fileDiffMetadata) {\n      let add = 0;\n      let del = 0;\n      for (const hunk of fileDiffMetadata.hunks) {\n        add += hunk.additionLines;\n        del += hunk.deletionLines;\n      }\n      return { additions: add, deletions: del };\n    }\n    if (isPatchMode && patch) {\n      let add = 0;\n      let del = 0;\n      for (const line of patch.split(\"\\n\")) {\n        if (line.startsWith(\"+\") && !line.startsWith(\"+++ \")) add++;\n        else if (line.startsWith(\"-\") && !line.startsWith(\"--- \")) del++;\n      }\n      return { additions: add, deletions: del };\n    }\n    return { additions: 0, deletions: 0 };\n  }, [isPatchMode, fileDiffMetadata, patch]);\n\n  const shouldCollapse = !!maxCollapsedLines && lineCount > maxCollapsedLines;\n  const isCollapsed = shouldCollapse && !expanded;\n\n  const copyCode = useCallback(() => {\n    void copy(copyableCode, COPY_ID);\n  }, [copyableCode, copy]);\n\n  const toggleExpanded = useCallback(() => {\n    setExpanded(!expanded);\n  }, [expanded, setExpanded]);\n\n  const state: CodeDiffSharedState = {\n    id,\n    isPatchMode,\n    language,\n    lineNumbers,\n    filename,\n    diffStyle,\n    copyableCode,\n    isCopied,\n    copyCode,\n    isCollapsed,\n    shouldCollapse,\n    toggleExpanded,\n    resolvedTheme,\n    pierreThemes,\n    fileDiffMetadata,\n    patch: isPatchMode ? (patch ?? null) : null,\n    additions,\n    deletions,\n  };\n\n  return (\n    <CodeDiffContext.Provider value={state}>\n      <div\n        className={cn(\n          \"@container flex w-full min-w-80 flex-col gap-3\",\n          className,\n        )}\n        data-tool-ui-id={id}\n        data-slot=\"code-diff\"\n      >\n        <div className=\"border-border bg-card overflow-hidden rounded-lg border shadow-xs\">\n          <Collapsible open={!isCollapsed}>{children}</Collapsible>\n        </div>\n      </div>\n    </CodeDiffContext.Provider>\n  );\n}\n\nexport type CodeDiffSectionProps = {\n  className?: string;\n};\n\nfunction CodeDiffHeader({ className }: CodeDiffSectionProps) {\n  const { language, filename, isCopied, copyCode, additions, deletions } =\n    useCodeDiff();\n  const hasChanges = additions > 0 || deletions > 0;\n  return (\n    <div\n      className={cn(\n        \"bg-card flex items-center justify-between gap-2 border-b px-4 py-2\",\n        className,\n      )}\n    >\n      <div className=\"flex items-center gap-1\">\n        <span className=\"text-muted-foreground text-sm\">\n          {getLanguageDisplayName(language)}\n        </span>\n        {filename && (\n          <>\n            <span className=\"text-muted-foreground/50\">&bull;</span>\n            <span className=\"text-foreground text-sm font-medium\">\n              {filename}\n            </span>\n          </>\n        )}\n      </div>\n      {hasChanges && (\n        <span className=\"ml-auto text-xs font-mono tabular-nums\">\n          {additions > 0 && (\n            <span style={{ color: \"#00cab1\" }}>+{additions}</span>\n          )}\n          {additions > 0 && deletions > 0 && \" \"}\n          {deletions > 0 && (\n            <span style={{ color: \"#ff2e3f\" }}>-{deletions}</span>\n          )}\n        </span>\n      )}\n      <Button\n        variant=\"ghost\"\n        size=\"sm\"\n        onClick={copyCode}\n        className=\"h-7 w-7 p-0\"\n        aria-label={isCopied ? \"Copied\" : \"Copy code\"}\n      >\n        {isCopied ? (\n          <Check className=\"h-4 w-4 text-green-700 dark:text-green-400\" />\n        ) : (\n          <Copy className=\"text-muted-foreground h-4 w-4\" />\n        )}\n      </Button>\n    </div>\n  );\n}\n\nfunction CodeDiffContent({ className }: CodeDiffSectionProps) {\n  const {\n    isPatchMode,\n    diffStyle,\n    lineNumbers,\n    isCollapsed,\n    resolvedTheme,\n    pierreThemes,\n    fileDiffMetadata,\n    patch,\n  } = useCodeDiff();\n\n  const disableLineNumbers = lineNumbers === \"hidden\";\n\n  return (\n    <div\n      className={cn(\n        \"overflow-x-auto overflow-y-clip text-sm\",\n        isCollapsed && \"max-h-[200px]\",\n        className,\n      )}\n    >\n      {!isPatchMode && fileDiffMetadata && (\n        <PierreFileDiff\n          fileDiff={fileDiffMetadata}\n          options={{\n            theme: pierreThemes,\n            themeType: resolvedTheme,\n            diffStyle,\n            disableFileHeader: true,\n            disableLineNumbers,\n          }}\n        />\n      )}\n      {isPatchMode && patch && (\n        <PierrePatchDiff\n          patch={patch}\n          options={{\n            theme: pierreThemes,\n            themeType: resolvedTheme,\n            diffStyle,\n            disableFileHeader: true,\n            disableLineNumbers,\n          }}\n        />\n      )}\n    </div>\n  );\n}\n\nfunction CodeDiffCollapseToggle({ className }: CodeDiffSectionProps) {\n  const { shouldCollapse, isCollapsed, toggleExpanded } = useCodeDiff();\n\n  if (!shouldCollapse) return null;\n\n  return (\n    <CollapsibleTrigger asChild>\n      <Button\n        variant=\"ghost\"\n        onClick={toggleExpanded}\n        className={cn(\n          \"text-muted-foreground w-full rounded-none border-t font-normal\",\n          className,\n        )}\n      >\n        {isCollapsed ? (\n          <>\n            <ChevronDown className=\"mr-1 size-4\" />\n            Show full diff\n          </>\n        ) : (\n          <>\n            <ChevronUp className=\"mr-2 h-4 w-4\" />\n            Collapse\n          </>\n        )}\n      </Button>\n    </CollapsibleTrigger>\n  );\n}\n\n/* ── Composed preset (callable as a flat component) ─────────────── */\n\nexport type CodeDiffComposedProps = Omit<CodeDiffRootProps, \"children\">;\n\nfunction CodeDiffComposed(props: CodeDiffComposedProps) {\n  return (\n    <CodeDiffRoot {...props}>\n      <CodeDiffHeader />\n      <CodeDiffContent />\n      <CodeDiffCollapseToggle />\n    </CodeDiffRoot>\n  );\n}\n\n/* ── Compound export: CodeDiff is callable AND has subcomponents ── */\n\ntype CodeDiffComponent = typeof CodeDiffComposed & {\n  Root: typeof CodeDiffRoot;\n  Header: typeof CodeDiffHeader;\n  Content: typeof CodeDiffContent;\n  CollapseToggle: typeof CodeDiffCollapseToggle;\n};\n\nexport const CodeDiff = Object.assign(CodeDiffComposed, {\n  Root: CodeDiffRoot,\n  Header: CodeDiffHeader,\n  Content: CodeDiffContent,\n  CollapseToggle: CodeDiffCollapseToggle,\n}) as CodeDiffComponent;\n"
  },
  {
    "path": "apps/www/components/tool-ui/code-diff/index.tsx",
    "content": "export { CodeDiff } from \"./code-diff\";\nexport type {\n  CodeDiffRootProps,\n  CodeDiffComposedProps,\n  CodeDiffSectionProps,\n} from \"./code-diff\";\nexport type { CodeDiffProps, SerializableCodeDiff } from \"./schema\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/code-diff/schema.ts",
    "content": "import { z } from \"zod\";\nimport { defineToolUiContract } from \"../shared/contract\";\nimport {\n  ToolUIIdSchema,\n  ToolUIReceiptSchema,\n  ToolUIRoleSchema,\n} from \"../shared/schema\";\n\nconst CodeDiffPropsSchemaBase = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  receipt: ToolUIReceiptSchema.optional(),\n  oldCode: z.string().optional(),\n  newCode: z.string().optional(),\n  patch: z.string().optional(),\n  language: z.string().trim().min(1).default(\"text\"),\n  filename: z.string().optional(),\n  lineNumbers: z.enum([\"visible\", \"hidden\"]).default(\"visible\"),\n  diffStyle: z.enum([\"unified\", \"split\"]).default(\"unified\"),\n  maxCollapsedLines: z.number().min(1).optional(),\n  className: z.string().optional(),\n});\n\nfunction validateCodeDiffInputMode(\n  data: { patch?: string; oldCode?: string; newCode?: string },\n  ctx: z.RefinementCtx,\n) {\n  const hasPatch = !!data.patch;\n  const hasFiles = !!data.oldCode || !!data.newCode;\n\n  if (!hasPatch && !hasFiles) {\n    ctx.addIssue({\n      code: \"custom\",\n      message:\n        \"Provide either a patch string or at least one of oldCode/newCode\",\n    });\n  }\n\n  if (hasPatch && hasFiles) {\n    ctx.addIssue({\n      code: \"custom\",\n      message:\n        \"Cannot mix patch mode with oldCode/newCode — use one or the other\",\n    });\n  }\n}\n\nexport const CodeDiffPropsSchema = CodeDiffPropsSchemaBase.superRefine(\n  validateCodeDiffInputMode,\n);\n\nexport type CodeDiffProps = z.infer<typeof CodeDiffPropsSchema>;\n\nexport const SerializableCodeDiffSchema = CodeDiffPropsSchemaBase.omit({\n  className: true,\n}).superRefine(validateCodeDiffInputMode);\n\nexport type SerializableCodeDiff = z.infer<typeof SerializableCodeDiffSchema>;\n\nconst SerializableCodeDiffSchemaContract = defineToolUiContract(\n  \"CodeDiff\",\n  SerializableCodeDiffSchema,\n);\n\nexport const parseSerializableCodeDiff: (\n  input: unknown,\n) => SerializableCodeDiff = SerializableCodeDiffSchemaContract.parse;\n\nexport const safeParseSerializableCodeDiff: (\n  input: unknown,\n) => SerializableCodeDiff | null = SerializableCodeDiffSchemaContract.safeParse;\n"
  },
  {
    "path": "apps/www/components/tool-ui/data-table/README.md",
    "content": "# Data Table\n\nImplementation for the \"data-table\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/data-table/index.tsx\n- serializable schema + parse helpers: components/tool-ui/data-table/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/data-table/content.mdx\n- Preset payload: lib/presets/data-table.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/data-table/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn           → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Button       → shadcn/ui Button\n *   DropdownMenu → shadcn/ui DropdownMenu\n *   Accordion    → shadcn/ui Accordion\n *   Tooltip      → shadcn/ui Tooltip\n *   Badge        → shadcn/ui Badge\n *   Table        → shadcn/ui Table\n */\n\nexport { cn } from \"@/lib/utils\";\nexport { Button } from \"@/components/ui/button\";\nexport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nexport {\n  Accordion,\n  AccordionContent,\n  AccordionItem,\n  AccordionTrigger,\n} from \"@/components/ui/accordion\";\nexport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nexport { Badge } from \"@/components/ui/badge\";\nexport {\n  Table,\n  TableHeader,\n  TableBody,\n  TableHead,\n  TableRow,\n  TableCell,\n} from \"@/components/ui/table\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/data-table/data-table.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  cn,\n  Table,\n  TableBody,\n  TableRow,\n  TableCell,\n  TableHeader,\n  TableHead,\n  Button,\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n  Accordion,\n  AccordionContent,\n  AccordionItem,\n  AccordionTrigger,\n} from \"./_adapter\";\nimport {\n  sortData,\n  createDataTableRowKeys,\n  getDataTableMobileDescriptionId,\n} from \"./utilities\";\nimport { renderFormattedValue } from \"./formatters\";\nimport type {\n  DataTableProps,\n  DataTableContextValue,\n  RowData,\n  DataTableRowData,\n  ColumnKey,\n  Column,\n} from \"./types\";\nimport type { FormatConfig } from \"./formatters\";\n\nexport const DEFAULT_LOCALE = \"en-US\" as const;\n\nfunction isNumericFormat(format?: FormatConfig): boolean {\n  const kind = format?.kind;\n  return (\n    kind === \"number\" ||\n    kind === \"currency\" ||\n    kind === \"percent\" ||\n    kind === \"delta\"\n  );\n}\n\nfunction getAlignmentClass(\n  align?: \"left\" | \"right\" | \"center\",\n): string | undefined {\n  if (align === \"right\") return \"text-right\";\n  if (align === \"center\") return \"text-center\";\n  return undefined;\n}\n\nconst DataTableContext = React.createContext<\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  DataTableContextValue<any> | undefined\n>(undefined);\n\nexport function useDataTable<T extends object = RowData>() {\n  const context = React.use(DataTableContext) as\n    | DataTableContextValue<T>\n    | undefined;\n  if (!context) {\n    throw new Error(\"useDataTable must be used within <DataTable.Provider />\");\n  }\n  return context;\n}\n\ntype DataTableLayout = \"auto\" | \"table\" | \"cards\";\n\ntype DataTableBaseProps<T extends object = RowData> = DataTableProps<T> & {\n  layout: DataTableLayout;\n};\n\ntype DataTableProviderProps<T extends object = RowData> = Pick<\n  DataTableProps<T>,\n  | \"columns\"\n  | \"data\"\n  | \"rowIdKey\"\n  | \"defaultSort\"\n  | \"sort\"\n  | \"onSortChange\"\n  | \"id\"\n  | \"locale\"\n> & {\n  children: React.ReactNode;\n};\n\nfunction DataTableProvider<T extends object = RowData>({\n  columns,\n  data: rawData,\n  rowIdKey,\n  defaultSort,\n  sort: controlledSort,\n  id,\n  onSortChange,\n  locale,\n  children,\n}: DataTableProviderProps<T>) {\n  // Default locale avoids SSR/client formatting mismatches.\n  const resolvedLocale = locale ?? DEFAULT_LOCALE;\n\n  const [internalSortBy, setInternalSortBy] = React.useState<\n    ColumnKey<T> | undefined\n  >(defaultSort?.by);\n  const [internalSortDirection, setInternalSortDirection] = React.useState<\n    \"asc\" | \"desc\" | undefined\n  >(defaultSort?.direction);\n\n  const sortBy = controlledSort?.by ?? internalSortBy;\n  const sortDirection = controlledSort?.direction ?? internalSortDirection;\n\n  const data = React.useMemo(() => {\n    if (!sortBy || !sortDirection) return rawData;\n    return sortData(rawData, sortBy, sortDirection, resolvedLocale);\n  }, [rawData, sortBy, sortDirection, resolvedLocale]);\n\n  const handleSort = React.useCallback(\n    (key: ColumnKey<T>) => {\n      let newDirection: \"asc\" | \"desc\" | undefined;\n\n      if (sortBy === key) {\n        if (sortDirection === \"asc\") {\n          newDirection = \"desc\";\n        } else if (sortDirection === \"desc\") {\n          newDirection = undefined;\n        } else {\n          newDirection = \"asc\";\n        }\n      } else {\n        newDirection = \"asc\";\n      }\n\n      const next = {\n        by: newDirection ? key : undefined,\n        direction: newDirection,\n      } as const;\n\n      if (controlledSort) {\n        onSortChange?.(next);\n      } else {\n        setInternalSortBy(next.by);\n        setInternalSortDirection(next.direction);\n      }\n    },\n    [sortBy, sortDirection, controlledSort, onSortChange],\n  );\n\n  const contextValue: DataTableContextValue<T> = {\n    columns,\n    data,\n    rowIdKey,\n    sortBy,\n    sortDirection,\n    toggleSort: handleSort,\n    id,\n    locale: resolvedLocale,\n  };\n\n  return (\n    <DataTableContext.Provider value={contextValue}>\n      {children}\n    </DataTableContext.Provider>\n  );\n}\n\ninterface DataTableLayoutProps {\n  layout: DataTableLayout;\n  emptyMessage: string;\n  maxHeight?: string;\n  className?: string;\n}\n\nfunction DataTableLayout({\n  layout,\n  emptyMessage,\n  maxHeight,\n  className,\n}: DataTableLayoutProps) {\n  const { columns, data, rowIdKey, sortBy, sortDirection, id } = useDataTable();\n  const rowKeys = React.useMemo(\n    () =>\n      createDataTableRowKeys(\n        data as Array<Record<string, unknown>>,\n        rowIdKey ? String(rowIdKey) : undefined,\n      ),\n    [data, rowIdKey],\n  );\n  const mobileDescriptionId = React.useMemo(\n    () => getDataTableMobileDescriptionId(String(id ?? \"data-table\")),\n    [id],\n  );\n\n  const sortAnnouncement = React.useMemo(() => {\n    const col = columns.find((c) => c.key === sortBy);\n    const label = col?.label ?? sortBy;\n    return sortBy && sortDirection\n      ? `Sorted by ${label}, ${sortDirection === \"asc\" ? \"ascending\" : \"descending\"}`\n      : \"\";\n  }, [columns, sortBy, sortDirection]);\n\n  return (\n    <div\n      className={cn(\"@container w-full min-w-80\", className)}\n      data-tool-ui-id={id}\n      data-slot=\"data-table\"\n      data-layout={layout}\n    >\n      <div\n        className={cn(\n          layout === \"table\"\n            ? \"block\"\n            : layout === \"cards\"\n              ? \"hidden\"\n              : \"hidden @md:block\",\n        )}\n      >\n        <div className=\"relative\">\n          <div\n            className={cn(\n              \"bg-card relative w-full overflow-clip overflow-y-auto rounded-lg border\",\n              \"touch-pan-x\",\n              maxHeight && \"max-h-[--max-height]\",\n            )}\n            style={\n              maxHeight\n                ? ({ \"--max-height\": maxHeight } as React.CSSProperties)\n                : undefined\n            }\n          >\n            <Table>\n              {columns.length > 0 && (\n                <colgroup>\n                  {columns.map((col) => (\n                    <col\n                      key={String(col.key)}\n                      style={col.width ? { width: col.width } : undefined}\n                    />\n                  ))}\n                </colgroup>\n              )}\n              {data.length === 0 ? (\n                <DataTableEmpty message={emptyMessage} />\n              ) : (\n                <DataTableContent />\n              )}\n            </Table>\n          </div>\n        </div>\n      </div>\n\n      <div\n        className={cn(\n          layout === \"cards\"\n            ? \"\"\n            : layout === \"table\"\n              ? \"hidden\"\n              : \"@md:hidden\",\n        )}\n        role=\"list\"\n        aria-label=\"Data table (mobile card view)\"\n        aria-describedby={mobileDescriptionId}\n      >\n        <div id={mobileDescriptionId} className=\"sr-only\">\n          Table data shown as expandable cards. Each card represents one row.\n          {columns.length > 0 &&\n            ` Columns: ${columns.map((c) => c.label).join(\", \")}.`}\n        </div>\n\n        {data.length === 0 ? (\n          <div className=\"text-muted-foreground py-8 text-center\">\n            {emptyMessage}\n          </div>\n        ) : (\n          <div className=\"bg-card flex flex-col overflow-hidden rounded-2xl border shadow-xs\">\n            {data.map((row, i) => {\n              const rowKey = rowKeys[i];\n              return (\n                <DataTableAccordionCard\n                  key={rowKey}\n                  row={row as unknown as DataTableRowData}\n                  index={i}\n                  rowKey={rowKey}\n                  isFirst={i === 0}\n                />\n              );\n            })}\n          </div>\n        )}\n      </div>\n\n      {sortAnnouncement && (\n        <div className=\"sr-only\" aria-live=\"polite\">\n          {sortAnnouncement}\n        </div>\n      )}\n    </div>\n  );\n}\n\nfunction DataTableBase<T extends object = RowData>(\n  props: DataTableBaseProps<T>,\n) {\n  const {\n    columns,\n    data,\n    rowIdKey,\n    defaultSort,\n    sort,\n    onSortChange,\n    id,\n    locale,\n    layout,\n    emptyMessage = \"No data available\",\n    maxHeight,\n    className,\n  } = props;\n\n  return (\n    <DataTableProvider\n      columns={columns}\n      data={data}\n      rowIdKey={rowIdKey}\n      defaultSort={defaultSort}\n      sort={sort}\n      onSortChange={onSortChange}\n      id={id}\n      locale={locale}\n    >\n      <DataTableLayout\n        layout={layout}\n        emptyMessage={emptyMessage}\n        maxHeight={maxHeight}\n        className={className}\n      />\n    </DataTableProvider>\n  );\n}\n\nfunction DataTableRoot<T extends object = RowData>(props: DataTableProps<T>) {\n  return <DataTableBase {...props} layout=\"auto\" />;\n}\n\nfunction DataTableTable<T extends object = RowData>(props: DataTableProps<T>) {\n  return <DataTableBase {...props} layout=\"table\" />;\n}\n\nfunction DataTableCards<T extends object = RowData>(props: DataTableProps<T>) {\n  return <DataTableBase {...props} layout=\"cards\" />;\n}\n\ntype DataTableComponent = {\n  <T extends object = RowData>(props: DataTableProps<T>): React.ReactElement;\n  Table: typeof DataTableTable;\n  Cards: typeof DataTableCards;\n  Provider: typeof DataTableProvider;\n};\n\nexport const DataTable = Object.assign(DataTableRoot, {\n  Table: DataTableTable,\n  Cards: DataTableCards,\n  Provider: DataTableProvider,\n}) as DataTableComponent;\n\nfunction DataTableContent() {\n  return (\n    <>\n      <DataTableHeader />\n      <DataTableBody />\n    </>\n  );\n}\n\nfunction DataTableEmpty({ message }: { message: string }) {\n  const { columns } = useDataTable();\n\n  return (\n    <TableBody>\n      <TableRow className=\"bg-card h-24 text-center\">\n        <TableCell colSpan={columns.length} role=\"status\" aria-live=\"polite\">\n          {message}\n        </TableCell>\n      </TableRow>\n    </TableBody>\n  );\n}\n\nfunction SortIcon({ state }: { state?: \"asc\" | \"desc\" }) {\n  let char = \"⇅\";\n  let className = \"opacity-20\";\n\n  if (state === \"asc\") {\n    char = \"↑\";\n    className = \"\";\n  }\n\n  if (state === \"desc\") {\n    char = \"↓\";\n    className = \"\";\n  }\n\n  return (\n    <span aria-hidden className={cn(\"min-w-4 shrink-0 text-center\", className)}>\n      {char}\n    </span>\n  );\n}\n\nfunction DataTableHeader() {\n  const { columns } = useDataTable();\n\n  return (\n    <TooltipProvider delayDuration={300}>\n      <TableHeader>\n        <TableRow className=\"hover:bg-transparent\">\n          {columns.map((column, columnIndex) => (\n            <DataTableHead\n              key={column.key}\n              column={column}\n              columnIndex={columnIndex}\n              totalColumns={columns.length}\n            />\n          ))}\n        </TableRow>\n      </TableHeader>\n    </TooltipProvider>\n  );\n}\n\ninterface DataTableHeadProps {\n  column: Column;\n  columnIndex?: number;\n  totalColumns?: number;\n}\n\nfunction DataTableHead({\n  column,\n  columnIndex = 0,\n  totalColumns = 1,\n}: DataTableHeadProps) {\n  const { sortBy, sortDirection, toggleSort } = useDataTable();\n  const isFirstColumn = columnIndex === 0;\n  const isLastColumn = columnIndex === totalColumns - 1;\n\n  const isSortable = column.sortable !== false;\n\n  const isSorted = sortBy === column.key;\n  const direction = isSorted ? sortDirection : undefined;\n  const isDisabled = !isSortable;\n\n  const handleClick = () => {\n    if (!isDisabled && toggleSort) {\n      toggleSort(column.key);\n    }\n  };\n\n  const displayText = column.abbr || column.label;\n  const shouldShowTooltip = column.abbr || displayText.length > 15;\n  const isNumericKind = isNumericFormat(column.format);\n  const align =\n    column.align ??\n    (columnIndex === 0 ? \"left\" : isNumericKind ? \"right\" : \"left\");\n  const alignClass = getAlignmentClass(align);\n  const buttonAlignClass = cn(\n    \"min-w-0 gap-1 font-normal\",\n    align === \"right\" && \"text-right\",\n    align === \"center\" && \"text-center\",\n    align === \"left\" && \"text-left\",\n  );\n  const labelAlignClass =\n    align === \"right\"\n      ? \"text-right\"\n      : align === \"center\"\n        ? \"text-center\"\n        : \"text-left\";\n\n  return (\n    <TableHead\n      scope=\"col\"\n      className={cn(\n        alignClass,\n        isFirstColumn && \"pl-1\",\n        isLastColumn && \"pr-1\",\n      )}\n      style={column.width ? { width: column.width } : undefined}\n      aria-sort={\n        isSorted\n          ? direction === \"asc\"\n            ? \"ascending\"\n            : \"descending\"\n          : undefined\n      }\n    >\n      <Button\n        type=\"button\"\n        size=\"sm\"\n        onClick={handleClick}\n        onKeyDown={(e) => {\n          if (isDisabled) return;\n          if (e.key === \"Enter\" || e.key === \" \") {\n            e.preventDefault();\n            handleClick();\n          }\n        }}\n        disabled={isDisabled}\n        variant=\"ghost\"\n        className={cn(\n          buttonAlignClass,\n          \"w-fit min-w-10\",\n          isFirstColumn && \"pl-4\",\n          isLastColumn && \"pr-4\",\n        )}\n        aria-label={\n          `Sort by ${column.label}` +\n          (isSorted && direction\n            ? ` (${direction === \"asc\" ? \"ascending\" : \"descending\"})`\n            : \"\")\n        }\n        aria-disabled={isDisabled || undefined}\n      >\n        {shouldShowTooltip ? (\n          <Tooltip>\n            <TooltipTrigger asChild>\n              <span className={cn(\"truncate\", labelAlignClass)}>\n                {column.abbr ? (\n                  <abbr\n                    title={column.label}\n                    className={cn(\n                      \"cursor-help border-b border-dotted border-current no-underline\",\n                      labelAlignClass,\n                    )}\n                  >\n                    {column.abbr}\n                  </abbr>\n                ) : (\n                  <span className={labelAlignClass}>{column.label}</span>\n                )}\n              </span>\n            </TooltipTrigger>\n            <TooltipContent>\n              <p>{column.label}</p>\n            </TooltipContent>\n          </Tooltip>\n        ) : (\n          <span className={cn(\"truncate\", labelAlignClass)}>\n            {column.label}\n          </span>\n        )}\n        {isSortable && <SortIcon state={direction} />}\n      </Button>\n    </TableHead>\n  );\n}\n\nfunction DataTableBody() {\n  const { data, rowIdKey } = useDataTable<DataTableRowData>();\n  const rowKeys = React.useMemo(\n    () =>\n      createDataTableRowKeys(\n        data as Array<Record<string, unknown>>,\n        rowIdKey ? String(rowIdKey) : undefined,\n      ),\n    [data, rowIdKey],\n  );\n  const hasWarnedRowKeyRef = React.useRef(false);\n\n  React.useEffect(() => {\n    if (hasWarnedRowKeyRef.current) return;\n    if (process.env.NODE_ENV !== \"production\" && !rowIdKey && data.length > 0) {\n      hasWarnedRowKeyRef.current = true;\n      console.warn(\n        \"[DataTable] Missing `rowIdKey` prop. Falling back to inferred/content-derived row keys. \" +\n          \"Strongly recommended: Pass a `rowIdKey` prop that points to a unique identifier in your row data (e.g., 'id', 'uuid', 'symbol').\\n\" +\n          'Example: <DataTable rowIdKey=\"id\" columns={...} data={...} />',\n      );\n    }\n  }, [rowIdKey, data.length]);\n\n  return (\n    <TableBody>\n      {data.map((row, index) => {\n        const rowKey = rowKeys[index];\n        return <DataTableRow key={rowKey} row={row} />;\n      })}\n    </TableBody>\n  );\n}\n\ninterface DataTableRowProps {\n  row: DataTableRowData;\n  className?: string;\n}\n\nfunction DataTableRow({ row, className }: DataTableRowProps) {\n  const { columns } = useDataTable();\n\n  return (\n    <TableRow className={className}>\n      {columns.map((column, columnIndex) => (\n        <DataTableCell\n          key={column.key}\n          value={row[column.key]}\n          column={column}\n          row={row}\n          columnIndex={columnIndex}\n        />\n      ))}\n    </TableRow>\n  );\n}\n\ninterface DataTableCellProps {\n  value:\n    | string\n    | number\n    | boolean\n    | null\n    | (string | number | boolean | null)[];\n  column: Column;\n  row: DataTableRowData;\n  className?: string;\n  columnIndex?: number;\n}\n\nfunction DataTableCell({\n  value,\n  column,\n  row,\n  className,\n  columnIndex = 0,\n}: DataTableCellProps) {\n  const { locale } = useDataTable();\n  const isNumericKind = isNumericFormat(column.format);\n  const isNumericValue = typeof value === \"number\";\n  const displayValue = renderFormattedValue({ value, column, row, locale });\n  const align =\n    column.align ??\n    (columnIndex === 0\n      ? \"left\"\n      : isNumericKind || isNumericValue\n        ? \"right\"\n        : \"left\");\n  const alignClass = getAlignmentClass(align);\n\n  return (\n    <TableCell className={cn(\"px-5 py-3\", alignClass, className)}>\n      {displayValue}\n    </TableCell>\n  );\n}\n\nfunction categorizeColumns(columns: Column[]) {\n  const primary: Column[] = [];\n  const secondary: Column[] = [];\n\n  let visibleColumnCount = 0;\n  columns.forEach((col) => {\n    if (col.hideOnMobile) return;\n\n    if (col.priority === \"primary\") {\n      primary.push(col);\n    } else if (col.priority === \"secondary\") {\n      secondary.push(col);\n    } else if (col.priority === \"tertiary\") {\n      return;\n    } else {\n      if (visibleColumnCount < 2) {\n        primary.push(col);\n      } else {\n        secondary.push(col);\n      }\n      visibleColumnCount++;\n    }\n  });\n\n  return { primary, secondary };\n}\n\ninterface DataTableAccordionCardProps {\n  row: DataTableRowData;\n  index: number;\n  rowKey: string;\n  isFirst?: boolean;\n}\n\nfunction getDataTableRowDomId(rowKey: string): string {\n  return encodeURIComponent(rowKey).replace(/%/g, \"_\");\n}\n\nfunction DataTableAccordionCard({\n  row,\n  index,\n  rowKey,\n  isFirst = false,\n}: DataTableAccordionCardProps) {\n  const { columns, locale } = useDataTable();\n\n  const { primary, secondary } = React.useMemo(\n    () => categorizeColumns(columns),\n    [columns],\n  );\n\n  if (secondary.length === 0) {\n    return (\n      <SimpleCard\n        row={row}\n        columns={primary}\n        index={index}\n        rowKey={rowKey}\n        isFirst={isFirst}\n      />\n    );\n  }\n\n  const primaryColumn = primary[0];\n  const remainingPrimaryColumns = primary.slice(1);\n\n  const stableRowId = getDataTableRowDomId(rowKey);\n\n  const headingId = `row-${stableRowId}-heading`;\n  const detailsId = `row-${stableRowId}-details`;\n  const remainingPrimaryDataIds = remainingPrimaryColumns.map(\n    (col) => `row-${stableRowId}-${String(col.key)}`,\n  );\n\n  const primaryValue = primaryColumn\n    ? String(row[primaryColumn.key] ?? \"\")\n    : \"\";\n  const rowLabel = `Row ${index + 1}: ${primaryValue}`;\n  const accordionItemId = `row-${stableRowId}`;\n\n  return (\n    <Accordion\n      type=\"single\"\n      collapsible\n      className={cn(!isFirst && \"border-t\")}\n      role=\"listitem\"\n      aria-label={rowLabel}\n    >\n      <AccordionItem value={accordionItemId} className=\"group border-0\">\n        <AccordionTrigger\n          className=\"group-data-[state=closed]:hover:bg-accent/50 active:bg-accent/50 group-data-[state=open]:bg-muted w-full rounded-none px-4 py-3 hover:no-underline\"\n          aria-controls={detailsId}\n          aria-label={`${rowLabel}. ${secondary.length > 0 ? \"Expand for details\" : \"\"}`}\n        >\n          <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n            {primaryColumn && (\n              <div\n                id={headingId}\n                role=\"heading\"\n                aria-level={3}\n                className=\"truncate\"\n                aria-label={`${primaryColumn.label}: ${row[primaryColumn.key]}`}\n              >\n                {renderFormattedValue({\n                  value: row[primaryColumn.key],\n                  column: primaryColumn,\n                  row,\n                  locale,\n                })}\n              </div>\n            )}\n\n            {remainingPrimaryColumns.length > 0 && (\n              <div\n                className=\"text-muted-foreground flex w-full flex-wrap gap-x-4 gap-y-0.5\"\n                role=\"group\"\n                aria-label=\"Summary information\"\n              >\n                {remainingPrimaryColumns.map((col, idx) => (\n                  <span\n                    key={col.key}\n                    id={remainingPrimaryDataIds[idx]}\n                    className=\"flex min-w-0 gap-1 font-normal\"\n                    role=\"cell\"\n                    aria-label={`${col.label}: ${row[col.key]}`}\n                  >\n                    <span className=\"sr-only\">{col.label}:</span>\n                    <span aria-hidden=\"true\">{col.label}:</span>\n                    <span className=\"truncate\">\n                      {renderFormattedValue({\n                        value: row[col.key],\n                        column: col,\n                        row,\n                        locale,\n                      })}\n                    </span>\n                  </span>\n                ))}\n              </div>\n            )}\n          </div>\n        </AccordionTrigger>\n\n        <AccordionContent\n          className={\"flex flex-col gap-4 px-4 pb-4\"}\n          id={detailsId}\n          role=\"region\"\n          aria-labelledby={headingId}\n        >\n          {secondary.length > 0 && (\n            <dl\n              className={cn(\n                \"flex flex-col gap-2 pt-4\",\n                \"motion-safe:group-data-[state=open]:animate-in motion-safe:group-data-[state=open]:fade-in-0\",\n                \"motion-safe:group-data-[state=open]:slide-in-from-top-1\",\n                \"motion-safe:group-data-[state=closed]:animate-out motion-safe:group-data-[state=closed]:fade-out-0\",\n                \"motion-safe:group-data-[state=closed]:slide-out-to-top-1\",\n                \"duration-150\",\n              )}\n              role=\"list\"\n              aria-label=\"Additional data\"\n            >\n              {secondary.map((col) => (\n                <div\n                  key={col.key}\n                  className=\"flex items-start justify-between gap-4\"\n                  role=\"listitem\"\n                >\n                  <dt\n                    className=\"text-muted-foreground shrink-0\"\n                    id={`row-${stableRowId}-${String(col.key)}-label`}\n                  >\n                    {col.label}\n                  </dt>\n                  <dd\n                    className={cn(\n                      \"text-foreground min-w-0 text-pretty wrap-break-word\",\n                      col.align === \"right\" && \"text-right\",\n                      col.align === \"center\" && \"text-center\",\n                    )}\n                    role=\"cell\"\n                    aria-labelledby={`row-${stableRowId}-${String(col.key)}-label`}\n                  >\n                    {renderFormattedValue({\n                      value: row[col.key],\n                      column: col,\n                      row,\n                      locale,\n                    })}\n                  </dd>\n                </div>\n              ))}\n            </dl>\n          )}\n        </AccordionContent>\n      </AccordionItem>\n    </Accordion>\n  );\n}\n\n/**\n * Simple card with no accordion,   for when there are only primary columns\n */\nfunction SimpleCard({\n  row,\n  columns,\n  index,\n  rowKey,\n  isFirst = false,\n}: {\n  row: DataTableRowData;\n  columns: Column[];\n  index: number;\n  rowKey: string;\n  isFirst?: boolean;\n}) {\n  const { locale } = useDataTable();\n  const primaryColumn = columns[0];\n  const otherColumns = columns.slice(1);\n\n  const stableRowId = getDataTableRowDomId(rowKey);\n\n  const primaryValue = primaryColumn\n    ? String(row[primaryColumn.key] ?? \"\")\n    : \"\";\n  const rowLabel = `Row ${index + 1}: ${primaryValue}`;\n\n  return (\n    <div\n      className={cn(\"flex flex-col gap-2 p-4\", !isFirst && \"border-t\")}\n      role=\"listitem\"\n      aria-label={rowLabel}\n    >\n      {primaryColumn && (\n        <div\n          role=\"heading\"\n          aria-level={3}\n          aria-label={`${primaryColumn.label}: ${row[primaryColumn.key]}`}\n        >\n          {renderFormattedValue({\n            value: row[primaryColumn.key],\n            column: primaryColumn,\n            row,\n            locale,\n          })}\n        </div>\n      )}\n\n      {otherColumns.map((col) => (\n        <div\n          key={col.key}\n          className=\"flex items-start justify-between gap-4\"\n          role=\"group\"\n        >\n          <span\n            className=\"text-muted-foreground\"\n            id={`row-${stableRowId}-${String(col.key)}-label`}\n          >\n            {col.label}:\n          </span>\n          <span\n            className={cn(\n              \"min-w-0 wrap-break-word\",\n              col.align === \"right\" && \"text-right\",\n              col.align === \"center\" && \"text-center\",\n            )}\n            role=\"cell\"\n            aria-labelledby={`row-${stableRowId}-${String(col.key)}-label`}\n          >\n            {renderFormattedValue({\n              value: row[col.key],\n              column: col,\n              row,\n              locale,\n            })}\n          </span>\n        </div>\n      ))}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/data-table/formatters.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn, Badge, Tooltip, TooltipContent, TooltipTrigger } from \"./_adapter\";\nimport { resolveSafeNavigationHref } from \"../shared/media\";\n\ntype Tone = \"success\" | \"warning\" | \"danger\" | \"info\" | \"neutral\";\n\nexport type FormatConfig =\n  | { kind: \"text\" }\n  | {\n      kind: \"number\";\n      decimals?: number;\n      unit?: string;\n      compact?: boolean;\n      showSign?: boolean;\n    }\n  | { kind: \"currency\"; currency: string; decimals?: number }\n  | {\n      kind: \"percent\";\n      decimals?: number;\n      showSign?: boolean;\n      basis?: \"fraction\" | \"unit\";\n    }\n  | { kind: \"date\"; dateFormat?: \"short\" | \"long\" | \"relative\" }\n  | {\n      kind: \"delta\";\n      decimals?: number;\n      upIsPositive?: boolean;\n      showSign?: boolean;\n    }\n  | {\n      kind: \"status\";\n      statusMap: Record<string, { tone: Tone; label?: string }>;\n    }\n  | { kind: \"boolean\"; labels?: { true: string; false: string } }\n  | { kind: \"link\"; hrefKey?: string; external?: boolean }\n  | { kind: \"badge\"; colorMap?: Record<string, Tone> }\n  | { kind: \"array\"; maxVisible?: number };\n\ninterface DeltaValueProps {\n  value: number;\n  options?: Extract<FormatConfig, { kind: \"delta\" }>;\n  locale?: string;\n}\n\nexport function DeltaValue({ value, options, locale }: DeltaValueProps) {\n  const decimals = options?.decimals ?? 2;\n  const upIsPositive = options?.upIsPositive ?? true;\n  const showSign = options?.showSign ?? true;\n\n  const isPositive = value > 0;\n  const isNegative = value < 0;\n  const isNeutral = value === 0;\n\n  const isGood = upIsPositive ? isPositive : isNegative;\n  const isBad = upIsPositive ? isNegative : isPositive;\n\n  const colorClass = isGood\n    ? \"text-green-700 dark:text-green-500\"\n    : isBad\n      ? \"text-destructive\"\n      : \"text-muted-foreground\";\n\n  const absValue = Math.abs(value);\n  const formatted = new Intl.NumberFormat(locale, {\n    minimumFractionDigits: decimals,\n    maximumFractionDigits: decimals,\n  }).format(absValue);\n\n  const display =\n    showSign && !isNeutral\n      ? isNegative\n        ? `-${formatted}`\n        : `+${formatted}`\n      : formatted;\n\n  const arrow = isPositive ? \"↑\" : isNegative ? \"↓\" : \"\";\n\n  return (\n    <span className={cn(\"tabular-nums\", colorClass)}>\n      {display}\n      {!isNeutral && <span className=\"ml-0.5\">{arrow}</span>}\n    </span>\n  );\n}\n\ninterface StatusBadgeProps {\n  value: string;\n  options?: Extract<FormatConfig, { kind: \"status\" }>;\n}\n\nexport function StatusBadge({ value, options }: StatusBadgeProps) {\n  const config = options?.statusMap?.[value] ?? {\n    tone: \"neutral\" as Tone,\n    label: value,\n  };\n  const label = config.label ?? value;\n\n  const variant =\n    config.tone === \"danger\"\n      ? \"destructive\"\n      : config.tone === \"neutral\"\n        ? \"outline\"\n        : \"secondary\";\n\n  return (\n    <Badge\n      variant={variant}\n      className={cn(\n        \"border\",\n        config.tone === \"warning\" &&\n          \"bg-amber-100 text-amber-700 dark:bg-amber-950 dark:text-amber-100\",\n        config.tone === \"success\" &&\n          \"bg-green-100 text-green-700 dark:bg-green-950 dark:text-green-100\",\n        config.tone === \"info\" &&\n          \"bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-100\",\n        config.tone === \"danger\" &&\n          \"bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-100\",\n      )}\n    >\n      {label}\n    </Badge>\n  );\n}\n\ninterface CurrencyValueProps {\n  value: number;\n  options?: Extract<FormatConfig, { kind: \"currency\" }>;\n  locale?: string;\n}\n\nexport function CurrencyValue({ value, options, locale }: CurrencyValueProps) {\n  const currency = options?.currency ?? \"USD\";\n  const decimals = options?.decimals ?? 2;\n\n  const formatted = new Intl.NumberFormat(locale, {\n    style: \"currency\",\n    currency,\n    minimumFractionDigits: decimals,\n    maximumFractionDigits: decimals,\n  }).format(value);\n\n  return <span className=\"tabular-nums\">{formatted}</span>;\n}\n\ninterface PercentValueProps {\n  value: number;\n  options?: Extract<FormatConfig, { kind: \"percent\" }>;\n  locale?: string;\n}\n\nexport function PercentValue({ value, options, locale }: PercentValueProps) {\n  const decimals = options?.decimals ?? 2;\n  const showSign = options?.showSign ?? false;\n  const basis = options?.basis ?? \"fraction\";\n\n  const numeric = basis === \"fraction\" ? value : value / 100;\n\n  const formatted = new Intl.NumberFormat(locale, {\n    style: \"percent\",\n    minimumFractionDigits: decimals,\n    maximumFractionDigits: decimals,\n    signDisplay: showSign ? \"always\" : \"auto\",\n  }).format(numeric);\n\n  return <span className=\"tabular-nums\">{formatted}</span>;\n}\n\ninterface DateValueProps {\n  value: string;\n  options?: Extract<FormatConfig, { kind: \"date\" }>;\n  locale?: string;\n}\n\nexport function DateValue({ value, options, locale }: DateValueProps) {\n  const dateFormat = options?.dateFormat ?? \"short\";\n  const date = new Date(value);\n\n  if (isNaN(date.getTime())) {\n    return <span className=\"text-muted-foreground\">{value}</span>;\n  }\n\n  let formatted: string;\n\n  if (dateFormat === \"relative\") {\n    formatted = getRelativeTime(date, locale);\n  } else if (dateFormat === \"long\") {\n    formatted = new Intl.DateTimeFormat(locale, {\n      year: \"numeric\",\n      month: \"long\",\n      day: \"numeric\",\n    }).format(date);\n  } else {\n    formatted = new Intl.DateTimeFormat(locale, {\n      year: \"numeric\",\n      month: \"short\",\n      day: \"numeric\",\n    }).format(date);\n  }\n\n  const title = new Intl.DateTimeFormat(locale, {\n    year: \"numeric\",\n    month: \"long\",\n    day: \"numeric\",\n    hour: \"2-digit\",\n    minute: \"2-digit\",\n  }).format(date);\n\n  return (\n    <span className=\"tabular-nums\" title={title}>\n      {formatted}\n    </span>\n  );\n}\n\nfunction getRelativeTime(date: Date, locale?: string): string {\n  const now = new Date();\n  const diffInSeconds = Math.trunc((date.getTime() - now.getTime()) / 1000);\n  const absDiffInSeconds = Math.abs(diffInSeconds);\n\n  if (absDiffInSeconds < 60) return \"just now\";\n\n  const rtf = new Intl.RelativeTimeFormat(locale, { numeric: \"auto\" });\n\n  if (absDiffInSeconds < 3600) {\n    const mins = Math.trunc(diffInSeconds / 60);\n    return rtf.format(mins, \"minute\");\n  }\n  if (absDiffInSeconds < 86400) {\n    const hours = Math.trunc(diffInSeconds / 3600);\n    return rtf.format(hours, \"hour\");\n  }\n  if (absDiffInSeconds < 604800) {\n    const days = Math.trunc(diffInSeconds / 86400);\n    return rtf.format(days, \"day\");\n  }\n\n  return new Intl.DateTimeFormat(locale, {\n    year: \"numeric\",\n    month: \"short\",\n    day: \"numeric\",\n  }).format(date);\n}\n\ninterface BooleanValueProps {\n  value: boolean;\n  options?: Extract<FormatConfig, { kind: \"boolean\" }>;\n}\n\nexport function BooleanValue({ value, options }: BooleanValueProps) {\n  const labels = options?.labels ?? { true: \"Yes\", false: \"No\" };\n  const label = value ? labels.true : labels.false;\n  const variant = value ? \"secondary\" : \"outline\";\n\n  return <Badge variant={variant}>{label}</Badge>;\n}\n\ninterface LinkValueProps {\n  value: string;\n  options?: Extract<FormatConfig, { kind: \"link\" }>;\n  row?: Record<\n    string,\n    string | number | boolean | null | (string | number | boolean | null)[]\n  >;\n}\n\nexport function LinkValue({ value, options, row }: LinkValueProps) {\n  const rawHref =\n    options?.hrefKey && row ? String(row[options.hrefKey] ?? \"\") : value;\n  const href = resolveSafeNavigationHref(rawHref);\n  const external = options?.external ?? false;\n\n  if (!href) {\n    return <span>{value}</span>;\n  }\n\n  return (\n    <a\n      href={href}\n      target={external ? \"_blank\" : undefined}\n      rel={external ? \"noopener noreferrer\" : undefined}\n      className=\"text-accent-foreground inline-block max-w-full break-words underline underline-offset-2 hover:opacity-90\"\n      aria-label={external ? `${value} (opens in a new tab)` : undefined}\n      onClick={(e) => e.stopPropagation()}\n    >\n      {value}\n      {external && (\n        <span className=\"ml-1 inline-block\" aria-label=\"Opens in new tab\">\n          ↗\n        </span>\n      )}\n    </a>\n  );\n}\n\ninterface NumberValueProps {\n  value: number;\n  options?: Extract<FormatConfig, { kind: \"number\" }>;\n  locale?: string;\n}\n\nexport function NumberValue({ value, options, locale }: NumberValueProps) {\n  const decimals = options?.decimals ?? 0;\n  const unit = options?.unit ?? \"\";\n  const compact = options?.compact ?? false;\n  const showSign = options?.showSign ?? false;\n\n  const formatted = new Intl.NumberFormat(locale, {\n    minimumFractionDigits: decimals,\n    maximumFractionDigits: decimals,\n    notation: compact ? \"compact\" : \"standard\",\n  }).format(value);\n\n  const display = showSign && value > 0 ? `+${formatted}` : formatted;\n\n  return (\n    <span className=\"tabular-nums\">\n      {display}\n      {unit}\n    </span>\n  );\n}\n\ninterface BadgeValueProps {\n  value: string;\n  options?: Extract<FormatConfig, { kind: \"badge\" }>;\n}\n\nexport function BadgeValue({ value, options }: BadgeValueProps) {\n  const tone = options?.colorMap?.[value] ?? \"neutral\";\n\n  const variant =\n    tone === \"danger\"\n      ? \"destructive\"\n      : tone === \"neutral\"\n        ? \"outline\"\n        : \"secondary\";\n\n  return (\n    <Badge\n      variant={variant}\n      className={cn(\n        \"border\",\n        tone === \"warning\" &&\n          \"bg-amber-100 text-amber-700 dark:bg-amber-950 dark:text-amber-100\",\n        tone === \"success\" &&\n          \"bg-green-100 text-green-700 dark:bg-green-950 dark:text-green-100\",\n        tone === \"info\" &&\n          \"bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-100\",\n        tone === \"danger\" &&\n          \"bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-100\",\n      )}\n    >\n      {value}\n    </Badge>\n  );\n}\n\ninterface ArrayValueProps {\n  value: (string | number | boolean | null)[] | string;\n  options?: Extract<FormatConfig, { kind: \"array\" }>;\n}\n\nexport function ArrayValue({ value, options }: ArrayValueProps) {\n  const maxVisible = options?.maxVisible ?? 3;\n  const items: (string | number | boolean | null)[] = Array.isArray(value)\n    ? value\n    : typeof value === \"string\"\n      ? value.split(\",\").map((s) => s.trim())\n      : [];\n\n  if (items.length === 0) {\n    return <span className=\"text-muted\">—</span>;\n  }\n\n  const visible = items.slice(0, maxVisible);\n  const remaining = items.length - maxVisible;\n\n  const hidden = items.slice(maxVisible);\n\n  return (\n    <span className=\"inline-flex flex-wrap items-center gap-1\">\n      {visible.map((item, i) => (\n        <span\n          key={i}\n          className=\"bg-muted text-muted-foreground inline-flex items-center rounded-md px-2 py-0.5\"\n        >\n          {item === null ? \"null\" : String(item)}\n        </span>\n      ))}\n      {remaining > 0 && (\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <span className=\"text-muted-foreground cursor-default\">\n              +{remaining} more\n            </span>\n          </TooltipTrigger>\n          <TooltipContent>\n            {hidden\n              .map((item) => (item === null ? \"null\" : String(item)))\n              .join(\", \")}\n          </TooltipContent>\n        </Tooltip>\n      )}\n    </span>\n  );\n}\n\ninterface RenderFormattedValueParams {\n  value:\n    | string\n    | number\n    | boolean\n    | null\n    | (string | number | boolean | null)[];\n  column: { format?: FormatConfig };\n  row?: Record<\n    string,\n    string | number | boolean | null | (string | number | boolean | null)[]\n  >;\n  locale?: string;\n}\n\nexport function renderFormattedValue({\n  value,\n  column,\n  row,\n  locale,\n}: RenderFormattedValueParams): React.ReactNode {\n  if (value == null || value === \"\") {\n    return <span className=\"text-muted\">—</span>;\n  }\n\n  const fmt = column.format;\n\n  switch (fmt?.kind) {\n    case \"delta\":\n      return <DeltaValue value={Number(value)} options={fmt} locale={locale} />;\n    case \"status\":\n      return <StatusBadge value={String(value)} options={fmt} />;\n    case \"currency\":\n      return (\n        <CurrencyValue value={Number(value)} options={fmt} locale={locale} />\n      );\n    case \"percent\":\n      return (\n        <PercentValue value={Number(value)} options={fmt} locale={locale} />\n      );\n    case \"date\":\n      return <DateValue value={String(value)} options={fmt} locale={locale} />;\n    case \"boolean\":\n      return <BooleanValue value={Boolean(value)} options={fmt} />;\n    case \"link\":\n      return <LinkValue value={String(value)} options={fmt} row={row} />;\n    case \"number\":\n      return (\n        <NumberValue value={Number(value)} options={fmt} locale={locale} />\n      );\n    case \"badge\":\n      return <BadgeValue value={String(value)} options={fmt} />;\n    case \"array\":\n      return (\n        <ArrayValue\n          value={Array.isArray(value) ? value : String(value)}\n          options={fmt}\n        />\n      );\n    case \"text\":\n    default:\n      return String(value);\n  }\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/data-table/index.tsx",
    "content": "export { DataTable, useDataTable } from \"./data-table\";\n\nexport { renderFormattedValue } from \"./formatters\";\nexport {\n  NumberValue,\n  CurrencyValue,\n  PercentValue,\n  DeltaValue,\n  DateValue,\n  BooleanValue,\n  LinkValue,\n  BadgeValue,\n  StatusBadge,\n  ArrayValue,\n} from \"./formatters\";\n\nexport type {\n  Column,\n  DataTableProps,\n  DataTableSerializableProps,\n  DataTableClientProps,\n  DataTableRowData,\n  RowPrimitive,\n  RowData,\n  ColumnKey,\n} from \"./types\";\nexport type { FormatConfig } from \"./formatters\";\n\nexport { sortData, parseNumericLike } from \"./utilities\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/data-table/schema.ts",
    "content": "import { z } from \"zod\";\nimport {\n  ToolUIIdSchema,\n  ToolUIReceiptSchema,\n  ToolUIRoleSchema,\n} from \"../shared/schema\";\nimport { defineToolUiContract } from \"../shared/contract\";\nimport type { Column, DataTableProps, RowData } from \"./types\";\n\nconst AlignEnum = z.enum([\"left\", \"right\", \"center\"]);\nconst PriorityEnum = z.enum([\"primary\", \"secondary\", \"tertiary\"]);\n\nconst formatSchema = z.discriminatedUnion(\"kind\", [\n  z.object({ kind: z.literal(\"text\") }),\n  z.object({\n    kind: z.literal(\"number\"),\n    decimals: z.number().optional(),\n    unit: z.string().optional(),\n    compact: z.boolean().optional(),\n    showSign: z.boolean().optional(),\n  }),\n  z.object({\n    kind: z.literal(\"currency\"),\n    currency: z.string(),\n    decimals: z.number().optional(),\n  }),\n  z.object({\n    kind: z.literal(\"percent\"),\n    decimals: z.number().optional(),\n    showSign: z.boolean().optional(),\n    basis: z.enum([\"fraction\", \"unit\"]).optional(),\n  }),\n  z.object({\n    kind: z.literal(\"date\"),\n    dateFormat: z.enum([\"short\", \"long\", \"relative\"]).optional(),\n  }),\n  z.object({\n    kind: z.literal(\"delta\"),\n    decimals: z.number().optional(),\n    upIsPositive: z.boolean().optional(),\n    showSign: z.boolean().optional(),\n  }),\n  z.object({\n    kind: z.literal(\"status\"),\n    statusMap: z.record(\n      z.string(),\n      z.object({\n        tone: z.enum([\"success\", \"warning\", \"danger\", \"info\", \"neutral\"]),\n        label: z.string().optional(),\n      }),\n    ),\n  }),\n  z.object({\n    kind: z.literal(\"boolean\"),\n    labels: z\n      .object({\n        true: z.string(),\n        false: z.string(),\n      })\n      .optional(),\n  }),\n  z.object({\n    kind: z.literal(\"link\"),\n    hrefKey: z.string().optional(),\n    external: z.boolean().optional(),\n  }),\n  z.object({\n    kind: z.literal(\"badge\"),\n    colorMap: z\n      .record(\n        z.string(),\n        z.enum([\"success\", \"warning\", \"danger\", \"info\", \"neutral\"]),\n      )\n      .optional(),\n  }),\n  z.object({\n    kind: z.literal(\"array\"),\n    maxVisible: z.number().optional(),\n  }),\n]);\n\nexport const serializableColumnSchema = z.object({\n  key: z.string(),\n  label: z.string(),\n  abbr: z.string().optional(),\n  sortable: z.boolean().optional(),\n  align: AlignEnum.optional(),\n  width: z.string().optional(),\n  truncate: z.boolean().optional(),\n  priority: PriorityEnum.optional(),\n  hideOnMobile: z.boolean().optional(),\n  format: formatSchema.optional(),\n});\n\nconst JsonPrimitiveSchema = z.union([\n  z.string(),\n  z.number(),\n  z.boolean(),\n  z.null(),\n]);\n\n/**\n * Schema for serializable row data.\n *\n * Supports:\n * - Primitives: string, number, boolean, null\n * - Arrays of primitives: string[], number[], boolean[], or mixed primitive arrays\n *\n * Does NOT support:\n * - Functions\n * - Class instances (Date, Map, Set, etc.)\n * - Plain objects (use format configs instead)\n *\n * @example\n * Valid row data:\n * ```json\n * {\n *   \"name\": \"Widget\",\n *   \"price\": 29.99,\n *   \"active\": true,\n *   \"tags\": [\"electronics\", \"featured\"],\n *   \"metrics\": [1.2, 3.4, 5.6],\n *   \"flags\": [true, false, true],\n *   \"mixed\": [\"label\", 42, true]\n * }\n * ```\n */\nexport const serializableDataSchema = z.record(\n  z.string(),\n  z.union([JsonPrimitiveSchema, z.array(JsonPrimitiveSchema)]),\n);\n\n/**\n * Zod schema for validating DataTable payloads from LLM tool calls.\n *\n * This schema validates the serializable parts of a DataTable:\n * - id: Unique identifier for this tool UI in the conversation\n * - columns: Column definitions (keys, labels, formatting, etc.)\n * - data: Data rows (primitives only - no functions or class instances)\n * - optional presentation props: rowIdKey, sort/defaultSort, locale, etc.\n *\n * Non-serializable props like `onSortChange`, `className`, and sibling action surfaces\n * must be provided separately in your React component.\n *\n * @example\n * ```ts\n * const result = SerializableDataTableSchema.safeParse(llmResponse)\n * if (result.success) {\n *   // result.data contains validated id, columns, and data\n * }\n * ```\n */\nexport const SerializableDataTableSchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  receipt: ToolUIReceiptSchema.optional(),\n  columns: z.array(serializableColumnSchema),\n  data: z.array(serializableDataSchema),\n  rowIdKey: z.string().optional(),\n  defaultSort: z\n    .object({\n      by: z.string().optional(),\n      direction: z.enum([\"asc\", \"desc\"]).optional(),\n    })\n    .optional(),\n  sort: z\n    .object({\n      by: z.string().optional(),\n      direction: z.enum([\"asc\", \"desc\"]).optional(),\n    })\n    .optional(),\n  emptyMessage: z.string().optional(),\n  maxHeight: z.string().optional(),\n  locale: z.string().optional(),\n});\n\nconst SerializableDataTableSchemaContract = defineToolUiContract(\n  \"DataTable\",\n  SerializableDataTableSchema,\n);\n\n/**\n * Type representing the serializable parts of a DataTable payload.\n *\n * This type includes only JSON-serializable data that can come from LLM tool calls:\n * - Column definitions (format configs, alignment, labels, etc.)\n * - Row data (primitives: strings, numbers, booleans, null, string arrays)\n *\n * Excluded from this type:\n * - Event handlers (`onSortChange`)\n * - React-specific props (`className`)\n *\n * @example\n * ```ts\n * const payload: SerializableDataTable = {\n *   id: \"data-table-expenses\",\n *   columns: [\n *     { key: \"name\", label: \"Name\" },\n *     { key: \"price\", label: \"Price\", format: { kind: \"currency\", currency: \"USD\" } }\n *   ],\n *   data: [\n *     { name: \"Widget\", price: 29.99 }\n *   ]\n * }\n * ```\n */\nexport type SerializableDataTable = z.infer<typeof SerializableDataTableSchema>;\n\n/**\n * Validates and parses a DataTable payload from unknown data (e.g., LLM tool call result).\n *\n * This function:\n * 1. Validates the input against the `SerializableDataTableSchema`\n * 2. Throws a descriptive error if validation fails\n * 3. Returns typed serializable props ready to pass to the `<DataTable>` component\n *\n * The returned props are **serializable only** - you must provide client-side props\n * separately (onSortChange, className).\n *\n * @param input - Unknown data to validate (typically from an LLM tool call)\n * @returns Validated and typed DataTable serializable props (id, columns, data)\n * @throws Error with validation details if input is invalid\n *\n * @example\n * ```tsx\n * function MyToolUI({ result }: { result: unknown }) {\n *   const serializableProps = parseSerializableDataTable(result)\n *\n *   return (\n *     <DataTable\n *       {...serializableProps}\n *     />\n *   )\n * }\n * ```\n */\nexport function parseSerializableDataTable(\n  input: unknown,\n): Pick<\n  DataTableProps<RowData>,\n  | \"id\"\n  | \"role\"\n  | \"receipt\"\n  | \"columns\"\n  | \"data\"\n  | \"rowIdKey\"\n  | \"defaultSort\"\n  | \"sort\"\n  | \"emptyMessage\"\n  | \"maxHeight\"\n  | \"locale\"\n> {\n  const {\n    id,\n    role,\n    receipt,\n    columns,\n    data,\n    rowIdKey,\n    defaultSort,\n    sort,\n    emptyMessage,\n    maxHeight,\n    locale,\n  } = SerializableDataTableSchemaContract.parse(input);\n  return {\n    id,\n    role,\n    receipt,\n    columns: columns as unknown as Column<RowData>[],\n    data: data as RowData[],\n    rowIdKey: rowIdKey as keyof RowData | undefined,\n    defaultSort: defaultSort\n      ? {\n          by: defaultSort.by as keyof RowData | undefined,\n          direction: defaultSort.direction,\n        }\n      : undefined,\n    sort: sort\n      ? {\n          by: sort.by as keyof RowData | undefined,\n          direction: sort.direction,\n        }\n      : undefined,\n    emptyMessage,\n    maxHeight,\n    locale,\n  };\n}\n\nexport function safeParseSerializableDataTable(\n  input: unknown,\n): Pick<\n  DataTableProps<RowData>,\n  | \"id\"\n  | \"role\"\n  | \"receipt\"\n  | \"columns\"\n  | \"data\"\n  | \"rowIdKey\"\n  | \"defaultSort\"\n  | \"sort\"\n  | \"emptyMessage\"\n  | \"maxHeight\"\n  | \"locale\"\n> | null {\n  const res = SerializableDataTableSchemaContract.safeParse(input);\n  if (!res) return null;\n  const {\n    id,\n    role,\n    receipt,\n    columns,\n    data,\n    rowIdKey,\n    defaultSort,\n    sort,\n    emptyMessage,\n    maxHeight,\n    locale,\n  } = res;\n  return {\n    id,\n    role,\n    receipt,\n    columns: columns as unknown as Column<RowData>[],\n    data: data as RowData[],\n    rowIdKey: rowIdKey as keyof RowData | undefined,\n    defaultSort: defaultSort\n      ? {\n          by: defaultSort.by as keyof RowData | undefined,\n          direction: defaultSort.direction,\n        }\n      : undefined,\n    sort: sort\n      ? {\n          by: sort.by as keyof RowData | undefined,\n          direction: sort.direction,\n        }\n      : undefined,\n    emptyMessage,\n    maxHeight,\n    locale,\n  };\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/data-table/types.ts",
    "content": "import type { ToolUIId, ToolUIReceipt, ToolUIRole } from \"../shared/schema\";\nimport type { FormatConfig } from \"./formatters\";\n\n/**\n * JSON primitive type that can be serialized.\n */\ntype JsonPrimitive = string | number | boolean | null;\n\n/**\n * Valid row value types for serializable DataTable data.\n *\n * Supports:\n * - Primitives: string, number, boolean, null\n * - Arrays of primitives: string[], number[], boolean[], or mixed primitive arrays\n *\n * For complex data (objects with href/label, etc.), use column format configs\n * instead of putting objects in row data.\n *\n * @example\n * ```ts\n * // 👍 Good: Use primitives and primitive arrays\n * const row = {\n *   name: \"Widget\",\n *   price: 29.99,\n *   tags: [\"electronics\", \"featured\"],\n *   metrics: [1.2, 3.4, 5.6]\n * }\n *\n * // 🚫 Bad: Don't put objects in row data\n * const row = {\n *   link: { href: \"/path\", label: \"Click\" }  // Use format: { kind: 'link' } instead\n * }\n * ```\n */\nexport type RowPrimitive = JsonPrimitive | JsonPrimitive[];\nexport type DataTableRowData = Record<string, RowPrimitive>;\nexport type RowData = Record<string, unknown>;\nexport type ColumnKey<T extends object> = Extract<keyof T, string>;\n\nexport type FormatFor<V> = V extends number\n  ? Extract<FormatConfig, { kind: \"number\" | \"currency\" | \"percent\" | \"delta\" }>\n  : V extends boolean\n    ? Extract<FormatConfig, { kind: \"boolean\" | \"status\" | \"badge\" }>\n    : V extends (string | number | boolean | null)[]\n      ? Extract<FormatConfig, { kind: \"array\" }>\n      : V extends string\n        ? Extract<\n            FormatConfig,\n            { kind: \"text\" | \"link\" | \"date\" | \"badge\" | \"status\" }\n          >\n        : Extract<FormatConfig, { kind: \"text\" }>;\n\n/**\n * Column definition for DataTable\n *\n * @remarks\n * **Important:** Columns are sortable by default (opt-out pattern).\n * Set `sortable: false` explicitly to disable sorting for specific columns.\n */\nexport interface Column<\n  T extends object = DataTableRowData,\n  K extends ColumnKey<T> = ColumnKey<T>,\n> {\n  /** Unique identifier that maps to a key in the row data */\n  key: K;\n  /** Display text for the column header */\n  label: string;\n  /** Abbreviated label for narrow viewports */\n  abbr?: string;\n  /** Whether column is sortable. Default: true (opt-out pattern) */\n  sortable?: boolean;\n  /** Text alignment for column cells */\n  align?: \"left\" | \"right\" | \"center\";\n  /** Optional fixed width (CSS value) */\n  width?: string;\n  /** Enable text truncation with ellipsis */\n  truncate?: boolean;\n  /** Mobile display priority (primary = always visible, secondary = expandable, tertiary = hidden) */\n  priority?: \"primary\" | \"secondary\" | \"tertiary\";\n  /** Completely hide column on mobile viewports */\n  hideOnMobile?: boolean;\n  /** Formatting configuration for cell values */\n  format?: FormatFor<T[K]>;\n}\n\n/**\n * Serializable props that can come from LLM tool calls or be JSON-serialized.\n *\n * These props contain only primitive values, arrays, and plain objects -\n * no functions, class instances, or other non-serializable values.\n *\n * @example\n * ```tsx\n * const serializableProps: DataTableSerializableProps = {\n *   columns: [...],\n *   data: [...],\n *   rowIdKey: \"id\",\n *   defaultSort: { by: \"price\", direction: \"desc\" }\n * }\n * ```\n */\nexport interface DataTableSerializableProps<T extends object = RowData> {\n  /**\n   * Unique identifier for this tool UI instance in the conversation.\n   *\n   * Used for:\n   * - Assistant referencing (\"the table above\")\n   * - Receipt generation (linking actions to their source)\n   * - Narration context\n   *\n   * Should be stable across re-renders, meaningful, and unique within the conversation.\n   *\n   * @example \"data-table-expenses-q3\", \"search-results-repos\"\n   */\n  id: ToolUIId;\n  /** Optional surface role metadata (serializable) */\n  role?: ToolUIRole;\n  /** Optional receipt metadata for consequential outcomes (serializable) */\n  receipt?: ToolUIReceipt;\n  /** Column definitions */\n  columns: Column<T>[];\n  /** Row data (primitives only - no functions or class instances) */\n  data: T[];\n  /**\n   * Key in row data to use as unique identifier for React keys\n   *\n   * **Strongly recommended:** Always provide this for dynamic data to prevent\n   * reconciliation issues (focus traps, animation glitches, incorrect state preservation)\n   * when data reorders. Falls back to array index if omitted (only acceptable for static mock data).\n   *\n   * @example rowIdKey=\"id\" or rowIdKey=\"uuid\"\n   */\n  rowIdKey?: ColumnKey<T>;\n  /**\n   * Uncontrolled initial sort state (table manages its own sort state internally)\n   *\n   * **Sorting cycle:** Clicking column headers cycles through tri-state:\n   * 1. none (unsorted) → 2. asc → 3. desc → 4. none (back to unsorted)\n   *\n   * @example\n   * ```tsx\n   * // Start with descending price sort\n   * <DataTable defaultSort={{ by: \"price\", direction: \"desc\" }} />\n   * ```\n   */\n  defaultSort?: { by?: ColumnKey<T>; direction?: \"asc\" | \"desc\" };\n  /**\n   * Controlled sort state (use with onSortChange from client props)\n   *\n   * When provided, you must also provide `onSortChange` to handle sort updates.\n   * The table will cycle through: none → asc → desc → none.\n   *\n   * @example\n   * ```tsx\n   * const [sort, setSort] = useState({ by: \"price\", direction: \"desc\" })\n   * <DataTable sort={sort} onSortChange={setSort} />\n   * ```\n   */\n  sort?: { by?: ColumnKey<T>; direction?: \"asc\" | \"desc\" };\n  /** Empty state message */\n  emptyMessage?: string;\n  /** Max table height with vertical scroll (CSS value) */\n  maxHeight?: string;\n  /**\n   * BCP47 locale for formatting and sorting (e.g., 'en-US', 'de-DE', 'ja-JP')\n   *\n   * Defaults to 'en-US' to ensure consistent server/client rendering.\n   * Pass explicit locale for internationalization.\n   *\n   * @example\n   * ```tsx\n   * <DataTable locale=\"de-DE\" /> // German formatting\n   * <DataTable locale=\"ja-JP\" /> // Japanese formatting\n   * <DataTable />               // Uses 'en-US' default\n   * ```\n   */\n  locale?: string;\n}\n\n/**\n * Client-side React-only props that cannot be serialized.\n *\n * These props contain functions, component state, or other React-specific values\n * that must be provided by your React code (not from LLM tool calls).\n *\n * @example\n * ```tsx\n * const clientProps: DataTableClientProps = {\n *   className: \"my-table\",\n *   onSortChange: (next) => setSort(next),\n *   // Compose local/decision actions externally via LocalActions/DecisionActions\n * }\n * ```\n */\nexport interface DataTableClientProps<T extends object = RowData> {\n  /** Additional CSS classes */\n  className?: string;\n  /**\n   * Sort change handler for controlled mode (required if sort is provided)\n   *\n   * **Tri-state cycle behavior:**\n   * - Click unsorted column: `{ by: \"column\", direction: \"asc\" }`\n   * - Click asc column: `{ by: \"column\", direction: \"desc\" }`\n   * - Click desc column: `{ by: \"column\", direction: undefined }` (returns to unsorted)\n   * - Click different column: `{ by: \"newColumn\", direction: \"asc\" }`\n   *\n   * @example\n   * ```tsx\n   * const [sort, setSort] = useState<{ by?: string; direction?: \"asc\" | \"desc\" }>({})\n   *\n   * <DataTable\n   *   sort={sort}\n   *   onSortChange={(next) => {\n   *     console.log(\"Sort changed:\", next)\n   *     setSort(next)\n   *   }}\n   * />\n   * ```\n   */\n  onSortChange?: (next: {\n    by?: ColumnKey<T>;\n    direction?: \"asc\" | \"desc\";\n  }) => void;\n}\n\n/**\n * Complete props for the DataTable component.\n *\n * Combines serializable props (can come from LLM tool calls) with client-side\n * React-only props. This separation makes the boundary explicit and prevents\n * accidental serialization of non-serializable values.\n *\n * @see {@link DataTableSerializableProps} for props that can be JSON-serialized\n * @see {@link DataTableClientProps} for React-only props\n * @see {@link parseSerializableDataTable} for parsing LLM tool call results\n *\n * @example\n * ```tsx\n * // From LLM tool call\n * const serializableProps = parseSerializableDataTable(llmResult)\n *\n * // Combine with React-specific props\n * <DataTable\n *   {...serializableProps}\n *   onSortChange={setSort}\n *   // Render sibling LocalActions / DecisionActions where needed\n * />\n * ```\n */\nexport interface DataTableProps<T extends object = RowData>\n  extends DataTableSerializableProps<T>, DataTableClientProps<T> {}\n\nexport interface DataTableContextValue<T extends object = RowData> {\n  columns: Column<T>[];\n  data: T[];\n  rowIdKey?: ColumnKey<T>;\n  sortBy?: ColumnKey<T>;\n  sortDirection?: \"asc\" | \"desc\";\n  toggleSort?: (key: ColumnKey<T>) => void;\n  id?: string;\n  locale?: string;\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/data-table/utilities.ts",
    "content": "/**\n * Sort an array of objects by a key\n */\nexport function sortData<T, K extends Extract<keyof T, string>>(\n  data: T[],\n  key: K,\n  direction: \"asc\" | \"desc\",\n  locale?: string,\n): T[] {\n  const get = (obj: T, k: K): unknown => (obj as Record<string, unknown>)[k];\n  const collator = new Intl.Collator(locale, {\n    numeric: true,\n    sensitivity: \"base\",\n  });\n  return [...data].sort((a, b) => {\n    const aVal = get(a, key);\n    const bVal = get(b, key);\n\n    // Handle nulls\n    if (aVal == null && bVal == null) return 0;\n    if (aVal == null) return 1;\n    if (bVal == null) return -1;\n\n    // Type-specific comparison\n    // Numbers\n    if (typeof aVal === \"number\" && typeof bVal === \"number\") {\n      return direction === \"asc\" ? aVal - bVal : bVal - aVal;\n    }\n    // Dates (Date instances)\n    if (aVal instanceof Date && bVal instanceof Date) {\n      const diff = aVal.getTime() - bVal.getTime();\n      return direction === \"asc\" ? diff : -diff;\n    }\n    // Booleans: false < true\n    if (typeof aVal === \"boolean\" && typeof bVal === \"boolean\") {\n      const diff = aVal === bVal ? 0 : aVal ? 1 : -1;\n      return direction === \"asc\" ? diff : -diff;\n    }\n    // Arrays: compare length\n    if (Array.isArray(aVal) && Array.isArray(bVal)) {\n      const diff = aVal.length - bVal.length;\n      return direction === \"asc\" ? diff : -diff;\n    }\n    // Strings that look like numbers -> numeric compare\n    if (typeof aVal === \"string\" && typeof bVal === \"string\") {\n      const numA = parseNumericLike(aVal);\n      const numB = parseNumericLike(bVal);\n      if (numA != null && numB != null) {\n        const diff = numA - numB;\n        return direction === \"asc\" ? diff : -diff;\n      }\n      // ISO-like date strings\n      if (/^\\d{4}-\\d{2}-\\d{2}/.test(aVal) && /^\\d{4}-\\d{2}-\\d{2}/.test(bVal)) {\n        const da = new Date(aVal).getTime();\n        const db = new Date(bVal).getTime();\n        const diff = da - db;\n        return direction === \"asc\" ? diff : -diff;\n      }\n    }\n\n    // Fallback: locale-aware string compare with numeric collation\n    const aStr = String(aVal);\n    const bStr = String(bVal);\n    const comparison = collator.compare(aStr, bStr);\n    return direction === \"asc\" ? comparison : -comparison;\n  });\n}\n\n/**\n * Return a human-friendly identifier for a row using common keys\n *\n * Accepts any JSON-serializable primitive or array of primitives.\n * Arrays are converted to comma-separated strings.\n */\nexport function getRowIdentifier(\n  row: Record<\n    string,\n    string | number | boolean | null | (string | number | boolean | null)[]\n  >,\n  identifierKey?: string,\n): string {\n  const candidate =\n    (identifierKey ? row[identifierKey] : undefined) ??\n    (row as Record<string, unknown>).name ??\n    (row as Record<string, unknown>).title ??\n    (row as Record<string, unknown>).id;\n\n  if (candidate == null) {\n    return \"\";\n  }\n\n  // Handle arrays by joining them\n  if (Array.isArray(candidate)) {\n    return candidate.map((v) => (v === null ? \"null\" : String(v))).join(\", \");\n  }\n\n  return String(candidate).trim();\n}\n\nfunction stableStringify(value: unknown): string {\n  if (value == null) return \"null\";\n  if (typeof value === \"string\") return JSON.stringify(value);\n  if (\n    typeof value === \"number\" ||\n    typeof value === \"boolean\" ||\n    typeof value === \"bigint\"\n  ) {\n    return String(value);\n  }\n  if (Array.isArray(value)) {\n    return `[${value.map((item) => stableStringify(item)).join(\",\")}]`;\n  }\n  if (typeof value === \"object\") {\n    const entries = Object.entries(value as Record<string, unknown>).sort(\n      ([a], [b]) => a.localeCompare(b),\n    );\n    return `{${entries\n      .map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`)\n      .join(\",\")}}`;\n  }\n  return JSON.stringify(String(value));\n}\n\nfunction hashString(value: string): string {\n  let hash = 5381;\n  for (let i = 0; i < value.length; i++) {\n    hash = (hash * 33) ^ value.charCodeAt(i);\n  }\n  return (hash >>> 0).toString(36);\n}\n\n/**\n * Create deterministic, reorder-stable React keys for DataTable rows.\n *\n * - Uses `identifierKey` or common identifier fields as the primary base.\n * - Falls back to stable content fingerprints when no identifier exists.\n * - Disambiguates duplicates without relying on array index.\n */\nexport function createDataTableRowKeys(\n  rows: Array<Record<string, unknown>>,\n  identifierKey?: string,\n): string[] {\n  const canonicalRows = rows.map((row) => stableStringify(row));\n\n  const baseKeys = rows.map((row, index) => {\n    const identifier = getRowIdentifier(\n      row as Record<\n        string,\n        string | number | boolean | null | (string | number | boolean | null)[]\n      >,\n      identifierKey,\n    );\n\n    if (identifier) {\n      return `id:${identifier}`;\n    }\n\n    return `row:${hashString(canonicalRows[index])}`;\n  });\n\n  const baseCounts = new Map<string, number>();\n  baseKeys.forEach((key) => {\n    baseCounts.set(key, (baseCounts.get(key) ?? 0) + 1);\n  });\n\n  const usedKeys = new Map<string, number>();\n\n  return rows.map((row, index) => {\n    const baseKey = baseKeys[index];\n    if ((baseCounts.get(baseKey) ?? 0) === 1) {\n      return baseKey;\n    }\n\n    const rowFingerprint = hashString(canonicalRows[index]);\n    let disambiguatedKey = `${baseKey}::${rowFingerprint}`;\n\n    const seenCount = usedKeys.get(disambiguatedKey) ?? 0;\n    usedKeys.set(disambiguatedKey, seenCount + 1);\n    if (seenCount > 0) {\n      disambiguatedKey = `${disambiguatedKey}::d${seenCount + 1}`;\n    }\n\n    return disambiguatedKey;\n  });\n}\n\nfunction sanitizeDomIdToken(value: string): string {\n  return encodeURIComponent(value).replace(/%/g, \"_\");\n}\n\nexport function getDataTableMobileDescriptionId(surfaceId: string): string {\n  return `${sanitizeDomIdToken(surfaceId)}-mobile-table-description`;\n}\n\n/**\n * Parse a string that represents a numeric value, handling various formats:\n * - Currency symbols: $, €, £, ¥, etc.\n * - Percent symbols: %\n * - Accounting negatives: (1234) → -1234\n * - Thousands/decimal separators: 1,234.56 or 1.234,56\n * - Compact notation: 2.8T (trillion), 1.5M (million), 500K (thousand)\n * - Byte suffixes: 768B (bytes), 1.5KB, 2GB, 1TB\n *\n * Note: Single \"B\" is disambiguated - integers < 1024 are bytes, otherwise billions.\n *\n * @param input - String to parse\n * @returns Parsed number or null if unparseable\n *\n * @example\n * parseNumericLike(\"$1,234.56\") // 1234.56\n * parseNumericLike(\"2.8T\") // 2800000000000\n * parseNumericLike(\"768B\") // 768\n * parseNumericLike(\"50%\") // 50\n * parseNumericLike(\"(1234)\") // -1234\n */\nexport function parseNumericLike(input: string): number | null {\n  // Normalize whitespace (spaces, NBSPs, thin spaces)\n  let s = input.replace(/[\\u00A0\\u202F\\s]/g, \"\").trim();\n  if (!s) return null;\n\n  // Accounting negatives: (1234) -> -1234\n  s = s.replace(/^\\((.*)\\)$/g, \"-$1\");\n\n  // Strip common currency and percent symbols\n  s = s.replace(/[%$€£¥₩₹₽₺₪₫฿₦₴₡₲₵₸]/g, \"\");\n\n  function hasGroupedThousands(value: string, sep: \",\" | \".\"): boolean {\n    const unsigned = value.replace(/^[+-]/, \"\");\n    const parts = unsigned.split(sep);\n    if (parts.length < 2) return false;\n    if (parts.some((part) => part.length === 0)) return false;\n    if (!/^\\d{1,3}$/.test(parts[0])) return false;\n    if (parts[0] === \"0\") return false;\n    return parts.slice(1).every((part) => /^\\d{3}$/.test(part));\n  }\n\n  const lastComma = s.lastIndexOf(\",\");\n  const lastDot = s.lastIndexOf(\".\");\n  if (lastComma !== -1 && lastDot !== -1) {\n    // Decide decimal by whichever occurs last\n    const decimalSep = lastComma > lastDot ? \",\" : \".\";\n    const thousandSep = decimalSep === \",\" ? \".\" : \",\";\n    s = s.split(thousandSep).join(\"\");\n    s = s.replace(decimalSep, \".\");\n  } else if (lastComma !== -1) {\n    // Only comma present\n    if (hasGroupedThousands(s, \",\")) {\n      s = s.replace(/,/g, \"\");\n    } else {\n      const frac = s.length - lastComma - 1;\n      if (frac >= 1 && frac <= 3) s = s.replace(/,/g, \".\");\n      else s = s.replace(/,/g, \"\");\n    }\n  } else if (lastDot !== -1) {\n    // Only dot present; normalize grouped thousands separators.\n    if (hasGroupedThousands(s, \".\")) {\n      s = s.replace(/\\./g, \"\");\n    } else if ((s.match(/\\./g) || []).length > 1) {\n      s = s.replace(/\\./g, \"\");\n    }\n  }\n\n  // Handle compact notation (K, M, B, T, P, G) and byte suffixes (KB, MB, GB, TB, PB)\n  const compactMatch = s.match(/^([+-]?\\d+\\.?\\d*|\\d*\\.\\d+)([KMBTPG]B?|B)$/i);\n  if (compactMatch) {\n    const baseNum = Number(compactMatch[1]);\n    if (Number.isNaN(baseNum)) return null;\n\n    const suffix = compactMatch[2].toUpperCase();\n\n    // Disambiguate single \"B\" (bytes vs billions)\n    // If whole number < 1024, treat as bytes. Otherwise, billions.\n    if (suffix === \"B\") {\n      const isLikelyBytes = Number.isInteger(baseNum) && baseNum < 1024;\n      return isLikelyBytes ? baseNum : baseNum * 1e9;\n    }\n\n    const multipliers: Record<string, number> = {\n      K: 1e3,\n      KB: 1024, // Kilo: metric vs binary\n      M: 1e6,\n      MB: 1024 ** 2, // Mega\n      G: 1e9,\n      GB: 1024 ** 3, // Giga\n      T: 1e12,\n      TB: 1024 ** 4, // Tera\n      P: 1e15,\n      PB: 1024 ** 5, // Peta\n    };\n\n    return baseNum * (multipliers[suffix] ?? 1);\n  }\n\n  if (/^[+-]?(?:\\d+\\.?\\d*|\\d*\\.\\d+)$/.test(s)) {\n    const n = Number(s);\n    return Number.isNaN(n) ? null : n;\n  }\n  return null;\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/geo-map/README.md",
    "content": "# Geo Map\n\nImplementation for the \"geo-map\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/geo-map/index.tsx\n- serializable schema + parse helpers: components/tool-ui/geo-map/schema.ts\n- public facade component: components/tool-ui/geo-map/geo-map.tsx\n- internal Leaflet engine: components/tool-ui/geo-map/geo-map-engine.tsx\n- colocated Leaflet shell theme styles: components/tool-ui/geo-map/geo-map-theme.module.css\n- icon construction helpers: components/tool-ui/geo-map/geo-map-icons.ts\n- popup/tooltip overlay renderer: components/tool-ui/geo-map/geo-map-overlays.tsx\n\n## Companion assets\n\n- Docs page: app/docs/geo-map/content.mdx\n- Preset payload: lib/presets/geo-map.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/geo-map/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn      → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Leaflet → map primitives from react-leaflet\n */\n\nexport { cn } from \"@/lib/utils\";\nexport {\n  CircleMarker,\n  MapContainer,\n  Marker,\n  Polyline,\n  Popup,\n  TileLayer,\n  Tooltip,\n  ZoomControl,\n  useMap,\n  useMapEvents,\n} from \"react-leaflet\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/geo-map/geo-map-engine.tsx",
    "content": "\"use client\";\n\nimport type { Map as LeafletMap } from \"leaflet\";\nimport { memo, useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport Supercluster from \"supercluster\";\nimport {\n  CircleMarker,\n  MapContainer,\n  Marker,\n  Polyline,\n  TileLayer,\n  ZoomControl,\n  useMap,\n  useMapEvents,\n} from \"./_adapter\";\nimport { createClusterIcon, resolveMarkerIcon } from \"./geo-map-icons\";\nimport { GeoMapOverlays } from \"./geo-map-overlays\";\nimport type {\n  GeoMapClustering,\n  GeoMapFitTarget,\n  GeoMapMarker,\n  GeoMapRoute,\n  GeoMapViewport,\n} from \"./schema\";\n\nconst TILE_ATTRIBUTION =\n  '&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors &copy; <a href=\"https://carto.com/attributions\">CARTO</a>';\nconst ROUTE_DEFAULT_COLOR = \"var(--primary)\";\nconst ROUTE_DEFAULT_WEIGHT = 3;\nconst ROUTE_DEFAULT_OPACITY = 0.85;\nconst EMPTY_ROUTES: GeoMapRoute[] = [];\n\nconst CLUSTER_RADIUS_DEFAULT = 60;\nconst CLUSTER_MAX_ZOOM_DEFAULT = 16;\nconst CLUSTER_MIN_POINTS_DEFAULT = 2;\n\nconst DEFAULT_CENTER: [number, number] = [20, 0];\nexport const DEFAULT_VIEW_ZOOM = 2;\nconst SINGLE_LOCATION_ZOOM = 13;\nconst DEFAULT_VIEWPORT_PADDING = 32;\n\ntype LeafletRuntime = Pick<\n  typeof import(\"leaflet\"),\n  \"divIcon\" | \"latLngBounds\"\n>;\n\nexport type GeoMapBbox = [\n  west: number,\n  south: number,\n  east: number,\n  north: number,\n];\nexport type GeoMapLatLng = [lat: number, lng: number];\n\nexport type GeoMapClusterProperties = {\n  cluster?: boolean;\n  cluster_id?: number;\n  point_count?: number;\n  markerId?: string;\n};\n\nexport type GeoMapClusterFeature = GeoJSON.Feature<\n  GeoJSON.Point,\n  GeoMapClusterProperties\n>;\n\ntype MarkerClusterPointProperties = GeoMapClusterProperties & {\n  markerId?: string;\n  marker?: GeoMapMarker;\n};\n\ntype MapViewportState = {\n  bbox: GeoMapBbox;\n  zoom: number;\n};\n\nfunction roundCoordinate(value: number): number {\n  return Math.round(value * 1_000_000) / 1_000_000;\n}\n\nfunction normalizeViewportState(state: MapViewportState): MapViewportState {\n  return {\n    bbox: [\n      roundCoordinate(state.bbox[0]),\n      roundCoordinate(state.bbox[1]),\n      roundCoordinate(state.bbox[2]),\n      roundCoordinate(state.bbox[3]),\n    ],\n    zoom: state.zoom,\n  };\n}\n\nfunction areViewportStatesEqual(\n  a: MapViewportState | null,\n  b: MapViewportState,\n): boolean {\n  if (!a) {\n    return false;\n  }\n\n  return (\n    a.zoom === b.zoom &&\n    a.bbox[0] === b.bbox[0] &&\n    a.bbox[1] === b.bbox[1] &&\n    a.bbox[2] === b.bbox[2] &&\n    a.bbox[3] === b.bbox[3]\n  );\n}\n\nfunction serializeFitPoints(points: [number, number][]): string {\n  return points\n    .map(([lat, lng]) => `${roundCoordinate(lat)},${roundCoordinate(lng)}`)\n    .join(\"|\");\n}\n\nfunction readViewportState(map: LeafletMap): MapViewportState {\n  const bounds = map.getBounds();\n  return normalizeViewportState({\n    bbox: [\n      bounds.getWest(),\n      bounds.getSouth(),\n      bounds.getEast(),\n      bounds.getNorth(),\n    ],\n    zoom: Math.round(map.getZoom()),\n  });\n}\n\nexport function collectFitPoints(\n  markers: GeoMapMarker[],\n  routes: GeoMapRoute[],\n  target: GeoMapFitTarget,\n): GeoMapLatLng[] {\n  const markerPoints =\n    target === \"markers\" || target === \"all\"\n      ? markers.map((marker) => [marker.lat, marker.lng] as GeoMapLatLng)\n      : [];\n\n  const routePoints =\n    target === \"routes\" || target === \"all\"\n      ? routes.flatMap((route) =>\n          route.points.map((point) => [point.lat, point.lng] as GeoMapLatLng),\n        )\n      : [];\n\n  return [...markerPoints, ...routePoints];\n}\n\nexport function resolveFitPointsWithFallback(\n  markers: GeoMapMarker[],\n  routes: GeoMapRoute[],\n  target: GeoMapFitTarget,\n): GeoMapLatLng[] {\n  const selected = collectFitPoints(markers, routes, target);\n  if (selected.length > 0) {\n    return selected;\n  }\n\n  if (target !== \"markers\") {\n    return collectFitPoints(markers, routes, \"markers\");\n  }\n\n  return [];\n}\n\nexport function splitDatelineBbox(bbox: GeoMapBbox): GeoMapBbox[] {\n  const [west, south, east, north] = bbox;\n\n  if (west <= east) {\n    return [bbox];\n  }\n\n  return [\n    [west, south, 180, north],\n    [-180, south, east, north],\n  ];\n}\n\nfunction getClusterFeatureKey(feature: GeoMapClusterFeature): string {\n  const properties = feature.properties ?? {};\n\n  if (properties.cluster && typeof properties.cluster_id === \"number\") {\n    return `cluster:${properties.cluster_id}`;\n  }\n\n  if (\n    typeof properties.markerId === \"string\" &&\n    properties.markerId.length > 0\n  ) {\n    return `marker:${properties.markerId}`;\n  }\n\n  if (feature.id !== undefined && feature.id !== null) {\n    return `id:${String(feature.id)}`;\n  }\n\n  const [lng, lat] = feature.geometry.coordinates;\n  return `point:${lat}:${lng}`;\n}\n\nfunction dedupeClusterFeatures(\n  features: GeoMapClusterFeature[],\n): GeoMapClusterFeature[] {\n  const seen = new Set<string>();\n  const deduped: GeoMapClusterFeature[] = [];\n\n  features.forEach((feature) => {\n    const key = getClusterFeatureKey(feature);\n    if (seen.has(key)) {\n      return;\n    }\n\n    seen.add(key);\n    deduped.push(feature);\n  });\n\n  return deduped;\n}\n\nexport function getClustersForDatelineAwareBbox(\n  bbox: GeoMapBbox,\n  zoom: number,\n  getClustersForBbox: (\n    candidateBbox: GeoMapBbox,\n    zoom: number,\n  ) => GeoMapClusterFeature[],\n): GeoMapClusterFeature[] {\n  const queried = splitDatelineBbox(bbox).flatMap((candidateBbox) =>\n    getClustersForBbox(candidateBbox, zoom),\n  );\n\n  return dedupeClusterFeatures(queried);\n}\n\nexport function toSafeExpansionZoom(\n  zoom: number,\n  options?: { minZoom?: number; maxZoom?: number; fallback?: number },\n): number {\n  const minZoom = options?.minZoom ?? 1;\n  const maxZoom = options?.maxZoom ?? 22;\n  const fallback = options?.fallback ?? 2;\n\n  if (!Number.isFinite(zoom)) {\n    return fallback;\n  }\n\n  return Math.min(maxZoom, Math.max(minZoom, Math.round(zoom)));\n}\n\nfunction resolveInitialView(\n  markers: GeoMapMarker[],\n  routes: GeoMapRoute[],\n  viewport: GeoMapViewport | undefined,\n): { center: [number, number]; zoom: number } {\n  if (viewport?.mode === \"center\") {\n    return {\n      center: [viewport.center.lat, viewport.center.lng],\n      zoom: viewport.zoom,\n    };\n  }\n\n  const fitTarget = viewport?.target ?? \"all\";\n  const fitPoints = resolveFitPointsWithFallback(markers, routes, fitTarget);\n\n  if (fitPoints.length === 1) {\n    return {\n      center: [fitPoints[0][0], fitPoints[0][1]],\n      zoom: viewport?.maxZoom\n        ? Math.min(SINGLE_LOCATION_ZOOM, viewport.maxZoom)\n        : SINGLE_LOCATION_ZOOM,\n    };\n  }\n\n  return { center: DEFAULT_CENTER, zoom: DEFAULT_VIEW_ZOOM };\n}\n\nfunction ViewportController({\n  markers,\n  routes,\n  viewport,\n  leafletRuntime,\n}: {\n  markers: GeoMapMarker[];\n  routes: GeoMapRoute[];\n  viewport: GeoMapViewport | undefined;\n  leafletRuntime: LeafletRuntime;\n}) {\n  const map = useMap();\n  const lastAppliedViewportRef = useRef<string | null>(null);\n\n  useEffect(() => {\n    lastAppliedViewportRef.current = null;\n  }, [map]);\n\n  useEffect(() => {\n    if (viewport?.mode === \"center\") {\n      const viewportKey = `center:${roundCoordinate(viewport.center.lat)}:${roundCoordinate(viewport.center.lng)}:${viewport.zoom}`;\n      if (lastAppliedViewportRef.current === viewportKey) {\n        return;\n      }\n\n      lastAppliedViewportRef.current = viewportKey;\n      map.setView([viewport.center.lat, viewport.center.lng], viewport.zoom);\n      return;\n    }\n\n    const fitTarget = viewport?.target ?? \"all\";\n    const fitPoints = resolveFitPointsWithFallback(markers, routes, fitTarget);\n    if (fitPoints.length === 0) {\n      return;\n    }\n\n    const maxZoom = viewport?.maxZoom;\n    if (fitPoints.length === 1) {\n      const [lat, lng] = fitPoints[0];\n      const zoom = maxZoom\n        ? Math.min(SINGLE_LOCATION_ZOOM, maxZoom)\n        : SINGLE_LOCATION_ZOOM;\n      const viewportKey = `fit-single:${roundCoordinate(lat)}:${roundCoordinate(lng)}:${zoom}`;\n      if (lastAppliedViewportRef.current === viewportKey) {\n        return;\n      }\n\n      lastAppliedViewportRef.current = viewportKey;\n      map.setView([lat, lng], zoom);\n      return;\n    }\n\n    const padding = viewport?.padding ?? DEFAULT_VIEWPORT_PADDING;\n    const viewportKey = `fit:${fitTarget}:${padding}:${maxZoom ?? \"none\"}:${serializeFitPoints(fitPoints)}`;\n    if (lastAppliedViewportRef.current === viewportKey) {\n      return;\n    }\n\n    lastAppliedViewportRef.current = viewportKey;\n    const bounds = leafletRuntime.latLngBounds(fitPoints);\n    map.fitBounds(bounds, {\n      maxZoom,\n      padding: [padding, padding],\n    });\n  }, [leafletRuntime, map, markers, routes, viewport]);\n\n  return null;\n}\n\nfunction MapObserver({\n  onViewportChange,\n  onMapReady,\n}: {\n  onViewportChange: (state: MapViewportState) => void;\n  onMapReady: (map: LeafletMap) => void;\n}) {\n  const map = useMapEvents({\n    moveend: () => {\n      onViewportChange(readViewportState(map));\n    },\n    zoomend: () => {\n      onViewportChange(readViewportState(map));\n    },\n  });\n\n  useEffect(() => {\n    onMapReady(map);\n    onViewportChange(readViewportState(map));\n  }, [map, onMapReady, onViewportChange]);\n\n  return null;\n}\n\nfunction resolveMarkerAriaLabel(marker: GeoMapMarker): string {\n  if (marker.label && marker.description) {\n    return `${marker.label}. ${marker.description}`;\n  }\n\n  return (\n    marker.label ??\n    marker.description ??\n    `Marker at ${marker.lat.toFixed(4)}, ${marker.lng.toFixed(4)}`\n  );\n}\n\nexport const GeoMapEngine = memo(function GeoMapEngine({\n  id,\n  markers,\n  routes,\n  clustering,\n  viewport,\n  showZoomControl,\n  tileUrl,\n  mapAriaLabel,\n  tooltipClassName,\n  popupClassName,\n  onMarkerClick,\n  onRouteClick,\n  onReadyChange,\n}: {\n  id: string;\n  markers: GeoMapMarker[];\n  routes?: GeoMapRoute[];\n  clustering?: GeoMapClustering;\n  viewport?: GeoMapViewport;\n  showZoomControl: boolean;\n  tileUrl: string;\n  mapAriaLabel: string;\n  tooltipClassName?: string;\n  popupClassName?: string;\n  onMarkerClick?: (marker: GeoMapMarker) => void;\n  onRouteClick?: (route: GeoMapRoute) => void;\n  onReadyChange?: (isReady: boolean) => void;\n}) {\n  const resolvedRoutes = routes ?? EMPTY_ROUTES;\n  const [leafletRuntime, setLeafletRuntime] = useState<LeafletRuntime | null>(\n    null,\n  );\n  const [mapInstance, setMapInstance] = useState<LeafletMap | null>(null);\n  const [viewportState, setViewportState] = useState<MapViewportState | null>(\n    null,\n  );\n\n  const handleViewportChange = useCallback((nextState: MapViewportState) => {\n    const normalized = normalizeViewportState(nextState);\n    setViewportState((previousState) =>\n      areViewportStatesEqual(previousState, normalized)\n        ? previousState\n        : normalized,\n    );\n  }, []);\n\n  useEffect(() => {\n    let isActive = true;\n\n    void import(\"leaflet\").then((module) => {\n      if (!isActive) {\n        return;\n      }\n\n      setLeafletRuntime({\n        divIcon: module.divIcon,\n        latLngBounds: module.latLngBounds,\n      });\n    });\n\n    return () => {\n      isActive = false;\n    };\n  }, []);\n\n  const isReady = leafletRuntime !== null;\n\n  useEffect(() => {\n    onReadyChange?.(isReady);\n  }, [isReady, onReadyChange]);\n\n  useEffect(() => {\n    if (!mapInstance) {\n      return;\n    }\n\n    const container = mapInstance.getContainer();\n    container.setAttribute(\"role\", \"region\");\n    container.setAttribute(\"aria-label\", mapAriaLabel);\n  }, [mapAriaLabel, mapInstance]);\n\n  useEffect(() => {\n    if (!mapInstance) {\n      return;\n    }\n\n    const handleEscape = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\") {\n        mapInstance.closePopup();\n      }\n    };\n\n    document.addEventListener(\"keydown\", handleEscape);\n    return () => {\n      document.removeEventListener(\"keydown\", handleEscape);\n    };\n  }, [mapInstance]);\n\n  const initialView = useMemo(\n    () => resolveInitialView(markers, resolvedRoutes, viewport),\n    [markers, resolvedRoutes, viewport],\n  );\n\n  const markerById = useMemo(() => {\n    const map = new Map<string, GeoMapMarker>();\n    markers.forEach((marker, index) => {\n      map.set(marker.id ?? `marker-${index}`, marker);\n    });\n    return map;\n  }, [markers]);\n\n  const clusterConfig = useMemo(\n    () => ({\n      enabled: clustering?.enabled === true,\n      radius: clustering?.radius ?? CLUSTER_RADIUS_DEFAULT,\n      maxZoom: clustering?.maxZoom ?? CLUSTER_MAX_ZOOM_DEFAULT,\n      minPoints: clustering?.minPoints ?? CLUSTER_MIN_POINTS_DEFAULT,\n    }),\n    [clustering],\n  );\n\n  const clusterIndex = useMemo(() => {\n    if (!clusterConfig.enabled) {\n      return null;\n    }\n\n    const index = new Supercluster<MarkerClusterPointProperties>({\n      radius: clusterConfig.radius,\n      maxZoom: clusterConfig.maxZoom,\n      minPoints: clusterConfig.minPoints,\n    });\n\n    const points = markers.map((marker, index) => {\n      const markerId = marker.id ?? `marker-${index}`;\n      return {\n        type: \"Feature\" as const,\n        id: markerId,\n        geometry: {\n          type: \"Point\" as const,\n          coordinates: [marker.lng, marker.lat] as [number, number],\n        },\n        properties: {\n          markerId,\n          marker,\n        },\n      };\n    });\n\n    index.load(points);\n    return index;\n  }, [\n    clusterConfig.enabled,\n    clusterConfig.maxZoom,\n    clusterConfig.minPoints,\n    clusterConfig.radius,\n    markers,\n  ]);\n\n  const clusteredFeatures = useMemo(() => {\n    if (!clusterConfig.enabled || !clusterIndex || !viewportState) {\n      return [] as GeoMapClusterFeature[];\n    }\n\n    return getClustersForDatelineAwareBbox(\n      viewportState.bbox,\n      viewportState.zoom,\n      (bbox, zoom) =>\n        clusterIndex.getClusters(bbox, zoom) as GeoMapClusterFeature[],\n    );\n  }, [clusterConfig.enabled, clusterIndex, viewportState]);\n\n  const renderMarker = useCallback(\n    (\n      marker: GeoMapMarker,\n      markerKey: string,\n      markerPositionOverride?: [number, number],\n    ) => {\n      const markerPosition: [number, number] = markerPositionOverride ?? [\n        marker.lat,\n        marker.lng,\n      ];\n      const tooltipMode = marker.tooltip ?? \"hover\";\n      const tooltipContent = marker.label ?? marker.description;\n      const icon = marker.icon;\n      const markerAriaLabel = resolveMarkerAriaLabel(marker);\n\n      if (!leafletRuntime) {\n        return null;\n      }\n\n      const leafletIcon = resolveMarkerIcon(icon, leafletRuntime);\n      if (leafletIcon) {\n        return (\n          <Marker\n            key={markerKey}\n            position={markerPosition}\n            icon={leafletIcon}\n            title={markerAriaLabel}\n            alt={markerAriaLabel}\n            eventHandlers={{\n              click: () => onMarkerClick?.(marker),\n            }}\n          >\n            <GeoMapOverlays\n              tooltipMode={tooltipMode}\n              tooltipContent={tooltipContent}\n              label={marker.label}\n              description={marker.description}\n              tooltipClassName={tooltipClassName}\n              popupClassName={popupClassName}\n            />\n          </Marker>\n        );\n      }\n\n      const markerStroke =\n        icon?.type === \"dot\"\n          ? (icon.borderColor ?? \"var(--border)\")\n          : \"var(--border)\";\n      const markerFill =\n        icon?.type === \"dot\"\n          ? (icon.color ?? \"var(--primary)\")\n          : \"var(--primary)\";\n      const markerRadius = icon?.type === \"dot\" ? (icon.radius ?? 7) : 7;\n\n      return (\n        <CircleMarker\n          key={markerKey}\n          center={markerPosition}\n          radius={markerRadius}\n          pathOptions={{\n            color: markerStroke,\n            fillColor: markerFill,\n            fillOpacity: 0.95,\n            weight: 2,\n          }}\n          eventHandlers={{\n            click: () => onMarkerClick?.(marker),\n          }}\n        >\n          <GeoMapOverlays\n            tooltipMode={tooltipMode}\n            tooltipContent={tooltipContent}\n            label={marker.label}\n            description={marker.description}\n            tooltipClassName={tooltipClassName}\n            popupClassName={popupClassName}\n          />\n        </CircleMarker>\n      );\n    },\n    [leafletRuntime, onMarkerClick, popupClassName, tooltipClassName],\n  );\n\n  if (!leafletRuntime) {\n    return null;\n  }\n\n  return (\n    <MapContainer\n      center={initialView.center}\n      zoom={initialView.zoom}\n      zoomControl={false}\n      className=\"h-full w-full\"\n      scrollWheelZoom\n    >\n      <TileLayer attribution={TILE_ATTRIBUTION} url={tileUrl} />\n      {showZoomControl && <ZoomControl position=\"topright\" />}\n      <MapObserver\n        onMapReady={setMapInstance}\n        onViewportChange={handleViewportChange}\n      />\n      <ViewportController\n        leafletRuntime={leafletRuntime}\n        markers={markers}\n        routes={resolvedRoutes}\n        viewport={viewport}\n      />\n\n      {resolvedRoutes.map((route, routeIndex) => {\n        const routeKey = route.id ?? `${id}-route-${routeIndex}`;\n        const positions = route.points.map((point) => [\n          point.lat,\n          point.lng,\n        ]) as [number, number][];\n        const tooltipMode = route.tooltip ?? \"hover\";\n        const tooltipContent = route.label ?? route.description;\n\n        return (\n          <Polyline\n            key={routeKey}\n            positions={positions}\n            pathOptions={{\n              color: route.color ?? ROUTE_DEFAULT_COLOR,\n              weight: route.weight ?? ROUTE_DEFAULT_WEIGHT,\n              opacity: route.opacity ?? ROUTE_DEFAULT_OPACITY,\n              dashArray: route.dashArray,\n            }}\n            eventHandlers={{\n              click: () => onRouteClick?.(route),\n            }}\n          >\n            <GeoMapOverlays\n              tooltipMode={tooltipMode}\n              tooltipContent={tooltipContent}\n              label={route.label}\n              description={route.description}\n              tooltipClassName={tooltipClassName}\n              popupClassName={popupClassName}\n            />\n          </Polyline>\n        );\n      })}\n\n      {clusterConfig.enabled && clusterIndex && viewportState\n        ? clusteredFeatures.map((feature, index) => {\n            const [lng, lat] = feature.geometry.coordinates;\n            const properties = (feature.properties ??\n              {}) as MarkerClusterPointProperties;\n\n            if (\n              properties.cluster &&\n              typeof properties.cluster_id === \"number\"\n            ) {\n              const pointCount = properties.point_count ?? 0;\n              const clusterId = properties.cluster_id;\n              const clusterIcon = createClusterIcon(pointCount, leafletRuntime);\n              const clusterAriaLabel = `Cluster containing ${pointCount} locations`;\n\n              return (\n                <Marker\n                  key={`cluster-${clusterId}`}\n                  position={[lat, lng]}\n                  icon={clusterIcon}\n                  title={clusterAriaLabel}\n                  alt={clusterAriaLabel}\n                  eventHandlers={{\n                    click: () => {\n                      if (!mapInstance) {\n                        return;\n                      }\n\n                      const expansionZoom = toSafeExpansionZoom(\n                        clusterIndex.getClusterExpansionZoom(clusterId),\n                        {\n                          maxZoom: 22,\n                          fallback:\n                            (viewportState.zoom ?? DEFAULT_VIEW_ZOOM) + 2,\n                        },\n                      );\n                      mapInstance.flyTo([lat, lng], expansionZoom);\n                    },\n                  }}\n                />\n              );\n            }\n\n            const marker =\n              properties.marker ??\n              markerById.get(properties.markerId ?? `marker-${index}`);\n            if (!marker) {\n              return null;\n            }\n\n            const markerKey =\n              marker.id ?? properties.markerId ?? `${id}-cluster-leaf-${index}`;\n            return renderMarker(marker, markerKey, [lat, lng]);\n          })\n        : markers.map((marker, index) =>\n            renderMarker(marker, marker.id ?? `${id}-marker-${index}`),\n          )}\n    </MapContainer>\n  );\n});\n"
  },
  {
    "path": "apps/www/components/tool-ui/geo-map/geo-map-icons.ts",
    "content": "import type { DivIcon } from \"leaflet\";\nimport type { GeoMapMarker } from \"./schema\";\n\ntype LeafletIconRuntime = Pick<typeof import(\"leaflet\"), \"divIcon\">;\n\nfunction isSafeHttpUrl(value: string | undefined): boolean {\n  if (!value) {\n    return false;\n  }\n\n  try {\n    const parsed = new URL(value);\n    return parsed.protocol === \"http:\" || parsed.protocol === \"https:\";\n  } catch {\n    return false;\n  }\n}\n\nfunction escapeHtml(value: string): string {\n  return value\n    .replaceAll(\"&\", \"&amp;\")\n    .replaceAll(\"<\", \"&lt;\")\n    .replaceAll(\">\", \"&gt;\")\n    .replaceAll('\"', \"&quot;\")\n    .replaceAll(\"'\", \"&#39;\");\n}\n\nfunction createEmojiIcon(\n  icon: Extract<NonNullable<GeoMapMarker[\"icon\"]>, { type: \"emoji\" }>,\n  leafletRuntime: LeafletIconRuntime,\n): DivIcon {\n  const size = icon.size ?? 24;\n  const background = icon.bgColor ?? \"var(--card)\";\n  const border = icon.borderColor ?? \"var(--border)\";\n\n  return leafletRuntime.divIcon({\n    className: \"\",\n    html: `<span style=\"\ndisplay:flex;\nalign-items:center;\njustify-content:center;\nwidth:${size}px;\nheight:${size}px;\nborder-radius:999px;\nbackground:${background};\nborder:1px solid ${border};\nfont-size:${Math.round(size * 0.62)}px;\nline-height:1;\nbox-shadow:0 1px 3px oklch(from var(--foreground) l c h / 0.22);\n\">${escapeHtml(icon.value)}</span>`,\n    iconSize: [size, size],\n    iconAnchor: [size / 2, size / 2],\n    popupAnchor: [0, -Math.round(size / 2)],\n    tooltipAnchor: [0, -Math.round(size / 2)],\n  });\n}\n\nfunction createImageIcon(\n  icon: Extract<NonNullable<GeoMapMarker[\"icon\"]>, { type: \"image\" }>,\n  leafletRuntime: LeafletIconRuntime,\n): DivIcon {\n  const width = icon.width ?? 28;\n  const height = icon.height ?? 28;\n  const borderRadius = icon.borderRadius ?? Math.min(width, height) / 2;\n  const border = icon.borderColor ?? \"var(--border)\";\n\n  return leafletRuntime.divIcon({\n    className: \"\",\n    html: `<span style=\"\ndisplay:block;\nwidth:${width}px;\nheight:${height}px;\nborder-radius:${borderRadius}px;\noverflow:hidden;\nborder:1px solid ${border};\nbackground:var(--card);\nbox-shadow:0 1px 3px oklch(from var(--foreground) l c h / 0.22);\n\"><img src=\"${escapeHtml(icon.url)}\" alt=\"\" style=\"width:100%;height:100%;object-fit:cover;display:block;\" /></span>`,\n    iconSize: [width, height],\n    iconAnchor: [width / 2, height / 2],\n    popupAnchor: [0, -Math.round(height / 2)],\n    tooltipAnchor: [0, -Math.round(height / 2)],\n  });\n}\n\nexport function createClusterIcon(\n  count: number,\n  leafletRuntime: LeafletIconRuntime,\n): DivIcon {\n  const size = count >= 100 ? 42 : count >= 10 ? 38 : 34;\n  const background = \"var(--primary)\";\n  const border = \"var(--background)\";\n\n  return leafletRuntime.divIcon({\n    className: \"\",\n    html: `<span style=\"\ndisplay:flex;\nalign-items:center;\njustify-content:center;\nwidth:${size}px;\nheight:${size}px;\nborder-radius:999px;\nbackground:${background};\nborder:2px solid ${border};\ncolor:var(--primary-foreground);\nfont-size:12px;\nfont-weight:700;\nline-height:1;\nbox-shadow:0 2px 6px oklch(from var(--foreground) l c h / 0.25);\n\">${count}</span>`,\n    iconSize: [size, size],\n    iconAnchor: [size / 2, size / 2],\n    popupAnchor: [0, -Math.round(size / 2)],\n    tooltipAnchor: [0, -Math.round(size / 2)],\n  });\n}\n\nexport function resolveMarkerIcon(\n  icon: GeoMapMarker[\"icon\"] | undefined,\n  leafletRuntime: LeafletIconRuntime,\n): DivIcon | null {\n  if (icon?.type === \"emoji\") {\n    return createEmojiIcon(icon, leafletRuntime);\n  }\n\n  if (icon?.type === \"image\" && isSafeHttpUrl(icon.url)) {\n    return createImageIcon(icon, leafletRuntime);\n  }\n\n  return null;\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/geo-map/geo-map-overlays.tsx",
    "content": "\"use client\";\n\nimport { useMemo, useState } from \"react\";\n\nimport { Popup, Tooltip, cn } from \"./_adapter\";\n\nfunction GeoMapPopupContent({\n  label,\n  description,\n}: {\n  label?: string;\n  description?: string;\n}) {\n  return (\n    <div className=\"flex flex-col gap-0.5\">\n      {label && (\n        <p className=\"block text-sm leading-tight font-semibold tracking-tight text-foreground\">\n          {label}\n        </p>\n      )}\n      {description && (\n        <p className=\"block text-xs leading-relaxed text-muted-foreground\">\n          {description}\n        </p>\n      )}\n    </div>\n  );\n}\n\nfunction GeoMapTooltipContent({ text }: { text: string }) {\n  return <span className=\"block\">{text}</span>;\n}\n\nexport function GeoMapOverlays({\n  tooltipMode,\n  tooltipContent,\n  label,\n  description,\n  tooltipClassName,\n  popupClassName,\n}: {\n  tooltipMode: \"none\" | \"hover\" | \"always\";\n  tooltipContent?: string;\n  label?: string;\n  description?: string;\n  tooltipClassName?: string;\n  popupClassName?: string;\n}) {\n  const hasPopup = Boolean(label || description);\n  const [isPopupOpen, setIsPopupOpen] = useState(false);\n  const shouldRenderTooltip =\n    tooltipMode !== \"none\" && tooltipContent && (!hasPopup || !isPopupOpen);\n  const popupEventHandlers = useMemo(\n    () => ({\n      add: () => setIsPopupOpen(true),\n      remove: () => setIsPopupOpen(false),\n    }),\n    [],\n  );\n\n  return (\n    <>\n      {shouldRenderTooltip && (\n        <Tooltip\n          direction=\"top\"\n          permanent={tooltipMode === \"always\"}\n          className={cn(\"geo-map-tooltip\", tooltipClassName)}\n        >\n          <GeoMapTooltipContent text={tooltipContent} />\n        </Tooltip>\n      )}\n      {hasPopup && (\n        <Popup\n          className={cn(\"geo-map-popup\", popupClassName)}\n          closeButton\n          closeOnEscapeKey\n          minWidth={0}\n          maxWidth={288}\n          eventHandlers={popupEventHandlers}\n        >\n          <GeoMapPopupContent label={label} description={description} />\n        </Popup>\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/geo-map/geo-map-theme.module.css",
    "content": ".root[data-slot=\"geo-map\"] {\n  --geo-map-canvas-bg: var(--muted);\n  --geo-map-tooltip-bg: var(--foreground);\n  --geo-map-tooltip-fg: var(--background);\n  --geo-map-tooltip-shadow: 0 8px 20px\n    oklch(from var(--foreground) l c h / 0.18);\n  --geo-map-tooltip-radius: calc(var(--radius) - 2px);\n  --geo-map-tooltip-padding: 0.375rem 0.625rem;\n  --geo-map-tooltip-font-size: 0.75rem;\n  --geo-map-tooltip-font-weight: 500;\n  --geo-map-tooltip-line-height: 1.2;\n  --geo-map-popup-margin-bottom: 12px;\n  --geo-map-popup-border: var(--border);\n  --geo-map-popup-radius: calc(var(--radius) + 2px);\n  --geo-map-popup-bg: oklch(from var(--popover) l c h / 0.96);\n  --geo-map-popup-fg: var(--popover-foreground);\n  --geo-map-popup-shadow: 0 10px 30px oklch(from var(--foreground) l c h / 0.12);\n  --geo-map-popup-blur: 8px;\n  --geo-map-popup-content-padding: 0.625rem 0.75rem;\n  --geo-map-popup-max-width: min(80vw, 18rem);\n  --geo-map-popup-font-family: var(\n    --font-sans,\n    ui-sans-serif,\n    system-ui,\n    sans-serif\n  );\n  --geo-map-zoom-bg: oklch(from var(--background) l c h / 0.78);\n  --geo-map-zoom-fg: var(--foreground);\n  --geo-map-zoom-border: var(--border);\n  --geo-map-zoom-hover-bg: oklch(from var(--accent) l c h / 0.82);\n  --geo-map-zoom-hover-fg: var(--accent-foreground);\n  --geo-map-zoom-disabled-bg: oklch(from var(--muted) l c h / 0.72);\n  --geo-map-zoom-disabled-fg: var(--muted-foreground);\n  --geo-map-zoom-shadow: 0 1px 2px oklch(from var(--foreground) l c h / 0.08);\n  --geo-map-zoom-focus-ring: var(--ring);\n  --geo-map-zoom-radius: 0.5rem;\n  --geo-map-zoom-size: 2.25rem;\n  --geo-map-zoom-font-size: 1.125rem;\n}\n\n.root[data-slot=\"geo-map\"] :global(.leaflet-container) {\n  background: var(--geo-map-canvas-bg);\n}\n\n.root[data-slot=\"geo-map\"] :global(.leaflet-control-zoom) {\n  border: 1px solid var(--geo-map-zoom-border);\n  box-shadow: var(--geo-map-zoom-shadow);\n  background: var(--geo-map-zoom-bg);\n  backdrop-filter: blur(var(--geo-map-popup-blur));\n  -webkit-backdrop-filter: blur(var(--geo-map-popup-blur));\n}\n\n.root[data-slot=\"geo-map\"] :global(.leaflet-control-zoom.leaflet-bar) {\n  border-radius: var(--geo-map-zoom-radius) !important;\n}\n\n.root[data-slot=\"geo-map\"] :global(.leaflet-control-zoom a) {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: var(--geo-map-zoom-size);\n  height: var(--geo-map-zoom-size);\n  line-height: 1;\n  text-indent: 0;\n  border: 0;\n  background: transparent;\n  color: var(--geo-map-zoom-fg);\n  font-size: var(--geo-map-zoom-font-size);\n  font-weight: 500;\n  box-shadow: none;\n  cursor: default;\n  transition:\n    background-color 150ms ease,\n    color 150ms ease,\n    border-color 150ms ease,\n    box-shadow 150ms ease,\n    opacity 150ms ease;\n  border-radius: 0 !important;\n}\n\n.root[data-slot=\"geo-map\"] :global(.leaflet-control-zoom a + a) {\n  border-top: 1px solid var(--geo-map-zoom-border);\n}\n\n.root[data-slot=\"geo-map\"] :global(.leaflet-control-zoom a:first-child),\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-touch .leaflet-control-zoom a:first-child),\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-control-zoom .leaflet-control-zoom-in) {\n  border-radius: var(--geo-map-zoom-radius) var(--geo-map-zoom-radius) 0 0 !important;\n}\n\n.root[data-slot=\"geo-map\"] :global(.leaflet-control-zoom a:last-child),\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-touch .leaflet-control-zoom a:last-child),\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-control-zoom .leaflet-control-zoom-out) {\n  border-top: 0;\n  border-radius: 0 0 var(--geo-map-zoom-radius) var(--geo-map-zoom-radius) !important;\n}\n\n.root[data-slot=\"geo-map\"] :global(.leaflet-control-zoom a:hover) {\n  background: var(--geo-map-zoom-hover-bg);\n  color: var(--geo-map-zoom-hover-fg);\n}\n\n.root[data-slot=\"geo-map\"] :global(.leaflet-control-zoom a:focus),\n.root[data-slot=\"geo-map\"] :global(.leaflet-control-zoom a:focus-visible) {\n  position: relative;\n  z-index: 1;\n  outline: 2px solid var(--geo-map-zoom-focus-ring);\n  outline-offset: 1px;\n}\n\n.root[data-slot=\"geo-map\"] :global(.leaflet-control-zoom a.leaflet-disabled),\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-control-zoom a.leaflet-disabled:hover) {\n  background: var(--geo-map-zoom-disabled-bg);\n  color: var(--geo-map-zoom-disabled-fg);\n  opacity: 0.55;\n}\n\n.root[data-slot=\"geo-map\"] :global(.leaflet-tooltip.geo-map-tooltip) {\n  border: 0;\n  border-radius: var(--geo-map-tooltip-radius);\n  background: var(--geo-map-tooltip-bg);\n  color: var(--geo-map-tooltip-fg);\n  box-shadow: var(--geo-map-tooltip-shadow);\n  font-size: var(--geo-map-tooltip-font-size);\n  font-weight: var(--geo-map-tooltip-font-weight);\n  line-height: var(--geo-map-tooltip-line-height);\n  padding: var(--geo-map-tooltip-padding);\n}\n\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-tooltip-top.geo-map-tooltip::before) {\n  border-top-color: var(--geo-map-tooltip-bg);\n}\n\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-tooltip-bottom.geo-map-tooltip::before) {\n  border-bottom-color: var(--geo-map-tooltip-bg);\n}\n\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-tooltip-left.geo-map-tooltip::before) {\n  border-left-color: var(--geo-map-tooltip-bg);\n}\n\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-tooltip-right.geo-map-tooltip::before) {\n  border-right-color: var(--geo-map-tooltip-bg);\n}\n\n.root[data-slot=\"geo-map\"] :global(.leaflet-popup.geo-map-popup) {\n  margin-bottom: var(--geo-map-popup-margin-bottom);\n}\n\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-popup.geo-map-popup .leaflet-popup-content-wrapper) {\n  border: 1px solid var(--geo-map-popup-border);\n  border-radius: var(--geo-map-popup-radius);\n  background: var(--geo-map-popup-bg);\n  color: var(--geo-map-popup-fg);\n  box-shadow: var(--geo-map-popup-shadow);\n  backdrop-filter: blur(var(--geo-map-popup-blur));\n  -webkit-backdrop-filter: blur(var(--geo-map-popup-blur));\n  padding: 0;\n}\n\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-popup.geo-map-popup .leaflet-popup-content) {\n  margin: 0;\n  min-width: 0;\n  width: max-content;\n  max-width: var(--geo-map-popup-max-width);\n  padding: var(--geo-map-popup-content-padding);\n  font-family: var(--geo-map-popup-font-family);\n}\n\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-popup.geo-map-popup .leaflet-popup-content p) {\n  margin: 0;\n}\n\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-popup.geo-map-popup .leaflet-popup-tip-container) {\n  display: none;\n}\n\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-popup.geo-map-popup .leaflet-popup-close-button) {\n  color: var(--geo-map-popup-fg);\n  opacity: 0.75;\n  top: 0.25rem;\n  right: 0.25rem;\n  width: 1.5rem;\n  height: 1.5rem;\n  font-size: 1rem;\n  line-height: 1.5rem;\n  border-radius: calc(var(--radius) - 2px);\n}\n\n.root[data-slot=\"geo-map\"]\n  :global(.leaflet-popup.geo-map-popup .leaflet-popup-close-button:hover) {\n  opacity: 1;\n  background: oklch(from var(--muted) l c h / 0.65);\n}\n\n.root[data-slot=\"geo-map\"]\n  :global(\n    .leaflet-popup.geo-map-popup .leaflet-popup-close-button:focus-visible\n  ) {\n  outline: 2px solid var(--ring);\n  outline-offset: 1px;\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/geo-map/geo-map.tsx",
    "content": "\"use client\";\n\nimport { memo, useEffect, useState } from \"react\";\nimport { cn } from \"./_adapter\";\nimport { GeoMapEngine } from \"./geo-map-engine\";\nimport styles from \"./geo-map-theme.module.css\";\nimport type { GeoMapProps, GeoMapStyle } from \"./schema\";\n\nconst LIGHT_TILE_URL =\n  \"https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png\";\nconst DARK_TILE_URL =\n  \"https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png\";\n\nfunction getSystemTheme(): \"light\" | \"dark\" {\n  if (typeof window === \"undefined\") return \"light\";\n  return window.matchMedia?.(\"(prefers-color-scheme: dark)\").matches\n    ? \"dark\"\n    : \"light\";\n}\n\nfunction getDocumentTheme(): \"light\" | \"dark\" | null {\n  if (typeof document === \"undefined\") return null;\n\n  const root = document.documentElement;\n  const dataTheme = root.getAttribute(\"data-theme\")?.toLowerCase();\n  if (dataTheme === \"dark\") return \"dark\";\n  if (dataTheme === \"light\") return \"light\";\n  if (root.classList.contains(\"dark\")) return \"dark\";\n  if (root.classList.contains(\"light\")) return \"light\";\n\n  return null;\n}\n\nfunction useInheritedTheme(): \"light\" | \"dark\" {\n  const [theme, setTheme] = useState<\"light\" | \"dark\">(() => {\n    return getDocumentTheme() ?? getSystemTheme();\n  });\n\n  useEffect(() => {\n    if (typeof window === \"undefined\" || typeof document === \"undefined\") {\n      return;\n    }\n\n    const update = () => setTheme(getDocumentTheme() ?? getSystemTheme());\n\n    const mql = window.matchMedia?.(\"(prefers-color-scheme: dark)\");\n    mql?.addEventListener(\"change\", update);\n\n    const observer = new MutationObserver(update);\n    observer.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\", \"data-theme\"],\n    });\n\n    return () => {\n      mql?.removeEventListener(\"change\", update);\n      observer.disconnect();\n    };\n  }, []);\n\n  return theme;\n}\n\nfunction resolveMapAriaLabel(title?: string, description?: string): string {\n  if (title && description) {\n    return `${title}. ${description}`;\n  }\n\n  return title ?? description ?? \"Geographic map\";\n}\n\nexport const GeoMap = memo(function GeoMap({\n  id,\n  role: _role,\n  receipt: _receipt,\n  title,\n  description,\n  markers,\n  routes,\n  clustering,\n  viewport,\n  showZoomControl = true,\n  theme,\n  className,\n  style,\n  tooltipClassName,\n  popupClassName,\n  onMarkerClick,\n  onRouteClick,\n}: GeoMapProps) {\n  const inheritedTheme = useInheritedTheme();\n  const resolvedTheme = theme ?? inheritedTheme;\n  const [isMapReady, setIsMapReady] = useState(false);\n  const tileUrl = resolvedTheme === \"dark\" ? DARK_TILE_URL : LIGHT_TILE_URL;\n  const mapAriaLabel = resolveMapAriaLabel(title, description);\n  const resolvedRootStyle: GeoMapStyle = {\n    \"--geo-map-canvas-bg\":\n      resolvedTheme === \"dark\" ? \"var(--background)\" : \"var(--muted)\",\n    ...style,\n  };\n\n  return (\n    <div\n      className={cn(\"w-full min-w-80\", styles.root, className)}\n      style={resolvedRootStyle}\n      data-slot=\"geo-map\"\n      data-tool-ui-id={id}\n    >\n      <div\n        className=\"bg-muted/20 relative h-[320px] w-full overflow-hidden rounded-lg border\"\n        role=\"region\"\n        aria-label={mapAriaLabel}\n      >\n        <GeoMapEngine\n          id={id}\n          markers={markers}\n          routes={routes}\n          clustering={clustering}\n          viewport={viewport}\n          showZoomControl={showZoomControl}\n          tileUrl={tileUrl}\n          mapAriaLabel={mapAriaLabel}\n          tooltipClassName={tooltipClassName}\n          popupClassName={popupClassName}\n          onMarkerClick={onMarkerClick}\n          onRouteClick={onRouteClick}\n          onReadyChange={setIsMapReady}\n        />\n\n        {(title || description) && (\n          <div\n            className={cn(\n              \"pointer-events-none absolute top-3 left-3 z-[900]\",\n              \"max-w-[min(75%,22rem)] rounded-lg border border-border/70 bg-background/70 px-3 py-2\",\n              \"shadow-sm backdrop-blur-md\",\n            )}\n          >\n            {title && (\n              <p className=\"text-foreground text-sm leading-tight font-semibold\">\n                {title}\n              </p>\n            )}\n            {description && (\n              <p className=\"text-muted-foreground mt-1 text-xs leading-snug\">\n                {description}\n              </p>\n            )}\n          </div>\n        )}\n\n        {!isMapReady && (\n          <div\n            data-slot=\"geo-map-loading\"\n            className=\"bg-muted/30 text-muted-foreground pointer-events-none absolute inset-0 flex items-center justify-center\"\n          >\n            <span data-slot=\"geo-map-loading-label\">Loading map...</span>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n});\n"
  },
  {
    "path": "apps/www/components/tool-ui/geo-map/index.tsx",
    "content": "export { GeoMap } from \"./geo-map\";\nexport {\n  type GeoMapClustering,\n  type GeoMapFitTarget,\n  type GeoMapMarker,\n  type GeoMapMarkerIcon,\n  type GeoMapStyle,\n  type GeoMapRoute,\n  type GeoMapViewport,\n  type GeoMapProps,\n  type GeoMapClientProps,\n  type SerializableGeoMap,\n} from \"./schema\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/geo-map/schema.ts",
    "content": "import { z } from \"zod\";\nimport type { CSSProperties } from \"react\";\nimport { defineToolUiContract } from \"../shared/contract\";\nimport {\n  ToolUIIdSchema,\n  ToolUIReceiptSchema,\n  ToolUIRoleSchema,\n} from \"../shared/schema\";\n\nconst LatitudeSchema = z.number().finite().min(-90).max(90);\nconst LongitudeSchema = z.number().finite().min(-180).max(180);\nconst HttpUrlSchema = z\n  .string()\n  .url()\n  .refine((value) => /^https?:\\/\\//i.test(value), {\n    message: \"Expected an http or https URL.\",\n  });\n\nconst GeoMapMarkerIconDotSchema = z.object({\n  type: z.literal(\"dot\"),\n  color: z.string().optional(),\n  borderColor: z.string().optional(),\n  radius: z.number().min(3).max(16).optional(),\n});\n\nconst GeoMapMarkerIconEmojiSchema = z.object({\n  type: z.literal(\"emoji\"),\n  value: z.string().min(1),\n  size: z.number().min(16).max(40).optional(),\n  bgColor: z.string().optional(),\n  borderColor: z.string().optional(),\n});\n\nconst GeoMapMarkerIconImageSchema = z.object({\n  type: z.literal(\"image\"),\n  url: HttpUrlSchema,\n  width: z.number().min(16).max(64).optional(),\n  height: z.number().min(16).max(64).optional(),\n  borderRadius: z.number().min(0).max(999).optional(),\n  borderColor: z.string().optional(),\n});\n\nexport const GeoMapMarkerIconSchema = z.union([\n  GeoMapMarkerIconDotSchema,\n  GeoMapMarkerIconEmojiSchema,\n  GeoMapMarkerIconImageSchema,\n]);\n\nexport type GeoMapMarkerIcon = z.infer<typeof GeoMapMarkerIconSchema>;\n\nexport const GeoMapMarkerSchema = z.object({\n  id: z.string().min(1).optional(),\n  lat: LatitudeSchema,\n  lng: LongitudeSchema,\n  label: z.string().optional(),\n  description: z.string().optional(),\n  tooltip: z.enum([\"none\", \"hover\", \"always\"]).optional(),\n  icon: GeoMapMarkerIconSchema.optional(),\n});\n\nexport type GeoMapMarker = z.infer<typeof GeoMapMarkerSchema>;\n\nexport const GeoMapRoutePointSchema = z.object({\n  lat: LatitudeSchema,\n  lng: LongitudeSchema,\n});\n\nexport const GeoMapRouteSchema = z.object({\n  id: z.string().min(1).optional(),\n  points: z.array(GeoMapRoutePointSchema).min(2),\n  label: z.string().optional(),\n  description: z.string().optional(),\n  tooltip: z.enum([\"none\", \"hover\", \"always\"]).optional(),\n  color: z.string().optional(),\n  weight: z.number().min(1).max(12).optional(),\n  opacity: z.number().min(0).max(1).optional(),\n  dashArray: z.string().optional(),\n});\n\nexport type GeoMapRoute = z.infer<typeof GeoMapRouteSchema>;\n\nexport const GeoMapClusteringSchema = z.object({\n  enabled: z.boolean().optional(),\n  radius: z.number().min(20).max(120).optional(),\n  maxZoom: z.number().min(1).max(22).optional(),\n  minPoints: z.number().min(2).max(20).optional(),\n});\n\nexport type GeoMapClustering = z.infer<typeof GeoMapClusteringSchema>;\n\nexport const GeoMapFitTargetSchema = z.enum([\"markers\", \"routes\", \"all\"]);\nexport type GeoMapFitTarget = z.infer<typeof GeoMapFitTargetSchema>;\n\nconst GeoMapFitViewportSchema = z.object({\n  mode: z.literal(\"fit\"),\n  padding: z.number().nonnegative().optional(),\n  maxZoom: z.number().min(1).max(22).optional(),\n  target: GeoMapFitTargetSchema.optional(),\n});\n\nconst GeoMapCenterViewportSchema = z.object({\n  mode: z.literal(\"center\"),\n  center: z.object({\n    lat: LatitudeSchema,\n    lng: LongitudeSchema,\n  }),\n  zoom: z.number().min(1).max(22),\n});\n\nexport const GeoMapViewportSchema = z.union([\n  GeoMapFitViewportSchema,\n  GeoMapCenterViewportSchema,\n]);\n\nexport type GeoMapViewport = z.infer<typeof GeoMapViewportSchema>;\n\nexport const GeoMapPropsSchema = z\n  .object({\n    id: ToolUIIdSchema,\n    role: ToolUIRoleSchema.optional(),\n    receipt: ToolUIReceiptSchema.optional(),\n    title: z.string().optional(),\n    description: z.string().optional(),\n    markers: z.array(GeoMapMarkerSchema).min(1),\n    routes: z.array(GeoMapRouteSchema).optional(),\n    clustering: GeoMapClusteringSchema.optional(),\n    viewport: GeoMapViewportSchema.optional(),\n    showZoomControl: z.boolean().optional(),\n    theme: z.enum([\"light\", \"dark\"]).optional(),\n  })\n  .superRefine((value, ctx) => {\n    const seenMarkerIds = new Set<string>();\n\n    value.markers.forEach((marker, index) => {\n      if (!marker.id) {\n        return;\n      }\n\n      if (seenMarkerIds.has(marker.id)) {\n        ctx.addIssue({\n          code: \"custom\",\n          path: [\"markers\", index, \"id\"],\n          message: `Duplicate marker id \"${marker.id}\".`,\n        });\n        return;\n      }\n\n      seenMarkerIds.add(marker.id);\n    });\n\n    const seenRouteIds = new Set<string>();\n    value.routes?.forEach((route, index) => {\n      if (!route.id) {\n        return;\n      }\n\n      if (seenRouteIds.has(route.id)) {\n        ctx.addIssue({\n          code: \"custom\",\n          path: [\"routes\", index, \"id\"],\n          message: `Duplicate route id \"${route.id}\".`,\n        });\n        return;\n      }\n\n      seenRouteIds.add(route.id);\n    });\n  });\n\nexport type GeoMapStyle = CSSProperties &\n  Partial<Record<`--${string}`, string | number>>;\n\nexport type GeoMapClientProps = {\n  className?: string;\n  style?: GeoMapStyle;\n  tooltipClassName?: string;\n  popupClassName?: string;\n  onMarkerClick?: (marker: GeoMapMarker) => void;\n  onRouteClick?: (route: GeoMapRoute) => void;\n};\n\nexport type GeoMapProps = z.infer<typeof GeoMapPropsSchema> & GeoMapClientProps;\n\nexport const SerializableGeoMapSchema = GeoMapPropsSchema;\n\nexport type SerializableGeoMap = z.infer<typeof SerializableGeoMapSchema>;\n\nconst SerializableGeoMapSchemaContract = defineToolUiContract(\n  \"GeoMap\",\n  SerializableGeoMapSchema,\n);\n\nexport const parseSerializableGeoMap: (input: unknown) => SerializableGeoMap =\n  SerializableGeoMapSchemaContract.parse;\n\nexport const safeParseSerializableGeoMap: (\n  input: unknown,\n) => SerializableGeoMap | null = SerializableGeoMapSchemaContract.safeParse;\n"
  },
  {
    "path": "apps/www/components/tool-ui/image/README.md",
    "content": "# Image\n\nImplementation for the \"image\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/image/index.ts\n- serializable schema + parse helpers: components/tool-ui/image/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/image/content.mdx\n- Preset payload: lib/presets/image.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/image/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n */\n\"use client\";\n\nexport { cn } from \"@/lib/utils\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/image/image.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"./_adapter\";\n\nimport {\n  RATIO_CLASS_MAP,\n  getFitClass,\n  openSafeNavigationHref,\n  resolveSafeNavigationHref,\n  sanitizeHref,\n} from \"../shared/media\";\nimport type { SerializableImage, Source } from \"./schema\";\n\nconst FALLBACK_LOCALE = \"en-US\";\n\nexport interface ImageProps extends SerializableImage {\n  className?: string;\n  onNavigate?: (href: string, image: SerializableImage) => void;\n}\n\nexport function Image(props: ImageProps) {\n  const { className, onNavigate, ...serializable } = props;\n\n  const {\n    id,\n    src,\n    alt,\n    title,\n    href: rawHref,\n    domain,\n    ratio = \"auto\",\n    fit = \"cover\",\n    source,\n    locale: providedLocale,\n  } = serializable;\n\n  const locale = providedLocale ?? FALLBACK_LOCALE;\n  const sanitizedHref = sanitizeHref(rawHref);\n  const resolvedSourceUrl = sanitizeHref(source?.url);\n\n  const imageData: SerializableImage = {\n    ...serializable,\n    href: sanitizedHref,\n    source: source ? { ...source, url: resolvedSourceUrl } : undefined,\n    locale,\n  };\n\n  const sourceLabel = source?.label ?? domain;\n  const fallbackInitial = (sourceLabel ?? \"\").trim().charAt(0).toUpperCase();\n  const hasSource = Boolean(sourceLabel || source?.iconUrl);\n\n  const handleSourceClick = (event: React.MouseEvent<HTMLButtonElement>) => {\n    event.preventDefault();\n    event.stopPropagation();\n    const targetUrl = resolveSafeNavigationHref(\n      resolvedSourceUrl,\n      source?.url,\n      sanitizedHref,\n      src,\n    );\n    if (!targetUrl) return;\n    if (onNavigate) {\n      onNavigate(targetUrl, imageData);\n    } else {\n      openSafeNavigationHref(targetUrl);\n    }\n  };\n\n  const handleImageClick = () => {\n    if (!sanitizedHref) return;\n    if (onNavigate) {\n      onNavigate(sanitizedHref, imageData);\n    } else {\n      openSafeNavigationHref(sanitizedHref);\n    }\n  };\n\n  const hasMetadata = title || hasSource;\n\n  return (\n    <article\n      className={cn(\"relative w-full max-w-md min-w-80\", className)}\n      lang={locale}\n      data-tool-ui-id={id}\n      data-slot=\"image\"\n    >\n      <div\n        className={cn(\n          \"group @container relative isolate flex w-full min-w-0 flex-col overflow-hidden rounded-xl\",\n          \"border-border bg-card border text-sm shadow-xs\",\n        )}\n      >\n        <>\n          <div\n            className={cn(\n              \"bg-muted group relative w-full overflow-hidden\",\n              ratio !== \"auto\" ? RATIO_CLASS_MAP[ratio] : \"min-h-[160px]\",\n              sanitizedHref && \"cursor-pointer\",\n            )}\n            onClick={sanitizedHref ? handleImageClick : undefined}\n            role={sanitizedHref ? \"link\" : undefined}\n            tabIndex={sanitizedHref ? 0 : undefined}\n            onKeyDown={\n              sanitizedHref\n                ? (e) => {\n                    if (e.key === \"Enter\" || e.key === \" \") {\n                      e.preventDefault();\n                      handleImageClick();\n                    }\n                  }\n                : undefined\n            }\n          >\n            <img\n              src={src}\n              alt={alt}\n              loading=\"lazy\"\n              decoding=\"async\"\n              className={cn(\"absolute inset-0 h-full w-full\", getFitClass(fit))}\n            />\n          </div>\n          {hasMetadata && (\n            <div className=\"flex items-center gap-3 px-4 py-3\">\n              <SourceAttribution\n                source={source}\n                sourceLabel={sourceLabel}\n                fallbackInitial={fallbackInitial}\n                hasClickableUrl={Boolean(resolvedSourceUrl)}\n                onSourceClick={handleSourceClick}\n                title={title}\n              />\n            </div>\n          )}\n        </>\n      </div>\n    </article>\n  );\n}\n\ninterface SourceAttributionProps {\n  source?: Source;\n  sourceLabel?: string;\n  fallbackInitial: string;\n  hasClickableUrl: boolean;\n  onSourceClick: (event: React.MouseEvent<HTMLButtonElement>) => void;\n  title?: string;\n}\n\nfunction SourceAttribution({\n  source,\n  sourceLabel,\n  fallbackInitial,\n  hasClickableUrl,\n  onSourceClick,\n  title,\n}: SourceAttributionProps) {\n  const hasSource = Boolean(sourceLabel || source?.iconUrl);\n\n  const content = (\n    <div className=\"flex min-w-0 flex-1 items-center gap-3\">\n      {source?.iconUrl ? (\n        <img\n          src={source.iconUrl}\n          alt=\"\"\n          aria-hidden=\"true\"\n          width={32}\n          height={32}\n          className=\"size-8 shrink-0 rounded-full object-cover\"\n          loading=\"lazy\"\n          decoding=\"async\"\n        />\n      ) : fallbackInitial ? (\n        <div className=\"bg-muted text-muted-foreground flex size-8 shrink-0 items-center justify-center rounded-full text-xs font-semibold uppercase\">\n          {fallbackInitial}\n        </div>\n      ) : null}\n      <div className=\"min-w-0 flex-1\">\n        {title && (\n          <div className=\"text-foreground line-clamp-1 text-sm font-medium\">\n            {title}\n          </div>\n        )}\n        {sourceLabel && (\n          <div className=\"text-muted-foreground line-clamp-1 text-xs\">\n            {sourceLabel}\n          </div>\n        )}\n      </div>\n    </div>\n  );\n\n  if (hasClickableUrl && hasSource) {\n    return (\n      <button\n        type=\"button\"\n        onClick={onSourceClick}\n        className=\"focus-visible:ring-ring flex w-full items-center gap-3 text-left hover:opacity-80 focus-visible:ring-2 focus-visible:outline-none\"\n      >\n        {content}\n      </button>\n    );\n  }\n\n  return <div className=\"flex w-full items-center gap-3\">{content}</div>;\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/image/index.ts",
    "content": "export { Image } from \"./image\";\nexport type { ImageProps } from \"./image\";\nexport type { SerializableImage, Source } from \"./schema\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/image/schema.ts",
    "content": "import { z } from \"zod\";\nimport { defineToolUiContract } from \"../shared/contract\";\nimport {\n  ToolUIIdSchema,\n  ToolUIReceiptSchema,\n  ToolUIRoleSchema,\n} from \"../shared/schema\";\n\nimport { AspectRatioSchema, MediaFitSchema } from \"../shared/media\";\n\nexport const SourceSchema = z.object({\n  label: z.string(),\n  iconUrl: z.url().optional(),\n  url: z.url().optional(),\n});\n\nexport type Source = z.infer<typeof SourceSchema>;\n\nexport const SerializableImageSchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  receipt: ToolUIReceiptSchema.optional(),\n  assetId: z.string(),\n  src: z.url(),\n  alt: z.string().min(1, \"Images require alt text for accessibility\"),\n  title: z.string().optional(),\n  description: z.string().optional(),\n  href: z.url().optional(),\n  domain: z.string().optional(),\n  ratio: AspectRatioSchema.optional(),\n  fit: MediaFitSchema.optional(),\n  fileSizeBytes: z.number().int().positive().optional(),\n  createdAt: z.string().datetime().optional(),\n  locale: z.string().optional(),\n  source: SourceSchema.optional(),\n});\n\nexport type SerializableImage = z.infer<typeof SerializableImageSchema>;\n\nconst SerializableImageSchemaContract = defineToolUiContract(\n  \"Image\",\n  SerializableImageSchema,\n);\n\nexport const parseSerializableImage: (input: unknown) => SerializableImage =\n  SerializableImageSchemaContract.parse;\n\nexport const safeParseSerializableImage: (\n  input: unknown,\n) => SerializableImage | null = SerializableImageSchemaContract.safeParse;\n"
  },
  {
    "path": "apps/www/components/tool-ui/image-gallery/README.md",
    "content": "# Image Gallery\n\nImplementation for the \"image-gallery\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/image-gallery/index.tsx\n- serializable schema + parse helpers: components/tool-ui/image-gallery/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/image-gallery/content.mdx\n- Preset payload: lib/presets/image-gallery.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/image-gallery/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn     → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Button → shadcn/ui Button\n */\n\nexport { cn } from \"@/lib/utils\";\nexport { Button } from \"@/components/ui/button\";\nexport { ChevronLeft, ChevronRight, X, ImageOff } from \"lucide-react\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/image-gallery/context.tsx",
    "content": "\"use client\";\n\nimport {\n  createContext,\n  use,\n  useState,\n  useCallback,\n  useMemo,\n  useRef,\n} from \"react\";\nimport { flushSync } from \"react-dom\";\nimport type { ImageGalleryItem } from \"./schema\";\n\nconst VIEW_TRANSITION_NAME = \"active-gallery-image\";\n\ninterface ImageGalleryContextValue {\n  images: ImageGalleryItem[];\n  activeIndex: number | null;\n  openLightbox: (index: number) => void;\n  closeLightbox: () => void;\n  registerImage: (id: string, element: HTMLElement | null) => void;\n  lightboxContentRef: React.RefObject<HTMLDivElement | null>;\n  setDialogRef: (element: HTMLDialogElement | null) => void;\n}\n\nconst ImageGalleryContext = createContext<ImageGalleryContextValue | null>(\n  null,\n);\n\nexport function useImageGallery(): ImageGalleryContextValue {\n  const context = use(ImageGalleryContext);\n  if (!context) {\n    throw new Error(\"useImageGallery must be used within ImageGalleryProvider\");\n  }\n  return context;\n}\n\nfunction supportsViewTransitions(): boolean {\n  return (\n    typeof document !== \"undefined\" &&\n    \"startViewTransition\" in document &&\n    typeof window !== \"undefined\" &&\n    !window.matchMedia?.(\"(prefers-reduced-motion: reduce)\")?.matches\n  );\n}\n\nfunction withViewTransition(\n  element: HTMLElement,\n  domUpdate: () => void,\n  onFinished?: () => void,\n): void {\n  if (!supportsViewTransitions()) {\n    domUpdate();\n    onFinished?.();\n    return;\n  }\n\n  element.style.viewTransitionName = VIEW_TRANSITION_NAME;\n\n  const transition = document.startViewTransition(() => domUpdate());\n\n  transition.finished.finally(() => {\n    element.style.removeProperty(\"view-transition-name\");\n    onFinished?.();\n  });\n}\n\ninterface ImageGalleryProviderProps {\n  images: ImageGalleryItem[];\n  children: React.ReactNode;\n}\n\nexport function ImageGalleryProvider({\n  images,\n  children,\n}: ImageGalleryProviderProps) {\n  const [activeIndex, setActiveIndex] = useState<number | null>(null);\n\n  const imageElementsRef = useRef<Map<string, HTMLElement>>(new Map());\n  const lightboxContentRef = useRef<HTMLDivElement>(null);\n  const dialogRef = useRef<HTMLDialogElement | null>(null);\n  const originalParentRef = useRef<HTMLElement | null>(null);\n\n  const registerImage = useCallback(\n    (id: string, element: HTMLElement | null) => {\n      if (element) {\n        imageElementsRef.current.set(id, element);\n      } else {\n        imageElementsRef.current.delete(id);\n      }\n    },\n    [],\n  );\n\n  const setDialogRef = useCallback((element: HTMLDialogElement | null) => {\n    dialogRef.current = element;\n  }, []);\n\n  const openLightbox = useCallback(\n    (index: number) => {\n      const image = images[index];\n      if (!image) return;\n\n      const imageElement = imageElementsRef.current.get(image.id);\n      const container = lightboxContentRef.current;\n      const dialog = dialogRef.current;\n\n      if (!imageElement || !container || !dialog) {\n        setActiveIndex(index);\n        dialog?.showModal();\n        return;\n      }\n\n      originalParentRef.current = imageElement.parentElement;\n\n      withViewTransition(imageElement, () => {\n        container.appendChild(imageElement);\n        flushSync(() => setActiveIndex(index));\n        dialog.showModal();\n      });\n    },\n    [images],\n  );\n\n  const closeLightbox = useCallback(() => {\n    if (activeIndex === null) return;\n\n    const image = images[activeIndex];\n    const dialog = dialogRef.current;\n\n    if (!image) {\n      setActiveIndex(null);\n      dialog?.close();\n      return;\n    }\n\n    const imageElement = imageElementsRef.current.get(image.id);\n    const originalParent = originalParentRef.current;\n\n    if (!imageElement || !originalParent) {\n      setActiveIndex(null);\n      dialog?.close();\n      return;\n    }\n\n    withViewTransition(\n      imageElement,\n      () => {\n        originalParent.appendChild(imageElement);\n        flushSync(() => setActiveIndex(null));\n        dialog?.close();\n      },\n      () => {\n        originalParentRef.current = null;\n      },\n    );\n  }, [activeIndex, images]);\n\n  const value = useMemo<ImageGalleryContextValue>(\n    () => ({\n      images,\n      activeIndex,\n      openLightbox,\n      closeLightbox,\n      registerImage,\n      lightboxContentRef,\n      setDialogRef,\n    }),\n    [\n      images,\n      activeIndex,\n      openLightbox,\n      closeLightbox,\n      registerImage,\n      setDialogRef,\n    ],\n  );\n\n  return (\n    <ImageGalleryContext.Provider value={value}>\n      {children}\n    </ImageGalleryContext.Provider>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/image-gallery/gallery-grid.tsx",
    "content": "\"use client\";\n\nimport { useState, useCallback, useEffect, useRef } from \"react\";\nimport { cn, ImageOff } from \"./_adapter\";\nimport { useImageGallery } from \"./context\";\nimport type { ImageGalleryItem } from \"./schema\";\n\ntype GridImage = Pick<\n  ImageGalleryItem,\n  \"id\" | \"src\" | \"alt\" | \"width\" | \"height\"\n>;\n\ninterface GalleryGridProps {\n  onImageClick?: (imageId: string) => void;\n}\n\nexport function GalleryGrid({ onImageClick }: GalleryGridProps) {\n  const { images, openLightbox } = useImageGallery();\n\n  const handleOpen = useCallback(\n    (index: number) => {\n      const image = images[index];\n      if (image && onImageClick) {\n        onImageClick(image.id);\n      }\n      openLightbox(index);\n    },\n    [images, onImageClick, openLightbox],\n  );\n\n  return (\n    <div\n      className=\"grid grid-cols-2 gap-2 @md:grid-cols-3 @lg:grid-cols-4\"\n      role=\"list\"\n    >\n      {images.map((image, index) => (\n        <GridImageCard\n          key={image.id}\n          image={image}\n          index={index}\n          onClick={handleOpen}\n        />\n      ))}\n    </div>\n  );\n}\n\ninterface GridImageCardProps {\n  image: GridImage;\n  index: number;\n  onClick: (index: number) => void;\n}\n\nfunction GridImageCard({ image, index, onClick }: GridImageCardProps) {\n  const [hasError, setHasError] = useState(false);\n  const wrapperRef = useRef<HTMLDivElement>(null);\n\n  const { registerImage } = useImageGallery();\n\n  const shouldSpanTwoRows = isPortraitImage(image);\n\n  useEffect(() => {\n    const wrapper = wrapperRef.current;\n    const img = wrapper?.querySelector(\"img\");\n    if (img) {\n      registerImage(image.id, img);\n    }\n    return () => {\n      registerImage(image.id, null);\n    };\n  }, [image.id, registerImage]);\n\n  const handleClick = useCallback(() => {\n    onClick(index);\n  }, [onClick, index]);\n\n  return (\n    <div\n      role=\"listitem\"\n      className={cn(\n        \"group relative cursor-pointer\",\n        shouldSpanTwoRows && \"row-span-2\",\n      )}\n      style={{ aspectRatio: shouldSpanTwoRows ? undefined : \"1 / 1\" }}\n    >\n      <button\n        type=\"button\"\n        onClick={handleClick}\n        className=\"absolute inset-0 z-20 h-full w-full rounded-lg outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2\"\n        aria-label={image.alt}\n      />\n\n      <div\n        ref={wrapperRef}\n        className=\"bg-muted relative h-full w-full overflow-hidden rounded-lg transition-transform duration-200 ease-[cubic-bezier(0.4,0,0.2,1)] group-hover:scale-[1.02] group-active:scale-[0.98]\"\n      >\n        {hasError ? (\n          <ImageErrorState alt={image.alt} />\n        ) : (\n          <img\n            src={image.src}\n            alt={image.alt}\n            width={image.width}\n            height={image.height}\n            loading=\"lazy\"\n            decoding=\"async\"\n            draggable={false}\n            onError={() => setHasError(true)}\n            className=\"h-full w-full object-cover\"\n          />\n        )}\n      </div>\n    </div>\n  );\n}\n\nfunction isPortraitImage(image: GridImage): boolean {\n  const aspectRatio = image.width / image.height;\n  const isPortrait = aspectRatio < 1;\n  const isSquarish = aspectRatio >= 0.9 && aspectRatio <= 1.1;\n  return isPortrait && !isSquarish;\n}\n\nfunction ImageErrorState({ alt }: { alt: string }) {\n  return (\n    <div className=\"absolute inset-0 flex flex-col items-center justify-center gap-2 p-4\">\n      <ImageOff className=\"text-muted-foreground h-8 w-8\" />\n      <span className=\"text-muted-foreground line-clamp-2 text-center text-xs\">\n        {alt}\n      </span>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/image-gallery/gallery-lightbox.tsx",
    "content": "\"use client\";\n\nimport { useRef, useCallback } from \"react\";\nimport { cn, Button, X } from \"./_adapter\";\nimport { useImageGallery } from \"./context\";\nimport type { ImageGalleryItem } from \"./schema\";\nimport { resolveSafeNavigationHref } from \"../shared/media\";\n\ntype LightboxImage = Pick<ImageGalleryItem, \"title\" | \"caption\" | \"source\">;\n\nexport function GalleryLightbox() {\n  const dialogRef = useRef<HTMLDialogElement>(null);\n\n  const {\n    images,\n    activeIndex,\n    closeLightbox,\n    lightboxContentRef,\n    setDialogRef,\n  } = useImageGallery();\n\n  const isOpen = activeIndex !== null;\n  const currentImage = isOpen ? images[activeIndex] : null;\n\n  const handleDialogRef = useCallback(\n    (element: HTMLDialogElement | null) => {\n      dialogRef.current = element;\n      setDialogRef(element);\n    },\n    [setDialogRef],\n  );\n\n  const handleBackdropClick = useCallback(\n    (e: React.MouseEvent<HTMLDialogElement>) => {\n      if (e.target === dialogRef.current) {\n        closeLightbox();\n      }\n    },\n    [closeLightbox],\n  );\n\n  const handleCancel = useCallback(\n    (e: React.SyntheticEvent<HTMLDialogElement>) => {\n      e.preventDefault();\n      closeLightbox();\n    },\n    [closeLightbox],\n  );\n\n  return (\n    <dialog\n      ref={handleDialogRef}\n      onClick={handleBackdropClick}\n      onCancel={handleCancel}\n      className={cn(\n        \"m-0 h-full max-h-full w-full max-w-full\",\n        \"overflow-hidden p-0\",\n        \"bg-transparent backdrop:bg-black/95 dark:backdrop:bg-black/90\",\n        \"focus-visible:outline-none\",\n      )}\n      aria-label=\"Image lightbox\"\n    >\n      <div className=\"relative h-full w-full\">\n        {isOpen && <CloseButton onClose={closeLightbox} />}\n        <div className=\"relative z-10 flex h-full w-full flex-col items-center justify-center gap-4 p-8\">\n          <div\n            ref={lightboxContentRef}\n            className={cn(\n              \"pointer-events-auto relative w-fit max-w-full overflow-hidden rounded-lg shadow-2xl\",\n              \"[&>img]:block [&>img]:max-h-[80vh] [&>img]:max-w-full\",\n              \"[&>img]:h-auto [&>img]:w-auto [&>img]:object-contain [&>img]:select-none\",\n            )}\n          />\n          {currentImage && <Metadata image={currentImage} />}\n        </div>\n      </div>\n    </dialog>\n  );\n}\n\nfunction CloseButton({ onClose }: { onClose: () => void }) {\n  return (\n    <div className=\"absolute top-4 right-4 z-20\">\n      <Button\n        type=\"button\"\n        variant=\"ghost\"\n        size=\"icon\"\n        onClick={onClose}\n        className=\"text-white/80 hover:bg-white/10 hover:text-white\"\n        aria-label=\"Close\"\n      >\n        <X className=\"h-5 w-5\" />\n      </Button>\n    </div>\n  );\n}\n\nfunction Metadata({ image }: { image: LightboxImage }) {\n  const { title, caption, source } = image;\n  const hasTitle = Boolean(title);\n  const hasCaption = Boolean(caption);\n  const hasSource = Boolean(source?.label);\n\n  if (!hasTitle && !hasCaption && !hasSource) {\n    return null;\n  }\n\n  return (\n    <div className=\"text-center\">\n      {hasTitle && (\n        <h3 className=\"text-base font-medium tracking-tight text-white\">\n          {title}\n        </h3>\n      )}\n      {(hasCaption || hasSource) && (\n        <p className=\"mt-1 text-sm text-white/60\">\n          {caption}\n          {hasCaption && hasSource && \" · \"}\n          {hasSource && <SourceLink source={source!} />}\n        </p>\n      )}\n    </div>\n  );\n}\n\nfunction SourceLink({\n  source,\n}: {\n  source: NonNullable<LightboxImage[\"source\"]>;\n}) {\n  const href = resolveSafeNavigationHref(source.url);\n  if (!href) {\n    return <>{source.label}</>;\n  }\n\n  return (\n    <a\n      href={href}\n      target=\"_blank\"\n      rel=\"noopener noreferrer\"\n      className=\"hover:text-white/80 hover:underline\"\n    >\n      {source.label}\n    </a>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/image-gallery/image-gallery.tsx",
    "content": "\"use client\";\n\nimport \"./styles.css\";\nimport { cn } from \"./_adapter\";\nimport { ImageGalleryProvider } from \"./context\";\nimport { GalleryGrid } from \"./gallery-grid\";\nimport { GalleryLightbox } from \"./gallery-lightbox\";\nimport type { ImageGalleryProps } from \"./schema\";\n\nexport function ImageGallery({\n  id,\n  images,\n  title,\n  description,\n  className,\n  onImageClick,\n}: ImageGalleryProps) {\n  const handleImageClick = (imageId: string) => {\n    if (!onImageClick) return;\n\n    const image = images.find((img) => img.id === imageId);\n    if (image) {\n      onImageClick(imageId, image);\n    }\n  };\n\n  return (\n    <article\n      className={cn(\"relative w-full min-w-80 max-w-lg\", className)}\n      data-tool-ui-id={id}\n      data-slot=\"image-gallery\"\n    >\n      <div\n        className={cn(\n          \"@container relative isolate flex w-full min-w-0 flex-col rounded-xl\",\n          \"border border-border bg-card text-sm shadow-xs\",\n        )}\n      >\n        <ImageGalleryProvider images={images}>\n          <Header title={title} description={description} />\n          <div className=\"p-3\">\n            <GalleryGrid onImageClick={handleImageClick} />\n          </div>\n          <GalleryLightbox />\n        </ImageGalleryProvider>\n      </div>\n    </article>\n  );\n}\n\ninterface HeaderProps {\n  title?: string;\n  description?: string;\n}\n\nfunction Header({ title, description }: HeaderProps) {\n  if (!title && !description) {\n    return null;\n  }\n\n  return (\n    <div className=\"border-border/60 border-b px-4 pt-4 pb-3\">\n      {title && (\n        <h3 className=\"text-[15px] leading-tight font-semibold tracking-tight\">\n          {title}\n        </h3>\n      )}\n      {description && (\n        <p className=\"text-muted-foreground mt-1 text-sm leading-snug\">\n          {description}\n        </p>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/image-gallery/index.tsx",
    "content": "export { ImageGallery } from \"./image-gallery\";\nexport type {\n  ImageGalleryProps,\n  ImageGalleryItem,\n  SerializableImageGallery,\n} from \"./schema\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/image-gallery/schema.ts",
    "content": "import { z } from \"zod\";\nimport { defineToolUiContract } from \"../shared/contract\";\nimport {\n  ToolUIIdSchema,\n  ToolUIReceiptSchema,\n  ToolUIRoleSchema,\n} from \"../shared/schema\";\n\nexport const ImageGallerySourceSchema = z.object({\n  label: z.string(),\n  url: z.string().url().optional(),\n});\n\nexport type ImageGallerySource = z.infer<typeof ImageGallerySourceSchema>;\n\nexport const ImageGalleryItemSchema = z.object({\n  id: z.string().min(1),\n  src: z.string().url(),\n  alt: z.string().min(1, \"Images require alt text for accessibility\"),\n  width: z.number().positive(),\n  height: z.number().positive(),\n  title: z.string().optional(),\n  caption: z.string().optional(),\n  source: ImageGallerySourceSchema.optional(),\n});\n\nexport type ImageGalleryItem = z.infer<typeof ImageGalleryItemSchema>;\n\nexport const SerializableImageGallerySchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  receipt: ToolUIReceiptSchema.optional(),\n  images: z.array(ImageGalleryItemSchema).min(1),\n  title: z.string().optional(),\n  description: z.string().optional(),\n});\n\nexport type SerializableImageGallery = z.infer<\n  typeof SerializableImageGallerySchema\n>;\n\nexport interface ImageGalleryProps extends SerializableImageGallery {\n  className?: string;\n  onImageClick?: (imageId: string, image: ImageGalleryItem) => void;\n}\n\nconst SerializableImageGallerySchemaContract = defineToolUiContract(\n  \"ImageGallery\",\n  SerializableImageGallerySchema,\n);\n\nexport const parseSerializableImageGallery: (\n  input: unknown,\n) => SerializableImageGallery = SerializableImageGallerySchemaContract.parse;\n\nexport const safeParseSerializableImageGallery: (\n  input: unknown,\n) => SerializableImageGallery | null =\n  SerializableImageGallerySchemaContract.safeParse;\n"
  },
  {
    "path": "apps/www/components/tool-ui/image-gallery/styles.css",
    "content": "@supports (view-transition-name: none) {\n  ::view-transition-group(active-gallery-image) {\n    animation-duration: 300ms;\n    animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);\n    overflow: clip;\n    border-radius: 0.75rem;\n  }\n\n  ::view-transition-image-pair(active-gallery-image) {\n    overflow: clip;\n    border-radius: 0.75rem;\n  }\n\n  ::view-transition-old(active-gallery-image),\n  ::view-transition-new(active-gallery-image) {\n    border-radius: 0.75rem;\n    mix-blend-mode: normal;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    ::view-transition-group(active-gallery-image) {\n      animation-duration: 0ms;\n    }\n  }\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/index.ts",
    "content": "export * as ApprovalCardToolUi from \"./approval-card\";\nexport * as AudioToolUi from \"./audio\";\nexport * as ChartToolUi from \"./chart\";\nexport * as CitationToolUi from \"./citation\";\nexport * as CodeBlockToolUi from \"./code-block\";\nexport * as CodeDiffToolUi from \"./code-diff\";\nexport * as DataTableToolUi from \"./data-table\";\nexport * as GeoMapToolUi from \"./geo-map\";\nexport * as ImageToolUi from \"./image\";\nexport * as ImageGalleryToolUi from \"./image-gallery\";\nexport * as InstagramPostToolUi from \"./instagram-post\";\nexport * as ItemCarouselToolUi from \"./item-carousel\";\nexport * as LinkPreviewToolUi from \"./link-preview\";\nexport * as LinkedInPostToolUi from \"./linkedin-post\";\nexport * as MessageDraftToolUi from \"./message-draft\";\nexport * as OptionListToolUi from \"./option-list\";\nexport * as OrderSummaryToolUi from \"./order-summary\";\nexport * as ParameterSliderToolUi from \"./parameter-slider\";\nexport * as PlanToolUi from \"./plan\";\nexport * as PreferencesPanelToolUi from \"./preferences-panel\";\nexport * as ProgressTrackerToolUi from \"./progress-tracker\";\nexport * as QuestionFlowToolUi from \"./question-flow\";\nexport * as SharedToolUi from \"./shared\";\nexport * as StatsDisplayToolUi from \"./stats-display\";\nexport * as TerminalToolUi from \"./terminal\";\nexport * as VideoToolUi from \"./video\";\nexport * as WeatherWidgetToolUi from \"./weather-widget/runtime\";\nexport * as XPostToolUi from \"./x-post\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/instagram-post/README.md",
    "content": "# Instagram Post\n\nImplementation for the \"instagram-post\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/instagram-post/index.ts\n- serializable schema + parse helpers: components/tool-ui/instagram-post/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/social-post/content.mdx\n- Preset payload: lib/presets/instagram-post.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/instagram-post/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn      → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Button  → shadcn/ui Button\n *   Tooltip → shadcn/ui Tooltip\n */\n\nexport { cn } from \"@/lib/utils\";\nexport { Button } from \"@/components/ui/button\";\nexport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/instagram-post/index.ts",
    "content": "export { InstagramPost } from \"./instagram-post\";\nexport type { InstagramPostProps } from \"./instagram-post\";\nexport type {\n  InstagramPostData,\n  InstagramPostAuthor,\n  InstagramPostMedia,\n  InstagramPostStats,\n} from \"./schema\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/instagram-post/instagram-post.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { BadgeCheck, Heart, Share } from \"lucide-react\";\nimport {\n  cn,\n  Button,\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"./_adapter\";\nimport { formatRelativeTime } from \"../shared/utils\";\n\nimport type { InstagramPostData, InstagramPostMedia } from \"./schema\";\n\nexport interface InstagramPostProps {\n  post: InstagramPostData;\n  className?: string;\n  onAction?: (action: string, post: InstagramPostData) => void;\n}\n\nfunction InstagramLogo({ className }: { className?: string }) {\n  const id = React.useId();\n  const gradientPrimaryId = `ig-primary-${id}`;\n  const gradientSecondaryId = `ig-secondary-${id}`;\n\n  return (\n    <svg\n      viewBox=\"0 0 132 132\"\n      className={className}\n      role=\"img\"\n      aria-label=\"Instagram logo\"\n    >\n      <defs>\n        <radialGradient\n          id={gradientPrimaryId}\n          cx=\"158.429\"\n          cy=\"578.088\"\n          r=\"65\"\n          gradientUnits=\"userSpaceOnUse\"\n          gradientTransform=\"matrix(0 -1.982 1.844 0 -1031.4 454)\"\n        >\n          <stop offset=\"0\" stopColor=\"#fd5\" />\n          <stop offset=\".1\" stopColor=\"#fd5\" />\n          <stop offset=\".5\" stopColor=\"#ff543e\" />\n          <stop offset=\"1\" stopColor=\"#c837ab\" />\n        </radialGradient>\n        <radialGradient\n          id={gradientSecondaryId}\n          cx=\"147.694\"\n          cy=\"473.455\"\n          r=\"65\"\n          gradientUnits=\"userSpaceOnUse\"\n          gradientTransform=\"matrix(.174 .869 -3.58 .717 1648 -458.5)\"\n        >\n          <stop offset=\"0\" stopColor=\"#3771c8\" />\n          <stop offset=\".128\" stopColor=\"#3771c8\" />\n          <stop offset=\"1\" stopColor=\"#60f\" stopOpacity=\"0\" />\n        </radialGradient>\n      </defs>\n      <path\n        fill={`url(#${gradientPrimaryId})`}\n        d=\"M65 0C37.9 0 30 .03 28.4.16c-5.6.46-9 1.34-12.8 3.22-2.9 1.44-5.2 3.12-7.5 5.47C4 13.1 1.5 18.4.6 24.66c-.44 3.04-.57 3.66-.6 19.2-.01 5.16 0 12 0 21.1 0 27.12.03 35.05.16 36.6.45 5.4 1.3 8.82 3.1 12.55 3.44 7.14 10 12.5 17.76 14.5 2.68.7 5.64 1.1 9.44 1.26 1.6.07 18 .12 34.44.12s32.84-.02 34.4-.1c4.4-.2 6.96-.55 9.8-1.28 7.78-2.01 14.23-7.3 17.74-14.53 1.76-3.64 2.66-7.18 3.07-12.32.08-1.12.12-18.97.12-36.8 0-17.85-.04-35.67-.13-36.8-.4-5.2-1.3-8.7-3.13-12.43-1.5-3.04-3.16-5.3-5.56-7.62C116.9 4 111.64 1.5 105.37.6 102.34.16 101.73.03 86.2 0H65z\"\n        transform=\"translate(1 1)\"\n      />\n      <path\n        fill={`url(#${gradientSecondaryId})`}\n        d=\"M65 0C37.9 0 30 .03 28.4.16c-5.6.46-9 1.34-12.8 3.22-2.9 1.44-5.2 3.12-7.5 5.47C4 13.1 1.5 18.4.6 24.66c-.44 3.04-.57 3.66-.6 19.2-.01 5.16 0 12 0 21.1 0 27.12.03 35.05.16 36.6.45 5.4 1.3 8.82 3.1 12.55 3.44 7.14 10 12.5 17.76 14.5 2.68.7 5.64 1.1 9.44 1.26 1.6.07 18 .12 34.44.12s32.84-.02 34.4-.1c4.4-.2 6.96-.55 9.8-1.28 7.78-2.01 14.23-7.3 17.74-14.53 1.76-3.64 2.66-7.18 3.07-12.32.08-1.12.12-18.97.12-36.8 0-17.85-.04-35.67-.13-36.8-.4-5.2-1.3-8.7-3.13-12.43-1.5-3.04-3.16-5.3-5.56-7.62C116.9 4 111.64 1.5 105.37.6 102.34.16 101.73.03 86.2 0H65z\"\n        transform=\"translate(1 1)\"\n      />\n      <path\n        fill=\"#fff\"\n        d=\"M66 18c-13 0-14.67.06-19.8.3-5.1.23-8.6 1.04-11.64 2.22-3.16 1.23-5.84 2.87-8.5 5.54-2.67 2.67-4.3 5.35-5.54 8.5-1.2 3.05-2 6.54-2.23 11.65C18.06 51.33 18 52.96 18 66s.06 14.67.3 19.78c.22 5.12 1.03 8.6 2.22 11.66 1.22 3.15 2.86 5.83 5.53 8.5 2.67 2.67 5.35 4.3 8.5 5.53 3.06 1.2 6.55 2 11.65 2.23 5.12.23 6.76.3 19.8.3 13 0 14.66-.07 19.78-.3 5.12-.23 8.6-1.03 11.66-2.23 3.15-1.23 5.83-2.87 8.5-5.53 2.67-2.67 4.3-5.35 5.53-8.5 1.2-3.06 2-6.54 2.23-11.66.23-5.1.3-6.75.3-19.78 0-13.04-.07-14.68-.3-19.8-.23-5.1-1.04-8.6-2.22-11.64-1.23-3.16-2.87-5.84-5.54-8.5-2.67-2.67-5.35-4.3-8.5-5.54-3.06-1.18-6.55-2-11.66-2.22-5.12-.24-6.75-.3-19.8-.3zm-4.3 8.65c1.28 0 2.7 0 4.3 0 12.82 0 14.34.05 19.4.28 4.67.2 7.22 1 8.9 1.65 2.25.87 3.84 1.9 5.52 3.6 1.68 1.67 2.72 3.27 3.6 5.5.65 1.7 1.43 4.24 1.64 8.92.23 5.05.28 6.57.28 19.4s-.05 14.32-.28 19.4c-.2 4.67-1 7.2-1.64 8.9-.88 2.25-1.92 3.84-3.6 5.52-1.68 1.68-3.27 2.72-5.52 3.6-1.7.65-4.23 1.43-8.9 1.64-5.06.23-6.58.28-19.4.28-12.82 0-14.34-.05-19.4-.28-4.68-.2-7.22-1-8.9-1.64-2.25-.88-3.84-1.92-5.52-3.6-1.68-1.68-2.72-3.27-3.6-5.52-.65-1.7-1.43-4.23-1.64-8.9-.23-5.06-.28-6.58-.28-19.4s.05-14.34.28-19.4c.2-4.68 1-7.22 1.64-8.9.88-2.24 1.92-3.83 3.6-5.52 1.68-1.68 3.27-2.72 5.52-3.6 1.7-.65 4.23-1.43 8.9-1.65 4.43-.2 6.15-.26 15.1-.27zm30 8c-3.2 0-5.77 2.57-5.77 5.75 0 3.2 2.58 5.77 5.77 5.77 3.18 0 5.76-2.58 5.76-5.77 0-3.18-2.58-5.76-5.76-5.76zm-25.63 6.72c-13.6 0-24.64 11.04-24.64 24.65 0 13.6 11.03 24.64 24.64 24.64 13.6 0 24.65-11.03 24.65-24.64 0-13.6-11.04-24.64-24.65-24.64zm0 8.65c8.84 0 16 7.16 16 16 0 8.84-7.16 16-16 16-8.84 0-16-7.16-16-16 0-8.84 7.16-16 16-16z\"\n      />\n    </svg>\n  );\n}\n\nfunction Header({\n  author,\n  createdAt,\n}: {\n  author: InstagramPostData[\"author\"];\n  createdAt?: string;\n}) {\n  return (\n    <header className=\"flex items-center gap-3 p-3\">\n      <img\n        src={author.avatarUrl}\n        alt={`${author.name} avatar`}\n        width={32}\n        height={32}\n        className=\"size-8 rounded-full object-cover\"\n      />\n      <div className=\"flex min-w-0 flex-1 items-center gap-1.5\">\n        <span className=\"truncate text-sm font-semibold\">{author.handle}</span>\n        {author.verified && (\n          <BadgeCheck\n            aria-label=\"Verified\"\n            className=\"size-3.5 shrink-0 text-sky-500\"\n          />\n        )}\n        {createdAt && (\n          <>\n            <span className=\"text-muted-foreground\">·</span>\n            <span className=\"text-muted-foreground text-sm\">\n              {formatRelativeTime(createdAt)}\n            </span>\n          </>\n        )}\n      </div>\n      <InstagramLogo className=\"size-5\" />\n    </header>\n  );\n}\n\nfunction MediaGrid({\n  media,\n  onOpen,\n}: {\n  media: InstagramPostMedia[];\n  onOpen?: (index: number) => void;\n}) {\n  if (media.length === 0) return null;\n\n  const renderItem = (item: InstagramPostMedia, index: number) => (\n    <button\n      key={index}\n      type=\"button\"\n      className=\"bg-muted relative block size-full overflow-hidden\"\n      onClick={() => onOpen?.(index)}\n    >\n      {item.type === \"image\" ? (\n        <img\n          src={item.url}\n          alt={item.alt}\n          className=\"size-full object-cover\"\n          loading=\"lazy\"\n        />\n      ) : (\n        <video src={item.url} playsInline className=\"size-full object-cover\" />\n      )}\n    </button>\n  );\n\n  if (media.length === 1) {\n    return (\n      <div className=\"aspect-square w-full overflow-hidden\">\n        {renderItem(media[0], 0)}\n      </div>\n    );\n  }\n\n  if (media.length === 2) {\n    return (\n      <div className=\"grid aspect-square w-full grid-cols-2 gap-0.5 overflow-hidden\">\n        {media.map(renderItem)}\n      </div>\n    );\n  }\n\n  if (media.length === 3) {\n    return (\n      <div className=\"grid aspect-square w-full grid-cols-2 gap-0.5 overflow-hidden\">\n        <div className=\"h-full\">{renderItem(media[0], 0)}</div>\n        <div className=\"grid h-full grid-rows-2 gap-0.5\">\n          {media.slice(1).map((item, i) => (\n            <div key={i + 1} className=\"h-full\">\n              {renderItem(item, i + 1)}\n            </div>\n          ))}\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"grid aspect-square w-full grid-cols-2 gap-0.5 overflow-hidden\">\n      {media.slice(0, 4).map((item, index) => (\n        <div key={index} className=\"relative h-full w-full\">\n          {renderItem(item, index)}\n          {index === 3 && media.length > 4 && (\n            <div className=\"pointer-events-none absolute inset-0 flex items-center justify-center bg-black/50\">\n              <span className=\"text-2xl font-semibold text-white\">\n                +{media.length - 4}\n              </span>\n            </div>\n          )}\n        </div>\n      ))}\n    </div>\n  );\n}\n\nfunction PostBody({ text }: { text?: string }) {\n  if (!text) return null;\n  return (\n    <span className=\"text-sm leading-relaxed text-pretty wrap-break-word whitespace-pre-wrap\">\n      {text}\n    </span>\n  );\n}\n\nfunction ActionButton({\n  icon: Icon,\n  label,\n  active,\n  hoverColor,\n  activeColor,\n  onClick,\n}: {\n  icon: React.ComponentType<{ className?: string }>;\n  label: string;\n  active?: boolean;\n  hoverColor: string;\n  activeColor?: string;\n  onClick: () => void;\n}) {\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>\n        <Button\n          variant=\"ghost\"\n          size=\"sm\"\n          onClick={(e) => {\n            e.stopPropagation();\n            onClick();\n          }}\n          className={cn(\"h-auto\", hoverColor, active && activeColor)}\n          aria-label={label}\n        >\n          <Icon className=\"size-5\" />\n        </Button>\n      </TooltipTrigger>\n      <TooltipContent>{label}</TooltipContent>\n    </Tooltip>\n  );\n}\n\nfunction PostActions({\n  stats,\n  onAction,\n}: {\n  stats?: InstagramPostData[\"stats\"];\n  onAction: (action: string) => void;\n}) {\n  return (\n    <TooltipProvider delayDuration={300}>\n      <div className=\"flex items-center gap-1\">\n        <ActionButton\n          icon={Heart}\n          label=\"Like\"\n          active={stats?.isLiked}\n          hoverColor=\"hover:opacity-60\"\n          activeColor=\"text-red-500 fill-red-500\"\n          onClick={() => onAction(\"like\")}\n        />\n        <ActionButton\n          icon={Share}\n          label=\"Share\"\n          hoverColor=\"hover:opacity-60\"\n          onClick={() => onAction(\"share\")}\n        />\n      </div>\n    </TooltipProvider>\n  );\n}\n\nexport function InstagramPost({\n  post,\n  className,\n  onAction,\n}: InstagramPostProps) {\n  return (\n    <div\n      className={cn(\"flex max-w-xl flex-col gap-3\", className)}\n      data-tool-ui-id={post.id}\n      data-slot=\"instagram-post\"\n    >\n      <article className=\"bg-card overflow-hidden rounded-lg border shadow-sm\">\n        <Header author={post.author} createdAt={post.createdAt} />\n\n        {post.media && post.media.length > 0 && (\n          <MediaGrid media={post.media} />\n        )}\n\n        <div className=\"flex flex-col gap-2 p-3\">\n          <PostActions\n            stats={post.stats}\n            onAction={(action) => onAction?.(action, post)}\n          />\n          {post.text && (\n            <div>\n              <span className=\"text-sm font-semibold\">\n                {post.author.handle}\n              </span>{\" \"}\n              <PostBody text={post.text} />\n            </div>\n          )}\n        </div>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/instagram-post/schema.ts",
    "content": "import { z } from \"zod\";\nimport { defineToolUiContract } from \"../shared/contract\";\n\nexport const InstagramPostAuthorSchema = z.object({\n  name: z.string(),\n  handle: z.string(),\n  avatarUrl: z.string(),\n  verified: z.boolean().optional(),\n});\n\nexport const InstagramPostMediaSchema = z.object({\n  type: z.enum([\"image\", \"video\"]),\n  url: z.string(),\n  alt: z.string(),\n});\n\nexport const InstagramPostStatsSchema = z.object({\n  likes: z.number().optional(),\n  isLiked: z.boolean().optional(),\n});\n\nexport interface InstagramPostData {\n  id: string;\n  author: z.infer<typeof InstagramPostAuthorSchema>;\n  text?: string;\n  media?: z.infer<typeof InstagramPostMediaSchema>[];\n  stats?: z.infer<typeof InstagramPostStatsSchema>;\n  createdAt?: string;\n}\n\nexport const SerializableInstagramPostSchema: z.ZodType<InstagramPostData> =\n  z.object({\n    id: z.string(),\n    author: InstagramPostAuthorSchema,\n    text: z.string().optional(),\n    media: z.array(InstagramPostMediaSchema).optional(),\n    stats: InstagramPostStatsSchema.optional(),\n    createdAt: z.string().optional(),\n  });\n\nexport type InstagramPostAuthor = z.infer<typeof InstagramPostAuthorSchema>;\nexport type InstagramPostMedia = z.infer<typeof InstagramPostMediaSchema>;\nexport type InstagramPostStats = z.infer<typeof InstagramPostStatsSchema>;\n\nconst SerializableInstagramPostSchemaContract = defineToolUiContract(\n  \"InstagramPost\",\n  SerializableInstagramPostSchema,\n);\n\nexport const parseSerializableInstagramPost: (\n  input: unknown,\n) => InstagramPostData = SerializableInstagramPostSchemaContract.parse;\n\nexport const safeParseSerializableInstagramPost: (\n  input: unknown,\n) => InstagramPostData | null =\n  SerializableInstagramPostSchemaContract.safeParse;\n"
  },
  {
    "path": "apps/www/components/tool-ui/item-carousel/README.md",
    "content": "# Item Carousel\n\nImplementation for the \"item-carousel\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/item-carousel/index.tsx\n- serializable schema + parse helpers: components/tool-ui/item-carousel/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/item-carousel/content.mdx\n- Preset payload: lib/presets/item-carousel.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/item-carousel/_adapter.tsx",
    "content": "/**\n * UI and utility re-exports for copy-standalone portability.\n *\n * This file centralizes dependencies so the component can be easily\n * copied to another project by updating these imports to match the target\n * project's paths.\n */\nexport { cn } from \"@/lib/utils\";\n\nexport { Button } from \"@/components/ui/button\";\nexport { Card } from \"@/components/ui/card\";\nexport { ChevronLeft, ChevronRight } from \"lucide-react\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/item-carousel/index.tsx",
    "content": "export { ItemCarousel } from \"./item-carousel\";\nexport { ItemCard } from \"./item-card\";\nexport type {\n  Item,\n  ItemCarouselProps,\n  SerializableItem,\n  SerializableItemCarousel,\n} from \"./schema\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/item-carousel/item-card.tsx",
    "content": "\"use client\";\n\nimport { cn, Button, Card } from \"./_adapter\";\nimport type { Item } from \"./schema\";\n\ninterface ItemCardProps {\n  item: Item;\n  onItemClick?: (itemId: string) => void;\n  onItemAction?: (itemId: string, actionId: string) => void;\n}\n\nexport function ItemCard({ item, onItemClick, onItemAction }: ItemCardProps) {\n  const { id, name, subtitle, image, color, actions } = item;\n  const isCardInteractive = typeof onItemClick === \"function\";\n\n  const handleCardClick = () => {\n    if (!isCardInteractive) return;\n    onItemClick?.(id);\n  };\n\n  const handleActionClick = (actionId: string) => {\n    onItemAction?.(id, actionId);\n  };\n\n  return (\n    <Card\n      className={cn(\n        \"group @container/card relative flex w-52 min-w-48 flex-col gap-0 self-stretch overflow-clip rounded-md p-0 @lg:w-56\",\n        isCardInteractive && \"cursor-pointer hover:shadow\",\n        \"touch-manipulation\",\n      )}\n    >\n      {isCardInteractive && (\n        <button\n          type=\"button\"\n          aria-label={`View item: ${name}`}\n          className={cn(\n            \"absolute inset-0 z-10 rounded-md\",\n            \"cursor-pointer touch-manipulation\",\n            \"focus-visible:ring-ring focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none\",\n          )}\n          onClick={handleCardClick}\n        />\n      )}\n\n      <div className=\"bg-muted relative aspect-square w-full overflow-hidden\">\n        {image ? (\n          <img\n            src={image}\n            alt={name}\n            loading=\"lazy\"\n            decoding=\"async\"\n            draggable={false}\n            className={cn(\n              \"h-full w-full object-cover transition-transform duration-200\",\n              isCardInteractive && \"group-hover:scale-105\",\n            )}\n          />\n        ) : (\n          <div\n            className={cn(\n              \"h-full w-full transition-transform duration-200\",\n              isCardInteractive && \"group-hover:scale-105\",\n            )}\n            style={color ? { backgroundColor: color } : undefined}\n            role=\"img\"\n            aria-label={name}\n          />\n        )}\n      </div>\n\n      <div className=\"flex flex-1 flex-col gap-1 p-3\">\n        <div className=\"flex flex-col gap-1\">\n          <h3 className=\"line-clamp-2 text-sm leading-tight font-medium\">\n            {name}\n          </h3>\n\n          {subtitle && (\n            <p className=\"text-muted-foreground line-clamp-1 text-sm\">\n              {subtitle}\n            </p>\n          )}\n        </div>\n\n        {actions && actions.length > 0 && (\n          <div\n            className={cn(\n              \"relative z-20 mt-auto flex flex-col-reverse gap-2 pt-2 @[176px]/card:flex-row\",\n            )}\n          >\n            {actions.map((action) => (\n              <Button\n                key={action.id}\n                type=\"button\"\n                variant={action.variant ?? \"default\"}\n                size=\"sm\"\n                disabled={action.disabled}\n                className=\"min-h-11 w-full px-3 md:min-h-8 @[176px]/card:h-8 @[176px]/card:w-auto @[176px]/card:flex-1\"\n                onClick={() => handleActionClick(action.id)}\n              >\n                {action.icon}\n                {action.label}\n              </Button>\n            ))}\n          </div>\n        )}\n      </div>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/item-carousel/item-carousel.tsx",
    "content": "\"use client\";\n\nimport { useRef, useState, useEffect, useCallback } from \"react\";\nimport { cn, Button, Card, ChevronLeft, ChevronRight } from \"./_adapter\";\nimport { ItemCard } from \"./item-card\";\nimport { prefersReducedMotion } from \"../shared/utils\";\nimport type { ItemCarouselProps } from \"./schema\";\n\nconst SCROLL_PADDING_STYLE = { scrollPaddingInline: \"1rem\" };\n\nconst SCROLL_EDGE_THRESHOLD_PX = 8;\nconst SNAP_EPSILON_PX = 5;\nconst SCROLL_ANIMATION_DURATION_MS = 300;\nconst PAGE_SCROLL_RATIO = 0.8;\nconst PAGE_SCROLL_BREAKPOINT_PX = 640;\n\ntype ScrollDirection = \"left\" | \"right\";\n\ninterface ScrollAnimationState {\n  target: number;\n  start: number;\n  startTime: number;\n  duration: number;\n  onComplete?: () => void;\n}\n\nfunction useSmoothScroll() {\n  const animationRef = useRef<ScrollAnimationState | null>(null);\n  const frameRef = useRef<number | null>(null);\n\n  const cancelAnimation = useCallback(() => {\n    if (frameRef.current !== null) {\n      cancelAnimationFrame(frameRef.current);\n      frameRef.current = null;\n    }\n    animationRef.current = null;\n  }, []);\n\n  useEffect(() => cancelAnimation, [cancelAnimation]);\n\n  const scrollTo = useCallback(\n    (\n      element: HTMLElement,\n      target: number,\n      duration = SCROLL_ANIMATION_DURATION_MS,\n      onComplete?: () => void,\n    ) => {\n      if (prefersReducedMotion() || duration <= 0) {\n        element.scrollLeft = target;\n        onComplete?.();\n        return;\n      }\n\n      cancelAnimation();\n\n      animationRef.current = {\n        target,\n        start: element.scrollLeft,\n        startTime: performance.now(),\n        duration,\n        onComplete,\n      };\n\n      element.style.scrollSnapType = \"none\";\n\n      const step = () => {\n        const anim = animationRef.current;\n        if (!anim) return;\n\n        const elapsed = performance.now() - anim.startTime;\n        const progress = Math.min(elapsed / anim.duration, 1);\n        const eased = 1 - Math.pow(1 - progress, 3);\n\n        element.scrollLeft = anim.start + (anim.target - anim.start) * eased;\n\n        if (progress < 1) {\n          frameRef.current = requestAnimationFrame(step);\n          return;\n        }\n\n        element.scrollLeft = anim.target;\n        const callback = anim.onComplete;\n        cancelAnimation();\n\n        requestAnimationFrame(() => {\n          element.style.scrollSnapType = \"\";\n          callback?.();\n        });\n      };\n\n      frameRef.current = requestAnimationFrame(step);\n    },\n    [cancelAnimation],\n  );\n\n  const isAnimating = useCallback(\n    () => animationRef.current !== null && frameRef.current !== null,\n    [],\n  );\n\n  return { scrollTo, isAnimating, cancelAnimation };\n}\n\nfunction useScrollEdgeState(\n  scrollRef: React.RefObject<HTMLDivElement | null>,\n  itemCount: number,\n) {\n  const [canScrollLeft, setCanScrollLeft] = useState(false);\n  const [canScrollRight, setCanScrollRight] = useState(false);\n\n  const updateState = useCallback(() => {\n    const container = scrollRef.current;\n    if (!container) return;\n\n    const scrollLeft = Math.round(container.scrollLeft);\n    const maxScroll = Math.max(\n      0,\n      Math.round(container.scrollWidth - container.clientWidth),\n    );\n\n    setCanScrollLeft(scrollLeft > SCROLL_EDGE_THRESHOLD_PX);\n    setCanScrollRight(scrollLeft < maxScroll - SCROLL_EDGE_THRESHOLD_PX);\n  }, [scrollRef]);\n\n  useEffect(() => {\n    const container = scrollRef.current;\n    if (!container) return;\n\n    let rafId: number | null = null;\n\n    const scheduleUpdate = () => {\n      if (rafId !== null) cancelAnimationFrame(rafId);\n      rafId = requestAnimationFrame(() => {\n        rafId = null;\n        updateState();\n      });\n    };\n\n    scheduleUpdate();\n\n    container.addEventListener(\"scroll\", scheduleUpdate, { passive: true });\n    const resizeObserver = new ResizeObserver(scheduleUpdate);\n    resizeObserver.observe(container);\n\n    return () => {\n      container.removeEventListener(\"scroll\", scheduleUpdate);\n      resizeObserver.disconnect();\n      if (rafId !== null) cancelAnimationFrame(rafId);\n    };\n  }, [scrollRef, updateState, itemCount]);\n\n  return { canScrollLeft, canScrollRight };\n}\n\nfunction CarouselNavButton({\n  direction,\n  visible,\n  onClick,\n}: {\n  direction: ScrollDirection;\n  visible: boolean;\n  onClick: () => void;\n}) {\n  const isLeft = direction === \"left\";\n  const Icon = isLeft ? ChevronLeft : ChevronRight;\n\n  return (\n    <Button\n      type=\"button\"\n      variant=\"secondary\"\n      size=\"icon-sm\"\n      className={cn(\n        \"pointer-events-none scale-90 border-none opacity-0\",\n        \"bg-background/60 absolute inset-y-0 z-20 my-auto hidden h-[6cqh] min-h-[50px] rounded-2xl backdrop-blur-lg\",\n        \"transition-[opacity,transform] duration-250 ease-[cubic-bezier(0.16,1,0.3,1)] motion-reduce:transition-none\",\n        \"@md:flex\",\n        isLeft ? \"left-1.5\" : \"right-1.5\",\n        visible &&\n          \"pointer-events-auto scale-100 opacity-100 @md:group-focus-within:pointer-events-auto @md:group-focus-within:scale-100 @md:group-focus-within:opacity-100 @md:group-hover:pointer-events-auto @md:group-hover:scale-100 @md:group-hover:opacity-100\",\n      )}\n      onClick={onClick}\n      aria-label={isLeft ? \"Scroll left\" : \"Scroll right\"}\n      tabIndex={visible ? 0 : -1}\n      aria-hidden={!visible}\n    >\n      <Icon className=\"h-4 w-4\" />\n    </Button>\n  );\n}\n\ninterface ItemCarouselHeaderProps {\n  title?: string;\n  description?: string;\n}\n\nfunction ItemCarouselHeader({ title, description }: ItemCarouselHeaderProps) {\n  if (!title && !description) return null;\n\n  return (\n    <div className=\"px-4 pt-4 pb-1\">\n      {title && (\n        <h3 className=\"text-[15px] leading-tight font-semibold tracking-tight\">\n          {title}\n        </h3>\n      )}\n      {description && (\n        <p className=\"text-muted-foreground mt-1 text-sm leading-snug\">\n          {description}\n        </p>\n      )}\n    </div>\n  );\n}\n\ninterface EmptyStateProps {\n  id: string;\n  className?: string;\n}\n\nfunction EmptyState({ id, className }: EmptyStateProps) {\n  return (\n    <Card\n      data-tool-ui-id={id}\n      data-slot=\"item-carousel\"\n      className={cn(\"flex h-48 items-center justify-center\", className)}\n    >\n      <p className=\"text-muted-foreground text-sm\">No items to display</p>\n    </Card>\n  );\n}\n\nfunction ItemCarouselRoot({\n  id,\n  title,\n  description,\n  items,\n  className,\n  onItemClick,\n  onItemAction,\n}: ItemCarouselProps) {\n  const scrollRef = useRef<HTMLDivElement>(null);\n  const targetIndexRef = useRef<number | null>(null);\n\n  const { scrollTo, isAnimating } = useSmoothScroll();\n  const { canScrollLeft, canScrollRight } = useScrollEdgeState(\n    scrollRef,\n    items.length,\n  );\n\n  const scroll = useCallback(\n    (direction: ScrollDirection) => {\n      const container = scrollRef.current;\n      if (!container) return;\n\n      const paddingValue = window.getComputedStyle(container).scrollPaddingLeft;\n      const scrollPaddingLeft = Number.isFinite(Number.parseFloat(paddingValue))\n        ? Number.parseFloat(paddingValue)\n        : 0;\n\n      const itemElements = Array.from(\n        container.querySelectorAll<HTMLElement>(\"[data-carousel-item]\"),\n      );\n      if (itemElements.length === 0) return;\n\n      const snapPositions = itemElements.map((el) =>\n        Math.max(0, el.offsetLeft - scrollPaddingLeft),\n      );\n\n      const scrollLeft = Math.round(container.scrollLeft);\n      let currentIndex: number;\n      if (isAnimating()) {\n        currentIndex = Math.min(\n          targetIndexRef.current ?? 0,\n          snapPositions.length - 1,\n        );\n      } else {\n        currentIndex = snapPositions.length - 1;\n        for (let i = 0; i < snapPositions.length; i++) {\n          const snap = snapPositions[i];\n          if (Math.abs(snap - scrollLeft) < SNAP_EPSILON_PX) {\n            currentIndex = i;\n            break;\n          }\n          if (snap > scrollLeft) {\n            currentIndex = Math.max(0, i - 1);\n            break;\n          }\n        }\n      }\n\n      const itemStep =\n        itemElements.length > 1\n          ? itemElements[1].offsetLeft - itemElements[0].offsetLeft\n          : 0;\n      const safeStep =\n        itemStep > 0 ? itemStep : itemElements[0].offsetWidth || 1;\n\n      const pageIndexStep =\n        container.clientWidth >= PAGE_SCROLL_BREAKPOINT_PX\n          ? Math.max(\n              1,\n              Math.floor(\n                (container.clientWidth * PAGE_SCROLL_RATIO) / safeStep,\n              ),\n            )\n          : 1;\n\n      const targetIndex =\n        direction === \"right\"\n          ? Math.min(currentIndex + pageIndexStep, itemElements.length - 1)\n          : Math.max(currentIndex - pageIndexStep, 0);\n\n      targetIndexRef.current = targetIndex;\n      const targetScrollLeft = snapPositions[targetIndex];\n\n      if (Math.abs(targetScrollLeft - container.scrollLeft) > 1) {\n        scrollTo(\n          container,\n          targetScrollLeft,\n          SCROLL_ANIMATION_DURATION_MS,\n          () => {\n            targetIndexRef.current = null;\n          },\n        );\n      }\n    },\n    [scrollTo, isAnimating],\n  );\n\n  const handleScrollLeft = useCallback(() => scroll(\"left\"), [scroll]);\n  const handleScrollRight = useCallback(() => scroll(\"right\"), [scroll]);\n\n  if (items.length === 0) {\n    return <EmptyState id={id} className={className} />;\n  }\n\n  return (\n    <div\n      data-tool-ui-id={id}\n      data-slot=\"item-carousel\"\n      className={cn(\n        \"bg-background @container relative isolate w-full gap-0 overflow-hidden rounded-2xl border p-0\",\n        className,\n      )}\n    >\n      <ItemCarouselHeader title={title} description={description} />\n\n      <div className=\"group relative\">\n        <CarouselNavButton\n          direction=\"left\"\n          visible={canScrollLeft}\n          onClick={handleScrollLeft}\n        />\n        <CarouselNavButton\n          direction=\"right\"\n          visible={canScrollRight}\n          onClick={handleScrollRight}\n        />\n\n        <div\n          ref={scrollRef}\n          className={cn(\n            \"grid auto-cols-max grid-flow-col gap-4 overflow-x-auto overscroll-x-contain p-4\",\n            \"snap-x snap-mandatory\",\n          )}\n          role=\"list\"\n          style={SCROLL_PADDING_STYLE}\n        >\n          {items.map((item) => (\n            <div\n              key={item.id}\n              data-carousel-item\n              data-item-id={item.id}\n              role=\"listitem\"\n              className=\"flex snap-start snap-always\"\n            >\n              <ItemCard\n                item={item}\n                onItemClick={onItemClick}\n                onItemAction={onItemAction}\n              />\n            </div>\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n}\n\ntype ItemCarouselComponent = typeof ItemCarouselRoot & {\n  Root: typeof ItemCarouselRoot;\n  Header: typeof ItemCarouselHeader;\n  EmptyState: typeof EmptyState;\n  NavButton: typeof CarouselNavButton;\n  Card: typeof ItemCard;\n};\n\nexport const ItemCarousel = Object.assign(ItemCarouselRoot, {\n  Root: ItemCarouselRoot,\n  Header: ItemCarouselHeader,\n  EmptyState,\n  NavButton: CarouselNavButton,\n  Card: ItemCard,\n}) as ItemCarouselComponent;\n"
  },
  {
    "path": "apps/www/components/tool-ui/item-carousel/schema.ts",
    "content": "import { z } from \"zod\";\nimport { defineToolUiContract } from \"../shared/contract\";\nimport {\n  ActionSchema,\n  SerializableActionSchema,\n  ToolUIIdSchema,\n} from \"../shared/schema\";\n\nexport const ItemSchema = z.object({\n  id: z.string().min(1),\n  name: z.string().min(1),\n  subtitle: z.string().optional(),\n  image: z.url().optional(),\n  color: z.string().optional(),\n  actions: z.array(ActionSchema).optional(),\n});\n\nexport const ItemCarouselPropsSchema = z.object({\n  id: ToolUIIdSchema,\n  title: z.string().optional(),\n  description: z.string().optional(),\n  items: z.array(ItemSchema),\n  className: z.string().optional(),\n});\n\nexport type Item = z.infer<typeof ItemSchema>;\n\nexport type ItemCarouselProps = z.infer<typeof ItemCarouselPropsSchema> & {\n  onItemClick?: (itemId: string) => void;\n  onItemAction?: (itemId: string, actionId: string) => void;\n};\n\nexport const SerializableItemSchema = ItemSchema.extend({\n  actions: z.array(SerializableActionSchema).optional(),\n});\n\nexport const SerializableItemCarouselSchema = ItemCarouselPropsSchema.omit({\n  className: true,\n})\n  .extend({\n    items: z.array(SerializableItemSchema),\n  })\n  .superRefine((payload, ctx) => {\n    const seenItemIds = new Map<string, number>();\n\n    payload.items.forEach((item, index) => {\n      const firstSeenAt = seenItemIds.get(item.id);\n      if (firstSeenAt !== undefined) {\n        ctx.addIssue({\n          code: z.ZodIssueCode.custom,\n          path: [\"items\", index, \"id\"],\n          message: `duplicate item id '${item.id}' (first seen at index ${firstSeenAt})`,\n        });\n        return;\n      }\n      seenItemIds.set(item.id, index);\n    });\n  });\n\nexport type SerializableItem = z.infer<typeof SerializableItemSchema>;\nexport type SerializableItemCarousel = z.infer<\n  typeof SerializableItemCarouselSchema\n>;\n\nconst SerializableItemCarouselSchemaContract = defineToolUiContract(\n  \"ItemCarousel\",\n  SerializableItemCarouselSchema,\n);\n\nexport const parseSerializableItemCarousel: (\n  input: unknown,\n) => SerializableItemCarousel = SerializableItemCarouselSchemaContract.parse;\n\nexport const safeParseSerializableItemCarousel: (\n  input: unknown,\n) => SerializableItemCarousel | null =\n  SerializableItemCarouselSchemaContract.safeParse;\n"
  },
  {
    "path": "apps/www/components/tool-ui/link-preview/README.md",
    "content": "# Link Preview\n\nImplementation for the \"link-preview\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/link-preview/index.ts\n- serializable schema + parse helpers: components/tool-ui/link-preview/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/link-preview/content.mdx\n- Preset payload: lib/presets/link-preview.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/link-preview/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n */\n\"use client\";\n\nexport { cn } from \"@/lib/utils\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/link-preview/index.ts",
    "content": "export { LinkPreview } from \"./link-preview\";\nexport type { LinkPreviewProps } from \"./link-preview\";\nexport type { SerializableLinkPreview } from \"./schema\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/link-preview/link-preview.tsx",
    "content": "\"use client\";\n\nimport { Globe } from \"lucide-react\";\nimport { cn } from \"./_adapter\";\n\nimport {\n  RATIO_CLASS_MAP,\n  getFitClass,\n  openSafeNavigationHref,\n  sanitizeHref,\n} from \"../shared/media\";\nimport type { SerializableLinkPreview } from \"./schema\";\n\nconst FALLBACK_LOCALE = \"en-US\";\nconst CONTENT_SPACING = \"px-5 py-4 gap-2\";\n\nexport interface LinkPreviewProps extends SerializableLinkPreview {\n  className?: string;\n  onNavigate?: (href: string, preview: SerializableLinkPreview) => void;\n}\n\nexport function LinkPreview(props: LinkPreviewProps) {\n  const { className, onNavigate, ...serializable } = props;\n\n  const {\n    id,\n    href: rawHref,\n    title,\n    description,\n    image,\n    domain,\n    favicon,\n    ratio = \"16:9\",\n    fit = \"cover\",\n    locale: providedLocale,\n  } = serializable;\n\n  const locale = providedLocale ?? FALLBACK_LOCALE;\n  const sanitizedHref = sanitizeHref(rawHref);\n\n  const previewData: SerializableLinkPreview = {\n    ...serializable,\n    href: sanitizedHref ?? rawHref,\n    locale,\n  };\n\n  const handleClick = () => {\n    if (!sanitizedHref) return;\n    if (onNavigate) {\n      onNavigate(sanitizedHref, previewData);\n    } else {\n      openSafeNavigationHref(sanitizedHref);\n    }\n  };\n\n  return (\n    <article\n      className={cn(\"relative w-full max-w-md min-w-80\", className)}\n      lang={locale}\n      data-tool-ui-id={id}\n      data-slot=\"link-preview\"\n    >\n      <div\n        className={cn(\n          \"group @container relative isolate flex w-full min-w-0 flex-col overflow-hidden rounded-xl\",\n          \"border-border bg-card border text-sm shadow-xs\",\n          sanitizedHref && \"cursor-pointer\",\n        )}\n        onClick={sanitizedHref ? handleClick : undefined}\n        role={sanitizedHref ? \"link\" : undefined}\n        tabIndex={sanitizedHref ? 0 : undefined}\n        onKeyDown={\n          sanitizedHref\n            ? (e) => {\n                if (e.key === \"Enter\" || e.key === \" \") {\n                  e.preventDefault();\n                  handleClick();\n                }\n              }\n            : undefined\n        }\n      >\n        <div className=\"flex flex-col\">\n          {image && (\n            <div\n              className={cn(\n                \"bg-muted relative w-full overflow-hidden\",\n                ratio !== \"auto\" ? RATIO_CLASS_MAP[ratio] : \"aspect-[5/3]\",\n              )}\n            >\n              <img\n                src={image}\n                alt=\"\"\n                loading=\"lazy\"\n                decoding=\"async\"\n                className={cn(\n                  \"absolute inset-0 h-full w-full\",\n                  getFitClass(fit),\n                  \"object-center transition-transform duration-200 group-hover:scale-[1.01]\",\n                )}\n              />\n            </div>\n          )}\n          <div className={cn(\"flex flex-col\", CONTENT_SPACING)}>\n            {domain && (\n              <div className=\"text-muted-foreground flex items-center gap-2 text-xs\">\n                {favicon ? (\n                  <img\n                    src={favicon}\n                    alt=\"\"\n                    aria-hidden=\"true\"\n                    width={16}\n                    height={16}\n                    className=\"size-4 rounded-full object-cover\"\n                    loading=\"lazy\"\n                    decoding=\"async\"\n                  />\n                ) : (\n                  <div className=\"border-border/60 bg-muted flex size-4 shrink-0 items-center justify-center rounded-full border\">\n                    <Globe className=\"h-2.5 w-2.5\" aria-hidden=\"true\" />\n                  </div>\n                )}\n                <span>{domain}</span>\n              </div>\n            )}\n            {title && (\n              <h3 className=\"text-foreground text-base font-medium text-pretty\">\n                <span className=\"line-clamp-2\">{title}</span>\n              </h3>\n            )}\n            {description && (\n              <p className=\"text-muted-foreground leading-snug text-pretty\">\n                <span className=\"line-clamp-2\">{description}</span>\n              </p>\n            )}\n          </div>\n        </div>\n      </div>\n    </article>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/link-preview/schema.ts",
    "content": "import { z } from \"zod\";\nimport { defineToolUiContract } from \"../shared/contract\";\nimport {\n  ToolUIIdSchema,\n  ToolUIReceiptSchema,\n  ToolUIRoleSchema,\n} from \"../shared/schema\";\n\nimport { AspectRatioSchema, MediaFitSchema } from \"../shared/media\";\n\nexport const SerializableLinkPreviewSchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  receipt: ToolUIReceiptSchema.optional(),\n  href: z.url(),\n  title: z.string().optional(),\n  description: z.string().optional(),\n  image: z.url().optional(),\n  domain: z.string().optional(),\n  favicon: z.url().optional(),\n  ratio: AspectRatioSchema.optional(),\n  fit: MediaFitSchema.optional(),\n  createdAt: z.string().datetime().optional(),\n  locale: z.string().optional(),\n});\n\nexport type SerializableLinkPreview = z.infer<\n  typeof SerializableLinkPreviewSchema\n>;\n\nconst SerializableLinkPreviewSchemaContract = defineToolUiContract(\n  \"LinkPreview\",\n  SerializableLinkPreviewSchema,\n);\n\nexport const parseSerializableLinkPreview: (\n  input: unknown,\n) => SerializableLinkPreview = SerializableLinkPreviewSchemaContract.parse;\n\nexport const safeParseSerializableLinkPreview: (\n  input: unknown,\n) => SerializableLinkPreview | null =\n  SerializableLinkPreviewSchemaContract.safeParse;\n"
  },
  {
    "path": "apps/www/components/tool-ui/linkedin-post/README.md",
    "content": "# Linkedin Post\n\nImplementation for the \"linkedin-post\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/linkedin-post/index.ts\n- serializable schema + parse helpers: components/tool-ui/linkedin-post/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/social-post/content.mdx\n- Preset payload: lib/presets/linkedin-post.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/linkedin-post/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn      → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Button  → shadcn/ui Button\n *   Tooltip → shadcn/ui Tooltip\n */\n\nexport { cn } from \"@/lib/utils\";\nexport { Button } from \"@/components/ui/button\";\nexport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/linkedin-post/index.ts",
    "content": "export { LinkedInPost } from \"./linkedin-post\";\nexport type { LinkedInPostProps } from \"./linkedin-post\";\nexport type {\n  LinkedInPostData,\n  LinkedInPostAuthor,\n  LinkedInPostMedia,\n  LinkedInPostLinkPreview,\n  LinkedInPostStats,\n} from \"./schema\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/linkedin-post/linkedin-post.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ThumbsUp, Share } from \"lucide-react\";\nimport {\n  cn,\n  Button,\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"./_adapter\";\nimport { formatCount, formatRelativeTime, getDomain } from \"../shared/utils\";\n\nimport { resolveSafeNavigationHref } from \"../shared/media\";\nimport type {\n  LinkedInPostData,\n  LinkedInPostMedia,\n  LinkedInPostLinkPreview,\n} from \"./schema\";\n\nconst TEXT_PREVIEW_LENGTH = 280;\n\nexport interface LinkedInPostProps {\n  post: LinkedInPostData;\n  className?: string;\n  onAction?: (action: string, post: LinkedInPostData) => void;\n}\n\nfunction LinkedInLogo({ className }: { className?: string }) {\n  return (\n    <svg\n      viewBox=\"0 0 72 72\"\n      className={className}\n      role=\"img\"\n      aria-label=\"LinkedIn logo\"\n    >\n      <g fill=\"none\" fillRule=\"evenodd\">\n        <path\n          d=\"M8 72h56c4.42 0 8-3.58 8-8V8c0-4.42-3.58-8-8-8H8C3.58 0 0 3.58 0 8v56c0 4.42 3.58 8 8 8z\"\n          fill=\"currentColor\"\n        />\n        <path\n          d=\"M62 62H51.3V43.8c0-4.98-1.9-7.78-5.83-7.78-4.3 0-6.54 2.9-6.54 7.78V62H28.63V27.33h10.3v4.67c0 0 3.1-5.73 10.45-5.73 7.36 0 12.62 4.5 12.62 13.8V62zM16.35 22.8c-3.5 0-6.35-2.86-6.35-6.4 0-3.52 2.85-6.4 6.35-6.4 3.5 0 6.35 2.88 6.35 6.4 0 3.54-2.85 6.4-6.35 6.4zM11.03 62h10.74V27.33H11.03V62z\"\n          fill=\"#FFF\"\n        />\n      </g>\n    </svg>\n  );\n}\n\nfunction Header({\n  author,\n  createdAt,\n}: {\n  author: LinkedInPostData[\"author\"];\n  createdAt?: string;\n}) {\n  return (\n    <header className=\"flex items-start gap-3\">\n      <img\n        src={author.avatarUrl}\n        alt={`${author.name} avatar`}\n        width={48}\n        height={48}\n        className=\"size-12 rounded-full object-cover\"\n      />\n      <div className=\"flex min-w-0 flex-1 flex-col leading-tight\">\n        <span className=\"text-sm font-semibold\">{author.name}</span>\n        {author.headline && (\n          <span className=\"text-muted-foreground line-clamp-1 text-xs\">\n            {author.headline}\n          </span>\n        )}\n        {createdAt && (\n          <div className=\"text-muted-foreground mt-0.5 flex items-center gap-1 text-xs\">\n            <span>{formatRelativeTime(createdAt)}</span>\n            <span>·</span>\n            <span>Edited</span>\n          </div>\n        )}\n      </div>\n      <LinkedInLogo className=\"size-5 text-[#0077b5]\" />\n    </header>\n  );\n}\n\nfunction PostBody({ text }: { text?: string }) {\n  const [isExpanded, setIsExpanded] = React.useState(false);\n  const shouldTruncate = text && text.length > TEXT_PREVIEW_LENGTH;\n\n  if (!text) return null;\n\n  return (\n    <div className=\"text-sm leading-relaxed text-pretty wrap-break-word whitespace-pre-wrap\">\n      {shouldTruncate && !isExpanded ? (\n        <>\n          {text.slice(0, TEXT_PREVIEW_LENGTH)}\n          ...\n          <button\n            onClick={() => setIsExpanded(true)}\n            className=\"text-muted-foreground hover:text-foreground ml-1 font-medium hover:underline\"\n          >\n            see more\n          </button>\n        </>\n      ) : (\n        text\n      )}\n    </div>\n  );\n}\n\nfunction PostMedia({ media }: { media: LinkedInPostMedia }) {\n  return (\n    <div className=\"overflow-hidden rounded-lg\">\n      {media.type === \"image\" ? (\n        <img\n          src={media.url}\n          alt={media.alt}\n          className=\"w-full object-cover\"\n          style={{ aspectRatio: \"16/9\" }}\n          loading=\"lazy\"\n        />\n      ) : (\n        <video\n          src={media.url}\n          controls\n          playsInline\n          className=\"w-full object-contain\"\n          style={{ aspectRatio: \"16/9\" }}\n        />\n      )}\n    </div>\n  );\n}\n\nfunction PostLinkPreview({ preview }: { preview: LinkedInPostLinkPreview }) {\n  const href = resolveSafeNavigationHref(preview.url);\n  const domain = preview.domain ?? getDomain(preview.url);\n  const content = (\n    <>\n      {preview.imageUrl && (\n        <img\n          src={preview.imageUrl}\n          alt=\"\"\n          className=\"h-40 w-full object-cover\"\n          loading=\"lazy\"\n        />\n      )}\n      <div className=\"p-3\">\n        {preview.title && (\n          <div className=\"line-clamp-2 font-medium text-pretty\">\n            {preview.title}\n          </div>\n        )}\n        {domain && (\n          <div className=\"text-muted-foreground mt-1 text-xs\">{domain}</div>\n        )}\n      </div>\n    </>\n  );\n\n  if (!href) {\n    return (\n      <div className=\"block overflow-hidden rounded-lg border\">{content}</div>\n    );\n  }\n\n  return (\n    <a\n      href={href}\n      target=\"_blank\"\n      rel=\"noopener noreferrer\"\n      className=\"hover:bg-muted/50 block overflow-hidden rounded-lg border transition-colors\"\n    >\n      {content}\n    </a>\n  );\n}\n\nfunction ActionButton({\n  icon: Icon,\n  label,\n  count,\n  active,\n  hoverColor,\n  activeColor,\n  onClick,\n}: {\n  icon: React.ComponentType<{ className?: string }>;\n  label: string;\n  count?: number;\n  active?: boolean;\n  hoverColor: string;\n  activeColor?: string;\n  onClick: () => void;\n}) {\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>\n        <Button\n          variant=\"ghost\"\n          size=\"sm\"\n          onClick={(e) => {\n            e.stopPropagation();\n            onClick();\n          }}\n          className={cn(\n            \"h-auto gap-1.5 px-3 py-2\",\n            hoverColor,\n            active && activeColor,\n          )}\n          aria-label={label}\n        >\n          <Icon className=\"size-4\" />\n          <span className=\"text-xs font-medium\">{label}</span>\n          {count !== undefined && (\n            <span className=\"text-muted-foreground text-xs\">\n              ({formatCount(count)})\n            </span>\n          )}\n        </Button>\n      </TooltipTrigger>\n      <TooltipContent>{label}</TooltipContent>\n    </Tooltip>\n  );\n}\n\nfunction PostActions({\n  stats,\n  onAction,\n}: {\n  stats?: LinkedInPostData[\"stats\"];\n  onAction: (action: string) => void;\n}) {\n  return (\n    <TooltipProvider delayDuration={300}>\n      <div className=\"mt-1 flex items-center gap-1 border-t pt-1.5\">\n        <ActionButton\n          icon={ThumbsUp}\n          label=\"Like\"\n          active={stats?.isLiked}\n          hoverColor=\"hover:bg-muted\"\n          activeColor=\"text-blue-600 fill-blue-600\"\n          onClick={() => onAction(\"like\")}\n        />\n        <ActionButton\n          icon={Share}\n          label=\"Share\"\n          hoverColor=\"hover:bg-muted\"\n          onClick={() => onAction(\"share\")}\n        />\n      </div>\n    </TooltipProvider>\n  );\n}\n\nexport function LinkedInPost({ post, className, onAction }: LinkedInPostProps) {\n  return (\n    <div\n      className={cn(\"flex max-w-xl flex-col gap-3\", className)}\n      data-tool-ui-id={post.id}\n      data-slot=\"linkedin-post\"\n    >\n      <article className=\"bg-card flex flex-col gap-3 rounded-lg border p-3 shadow-sm\">\n        <Header author={post.author} createdAt={post.createdAt} />\n        <PostBody text={post.text} />\n\n        {post.media && <PostMedia media={post.media} />}\n\n        {post.linkPreview && !post.media && (\n          <PostLinkPreview preview={post.linkPreview} />\n        )}\n\n        <PostActions\n          stats={post.stats}\n          onAction={(action) => onAction?.(action, post)}\n        />\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/linkedin-post/schema.ts",
    "content": "import { z } from \"zod\";\nimport { defineToolUiContract } from \"../shared/contract\";\n\nexport const LinkedInPostAuthorSchema = z.object({\n  name: z.string(),\n  avatarUrl: z.string(),\n  headline: z.string().optional(),\n});\n\nexport const LinkedInPostMediaSchema = z.object({\n  type: z.enum([\"image\", \"video\"]),\n  url: z.string(),\n  alt: z.string(),\n});\n\nexport const LinkedInPostLinkPreviewSchema = z.object({\n  url: z.string(),\n  title: z.string().optional(),\n  description: z.string().optional(),\n  imageUrl: z.string().optional(),\n  domain: z.string().optional(),\n});\n\nexport const LinkedInPostStatsSchema = z.object({\n  likes: z.number().optional(),\n  isLiked: z.boolean().optional(),\n});\n\nexport const SerializableLinkedInPostSchema = z.object({\n  id: z.string(),\n  author: LinkedInPostAuthorSchema,\n  text: z.string().optional(),\n  media: LinkedInPostMediaSchema.optional(),\n  linkPreview: LinkedInPostLinkPreviewSchema.optional(),\n  stats: LinkedInPostStatsSchema.optional(),\n  createdAt: z.string().optional(),\n});\n\nexport type LinkedInPostData = z.infer<typeof SerializableLinkedInPostSchema>;\n\nexport type LinkedInPostAuthor = z.infer<typeof LinkedInPostAuthorSchema>;\nexport type LinkedInPostMedia = z.infer<typeof LinkedInPostMediaSchema>;\nexport type LinkedInPostLinkPreview = z.infer<\n  typeof LinkedInPostLinkPreviewSchema\n>;\nexport type LinkedInPostStats = z.infer<typeof LinkedInPostStatsSchema>;\n\nconst SerializableLinkedInPostSchemaContract = defineToolUiContract(\n  \"LinkedInPost\",\n  SerializableLinkedInPostSchema,\n);\n\nexport const parseSerializableLinkedInPost: (\n  input: unknown,\n) => LinkedInPostData = SerializableLinkedInPostSchemaContract.parse;\n\nexport const safeParseSerializableLinkedInPost: (\n  input: unknown,\n) => LinkedInPostData | null = SerializableLinkedInPostSchemaContract.safeParse;\n"
  },
  {
    "path": "apps/www/components/tool-ui/message-draft/README.md",
    "content": "# Message Draft\n\nImplementation for the \"message-draft\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/message-draft/index.tsx\n- serializable schema + parse helpers: components/tool-ui/message-draft/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/message-draft/content.mdx\n- Preset payload: lib/presets/message-draft.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/message-draft/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn        → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Button    → shadcn/ui Button\n */\n\nexport { cn } from \"@/lib/utils\";\nexport { Button } from \"@/components/ui/button\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/message-draft/index.tsx",
    "content": "export { MessageDraft } from \"./message-draft\";\nexport {\n  type SerializableMessageDraft,\n  type SerializableEmailDraft,\n  type SerializableSlackDraft,\n  type MessageDraftChannel,\n  type MessageDraftOutcome,\n  type SlackTarget,\n  type MessageDraftProps,\n} from \"./schema\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/message-draft/message-draft.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn, Button } from \"./_adapter\";\nimport type {\n  MessageDraftProps,\n  SerializableEmailDraft,\n  SerializableSlackDraft,\n} from \"./schema\";\nimport { ActionButtons } from \"../shared/action-buttons\";\nimport type { Action } from \"../shared/schema\";\nimport { Check, ChevronDown } from \"lucide-react\";\n\ntype DraftState = \"review\" | \"sending\" | \"sent\" | \"cancelled\";\ntype DraftOutcome = MessageDraftProps[\"outcome\"];\n\nconst DEFAULT_GRACE_PERIOD = 5000;\nconst COLLAPSED_BODY_HEIGHT = 280;\n\ninterface RecipientRowProps {\n  label: string;\n  recipients: string[];\n  maxVisible?: number;\n  muted?: boolean;\n}\n\nfunction RecipientRow({\n  label,\n  recipients,\n  maxVisible = 3,\n  muted = false,\n}: RecipientRowProps) {\n  const visibleRecipients = recipients.slice(0, maxVisible);\n  const overflowCount = recipients.length - maxVisible;\n\n  return (\n    <tr className=\"text-sm\">\n      <td className=\"text-muted-foreground w-0 pr-4 pb-1 text-right align-top font-medium whitespace-nowrap\">\n        {label}\n      </td>\n      <td className={cn(\"pb-1 align-top\", muted && \"text-muted-foreground\")}>\n        {visibleRecipients.join(\", \")}\n        {overflowCount > 0 && (\n          <span className=\"text-muted-foreground\"> +{overflowCount} more</span>\n        )}\n      </td>\n    </tr>\n  );\n}\n\ninterface SingleFieldRowProps {\n  label: string;\n  value: string;\n}\n\nfunction SingleFieldRow({ label, value }: SingleFieldRowProps) {\n  return (\n    <tr className=\"text-sm\">\n      <td className=\"text-muted-foreground w-0 pr-4 pb-1 text-right align-top font-medium whitespace-nowrap\">\n        {label}\n      </td>\n      <td className=\"pb-1 align-top\">{value}</td>\n    </tr>\n  );\n}\n\ninterface ExpandableBodyProps {\n  body: string;\n  isExpanded: boolean;\n  onNeedsExpansionChange?: (needsExpansion: boolean) => void;\n}\n\nfunction ExpandableBody({\n  body,\n  isExpanded,\n  onNeedsExpansionChange,\n}: ExpandableBodyProps) {\n  const [needsExpansion, setNeedsExpansion] = React.useState<boolean | null>(\n    null,\n  );\n  const contentRef = React.useRef<HTMLDivElement>(null);\n\n  React.useLayoutEffect(() => {\n    if (contentRef.current) {\n      const needs = contentRef.current.scrollHeight > COLLAPSED_BODY_HEIGHT;\n      setNeedsExpansion(needs);\n      onNeedsExpansionChange?.(needs);\n    }\n  }, [body, onNeedsExpansionChange]);\n\n  return (\n    <div className=\"relative\">\n      <div\n        ref={contentRef}\n        className={cn(\n          \"overflow-hidden text-sm leading-relaxed\",\n          needsExpansion !== null &&\n            \"transition-[max-height] duration-300 ease-in-out\",\n        )}\n        style={{\n          maxHeight:\n            needsExpansion === null\n              ? `${COLLAPSED_BODY_HEIGHT}px`\n              : isExpanded || !needsExpansion\n                ? `${contentRef.current?.scrollHeight ?? 1000}px`\n                : `${COLLAPSED_BODY_HEIGHT}px`,\n        }}\n      >\n        <p className=\"pt-1 whitespace-pre-wrap\">{body}</p>\n      </div>\n      {needsExpansion && (\n        <div\n          className={cn(\n            \"from-card pointer-events-none absolute inset-x-0 bottom-0 bg-gradient-to-t to-transparent transition-[height] duration-300 ease-in-out\",\n            isExpanded ? \"h-0\" : \"h-12\",\n          )}\n        />\n      )}\n    </div>\n  );\n}\n\ninterface EmailDraftContentProps {\n  draft: SerializableEmailDraft;\n  titleId: string;\n  isExpanded: boolean;\n  onNeedsExpansionChange?: (needsExpansion: boolean) => void;\n}\n\nfunction EmailDraftContent({\n  draft,\n  titleId,\n  isExpanded,\n  onNeedsExpansionChange,\n}: EmailDraftContentProps) {\n  return (\n    <>\n      <h2 id={titleId} className=\"pt-2 text-base leading-tight font-semibold\">\n        {draft.subject}\n      </h2>\n\n      <table className=\"w-full\">\n        <tbody>\n          {draft.from && <SingleFieldRow label=\"From\" value={draft.from} />}\n          <RecipientRow label=\"To\" recipients={draft.to} />\n          {draft.cc && draft.cc.length > 0 && (\n            <RecipientRow label=\"Cc\" recipients={draft.cc} />\n          )}\n          {draft.bcc && draft.bcc.length > 0 && (\n            <RecipientRow label=\"Bcc\" recipients={draft.bcc} muted />\n          )}\n        </tbody>\n      </table>\n\n      <div className=\"bg-border -mx-5 h-px\" role=\"separator\" />\n\n      <ExpandableBody\n        body={draft.body}\n        isExpanded={isExpanded}\n        onNeedsExpansionChange={onNeedsExpansionChange}\n      />\n    </>\n  );\n}\n\ninterface SlackDraftContentProps {\n  draft: SerializableSlackDraft;\n  titleId: string;\n  isExpanded: boolean;\n  onNeedsExpansionChange?: (needsExpansion: boolean) => void;\n}\n\nfunction SlackLogo({ className }: { className?: string }) {\n  return (\n    <svg className={className} viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n      <path\n        fill=\"#E01E5A\"\n        d=\"M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zm1.271 0a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313z\"\n      />\n      <path\n        fill=\"#36C5F0\"\n        d=\"M8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zm0 1.271a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312z\"\n      />\n      <path\n        fill=\"#2EB67D\"\n        d=\"M18.958 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.52 2.521h-2.522V8.834zm-1.271 0a2.528 2.528 0 0 1-2.521 2.521 2.528 2.528 0 0 1-2.521-2.521V2.522A2.528 2.528 0 0 1 15.165 0a2.528 2.528 0 0 1 2.522 2.522v6.312z\"\n      />\n      <path\n        fill=\"#ECB22E\"\n        d=\"M15.165 18.958a2.528 2.528 0 0 1 2.522 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.521-2.52v-2.522h2.521zm0-1.271a2.527 2.527 0 0 1-2.521-2.521 2.526 2.526 0 0 1 2.521-2.521h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.521h-6.313z\"\n      />\n    </svg>\n  );\n}\n\nfunction SlackDraftContent({\n  draft,\n  titleId,\n  isExpanded,\n  onNeedsExpansionChange,\n}: SlackDraftContentProps) {\n  const { target } = draft;\n  const isChannel = target.type === \"channel\";\n  const targetDisplay = isChannel\n    ? `#${target.name}`\n    : `Message to @${target.name}`;\n  const memberCount = isChannel ? target.memberCount : undefined;\n\n  return (\n    <>\n      <div\n        id={titleId}\n        className=\"flex items-center gap-1.5 text-sm font-medium\"\n      >\n        <SlackLogo className=\"size-4\" />\n        <span>{targetDisplay}</span>\n        {memberCount !== undefined && (\n          <span className=\"text-muted-foreground ml-auto text-sm font-normal\">\n            {memberCount.toLocaleString()} members\n          </span>\n        )}\n      </div>\n\n      <div className=\"bg-border -mx-5 h-px\" role=\"separator\" />\n\n      <ExpandableBody\n        body={draft.body}\n        isExpanded={isExpanded}\n        onNeedsExpansionChange={onNeedsExpansionChange}\n      />\n    </>\n  );\n}\n\nfunction formatSentTime(date: Date): string {\n  return date.toLocaleTimeString(undefined, {\n    hour: \"numeric\",\n    minute: \"2-digit\",\n  });\n}\n\nexport function resolveStateFromOutcome(outcome: DraftOutcome): DraftState {\n  if (outcome === \"sent\") return \"sent\";\n  if (outcome === \"cancelled\") return \"cancelled\";\n  return \"review\";\n}\n\nexport function resolveOutcomeTransition(\n  previousOutcome: DraftOutcome,\n  nextOutcome: DraftOutcome,\n): DraftState | null {\n  if (previousOutcome === nextOutcome) {\n    return null;\n  }\n\n  return resolveStateFromOutcome(nextOutcome);\n}\n\ninterface SentConfirmationProps {\n  sentAt: Date;\n}\n\nfunction SentConfirmation({ sentAt }: SentConfirmationProps) {\n  return (\n    <div\n      className=\"flex items-center justify-end gap-2 text-sm\"\n      role=\"status\"\n      aria-label=\"Message sent\"\n    >\n      <span className=\"text-muted-foreground\">\n        Sent at {formatSentTime(sentAt)}\n      </span>\n      <span className=\"bg-primary/10 text-primary flex size-6 shrink-0 items-center justify-center rounded-full\">\n        <Check className=\"size-3.5\" />\n      </span>\n    </div>\n  );\n}\n\nexport function MessageDraft(props: MessageDraftProps) {\n  const {\n    id,\n    className,\n    outcome,\n    undoGracePeriod = DEFAULT_GRACE_PERIOD,\n    onSend,\n    onUndo,\n    onCancel,\n  } = props;\n\n  const [state, setState] = React.useState<DraftState>(() =>\n    resolveStateFromOutcome(outcome),\n  );\n  const [countdown, setCountdown] = React.useState(\n    Math.ceil(undoGracePeriod / 1000),\n  );\n  const [sentAt, setSentAt] = React.useState<Date | null>(() =>\n    outcome === \"sent\" ? new Date() : null,\n  );\n  const [isExpanded, setIsExpanded] = React.useState(false);\n  const [needsExpansion, setNeedsExpansion] = React.useState(false);\n  const undoButtonRef = React.useRef<HTMLButtonElement>(null);\n  const timerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n  const countdownRef = React.useRef<ReturnType<typeof setInterval> | null>(\n    null,\n  );\n  const previousOutcomeRef = React.useRef<DraftOutcome>(outcome);\n\n  const clearTimers = React.useCallback(() => {\n    if (timerRef.current) {\n      clearTimeout(timerRef.current);\n      timerRef.current = null;\n    }\n    if (countdownRef.current) {\n      clearInterval(countdownRef.current);\n      countdownRef.current = null;\n    }\n  }, []);\n\n  React.useEffect(() => {\n    return clearTimers;\n  }, [clearTimers]);\n\n  React.useEffect(() => {\n    const nextState = resolveOutcomeTransition(\n      previousOutcomeRef.current,\n      outcome,\n    );\n\n    previousOutcomeRef.current = outcome;\n\n    if (nextState === null) {\n      return;\n    }\n\n    clearTimers();\n    setState(nextState);\n    setCountdown(Math.ceil(undoGracePeriod / 1000));\n    setSentAt(nextState === \"sent\" ? new Date() : null);\n  }, [outcome, undoGracePeriod, clearTimers]);\n\n  React.useEffect(() => {\n    if (state === \"sending\") {\n      undoButtonRef.current?.focus();\n\n      setCountdown(Math.ceil(undoGracePeriod / 1000));\n\n      countdownRef.current = setInterval(() => {\n        setCountdown((prev) => {\n          if (prev <= 1) {\n            if (countdownRef.current) {\n              clearInterval(countdownRef.current);\n              countdownRef.current = null;\n            }\n            return 0;\n          }\n          return prev - 1;\n        });\n      }, 1000);\n\n      timerRef.current = setTimeout(async () => {\n        clearTimers();\n        await onSend?.();\n        setSentAt(new Date());\n        setState(\"sent\");\n      }, undoGracePeriod);\n    }\n  }, [state, undoGracePeriod, onSend, clearTimers]);\n\n  const handleSend = React.useCallback(() => {\n    setState(\"sending\");\n  }, []);\n\n  const handleUndo = React.useCallback(() => {\n    clearTimers();\n    setState(\"review\");\n    onUndo?.();\n  }, [clearTimers, onUndo]);\n\n  const handleCancel = React.useCallback(() => {\n    clearTimers();\n    setState(\"cancelled\");\n    onCancel?.();\n  }, [clearTimers, onCancel]);\n\n  const handleKeyDown = React.useCallback(\n    (event: React.KeyboardEvent) => {\n      if (event.key === \"Escape\" && state === \"review\") {\n        event.preventDefault();\n        handleCancel();\n      }\n    },\n    [state, handleCancel],\n  );\n\n  const handleNeedsExpansionChange = React.useCallback((needs: boolean) => {\n    setNeedsExpansion(needs);\n  }, []);\n\n  const handleToggleExpand = React.useCallback(() => {\n    setIsExpanded((prev) => !prev);\n  }, []);\n\n  const handleAction = React.useCallback(\n    async (actionId: string) => {\n      if (actionId === \"send\") {\n        handleSend();\n      } else if (actionId === \"cancel\") {\n        handleCancel();\n      }\n    },\n    [handleSend, handleCancel],\n  );\n\n  const actions: Action[] = [\n    {\n      id: \"cancel\",\n      label: \"Cancel\",\n      variant: \"ghost\",\n    },\n    {\n      id: \"send\",\n      label: \"Send\",\n      variant: \"default\",\n    },\n  ];\n\n  const expandButton = needsExpansion ? (\n    <Button\n      variant=\"ghost\"\n      size=\"sm\"\n      onClick={handleToggleExpand}\n      className=\"h-7 gap-1 px-2 text-sm\"\n    >\n      {isExpanded ? \"Show less\" : \"Read more\"}\n      <ChevronDown className={cn(\"size-3\", isExpanded && \"rotate-180\")} />\n    </Button>\n  ) : null;\n\n  const renderActions = () => {\n    switch (state) {\n      case \"sending\":\n        return (\n          <div\n            className=\"flex items-center justify-end gap-3\"\n            aria-live=\"polite\"\n          >\n            <span className=\"text-muted-foreground text-sm\">\n              Sending in {countdown}s\n            </span>\n            <Button\n              ref={undoButtonRef}\n              variant=\"outline\"\n              size=\"sm\"\n              onClick={handleUndo}\n              className=\"rounded-full\"\n            >\n              Undo\n            </Button>\n          </div>\n        );\n      case \"sent\":\n        return <SentConfirmation sentAt={sentAt ?? new Date()} />;\n      case \"cancelled\":\n        return null;\n      default:\n        return <ActionButtons actions={actions} onAction={handleAction} />;\n    }\n  };\n\n  if (state === \"cancelled\") {\n    return null;\n  }\n\n  return (\n    <article\n      className={cn(\n        \"flex w-full max-w-lg min-w-64 flex-col gap-3\",\n        \"text-foreground\",\n        className,\n      )}\n      data-slot=\"message-draft\"\n      data-tool-ui-id={id}\n      data-state={state}\n      aria-labelledby={`${id}-title`}\n      onKeyDown={handleKeyDown}\n    >\n      <div className=\"bg-card flex w-full flex-col gap-3 rounded-2xl border px-5 pt-3 pb-5 shadow-xs transition-none\">\n        {props.channel === \"email\" ? (\n          <EmailDraftContent\n            draft={props}\n            titleId={`${id}-title`}\n            isExpanded={isExpanded}\n            onNeedsExpansionChange={handleNeedsExpansionChange}\n          />\n        ) : (\n          <SlackDraftContent\n            draft={props}\n            titleId={`${id}-title`}\n            isExpanded={isExpanded}\n            onNeedsExpansionChange={handleNeedsExpansionChange}\n          />\n        )}\n\n        {expandButton}\n      </div>\n\n      <div className=\"@container/actions\">{renderActions()}</div>\n    </article>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/message-draft/schema.ts",
    "content": "import { z } from \"zod\";\nimport { ToolUIIdSchema, ToolUIRoleSchema } from \"../shared/schema\";\nimport { defineToolUiContract } from \"../shared/contract\";\n\nexport const MessageDraftChannelSchema = z.enum([\"email\", \"slack\"]);\n\nexport type MessageDraftChannel = z.infer<typeof MessageDraftChannelSchema>;\n\nexport const MessageDraftOutcomeSchema = z.enum([\"sent\", \"cancelled\"]);\n\nexport type MessageDraftOutcome = z.infer<typeof MessageDraftOutcomeSchema>;\n\nconst SlackTargetSchema = z.discriminatedUnion(\"type\", [\n  z.object({\n    type: z.literal(\"channel\"),\n    name: z.string().min(1),\n    memberCount: z.number().optional(),\n  }),\n  z.object({ type: z.literal(\"dm\"), name: z.string().min(1) }),\n]);\n\nexport type SlackTarget = z.infer<typeof SlackTargetSchema>;\n\nexport const SerializableEmailDraftSchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  body: z.string().min(1),\n  outcome: MessageDraftOutcomeSchema.optional(),\n  channel: z.literal(\"email\"),\n  subject: z.string().min(1),\n  from: z.string().optional(),\n  to: z.array(z.string()).min(1),\n  cc: z.array(z.string()).optional(),\n  bcc: z.array(z.string()).optional(),\n});\n\nexport const SerializableSlackDraftSchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  body: z.string().min(1),\n  outcome: MessageDraftOutcomeSchema.optional(),\n  channel: z.literal(\"slack\"),\n  target: SlackTargetSchema,\n});\n\nexport const SerializableMessageDraftSchema = z.discriminatedUnion(\"channel\", [\n  SerializableEmailDraftSchema,\n  SerializableSlackDraftSchema,\n]);\n\nexport type SerializableMessageDraft = z.infer<\n  typeof SerializableMessageDraftSchema\n>;\n\nexport type SerializableEmailDraft = z.infer<\n  typeof SerializableEmailDraftSchema\n>;\n\nexport type SerializableSlackDraft = z.infer<\n  typeof SerializableSlackDraftSchema\n>;\n\nconst SerializableMessageDraftSchemaContract = defineToolUiContract(\n  \"MessageDraft\",\n  SerializableMessageDraftSchema,\n);\n\nexport const parseSerializableMessageDraft: (\n  input: unknown,\n) => SerializableMessageDraft = SerializableMessageDraftSchemaContract.parse;\n\nexport const safeParseSerializableMessageDraft: (\n  input: unknown,\n) => SerializableMessageDraft | null =\n  SerializableMessageDraftSchemaContract.safeParse;\n\nexport type MessageDraftProps = SerializableMessageDraft & {\n  className?: string;\n  undoGracePeriod?: number;\n  onSend?: () => void | Promise<void>;\n  onUndo?: () => void;\n  onCancel?: () => void;\n};\n"
  },
  {
    "path": "apps/www/components/tool-ui/option-list/README.md",
    "content": "# Option List\n\nImplementation for the \"option-list\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/option-list/index.tsx\n- serializable schema + parse helpers: components/tool-ui/option-list/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/option-list/content.mdx\n- Preset payload: lib/presets/option-list.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/option-list/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn        → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Button    → shadcn/ui Button\n *   Separator → shadcn/ui Separator\n */\n\nexport { cn } from \"@/lib/utils\";\nexport { Button } from \"@/components/ui/button\";\nexport { Separator } from \"@/components/ui/separator\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/option-list/index.tsx",
    "content": "export { OptionList } from \"./option-list\";\nexport type {\n  OptionListProps,\n  OptionListOption,\n  OptionListSelection,\n  SerializableOptionList,\n} from \"./schema\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/option-list/option-list.tsx",
    "content": "\"use client\";\n\nimport {\n  useMemo,\n  useState,\n  useCallback,\n  useEffect,\n  useRef,\n  Fragment,\n} from \"react\";\nimport type { KeyboardEvent } from \"react\";\nimport type {\n  OptionListProps,\n  OptionListSelection,\n  OptionListOption,\n} from \"./schema\";\nimport {\n  normalizeSelectionForOptions,\n  parseSelectionToIdSet,\n} from \"./selection\";\nimport { ActionButtons } from \"../shared/action-buttons\";\nimport { normalizeActionsConfig } from \"../shared/actions-config\";\nimport type { Action } from \"../shared/schema\";\nimport { cn, Button, Separator } from \"./_adapter\";\nimport { Check } from \"lucide-react\";\n\nfunction convertIdSetToSelection(\n  selected: Set<string>,\n  mode: \"multi\" | \"single\",\n): OptionListSelection {\n  if (mode === \"single\") {\n    const [first] = selected;\n    return first ?? null;\n  }\n  return Array.from(selected);\n}\n\nfunction areSetsEqual(a: Set<string>, b: Set<string>) {\n  if (a.size !== b.size) return false;\n  for (const val of a) {\n    if (!b.has(val)) return false;\n  }\n  return true;\n}\n\ninterface SelectionIndicatorProps {\n  mode: \"multi\" | \"single\";\n  isSelected: boolean;\n  disabled?: boolean;\n}\n\nfunction SelectionIndicator({\n  mode,\n  isSelected,\n  disabled,\n}: SelectionIndicatorProps) {\n  const shape = mode === \"single\" ? \"rounded-full\" : \"rounded\";\n\n  return (\n    <div\n      className={cn(\n        \"flex size-4 shrink-0 items-center justify-center border-2 transition-colors\",\n        shape,\n        isSelected && \"border-primary bg-primary text-primary-foreground\",\n        !isSelected && \"border-muted-foreground/50\",\n        disabled && \"opacity-50\",\n      )}\n    >\n      {mode === \"multi\" && isSelected && <Check className=\"size-3\" />}\n      {mode === \"single\" && isSelected && (\n        <span className=\"size-2 rounded-full bg-current\" />\n      )}\n    </div>\n  );\n}\n\ninterface OptionItemProps {\n  option: OptionListOption;\n  isSelected: boolean;\n  isDisabled: boolean;\n  selectionMode: \"multi\" | \"single\";\n  isFirst: boolean;\n  isLast: boolean;\n  onToggle: () => void;\n  tabIndex?: number;\n  onFocus?: () => void;\n  buttonRef?: (el: HTMLButtonElement | null) => void;\n}\n\nfunction OptionItem({\n  option,\n  isSelected,\n  isDisabled,\n  selectionMode,\n  isFirst,\n  isLast,\n  onToggle,\n  tabIndex,\n  onFocus,\n  buttonRef,\n}: OptionItemProps) {\n  const hasAdjacentOptions = !isFirst && !isLast;\n\n  return (\n    <Button\n      ref={buttonRef}\n      data-id={option.id}\n      variant=\"ghost\"\n      size=\"lg\"\n      role=\"option\"\n      aria-selected={isSelected}\n      onClick={onToggle}\n      onFocus={onFocus}\n      tabIndex={tabIndex}\n      disabled={isDisabled}\n      className={cn(\n        \"peer group relative h-auto min-h-[50px] w-full justify-start text-left text-sm font-medium\",\n        \"rounded-none border-0 bg-transparent px-0 py-2 text-base shadow-none transition-none hover:bg-transparent! @md/option-list:text-sm\",\n        isFirst && \"pb-2.5\",\n        hasAdjacentOptions && \"py-2.5\",\n      )}\n    >\n      <span\n        className={cn(\n          \"bg-primary/5 absolute inset-0 -mx-3 -my-0.5 rounded-xl opacity-0 transition-opacity group-hover:opacity-100\",\n        )}\n      />\n      <div className=\"relative flex items-start gap-3\">\n        <span className=\"flex h-6 items-center\">\n          <SelectionIndicator\n            mode={selectionMode}\n            isSelected={isSelected}\n            disabled={option.disabled}\n          />\n        </span>\n        {option.icon && (\n          <span className=\"flex h-6 items-center\">{option.icon}</span>\n        )}\n        <div className=\"flex flex-col text-left\">\n          <span className=\"leading-6 text-pretty\">{option.label}</span>\n          {option.description && (\n            <span className=\"text-muted-foreground text-sm font-normal text-pretty\">\n              {option.description}\n            </span>\n          )}\n        </div>\n      </div>\n    </Button>\n  );\n}\n\ninterface OptionListConfirmationProps {\n  id: string;\n  options: OptionListOption[];\n  selectedIds: Set<string>;\n  className?: string;\n}\n\nfunction OptionListConfirmation({\n  id,\n  options,\n  selectedIds,\n  className,\n}: OptionListConfirmationProps) {\n  const confirmedOptions = options.filter((opt) => selectedIds.has(opt.id));\n\n  return (\n    <div\n      className={cn(\n        \"@container/option-list flex w-full max-w-md min-w-80 flex-col\",\n        \"text-foreground\",\n        \"motion-safe:animate-in motion-safe:fade-in motion-safe:blur-in-sm motion-safe:zoom-in-95 motion-safe:duration-300 motion-safe:ease-[cubic-bezier(0.16,1,0.3,1)] motion-safe:fill-mode-both\",\n        className,\n      )}\n      data-slot=\"option-list\"\n      data-tool-ui-id={id}\n      data-receipt=\"true\"\n      role=\"status\"\n      aria-label=\"Confirmed selection\"\n    >\n      <div\n        className={cn(\n          \"bg-card/60 flex w-full flex-col overflow-hidden rounded-2xl border px-5 py-2.5 shadow-xs\",\n        )}\n      >\n        {confirmedOptions.map((option, index) => (\n          <Fragment key={option.id}>\n            {index > 0 && (\n              <Separator className=\"my-1.5\" orientation=\"horizontal\" />\n            )}\n            <div className=\"flex items-start gap-3 py-1\">\n              <span className=\"flex h-6 items-center\">\n                <Check className=\"text-primary size-4 shrink-0\" />\n              </span>\n              {option.icon && (\n                <span className=\"flex h-6 items-center\">{option.icon}</span>\n              )}\n              <div className=\"flex flex-col text-left\">\n                <span className=\"text-base leading-6 font-medium text-pretty @md/option-list:text-sm\">\n                  {option.label}\n                </span>\n                {option.description && (\n                  <span className=\"text-muted-foreground text-sm font-normal text-pretty\">\n                    {option.description}\n                  </span>\n                )}\n              </div>\n            </div>\n          </Fragment>\n        ))}\n      </div>\n    </div>\n  );\n}\n\nexport function OptionList({\n  id,\n  options,\n  selectionMode = \"multi\",\n  minSelections = 1,\n  maxSelections,\n  value,\n  defaultValue,\n  choice,\n  onChange,\n  actions,\n  onAction,\n  onBeforeAction,\n  className,\n}: OptionListProps) {\n  if (process.env[\"NODE_ENV\"] !== \"production\") {\n    if (value !== undefined && defaultValue !== undefined) {\n      console.warn(\n        \"[OptionList] Both `value` (controlled) and `defaultValue` (uncontrolled) were provided. `defaultValue` is ignored when `value` is set.\",\n      );\n    }\n    if (value !== undefined && !onChange) {\n      console.warn(\n        \"[OptionList] `value` was provided without `onChange`. This makes OptionList controlled; selection will not update unless the parent updates `value`.\",\n      );\n    }\n  }\n\n  const effectiveMaxSelections = selectionMode === \"single\" ? 1 : maxSelections;\n  const optionIds = useMemo(\n    () => new Set(options.map((option) => option.id)),\n    [options],\n  );\n\n  const [uncontrolledSelected, setUncontrolledSelected] = useState<Set<string>>(\n    () =>\n      normalizeSelectionForOptions(\n        parseSelectionToIdSet(\n          defaultValue,\n          selectionMode,\n          effectiveMaxSelections,\n        ),\n        optionIds,\n      ),\n  );\n\n  const selectedIds = useMemo(() => {\n    const parsed =\n      value !== undefined\n        ? parseSelectionToIdSet(value, selectionMode, effectiveMaxSelections)\n        : uncontrolledSelected;\n    return normalizeSelectionForOptions(parsed, optionIds);\n  }, [\n    value,\n    uncontrolledSelected,\n    selectionMode,\n    effectiveMaxSelections,\n    optionIds,\n  ]);\n\n  const selectedCount = selectedIds.size;\n\n  const optionStates = useMemo(() => {\n    return options.map((option) => {\n      const isSelected = selectedIds.has(option.id);\n      const isSelectionLocked =\n        selectionMode === \"multi\" &&\n        effectiveMaxSelections !== undefined &&\n        selectedCount >= effectiveMaxSelections &&\n        !isSelected;\n      const isDisabled = option.disabled || isSelectionLocked;\n\n      return { option, isSelected, isDisabled };\n    });\n  }, [\n    options,\n    selectedIds,\n    selectionMode,\n    effectiveMaxSelections,\n    selectedCount,\n  ]);\n\n  const optionRefs = useRef<Array<HTMLButtonElement | null>>([]);\n  const [activeIndex, setActiveIndex] = useState(() => {\n    const firstSelected = optionStates.findIndex(\n      (s) => s.isSelected && !s.isDisabled,\n    );\n    if (firstSelected >= 0) return firstSelected;\n    const firstEnabled = optionStates.findIndex((s) => !s.isDisabled);\n    return firstEnabled >= 0 ? firstEnabled : 0;\n  });\n\n  useEffect(() => {\n    if (optionStates.length === 0) return;\n    setActiveIndex((prev) => {\n      if (\n        prev < 0 ||\n        prev >= optionStates.length ||\n        optionStates[prev].isDisabled\n      ) {\n        const firstEnabled = optionStates.findIndex((s) => !s.isDisabled);\n        return firstEnabled >= 0 ? firstEnabled : 0;\n      }\n      return prev;\n    });\n  }, [optionStates]);\n\n  const updateSelection = useCallback(\n    (next: Set<string>) => {\n      const normalizedNext = normalizeSelectionForOptions(\n        parseSelectionToIdSet(\n          Array.from(next),\n          selectionMode,\n          effectiveMaxSelections,\n        ),\n        optionIds,\n      );\n\n      if (value === undefined) {\n        if (!areSetsEqual(uncontrolledSelected, normalizedNext)) {\n          setUncontrolledSelected(normalizedNext);\n        }\n      }\n\n      onChange?.(convertIdSetToSelection(normalizedNext, selectionMode));\n    },\n    [\n      effectiveMaxSelections,\n      selectionMode,\n      uncontrolledSelected,\n      value,\n      onChange,\n      optionIds,\n    ],\n  );\n\n  const toggleSelection = useCallback(\n    (optionId: string) => {\n      const next = new Set(selectedIds);\n      const isSelected = next.has(optionId);\n\n      if (selectionMode === \"single\") {\n        if (isSelected) {\n          next.delete(optionId);\n        } else {\n          next.clear();\n          next.add(optionId);\n        }\n      } else {\n        if (isSelected) {\n          next.delete(optionId);\n        } else {\n          if (effectiveMaxSelections && next.size >= effectiveMaxSelections) {\n            return;\n          }\n          next.add(optionId);\n        }\n      }\n\n      updateSelection(next);\n    },\n    [effectiveMaxSelections, selectedIds, selectionMode, updateSelection],\n  );\n\n  const toSelectionState = useCallback(\n    (selected: Set<string>): OptionListSelection =>\n      convertIdSetToSelection(selected, selectionMode),\n    [selectionMode],\n  );\n\n  const handleCancel = useCallback((): OptionListSelection => {\n    const empty = new Set<string>();\n    updateSelection(empty);\n    return toSelectionState(empty);\n  }, [toSelectionState, updateSelection]);\n\n  const customActions = useMemo(\n    () => normalizeActionsConfig(actions),\n    [actions],\n  );\n\n  const handleFooterAction = useCallback(\n    async (actionId: string) => {\n      let nextState = toSelectionState(selectedIds);\n\n      if (actionId === \"cancel\") {\n        nextState = handleCancel();\n      }\n\n      await onAction?.(actionId, nextState);\n    },\n    [handleCancel, onAction, selectedIds, toSelectionState],\n  );\n\n  const normalizedFooterActions = useMemo(() => {\n    if (customActions) return customActions;\n    return {\n      items: [\n        { id: \"cancel\", label: \"Clear\", variant: \"ghost\" as const },\n        { id: \"confirm\", label: \"Confirm\", variant: \"default\" as const },\n      ],\n      align: \"right\" as const,\n    } satisfies ReturnType<typeof normalizeActionsConfig>;\n  }, [customActions]);\n\n  const isConfirmDisabled =\n    selectedCount < minSelections || selectedCount === 0;\n  const hasNothingToClear = selectedCount === 0;\n\n  const focusOptionAt = useCallback((index: number) => {\n    const el = optionRefs.current[index];\n    if (el) el.focus();\n    setActiveIndex(index);\n  }, []);\n\n  const findFirstEnabledIndex = useCallback(() => {\n    const idx = optionStates.findIndex((s) => !s.isDisabled);\n    return idx >= 0 ? idx : 0;\n  }, [optionStates]);\n\n  const findLastEnabledIndex = useCallback(() => {\n    for (let i = optionStates.length - 1; i >= 0; i--) {\n      if (!optionStates[i].isDisabled) return i;\n    }\n    return 0;\n  }, [optionStates]);\n\n  const findNextEnabledIndex = useCallback(\n    (start: number, direction: 1 | -1) => {\n      const len = optionStates.length;\n      if (len === 0) return 0;\n      for (let step = 1; step <= len; step++) {\n        const idx = (start + direction * step + len) % len;\n        if (!optionStates[idx].isDisabled) return idx;\n      }\n      return start;\n    },\n    [optionStates],\n  );\n\n  const handleListboxKeyDown = useCallback(\n    (e: KeyboardEvent<HTMLDivElement>) => {\n      if (optionStates.length === 0) return;\n\n      const key = e.key;\n\n      if (key === \"ArrowDown\") {\n        e.preventDefault();\n        e.stopPropagation();\n        focusOptionAt(findNextEnabledIndex(activeIndex, 1));\n        return;\n      }\n\n      if (key === \"ArrowUp\") {\n        e.preventDefault();\n        e.stopPropagation();\n        focusOptionAt(findNextEnabledIndex(activeIndex, -1));\n        return;\n      }\n\n      if (key === \"Home\") {\n        e.preventDefault();\n        e.stopPropagation();\n        focusOptionAt(findFirstEnabledIndex());\n        return;\n      }\n\n      if (key === \"End\") {\n        e.preventDefault();\n        e.stopPropagation();\n        focusOptionAt(findLastEnabledIndex());\n        return;\n      }\n\n      if (key === \"Enter\" || key === \" \") {\n        e.preventDefault();\n        e.stopPropagation();\n        const current = optionStates[activeIndex];\n        if (!current || current.isDisabled) return;\n        toggleSelection(current.option.id);\n        return;\n      }\n\n      if (key === \"Escape\") {\n        e.preventDefault();\n        e.stopPropagation();\n        if (!hasNothingToClear) {\n          handleCancel();\n        }\n      }\n    },\n    [\n      activeIndex,\n      findFirstEnabledIndex,\n      findLastEnabledIndex,\n      findNextEnabledIndex,\n      focusOptionAt,\n      handleCancel,\n      hasNothingToClear,\n      optionStates,\n      toggleSelection,\n    ],\n  );\n\n  const actionsWithDisabledState = useMemo((): Action[] => {\n    return normalizedFooterActions.items.map((action) => {\n      const isDisabledByValidation =\n        (action.id === \"confirm\" && isConfirmDisabled) ||\n        (action.id === \"cancel\" && hasNothingToClear);\n      return {\n        ...action,\n        disabled: action.disabled || isDisabledByValidation,\n        label:\n          action.id === \"confirm\" &&\n          selectionMode === \"multi\" &&\n          selectedCount > 0\n            ? `${action.label} (${selectedCount})`\n            : action.label,\n      };\n    });\n  }, [\n    normalizedFooterActions.items,\n    isConfirmDisabled,\n    hasNothingToClear,\n    selectionMode,\n    selectedCount,\n  ]);\n\n  const isReceipt = choice !== undefined && choice !== null;\n  const viewKey = isReceipt ? `receipt-${String(choice)}` : \"interactive\";\n\n  return (\n    <div key={viewKey} className=\"contents\">\n      {isReceipt ? (\n        <OptionListConfirmation\n          id={id}\n          options={options}\n          selectedIds={normalizeSelectionForOptions(\n            parseSelectionToIdSet(choice, selectionMode),\n            optionIds,\n          )}\n          className={className}\n        />\n      ) : (\n        <div\n          className={cn(\n            \"@container/option-list flex w-full max-w-md min-w-80 flex-col gap-3\",\n            \"text-foreground\",\n            className,\n          )}\n          data-slot=\"option-list\"\n          data-tool-ui-id={id}\n          role=\"group\"\n          aria-label=\"Option list\"\n        >\n          <div\n            className={cn(\n              \"group/list bg-card flex w-full flex-col overflow-hidden rounded-2xl border px-4 py-1.5 shadow-xs\",\n            )}\n            role=\"listbox\"\n            aria-multiselectable={selectionMode === \"multi\"}\n            onKeyDown={handleListboxKeyDown}\n          >\n            {optionStates.map(({ option, isSelected, isDisabled }, index) => {\n              return (\n                <Fragment key={option.id}>\n                  {index > 0 && (\n                    <Separator\n                      className=\"transition-opacity [@media(hover:hover)]:[&:has(+_:hover)]:opacity-0 [@media(hover:hover)]:[.peer:hover+&]:opacity-0\"\n                      orientation=\"horizontal\"\n                    />\n                  )}\n                  <OptionItem\n                    option={option}\n                    isSelected={isSelected}\n                    isDisabled={isDisabled}\n                    selectionMode={selectionMode}\n                    isFirst={index === 0}\n                    isLast={index === optionStates.length - 1}\n                    tabIndex={index === activeIndex ? 0 : -1}\n                    onFocus={() => setActiveIndex(index)}\n                    buttonRef={(el) => {\n                      optionRefs.current[index] = el;\n                    }}\n                    onToggle={() => toggleSelection(option.id)}\n                  />\n                </Fragment>\n              );\n            })}\n          </div>\n\n          <div className=\"@container/actions\">\n            <ActionButtons\n              actions={actionsWithDisabledState}\n              align={normalizedFooterActions.align}\n              confirmTimeout={normalizedFooterActions.confirmTimeout}\n              onAction={handleFooterAction}\n              onBeforeAction={\n                onBeforeAction\n                  ? (actionId) =>\n                      onBeforeAction(actionId, toSelectionState(selectedIds))\n                  : undefined\n              }\n            />\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/option-list/schema.ts",
    "content": "import { z } from \"zod\";\nimport type { ReactNode } from \"react\";\nimport type { ActionsProp } from \"../shared/actions-config\";\nimport type { EmbeddedActionsProps } from \"../shared/embedded-actions\";\nimport {\n  ActionSchema,\n  SerializableActionSchema,\n  SerializableActionsConfigSchema,\n  ToolUIIdSchema,\n  ToolUIReceiptSchema,\n  ToolUIRoleSchema,\n} from \"../shared/schema\";\nimport { defineToolUiContract } from \"../shared/contract\";\n\nexport const OptionListOptionSchema = z.object({\n  id: z.string().min(1),\n  label: z.string().min(1),\n  description: z.string().optional(),\n  icon: z.custom<ReactNode>().optional(),\n  disabled: z.boolean().optional(),\n});\n\nexport type OptionListSelection = string[] | string | null;\n\nconst OptionListSelectionSchema = z\n  .union([z.array(z.string()), z.string(), z.null()])\n  .optional();\n\ntype OptionListSchemaInvariantInput = {\n  options: Array<{ id: string }>;\n  minSelections?: number;\n  maxSelections?: number;\n  value?: OptionListSelection;\n  defaultValue?: OptionListSelection;\n  choice?: OptionListSelection;\n};\n\nfunction selectionToIds(selection: OptionListSelection | undefined): string[] {\n  if (selection == null) return [];\n  if (typeof selection === \"string\") return [selection];\n  return Array.isArray(selection) ? selection : [];\n}\n\nfunction validateOptionListInvariants(\n  data: OptionListSchemaInvariantInput,\n  ctx: z.RefinementCtx,\n) {\n  if (\n    data.minSelections !== undefined &&\n    data.maxSelections !== undefined &&\n    data.minSelections > data.maxSelections\n  ) {\n    ctx.addIssue({\n      code: z.ZodIssueCode.custom,\n      path: [\"minSelections\"],\n      message: \"`minSelections` cannot be greater than `maxSelections`.\",\n    });\n  }\n\n  const optionIds = new Set<string>();\n  for (let index = 0; index < data.options.length; index++) {\n    const optionId = data.options[index]?.id;\n    if (!optionId) continue;\n\n    if (optionIds.has(optionId)) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        path: [\"options\", index, \"id\"],\n        message: `Duplicate option id \"${optionId}\" is not allowed.`,\n      });\n    } else {\n      optionIds.add(optionId);\n    }\n  }\n\n  const selectionFields: Array<\n    [\"value\" | \"defaultValue\" | \"choice\", OptionListSelection | undefined]\n  > = [\n    [\"value\", data.value],\n    [\"defaultValue\", data.defaultValue],\n    [\"choice\", data.choice],\n  ];\n\n  for (const [fieldName, selection] of selectionFields) {\n    if (selection == null) continue;\n\n    const ids = selectionToIds(selection);\n    ids.forEach((selectionId, index) => {\n      if (!optionIds.has(selectionId)) {\n        ctx.addIssue({\n          code: z.ZodIssueCode.custom,\n          path:\n            typeof selection === \"string\" ? [fieldName] : [fieldName, index],\n          message: `Selection id \"${selectionId}\" must exist in options.`,\n        });\n      }\n    });\n  }\n}\n\nconst OptionListPropsSchemaBase = z.object({\n  /**\n   * Unique identifier for this tool UI instance in the conversation.\n   *\n   * Used for:\n   * - Assistant referencing (\"the options above\")\n   * - Receipt generation (linking selections to their source)\n   * - Narration context\n   *\n   * Should be stable across re-renders, meaningful, and unique within the conversation.\n   *\n   * @example \"option-list-deploy-target\", \"format-selection\"\n   */\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  receipt: ToolUIReceiptSchema.optional(),\n  options: z.array(OptionListOptionSchema).min(1),\n  selectionMode: z.enum([\"multi\", \"single\"]).optional(),\n  /**\n   * Controlled selection value (advanced / runtime only).\n   *\n   * For Tool UI tool payloads, prefer `defaultValue` (initial selection) and\n   * `choice` (receipt state). Controlled `value` is intentionally excluded\n   * from `SerializableOptionListSchema` to avoid accidental \"controlled but\n   * non-interactive\" states when an LLM includes `value` in args.\n   */\n  value: OptionListSelectionSchema,\n  defaultValue: OptionListSelectionSchema,\n  /**\n   * When set, renders the component in receipt state showing the user's choice.\n   *\n   * In receipt state:\n   * - Only the chosen option(s) are shown\n   * - Actions are hidden\n   * - The component is read-only\n   *\n   * Use this with assistant-ui's `addResult` to show the outcome of a decision.\n   *\n   * @example\n   * ```tsx\n   * // In a toolkit render function:\n   * if (result) {\n   *   return <OptionList {...args} choice={result} />;\n   * }\n   * ```\n   */\n  choice: OptionListSelectionSchema,\n  actions: z\n    .union([z.array(ActionSchema), SerializableActionsConfigSchema])\n    .optional(),\n  minSelections: z.number().min(0).optional(),\n  maxSelections: z.number().min(1).optional(),\n});\n\nexport const OptionListPropsSchema = OptionListPropsSchemaBase.superRefine(\n  validateOptionListInvariants,\n);\n\nexport type OptionListOption = z.infer<typeof OptionListOptionSchema>;\n\nexport type OptionListProps = Omit<\n  z.infer<typeof OptionListPropsSchema>,\n  \"value\" | \"defaultValue\" | \"choice\" | \"actions\"\n> & {\n  /** @see OptionListPropsSchema.id */\n  id: string;\n  value?: OptionListSelection;\n  defaultValue?: OptionListSelection;\n  /** @see OptionListPropsSchema.choice */\n  choice?: OptionListSelection;\n  onChange?: (value: OptionListSelection) => void;\n  actions?: ActionsProp;\n  onAction?: EmbeddedActionsProps<OptionListSelection>[\"onAction\"];\n  onBeforeAction?: EmbeddedActionsProps<OptionListSelection>[\"onBeforeAction\"];\n  className?: string;\n};\n\nexport const SerializableOptionListSchema = OptionListPropsSchemaBase.omit({\n  // Exclude controlled selection from tool/LLM payloads.\n  value: true,\n})\n  .extend({\n    options: z.array(OptionListOptionSchema.omit({ icon: true })),\n    actions: z\n      .union([\n        z.array(SerializableActionSchema),\n        SerializableActionsConfigSchema,\n      ])\n      .optional(),\n  })\n  .strict()\n  .superRefine(validateOptionListInvariants);\n\nexport type SerializableOptionList = z.infer<\n  typeof SerializableOptionListSchema\n>;\n\nconst SerializableOptionListSchemaContract = defineToolUiContract(\n  \"OptionList\",\n  SerializableOptionListSchema,\n);\n\nexport const parseSerializableOptionList: (\n  input: unknown,\n) => SerializableOptionList = SerializableOptionListSchemaContract.parse;\n\nexport const safeParseSerializableOptionList: (\n  input: unknown,\n) => SerializableOptionList | null =\n  SerializableOptionListSchemaContract.safeParse;\n"
  },
  {
    "path": "apps/www/components/tool-ui/option-list/selection.ts",
    "content": "import type { OptionListSelection } from \"./schema\";\n\nexport function parseSelectionToIdSet(\n  value: OptionListSelection | undefined,\n  mode: \"multi\" | \"single\",\n  maxSelections?: number,\n): Set<string> {\n  if (mode === \"single\") {\n    const single =\n      typeof value === \"string\"\n        ? value\n        : Array.isArray(value)\n          ? value[0]\n          : null;\n    return single ? new Set([single]) : new Set();\n  }\n\n  const arr =\n    typeof value === \"string\" ? [value] : Array.isArray(value) ? value : [];\n\n  return new Set(maxSelections ? arr.slice(0, maxSelections) : arr);\n}\n\nexport function normalizeSelectionForOptions(\n  selection: Set<string>,\n  optionIds: Set<string>,\n): Set<string> {\n  const normalized = new Set<string>();\n  for (const id of selection) {\n    if (optionIds.has(id)) {\n      normalized.add(id);\n    }\n  }\n  return normalized;\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/order-summary/README.md",
    "content": "# Order Summary\n\nImplementation for the \"order-summary\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/order-summary/index.tsx\n- serializable schema + parse helpers: components/tool-ui/order-summary/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/order-summary/content.mdx\n- Preset payload: lib/presets/order-summary.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/order-summary/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn        → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Button    → shadcn/ui Button\n *   Separator → shadcn/ui Separator\n *   Skeleton  → shadcn/ui Skeleton\n */\n\nexport { cn } from \"@/lib/utils\";\nexport { Button } from \"@/components/ui/button\";\nexport { Separator } from \"@/components/ui/separator\";\nexport { Skeleton } from \"@/components/ui/skeleton\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/order-summary/index.tsx",
    "content": "export { OrderSummary } from \"./order-summary\";\nexport type {\n  OrderSummaryDisplayProps,\n  OrderSummaryReceiptProps,\n  OrderSummaryCompoundComponent,\n} from \"./order-summary\";\nexport {\n  type SerializableOrderSummary,\n  type OrderSummaryProps,\n  type OrderSummaryVariant,\n  type OrderItem,\n  type Pricing,\n  type OrderDecision,\n} from \"./schema\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/order-summary/order-summary.tsx",
    "content": "import { CheckCircle, Package } from \"lucide-react\";\nimport type { ReactElement } from \"react\";\nimport { cn, Separator } from \"./_adapter\";\nimport type {\n  OrderSummaryProps,\n  OrderItem,\n  Pricing,\n  OrderDecision,\n  OrderSummaryVariant,\n} from \"./schema\";\n\nfunction formatCurrency(amount: number, currency: string): string {\n  try {\n    return new Intl.NumberFormat(undefined, {\n      style: \"currency\",\n      currency,\n    }).format(amount);\n  } catch {\n    return `${currency} ${amount.toFixed(2)}`;\n  }\n}\n\nfunction formatQuantity(quantity: number): string {\n  return quantity === 1 ? \"\" : `Qty: ${quantity}`;\n}\n\nfunction ItemImage({ src, alt }: { src?: string; alt: string }) {\n  if (!src) {\n    return (\n      <div className=\"bg-muted flex h-12 w-12 shrink-0 items-center justify-center rounded-md\">\n        <Package\n          aria-hidden=\"true\"\n          focusable=\"false\"\n          className=\"text-muted-foreground h-5 w-5\"\n        />\n      </div>\n    );\n  }\n\n  return (\n    <img\n      src={src}\n      alt={alt}\n      width={48}\n      height={48}\n      className=\"h-12 w-12 shrink-0 rounded-md object-cover\"\n    />\n  );\n}\n\nfunction OrderItemRow({\n  item,\n  currency,\n}: {\n  item: OrderItem;\n  currency: string;\n}) {\n  const quantity = item.quantity ?? 1;\n  const quantityText = formatQuantity(quantity);\n  const hasDescription = item.description || quantityText;\n  const lineTotal = item.unitPrice * quantity;\n\n  return (\n    <div className=\"flex gap-3\">\n      <ItemImage src={item.imageUrl} alt={item.name} />\n      <div className=\"flex min-w-0 flex-1 items-center justify-between\">\n        <div className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n          <div className=\"flex items-center justify-between\">\n            <span className=\"truncate text-sm font-medium\">{item.name}</span>\n            <span className=\"truncate text-sm tabular-nums\">\n              {formatCurrency(lineTotal, currency)}\n            </span>\n          </div>\n          {hasDescription && (\n            <div className=\"text-muted-foreground truncate text-sm\">\n              {[item.description, quantityText].filter(Boolean).join(\" · \")}\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction PricingBreakdown({\n  pricing,\n  className,\n}: {\n  pricing: Pricing;\n  className?: string;\n}) {\n  const currency = pricing.currency ?? \"USD\";\n\n  return (\n    <dl className={cn(\"flex flex-col gap-2 text-sm\", className)}>\n      <div className=\"flex justify-between gap-4\">\n        <dt className=\"text-muted-foreground\">Subtotal</dt>\n        <dd className=\"tabular-nums\">\n          {formatCurrency(pricing.subtotal, currency)}\n        </dd>\n      </div>\n\n      {pricing.discount !== undefined && pricing.discount > 0 && (\n        <div className=\"flex justify-between gap-4 text-green-600 dark:text-green-500\">\n          <dt>{pricing.discountLabel || \"Discount\"}</dt>\n          <dd className=\"tabular-nums\">\n            -{formatCurrency(pricing.discount, currency)}\n          </dd>\n        </div>\n      )}\n\n      {pricing.shipping !== undefined && (\n        <div className=\"flex justify-between gap-4\">\n          <dt className=\"text-muted-foreground\">Shipping</dt>\n          <dd className=\"tabular-nums\">\n            {pricing.shipping === 0\n              ? \"Free\"\n              : formatCurrency(pricing.shipping, currency)}\n          </dd>\n        </div>\n      )}\n\n      {pricing.tax !== undefined && (\n        <div className=\"flex justify-between gap-4\">\n          <dt className=\"text-muted-foreground\">{pricing.taxLabel || \"Tax\"}</dt>\n          <dd className=\"tabular-nums\">\n            {formatCurrency(pricing.tax, currency)}\n          </dd>\n        </div>\n      )}\n\n      <div className=\"flex justify-between gap-4\">\n        <dt className=\"font-medium\">Total</dt>\n        <dd className=\"font-semibold tabular-nums\">\n          {formatCurrency(pricing.total, currency)}\n        </dd>\n      </div>\n    </dl>\n  );\n}\n\nfunction formatDate(isoString: string): string | undefined {\n  try {\n    const date = new Date(isoString);\n    if (isNaN(date.getTime())) return undefined;\n    return date.toLocaleDateString(undefined, {\n      month: \"short\",\n      day: \"numeric\",\n      year: \"numeric\",\n    });\n  } catch {\n    return undefined;\n  }\n}\n\nfunction ReceiptBadge({\n  orderId,\n  confirmedAt,\n}: {\n  orderId?: string;\n  confirmedAt?: string;\n}) {\n  const formattedDate = confirmedAt ? formatDate(confirmedAt) : undefined;\n\n  const parts = [orderId && `#${orderId}`, formattedDate].filter(Boolean);\n  if (parts.length === 0) return null;\n\n  return (\n    <p className=\"text-muted-foreground mt-1 text-sm\">{parts.join(\" · \")}</p>\n  );\n}\n\nfunction OrderSummaryRoot({\n  id,\n  title = \"Order Summary\",\n  variant,\n  items,\n  pricing,\n  choice,\n  className,\n}: OrderSummaryProps) {\n  const titleId = `${id}-title`;\n  const resolvedVariant: OrderSummaryVariant =\n    variant ?? (choice === undefined ? \"summary\" : \"receipt\");\n  const isReceipt = resolvedVariant === \"receipt\";\n  const isMalformedPayload =\n    !Array.isArray(items) ||\n    items.length === 0 ||\n    pricing == null ||\n    (isReceipt && choice === undefined);\n\n  if (isMalformedPayload) {\n    return (\n      <article\n        data-slot=\"order-summary\"\n        data-tool-ui-id={id}\n        aria-labelledby={titleId}\n        className={cn(\"flex max-w-md min-w-80 flex-col gap-3\", className)}\n      >\n        <div className=\"text-card-foreground rounded-lg border bg-card p-4 shadow-sm\">\n          <h2 id={titleId} className=\"text-base font-semibold\">\n            {title}\n          </h2>\n          <p className=\"text-muted-foreground mt-2 text-sm\">\n            Unable to render order summary\n          </p>\n        </div>\n      </article>\n    );\n  }\n\n  return (\n    <article\n      data-slot=\"order-summary\"\n      data-tool-ui-id={id}\n      aria-labelledby={titleId}\n      className={cn(\"flex max-w-md min-w-80 flex-col gap-3\", className)}\n    >\n      <div\n        className={cn(\n          \"text-card-foreground rounded-lg border shadow-sm\",\n          isReceipt ? \"bg-card/60\" : \"bg-card\",\n        )}\n      >\n        <div className={cn(\"space-y-4 p-4\", isReceipt && \"opacity-95\")}>\n          <div>\n            <h2\n              id={titleId}\n              className=\"flex items-center gap-2 text-base font-semibold\"\n            >\n              {isReceipt && (\n                <CheckCircle\n                  aria-hidden=\"true\"\n                  focusable=\"false\"\n                  className=\"h-5 w-5 text-green-600 dark:text-green-500\"\n                />\n              )}\n              {title}\n            </h2>\n            {isReceipt && choice && (\n              <ReceiptBadge\n                orderId={choice.orderId}\n                confirmedAt={choice.confirmedAt}\n              />\n            )}\n          </div>\n\n          <div className=\"space-y-3\">\n            {items.map((item) => (\n              <OrderItemRow\n                key={item.id}\n                item={item}\n                currency={pricing.currency ?? \"USD\"}\n              />\n            ))}\n          </div>\n\n          <Separator />\n\n          <PricingBreakdown pricing={pricing} />\n        </div>\n      </div>\n    </article>\n  );\n}\n\nexport type OrderSummaryDisplayProps = OrderSummaryProps;\n\nfunction OrderSummaryDisplay(props: OrderSummaryDisplayProps) {\n  return <OrderSummaryRoot {...props} variant=\"summary\" />;\n}\n\nexport interface OrderSummaryReceiptProps extends Omit<\n  OrderSummaryProps,\n  \"choice\"\n> {\n  choice: OrderDecision;\n}\n\nfunction OrderSummaryReceipt(props: OrderSummaryReceiptProps) {\n  return <OrderSummaryRoot {...props} variant=\"receipt\" />;\n}\n\nexport interface OrderSummaryCompoundComponent {\n  (props: OrderSummaryProps): ReactElement;\n  Display: (props: OrderSummaryDisplayProps) => ReactElement;\n  Receipt: (props: OrderSummaryReceiptProps) => ReactElement;\n}\n\nexport const OrderSummary: OrderSummaryCompoundComponent = Object.assign(\n  OrderSummaryRoot,\n  {\n    Display: OrderSummaryDisplay,\n    Receipt: OrderSummaryReceipt,\n  },\n);\n"
  },
  {
    "path": "apps/www/components/tool-ui/order-summary/schema.ts",
    "content": "import { z } from \"zod\";\nimport { defineToolUiContract } from \"../shared/contract\";\nimport { ToolUIIdSchema, ToolUIRoleSchema } from \"../shared/schema\";\n\nexport const OrderItemSchema = z.object({\n  id: z.string(),\n  name: z.string(),\n  description: z.string().optional(),\n  imageUrl: z.string().url().optional(),\n  quantity: z.number().int().positive().optional(),\n  unitPrice: z.number(),\n});\n\nexport type OrderItem = z.infer<typeof OrderItemSchema>;\n\nconst OrderItemsSchema = z\n  .array(OrderItemSchema)\n  .min(1)\n  .superRefine((items, ctx) => {\n    const seenIds = new Set<string>();\n\n    for (const [index, item] of items.entries()) {\n      if (seenIds.has(item.id)) {\n        ctx.addIssue({\n          code: z.ZodIssueCode.custom,\n          message: `Duplicate item id: \"${item.id}\"`,\n          path: [index, \"id\"],\n        });\n      }\n\n      seenIds.add(item.id);\n    }\n  });\n\nexport const PricingSchema = z.object({\n  subtotal: z.number(),\n  tax: z.number().optional(),\n  taxLabel: z.string().optional(),\n  shipping: z.number().optional(),\n  discount: z.number().nonnegative().optional(),\n  discountLabel: z.string().optional(),\n  total: z.number(),\n  currency: z.string().optional(),\n});\n\nexport type Pricing = z.infer<typeof PricingSchema>;\n\nexport const OrderSummaryVariantSchema = z.enum([\"summary\", \"receipt\"]);\nexport type OrderSummaryVariant = z.infer<typeof OrderSummaryVariantSchema>;\n\nexport const OrderDecisionSchema = z.object({\n  action: z.literal(\"confirm\"),\n  orderId: z.string().optional(),\n  confirmedAt: z.string().datetime().optional(),\n});\n\nexport type OrderDecision = z.infer<typeof OrderDecisionSchema>;\n\nexport const SerializableOrderSummarySchema = z\n  .object({\n    id: ToolUIIdSchema,\n    role: ToolUIRoleSchema.optional(),\n    title: z.string().optional(),\n    variant: OrderSummaryVariantSchema.optional(),\n    items: OrderItemsSchema,\n    pricing: PricingSchema,\n    choice: OrderDecisionSchema.optional(),\n  })\n  .strict()\n  .superRefine((value, ctx) => {\n    if (value.variant === \"receipt\" && value.choice === undefined) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        message: 'Receipt variant requires \"choice\".',\n        path: [\"choice\"],\n      });\n    }\n\n    if (value.variant === \"summary\" && value.choice !== undefined) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        message: 'Summary variant cannot include \"choice\".',\n        path: [\"choice\"],\n      });\n    }\n  });\n\nexport type SerializableOrderSummary = z.infer<\n  typeof SerializableOrderSummarySchema\n>;\n\nconst SerializableOrderSummarySchemaContract = defineToolUiContract(\n  \"OrderSummary\",\n  SerializableOrderSummarySchema,\n);\n\nexport const parseSerializableOrderSummary: (\n  input: unknown,\n) => SerializableOrderSummary = SerializableOrderSummarySchemaContract.parse;\n\nexport const safeParseSerializableOrderSummary: (\n  input: unknown,\n) => SerializableOrderSummary | null =\n  SerializableOrderSummarySchemaContract.safeParse;\n\nexport interface OrderSummaryProps extends SerializableOrderSummary {\n  className?: string;\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/parameter-slider/README.md",
    "content": "# Parameter Slider\n\nImplementation for the \"parameter-slider\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/parameter-slider/index.tsx\n- serializable schema + parse helpers: components/tool-ui/parameter-slider/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/parameter-slider/content.mdx\n- Preset payload: lib/presets/parameter-slider.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/parameter-slider/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn        → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Button    → shadcn/ui Button\n *   Separator → shadcn/ui Separator\n *   Slider    → shadcn/ui Slider\n */\n\nexport { cn } from \"@/lib/utils\";\nexport { Button } from \"@/components/ui/button\";\nexport { Separator } from \"@/components/ui/separator\";\nexport { Slider } from \"@/components/ui/slider\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/parameter-slider/index.tsx",
    "content": "export { ParameterSlider } from \"./parameter-slider\";\nexport type {\n  ParameterSliderProps,\n  SliderConfig,\n  SliderValue,\n  SerializableParameterSlider,\n} from \"./schema\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/parameter-slider/math.ts",
    "content": "import type { SliderConfig, SliderValue } from \"./schema\";\n\ntype SliderPercentInput = {\n  value: number;\n  min: number;\n  max: number;\n};\n\nfunction clampPercent(value: number): number {\n  if (!Number.isFinite(value)) return 0;\n  return Math.max(0, Math.min(100, value));\n}\n\nexport function sliderRangeToPercent({\n  value,\n  min,\n  max,\n}: SliderPercentInput): number {\n  const range = max - min;\n  if (!Number.isFinite(range) || range <= 0) return 0;\n  return clampPercent(((value - min) / range) * 100);\n}\n\nexport function createSliderValueSnapshot(\n  sliders: SliderConfig[],\n): SliderValue[] {\n  return sliders.map((slider) => ({ id: slider.id, value: slider.value }));\n}\n\nexport function createSliderSignature(sliders: SliderConfig[]): string {\n  return JSON.stringify(\n    sliders.map(({ id, min, max, step, value, unit, precision }) => ({\n      id,\n      min,\n      max,\n      step: step ?? 1,\n      value,\n      unit: unit ?? \"\",\n      precision: precision ?? null,\n    })),\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/parameter-slider/parameter-slider.tsx",
    "content": "\"use client\";\n\nimport {\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport * as SliderPrimitive from \"@radix-ui/react-slider\";\nimport type { ParameterSliderProps, SliderConfig, SliderValue } from \"./schema\";\nimport { ActionButtons } from \"../shared/action-buttons\";\nimport { normalizeActionsConfig } from \"../shared/actions-config\";\nimport { useControllableState } from \"../shared/use-controllable-state\";\nimport { useSignatureReset } from \"../shared/use-signature-reset\";\n\nimport { cn } from \"./_adapter\";\nimport {\n  createSliderSignature,\n  createSliderValueSnapshot,\n  sliderRangeToPercent,\n} from \"./math\";\n\nfunction formatSignedValue(\n  value: number,\n  min: number,\n  max: number,\n  precision?: number,\n  unit?: string,\n): string {\n  const crossesZero = min < 0 && max > 0;\n  const fixed =\n    precision !== undefined ? value.toFixed(precision) : String(value);\n  const numericPart = crossesZero && value >= 0 ? `+${fixed}` : fixed;\n  return unit ? `${numericPart} ${unit}` : numericPart;\n}\n\nfunction getAriaValueText(\n  value: number,\n  min: number,\n  max: number,\n  unit?: string,\n): string {\n  const crossesZero = min < 0 && max > 0;\n  if (crossesZero) {\n    if (value > 0) {\n      return unit ? `plus ${value} ${unit}` : `plus ${value}`;\n    } else if (value < 0) {\n      return unit\n        ? `minus ${Math.abs(value)} ${unit}`\n        : `minus ${Math.abs(value)}`;\n    }\n  }\n  return unit ? `${value} ${unit}` : String(value);\n}\n\nconst TICK_COUNT = 16;\nconst TEXT_PADDING_X = 4;\nconst TEXT_PADDING_X_OUTER = 0; // Less inset on outer-facing side (near edges)\nconst TEXT_PADDING_Y = 2;\nconst DETECTION_MARGIN_X = 12;\nconst DETECTION_MARGIN_X_OUTER = 4; // Small margin at edges for steep falloff - segments fully close at terminal positions\nconst DETECTION_MARGIN_Y = 12;\nconst TRACK_HEIGHT = 48;\nconst TEXT_RELEASE_INSET = 8;\nconst TRACK_EDGE_INSET = 4; // px from track edge - keeps elements visible at extremes\nconst THUMB_WIDTH = 12; // w-3\n// Text vertical offset: raised slightly from center\n// Positive = raised, negative = lowered\nconst TEXT_VERTICAL_OFFSET = 0.5;\n\nfunction clampPercent(value: number): number {\n  if (!Number.isFinite(value)) return 0;\n  return Math.max(0, Math.min(100, value));\n}\n\n// Convert a percentage (0-100) to an inset position string\n// At 0%: 4px from left edge; at 100%: 4px from right edge\nfunction toInsetPosition(percent: number): string {\n  const safePercent = clampPercent(percent);\n  return `calc(${TRACK_EDGE_INSET}px + (100% - ${TRACK_EDGE_INSET * 2}px) * ${safePercent / 100})`;\n}\n\n// Radix keeps the thumb in bounds by applying a percent-dependent px offset.\n// Matching this for fill clipping prevents handle/fill drift near extremes.\nfunction getRadixThumbInBoundsOffsetPx(percent: number): number {\n  const safePercent = clampPercent(percent);\n  const halfWidth = THUMB_WIDTH / 2;\n  return halfWidth - (safePercent * halfWidth) / 50;\n}\n\nfunction toRadixThumbPosition(percent: number): string {\n  const safePercent = clampPercent(percent);\n  const offsetPx = getRadixThumbInBoundsOffsetPx(safePercent);\n  return `calc(${safePercent}% + ${offsetPx}px)`;\n}\n\nfunction signedDistanceToRoundedRect(\n  px: number,\n  py: number,\n  left: number,\n  right: number,\n  top: number,\n  bottom: number,\n  radiusLeft: number,\n  radiusRight: number,\n): number {\n  const innerLeft = left + radiusLeft;\n  const innerRight = right - radiusRight;\n  const innerTop = top + Math.max(radiusLeft, radiusRight);\n  const innerBottom = bottom - Math.max(radiusLeft, radiusRight);\n\n  const inLeftCorner = px < innerLeft;\n  const inRightCorner = px > innerRight;\n  const inCornerY = py < innerTop || py > innerBottom;\n\n  if ((inLeftCorner || inRightCorner) && inCornerY) {\n    const radius = inLeftCorner ? radiusLeft : radiusRight;\n    const cornerX = inLeftCorner ? innerLeft : innerRight;\n    const cornerY = py < innerTop ? top + radius : bottom - radius;\n    const distToCornerCenter = Math.hypot(px - cornerX, py - cornerY);\n    return distToCornerCenter - radius;\n  }\n\n  const dx = Math.max(left - px, px - right, 0);\n  const dy = Math.max(top - py, py - bottom, 0);\n\n  if (dx === 0 && dy === 0) {\n    return -Math.min(px - left, right - px, py - top, bottom - py);\n  }\n\n  return Math.max(dx, dy);\n}\n\nconst OUTER_EDGE_RADIUS_FACTOR = 0.3; // Reduced radius on outer-facing sides for steeper falloff\n\nfunction calculateGap(\n  thumbCenterX: number,\n  textRect: { left: number; right: number; height: number; centerY: number },\n  isLeftAligned: boolean,\n): number {\n  const { left, right, height, centerY } = textRect;\n  // Asymmetric padding/margin: outer-facing side has less padding, more margin\n  const paddingLeft = isLeftAligned ? TEXT_PADDING_X_OUTER : TEXT_PADDING_X;\n  const paddingRight = isLeftAligned ? TEXT_PADDING_X : TEXT_PADDING_X_OUTER;\n  const marginLeft = isLeftAligned\n    ? DETECTION_MARGIN_X_OUTER\n    : DETECTION_MARGIN_X;\n  const marginRight = isLeftAligned\n    ? DETECTION_MARGIN_X\n    : DETECTION_MARGIN_X_OUTER;\n  const paddingY = TEXT_PADDING_Y;\n  const marginY = DETECTION_MARGIN_Y;\n  const thumbCenterY = centerY;\n\n  // Inner boundary (where max gap occurs)\n  const innerLeft = left - paddingLeft;\n  const innerRight = right + paddingRight;\n  const innerTop = centerY - height / 2 - paddingY;\n  const innerBottom = centerY + height / 2 + paddingY;\n  const innerHeight = height + paddingY * 2;\n  const innerRadius = innerHeight / 2;\n  // Smaller radius on outer-facing side (left for label, right for value)\n  const innerRadiusLeft = isLeftAligned\n    ? innerRadius * OUTER_EDGE_RADIUS_FACTOR\n    : innerRadius;\n  const innerRadiusRight = isLeftAligned\n    ? innerRadius\n    : innerRadius * OUTER_EDGE_RADIUS_FACTOR;\n\n  // Outer boundary (where effect starts) - proportionally larger\n  const outerLeft = left - paddingLeft - marginLeft;\n  const outerRight = right + paddingRight + marginRight;\n  const outerTop = centerY - height / 2 - paddingY - marginY;\n  const outerBottom = centerY + height / 2 + paddingY + marginY;\n  const outerHeight = height + paddingY * 2 + marginY * 2;\n  const outerRadius = outerHeight / 2;\n  const outerRadiusLeft = isLeftAligned\n    ? outerRadius * OUTER_EDGE_RADIUS_FACTOR\n    : outerRadius;\n  const outerRadiusRight = isLeftAligned\n    ? outerRadius\n    : outerRadius * OUTER_EDGE_RADIUS_FACTOR;\n\n  const outerDist = signedDistanceToRoundedRect(\n    thumbCenterX,\n    thumbCenterY,\n    outerLeft,\n    outerRight,\n    outerTop,\n    outerBottom,\n    outerRadiusLeft,\n    outerRadiusRight,\n  );\n\n  // Outside outer boundary - no gap\n  if (outerDist > 0) return 0;\n\n  const innerDist = signedDistanceToRoundedRect(\n    thumbCenterX,\n    thumbCenterY,\n    innerLeft,\n    innerRight,\n    innerTop,\n    innerBottom,\n    innerRadiusLeft,\n    innerRadiusRight,\n  );\n\n  // Inside inner boundary - max gap\n  const maxGap = height + paddingY * 2;\n  if (innerDist <= 0) return maxGap;\n\n  // Between boundaries - linear interpolation\n  // outerDist is negative (inside outer), innerDist is positive (outside inner)\n  const totalDist = Math.abs(outerDist) + innerDist;\n  const t = Math.abs(outerDist) / totalDist;\n\n  return maxGap * t;\n}\n\ninterface SliderRowProps {\n  config: SliderConfig;\n  value: number;\n  onChange: (value: number) => void;\n  trackClassName?: string;\n  fillClassName?: string;\n  handleClassName?: string;\n}\n\nfunction SliderRow({\n  config,\n  value,\n  onChange,\n  trackClassName,\n  fillClassName,\n  handleClassName,\n}: SliderRowProps) {\n  const { id, label, min, max, step = 1, unit, precision, disabled } = config;\n  // Per-slider theming overrides component-level theming\n  const resolvedTrackClassName = config.trackClassName ?? trackClassName;\n  const resolvedFillClassName = config.fillClassName ?? fillClassName;\n  const resolvedHandleClassName = config.handleClassName ?? handleClassName;\n  const crossesZero = min < 0 && max > 0;\n  const [isDragging, setIsDragging] = useState(false);\n  const [isHovered, setIsHovered] = useState(false);\n\n  const trackRef = useRef<HTMLSpanElement>(null);\n  const labelRef = useRef<HTMLSpanElement>(null);\n  const valueRef = useRef<HTMLSpanElement>(null);\n\n  const [dragGap, setDragGap] = useState(0);\n  const [fullGap, setFullGap] = useState(0);\n  const [intersectsText, setIntersectsText] = useState(false);\n  const [layoutVersion, setLayoutVersion] = useState(0);\n\n  useEffect(() => {\n    if (!isDragging) return;\n    const handlePointerUp = () => setIsDragging(false);\n    document.addEventListener(\"pointerup\", handlePointerUp);\n    return () => document.removeEventListener(\"pointerup\", handlePointerUp);\n  }, [isDragging]);\n\n  useEffect(() => {\n    const track = trackRef.current;\n    const labelEl = labelRef.current;\n    const valueEl = valueRef.current;\n    if (!track || !labelEl || !valueEl) return;\n\n    const bumpLayoutVersion = () => setLayoutVersion((v) => v + 1);\n\n    if (typeof ResizeObserver !== \"undefined\") {\n      const observer = new ResizeObserver(() => {\n        bumpLayoutVersion();\n      });\n      observer.observe(track);\n      observer.observe(labelEl);\n      observer.observe(valueEl);\n      return () => observer.disconnect();\n    }\n\n    window.addEventListener(\"resize\", bumpLayoutVersion);\n    return () => window.removeEventListener(\"resize\", bumpLayoutVersion);\n  }, []);\n\n  useLayoutEffect(() => {\n    const track = trackRef.current;\n    const labelEl = labelRef.current;\n    const valueEl = valueRef.current;\n\n    if (!track || !labelEl || !valueEl) return;\n\n    const trackRect = track.getBoundingClientRect();\n    const labelRect = labelEl.getBoundingClientRect();\n    const valueRect = valueEl.getBoundingClientRect();\n\n    const trackWidth = trackRect.width;\n    const valuePercent = sliderRangeToPercent({ value, min, max });\n    // Use same inset coordinate system as visual elements\n    const thumbCenterPx =\n      (trackWidth * clampPercent(valuePercent)) / 100 +\n      getRadixThumbInBoundsOffsetPx(valuePercent);\n    const thumbHalfWidth = THUMB_WIDTH / 2;\n\n    // Text is raised by TEXT_VERTICAL_OFFSET from center\n    const trackCenterY = TRACK_HEIGHT / 2 - TEXT_VERTICAL_OFFSET;\n\n    const labelGap = calculateGap(\n      thumbCenterPx,\n      {\n        left: labelRect.left - trackRect.left,\n        right: labelRect.right - trackRect.left,\n        height: labelRect.height,\n        centerY: trackCenterY,\n      },\n      true,\n    ); // label is left-aligned\n\n    const valueGap = calculateGap(\n      thumbCenterPx,\n      {\n        left: valueRect.left - trackRect.left,\n        right: valueRect.right - trackRect.left,\n        height: valueRect.height,\n        centerY: trackCenterY,\n      },\n      false,\n    ); // value is right-aligned\n\n    setDragGap(Math.max(labelGap, valueGap));\n\n    // Tight intersection check for release state\n    // Inset by px-2 (8px) padding to check against actual text, not padded container\n    const labelLeft = labelRect.left - trackRect.left + TEXT_RELEASE_INSET;\n    const labelRight = labelRect.right - trackRect.left - TEXT_RELEASE_INSET;\n    const valueLeft = valueRect.left - trackRect.left + TEXT_RELEASE_INSET;\n    const valueRight = valueRect.right - trackRect.left - TEXT_RELEASE_INSET;\n\n    const thumbLeft = thumbCenterPx - thumbHalfWidth;\n    const thumbRight = thumbCenterPx + thumbHalfWidth;\n\n    const hitsLabel = thumbRight > labelLeft && thumbLeft < labelRight;\n    const hitsValue = thumbRight > valueLeft && thumbLeft < valueRight;\n\n    setIntersectsText(hitsLabel || hitsValue);\n\n    // Calculate full separation gap for release state\n    // Use the max gap of whichever text element(s) the handle intersects\n    const labelFullGap = labelRect.height + TEXT_PADDING_Y * 2;\n    const valueFullGap = valueRect.height + TEXT_PADDING_Y * 2;\n    const releaseGap =\n      hitsLabel && hitsValue\n        ? Math.max(labelFullGap, valueFullGap)\n        : hitsLabel\n          ? labelFullGap\n          : hitsValue\n            ? valueFullGap\n            : 0;\n    setFullGap(releaseGap);\n  }, [value, min, max, layoutVersion]);\n\n  // While dragging: use distance-based separation, but never collapse below\n  // the release split when the thumb still intersects text.\n  const gap = isDragging\n    ? Math.max(dragGap, intersectsText ? fullGap : 0)\n    : intersectsText\n      ? fullGap\n      : 0;\n\n  const ticks = useMemo(() => {\n    // Generate equidistant ticks regardless of step value\n    const majorTickCount = TICK_COUNT;\n    const result: { percent: number; isCenter: boolean; isSubtick: boolean }[] =\n      [];\n\n    for (let i = 0; i <= majorTickCount; i++) {\n      const percent = (i / majorTickCount) * 100;\n      const isCenter = !crossesZero && percent === 50;\n\n      // Skip the center tick (50%) for crossesZero sliders\n      if (crossesZero && percent === 50) continue;\n\n      // Add subtick at midpoint before this tick (except for first)\n      if (i > 0) {\n        const prevPercent = ((i - 1) / majorTickCount) * 100;\n        // Don't add subtick if it would be at 50% for crossesZero\n        const midPercent = (prevPercent + percent) / 2;\n        if (!(crossesZero && midPercent === 50)) {\n          result.push({\n            percent: midPercent,\n            isCenter: false,\n            isSubtick: true,\n          });\n        }\n      }\n\n      result.push({ percent, isCenter, isSubtick: false });\n    }\n\n    return result;\n  }, [crossesZero]);\n\n  const zeroPercent = crossesZero\n    ? sliderRangeToPercent({ value: 0, min, max })\n    : 0;\n  const valuePercent = sliderRangeToPercent({ value, min, max });\n\n  // Fill clip-path uses the same inset coordinate system as the handle.\n  // This keeps the collapsed stroke aligned with the fill edge near extremes.\n  const fillClipPath = useMemo(() => {\n    const toClipFromRightInset = (percent: number) =>\n      `calc(100% - ${toRadixThumbPosition(percent)})`;\n    const toClipFromLeftInset = (percent: number) =>\n      toRadixThumbPosition(percent);\n    const TERMINAL_EPSILON = 1e-6;\n    const snapLeftInset = (percent: number) => {\n      if (percent <= TERMINAL_EPSILON) return \"0\";\n      if (percent >= 100 - TERMINAL_EPSILON) return \"100%\";\n      return toClipFromLeftInset(percent);\n    };\n    const snapRightInset = (percent: number) => {\n      if (percent <= TERMINAL_EPSILON) return \"100%\";\n      if (percent >= 100 - TERMINAL_EPSILON) return \"0\";\n      return toClipFromRightInset(percent);\n    };\n\n    if (crossesZero) {\n      // Keep center anchor stable by always clipping the low/high pair,\n      // independent of sign branch, then snapping at terminal edges.\n      const lowPercent = Math.min(valuePercent, zeroPercent);\n      const highPercent = Math.max(valuePercent, zeroPercent);\n      return `inset(0 ${snapRightInset(highPercent)} 0 ${snapLeftInset(lowPercent)})`;\n    }\n    // Non-crossing: fill starts at left edge; snap right inset at terminals.\n    return `inset(0 ${snapRightInset(valuePercent)} 0 0)`;\n  }, [crossesZero, zeroPercent, valuePercent]);\n\n  const fillMaskImage = crossesZero\n    ? \"linear-gradient(to right, rgba(0,0,0,0.2) 0%, rgba(0,0,0,0.35) 50%, rgba(0,0,0,0.7) 100%)\"\n    : \"linear-gradient(to right, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.7) 100%)\";\n\n  // Metallic reflection gradient that follows the handle position\n  // Visible while dragging OR when resting at edges (0%/100%)\n  const reflectionStyle = useMemo(() => {\n    const edgeThreshold = 3;\n    const nearEdge =\n      valuePercent <= edgeThreshold || valuePercent >= 100 - edgeThreshold;\n\n    // Narrower spread when stationary at edges (~35% narrower)\n    const spreadPercent = nearEdge && !isDragging ? 6.5 : 10;\n    const handlePos = toRadixThumbPosition(valuePercent);\n    const start = `clamp(0%, calc(${handlePos} - ${spreadPercent}%), 100%)`;\n    const end = `clamp(0%, calc(${handlePos} + ${spreadPercent}%), 100%)`;\n\n    const gradient = `linear-gradient(to right,\n      transparent ${start},\n      white ${handlePos},\n      transparent ${end})`;\n\n    return {\n      background: gradient,\n      WebkitMask:\n        \"linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)\",\n      WebkitMaskComposite: \"xor\",\n      maskComposite: \"exclude\",\n      padding: \"1px\",\n    };\n  }, [valuePercent, isDragging]);\n\n  // Opacity scales with handle size: rest → hover → drag\n  const reflectionOpacity = useMemo(() => {\n    const edgeThreshold = 3;\n    const atEdge =\n      valuePercent <= edgeThreshold || valuePercent >= 100 - edgeThreshold;\n\n    if (isDragging || atEdge) {\n      return 1;\n    }\n    if (isHovered) {\n      return 0.6;\n    }\n    return 0;\n  }, [valuePercent, isDragging, isHovered]);\n\n  const handleValueChange = useCallback(\n    (values: number[]) => {\n      if (values[0] !== undefined) {\n        onChange(values[0]);\n      }\n    },\n    [onChange],\n  );\n\n  return (\n    <div className=\"py-2\">\n      <SliderPrimitive.Root\n        id={id}\n        className={cn(\n          \"group/slider relative flex w-full touch-none items-center select-none\",\n          \"isolate h-12\",\n          isDragging\n            ? \"[&>span]:transition-[left,transform] [&>span]:duration-45 [&>span]:ease-linear\"\n            : \"[&>span]:transition-[left,transform] [&>span]:duration-90 [&>span]:ease-[cubic-bezier(0.22,1,0.36,1)]\",\n          \"[&>span]:will-change-[left,transform]\",\n          \"motion-reduce:[&>span]:transition-none\",\n          disabled && \"pointer-events-none opacity-50\",\n        )}\n        value={[value]}\n        onValueChange={handleValueChange}\n        onPointerDown={() => setIsDragging(true)}\n        onPointerUp={() => setIsDragging(false)}\n        onPointerEnter={() => setIsHovered(true)}\n        onPointerLeave={() => setIsHovered(false)}\n        min={min}\n        max={max}\n        step={step}\n        disabled={disabled}\n        aria-valuetext={getAriaValueText(value, min, max, unit)}\n      >\n        <SliderPrimitive.Track\n          ref={trackRef}\n          className={cn(\n            \"squircle relative h-12 w-full grow overflow-hidden rounded-sm\",\n            \"ring-border ring-1 ring-inset\",\n            \"dark:ring-white/10\",\n            resolvedTrackClassName ?? \"bg-muted\",\n          )}\n        >\n          <div\n            className={cn(\n              \"absolute inset-0 will-change-[clip-path]\",\n              isDragging\n                ? \"transition-[clip-path] duration-45 ease-linear\"\n                : \"transition-[clip-path] duration-90 ease-[cubic-bezier(0.22,1,0.36,1)]\",\n              \"motion-reduce:transition-none\",\n              resolvedFillClassName ?? \"bg-primary/30 dark:bg-primary/40\",\n            )}\n            style={{\n              maskImage: fillMaskImage,\n              WebkitMaskImage: fillMaskImage,\n              clipPath: fillClipPath,\n            }}\n          />\n\n          {ticks.map((tick, i) => {\n            const isEdge =\n              !tick.isSubtick && (tick.percent === 0 || tick.percent === 100);\n            return (\n              <span\n                key={i}\n                className={cn(\n                  \"pointer-events-none absolute bottom-px w-px\",\n                  tick.isSubtick ? \"h-1.5\" : \"h-2\",\n                  isEdge\n                    ? \"bg-transparent\"\n                    : tick.isSubtick\n                      ? \"bg-foreground/8 dark:bg-white/5\"\n                      : tick.isCenter\n                        ? \"bg-foreground/30 dark:bg-white/25\"\n                        : \"bg-foreground/15 dark:bg-white/8\",\n                )}\n                style={{\n                  left: toInsetPosition(tick.percent),\n                  transform: \"translateX(-50%)\",\n                }}\n              />\n            );\n          })}\n        </SliderPrimitive.Track>\n\n        {/* Metallic reflection overlay - follows handle, brightness scales with interaction */}\n        <div\n          className={cn(\n            \"squircle pointer-events-none absolute inset-0 rounded-sm\",\n            isDragging\n              ? \"transition-[opacity,background] duration-45 ease-linear\"\n              : \"transition-[opacity,background] duration-90 ease-[cubic-bezier(0.22,1,0.36,1)]\",\n            \"motion-reduce:transition-none\",\n          )}\n          style={{\n            ...reflectionStyle,\n            opacity: reflectionOpacity,\n            filter: \"blur(1px)\",\n            mixBlendMode: \"overlay\",\n          }}\n        />\n\n        <SliderPrimitive.Thumb\n          className={cn(\n            \"group/thumb z-0 block w-3 shrink-0 cursor-grab rounded-sm\",\n            \"relative bg-transparent outline-none\",\n            \"transition-[height,opacity] duration-150 ease-[var(--cubic-ease-in-out)]\",\n            \"focus-visible:outline-ring focus-visible:outline-2 focus-visible:outline-offset-1\",\n            \"active:cursor-grabbing\",\n            \"disabled:pointer-events-none disabled:opacity-50\",\n            // Height morphs: rest (track height) → hover → active\n            isDragging ? \"h-[56px]\" : isHovered ? \"h-[54px]\" : \"h-12\",\n          )}\n        >\n          {(() => {\n            // Calculate morph state\n            const isActive = isHovered || isDragging;\n\n            // Indicator stays centered on the real thumb while CSS transitions\n            // smooth thumb wrapper and fill movement together.\n            const fillEdgeOffset = 0;\n\n            // Hide rest-state indicator at edges (0% or 100%) - the reflection gradient handles this\n            const edgeThreshold = 3;\n            const atEdge =\n              valuePercent <= edgeThreshold ||\n              valuePercent >= 100 - edgeThreshold;\n            const restOpacity = atEdge ? 0 : 0.25;\n\n            // Asymmetric segment heights: gap is shifted up to match raised text position\n            // Top segment is shorter, bottom segment is taller\n            const topHeight =\n              isActive && gap > 0\n                ? `calc(50% - ${gap / 2 + TEXT_VERTICAL_OFFSET}px)`\n                : \"50%\";\n            const bottomHeight =\n              isActive && gap > 0\n                ? `calc(50% - ${gap / 2 - TEXT_VERTICAL_OFFSET}px)`\n                : \"50%\";\n\n            return (\n              <>\n                <span\n                  className={cn(\n                    \"absolute top-0 left-1/2\",\n                    \"transition-all duration-100 ease-[var(--cubic-ease-in-out)]\",\n                    isActive\n                      ? gap > 0\n                        ? \"rounded-full\"\n                        : \"rounded-t-full\"\n                      : \"rounded-t-sm\",\n                    isDragging ? \"w-2\" : isActive ? \"w-1.5\" : \"w-px\",\n                    resolvedHandleClassName ?? \"bg-primary\",\n                  )}\n                  style={{\n                    transform: `translateX(calc(-50% + ${fillEdgeOffset}px))`,\n                    height: topHeight,\n                    opacity: isActive ? 1 : restOpacity,\n                  }}\n                />\n                <span\n                  className={cn(\n                    \"absolute bottom-0 left-1/2\",\n                    \"transition-all duration-100 ease-[var(--cubic-ease-in-out)]\",\n                    isActive\n                      ? gap > 0\n                        ? \"rounded-full\"\n                        : \"rounded-b-full\"\n                      : \"rounded-b-sm\",\n                    isDragging ? \"w-2\" : isActive ? \"w-1.5\" : \"w-px\",\n                    resolvedHandleClassName ?? \"bg-primary\",\n                  )}\n                  style={{\n                    transform: `translateX(calc(-50% + ${fillEdgeOffset}px))`,\n                    height: bottomHeight,\n                    opacity: isActive ? 1 : restOpacity,\n                  }}\n                />\n              </>\n            );\n          })()}\n        </SliderPrimitive.Thumb>\n\n        <div\n          className=\"pointer-events-none absolute inset-x-3 top-1/2 z-10 flex items-center justify-between\"\n          style={{\n            transform: `translateY(calc(-50% - ${TEXT_VERTICAL_OFFSET}px))`,\n          }}\n        >\n          <span\n            ref={labelRef}\n            className=\"text-primary -mt-px rounded-full px-2 py-px text-sm font-normal tracking-wide\"\n          >\n            {label}\n          </span>\n          <span\n            ref={valueRef}\n            className=\"text-foreground -mt-px -mb-0.5 flex h-6 items-center rounded-full px-2 font-mono text-xs tabular-nums\"\n          >\n            {formatSignedValue(value, min, max, precision, unit)}\n          </span>\n        </div>\n      </SliderPrimitive.Root>\n    </div>\n  );\n}\n\nexport function ParameterSlider({\n  id,\n  sliders,\n  values: controlledValues,\n  onChange,\n  actions,\n  onAction,\n  onBeforeAction,\n  className,\n  trackClassName,\n  fillClassName,\n  handleClassName,\n}: ParameterSliderProps) {\n  const slidersSignature = useMemo(\n    () => createSliderSignature(sliders),\n    [sliders],\n  );\n  const sliderSnapshot = useMemo(\n    () => createSliderValueSnapshot(sliders),\n    [sliders],\n  );\n  const {\n    value: currentValues,\n    isControlled,\n    setValue,\n    setUncontrolledValue,\n  } = useControllableState<SliderValue[]>({\n    value: controlledValues,\n    defaultValue: sliderSnapshot,\n    onChange,\n  });\n\n  useSignatureReset(slidersSignature, () => {\n    if (!isControlled) {\n      setUncontrolledValue(sliderSnapshot);\n    }\n  });\n\n  const valueMap = useMemo(() => {\n    const map = new Map<string, number>();\n    for (const v of currentValues) {\n      map.set(v.id, v.value);\n    }\n    return map;\n  }, [currentValues]);\n\n  const updateValue = useCallback(\n    (sliderId: string, newValue: number) => {\n      setValue((prev) =>\n        prev.map((v) => (v.id === sliderId ? { ...v, value: newValue } : v)),\n      );\n    },\n    [setValue],\n  );\n\n  const handleReset = useCallback(() => {\n    setValue(sliderSnapshot);\n  }, [setValue, sliderSnapshot]);\n\n  const handleAction = useCallback(\n    async (actionId: string) => {\n      let nextValues = currentValues;\n      if (actionId === \"reset\") {\n        handleReset();\n        nextValues = sliderSnapshot;\n      }\n\n      await onAction?.(actionId, nextValues);\n    },\n    [currentValues, handleReset, onAction, sliderSnapshot],\n  );\n\n  const normalizedActions = useMemo(() => {\n    const normalized = normalizeActionsConfig(actions);\n    if (normalized) return normalized;\n    return {\n      items: [\n        { id: \"reset\", label: \"Reset\", variant: \"ghost\" as const },\n        { id: \"apply\", label: \"Apply\", variant: \"default\" as const },\n      ],\n      align: \"right\" as const,\n    };\n  }, [actions]);\n\n  return (\n    <article\n      className={cn(\n        \"@container/parameter-slider isolate flex w-full max-w-md min-w-80 flex-col gap-3\",\n        \"text-foreground\",\n        className,\n      )}\n      data-slot=\"parameter-slider\"\n      data-tool-ui-id={id}\n    >\n      <div\n        className={cn(\n          \"bg-card flex w-full flex-col overflow-hidden rounded-2xl border px-5 py-3 shadow-xs\",\n        )}\n      >\n        {sliders.map((slider) => (\n          <SliderRow\n            key={slider.id}\n            config={slider}\n            value={valueMap.get(slider.id) ?? slider.value}\n            onChange={(v) => updateValue(slider.id, v)}\n            trackClassName={trackClassName}\n            fillClassName={fillClassName}\n            handleClassName={handleClassName}\n          />\n        ))}\n      </div>\n\n      <div className=\"@container/actions\">\n        <ActionButtons\n          actions={normalizedActions.items}\n          align={normalizedActions.align}\n          confirmTimeout={normalizedActions.confirmTimeout}\n          onAction={handleAction}\n          onBeforeAction={\n            onBeforeAction\n              ? (actionId) => onBeforeAction(actionId, currentValues)\n              : undefined\n          }\n        />\n      </div>\n    </article>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/parameter-slider/schema.ts",
    "content": "import { z } from \"zod\";\nimport { type ActionsProp } from \"../shared/actions-config\";\nimport type { EmbeddedActionsProps } from \"../shared/embedded-actions\";\nimport { defineToolUiContract } from \"../shared/contract\";\nimport {\n  SerializableActionSchema,\n  SerializableActionsConfigSchema,\n  ToolUIIdSchema,\n  ToolUIRoleSchema,\n} from \"../shared/schema\";\n\nexport const SliderConfigSchema = z\n  .object({\n    id: z.string().min(1),\n    label: z.string().min(1),\n    min: z.number().finite(),\n    max: z.number().finite(),\n    step: z.number().finite().positive().optional(),\n    value: z.number().finite(),\n    unit: z.string().optional(),\n    precision: z.number().int().min(0).optional(),\n    disabled: z.boolean().optional(),\n    trackClassName: z.string().optional(),\n    fillClassName: z.string().optional(),\n    handleClassName: z.string().optional(),\n  })\n  .superRefine((slider, ctx) => {\n    if (slider.max <= slider.min) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        path: [\"max\"],\n        message: \"max must be greater than min\",\n      });\n    }\n\n    if (slider.value < slider.min || slider.value > slider.max) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        path: [\"value\"],\n        message: \"value must be between min and max\",\n      });\n    }\n  });\n\nexport type SliderConfig = z.infer<typeof SliderConfigSchema>;\n\nexport const SerializableParameterSliderSchema = z\n  .object({\n    id: ToolUIIdSchema,\n    role: ToolUIRoleSchema.optional(),\n    sliders: z.array(SliderConfigSchema).min(1),\n    actions: z\n      .union([\n        z.array(SerializableActionSchema),\n        SerializableActionsConfigSchema,\n      ])\n      .optional(),\n  })\n  .strict()\n  .superRefine((payload, ctx) => {\n    const seenIds = new Map<string, number>();\n\n    payload.sliders.forEach((slider, index) => {\n      const firstSeenAt = seenIds.get(slider.id);\n      if (firstSeenAt !== undefined) {\n        ctx.addIssue({\n          code: z.ZodIssueCode.custom,\n          path: [\"sliders\", index, \"id\"],\n          message: `duplicate slider id '${slider.id}' (first seen at index ${firstSeenAt})`,\n        });\n        return;\n      }\n      seenIds.set(slider.id, index);\n    });\n  });\n\nexport type SerializableParameterSlider = z.infer<\n  typeof SerializableParameterSliderSchema\n>;\n\nconst SerializableParameterSliderSchemaContract = defineToolUiContract(\n  \"ParameterSlider\",\n  SerializableParameterSliderSchema,\n);\n\nexport const parseSerializableParameterSlider: (\n  input: unknown,\n) => SerializableParameterSlider =\n  SerializableParameterSliderSchemaContract.parse;\n\nexport const safeParseSerializableParameterSlider: (\n  input: unknown,\n) => SerializableParameterSlider | null =\n  SerializableParameterSliderSchemaContract.safeParse;\n\nexport interface SliderValue {\n  id: string;\n  value: number;\n}\n\nexport interface ParameterSliderProps extends Omit<\n  SerializableParameterSlider,\n  \"actions\"\n> {\n  className?: string;\n  values?: SliderValue[];\n  onChange?: (values: SliderValue[]) => void;\n  actions?: ActionsProp;\n  onAction?: EmbeddedActionsProps<SliderValue[]>[\"onAction\"];\n  onBeforeAction?: EmbeddedActionsProps<SliderValue[]>[\"onBeforeAction\"];\n  trackClassName?: string;\n  fillClassName?: string;\n  handleClassName?: string;\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/plan/README.md",
    "content": "# Plan\n\nImplementation for the \"plan\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/plan/index.tsx\n- serializable schema + parse helpers: components/tool-ui/plan/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/plan/content.mdx\n- Preset payload: lib/presets/plan.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/plan/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn          → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Accordion   → shadcn/ui Accordion\n *   Card        → shadcn/ui Card\n *   Collapsible → shadcn/ui Collapsible\n */\n\nexport { cn } from \"@/lib/utils\";\nexport {\n  Accordion,\n  AccordionItem,\n  AccordionTrigger,\n  AccordionContent,\n} from \"@/components/ui/accordion\";\nexport {\n  Card,\n  CardHeader,\n  CardTitle,\n  CardDescription,\n  CardContent,\n  CardFooter,\n} from \"@/components/ui/card\";\nexport {\n  Collapsible,\n  CollapsibleTrigger,\n  CollapsibleContent,\n} from \"@/components/ui/collapsible\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/plan/index.tsx",
    "content": "export { Plan, PlanCompact } from \"./plan\";\nexport type {\n  PlanProps,\n  PlanTodo,\n  PlanTodoStatus,\n  SerializablePlan,\n} from \"./schema\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/plan/plan.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { useMemo, useState, useEffect, useRef, memo } from \"react\";\nimport { Loader2, Check, X, MoreHorizontal, ChevronRight } from \"lucide-react\";\nimport type { PlanProps, PlanTodo, PlanTodoStatus } from \"./schema\";\nimport {\n  cn,\n  Card,\n  CardHeader,\n  CardTitle,\n  CardDescription,\n  CardContent,\n  Accordion,\n  AccordionItem,\n  AccordionTrigger,\n  AccordionContent,\n  Collapsible,\n  CollapsibleTrigger,\n  CollapsibleContent,\n} from \"./_adapter\";\nimport { calculatePlanProgress, shouldCelebrateProgress } from \"./progress\";\n\nconst INITIAL_VISIBLE_TODO_COUNT = 4;\n\nconst TodoIcon = memo(function TodoIcon({\n  status,\n}: {\n  status: PlanTodoStatus;\n}) {\n  if (status === \"pending\") {\n    return (\n      <span\n        className=\"border-border bg-card flex size-6 shrink-0 items-center justify-center rounded-full border motion-safe:transition-all motion-safe:duration-200\"\n        aria-hidden=\"true\"\n      />\n    );\n  }\n\n  if (status === \"in_progress\") {\n    return (\n      <span\n        className=\"border-border bg-card flex size-6 shrink-0 items-center justify-center rounded-full border shadow-[0_0_0_4px_hsl(var(--primary)/0.1)] motion-safe:transition-all motion-safe:duration-300\"\n        aria-hidden=\"true\"\n      >\n        <Loader2 className=\"text-primary size-5 motion-safe:animate-spin\" />\n      </span>\n    );\n  }\n\n  if (status === \"completed\") {\n    return (\n      <span\n        className=\"border-primary bg-primary flex size-6 shrink-0 items-center justify-center rounded-full border shadow-sm motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:duration-300 motion-safe:ease-out\"\n        aria-hidden=\"true\"\n      >\n        <Check\n          className=\"text-primary-foreground size-4 motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:delay-75 motion-safe:duration-200 motion-safe:fill-mode-both\"\n          strokeWidth={3}\n        />\n      </span>\n    );\n  }\n\n  if (status === \"cancelled\") {\n    return (\n      <span\n        className=\"border-destructive bg-destructive flex size-6 shrink-0 items-center justify-center rounded-full border shadow-sm motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:duration-300 motion-safe:ease-out dark:border-red-600 dark:bg-red-600\"\n        aria-hidden=\"true\"\n      >\n        <X\n          className=\"size-4 text-white motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:delay-75 motion-safe:duration-200 motion-safe:fill-mode-both\"\n          strokeWidth={3}\n        />\n      </span>\n    );\n  }\n\n  return null;\n});\n\ninterface PlanTodoItemProps {\n  todo: PlanTodo;\n  className?: string;\n  style?: React.CSSProperties;\n  showConnector?: boolean;\n}\n\nfunction areTodoPropsEqual(\n  prev: PlanTodoItemProps,\n  next: PlanTodoItemProps,\n): boolean {\n  if (prev.todo.id !== next.todo.id) return false;\n  if (prev.todo.label !== next.todo.label) return false;\n  if (prev.todo.status !== next.todo.status) return false;\n  if (prev.todo.description !== next.todo.description) return false;\n  if (prev.showConnector !== next.showConnector) return false;\n  if (prev.className !== next.className) return false;\n  const prevStyle = prev.style;\n  const nextStyle = next.style;\n  if (prevStyle === nextStyle) return true;\n  if (!prevStyle || !nextStyle) return false;\n  return (\n    prevStyle.animationDelay === nextStyle.animationDelay &&\n    prevStyle.animationFillMode === nextStyle.animationFillMode\n  );\n}\n\nconst PlanTodoItem = memo(function PlanTodoItem({\n  todo,\n  className,\n  style,\n  showConnector,\n}: PlanTodoItemProps) {\n  const [isOpen, setIsOpen] = React.useState(false);\n\n  const labelElement = (\n    <span\n      className={cn(\n        \"text-sm leading-6 font-medium break-words\",\n        todo.status === \"pending\" && \"text-muted-foreground\",\n        todo.status === \"in_progress\" &&\n          \"motion-safe:shimmer shimmer-invert text-foreground\",\n        (todo.status === \"completed\" || todo.status === \"cancelled\") &&\n          \"text-muted-foreground\",\n      )}\n    >\n      {todo.label}\n    </span>\n  );\n\n  if (!todo.description) {\n    return (\n      <li\n        className={cn(\n          \"relative -mx-2 flex cursor-default items-start gap-3 rounded-md px-2 py-1.5\",\n          className,\n        )}\n        style={style}\n      >\n        {showConnector && (\n          <div\n            className=\"bg-border absolute top-6 left-5 w-px\"\n            style={{\n              height: \"calc(100% + 0.25rem)\",\n            }}\n            aria-hidden=\"true\"\n          />\n        )}\n        <div className=\"relative z-10\">\n          <TodoIcon status={todo.status} />\n        </div>\n        <div className=\"min-w-0 flex-1\">{labelElement}</div>\n      </li>\n    );\n  }\n\n  return (\n    <li\n      className={cn(\n        \"relative -mx-2 min-w-0 cursor-default rounded-md\",\n        className,\n      )}\n      style={style}\n    >\n      {showConnector && (\n        <div\n          className=\"bg-border absolute top-6 left-5 w-px\"\n          style={{\n            height: \"calc(100% + 0.25rem)\",\n          }}\n          aria-hidden=\"true\"\n        />\n      )}\n      <Collapsible asChild open={isOpen} onOpenChange={setIsOpen}>\n        <div\n          className=\"data-[state=open]:bg-primary/5 min-w-0 rounded-md motion-safe:transition-all motion-safe:duration-200\"\n          style={{\n            backdropFilter: isOpen ? \"blur(2px)\" : undefined,\n          }}\n        >\n          <CollapsibleTrigger className=\"group/todo flex w-full cursor-default items-start gap-3 px-2 py-1.5 text-left\">\n            <div className=\"relative z-10\">\n              <TodoIcon status={todo.status} />\n            </div>\n            <span className=\"min-w-0 flex-1\">{labelElement}</span>\n            <ChevronRight className=\"text-muted-foreground/50 group-hover/todo:text-muted-foreground mt-0.5 size-4 shrink-0 rotate-90 group-data-[state=open]/todo:[transform:rotateY(180deg)] motion-safe:transition-transform motion-safe:duration-300 motion-safe:ease-[cubic-bezier(0.34,1.56,0.64,1)]\" />\n          </CollapsibleTrigger>\n          <CollapsibleContent\n            className=\"group/content\"\n            data-slot=\"collapsible-content\"\n          >\n            <div className=\"min-w-0 motion-safe:group-data-[state=closed]/content:animate-out motion-safe:group-data-[state=closed]/content:fade-out motion-safe:group-data-[state=closed]/content:slide-out-to-top-1 motion-safe:group-data-[state=closed]/content:duration-150 motion-safe:group-data-[state=open]/content:animate-in motion-safe:group-data-[state=open]/content:fade-in motion-safe:group-data-[state=open]/content:slide-in-from-top-1 motion-safe:group-data-[state=open]/content:delay-75 motion-safe:group-data-[state=open]/content:duration-150 motion-safe:group-data-[state=open]/content:fill-mode-both\">\n              <p className=\"text-muted-foreground min-w-0 pr-2 pb-1.5 pl-11 text-sm text-pretty break-words\">\n                {todo.description}\n              </p>\n            </div>\n          </CollapsibleContent>\n        </div>\n      </Collapsible>\n    </li>\n  );\n}, areTodoPropsEqual);\n\ninterface TodoListProps {\n  todos: PlanTodo[];\n  newTodoIds: Set<string>;\n}\n\nfunction TodoList({ todos, newTodoIds }: TodoListProps) {\n  return (\n    <>\n      {todos.map((todo, index) => {\n        const isNew = newTodoIds.has(todo.id);\n        const staggerDelay = isNew ? index * 50 : 0;\n\n        return (\n          <PlanTodoItem\n            key={todo.id}\n            todo={todo}\n            showConnector={index < todos.length - 1}\n            className={cn(\n              isNew &&\n                \"motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-1 motion-safe:duration-300 motion-safe:ease-out\",\n            )}\n            style={\n              isNew\n                ? {\n                    animationDelay: `${staggerDelay}ms`,\n                    animationFillMode: \"backwards\",\n                  }\n                : undefined\n            }\n          />\n        );\n      })}\n    </>\n  );\n}\n\ninterface ProgressBarProps {\n  progress: number;\n  isCelebrating: boolean;\n}\n\nconst ProgressBar = memo(function ProgressBar({\n  progress,\n  isCelebrating,\n}: ProgressBarProps) {\n  return (\n    <div\n      className=\"bg-muted relative mb-3 h-1.5 overflow-hidden rounded-full\"\n      role=\"progressbar\"\n      aria-valuemin={0}\n      aria-valuemax={100}\n      aria-valuenow={progress}\n    >\n      <div\n        className={cn(\n          \"h-full rounded-full transition-all duration-500\",\n          progress === 100\n            ? \"bg-gradient-to-r from-emerald-600 via-emerald-500 to-emerald-400 motion-safe:animate-in motion-safe:fade-in motion-safe:duration-500 motion-safe:ease-out\"\n            : \"bg-primary\",\n        )}\n        style={{\n          width: `${progress}%`,\n          boxShadow:\n            \"inset 0 1px 0 rgba(255,255,255,0.3), 0 1px 2px rgba(0,0,0,0.2)\",\n        }}\n      />\n      {isCelebrating && (\n        <div\n          className=\"pointer-events-none absolute inset-0 rounded-full motion-safe:animate-pulse\"\n          style={{\n            boxShadow: \"0 0 20px rgba(16, 185, 129, 0.6)\",\n          }}\n        />\n      )}\n    </div>\n  );\n});\n\nfunction PlanRoot({\n  id,\n  title,\n  description,\n  todos,\n  maxVisibleTodos = INITIAL_VISIBLE_TODO_COUNT,\n  className,\n  compact = false,\n}: PlanProps & { compact?: boolean }) {\n  const seenTodoIds = useRef(new Set<string>());\n  const [newTodoIds, setNewTodoIds] = useState<Set<string>>(new Set());\n  const [isCelebrating, setIsCelebrating] = useState(false);\n  const prevProgressRef = useRef(0);\n\n  const { visibleTodos, hiddenTodos, completedCount, allComplete, progress } =\n    useMemo(() => {\n      const completed = todos.filter((t) => t.status === \"completed\").length;\n      return {\n        visibleTodos: todos.slice(0, maxVisibleTodos),\n        hiddenTodos: todos.slice(maxVisibleTodos),\n        completedCount: completed,\n        allComplete: completed === todos.length,\n        progress: calculatePlanProgress({\n          completedCount: completed,\n          totalCount: todos.length,\n        }),\n      };\n    }, [todos, maxVisibleTodos]);\n\n  useEffect(() => {\n    const newIds = new Set<string>();\n\n    todos.forEach((todo) => {\n      if (!seenTodoIds.current.has(todo.id)) {\n        newIds.add(todo.id);\n        seenTodoIds.current.add(todo.id);\n      }\n    });\n\n    if (newIds.size > 0) {\n      setNewTodoIds(newIds);\n\n      // Clear animation class after entrance completes\n      const timer = setTimeout(() => {\n        setNewTodoIds(new Set());\n      }, 500);\n\n      return () => clearTimeout(timer);\n    }\n  }, [todos]);\n\n  useEffect(() => {\n    const shouldCelebrate = shouldCelebrateProgress({\n      previous: prevProgressRef.current,\n      next: progress,\n    });\n    prevProgressRef.current = progress;\n\n    if (shouldCelebrate) {\n      setIsCelebrating(true);\n      const timer = setTimeout(() => setIsCelebrating(false), 1000);\n      return () => clearTimeout(timer);\n    }\n  }, [progress]);\n\n  const todoList = (\n    <ul className={cn(\"min-w-0 space-y-1\", compact ? \"mt-0\" : \"mt-4\")}>\n      <TodoList todos={visibleTodos} newTodoIds={newTodoIds} />\n\n      {hiddenTodos.length > 0 && (\n        <li className=\"mt-1\">\n          <Accordion type=\"single\" collapsible>\n            <AccordionItem value=\"more\" className=\"border-0\">\n              <AccordionTrigger className=\"text-muted-foreground hover:text-primary flex cursor-default items-start justify-start gap-2 py-1 text-sm font-normal [&>svg:last-child]:hidden\">\n                <MoreHorizontal className=\"text-muted-foreground/70 mt-0.5 size-4 shrink-0\" />\n                <span>{hiddenTodos.length} more</span>\n              </AccordionTrigger>\n              <AccordionContent className=\"pt-2 pb-0\">\n                <ul className=\"-mx-2 space-y-2 px-2\">\n                  <TodoList todos={hiddenTodos} newTodoIds={newTodoIds} />\n                </ul>\n              </AccordionContent>\n            </AccordionItem>\n          </Accordion>\n        </li>\n      )}\n    </ul>\n  );\n\n  return (\n    <Card\n      className={cn(\"isolate w-full max-w-xl min-w-80 gap-4 py-4\", className)}\n      data-tool-ui-id={id}\n      data-slot=\"plan\"\n    >\n      {!compact && (\n        <CardHeader className=\"flex flex-row items-start justify-between gap-4\">\n          <div className=\"space-y-1.5\">\n            <CardTitle className=\"leading-5 font-medium text-pretty\">\n              {title}\n            </CardTitle>\n            {description && <CardDescription>{description}</CardDescription>}\n          </div>\n          {allComplete && (\n            <Check className=\"mt-0.5 size-5 shrink-0 text-emerald-500\" />\n          )}\n        </CardHeader>\n      )}\n\n      <CardContent className=\"min-w-0 px-4\">\n        <div\n          className={cn(\n            \"min-w-0\",\n            !compact && \"bg-muted/70 rounded-lg px-6 py-4\",\n          )}\n        >\n          {!compact && (\n            <>\n              <div className=\"text-muted-foreground mb-2 text-sm\">\n                {completedCount} of {todos.length} complete\n              </div>\n              <ProgressBar progress={progress} isCelebrating={isCelebrating} />\n            </>\n          )}\n          {todoList}\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n\nfunction PlanComponent(props: PlanProps) {\n  return <PlanRoot key={props.id} {...props} />;\n}\n\nexport function PlanCompact(props: PlanProps) {\n  return <PlanRoot key={props.id} {...props} compact />;\n}\n\ntype PlanComponentType = typeof PlanComponent & {\n  Compact: typeof PlanCompact;\n};\n\nexport const Plan = Object.assign(PlanComponent, {\n  Compact: PlanCompact,\n}) as PlanComponentType;\n"
  },
  {
    "path": "apps/www/components/tool-ui/plan/progress.ts",
    "content": "type ProgressInput = {\n  completedCount: number;\n  totalCount: number;\n};\n\ntype CelebrateProgressInput = {\n  previous: number;\n  next: number;\n};\n\nfunction clampProgress(value: number): number {\n  if (!Number.isFinite(value)) return 0;\n  return Math.max(0, Math.min(100, value));\n}\n\nexport function calculatePlanProgress({\n  completedCount,\n  totalCount,\n}: ProgressInput): number {\n  if (totalCount <= 0) return 0;\n  return clampProgress((completedCount / totalCount) * 100);\n}\n\nexport function shouldCelebrateProgress({\n  previous,\n  next,\n}: CelebrateProgressInput): boolean {\n  return previous < 100 && next === 100;\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/plan/schema.ts",
    "content": "import { z } from \"zod\";\nimport {\n  ToolUIIdSchema,\n  ToolUIReceiptSchema,\n  ToolUIRoleSchema,\n} from \"../shared/schema\";\nimport { defineToolUiContract } from \"../shared/contract\";\n\nexport const PlanTodoStatusSchema = z.enum([\n  \"pending\",\n  \"in_progress\",\n  \"completed\",\n  \"cancelled\",\n]);\n\nexport const PlanTodoSchema = z.object({\n  id: z.string().min(1),\n  label: z.string().min(1),\n  status: PlanTodoStatusSchema,\n  description: z.string().optional(),\n});\n\nexport type PlanTodoStatus = z.infer<typeof PlanTodoStatusSchema>;\nexport type PlanTodo = z.infer<typeof PlanTodoSchema>;\n\nexport const PlanPropsSchema = z\n  .object({\n    id: ToolUIIdSchema,\n    role: ToolUIRoleSchema.optional(),\n    receipt: ToolUIReceiptSchema.optional(),\n    title: z.string().min(1),\n    description: z.string().optional(),\n    todos: z.array(PlanTodoSchema).min(1),\n    maxVisibleTodos: z.number().finite().int().min(1).optional(),\n  })\n  .superRefine((value, ctx) => {\n    const seenTodoIds = new Set<string>();\n    value.todos.forEach((todo, index) => {\n      if (seenTodoIds.has(todo.id)) {\n        ctx.addIssue({\n          code: \"custom\",\n          path: [\"todos\", index, \"id\"],\n          message: `Duplicate todo id \"${todo.id}\".`,\n        });\n        return;\n      }\n      seenTodoIds.add(todo.id);\n    });\n  });\n\nexport type PlanProps = z.infer<typeof PlanPropsSchema> & {\n  className?: string;\n};\n\nexport const SerializablePlanSchema = PlanPropsSchema;\n\nexport type SerializablePlan = z.infer<typeof SerializablePlanSchema>;\n\nconst SerializablePlanSchemaContract = defineToolUiContract(\n  \"Plan\",\n  SerializablePlanSchema,\n);\n\nexport const parseSerializablePlan: (input: unknown) => SerializablePlan =\n  SerializablePlanSchemaContract.parse;\n\nexport const safeParseSerializablePlan: (\n  input: unknown,\n) => SerializablePlan | null = SerializablePlanSchemaContract.safeParse;\n"
  },
  {
    "path": "apps/www/components/tool-ui/preferences-panel/README.md",
    "content": "# Preferences Panel\n\nImplementation for the \"preferences-panel\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/preferences-panel/index.tsx\n- serializable schema + parse helpers: components/tool-ui/preferences-panel/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/preferences-panel/content.mdx\n- Preset payload: lib/presets/preferences-panel.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/preferences-panel/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn           → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Button       → shadcn/ui Button\n *   Switch       → shadcn/ui Switch\n *   ToggleGroup  → shadcn/ui ToggleGroup\n *   Select       → shadcn/ui Select\n *   Separator    → shadcn/ui Separator\n *   Label        → shadcn/ui Label\n */\n\nexport { cn } from \"@/lib/utils\";\nexport { Button } from \"@/components/ui/button\";\nexport { Switch } from \"@/components/ui/switch\";\nexport { ToggleGroup, ToggleGroupItem } from \"@/components/ui/toggle-group\";\nexport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nexport { Separator } from \"@/components/ui/separator\";\nexport { Label } from \"@/components/ui/label\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/preferences-panel/index.tsx",
    "content": "export { PreferencesPanel, PreferencesPanelReceipt } from \"./preferences-panel\";\nexport {\n  type SerializablePreferencesPanel,\n  type SerializablePreferencesPanelReceipt,\n  type PreferencesPanelProps,\n  type PreferencesPanelReceiptProps,\n  type PreferencesValue,\n  type PreferenceItem,\n  type PreferenceSection,\n} from \"./schema\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/preferences-panel/preferences-panel.tsx",
    "content": "\"use client\";\n\nimport { useCallback, useMemo } from \"react\";\nimport type {\n  PreferencesPanelProps,\n  PreferencesPanelReceiptProps,\n  PreferencesValue,\n  PreferenceItem,\n  PreferenceSection,\n} from \"./schema\";\nimport { ActionButtons } from \"../shared/action-buttons\";\nimport { normalizeActionsConfig } from \"../shared/actions-config\";\nimport { type Action } from \"../shared/schema\";\nimport { useControllableState } from \"../shared/use-controllable-state\";\nimport { useSignatureReset } from \"../shared/use-signature-reset\";\n\nimport {\n  cn,\n  Switch,\n  ToggleGroup,\n  ToggleGroupItem,\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n  Separator,\n  Label,\n} from \"./_adapter\";\nimport { Check, AlertCircle } from \"lucide-react\";\nimport { createPreferencesSectionSignature } from \"./signature\";\n\nfunction getInitialValue(item: PreferenceItem): string | boolean {\n  switch (item.type) {\n    case \"switch\":\n      return item.defaultChecked ?? false;\n    case \"toggle\":\n      return item.defaultValue ?? item.options?.[0]?.value ?? \"\";\n    case \"select\":\n      return item.defaultSelected ?? item.selectOptions?.[0]?.value ?? \"\";\n  }\n}\n\nfunction formatDisplayValue(\n  item: PreferenceItem,\n  value: string | boolean,\n): string {\n  if (item.type === \"switch\") {\n    return typeof value === \"boolean\" && value ? \"On\" : \"Off\";\n  }\n\n  const stringValue = typeof value === \"string\" ? value : \"\";\n  const options = item.type === \"toggle\" ? item.options : item.selectOptions;\n  const option = options?.find((opt) => opt.value === stringValue);\n\n  return option?.label ?? stringValue;\n}\n\nfunction computeInitialValues(sections: PreferenceSection[]): PreferencesValue {\n  return sections.reduce<PreferencesValue>((acc, section) => {\n    section.items.forEach((item) => {\n      acc[item.id] = getInitialValue(item);\n    });\n    return acc;\n  }, {});\n}\n\ninterface PreferenceControlProps {\n  item: PreferenceItem;\n  value: string | boolean;\n  onChange: (value: string | boolean) => void;\n  disabled?: boolean;\n}\n\nfunction SwitchControl({\n  id,\n  checked,\n  onChange,\n  disabled,\n  label,\n}: {\n  id: string;\n  checked: boolean;\n  onChange: (value: boolean) => void;\n  disabled?: boolean;\n  label: string;\n}) {\n  return (\n    <Switch\n      id={id}\n      checked={checked}\n      onCheckedChange={onChange}\n      disabled={disabled}\n      aria-label={label}\n    />\n  );\n}\n\nfunction ToggleControl({\n  value,\n  options,\n  onChange,\n  disabled,\n  label,\n}: {\n  value: string;\n  options: Array<{ value: string; label: string }>;\n  onChange: (value: string) => void;\n  disabled?: boolean;\n  label: string;\n}) {\n  return (\n    <ToggleGroup\n      type=\"single\"\n      value={value}\n      onValueChange={(v) => v && onChange(v)}\n      disabled={disabled}\n      aria-label={label}\n      className=\"gap-1\"\n    >\n      {options.map((opt) => (\n        <ToggleGroupItem\n          key={opt.value}\n          value={opt.value}\n          aria-label={opt.label}\n          className=\"!rounded-full px-3 py-1.5 text-sm\"\n        >\n          {opt.label}\n        </ToggleGroupItem>\n      ))}\n    </ToggleGroup>\n  );\n}\n\nfunction SelectControl({\n  id,\n  value,\n  options,\n  onChange,\n  disabled,\n  label,\n}: {\n  id: string;\n  value: string;\n  options: Array<{ value: string; label: string }>;\n  onChange: (value: string) => void;\n  disabled?: boolean;\n  label: string;\n}) {\n  return (\n    <Select value={value} onValueChange={onChange} disabled={disabled}>\n      <SelectTrigger id={id} className=\"w-[180px]\" aria-label={label}>\n        <SelectValue placeholder=\"Select...\" />\n      </SelectTrigger>\n      <SelectContent>\n        {options.map((opt) => (\n          <SelectItem key={opt.value} value={opt.value}>\n            {opt.label}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  );\n}\n\nfunction PreferenceControl({\n  item,\n  value,\n  onChange,\n  disabled,\n}: PreferenceControlProps) {\n  const id = `preference-${item.id}`;\n\n  if (item.type === \"switch\") {\n    return (\n      <SwitchControl\n        id={id}\n        checked={typeof value === \"boolean\" ? value : false}\n        onChange={onChange}\n        disabled={disabled}\n        label={item.label}\n      />\n    );\n  }\n\n  const stringValue = typeof value === \"string\" ? value : \"\";\n\n  if (item.type === \"toggle\" && item.options) {\n    return (\n      <ToggleControl\n        value={stringValue}\n        options={item.options}\n        onChange={onChange}\n        disabled={disabled}\n        label={item.label}\n      />\n    );\n  }\n\n  if (item.type === \"select\" && item.selectOptions) {\n    return (\n      <SelectControl\n        id={id}\n        value={stringValue}\n        options={item.selectOptions}\n        onChange={onChange}\n        disabled={disabled}\n        label={item.label}\n      />\n    );\n  }\n\n  return null;\n}\n\ninterface PreferenceItemRowProps {\n  item: PreferenceItem;\n  value: string | boolean;\n  onChange?: (value: string | boolean) => void;\n  disabled?: boolean;\n  isReceipt?: boolean;\n  error?: string;\n  showSuccessIndicators?: boolean;\n  isFirstInSectionWithoutHeading?: boolean;\n}\n\nfunction ItemLabel({\n  item,\n  error,\n  isReceipt,\n}: {\n  item: PreferenceItem;\n  error?: string;\n  isReceipt: boolean;\n}) {\n  const htmlFor = `preference-${item.id}`;\n\n  if (isReceipt) {\n    return (\n      <>\n        <span className=\"text-sm leading-6 font-medium text-pretty\">\n          {item.label}\n        </span>\n        {error ? (\n          <span className=\"text-destructive text-sm font-normal text-pretty\">\n            {error}\n          </span>\n        ) : item.description ? (\n          <span className=\"text-muted-foreground text-sm font-normal text-pretty\">\n            {item.description}\n          </span>\n        ) : null}\n      </>\n    );\n  }\n\n  return (\n    <>\n      <Label htmlFor={htmlFor} className=\"leading-6 font-medium text-pretty\">\n        {item.label}\n      </Label>\n      {item.description && (\n        <p className=\"text-muted-foreground text-sm font-normal text-pretty\">\n          {item.description}\n        </p>\n      )}\n    </>\n  );\n}\n\nfunction ItemValue({\n  item,\n  value,\n  error,\n  showSuccessIndicators,\n}: {\n  item: PreferenceItem;\n  value: string | boolean;\n  error?: string;\n  showSuccessIndicators: boolean;\n}) {\n  const displayValue = formatDisplayValue(item, value);\n\n  return (\n    <div className=\"flex shrink-0 items-center gap-2\">\n      <span className=\"text-muted-foreground text-sm font-medium\">\n        {displayValue}\n      </span>\n      {error ? (\n        <AlertCircle className=\"text-destructive size-3.5\" />\n      ) : showSuccessIndicators ? (\n        <Check className=\"size-3.5 text-emerald-600 dark:text-emerald-500\" />\n      ) : null}\n    </div>\n  );\n}\n\nfunction PreferenceItemRow({\n  item,\n  value,\n  onChange,\n  disabled,\n  isReceipt = false,\n  error,\n  showSuccessIndicators = false,\n  isFirstInSectionWithoutHeading = false,\n}: PreferenceItemRowProps) {\n  const shouldStack = item.type !== \"switch\" && !isReceipt;\n\n  return (\n    <div\n      className={cn(\n        \"flex items-start justify-between gap-4\",\n        isFirstInSectionWithoutHeading ? \"pt-0 pb-3\" : \"py-3\",\n        shouldStack &&\n          \"flex-col gap-3 @sm/preferences-panel:flex-row @sm/preferences-panel:gap-4\",\n      )}\n    >\n      <div className=\"flex flex-col gap-1\">\n        <ItemLabel item={item} error={error} isReceipt={isReceipt} />\n      </div>\n\n      {isReceipt ? (\n        <ItemValue\n          item={item}\n          value={value}\n          error={error}\n          showSuccessIndicators={showSuccessIndicators}\n        />\n      ) : (\n        <div className=\"flex shrink-0\">\n          <PreferenceControl\n            item={item}\n            value={value}\n            onChange={onChange!}\n            disabled={disabled}\n          />\n        </div>\n      )}\n    </div>\n  );\n}\n\ninterface ItemListProps {\n  items: PreferenceItem[];\n  values: PreferencesValue;\n  onChangeValue?: (itemId: string, value: string | boolean) => void;\n  disabled?: boolean;\n  isReceipt?: boolean;\n  errors?: Record<string, string>;\n  showSuccessIndicators?: boolean;\n  hasHeading?: boolean;\n  hasTitle?: boolean;\n}\n\nfunction ItemList({\n  items,\n  values,\n  onChangeValue,\n  disabled,\n  isReceipt,\n  errors,\n  showSuccessIndicators,\n  hasHeading = false,\n  hasTitle = false,\n}: ItemListProps) {\n  const shouldRemoveFirstPadding = !hasHeading && hasTitle;\n\n  return (\n    <div className=\"flex flex-col\">\n      {items.map((item, index) => {\n        const isFirst = index === 0;\n        const itemValue = values[item.id] ?? getInitialValue(item);\n        const handleChange = onChangeValue\n          ? (v: string | boolean) => onChangeValue(item.id, v)\n          : undefined;\n\n        return (\n          <div key={item.id}>\n            {!isFirst && <Separator className=\"my-1\" />}\n            <PreferenceItemRow\n              item={item}\n              value={itemValue}\n              onChange={handleChange}\n              disabled={disabled}\n              isReceipt={isReceipt}\n              error={errors?.[item.id]}\n              showSuccessIndicators={showSuccessIndicators}\n              isFirstInSectionWithoutHeading={\n                isFirst && shouldRemoveFirstPadding\n              }\n            />\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n\ninterface PreferencesSectionProps {\n  section: PreferenceSection;\n  values: PreferencesValue;\n  onChangeValue?: (itemId: string, value: string | boolean) => void;\n  disabled?: boolean;\n  isReceipt?: boolean;\n  errors?: Record<string, string>;\n  hasTitle?: boolean;\n}\n\nfunction PreferencesSection({\n  section,\n  values,\n  onChangeValue,\n  disabled,\n  isReceipt = false,\n  errors,\n  hasTitle = false,\n}: PreferencesSectionProps) {\n  const hasErrors = !!(errors && Object.keys(errors).length > 0);\n\n  const content = (\n    <ItemList\n      items={section.items}\n      values={values}\n      onChangeValue={onChangeValue}\n      disabled={disabled}\n      isReceipt={isReceipt}\n      errors={errors}\n      showSuccessIndicators={hasErrors}\n      hasHeading={!!section.heading}\n      hasTitle={hasTitle}\n    />\n  );\n\n  if (section.heading) {\n    return (\n      <fieldset className=\"flex flex-col\">\n        <legend className=\"text-muted-foreground pb-1 text-xs tracking-widest uppercase\">\n          {section.heading}\n        </legend>\n        {content}\n      </fieldset>\n    );\n  }\n\n  return content;\n}\n\ninterface ReceiptHeaderProps {\n  title: string;\n  hasErrors: boolean;\n}\n\nfunction ReceiptHeader({ title, hasErrors }: ReceiptHeaderProps) {\n  return (\n    <>\n      <div className=\"flex items-center justify-between gap-3 px-5 py-4\">\n        <h2 className=\"text-base leading-none font-semibold\">{title}</h2>\n        {hasErrors === true ? (\n          <span className=\"text-destructive flex items-center gap-1.5 text-xs font-medium\">\n            <AlertCircle className=\"size-3.5\" />\n            Error\n          </span>\n        ) : (\n          <span className=\"flex items-center gap-1.5 text-xs font-medium text-emerald-600 dark:text-emerald-500\">\n            <Check className=\"size-3.5\" />\n            Saved\n          </span>\n        )}\n      </div>\n      <Separator />\n    </>\n  );\n}\n\nexport function PreferencesPanelReceipt({\n  id,\n  title,\n  sections,\n  choice,\n  error,\n  className,\n}: PreferencesPanelReceiptProps) {\n  const hasErrors = error && Object.keys(error).length > 0;\n\n  return (\n    <article\n      data-slot=\"preferences-panel\"\n      data-tool-ui-id={id}\n      data-receipt=\"true\"\n      role=\"status\"\n      aria-label={\n        hasErrors ? \"Preferences with errors\" : \"Confirmed preferences\"\n      }\n      className={cn(\n        \"@container/preferences-panel flex w-full max-w-md min-w-80 flex-col\",\n        className,\n      )}\n    >\n      <div className=\"bg-card/60 flex w-full flex-col overflow-hidden rounded-2xl border opacity-95 shadow-xs\">\n        {title && <ReceiptHeader title={title} hasErrors={!!hasErrors} />}\n        <div\n          className={cn(\"flex flex-col gap-4 px-5\", title ? \"py-6\" : \"py-2\")}\n        >\n          {sections.map((section, index) => (\n            <div key={index}>\n              <PreferencesSection\n                section={section}\n                values={choice}\n                errors={error}\n                isReceipt={true}\n                hasTitle={!!title}\n              />\n            </div>\n          ))}\n        </div>\n      </div>\n    </article>\n  );\n}\n\nfunction PreferencesPanelRoot({\n  id,\n  title,\n  sections,\n  value: controlledValue,\n  onChange,\n  actions,\n  onAction,\n  onBeforeAction,\n  className,\n}: PreferencesPanelProps) {\n  const initialValues = useMemo(\n    () => computeInitialValues(sections),\n    [sections],\n  );\n  const sectionsSignature = useMemo(\n    () => createPreferencesSectionSignature(sections),\n    [sections],\n  );\n  const {\n    value: currentValue,\n    isControlled,\n    setValue,\n    setUncontrolledValue,\n  } = useControllableState<PreferencesValue>({\n    value: controlledValue,\n    defaultValue: initialValues,\n    onChange,\n  });\n\n  useSignatureReset(sectionsSignature, () => {\n    if (!isControlled) {\n      setUncontrolledValue(initialValues);\n    }\n  });\n\n  const updateValue = useCallback(\n    (itemId: string, newValue: string | boolean) => {\n      setValue((prev) => ({ ...prev, [itemId]: newValue }));\n    },\n    [setValue],\n  );\n\n  const isDirty = useMemo(() => {\n    return Object.keys(currentValue).some(\n      (key) => currentValue[key] !== initialValues[key],\n    );\n  }, [currentValue, initialValues]);\n\n  const handleCancel = useCallback((): PreferencesValue => {\n    setValue(initialValues);\n    return initialValues;\n  }, [initialValues, setValue]);\n\n  const handleAction = useCallback(\n    async (actionId: string) => {\n      let nextValue = currentValue;\n\n      if (actionId === \"cancel\") {\n        nextValue = handleCancel();\n      }\n\n      await onAction?.(actionId, nextValue);\n    },\n    [currentValue, handleCancel, onAction],\n  );\n\n  const normalizedActions = useMemo(() => {\n    const normalized = normalizeActionsConfig(actions);\n    if (normalized) {\n      return {\n        ...normalized,\n        align: normalized.align ?? (\"right\" as const),\n      };\n    }\n\n    const defaultActions: Action[] = [\n      { id: \"cancel\", label: \"Cancel\", variant: \"ghost\" },\n      { id: \"save\", label: \"Save Changes\", variant: \"default\" },\n    ];\n\n    return {\n      items: defaultActions,\n      align: \"right\" as const,\n    };\n  }, [actions]);\n\n  const actionsWithState = useMemo((): Action[] => {\n    return normalizedActions.items.map((action) => {\n      const isSaveAction = action.id === \"save\";\n      const baseDisabled = \"disabled\" in action ? action.disabled : false;\n      const shouldDisable = baseDisabled || (isSaveAction && !isDirty);\n\n      return {\n        ...action,\n        disabled: shouldDisable,\n      };\n    });\n  }, [normalizedActions.items, isDirty]);\n\n  return (\n    <article\n      data-slot=\"preferences-panel\"\n      data-tool-ui-id={id}\n      role=\"form\"\n      className={cn(\n        \"text-foreground @container/preferences-panel flex w-full max-w-md min-w-80 flex-col gap-3\",\n        className,\n      )}\n    >\n      <div className=\"bg-card flex w-full flex-col overflow-hidden rounded-2xl border shadow-xs\">\n        {title && (\n          <>\n            <div className=\"px-5 py-4\">\n              <h2 className=\"text-base leading-none font-semibold\">{title}</h2>\n            </div>\n            <Separator />\n          </>\n        )}\n        <div\n          className={cn(\"flex flex-col gap-4 px-5\", title ? \"py-6\" : \"py-2\")}\n        >\n          {sections.map((section, sectionIndex) => (\n            <div key={sectionIndex}>\n              <PreferencesSection\n                section={section}\n                values={currentValue}\n                onChangeValue={updateValue}\n                isReceipt={false}\n                hasTitle={!!title}\n              />\n            </div>\n          ))}\n        </div>\n      </div>\n\n      <div className=\"@container/actions\">\n        <ActionButtons\n          actions={actionsWithState}\n          align={normalizedActions.align}\n          confirmTimeout={normalizedActions.confirmTimeout}\n          onAction={handleAction}\n          onBeforeAction={\n            onBeforeAction\n              ? (actionId) => onBeforeAction(actionId, currentValue)\n              : undefined\n          }\n        />\n      </div>\n    </article>\n  );\n}\n\ntype PreferencesPanelComponent = typeof PreferencesPanelRoot & {\n  Receipt: typeof PreferencesPanelReceipt;\n};\n\nexport const PreferencesPanel = Object.assign(PreferencesPanelRoot, {\n  Receipt: PreferencesPanelReceipt,\n}) as PreferencesPanelComponent;\n"
  },
  {
    "path": "apps/www/components/tool-ui/preferences-panel/schema.ts",
    "content": "import { z } from \"zod\";\nimport { type ActionsProp } from \"../shared/actions-config\";\nimport type { EmbeddedActionsProps } from \"../shared/embedded-actions\";\nimport { defineToolUiContract } from \"../shared/contract\";\nimport {\n  SerializableActionSchema,\n  SerializableActionsConfigSchema,\n  ToolUIIdSchema,\n  ToolUIReceiptSchema,\n  ToolUIRoleSchema,\n} from \"../shared/schema\";\n\nconst PreferenceItemBaseSchema = z.object({\n  id: z.string().min(1),\n  label: z.string().min(1),\n  description: z.string().optional(),\n});\n\nconst PreferenceSwitchSchema = PreferenceItemBaseSchema.extend({\n  type: z.literal(\"switch\"),\n  defaultChecked: z.boolean().optional(),\n});\n\nconst PreferenceToggleSchema = PreferenceItemBaseSchema.extend({\n  type: z.literal(\"toggle\"),\n  options: z\n    .array(\n      z.object({\n        value: z.string().min(1),\n        label: z.string().min(1),\n      }),\n    )\n    .min(2),\n  defaultValue: z.string().optional(),\n});\n\nconst PreferenceSelectSchema = PreferenceItemBaseSchema.extend({\n  type: z.literal(\"select\"),\n  selectOptions: z\n    .array(\n      z.object({\n        value: z.string().min(1),\n        label: z.string().min(1),\n      }),\n    )\n    .min(5),\n  defaultSelected: z.string().optional(),\n});\n\nconst PreferenceItemSchema = z.discriminatedUnion(\"type\", [\n  PreferenceSwitchSchema,\n  PreferenceToggleSchema,\n  PreferenceSelectSchema,\n]);\n\nconst PreferenceSectionSchema = z.object({\n  heading: z.string().min(1).optional(),\n  items: z.array(PreferenceItemSchema).min(1),\n});\n\nconst PreferencesPanelBaseSchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  receipt: ToolUIReceiptSchema.optional(),\n  title: z.string().min(1).optional(),\n  sections: z.array(PreferenceSectionSchema).min(1),\n});\n\nexport const SerializablePreferencesPanelSchema =\n  PreferencesPanelBaseSchema.extend({\n    actions: z\n      .union([\n        z.array(SerializableActionSchema),\n        SerializableActionsConfigSchema,\n      ])\n      .optional(),\n  }).strict();\n\nexport const SerializablePreferencesPanelReceiptSchema =\n  PreferencesPanelBaseSchema.extend({\n    choice: z.record(z.string(), z.union([z.string(), z.boolean()])),\n    error: z.record(z.string(), z.string()).optional(),\n  }).strict();\n\nexport type SerializablePreferencesPanel = z.infer<\n  typeof SerializablePreferencesPanelSchema\n>;\n\nexport type SerializablePreferencesPanelReceipt = z.infer<\n  typeof SerializablePreferencesPanelReceiptSchema\n>;\n\nconst SerializablePreferencesPanelSchemaContract = defineToolUiContract(\n  \"PreferencesPanel\",\n  SerializablePreferencesPanelSchema,\n);\n\nconst SerializablePreferencesPanelReceiptSchemaContract = defineToolUiContract(\n  \"PreferencesPanelReceipt\",\n  SerializablePreferencesPanelReceiptSchema,\n);\n\nexport const parseSerializablePreferencesPanel: (\n  input: unknown,\n) => SerializablePreferencesPanel =\n  SerializablePreferencesPanelSchemaContract.parse;\n\nexport const safeParseSerializablePreferencesPanel: (\n  input: unknown,\n) => SerializablePreferencesPanel | null =\n  SerializablePreferencesPanelSchemaContract.safeParse;\n\nexport const parseSerializablePreferencesPanelReceipt: (\n  input: unknown,\n) => SerializablePreferencesPanelReceipt =\n  SerializablePreferencesPanelReceiptSchemaContract.parse;\n\nexport const safeParseSerializablePreferencesPanelReceipt: (\n  input: unknown,\n) => SerializablePreferencesPanelReceipt | null =\n  SerializablePreferencesPanelReceiptSchemaContract.safeParse;\n\nexport interface PreferencesValue {\n  [itemId: string]: string | boolean;\n}\n\nexport interface PreferencesPanelProps extends Omit<\n  SerializablePreferencesPanel,\n  \"actions\"\n> {\n  className?: string;\n  value?: PreferencesValue;\n  onChange?: (value: PreferencesValue) => void;\n  actions?: ActionsProp;\n  onAction?: EmbeddedActionsProps<PreferencesValue>[\"onAction\"];\n  onBeforeAction?: EmbeddedActionsProps<PreferencesValue>[\"onBeforeAction\"];\n}\n\nexport interface PreferencesPanelReceiptProps extends SerializablePreferencesPanelReceipt {\n  className?: string;\n}\n\nexport type PreferenceItem = z.infer<typeof PreferenceItemSchema>;\nexport type PreferenceSection = z.infer<typeof PreferenceSectionSchema>;\n"
  },
  {
    "path": "apps/www/components/tool-ui/preferences-panel/signature.ts",
    "content": "import type { PreferenceSection } from \"./schema\";\n\nexport function createPreferencesSectionSignature(\n  sections: PreferenceSection[],\n): string {\n  return JSON.stringify(\n    sections.map((section) => ({\n      heading: section.heading ?? \"\",\n      items: section.items.map((item) => {\n        if (item.type === \"switch\") {\n          return {\n            id: item.id,\n            type: item.type,\n            defaultChecked: item.defaultChecked ?? false,\n          };\n        }\n\n        if (item.type === \"toggle\") {\n          return {\n            id: item.id,\n            type: item.type,\n            defaultValue: item.defaultValue ?? item.options[0]?.value ?? \"\",\n            options: item.options.map((option) => option.value),\n          };\n        }\n\n        return {\n          id: item.id,\n          type: item.type,\n          defaultSelected:\n            item.defaultSelected ?? item.selectOptions[0]?.value ?? \"\",\n          options: item.selectOptions.map((option) => option.value),\n        };\n      }),\n    })),\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/progress-tracker/README.md",
    "content": "# Progress Tracker\n\nImplementation for the \"progress-tracker\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/progress-tracker/index.tsx\n- serializable schema + parse helpers: components/tool-ui/progress-tracker/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/progress-tracker/content.mdx\n- Preset payload: lib/presets/progress-tracker.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/progress-tracker/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn        → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n */\n\nexport { cn } from \"@/lib/utils\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/progress-tracker/index.tsx",
    "content": "export { ProgressTracker } from \"./progress-tracker\";\nexport {\n  type SerializableProgressTracker,\n  type ProgressTrackerProps,\n  type ProgressTrackerChoice,\n  type ProgressStep,\n} from \"./schema\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/progress-tracker/progress-tracker.tsx",
    "content": "import { cn } from \"./_adapter\";\nimport type {\n  ProgressStep,\n  ProgressTrackerChoice,\n  ProgressTrackerProps,\n} from \"./schema\";\nimport { Check, X, Loader2, Timer, AlertCircle } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\n\nfunction formatElapsedTime(milliseconds: number): string {\n  const roundedSeconds = Math.round(Math.max(0, milliseconds) / 100) / 10;\n\n  if (roundedSeconds < 60) {\n    return `${roundedSeconds.toFixed(1)}s`;\n  }\n\n  const wholeSeconds = Math.floor(roundedSeconds);\n  const minutes = Math.floor(wholeSeconds / 60);\n  const remainingSeconds = wholeSeconds % 60;\n  return `${minutes}m ${remainingSeconds}s`;\n}\n\nfunction formatElapsedTimeDateTime(milliseconds: number): string {\n  const roundedSeconds = Math.round(Math.max(0, milliseconds) / 100) / 10;\n\n  if (roundedSeconds < 60) {\n    return `PT${Number(roundedSeconds.toFixed(1))}S`;\n  }\n\n  const wholeSeconds = Math.floor(roundedSeconds);\n  const hours = Math.floor(wholeSeconds / 3600);\n  const minutes = Math.floor((wholeSeconds % 3600) / 60);\n  const seconds = wholeSeconds % 60;\n\n  const hourPart = hours > 0 ? `${hours}H` : \"\";\n  const minutePart = minutes > 0 ? `${minutes}M` : \"\";\n  const secondPart = seconds > 0 ? `${seconds}S` : \"\";\n\n  if (!hourPart && !minutePart && !secondPart) {\n    return \"PT0S\";\n  }\n\n  return `PT${hourPart}${minutePart}${secondPart}`;\n}\n\nfunction getCurrentStepId(steps: ProgressStep[]): string | null {\n  const inProgressStep = steps.find((s) => s.status === \"in-progress\");\n  if (inProgressStep) return inProgressStep.id;\n\n  const failedStep = steps.find((s) => s.status === \"failed\");\n  if (failedStep) return failedStep.id;\n\n  const firstPendingStep = steps.find((s) => s.status === \"pending\");\n  if (firstPendingStep) return firstPendingStep.id;\n\n  return null;\n}\n\nfunction getReceiptState(outcome: ProgressTrackerChoice[\"outcome\"]): {\n  toneClassName: string;\n  icon: LucideIcon;\n} {\n  switch (outcome) {\n    case \"success\":\n      return {\n        toneClassName: \"text-emerald-600 dark:text-emerald-500\",\n        icon: Check,\n      };\n    case \"partial\":\n      return {\n        toneClassName: \"text-amber-600 dark:text-amber-500\",\n        icon: AlertCircle,\n      };\n    case \"failed\":\n      return {\n        toneClassName: \"text-destructive\",\n        icon: AlertCircle,\n      };\n    case \"cancelled\":\n      return {\n        toneClassName: \"text-muted-foreground\",\n        icon: X,\n      };\n  }\n}\n\ninterface StepIndicatorProps {\n  status: \"pending\" | \"in-progress\" | \"completed\" | \"failed\";\n}\n\nfunction StepIndicator({ status }: StepIndicatorProps) {\n  if (status === \"pending\") {\n    return (\n      <span\n        className=\"bg-card border-border flex size-6 shrink-0 items-center justify-center rounded-full border motion-safe:transition-all motion-safe:duration-200\"\n        aria-hidden=\"true\"\n      />\n    );\n  }\n\n  if (status === \"in-progress\") {\n    return (\n      <span\n        className=\"bg-card border-border flex size-6 shrink-0 items-center justify-center rounded-full border shadow-[0_0_0_4px_hsl(var(--primary)/0.1)] motion-safe:transition-all motion-safe:duration-300\"\n        aria-hidden=\"true\"\n      >\n        <Loader2 className=\"text-primary size-5 motion-safe:animate-spin\" />\n      </span>\n    );\n  }\n\n  if (status === \"completed\") {\n    return (\n      <span\n        className=\"bg-primary text-primary-foreground border-primary flex size-6 shrink-0 items-center justify-center rounded-full border shadow-sm motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:duration-300 motion-safe:ease-out\"\n        aria-hidden=\"true\"\n      >\n        <Check\n          className=\"size-4 motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:delay-75 motion-safe:duration-200 motion-safe:fill-mode-both\"\n          strokeWidth={3}\n        />\n      </span>\n    );\n  }\n\n  if (status === \"failed\") {\n    return (\n      <span\n        className=\"bg-destructive border-destructive flex size-6 shrink-0 items-center justify-center rounded-full border text-white shadow-sm motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:duration-300 motion-safe:ease-out dark:border-red-600 dark:bg-red-600\"\n        aria-hidden=\"true\"\n      >\n        <X\n          className=\"size-4 motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:delay-75 motion-safe:duration-200 motion-safe:fill-mode-both\"\n          strokeWidth={3}\n        />\n      </span>\n    );\n  }\n\n  return null;\n}\n\nfunction ElapsedTimeBadge({ elapsedTime }: { elapsedTime?: number }) {\n  if (elapsedTime === undefined || elapsedTime <= 0) {\n    return null;\n  }\n\n  return (\n    <div className=\"text-muted-foreground flex items-center gap-1.5 font-mono text-xs\">\n      <Timer className=\"-mt-px size-3.5\" />\n      <time dateTime={formatElapsedTimeDateTime(elapsedTime)}>\n        {formatElapsedTime(elapsedTime)}\n      </time>\n    </div>\n  );\n}\n\ninterface ProgressTrackerBaseProps {\n  id: ProgressTrackerProps[\"id\"];\n  steps: ProgressTrackerProps[\"steps\"];\n  elapsedTime?: ProgressTrackerProps[\"elapsedTime\"];\n  className?: ProgressTrackerProps[\"className\"];\n}\n\nfunction ProgressTrackerReceipt({\n  id,\n  steps,\n  elapsedTime,\n  className,\n  choice,\n}: ProgressTrackerBaseProps & { choice: ProgressTrackerChoice }) {\n  const receiptState = getReceiptState(choice.outcome);\n  const ReceiptIcon = receiptState.icon;\n\n  return (\n    <div\n      className={cn(\n        \"isolate flex w-full max-w-md min-w-80 flex-col\",\n        \"text-foreground select-none\",\n        \"motion-safe:animate-in motion-safe:fade-in motion-safe:blur-in-sm motion-safe:zoom-in-95 motion-safe:duration-300 motion-safe:ease-[cubic-bezier(0.16,1,0.3,1)] motion-safe:fill-mode-both\",\n        className,\n      )}\n      data-slot=\"progress-tracker\"\n      data-tool-ui-id={id}\n      data-receipt=\"true\"\n      role=\"status\"\n      aria-label={choice.summary}\n    >\n      <div className=\"bg-card/60 flex w-full flex-col gap-4 rounded-2xl border p-5 shadow-xs\">\n        <div className=\"flex items-center justify-between\">\n          <ElapsedTimeBadge elapsedTime={elapsedTime} />\n          <span\n            className={cn(\n              \"flex items-center gap-1.5 text-xs font-medium\",\n              receiptState.toneClassName,\n            )}\n          >\n            <ReceiptIcon className=\"size-3.5\" />\n            {choice.summary}\n          </span>\n        </div>\n\n        <ol className=\"m-0 flex list-none flex-col gap-2 p-0\">\n          {steps.map((step, index) => (\n            <li\n              key={step.id}\n              className=\"relative -mx-2 flex items-start gap-3 rounded-lg px-2 py-1.5\"\n            >\n              {index < steps.length - 1 && (\n                <div\n                  className=\"bg-border absolute top-8 left-5 w-px\"\n                  style={{\n                    height: \"calc(100% + 0.5rem)\",\n                  }}\n                  aria-hidden=\"true\"\n                />\n              )}\n              <div className=\"relative z-10\">\n                <StepIndicator status={step.status} />\n              </div>\n              <div className=\"flex flex-1 flex-col gap-0.5\">\n                <span className=\"text-sm leading-6 font-medium\">\n                  {step.label}\n                </span>\n                {step.description && (\n                  <span className=\"text-muted-foreground text-sm\">\n                    {step.description}\n                  </span>\n                )}\n              </div>\n            </li>\n          ))}\n        </ol>\n      </div>\n    </div>\n  );\n}\n\nfunction ProgressTrackerLive({\n  id,\n  steps,\n  elapsedTime,\n  className,\n}: ProgressTrackerBaseProps) {\n  const hasInProgress = steps.some((step) => step.status === \"in-progress\");\n  const currentStepId = getCurrentStepId(steps);\n\n  return (\n    <article\n      className={cn(\n        \"isolate flex w-full max-w-md min-w-80 flex-col gap-3\",\n        \"text-foreground select-none\",\n        className,\n      )}\n      data-slot=\"progress-tracker\"\n      data-tool-ui-id={id}\n      role=\"status\"\n      aria-live=\"polite\"\n      aria-busy={hasInProgress}\n    >\n      <div className=\"bg-card flex w-full flex-col gap-4 rounded-2xl border p-5 shadow-xs\">\n        <ElapsedTimeBadge elapsedTime={elapsedTime} />\n\n        <ol className=\"m-0 flex list-none flex-col gap-3 p-0\">\n          {steps.map((step, index) => {\n            const isCurrent = step.id === currentStepId;\n            const isActive = step.status === \"in-progress\";\n            const isFailed = step.status === \"failed\";\n            const hasDescription = !!step.description;\n            const shouldShowDescription = isActive || isFailed;\n\n            return (\n              <li\n                key={step.id}\n                className=\"relative -mx-2\"\n                aria-current={isCurrent ? \"step\" : undefined}\n              >\n                {index < steps.length - 1 && (\n                  <div\n                    className={cn(\n                      \"bg-border absolute top-6 left-5 w-px\",\n                      \"motion-safe:transition-all motion-safe:duration-300\",\n                    )}\n                    style={{\n                      height: \"calc(100% + 0.25rem)\",\n                    }}\n                    aria-hidden=\"true\"\n                  />\n                )}\n                <div\n                  className={cn(\n                    \"relative z-10 flex items-start gap-3 rounded-lg px-2 py-1.5\",\n                    \"motion-safe:transition-all motion-safe:duration-300\",\n                    isCurrent && \"bg-primary/5\",\n                  )}\n                  style={{\n                    backdropFilter: isCurrent ? \"blur(2px)\" : undefined,\n                  }}\n                >\n                  <div className=\"relative z-10\">\n                    <StepIndicator status={step.status} />\n                  </div>\n                  <div className=\"flex flex-1 flex-col\">\n                    <span\n                      className={cn(\n                        \"text-sm leading-6 font-medium\",\n                        step.status === \"pending\" && \"text-muted-foreground\",\n                        step.status === \"in-progress\" &&\n                          \"motion-safe:shimmer shimmer-invert text-foreground\",\n                      )}\n                    >\n                      {step.label}\n                    </span>\n                    {hasDescription && (\n                      <div\n                        className={cn(\n                          \"grid motion-safe:transition-[grid-template-rows,opacity] motion-safe:duration-300 motion-safe:ease-out\",\n                          shouldShowDescription\n                            ? \"grid-rows-[1fr] opacity-100\"\n                            : \"grid-rows-[0fr] opacity-0\",\n                        )}\n                        aria-hidden={!shouldShowDescription}\n                      >\n                        <div className=\"overflow-hidden\">\n                          <span className=\"text-muted-foreground block pt-0.5 text-sm\">\n                            {step.description}\n                          </span>\n                        </div>\n                      </div>\n                    )}\n                  </div>\n                </div>\n              </li>\n            );\n          })}\n        </ol>\n      </div>\n    </article>\n  );\n}\n\nfunction ProgressTrackerRoot({\n  id,\n  steps,\n  elapsedTime,\n  className,\n  choice,\n}: ProgressTrackerProps) {\n  const viewKey = choice ? `receipt-${choice.outcome}` : \"interactive\";\n\n  return (\n    <div key={viewKey} className=\"contents\">\n      {choice ? (\n        <ProgressTrackerReceipt\n          id={id}\n          steps={steps}\n          elapsedTime={elapsedTime}\n          className={className}\n          choice={choice}\n        />\n      ) : (\n        <ProgressTrackerLive\n          id={id}\n          steps={steps}\n          elapsedTime={elapsedTime}\n          className={className}\n        />\n      )}\n    </div>\n  );\n}\n\ntype ProgressTrackerComponent = typeof ProgressTrackerRoot & {\n  Live: typeof ProgressTrackerLive;\n  Receipt: typeof ProgressTrackerReceipt;\n};\n\nexport const ProgressTracker = Object.assign(ProgressTrackerRoot, {\n  Live: ProgressTrackerLive,\n  Receipt: ProgressTrackerReceipt,\n}) as ProgressTrackerComponent;\n"
  },
  {
    "path": "apps/www/components/tool-ui/progress-tracker/schema.ts",
    "content": "import { z } from \"zod\";\nimport {\n  ToolUISurfaceSchema,\n  ToolUIReceiptSchema,\n  type ToolUIReceipt,\n} from \"../shared/schema\";\nimport { defineToolUiContract } from \"../shared/contract\";\n\n/**\n * Receipt state for ProgressTracker showing the outcome of a workflow.\n */\nexport type ProgressTrackerChoice = ToolUIReceipt;\n\nexport const ProgressStepSchema = z.object({\n  id: z.string().min(1),\n  label: z.string().min(1),\n  description: z.string().optional(),\n  status: z.enum([\"pending\", \"in-progress\", \"completed\", \"failed\"]),\n});\n\nexport type ProgressStep = z.infer<typeof ProgressStepSchema>;\n\nconst ProgressStepsSchema = z\n  .array(ProgressStepSchema)\n  .min(1)\n  .superRefine((steps, ctx) => {\n    const seenIds = new Set<string>();\n\n    for (const [index, step] of steps.entries()) {\n      if (seenIds.has(step.id)) {\n        ctx.addIssue({\n          code: z.ZodIssueCode.custom,\n          message: `Duplicate step id: \"${step.id}\"`,\n          path: [index, \"id\"],\n        });\n      }\n\n      seenIds.add(step.id);\n    }\n  });\n\nexport const SerializableProgressTrackerSchema = ToolUISurfaceSchema.omit({\n  receipt: true,\n})\n  .extend({\n    steps: ProgressStepsSchema,\n    elapsedTime: z.number().finite().nonnegative().optional(),\n    /**\n     * When set, renders the component in receipt state showing the workflow outcome.\n     */\n    choice: ToolUIReceiptSchema.optional(),\n  })\n  .strict();\n\nexport type SerializableProgressTracker = z.infer<\n  typeof SerializableProgressTrackerSchema\n>;\n\nconst SerializableProgressTrackerSchemaContract = defineToolUiContract(\n  \"ProgressTracker\",\n  SerializableProgressTrackerSchema,\n);\n\nexport const parseSerializableProgressTracker: (\n  input: unknown,\n) => SerializableProgressTracker =\n  SerializableProgressTrackerSchemaContract.parse;\n\nexport const safeParseSerializableProgressTracker: (\n  input: unknown,\n) => SerializableProgressTracker | null =\n  SerializableProgressTrackerSchemaContract.safeParse;\n\nexport interface ProgressTrackerProps extends SerializableProgressTracker {\n  className?: string;\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/question-flow/README.md",
    "content": "# Question Flow\n\nImplementation for the \"question-flow\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/question-flow/index.tsx\n- serializable schema + parse helpers: components/tool-ui/question-flow/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/question-flow/content.mdx\n- Preset payload: lib/presets/question-flow.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/question-flow/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn        → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Button    → shadcn/ui Button\n *   Separator → shadcn/ui Separator\n */\n\nexport { cn } from \"@/lib/utils\";\nexport { Button } from \"@/components/ui/button\";\nexport { Separator } from \"@/components/ui/separator\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/question-flow/index.tsx",
    "content": "export { QuestionFlow } from \"./question-flow\";\nexport {\n  type SerializableQuestionFlow,\n  type SerializableProgressiveMode,\n  type SerializableUpfrontMode,\n  type SerializableReceiptMode,\n  type QuestionFlowProps,\n  type QuestionFlowProgressiveProps,\n  type QuestionFlowUpfrontProps,\n  type QuestionFlowReceiptProps,\n  type QuestionFlowOption,\n  type QuestionFlowStepDefinition,\n  type QuestionFlowChoice,\n  type QuestionFlowSummaryItem,\n} from \"./schema\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/question-flow/question-flow.tsx",
    "content": "\"use client\";\n\nimport {\n  useMemo,\n  useState,\n  useCallback,\n  useRef,\n  useEffect,\n  Fragment,\n} from \"react\";\nimport type { KeyboardEvent } from \"react\";\nimport type {\n  QuestionFlowProps,\n  QuestionFlowProgressiveProps,\n  QuestionFlowUpfrontProps,\n  QuestionFlowReceiptProps,\n  QuestionFlowOption,\n} from \"./schema\";\nimport { cn, Button, Separator } from \"./_adapter\";\nimport { Check, ChevronLeft } from \"lucide-react\";\n\ninterface SelectionIndicatorProps {\n  mode: \"single\" | \"multi\";\n  isSelected: boolean;\n  disabled?: boolean;\n}\n\ninterface ProgressBarProps {\n  current: number;\n  total: number;\n}\n\nfunction ProgressBar({ current, total }: ProgressBarProps) {\n  return (\n    <div\n      className=\"flex h-1.5 gap-1\"\n      role=\"progressbar\"\n      aria-valuenow={current}\n      aria-valuemin={1}\n      aria-valuemax={total}\n    >\n      {Array.from({ length: total }).map((_, i) => (\n        <div\n          key={i}\n          className=\"relative flex-1 overflow-hidden rounded-full bg-muted\"\n        >\n          <div\n            className={cn(\n              \"absolute inset-0 origin-left rounded-full bg-primary\",\n              \"motion-safe:transition-transform motion-safe:duration-300 motion-safe:ease-[var(--cubic-ease-in-out)]\",\n              i < current ? \"scale-x-100\" : \"scale-x-0\",\n            )}\n          />\n        </div>\n      ))}\n    </div>\n  );\n}\n\nfunction SelectionIndicator({\n  mode,\n  isSelected,\n  disabled,\n}: SelectionIndicatorProps) {\n  const shape = mode === \"single\" ? \"rounded-full\" : \"rounded\";\n\n  return (\n    <div\n      className={cn(\n        \"flex size-4 shrink-0 items-center justify-center border-2\",\n        \"motion-safe:transition-colors motion-safe:duration-200\",\n        shape,\n        isSelected && [\n          \"border-primary bg-primary text-primary-foreground\",\n          \"motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:duration-300 motion-safe:ease-out\",\n        ],\n        !isSelected && \"border-muted-foreground/50\",\n        disabled && \"opacity-50\",\n      )}\n    >\n      {mode === \"multi\" && isSelected && (\n        <Check\n          className=\"size-3 motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:delay-75 motion-safe:duration-200 motion-safe:fill-mode-both\"\n          strokeWidth={3}\n        />\n      )}\n      {mode === \"single\" && isSelected && (\n        <span className=\"size-2 rounded-full bg-current motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:duration-300 motion-safe:ease-out\" />\n      )}\n    </div>\n  );\n}\n\ninterface OptionItemProps {\n  option: QuestionFlowOption;\n  isSelected: boolean;\n  isDisabled: boolean;\n  selectionMode: \"single\" | \"multi\";\n  isFirst: boolean;\n  isLast: boolean;\n  onToggle: () => void;\n  tabIndex?: number;\n  onFocus?: () => void;\n  buttonRef?: (el: HTMLButtonElement | null) => void;\n}\n\nfunction OptionItem({\n  option,\n  isSelected,\n  isDisabled,\n  selectionMode,\n  isFirst,\n  isLast,\n  onToggle,\n  tabIndex,\n  onFocus,\n  buttonRef,\n}: OptionItemProps) {\n  const hasAdjacentOptions = !isFirst && !isLast;\n\n  return (\n    <Button\n      ref={buttonRef}\n      data-id={option.id}\n      variant=\"ghost\"\n      size=\"lg\"\n      role=\"option\"\n      aria-selected={isSelected}\n      onClick={onToggle}\n      onFocus={onFocus}\n      tabIndex={tabIndex}\n      disabled={isDisabled}\n      className={cn(\n        \"peer group relative h-auto min-h-[50px] w-full justify-start text-left text-sm font-medium\",\n        \"rounded-none border-0 bg-transparent px-0 py-2 text-base shadow-none transition-none hover:bg-transparent! @md/question-flow:text-sm\",\n        isFirst && \"pb-2.5\",\n        hasAdjacentOptions && \"py-2.5\",\n      )}\n    >\n      <span\n        className={cn(\n          \"bg-primary/5 absolute inset-0 -mx-3 -my-0.5 rounded-xl opacity-0 transition-opacity group-hover:opacity-100\",\n        )}\n      />\n      <div className=\"relative flex items-start gap-3\">\n        <span className=\"flex h-6 items-center\">\n          <SelectionIndicator\n            mode={selectionMode}\n            isSelected={isSelected}\n            disabled={option.disabled}\n          />\n        </span>\n        {option.icon && (\n          <span className=\"flex h-6 items-center\">{option.icon}</span>\n        )}\n        <div className=\"flex flex-col text-left\">\n          <span className=\"leading-6 text-pretty\">{option.label}</span>\n          {option.description && (\n            <span className=\"text-muted-foreground text-sm font-normal text-pretty\">\n              {option.description}\n            </span>\n          )}\n        </div>\n      </div>\n    </Button>\n  );\n}\n\nfunction QuestionFlowReceipt({\n  id,\n  choice,\n  className,\n}: QuestionFlowReceiptProps) {\n  return (\n    <div\n      className={cn(\n        \"@container/question-flow flex w-full min-w-80 max-w-md flex-col\",\n        \"text-foreground\",\n        \"motion-safe:animate-in motion-safe:fade-in motion-safe:blur-in-sm motion-safe:zoom-in-95 motion-safe:duration-300 motion-safe:ease-out motion-safe:fill-mode-both\",\n        className,\n      )}\n      data-slot=\"question-flow\"\n      data-tool-ui-id={id}\n      data-receipt=\"true\"\n      role=\"status\"\n      aria-label={choice.title}\n    >\n      <div\n        className={cn(\n          \"bg-card/60 flex w-full flex-col gap-3 rounded-2xl border px-5 py-4 shadow-xs\",\n        )}\n      >\n        <div className=\"flex items-center justify-between gap-3\">\n          <span className=\"text-base font-medium\">{choice.title}</span>\n          <span className=\"flex items-center gap-1.5 text-xs font-medium text-emerald-600 dark:text-emerald-500\">\n            <Check className=\"size-3.5\" />\n            Complete\n          </span>\n        </div>\n        <div className=\"flex flex-col\">\n          {choice.summary.map((item, index) => (\n            <Fragment key={index}>\n              {index > 0 && <Separator className=\"my-2\" />}\n              <div\n                className=\"flex flex-col gap-0.5 text-sm motion-safe:animate-in motion-safe:fade-in motion-safe:blur-in-sm motion-safe:slide-in-from-bottom-1 motion-safe:duration-300 motion-safe:ease-out motion-safe:fill-mode-both\"\n                style={{ animationDelay: `${150 + index * 75}ms` }}\n              >\n                <span className=\"text-muted-foreground\">{item.label}</span>\n                <span className=\"font-medium\">{item.value}</span>\n              </div>\n            </Fragment>\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n}\n\ninterface StepBodyData {\n  stepKey: string;\n  title: string;\n  description?: string;\n  options: QuestionFlowOption[];\n  selectionMode: \"single\" | \"multi\";\n  selectedIds: Set<string>;\n}\n\nexport function getQuestionFlowStepIds(id: string, stepKey: string) {\n  const safeId = encodeURIComponent(id).replace(/%/g, \"_\");\n  const safeStepKey = encodeURIComponent(stepKey).replace(/%/g, \"_\");\n  return {\n    titleId: `${safeId}-${safeStepKey}-title`,\n    descriptionId: `${safeId}-${safeStepKey}-description`,\n  };\n}\n\ninterface StepContentProps {\n  step: number;\n  totalSteps?: number;\n  title: string;\n  description?: string;\n  options: QuestionFlowOption[];\n  selectionMode: \"single\" | \"multi\";\n  selectedIds: Set<string>;\n  onToggle: (optionId: string) => void;\n  onBack?: () => void;\n  onNext: () => void;\n  showBack: boolean;\n  isLastStep: boolean;\n  id: string;\n  className?: string;\n  stepKey?: string;\n  exitingStepData?: StepBodyData | null;\n  transitionDirection?: \"forward\" | \"backward\";\n}\n\nfunction StepBodyContent({\n  stepKey,\n  title,\n  description,\n  options,\n  selectionMode,\n  selectedIds,\n  onToggle,\n  id,\n  isExiting,\n  transitionDirection,\n}: {\n  stepKey: string;\n  title: string;\n  description?: string;\n  options: QuestionFlowOption[];\n  selectionMode: \"single\" | \"multi\";\n  selectedIds: Set<string>;\n  onToggle?: (optionId: string) => void;\n  id: string;\n  isExiting?: boolean;\n  transitionDirection?: \"forward\" | \"backward\";\n}) {\n  const optionRefs = useRef<Array<HTMLButtonElement | null>>([]);\n  const { titleId, descriptionId } = getQuestionFlowStepIds(id, stepKey);\n\n  const optionStates = useMemo(() => {\n    return options.map((option) => {\n      const isSelected = selectedIds.has(option.id);\n      const isDisabled = option.disabled ?? false;\n      return { option, isSelected, isDisabled };\n    });\n  }, [options, selectedIds]);\n\n  const [activeIndex, setActiveIndex] = useState(() => {\n    const firstSelected = optionStates.findIndex(\n      (s) => s.isSelected && !s.isDisabled,\n    );\n    if (firstSelected >= 0) return firstSelected;\n    const firstEnabled = optionStates.findIndex((s) => !s.isDisabled);\n    return firstEnabled >= 0 ? firstEnabled : 0;\n  });\n\n  const focusOptionAt = useCallback((index: number) => {\n    const el = optionRefs.current[index];\n    if (el) el.focus();\n    setActiveIndex(index);\n  }, []);\n\n  const findNextEnabledIndex = useCallback(\n    (start: number, direction: 1 | -1) => {\n      const len = optionStates.length;\n      if (len === 0) return 0;\n      for (let s = 1; s <= len; s++) {\n        const idx = (start + direction * s + len) % len;\n        if (!optionStates[idx].isDisabled) return idx;\n      }\n      return start;\n    },\n    [optionStates],\n  );\n\n  const handleKeyDown = useCallback(\n    (e: KeyboardEvent<HTMLDivElement>) => {\n      if (optionStates.length === 0 || isExiting) return;\n\n      const key = e.key;\n\n      if (key === \"ArrowDown\") {\n        e.preventDefault();\n        focusOptionAt(findNextEnabledIndex(activeIndex, 1));\n        return;\n      }\n\n      if (key === \"ArrowUp\") {\n        e.preventDefault();\n        focusOptionAt(findNextEnabledIndex(activeIndex, -1));\n        return;\n      }\n\n      if (key === \"Home\") {\n        e.preventDefault();\n        const first = optionStates.findIndex((s) => !s.isDisabled);\n        focusOptionAt(first >= 0 ? first : 0);\n        return;\n      }\n\n      if (key === \"End\") {\n        e.preventDefault();\n        for (let i = optionStates.length - 1; i >= 0; i--) {\n          if (!optionStates[i].isDisabled) {\n            focusOptionAt(i);\n            return;\n          }\n        }\n        return;\n      }\n\n      if (key === \"Enter\" || key === \" \") {\n        e.preventDefault();\n        const current = optionStates[activeIndex];\n        if (!current || current.isDisabled) return;\n        onToggle?.(current.option.id);\n        return;\n      }\n    },\n    [\n      activeIndex,\n      findNextEnabledIndex,\n      focusOptionAt,\n      isExiting,\n      onToggle,\n      optionStates,\n    ],\n  );\n\n  const isTransitioning = transitionDirection !== undefined;\n\n  const enterClass =\n    transitionDirection === \"forward\"\n      ? \"motion-safe:slide-in-from-right-4\"\n      : \"motion-safe:slide-in-from-left-4\";\n\n  const exitClass =\n    transitionDirection === \"forward\"\n      ? \"motion-safe:slide-out-to-left-4\"\n      : \"motion-safe:slide-out-to-right-4\";\n\n  return (\n    <div\n      key={stepKey}\n      className={cn(\n        \"flex flex-col gap-4\",\n        isExiting && [\n          \"absolute inset-0\",\n          \"motion-safe:animate-out motion-safe:fade-out motion-safe:blur-out-sm motion-safe:duration-250 motion-safe:ease-[var(--cubic-ease-in-out)] motion-safe:fill-mode-forwards\",\n          exitClass,\n        ],\n        !isExiting &&\n          isTransitioning && [\n            \"motion-safe:animate-in motion-safe:fade-in motion-safe:blur-in-sm motion-safe:duration-250 motion-safe:ease-[var(--cubic-ease-in-out)] motion-safe:fill-mode-both\",\n            enterClass,\n          ],\n      )}\n      aria-hidden={isExiting}\n    >\n      <div className=\"flex flex-col gap-1\">\n        <h2 id={titleId} className=\"text-lg font-semibold leading-tight\">\n          {title}\n        </h2>\n        {description && (\n          <p id={descriptionId} className=\"text-muted-foreground text-sm\">\n            {description}\n          </p>\n        )}\n      </div>\n\n      <div\n        className=\"flex flex-col px-1\"\n        role=\"listbox\"\n        aria-multiselectable={selectionMode === \"multi\"}\n        onKeyDown={isExiting ? undefined : handleKeyDown}\n      >\n        {optionStates.map(({ option, isSelected, isDisabled }, index) => (\n          <Fragment key={option.id}>\n            {index > 0 && (\n              <Separator\n                className=\"transition-opacity [@media(hover:hover)]:[&:has(+_:hover)]:opacity-0 [@media(hover:hover)]:[.peer:hover+&]:opacity-0\"\n                orientation=\"horizontal\"\n              />\n            )}\n            <OptionItem\n              option={option}\n              isSelected={isSelected}\n              isDisabled={isExiting || isDisabled}\n              selectionMode={selectionMode}\n              isFirst={index === 0}\n              isLast={index === optionStates.length - 1}\n              tabIndex={isExiting ? -1 : index === activeIndex ? 0 : -1}\n              onFocus={() => !isExiting && setActiveIndex(index)}\n              buttonRef={(el) => {\n                optionRefs.current[index] = el;\n              }}\n              onToggle={() => !isExiting && onToggle?.(option.id)}\n            />\n          </Fragment>\n        ))}\n      </div>\n    </div>\n  );\n}\n\nfunction StepContent({\n  step,\n  totalSteps,\n  title,\n  description,\n  options,\n  selectionMode,\n  selectedIds,\n  onToggle,\n  onBack,\n  onNext,\n  showBack,\n  isLastStep,\n  id,\n  className,\n  stepKey,\n  exitingStepData,\n  transitionDirection = \"forward\",\n}: StepContentProps) {\n  const isTransitioning =\n    exitingStepData !== null && exitingStepData !== undefined;\n  const canProceed = selectedIds.size > 0;\n  const resolvedStepKey = stepKey ?? \"current\";\n  const { titleId, descriptionId } = getQuestionFlowStepIds(\n    id,\n    resolvedStepKey,\n  );\n\n  const stepLabel = totalSteps\n    ? `Step ${step} of ${totalSteps}`\n    : `Step ${step}`;\n\n  return (\n    <div\n      className={cn(\n        \"@container/question-flow flex w-full min-w-80 max-w-md flex-col gap-3\",\n        \"text-foreground\",\n        className,\n      )}\n      data-slot=\"question-flow\"\n      data-tool-ui-id={id}\n      role=\"form\"\n      aria-labelledby={titleId}\n      aria-describedby={description ? descriptionId : undefined}\n    >\n      <div\n        className={cn(\n          \"bg-card flex w-full flex-col gap-4 rounded-2xl border p-5 shadow-xs\",\n        )}\n      >\n        <div className=\"flex flex-col gap-1\">\n          <div className=\"flex flex-col gap-2\">\n            <span\n              className=\"text-muted-foreground text-xs font-medium uppercase tracking-wide\"\n              aria-label={stepLabel}\n            >\n              {stepLabel}\n            </span>\n            {totalSteps && <ProgressBar current={step} total={totalSteps} />}\n          </div>\n        </div>\n\n        <div className=\"relative mt-1\">\n          {exitingStepData && (\n            <StepBodyContent\n              key={exitingStepData.stepKey}\n              stepKey={exitingStepData.stepKey}\n              title={exitingStepData.title}\n              description={exitingStepData.description}\n              options={exitingStepData.options}\n              selectionMode={exitingStepData.selectionMode}\n              selectedIds={exitingStepData.selectedIds}\n              id={id}\n              isExiting\n              transitionDirection={transitionDirection}\n            />\n          )}\n          <StepBodyContent\n            key={resolvedStepKey}\n            stepKey={resolvedStepKey}\n            title={title}\n            description={description}\n            options={options}\n            selectionMode={selectionMode}\n            selectedIds={selectedIds}\n            onToggle={onToggle}\n            id={id}\n            isExiting={false}\n            transitionDirection={\n              exitingStepData ? transitionDirection : undefined\n            }\n          />\n        </div>\n\n        <div className=\"flex items-center justify-between pt-2\">\n          {showBack ? (\n            <Button\n              variant=\"ghost\"\n              size=\"default\"\n              onClick={onBack}\n              disabled={isTransitioning}\n              className=\"gap-1 rounded-full text-muted-foreground\"\n            >\n              <ChevronLeft className=\"size-4\" />\n              Back\n            </Button>\n          ) : (\n            <div />\n          )}\n          <Button\n            variant=\"default\"\n            size=\"default\"\n            onClick={onNext}\n            disabled={!canProceed || isTransitioning}\n            className=\"rounded-full\"\n          >\n            {isLastStep ? \"Complete\" : \"Next\"}\n          </Button>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction QuestionFlowProgressive({\n  id,\n  step,\n  title,\n  description,\n  options,\n  selectionMode = \"single\",\n  defaultValue,\n  onSelect,\n  onBack,\n  className,\n}: QuestionFlowProgressiveProps) {\n  const [selectedIds, setSelectedIds] = useState<Set<string>>(\n    () => new Set(defaultValue ?? []),\n  );\n\n  const handleToggle = useCallback(\n    (optionId: string) => {\n      setSelectedIds((prev) => {\n        const next = new Set(prev);\n        if (selectionMode === \"single\") {\n          if (next.has(optionId)) {\n            next.delete(optionId);\n          } else {\n            next.clear();\n            next.add(optionId);\n          }\n        } else {\n          if (next.has(optionId)) {\n            next.delete(optionId);\n          } else {\n            next.add(optionId);\n          }\n        }\n        return next;\n      });\n    },\n    [selectionMode],\n  );\n\n  const handleNext = useCallback(() => {\n    if (selectedIds.size === 0) return;\n    const selection = Array.from(selectedIds);\n    onSelect?.(selection);\n  }, [onSelect, selectedIds]);\n\n  return (\n    <StepContent\n      id={id}\n      step={step}\n      title={title}\n      description={description}\n      options={options}\n      selectionMode={selectionMode}\n      selectedIds={selectedIds}\n      onToggle={handleToggle}\n      onBack={onBack}\n      onNext={handleNext}\n      showBack={step > 1 && onBack !== undefined}\n      isLastStep={false}\n      className={className}\n    />\n  );\n}\n\nfunction QuestionFlowUpfront({\n  id,\n  steps,\n  onStepChange,\n  onComplete,\n  className,\n}: QuestionFlowUpfrontProps) {\n  const [currentStepIndex, setCurrentStepIndex] = useState(0);\n  const [answers, setAnswers] = useState<Record<string, string[]>>({});\n  const [exitingStepData, setExitingStepData] = useState<StepBodyData | null>(\n    null,\n  );\n  const [transitionDirection, setTransitionDirection] = useState<\n    \"forward\" | \"backward\"\n  >(\"forward\");\n\n  const currentStep = steps[currentStepIndex];\n  const isLastStep = currentStepIndex === steps.length - 1;\n  const totalSteps = steps.length;\n\n  useEffect(() => {\n    if (exitingStepData) {\n      const timer = setTimeout(() => setExitingStepData(null), 250);\n      return () => clearTimeout(timer);\n    }\n  }, [exitingStepData]);\n\n  const currentSelection = useMemo(() => {\n    const answer = answers[currentStep.id];\n    return new Set(answer ?? []);\n  }, [answers, currentStep.id]);\n\n  const handleToggle = useCallback(\n    (optionId: string) => {\n      const mode = currentStep.selectionMode ?? \"single\";\n      setAnswers((prev) => {\n        const current = prev[currentStep.id] ?? [];\n        let next: string[];\n\n        if (mode === \"single\") {\n          next = current.includes(optionId) ? [] : [optionId];\n        } else {\n          next = current.includes(optionId)\n            ? current.filter((id) => id !== optionId)\n            : [...current, optionId];\n        }\n\n        return { ...prev, [currentStep.id]: next };\n      });\n    },\n    [currentStep.id, currentStep.selectionMode],\n  );\n\n  const handleBack = useCallback(() => {\n    if (currentStepIndex > 0) {\n      const currentStepData = steps[currentStepIndex];\n      const stepOptions: QuestionFlowOption[] = currentStepData.options.map(\n        (opt) => ({\n          ...opt,\n          icon: undefined,\n        }),\n      );\n\n      setExitingStepData({\n        stepKey: currentStepData.id,\n        title: currentStepData.title,\n        description: currentStepData.description,\n        options: stepOptions,\n        selectionMode: currentStepData.selectionMode ?? \"single\",\n        selectedIds: new Set(answers[currentStepData.id] ?? []),\n      });\n      setTransitionDirection(\"backward\");\n      const prevIndex = currentStepIndex - 1;\n      setCurrentStepIndex(prevIndex);\n      onStepChange?.(steps[prevIndex].id);\n    }\n  }, [answers, currentStepIndex, onStepChange, steps]);\n\n  const handleNext = useCallback(() => {\n    if (currentSelection.size === 0) return;\n\n    if (isLastStep) {\n      onComplete?.(answers);\n    } else {\n      const currentStepData = steps[currentStepIndex];\n      const stepOptions: QuestionFlowOption[] = currentStepData.options.map(\n        (opt) => ({\n          ...opt,\n          icon: undefined,\n        }),\n      );\n\n      setExitingStepData({\n        stepKey: currentStepData.id,\n        title: currentStepData.title,\n        description: currentStepData.description,\n        options: stepOptions,\n        selectionMode: currentStepData.selectionMode ?? \"single\",\n        selectedIds: new Set(answers[currentStepData.id] ?? []),\n      });\n      setTransitionDirection(\"forward\");\n      const nextIndex = currentStepIndex + 1;\n      setCurrentStepIndex(nextIndex);\n      onStepChange?.(steps[nextIndex].id);\n    }\n  }, [\n    answers,\n    currentSelection.size,\n    currentStepIndex,\n    isLastStep,\n    onComplete,\n    onStepChange,\n    steps,\n  ]);\n\n  const stepOptions: QuestionFlowOption[] = currentStep.options.map((opt) => ({\n    ...opt,\n    icon: undefined,\n  }));\n\n  return (\n    <StepContent\n      id={id}\n      step={currentStepIndex + 1}\n      totalSteps={totalSteps}\n      title={currentStep.title}\n      description={currentStep.description}\n      options={stepOptions}\n      selectionMode={currentStep.selectionMode ?? \"single\"}\n      selectedIds={currentSelection}\n      onToggle={handleToggle}\n      onBack={handleBack}\n      onNext={handleNext}\n      showBack={currentStepIndex > 0}\n      isLastStep={isLastStep}\n      className={className}\n      stepKey={currentStep.id}\n      exitingStepData={exitingStepData}\n      transitionDirection={transitionDirection}\n    />\n  );\n}\n\nexport function QuestionFlow(props: QuestionFlowProps) {\n  if (\"choice\" in props && props.choice !== undefined) {\n    return <QuestionFlowReceipt {...(props as QuestionFlowReceiptProps)} />;\n  }\n\n  if (\"steps\" in props && props.steps !== undefined) {\n    return <QuestionFlowUpfront {...(props as QuestionFlowUpfrontProps)} />;\n  }\n\n  return (\n    <QuestionFlowProgressive {...(props as QuestionFlowProgressiveProps)} />\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/question-flow/schema.ts",
    "content": "import { z } from \"zod\";\nimport type { ReactNode } from \"react\";\nimport { defineToolUiContract } from \"../shared/contract\";\nimport { ToolUIIdSchema, ToolUIRoleSchema } from \"../shared/schema\";\n\nexport const QuestionFlowOptionSchema = z.object({\n  id: z.string().min(1),\n  label: z.string().min(1),\n  description: z.string().optional(),\n  icon: z.custom<ReactNode>().optional(),\n  disabled: z.boolean().optional(),\n});\n\nexport type QuestionFlowOption = z.infer<typeof QuestionFlowOptionSchema>;\n\nexport const QuestionFlowStepDefinitionSchema = z.object({\n  id: z.string().min(1),\n  title: z.string().min(1),\n  description: z.string().optional(),\n  options: z.array(QuestionFlowOptionSchema.omit({ icon: true })).min(1),\n  selectionMode: z.enum([\"single\", \"multi\"]).optional(),\n});\n\nexport type QuestionFlowStepDefinition = z.infer<\n  typeof QuestionFlowStepDefinitionSchema\n>;\n\nexport const QuestionFlowSummaryItemSchema = z.object({\n  label: z.string().min(1),\n  value: z.string().min(1),\n});\n\nexport type QuestionFlowSummaryItem = z.infer<\n  typeof QuestionFlowSummaryItemSchema\n>;\n\nexport const QuestionFlowChoiceSchema = z.object({\n  title: z.string().min(1),\n  summary: z.array(QuestionFlowSummaryItemSchema).min(1),\n});\n\nexport type QuestionFlowChoice = z.infer<typeof QuestionFlowChoiceSchema>;\n\nconst BaseSchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n});\n\nexport const SerializableProgressiveModeSchema = BaseSchema.extend({\n  step: z.number().min(1),\n  title: z.string().min(1),\n  description: z.string().optional(),\n  options: z.array(QuestionFlowOptionSchema.omit({ icon: true })).min(1),\n  selectionMode: z.enum([\"single\", \"multi\"]).optional(),\n});\n\nexport type SerializableProgressiveMode = z.infer<\n  typeof SerializableProgressiveModeSchema\n>;\n\nexport const SerializableUpfrontModeSchema = BaseSchema.extend({\n  steps: z.array(QuestionFlowStepDefinitionSchema).min(1),\n});\n\nexport type SerializableUpfrontMode = z.infer<\n  typeof SerializableUpfrontModeSchema\n>;\n\nexport const SerializableReceiptModeSchema = BaseSchema.extend({\n  choice: QuestionFlowChoiceSchema,\n});\n\nexport type SerializableReceiptMode = z.infer<\n  typeof SerializableReceiptModeSchema\n>;\n\nexport const SerializableQuestionFlowSchema = z.union([\n  SerializableProgressiveModeSchema,\n  SerializableUpfrontModeSchema,\n  SerializableReceiptModeSchema,\n]);\n\nexport type SerializableQuestionFlow = z.infer<\n  typeof SerializableQuestionFlowSchema\n>;\n\nconst SerializableQuestionFlowSchemaContract = defineToolUiContract(\n  \"QuestionFlow\",\n  SerializableQuestionFlowSchema,\n);\n\nexport const parseSerializableQuestionFlow: (\n  input: unknown,\n) => SerializableQuestionFlow = SerializableQuestionFlowSchemaContract.parse;\n\nexport const safeParseSerializableQuestionFlow: (\n  input: unknown,\n) => SerializableQuestionFlow | null =\n  SerializableQuestionFlowSchemaContract.safeParse;\ninterface BaseRuntimeProps {\n  className?: string;\n}\n\nexport interface QuestionFlowProgressiveProps\n  extends BaseRuntimeProps, Omit<SerializableProgressiveMode, \"options\"> {\n  options: QuestionFlowOption[];\n  defaultValue?: string[];\n  onSelect?: (optionIds: string[]) => void | Promise<void>;\n  onBack?: () => void;\n  steps?: never;\n  choice?: never;\n}\n\nexport interface QuestionFlowUpfrontProps\n  extends BaseRuntimeProps, SerializableUpfrontMode {\n  onStepChange?: (stepId: string) => void;\n  onComplete?: (answers: Record<string, string[]>) => void | Promise<void>;\n  step?: never;\n  choice?: never;\n}\n\nexport interface QuestionFlowReceiptProps\n  extends BaseRuntimeProps, SerializableReceiptMode {\n  step?: never;\n  steps?: never;\n}\n\nexport type QuestionFlowProps =\n  | QuestionFlowProgressiveProps\n  | QuestionFlowUpfrontProps\n  | QuestionFlowReceiptProps;\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/README.md",
    "content": "# Shared\n\nImplementation for the \"shared\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/shared/index.ts\n- serializable schema + parse helpers: components/tool-ui/shared/schema.ts\n\n## Companion assets\n\n- Docs page: no dedicated docs page yet\n- Preset payload: no dedicated preset yet\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn     → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Button → shadcn/ui Button\n */\n\nexport { cn } from \"@/lib/utils\";\nexport { Button } from \"@/components/ui/button\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/action-buttons.tsx",
    "content": "\"use client\";\n\nimport type { Action } from \"./schema\";\nimport { cn, Button } from \"./_adapter\";\nimport { useActionButtons } from \"./use-action-buttons\";\n\nexport interface ActionButtonsProps {\n  actions: Action[];\n  onAction: (actionId: string) => void | Promise<void>;\n  onBeforeAction?: (actionId: string) => boolean | Promise<boolean>;\n  confirmTimeout?: number;\n  align?: \"left\" | \"center\" | \"right\";\n  className?: string;\n}\n\nexport function ActionButtons({\n  actions,\n  onAction,\n  onBeforeAction,\n  confirmTimeout = 3000,\n  align = \"right\",\n  className,\n}: ActionButtonsProps) {\n  const { actions: resolvedActions, runAction } = useActionButtons({\n    actions,\n    onAction,\n    onBeforeAction,\n    confirmTimeout,\n  });\n\n  return (\n    <div\n      className={cn(\n        \"flex flex-col gap-3\",\n        \"@sm/actions:flex-row @sm/actions:flex-wrap @sm/actions:items-center @sm/actions:gap-2\",\n        align === \"left\" && \"@sm/actions:justify-start\",\n        align === \"center\" && \"@sm/actions:justify-center\",\n        align === \"right\" && \"@sm/actions:justify-end\",\n        className,\n      )}\n    >\n      {resolvedActions.map((action) => {\n        const label = action.currentLabel;\n        const variant = action.variant || \"default\";\n\n        return (\n          <Button\n            key={action.id}\n            variant={variant}\n            onClick={() => runAction(action.id)}\n            disabled={action.isDisabled}\n            className={cn(\n              \"rounded-full px-4!\",\n              \"justify-center\",\n              \"min-h-11 w-full text-base\",\n              \"@sm/actions:min-h-0 @sm/actions:w-auto @sm/actions:px-3 @sm/actions:py-2 @sm/actions:text-sm\",\n              action.isConfirming &&\n                \"ring-destructive ring-2 ring-offset-2 motion-safe:animate-pulse\",\n            )}\n            aria-label={\n              action.shortcut ? `${label} (${action.shortcut})` : label\n            }\n          >\n            {action.isLoading && (\n              <svg\n                className=\"mr-2 h-4 w-4 motion-safe:animate-spin\"\n                xmlns=\"http://www.w3.org/2000/svg\"\n                fill=\"none\"\n                viewBox=\"0 0 24 24\"\n              >\n                <circle\n                  className=\"opacity-25\"\n                  cx=\"12\"\n                  cy=\"12\"\n                  r=\"10\"\n                  stroke=\"currentColor\"\n                  strokeWidth=\"4\"\n                />\n                <path\n                  className=\"opacity-75\"\n                  fill=\"currentColor\"\n                  d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n                />\n              </svg>\n            )}\n            {action.icon && !action.isLoading && (\n              <span className=\"mr-2\">{action.icon}</span>\n            )}\n            {label}\n            {action.shortcut && !action.isLoading && (\n              <kbd className=\"border-border bg-muted ml-2.5 hidden rounded-lg border px-2 py-0.5 font-mono text-xs font-medium sm:inline-block\">\n                {action.shortcut}\n              </kbd>\n            )}\n          </Button>\n        );\n      })}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/actions-config.ts",
    "content": "import type { Action, ActionsConfig } from \"./schema\";\n\nexport type ActionsProp = ActionsConfig | Action[];\n\nconst NEGATORY_ACTION_IDS = new Set([\n  \"cancel\",\n  \"dismiss\",\n  \"skip\",\n  \"no\",\n  \"reset\",\n  \"close\",\n  \"decline\",\n  \"reject\",\n  \"back\",\n  \"later\",\n  \"not-now\",\n  \"maybe-later\",\n]);\n\nfunction inferVariant(action: Action): Action {\n  if (action.variant) return action;\n  if (NEGATORY_ACTION_IDS.has(action.id)) {\n    return { ...action, variant: \"ghost\" };\n  }\n  return action;\n}\n\nexport function normalizeActionsConfig(\n  actions?: ActionsProp,\n): ActionsConfig | null {\n  if (!actions) return null;\n\n  const rawItems = Array.isArray(actions) ? actions : (actions.items ?? []);\n\n  if (rawItems.length === 0) {\n    return null;\n  }\n\n  const items = rawItems.map(inferVariant);\n\n  return Array.isArray(actions)\n    ? { items }\n    : {\n        items,\n        align: actions.align,\n        confirmTimeout: actions.confirmTimeout,\n      };\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/contract.ts",
    "content": "import { z } from \"zod\";\nimport { parseWithSchema, safeParseWithSchema } from \"./parse\";\n\nexport interface ToolUiContract<T> {\n  schema: z.ZodType<T>;\n  parse: (input: unknown) => T;\n  safeParse: (input: unknown) => T | null;\n}\n\nexport function defineToolUiContract<T>(\n  componentName: string,\n  schema: z.ZodType<T>,\n): ToolUiContract<T> {\n  return {\n    schema,\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\n  };\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/decision-actions.tsx",
    "content": "\"use client\";\n\nimport { useCallback } from \"react\";\nimport { ActionButtons } from \"./action-buttons\";\nimport {\n  DecisionResultSchema,\n  type DecisionAction,\n  type DecisionResult,\n} from \"./schema\";\nimport { cn } from \"./_adapter\";\nimport { useOptionalToolUI } from \"./tool-ui-context\";\n\nexport interface DecisionActionsProps<\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\n> {\n  id?: string;\n  actions: DecisionAction[];\n  onAction: (action: {\n    id: string;\n    label: string;\n  }) => DecisionResult<TPayload> | Promise<DecisionResult<TPayload>>;\n  onCommit: (result: DecisionResult<TPayload>) => void | Promise<void>;\n  onBeforeAction?: (actionId: string) => boolean | Promise<boolean>;\n  confirmTimeout?: number;\n  align?: \"left\" | \"center\" | \"right\";\n  ariaLabel?: string;\n  className?: string;\n}\n\nexport function DecisionActions<\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\n>({\n  id: explicitId,\n  actions,\n  onAction,\n  onCommit,\n  onBeforeAction,\n  confirmTimeout,\n  align = \"right\",\n  ariaLabel,\n  className,\n}: DecisionActionsProps<TPayload>) {\n  const context = useOptionalToolUI();\n  const id = context?.id ?? explicitId;\n\n  if (!id) {\n    throw new Error(\n      \"DecisionActions requires a ToolUI provider or an explicit id prop.\",\n    );\n  }\n\n  const handleAction = useCallback(\n    async (actionId: string) => {\n      const action = actions.find((item) => item.id === actionId);\n      if (!action) return;\n\n      const result = await onAction({ id: action.id, label: action.label });\n      const parsed = DecisionResultSchema.safeParse(result);\n\n      if (!parsed.success) {\n        throw new Error(\n          `DecisionActions expected a valid DecisionResult envelope for action \"${action.id}\".`,\n        );\n      }\n\n      await onCommit(parsed.data as DecisionResult<TPayload>);\n    },\n    [actions, onAction, onCommit],\n  );\n\n  return (\n    <div\n      className={cn(\"@container/actions flex flex-col gap-2\", className)}\n      data-slot=\"decision-actions\"\n      data-tool-ui-id={id}\n      aria-label={ariaLabel ?? \"Decision actions\"}\n    >\n      <ActionButtons\n        actions={actions}\n        onAction={handleAction}\n        onBeforeAction={onBeforeAction}\n        confirmTimeout={confirmTimeout}\n        align={align}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/embedded-actions.ts",
    "content": "import type { ActionsProp } from \"./actions-config\";\n\nexport type EmbeddedActionHandler<TState> = (\n  actionId: string,\n  state: TState,\n) => void | Promise<void>;\n\nexport type EmbeddedBeforeActionHandler<TState> = (\n  actionId: string,\n  state: TState,\n) => boolean | Promise<boolean>;\n\nexport interface EmbeddedActionsProps<TState> {\n  actions?: ActionsProp;\n  onAction?: EmbeddedActionHandler<TState>;\n  onBeforeAction?: EmbeddedBeforeActionHandler<TState>;\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/index.ts",
    "content": "export { ActionButtons } from \"./action-buttons\";\nexport type { ActionButtonsProps } from \"./action-buttons\";\nexport { DecisionActions } from \"./decision-actions\";\nexport type { DecisionActionsProps } from \"./decision-actions\";\nexport { LocalActions } from \"./local-actions\";\nexport type { LocalActionsProps } from \"./local-actions\";\nexport { ToolUI } from \"./tool-ui\";\nexport type {\n  ToolUIProps,\n  ToolUISurfaceProps,\n  ToolUIActionsProps,\n} from \"./tool-ui\";\nexport { useToolUI } from \"./tool-ui-context\";\nexport { normalizeActionsConfig, type ActionsProp } from \"./actions-config\";\nexport type {\n  EmbeddedActionHandler,\n  EmbeddedBeforeActionHandler,\n  EmbeddedActionsProps,\n} from \"./embedded-actions\";\nexport * from \"./contract\";\nexport * from \"./parse\";\nexport * from \"./schema\";\nexport * from \"./toolkit\";\nexport * from \"./use-controllable-state\";\nexport * from \"./use-copy-to-clipboard\";\nexport * from \"./use-signature-reset\";\nexport * from \"./utils\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/local-actions.tsx",
    "content": "\"use client\";\n\nimport { ActionButtons } from \"./action-buttons\";\nimport type { LocalAction } from \"./schema\";\nimport { cn } from \"./_adapter\";\nimport { useOptionalToolUI } from \"./tool-ui-context\";\n\nexport interface LocalActionsProps {\n  id?: string;\n  actions: LocalAction[];\n  onAction: (actionId: string) => void | Promise<void>;\n  onBeforeAction?: (actionId: string) => boolean | Promise<boolean>;\n  confirmTimeout?: number;\n  align?: \"left\" | \"center\" | \"right\";\n  ariaLabel?: string;\n  className?: string;\n}\n\nexport function LocalActions({\n  id: explicitId,\n  actions,\n  onAction,\n  onBeforeAction,\n  confirmTimeout,\n  align = \"right\",\n  ariaLabel,\n  className,\n}: LocalActionsProps) {\n  const context = useOptionalToolUI();\n  const id = context?.id ?? explicitId;\n\n  if (!id) {\n    throw new Error(\n      \"LocalActions requires a ToolUI provider or an explicit id prop.\",\n    );\n  }\n\n  return (\n    <div\n      className={cn(\"@container/actions flex flex-col gap-2\", className)}\n      data-slot=\"local-actions\"\n      data-tool-ui-id={id}\n      aria-label={ariaLabel ?? \"Local actions\"}\n    >\n      <ActionButtons\n        actions={actions}\n        onAction={onAction}\n        onBeforeAction={onBeforeAction}\n        confirmTimeout={confirmTimeout}\n        align={align}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/media/aspect-ratio.ts",
    "content": "import { z } from \"zod\";\n\nexport const AspectRatioSchema = z\n  .enum([\"auto\", \"1:1\", \"4:3\", \"16:9\", \"9:16\"])\n  .default(\"auto\");\n\nexport type AspectRatio = z.infer<typeof AspectRatioSchema>;\n\nexport const MediaFitSchema = z.enum([\"cover\", \"contain\"]).default(\"cover\");\n\nexport type MediaFit = z.infer<typeof MediaFitSchema>;\n\nexport const RATIO_CLASS_MAP: Record<AspectRatio, string> = {\n  auto: \"\",\n  \"1:1\": \"aspect-square\",\n  \"4:3\": \"aspect-[4/3]\",\n  \"16:9\": \"aspect-video\",\n  \"9:16\": \"aspect-[9/16]\",\n};\n\nexport function getRatioClass(ratio: AspectRatio): string {\n  return RATIO_CLASS_MAP[ratio];\n}\n\nexport function getFitClass(fit: MediaFit): string {\n  return fit === \"cover\" ? \"object-cover\" : \"object-contain\";\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/media/format-utils.ts",
    "content": "/**\n * Format duration in milliseconds to human-readable string.\n * @example formatDuration(128000) => \"2:08\"\n * @example formatDuration(3661000) => \"1:01:01\"\n */\nexport function formatDuration(durationMs: number): string {\n  const totalSeconds = Math.round(durationMs / 1000);\n  const hours = Math.floor(totalSeconds / 3600);\n  const minutes = Math.floor((totalSeconds % 3600) / 60);\n  const seconds = totalSeconds % 60;\n\n  if (hours > 0) {\n    return `${hours}:${minutes.toString().padStart(2, \"0\")}:${seconds\n      .toString()\n      .padStart(2, \"0\")}`;\n  }\n  return `${minutes}:${seconds.toString().padStart(2, \"0\")}`;\n}\n\n/**\n * Format file size in bytes to human-readable string.\n * @example formatFileSize(1024) => \"1 KB\"\n * @example formatFileSize(1536000) => \"1.5 MB\"\n */\nexport function formatFileSize(bytes: number): string {\n  if (bytes < 1024) return `${bytes} B`;\n  const units = [\"KB\", \"MB\", \"GB\"];\n  let size = bytes / 1024;\n  let unit = 0;\n  while (size >= 1024 && unit < units.length - 1) {\n    size /= 1024;\n    unit += 1;\n  }\n  return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unit]}`;\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/media/index.ts",
    "content": "export {\n  AspectRatioSchema,\n  MediaFitSchema,\n  RATIO_CLASS_MAP,\n  getRatioClass,\n  getFitClass,\n  type AspectRatio,\n  type MediaFit,\n} from \"./aspect-ratio\";\n\nexport { OVERLAY_GRADIENT } from \"./overlay-gradient\";\n\nexport { formatDuration, formatFileSize } from \"./format-utils\";\n\nexport { sanitizeHref } from \"./sanitize-href\";\nexport {\n  resolveSafeNavigationHref,\n  openSafeNavigationHref,\n} from \"./safe-navigation\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/media/overlay-gradient.ts",
    "content": "/**\n * Eased gradient for hover overlays on media elements.\n * Creates a smooth fade from opaque black at top to transparent.\n *\n * @see https://larsenwork.com/easing-gradients/\n */\nexport const OVERLAY_GRADIENT = `linear-gradient(\n  to bottom,\n  hsl(0, 0%, 0%) 0%,\n  hsla(0, 0%, 0%, 0.987) 8.3%,\n  hsla(0, 0%, 0%, 0.951) 16.6%,\n  hsla(0, 0%, 0%, 0.896) 24.6%,\n  hsla(0, 0%, 0%, 0.825) 32.5%,\n  hsla(0, 0%, 0%, 0.741) 40.1%,\n  hsla(0, 0%, 0%, 0.648) 47.6%,\n  hsla(0, 0%, 0%, 0.55) 54.8%,\n  hsla(0, 0%, 0%, 0.45) 61.7%,\n  hsla(0, 0%, 0%, 0.352) 68.3%,\n  hsla(0, 0%, 0%, 0.259) 74.5%,\n  hsla(0, 0%, 0%, 0.175) 80.4%,\n  hsla(0, 0%, 0%, 0.104) 86%,\n  hsla(0, 0%, 0%, 0.049) 91.1%,\n  hsla(0, 0%, 0%, 0.013) 95.8%,\n  hsla(0, 0%, 0%, 0) 100%\n)` as const;\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/media/safe-navigation.ts",
    "content": "import { sanitizeHref } from \"./sanitize-href\";\n\nexport function resolveSafeNavigationHref(\n  ...candidates: Array<string | null | undefined>\n): string | undefined {\n  for (const candidate of candidates) {\n    const safeHref = sanitizeHref(candidate ?? undefined);\n    if (safeHref) {\n      return safeHref;\n    }\n  }\n\n  return undefined;\n}\n\nexport function openSafeNavigationHref(href: string | undefined): boolean {\n  if (!href || typeof window === \"undefined\") {\n    return false;\n  }\n\n  window.open(href, \"_blank\", \"noopener,noreferrer\");\n  return true;\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/media/sanitize-href.ts",
    "content": "/**\n * Sanitize a URL to ensure it's safe for use in href attributes.\n * Allows:\n * - Absolute http(s) URLs\n * - Relative URLs (/path, ./path, ../path, ?query, #hash)\n *\n * @returns The sanitized URL string, or undefined if invalid/unsafe\n */\nexport function sanitizeHref(href?: string): string | undefined {\n  if (!href) return undefined;\n  const candidate = href.trim();\n  if (!candidate) return undefined;\n\n  if (\n    candidate.startsWith(\"/\") ||\n    candidate.startsWith(\"./\") ||\n    candidate.startsWith(\"../\") ||\n    candidate.startsWith(\"?\") ||\n    candidate.startsWith(\"#\")\n  ) {\n    if (candidate.startsWith(\"//\")) return undefined;\n    // eslint-disable-next-line no-control-regex -- intentionally matching control characters\n    if (/[\\u0000-\\u001F\\u007F]/.test(candidate)) return undefined;\n    return candidate;\n  }\n\n  try {\n    const url = new URL(candidate);\n    if (url.protocol === \"http:\" || url.protocol === \"https:\") {\n      return url.toString();\n    }\n  } catch {\n    return undefined;\n  }\n  return undefined;\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/parse.ts",
    "content": "import { z } from \"zod\";\n\nfunction formatZodPath(path: Array<string | number | symbol>): string {\n  if (path.length === 0) return \"root\";\n  return path\n    .map((segment) =>\n      typeof segment === \"number\" ? `[${segment}]` : String(segment),\n    )\n    .join(\".\");\n}\n\n/**\n * Format Zod errors into a compact `path: message` string.\n */\nexport function formatZodError(error: z.ZodError): string {\n  const parts = error.issues.map((issue) => {\n    const path = formatZodPath(issue.path);\n    return `${path}: ${issue.message}`;\n  });\n\n  return Array.from(new Set(parts)).join(\"; \");\n}\n\n/**\n * Parse unknown input and throw a readable error.\n */\nexport function parseWithSchema<T>(\n  schema: z.ZodType<T>,\n  input: unknown,\n  name: string,\n): T {\n  const res = schema.safeParse(input);\n  if (!res.success) {\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\n  }\n  return res.data;\n}\n\n/**\n * Parse unknown input, returning `null` instead of throwing on failure.\n *\n * Use this in assistant-ui `render` functions where `args` stream in\n * incrementally and may be incomplete until the tool call finishes.\n */\nexport function safeParseWithSchema<T>(\n  schema: z.ZodType<T>,\n  input: unknown,\n): T | null {\n  const res = schema.safeParse(input);\n  return res.success ? res.data : null;\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/pierre-dark-theme.js",
    "content": "//#region src/themes/pierre-dark.json\nvar name = \"pierre-dark\";\nvar type = \"dark\";\nvar colors = {\n  \"editor.background\": \"#070707\",\n  \"editor.foreground\": \"#fbfbfb\",\n  foreground: \"#fbfbfb\",\n  focusBorder: \"#009fff\",\n  \"selection.background\": \"#19283c\",\n  \"editor.selectionBackground\": \"#009fff4d\",\n  \"editor.lineHighlightBackground\": \"#19283c8c\",\n  \"editorCursor.foreground\": \"#009fff\",\n  \"editorLineNumber.foreground\": \"#84848A\",\n  \"editorLineNumber.activeForeground\": \"#adadb1\",\n  \"editorIndentGuide.background\": \"#39393c\",\n  \"editorIndentGuide.activeBackground\": \"#2e2e30\",\n  \"diffEditor.insertedTextBackground\": \"#00cab11a\",\n  \"diffEditor.deletedTextBackground\": \"#ff2e3f1a\",\n  \"sideBar.background\": \"#141415\",\n  \"sideBar.foreground\": \"#adadb1\",\n  \"sideBar.border\": \"#070707\",\n  \"sideBarTitle.foreground\": \"#fbfbfb\",\n  \"sideBarSectionHeader.background\": \"#141415\",\n  \"sideBarSectionHeader.foreground\": \"#adadb1\",\n  \"sideBarSectionHeader.border\": \"#070707\",\n  \"activityBar.background\": \"#141415\",\n  \"activityBar.foreground\": \"#fbfbfb\",\n  \"activityBar.border\": \"#070707\",\n  \"activityBar.activeBorder\": \"#009fff\",\n  \"activityBarBadge.background\": \"#009fff\",\n  \"activityBarBadge.foreground\": \"#070707\",\n  \"titleBar.activeBackground\": \"#141415\",\n  \"titleBar.activeForeground\": \"#fbfbfb\",\n  \"titleBar.inactiveBackground\": \"#141415\",\n  \"titleBar.inactiveForeground\": \"#84848A\",\n  \"titleBar.border\": \"#070707\",\n  \"list.activeSelectionBackground\": \"#19283c99\",\n  \"list.activeSelectionForeground\": \"#fbfbfb\",\n  \"list.inactiveSelectionBackground\": \"#19283c73\",\n  \"list.hoverBackground\": \"#19283c59\",\n  \"list.focusOutline\": \"#009fff\",\n  \"tab.activeBackground\": \"#070707\",\n  \"tab.activeForeground\": \"#fbfbfb\",\n  \"tab.activeBorderTop\": \"#009fff\",\n  \"tab.inactiveBackground\": \"#141415\",\n  \"tab.inactiveForeground\": \"#84848A\",\n  \"tab.border\": \"#070707\",\n  \"editorGroupHeader.tabsBackground\": \"#141415\",\n  \"editorGroupHeader.tabsBorder\": \"#070707\",\n  \"panel.background\": \"#141415\",\n  \"panel.border\": \"#070707\",\n  \"panelTitle.activeBorder\": \"#009fff\",\n  \"panelTitle.activeForeground\": \"#fbfbfb\",\n  \"panelTitle.inactiveForeground\": \"#84848A\",\n  \"statusBar.background\": \"#141415\",\n  \"statusBar.foreground\": \"#adadb1\",\n  \"statusBar.border\": \"#070707\",\n  \"statusBar.noFolderBackground\": \"#141415\",\n  \"statusBar.debuggingBackground\": \"#ffca00\",\n  \"statusBar.debuggingForeground\": \"#070707\",\n  \"statusBarItem.remoteBackground\": \"#141415\",\n  \"statusBarItem.remoteForeground\": \"#adadb1\",\n  \"input.background\": \"#1F1F21\",\n  \"input.border\": \"#424245\",\n  \"input.foreground\": \"#fbfbfb\",\n  \"input.placeholderForeground\": \"#79797F\",\n  \"dropdown.background\": \"#1F1F21\",\n  \"dropdown.border\": \"#424245\",\n  \"dropdown.foreground\": \"#fbfbfb\",\n  \"button.background\": \"#009fff\",\n  \"button.foreground\": \"#070707\",\n  \"button.hoverBackground\": \"#0190e6\",\n  \"textLink.foreground\": \"#009fff\",\n  \"textLink.activeForeground\": \"#009fff\",\n  \"gitDecoration.addedResourceForeground\": \"#00cab1\",\n  \"gitDecoration.conflictingResourceForeground\": \"#ffca00\",\n  \"gitDecoration.modifiedResourceForeground\": \"#009fff\",\n  \"gitDecoration.deletedResourceForeground\": \"#ff2e3f\",\n  \"gitDecoration.untrackedResourceForeground\": \"#00cab1\",\n  \"gitDecoration.ignoredResourceForeground\": \"#84848A\",\n  \"terminal.titleForeground\": \"#adadb1\",\n  \"terminal.titleInactiveForeground\": \"#84848A\",\n  \"terminal.background\": \"#141415\",\n  \"terminal.foreground\": \"#adadb1\",\n  \"terminal.ansiBlack\": \"#141415\",\n  \"terminal.ansiRed\": \"#ff2e3f\",\n  \"terminal.ansiGreen\": \"#0dbe4e\",\n  \"terminal.ansiYellow\": \"#ffca00\",\n  \"terminal.ansiBlue\": \"#009fff\",\n  \"terminal.ansiMagenta\": \"#c635e4\",\n  \"terminal.ansiCyan\": \"#08c0ef\",\n  \"terminal.ansiWhite\": \"#c6c6c8\",\n  \"terminal.ansiBrightBlack\": \"#141415\",\n  \"terminal.ansiBrightRed\": \"#ff2e3f\",\n  \"terminal.ansiBrightGreen\": \"#0dbe4e\",\n  \"terminal.ansiBrightYellow\": \"#ffca00\",\n  \"terminal.ansiBrightBlue\": \"#009fff\",\n  \"terminal.ansiBrightMagenta\": \"#c635e4\",\n  \"terminal.ansiBrightCyan\": \"#08c0ef\",\n  \"terminal.ansiBrightWhite\": \"#c6c6c8\",\n};\nvar tokenColors = [\n  {\n    scope: [\"comment\", \"punctuation.definition.comment\"],\n    settings: { foreground: \"#84848A\" },\n  },\n  {\n    scope: \"comment markup.link\",\n    settings: { foreground: \"#84848A\" },\n  },\n  {\n    scope: [\"string\", \"constant.other.symbol\"],\n    settings: { foreground: \"#5ecc71\" },\n  },\n  {\n    scope: [\n      \"punctuation.definition.string.begin\",\n      \"punctuation.definition.string.end\",\n    ],\n    settings: { foreground: \"#5ecc71\" },\n  },\n  {\n    scope: [\"constant.numeric\", \"constant.language.boolean\"],\n    settings: { foreground: \"#68cdf2\" },\n  },\n  {\n    scope: \"constant\",\n    settings: { foreground: \"#ffd452\" },\n  },\n  {\n    scope: \"punctuation.definition.constant\",\n    settings: { foreground: \"#ffd452\" },\n  },\n  {\n    scope: \"constant.language\",\n    settings: { foreground: \"#68cdf2\" },\n  },\n  {\n    scope: \"variable.other.constant\",\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: \"keyword\",\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: \"keyword.control\",\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: [\"storage\", \"storage.type\", \"storage.modifier\"],\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: \"token.storage\",\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: [\n      \"keyword.operator.new\",\n      \"keyword.operator.expression.instanceof\",\n      \"keyword.operator.expression.typeof\",\n      \"keyword.operator.expression.void\",\n      \"keyword.operator.expression.delete\",\n      \"keyword.operator.expression.in\",\n      \"keyword.operator.expression.of\",\n      \"keyword.operator.expression.keyof\",\n    ],\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: \"keyword.operator.delete\",\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: [\"variable\", \"identifier\", \"meta.definition.variable\"],\n    settings: { foreground: \"#ffa359\" },\n  },\n  {\n    scope: [\n      \"variable.other.readwrite\",\n      \"meta.object-literal.key\",\n      \"support.variable.property\",\n      \"support.variable.object.process\",\n      \"support.variable.object.node\",\n    ],\n    settings: { foreground: \"#ffa359\" },\n  },\n  {\n    scope: \"variable.language\",\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: \"variable.parameter.function\",\n    settings: { foreground: \"#adadb1\" },\n  },\n  {\n    scope: \"function.parameter\",\n    settings: { foreground: \"#adadb1\" },\n  },\n  {\n    scope: \"variable.parameter\",\n    settings: { foreground: \"#adadb1\" },\n  },\n  {\n    scope: \"variable.parameter.function.language.python\",\n    settings: { foreground: \"#ffd452\" },\n  },\n  {\n    scope: \"variable.parameter.function.python\",\n    settings: { foreground: \"#ffd452\" },\n  },\n  {\n    scope: [\n      \"support.function\",\n      \"entity.name.function\",\n      \"meta.function-call\",\n      \"meta.require\",\n      \"support.function.any-method\",\n      \"variable.function\",\n    ],\n    settings: { foreground: \"#9d6afb\" },\n  },\n  {\n    scope: \"keyword.other.special-method\",\n    settings: { foreground: \"#9d6afb\" },\n  },\n  {\n    scope: \"entity.name.function\",\n    settings: { foreground: \"#9d6afb\" },\n  },\n  {\n    scope: \"support.function.console\",\n    settings: { foreground: \"#9d6afb\" },\n  },\n  {\n    scope: [\n      \"support.type\",\n      \"entity.name.type\",\n      \"entity.name.class\",\n      \"storage.type\",\n    ],\n    settings: { foreground: \"#d568ea\" },\n  },\n  {\n    scope: [\"support.class\", \"entity.name.type.class\"],\n    settings: { foreground: \"#d568ea\" },\n  },\n  {\n    scope: [\n      \"entity.name.class\",\n      \"variable.other.class.js\",\n      \"variable.other.class.ts\",\n    ],\n    settings: { foreground: \"#d568ea\" },\n  },\n  {\n    scope: \"entity.name.class.identifier.namespace.type\",\n    settings: { foreground: \"#d568ea\" },\n  },\n  {\n    scope: \"entity.name.type.namespace\",\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: \"entity.other.inherited-class\",\n    settings: { foreground: \"#d568ea\" },\n  },\n  {\n    scope: \"entity.name.namespace\",\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: \"keyword.operator\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: [\n      \"keyword.operator.logical\",\n      \"keyword.operator.bitwise\",\n      \"keyword.operator.channel\",\n    ],\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: [\n      \"keyword.operator.arithmetic\",\n      \"keyword.operator.comparison\",\n      \"keyword.operator.relational\",\n      \"keyword.operator.increment\",\n      \"keyword.operator.decrement\",\n    ],\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"keyword.operator.assignment\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"keyword.operator.assignment.compound\",\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: [\n      \"keyword.operator.assignment.compound.js\",\n      \"keyword.operator.assignment.compound.ts\",\n    ],\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"keyword.operator.ternary\",\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: \"keyword.operator.optional\",\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: \"punctuation\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"punctuation.separator.delimiter\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"punctuation.separator.key-value\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"punctuation.terminator\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"meta.brace\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"meta.brace.square\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"meta.brace.round\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"function.brace\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: [\n      \"punctuation.definition.parameters\",\n      \"punctuation.definition.typeparameters\",\n    ],\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: [\"punctuation.definition.block\", \"punctuation.definition.tag\"],\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: [\"meta.tag.tsx\", \"meta.tag.jsx\", \"meta.tag.js\", \"meta.tag.ts\"],\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"keyword.operator.expression.import\",\n    settings: { foreground: \"#9d6afb\" },\n  },\n  {\n    scope: \"keyword.operator.module\",\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: \"support.type.object.console\",\n    settings: { foreground: \"#ffa359\" },\n  },\n  {\n    scope: [\n      \"support.module.node\",\n      \"support.type.object.module\",\n      \"entity.name.type.module\",\n    ],\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: \"support.constant.math\",\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: \"support.constant.property.math\",\n    settings: { foreground: \"#ffd452\" },\n  },\n  {\n    scope: \"support.constant.json\",\n    settings: { foreground: \"#ffd452\" },\n  },\n  {\n    scope: \"support.type.object.dom\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: [\"support.variable.dom\", \"support.variable.property.dom\"],\n    settings: { foreground: \"#ffa359\" },\n  },\n  {\n    scope: \"support.variable.property.process\",\n    settings: { foreground: \"#ffd452\" },\n  },\n  {\n    scope: \"meta.property.object\",\n    settings: { foreground: \"#ffa359\" },\n  },\n  {\n    scope: \"variable.parameter.function.js\",\n    settings: { foreground: \"#ffa359\" },\n  },\n  {\n    scope: [\"keyword.other.template.begin\", \"keyword.other.template.end\"],\n    settings: { foreground: \"#5ecc71\" },\n  },\n  {\n    scope: [\n      \"keyword.other.substitution.begin\",\n      \"keyword.other.substitution.end\",\n    ],\n    settings: { foreground: \"#5ecc71\" },\n  },\n  {\n    scope: [\n      \"punctuation.definition.template-expression.begin\",\n      \"punctuation.definition.template-expression.end\",\n    ],\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: \"meta.template.expression\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"punctuation.section.embedded\",\n    settings: { foreground: \"#ffa359\" },\n  },\n  {\n    scope: \"variable.interpolation\",\n    settings: { foreground: \"#ffa359\" },\n  },\n  {\n    scope: [\n      \"punctuation.section.embedded.begin\",\n      \"punctuation.section.embedded.end\",\n    ],\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: \"punctuation.quasi.element\",\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: [\n      \"support.type.primitive.ts\",\n      \"support.type.builtin.ts\",\n      \"support.type.primitive.tsx\",\n      \"support.type.builtin.tsx\",\n    ],\n    settings: { foreground: \"#d568ea\" },\n  },\n  {\n    scope: \"support.type.type.flowtype\",\n    settings: { foreground: \"#9d6afb\" },\n  },\n  {\n    scope: \"support.type.primitive\",\n    settings: { foreground: \"#d568ea\" },\n  },\n  {\n    scope: \"support.variable.magic.python\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: \"variable.parameter.function.language.special.self.python\",\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: [\n      \"punctuation.separator.period.python\",\n      \"punctuation.separator.element.python\",\n      \"punctuation.parenthesis.begin.python\",\n      \"punctuation.parenthesis.end.python\",\n    ],\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: [\n      \"punctuation.definition.arguments.begin.python\",\n      \"punctuation.definition.arguments.end.python\",\n      \"punctuation.separator.arguments.python\",\n      \"punctuation.definition.list.begin.python\",\n      \"punctuation.definition.list.end.python\",\n    ],\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"support.type.python\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"keyword.operator.logical.python\",\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: \"meta.function-call.generic.python\",\n    settings: { foreground: \"#9d6afb\" },\n  },\n  {\n    scope: \"constant.character.format.placeholder.other.python\",\n    settings: { foreground: \"#ffd452\" },\n  },\n  {\n    scope: \"meta.function.decorator.python\",\n    settings: { foreground: \"#9d6afb\" },\n  },\n  {\n    scope: [\n      \"support.token.decorator.python\",\n      \"meta.function.decorator.identifier.python\",\n    ],\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"storage.modifier.lifetime.rust\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"support.function.std.rust\",\n    settings: { foreground: \"#9d6afb\" },\n  },\n  {\n    scope: \"entity.name.lifetime.rust\",\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: \"variable.language.rust\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: \"keyword.operator.misc.rust\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"keyword.operator.sigil.rust\",\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: \"support.constant.core.rust\",\n    settings: { foreground: \"#ffd452\" },\n  },\n  {\n    scope: [\"meta.function.c\", \"meta.function.cpp\"],\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: [\n      \"punctuation.section.block.begin.bracket.curly.cpp\",\n      \"punctuation.section.block.end.bracket.curly.cpp\",\n      \"punctuation.terminator.statement.c\",\n      \"punctuation.section.block.begin.bracket.curly.c\",\n      \"punctuation.section.block.end.bracket.curly.c\",\n      \"punctuation.section.parens.begin.bracket.round.c\",\n      \"punctuation.section.parens.end.bracket.round.c\",\n      \"punctuation.section.parameters.begin.bracket.round.c\",\n      \"punctuation.section.parameters.end.bracket.round.c\",\n    ],\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: [\n      \"keyword.operator.assignment.c\",\n      \"keyword.operator.comparison.c\",\n      \"keyword.operator.c\",\n      \"keyword.operator.increment.c\",\n      \"keyword.operator.decrement.c\",\n      \"keyword.operator.bitwise.shift.c\",\n    ],\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: [\n      \"keyword.operator.assignment.cpp\",\n      \"keyword.operator.comparison.cpp\",\n      \"keyword.operator.cpp\",\n      \"keyword.operator.increment.cpp\",\n      \"keyword.operator.decrement.cpp\",\n      \"keyword.operator.bitwise.shift.cpp\",\n    ],\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: [\"punctuation.separator.c\", \"punctuation.separator.cpp\"],\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: [\"support.type.posix-reserved.c\", \"support.type.posix-reserved.cpp\"],\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: [\"keyword.operator.sizeof.c\", \"keyword.operator.sizeof.cpp\"],\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: \"variable.c\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: [\"storage.type.annotation.java\", \"storage.type.object.array.java\"],\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: \"source.java\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: [\n      \"punctuation.section.block.begin.java\",\n      \"punctuation.section.block.end.java\",\n      \"punctuation.definition.method-parameters.begin.java\",\n      \"punctuation.definition.method-parameters.end.java\",\n      \"meta.method.identifier.java\",\n      \"punctuation.section.method.begin.java\",\n      \"punctuation.section.method.end.java\",\n      \"punctuation.terminator.java\",\n      \"punctuation.section.class.begin.java\",\n      \"punctuation.section.class.end.java\",\n      \"punctuation.section.inner-class.begin.java\",\n      \"punctuation.section.inner-class.end.java\",\n      \"meta.method-call.java\",\n      \"punctuation.section.class.begin.bracket.curly.java\",\n      \"punctuation.section.class.end.bracket.curly.java\",\n      \"punctuation.section.method.begin.bracket.curly.java\",\n      \"punctuation.section.method.end.bracket.curly.java\",\n      \"punctuation.separator.period.java\",\n      \"punctuation.bracket.angle.java\",\n      \"punctuation.definition.annotation.java\",\n      \"meta.method.body.java\",\n    ],\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"meta.method.java\",\n    settings: { foreground: \"#9d6afb\" },\n  },\n  {\n    scope: [\n      \"storage.modifier.import.java\",\n      \"storage.type.java\",\n      \"storage.type.generic.java\",\n    ],\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: \"keyword.operator.instanceof.java\",\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: \"meta.definition.variable.name.java\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: \"token.variable.parameter.java\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"import.storage.java\",\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: \"token.package.keyword\",\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: \"token.package\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"token.storage.type.java\",\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: \"keyword.operator.assignment.go\",\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: [\"keyword.operator.arithmetic.go\", \"keyword.operator.address.go\"],\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: \"entity.name.package.go\",\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: [\n      \"support.other.namespace.use.php\",\n      \"support.other.namespace.use-as.php\",\n      \"support.other.namespace.php\",\n      \"entity.other.alias.php\",\n      \"meta.interface.php\",\n    ],\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: \"keyword.operator.error-control.php\",\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: \"keyword.operator.type.php\",\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: [\n      \"punctuation.section.array.begin.php\",\n      \"punctuation.section.array.end.php\",\n    ],\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: [\n      \"storage.type.php\",\n      \"meta.other.type.phpdoc.php\",\n      \"keyword.other.type.php\",\n      \"keyword.other.array.phpdoc.php\",\n    ],\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: [\n      \"meta.function-call.php\",\n      \"meta.function-call.object.php\",\n      \"meta.function-call.static.php\",\n    ],\n    settings: { foreground: \"#9d6afb\" },\n  },\n  {\n    scope: [\n      \"punctuation.definition.parameters.begin.bracket.round.php\",\n      \"punctuation.definition.parameters.end.bracket.round.php\",\n      \"punctuation.separator.delimiter.php\",\n      \"punctuation.section.scope.begin.php\",\n      \"punctuation.section.scope.end.php\",\n      \"punctuation.terminator.expression.php\",\n      \"punctuation.definition.arguments.begin.bracket.round.php\",\n      \"punctuation.definition.arguments.end.bracket.round.php\",\n      \"punctuation.definition.storage-type.begin.bracket.round.php\",\n      \"punctuation.definition.storage-type.end.bracket.round.php\",\n      \"punctuation.definition.array.begin.bracket.round.php\",\n      \"punctuation.definition.array.end.bracket.round.php\",\n      \"punctuation.definition.begin.bracket.round.php\",\n      \"punctuation.definition.end.bracket.round.php\",\n      \"punctuation.definition.begin.bracket.curly.php\",\n      \"punctuation.definition.end.bracket.curly.php\",\n      \"punctuation.definition.section.switch-block.end.bracket.curly.php\",\n      \"punctuation.definition.section.switch-block.start.bracket.curly.php\",\n      \"punctuation.definition.section.switch-block.begin.bracket.curly.php\",\n      \"punctuation.definition.section.switch-block.end.bracket.curly.php\",\n    ],\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: [\n      \"support.constant.ext.php\",\n      \"support.constant.std.php\",\n      \"support.constant.core.php\",\n      \"support.constant.parser-token.php\",\n    ],\n    settings: { foreground: \"#ffd452\" },\n  },\n  {\n    scope: [\"entity.name.goto-label.php\", \"support.other.php\"],\n    settings: { foreground: \"#9d6afb\" },\n  },\n  {\n    scope: [\n      \"keyword.operator.logical.php\",\n      \"keyword.operator.bitwise.php\",\n      \"keyword.operator.arithmetic.php\",\n    ],\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"keyword.operator.regexp.php\",\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: \"keyword.operator.comparison.php\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: [\"keyword.operator.heredoc.php\", \"keyword.operator.nowdoc.php\"],\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: \"variable.other.class.php\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: \"invalid.illegal.non-null-typehinted.php\",\n    settings: { foreground: \"#f44747\" },\n  },\n  {\n    scope: \"variable.other.generic-type.haskell\",\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: \"storage.type.haskell\",\n    settings: { foreground: \"#ffd452\" },\n  },\n  {\n    scope: \"storage.type.cs\",\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: \"entity.name.variable.local.cs\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: \"entity.name.label.cs\",\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: [\n      \"entity.name.scope-resolution.function.call\",\n      \"entity.name.scope-resolution.function.definition\",\n    ],\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: [\n      \"punctuation.definition.delayed.unison\",\n      \"punctuation.definition.list.begin.unison\",\n      \"punctuation.definition.list.end.unison\",\n      \"punctuation.definition.ability.begin.unison\",\n      \"punctuation.definition.ability.end.unison\",\n      \"punctuation.operator.assignment.as.unison\",\n      \"punctuation.separator.pipe.unison\",\n      \"punctuation.separator.delimiter.unison\",\n      \"punctuation.definition.hash.unison\",\n    ],\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: \"support.constant.edge\",\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: \"support.type.prelude.elm\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"support.constant.elm\",\n    settings: { foreground: \"#ffd452\" },\n  },\n  {\n    scope: \"entity.global.clojure\",\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: \"meta.symbol.clojure\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: \"constant.keyword.clojure\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: [\"meta.arguments.coffee\", \"variable.parameter.function.coffee\"],\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: \"storage.modifier.import.groovy\",\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: \"meta.method.groovy\",\n    settings: { foreground: \"#9d6afb\" },\n  },\n  {\n    scope: \"meta.definition.variable.name.groovy\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: \"meta.definition.class.inherited.classes.groovy\",\n    settings: { foreground: \"#5ecc71\" },\n  },\n  {\n    scope: \"support.variable.semantic.hlsl\",\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: [\n      \"support.type.texture.hlsl\",\n      \"support.type.sampler.hlsl\",\n      \"support.type.object.hlsl\",\n      \"support.type.object.rw.hlsl\",\n      \"support.type.fx.hlsl\",\n      \"support.type.object.hlsl\",\n    ],\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: [\"text.variable\", \"text.bracketed\"],\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: [\"support.type.swift\", \"support.type.vb.asp\"],\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: \"meta.scope.prerequisites.makefile\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: \"source.makefile\",\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: \"source.ini\",\n    settings: { foreground: \"#5ecc71\" },\n  },\n  {\n    scope: \"constant.language.symbol.ruby\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: [\"function.parameter.ruby\", \"function.parameter.cs\"],\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"constant.language.symbol.elixir\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope:\n      \"text.html.laravel-blade source.php.embedded.line.html entity.name.tag.laravel-blade\",\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope:\n      \"text.html.laravel-blade source.php.embedded.line.html support.constant.laravel-blade\",\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: \"entity.name.function.xi\",\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: \"entity.name.class.xi\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"constant.character.character-class.regexp.xi\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: \"constant.regexp.xi\",\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: \"keyword.control.xi\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"invalid.xi\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"beginning.punctuation.definition.quote.markdown.xi\",\n    settings: { foreground: \"#5ecc71\" },\n  },\n  {\n    scope: \"beginning.punctuation.definition.list.markdown.xi\",\n    settings: { foreground: \"#84848A\" },\n  },\n  {\n    scope: \"constant.character.xi\",\n    settings: { foreground: \"#9d6afb\" },\n  },\n  {\n    scope: \"accent.xi\",\n    settings: { foreground: \"#9d6afb\" },\n  },\n  {\n    scope: \"wikiword.xi\",\n    settings: { foreground: \"#ffd452\" },\n  },\n  {\n    scope: \"constant.other.color.rgb-value.xi\",\n    settings: { foreground: \"#ffffff\" },\n  },\n  {\n    scope: \"punctuation.definition.tag.xi\",\n    settings: { foreground: \"#84848A\" },\n  },\n  {\n    scope: [\n      \"support.constant.property-value.scss\",\n      \"support.constant.property-value.css\",\n    ],\n    settings: { foreground: \"#ffd452\" },\n  },\n  {\n    scope: [\n      \"keyword.operator.css\",\n      \"keyword.operator.scss\",\n      \"keyword.operator.less\",\n    ],\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: [\n      \"support.constant.color.w3c-standard-color-name.css\",\n      \"support.constant.color.w3c-standard-color-name.scss\",\n    ],\n    settings: { foreground: \"#ffd452\" },\n  },\n  {\n    scope: \"punctuation.separator.list.comma.css\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"support.type.vendored.property-name.css\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"support.type.property-name.css\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"support.type.property-name\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"support.constant.property-value\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"support.constant.font-name\",\n    settings: { foreground: \"#ffd452\" },\n  },\n  {\n    scope: \"entity.other.attribute-name.class.css\",\n    settings: {\n      foreground: \"#61d5c0\",\n      fontStyle: \"normal\",\n    },\n  },\n  {\n    scope: \"entity.other.attribute-name.id\",\n    settings: {\n      foreground: \"#9d6afb\",\n      fontStyle: \"normal\",\n    },\n  },\n  {\n    scope: [\n      \"entity.other.attribute-name.pseudo-element\",\n      \"entity.other.attribute-name.pseudo-class\",\n    ],\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"meta.selector\",\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: \"selector.sass\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: \"rgb-value\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"inline-color-decoration rgb-value\",\n    settings: { foreground: \"#ffd452\" },\n  },\n  {\n    scope: \"less rgb-value\",\n    settings: { foreground: \"#ffd452\" },\n  },\n  {\n    scope: \"control.elements\",\n    settings: { foreground: \"#ffd452\" },\n  },\n  {\n    scope: \"keyword.operator.less\",\n    settings: { foreground: \"#ffd452\" },\n  },\n  {\n    scope: \"entity.name.tag\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: \"entity.other.attribute-name\",\n    settings: {\n      foreground: \"#61d5c0\",\n      fontStyle: \"normal\",\n    },\n  },\n  {\n    scope: \"constant.character.entity\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: \"meta.tag\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"invalid.illegal.bad-ampersand.html\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"markup.heading\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: [\n      \"markup.heading punctuation.definition.heading\",\n      \"entity.name.section\",\n    ],\n    settings: { foreground: \"#9d6afb\" },\n  },\n  {\n    scope: \"entity.name.section.markdown\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: \"punctuation.definition.heading.markdown\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: \"markup.heading.setext\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: [\n      \"markup.heading.setext.1.markdown\",\n      \"markup.heading.setext.2.markdown\",\n    ],\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: [\"markup.bold\", \"todo.bold\"],\n    settings: { foreground: \"#ffd452\" },\n  },\n  {\n    scope: \"punctuation.definition.bold\",\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: \"punctuation.definition.bold.markdown\",\n    settings: { foreground: \"#ffd452\" },\n  },\n  {\n    scope: [\"markup.italic\", \"punctuation.definition.italic\", \"todo.emphasis\"],\n    settings: {\n      foreground: \"#ff678d\",\n      fontStyle: \"italic\",\n    },\n  },\n  {\n    scope: \"emphasis md\",\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: \"markup.italic.markdown\",\n    settings: { fontStyle: \"italic\" },\n  },\n  {\n    scope: [\n      \"markup.underline.link.markdown\",\n      \"markup.underline.link.image.markdown\",\n    ],\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: [\n      \"string.other.link.title.markdown\",\n      \"string.other.link.description.markdown\",\n    ],\n    settings: { foreground: \"#9d6afb\" },\n  },\n  {\n    scope: \"punctuation.definition.metadata.markdown\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: [\"markup.inline.raw.markdown\", \"markup.inline.raw.string.markdown\"],\n    settings: { foreground: \"#5ecc71\" },\n  },\n  {\n    scope: \"punctuation.definition.list.begin.markdown\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: \"punctuation.definition.list.markdown\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: \"beginning.punctuation.definition.list.markdown\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: [\n      \"punctuation.definition.string.begin.markdown\",\n      \"punctuation.definition.string.end.markdown\",\n    ],\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: \"markup.quote.markdown\",\n    settings: { foreground: \"#84848A\" },\n  },\n  {\n    scope: \"keyword.other.unit\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: \"markup.changed.diff\",\n    settings: { foreground: \"#ffca00\" },\n  },\n  {\n    scope: [\n      \"meta.diff.header.from-file\",\n      \"meta.diff.header.to-file\",\n      \"punctuation.definition.from-file.diff\",\n      \"punctuation.definition.to-file.diff\",\n    ],\n    settings: { foreground: \"#9d6afb\" },\n  },\n  {\n    scope: \"markup.inserted.diff\",\n    settings: { foreground: \"#5ecc71\" },\n  },\n  {\n    scope: \"markup.deleted.diff\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: \"string.regexp\",\n    settings: { foreground: \"#64d1db\" },\n  },\n  {\n    scope: \"constant.other.character-class.regexp\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: \"keyword.operator.quantifier.regexp\",\n    settings: { foreground: \"#ffd452\" },\n  },\n  {\n    scope: \"constant.character.escape\",\n    settings: { foreground: \"#68cdf2\" },\n  },\n  {\n    scope: \"source.json meta.structure.dictionary.json > string.quoted.json\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope:\n      \"source.json meta.structure.dictionary.json > string.quoted.json > punctuation.string\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: [\n      \"source.json meta.structure.dictionary.json > value.json > string.quoted.json\",\n      \"source.json meta.structure.array.json > value.json > string.quoted.json\",\n      \"source.json meta.structure.dictionary.json > value.json > string.quoted.json > punctuation\",\n      \"source.json meta.structure.array.json > value.json > string.quoted.json > punctuation\",\n    ],\n    settings: { foreground: \"#5ecc71\" },\n  },\n  {\n    scope: [\n      \"source.json meta.structure.dictionary.json > constant.language.json\",\n      \"source.json meta.structure.array.json > constant.language.json\",\n    ],\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"support.type.property-name.json\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: \"support.type.property-name.json punctuation\",\n    settings: { foreground: \"#ff6762\" },\n  },\n  {\n    scope: \"punctuation.definition.block.sequence.item.yaml\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"block.scope.end\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"block.scope.begin\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"token.info-token\",\n    settings: { foreground: \"#9d6afb\" },\n  },\n  {\n    scope: \"token.warn-token\",\n    settings: { foreground: \"#ffd452\" },\n  },\n  {\n    scope: \"token.error-token\",\n    settings: { foreground: \"#f44747\" },\n  },\n  {\n    scope: \"token.debug-token\",\n    settings: { foreground: \"#ff678d\" },\n  },\n  {\n    scope: \"invalid.illegal\",\n    settings: { foreground: \"#ffffff\" },\n  },\n  {\n    scope: \"invalid.broken\",\n    settings: { foreground: \"#ffffff\" },\n  },\n  {\n    scope: \"invalid.deprecated\",\n    settings: { foreground: \"#ffffff\" },\n  },\n  {\n    scope: \"invalid.unimplemented\",\n    settings: { foreground: \"#ffffff\" },\n  },\n];\nvar semanticTokenColors = {\n  comment: \"#84848A\",\n  string: \"#5ecc71\",\n  number: \"#68cdf2\",\n  regexp: \"#64d1db\",\n  keyword: \"#ff678d\",\n  variable: \"#ffa359\",\n  parameter: \"#adadb1\",\n  property: \"#ffa359\",\n  function: \"#9d6afb\",\n  method: \"#9d6afb\",\n  type: \"#d568ea\",\n  class: \"#d568ea\",\n  namespace: \"#ffca00\",\n  enumMember: \"#08c0ef\",\n  \"variable.constant\": \"#ffd452\",\n  \"variable.defaultLibrary\": \"#ffca00\",\n};\nvar pierre_dark_default = {\n  name,\n  type,\n  colors,\n  tokenColors,\n  semanticTokenColors,\n};\n\n//#endregion\nexport {\n  colors,\n  pierre_dark_default as default,\n  name,\n  semanticTokenColors,\n  tokenColors,\n  type,\n};\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/pierre-light-theme.js",
    "content": "//#region src/themes/pierre-light.json\nvar name = \"pierre-light\";\nvar type = \"light\";\nvar colors = {\n  \"editor.background\": \"#ffffff\",\n  \"editor.foreground\": \"#070707\",\n  foreground: \"#070707\",\n  focusBorder: \"#009fff\",\n  \"selection.background\": \"#dfebff\",\n  \"editor.selectionBackground\": \"#009fff2e\",\n  \"editor.lineHighlightBackground\": \"#dfebff8c\",\n  \"editorCursor.foreground\": \"#009fff\",\n  \"editorLineNumber.foreground\": \"#84848A\",\n  \"editorLineNumber.activeForeground\": \"#6C6C71\",\n  \"editorIndentGuide.background\": \"#eeeeef\",\n  \"editorIndentGuide.activeBackground\": \"#dbdbdd\",\n  \"diffEditor.insertedTextBackground\": \"#00cab133\",\n  \"diffEditor.deletedTextBackground\": \"#ff2e3f33\",\n  \"sideBar.background\": \"#f8f8f8\",\n  \"sideBar.foreground\": \"#6C6C71\",\n  \"sideBar.border\": \"#eeeeef\",\n  \"sideBarTitle.foreground\": \"#070707\",\n  \"sideBarSectionHeader.background\": \"#f8f8f8\",\n  \"sideBarSectionHeader.foreground\": \"#6C6C71\",\n  \"sideBarSectionHeader.border\": \"#eeeeef\",\n  \"activityBar.background\": \"#f8f8f8\",\n  \"activityBar.foreground\": \"#070707\",\n  \"activityBar.border\": \"#eeeeef\",\n  \"activityBar.activeBorder\": \"#009fff\",\n  \"activityBarBadge.background\": \"#009fff\",\n  \"activityBarBadge.foreground\": \"#ffffff\",\n  \"titleBar.activeBackground\": \"#f8f8f8\",\n  \"titleBar.activeForeground\": \"#070707\",\n  \"titleBar.inactiveBackground\": \"#f8f8f8\",\n  \"titleBar.inactiveForeground\": \"#84848A\",\n  \"titleBar.border\": \"#eeeeef\",\n  \"list.activeSelectionBackground\": \"#dfebffcc\",\n  \"list.activeSelectionForeground\": \"#070707\",\n  \"list.inactiveSelectionBackground\": \"#dfebff73\",\n  \"list.hoverBackground\": \"#dfebff59\",\n  \"list.focusOutline\": \"#009fff\",\n  \"tab.activeBackground\": \"#ffffff\",\n  \"tab.activeForeground\": \"#070707\",\n  \"tab.activeBorderTop\": \"#009fff\",\n  \"tab.inactiveBackground\": \"#f8f8f8\",\n  \"tab.inactiveForeground\": \"#84848A\",\n  \"tab.border\": \"#eeeeef\",\n  \"editorGroupHeader.tabsBackground\": \"#f8f8f8\",\n  \"editorGroupHeader.tabsBorder\": \"#eeeeef\",\n  \"panel.background\": \"#f8f8f8\",\n  \"panel.border\": \"#eeeeef\",\n  \"panelTitle.activeBorder\": \"#009fff\",\n  \"panelTitle.activeForeground\": \"#070707\",\n  \"panelTitle.inactiveForeground\": \"#84848A\",\n  \"statusBar.background\": \"#f8f8f8\",\n  \"statusBar.foreground\": \"#6C6C71\",\n  \"statusBar.border\": \"#eeeeef\",\n  \"statusBar.noFolderBackground\": \"#f8f8f8\",\n  \"statusBar.debuggingBackground\": \"#ffca00\",\n  \"statusBar.debuggingForeground\": \"#ffffff\",\n  \"statusBarItem.remoteBackground\": \"#f8f8f8\",\n  \"statusBarItem.remoteForeground\": \"#6C6C71\",\n  \"input.background\": \"#f2f2f3\",\n  \"input.border\": \"#dbdbdd\",\n  \"input.foreground\": \"#070707\",\n  \"input.placeholderForeground\": \"#8E8E95\",\n  \"dropdown.background\": \"#f2f2f3\",\n  \"dropdown.border\": \"#dbdbdd\",\n  \"dropdown.foreground\": \"#070707\",\n  \"button.background\": \"#009fff\",\n  \"button.foreground\": \"#ffffff\",\n  \"button.hoverBackground\": \"#1aa9ff\",\n  \"textLink.foreground\": \"#009fff\",\n  \"textLink.activeForeground\": \"#009fff\",\n  \"gitDecoration.addedResourceForeground\": \"#00cab1\",\n  \"gitDecoration.conflictingResourceForeground\": \"#ffca00\",\n  \"gitDecoration.modifiedResourceForeground\": \"#009fff\",\n  \"gitDecoration.deletedResourceForeground\": \"#ff2e3f\",\n  \"gitDecoration.untrackedResourceForeground\": \"#00cab1\",\n  \"gitDecoration.ignoredResourceForeground\": \"#84848A\",\n  \"terminal.titleForeground\": \"#6C6C71\",\n  \"terminal.titleInactiveForeground\": \"#84848A\",\n  \"terminal.background\": \"#f8f8f8\",\n  \"terminal.foreground\": \"#6C6C71\",\n  \"terminal.ansiBlack\": \"#1F1F21\",\n  \"terminal.ansiRed\": \"#ff2e3f\",\n  \"terminal.ansiGreen\": \"#0dbe4e\",\n  \"terminal.ansiYellow\": \"#ffca00\",\n  \"terminal.ansiBlue\": \"#009fff\",\n  \"terminal.ansiMagenta\": \"#c635e4\",\n  \"terminal.ansiCyan\": \"#08c0ef\",\n  \"terminal.ansiWhite\": \"#c6c6c8\",\n  \"terminal.ansiBrightBlack\": \"#1F1F21\",\n  \"terminal.ansiBrightRed\": \"#ff2e3f\",\n  \"terminal.ansiBrightGreen\": \"#0dbe4e\",\n  \"terminal.ansiBrightYellow\": \"#ffca00\",\n  \"terminal.ansiBrightBlue\": \"#009fff\",\n  \"terminal.ansiBrightMagenta\": \"#c635e4\",\n  \"terminal.ansiBrightCyan\": \"#08c0ef\",\n  \"terminal.ansiBrightWhite\": \"#c6c6c8\",\n};\nvar tokenColors = [\n  {\n    scope: [\"comment\", \"punctuation.definition.comment\"],\n    settings: { foreground: \"#84848A\" },\n  },\n  {\n    scope: \"comment markup.link\",\n    settings: { foreground: \"#84848A\" },\n  },\n  {\n    scope: [\"string\", \"constant.other.symbol\"],\n    settings: { foreground: \"#199f43\" },\n  },\n  {\n    scope: [\n      \"punctuation.definition.string.begin\",\n      \"punctuation.definition.string.end\",\n    ],\n    settings: { foreground: \"#199f43\" },\n  },\n  {\n    scope: [\"constant.numeric\", \"constant.language.boolean\"],\n    settings: { foreground: \"#1ca1c7\" },\n  },\n  {\n    scope: \"constant\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"punctuation.definition.constant\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"constant.language\",\n    settings: { foreground: \"#1ca1c7\" },\n  },\n  {\n    scope: \"variable.other.constant\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"keyword\",\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: \"keyword.control\",\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: [\"storage\", \"storage.type\", \"storage.modifier\"],\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: \"token.storage\",\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: [\n      \"keyword.operator.new\",\n      \"keyword.operator.expression.instanceof\",\n      \"keyword.operator.expression.typeof\",\n      \"keyword.operator.expression.void\",\n      \"keyword.operator.expression.delete\",\n      \"keyword.operator.expression.in\",\n      \"keyword.operator.expression.of\",\n      \"keyword.operator.expression.keyof\",\n    ],\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: \"keyword.operator.delete\",\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: [\"variable\", \"identifier\", \"meta.definition.variable\"],\n    settings: { foreground: \"#d47628\" },\n  },\n  {\n    scope: [\n      \"variable.other.readwrite\",\n      \"meta.object-literal.key\",\n      \"support.variable.property\",\n      \"support.variable.object.process\",\n      \"support.variable.object.node\",\n    ],\n    settings: { foreground: \"#d47628\" },\n  },\n  {\n    scope: \"variable.language\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"variable.parameter.function\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"function.parameter\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"variable.parameter\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"variable.parameter.function.language.python\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"variable.parameter.function.python\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: [\n      \"support.function\",\n      \"entity.name.function\",\n      \"meta.function-call\",\n      \"meta.require\",\n      \"support.function.any-method\",\n      \"variable.function\",\n    ],\n    settings: { foreground: \"#7b43f8\" },\n  },\n  {\n    scope: \"keyword.other.special-method\",\n    settings: { foreground: \"#7b43f8\" },\n  },\n  {\n    scope: \"entity.name.function\",\n    settings: { foreground: \"#7b43f8\" },\n  },\n  {\n    scope: \"support.function.console\",\n    settings: { foreground: \"#7b43f8\" },\n  },\n  {\n    scope: [\n      \"support.type\",\n      \"entity.name.type\",\n      \"entity.name.class\",\n      \"storage.type\",\n    ],\n    settings: { foreground: \"#c635e4\" },\n  },\n  {\n    scope: [\"support.class\", \"entity.name.type.class\"],\n    settings: { foreground: \"#c635e4\" },\n  },\n  {\n    scope: [\n      \"entity.name.class\",\n      \"variable.other.class.js\",\n      \"variable.other.class.ts\",\n    ],\n    settings: { foreground: \"#c635e4\" },\n  },\n  {\n    scope: \"entity.name.class.identifier.namespace.type\",\n    settings: { foreground: \"#c635e4\" },\n  },\n  {\n    scope: \"entity.name.type.namespace\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"entity.other.inherited-class\",\n    settings: { foreground: \"#c635e4\" },\n  },\n  {\n    scope: \"entity.name.namespace\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"keyword.operator\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: [\n      \"keyword.operator.logical\",\n      \"keyword.operator.bitwise\",\n      \"keyword.operator.channel\",\n    ],\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: [\n      \"keyword.operator.arithmetic\",\n      \"keyword.operator.comparison\",\n      \"keyword.operator.relational\",\n      \"keyword.operator.increment\",\n      \"keyword.operator.decrement\",\n    ],\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"keyword.operator.assignment\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"keyword.operator.assignment.compound\",\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: [\n      \"keyword.operator.assignment.compound.js\",\n      \"keyword.operator.assignment.compound.ts\",\n    ],\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"keyword.operator.ternary\",\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: \"keyword.operator.optional\",\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: \"punctuation\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"punctuation.separator.delimiter\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"punctuation.separator.key-value\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"punctuation.terminator\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"meta.brace\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"meta.brace.square\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"meta.brace.round\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"function.brace\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: [\n      \"punctuation.definition.parameters\",\n      \"punctuation.definition.typeparameters\",\n    ],\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: [\"punctuation.definition.block\", \"punctuation.definition.tag\"],\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: [\"meta.tag.tsx\", \"meta.tag.jsx\", \"meta.tag.js\", \"meta.tag.ts\"],\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"keyword.operator.expression.import\",\n    settings: { foreground: \"#7b43f8\" },\n  },\n  {\n    scope: \"keyword.operator.module\",\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: \"support.type.object.console\",\n    settings: { foreground: \"#d47628\" },\n  },\n  {\n    scope: [\n      \"support.module.node\",\n      \"support.type.object.module\",\n      \"entity.name.type.module\",\n    ],\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"support.constant.math\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"support.constant.property.math\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"support.constant.json\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"support.type.object.dom\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: [\"support.variable.dom\", \"support.variable.property.dom\"],\n    settings: { foreground: \"#d47628\" },\n  },\n  {\n    scope: \"support.variable.property.process\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"meta.property.object\",\n    settings: { foreground: \"#d47628\" },\n  },\n  {\n    scope: \"variable.parameter.function.js\",\n    settings: { foreground: \"#d47628\" },\n  },\n  {\n    scope: [\"keyword.other.template.begin\", \"keyword.other.template.end\"],\n    settings: { foreground: \"#199f43\" },\n  },\n  {\n    scope: [\n      \"keyword.other.substitution.begin\",\n      \"keyword.other.substitution.end\",\n    ],\n    settings: { foreground: \"#199f43\" },\n  },\n  {\n    scope: [\n      \"punctuation.definition.template-expression.begin\",\n      \"punctuation.definition.template-expression.end\",\n    ],\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: \"meta.template.expression\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"punctuation.section.embedded\",\n    settings: { foreground: \"#d47628\" },\n  },\n  {\n    scope: \"variable.interpolation\",\n    settings: { foreground: \"#d47628\" },\n  },\n  {\n    scope: [\n      \"punctuation.section.embedded.begin\",\n      \"punctuation.section.embedded.end\",\n    ],\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: \"punctuation.quasi.element\",\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: [\n      \"support.type.primitive.ts\",\n      \"support.type.builtin.ts\",\n      \"support.type.primitive.tsx\",\n      \"support.type.builtin.tsx\",\n    ],\n    settings: { foreground: \"#c635e4\" },\n  },\n  {\n    scope: \"support.type.type.flowtype\",\n    settings: { foreground: \"#7b43f8\" },\n  },\n  {\n    scope: \"support.type.primitive\",\n    settings: { foreground: \"#c635e4\" },\n  },\n  {\n    scope: \"support.variable.magic.python\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: \"variable.parameter.function.language.special.self.python\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: [\n      \"punctuation.separator.period.python\",\n      \"punctuation.separator.element.python\",\n      \"punctuation.parenthesis.begin.python\",\n      \"punctuation.parenthesis.end.python\",\n    ],\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: [\n      \"punctuation.definition.arguments.begin.python\",\n      \"punctuation.definition.arguments.end.python\",\n      \"punctuation.separator.arguments.python\",\n      \"punctuation.definition.list.begin.python\",\n      \"punctuation.definition.list.end.python\",\n    ],\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"support.type.python\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"keyword.operator.logical.python\",\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: \"meta.function-call.generic.python\",\n    settings: { foreground: \"#7b43f8\" },\n  },\n  {\n    scope: \"constant.character.format.placeholder.other.python\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"meta.function.decorator.python\",\n    settings: { foreground: \"#7b43f8\" },\n  },\n  {\n    scope: [\n      \"support.token.decorator.python\",\n      \"meta.function.decorator.identifier.python\",\n    ],\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"storage.modifier.lifetime.rust\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"support.function.std.rust\",\n    settings: { foreground: \"#7b43f8\" },\n  },\n  {\n    scope: \"entity.name.lifetime.rust\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"variable.language.rust\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: \"keyword.operator.misc.rust\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"keyword.operator.sigil.rust\",\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: \"support.constant.core.rust\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: [\"meta.function.c\", \"meta.function.cpp\"],\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: [\n      \"punctuation.section.block.begin.bracket.curly.cpp\",\n      \"punctuation.section.block.end.bracket.curly.cpp\",\n      \"punctuation.terminator.statement.c\",\n      \"punctuation.section.block.begin.bracket.curly.c\",\n      \"punctuation.section.block.end.bracket.curly.c\",\n      \"punctuation.section.parens.begin.bracket.round.c\",\n      \"punctuation.section.parens.end.bracket.round.c\",\n      \"punctuation.section.parameters.begin.bracket.round.c\",\n      \"punctuation.section.parameters.end.bracket.round.c\",\n    ],\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: [\n      \"keyword.operator.assignment.c\",\n      \"keyword.operator.comparison.c\",\n      \"keyword.operator.c\",\n      \"keyword.operator.increment.c\",\n      \"keyword.operator.decrement.c\",\n      \"keyword.operator.bitwise.shift.c\",\n    ],\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: [\n      \"keyword.operator.assignment.cpp\",\n      \"keyword.operator.comparison.cpp\",\n      \"keyword.operator.cpp\",\n      \"keyword.operator.increment.cpp\",\n      \"keyword.operator.decrement.cpp\",\n      \"keyword.operator.bitwise.shift.cpp\",\n    ],\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: [\"punctuation.separator.c\", \"punctuation.separator.cpp\"],\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: [\"support.type.posix-reserved.c\", \"support.type.posix-reserved.cpp\"],\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: [\"keyword.operator.sizeof.c\", \"keyword.operator.sizeof.cpp\"],\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: \"variable.c\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: [\"storage.type.annotation.java\", \"storage.type.object.array.java\"],\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"source.java\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: [\n      \"punctuation.section.block.begin.java\",\n      \"punctuation.section.block.end.java\",\n      \"punctuation.definition.method-parameters.begin.java\",\n      \"punctuation.definition.method-parameters.end.java\",\n      \"meta.method.identifier.java\",\n      \"punctuation.section.method.begin.java\",\n      \"punctuation.section.method.end.java\",\n      \"punctuation.terminator.java\",\n      \"punctuation.section.class.begin.java\",\n      \"punctuation.section.class.end.java\",\n      \"punctuation.section.inner-class.begin.java\",\n      \"punctuation.section.inner-class.end.java\",\n      \"meta.method-call.java\",\n      \"punctuation.section.class.begin.bracket.curly.java\",\n      \"punctuation.section.class.end.bracket.curly.java\",\n      \"punctuation.section.method.begin.bracket.curly.java\",\n      \"punctuation.section.method.end.bracket.curly.java\",\n      \"punctuation.separator.period.java\",\n      \"punctuation.bracket.angle.java\",\n      \"punctuation.definition.annotation.java\",\n      \"meta.method.body.java\",\n    ],\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"meta.method.java\",\n    settings: { foreground: \"#7b43f8\" },\n  },\n  {\n    scope: [\n      \"storage.modifier.import.java\",\n      \"storage.type.java\",\n      \"storage.type.generic.java\",\n    ],\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"keyword.operator.instanceof.java\",\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: \"meta.definition.variable.name.java\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: \"token.variable.parameter.java\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"import.storage.java\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"token.package.keyword\",\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: \"token.package\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"token.storage.type.java\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"keyword.operator.assignment.go\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: [\"keyword.operator.arithmetic.go\", \"keyword.operator.address.go\"],\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: \"entity.name.package.go\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: [\n      \"support.other.namespace.use.php\",\n      \"support.other.namespace.use-as.php\",\n      \"support.other.namespace.php\",\n      \"entity.other.alias.php\",\n      \"meta.interface.php\",\n    ],\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"keyword.operator.error-control.php\",\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: \"keyword.operator.type.php\",\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: [\n      \"punctuation.section.array.begin.php\",\n      \"punctuation.section.array.end.php\",\n    ],\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: [\n      \"storage.type.php\",\n      \"meta.other.type.phpdoc.php\",\n      \"keyword.other.type.php\",\n      \"keyword.other.array.phpdoc.php\",\n    ],\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: [\n      \"meta.function-call.php\",\n      \"meta.function-call.object.php\",\n      \"meta.function-call.static.php\",\n    ],\n    settings: { foreground: \"#7b43f8\" },\n  },\n  {\n    scope: [\n      \"punctuation.definition.parameters.begin.bracket.round.php\",\n      \"punctuation.definition.parameters.end.bracket.round.php\",\n      \"punctuation.separator.delimiter.php\",\n      \"punctuation.section.scope.begin.php\",\n      \"punctuation.section.scope.end.php\",\n      \"punctuation.terminator.expression.php\",\n      \"punctuation.definition.arguments.begin.bracket.round.php\",\n      \"punctuation.definition.arguments.end.bracket.round.php\",\n      \"punctuation.definition.storage-type.begin.bracket.round.php\",\n      \"punctuation.definition.storage-type.end.bracket.round.php\",\n      \"punctuation.definition.array.begin.bracket.round.php\",\n      \"punctuation.definition.array.end.bracket.round.php\",\n      \"punctuation.definition.begin.bracket.round.php\",\n      \"punctuation.definition.end.bracket.round.php\",\n      \"punctuation.definition.begin.bracket.curly.php\",\n      \"punctuation.definition.end.bracket.curly.php\",\n      \"punctuation.definition.section.switch-block.end.bracket.curly.php\",\n      \"punctuation.definition.section.switch-block.start.bracket.curly.php\",\n      \"punctuation.definition.section.switch-block.begin.bracket.curly.php\",\n      \"punctuation.definition.section.switch-block.end.bracket.curly.php\",\n    ],\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: [\n      \"support.constant.ext.php\",\n      \"support.constant.std.php\",\n      \"support.constant.core.php\",\n      \"support.constant.parser-token.php\",\n    ],\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: [\"entity.name.goto-label.php\", \"support.other.php\"],\n    settings: { foreground: \"#7b43f8\" },\n  },\n  {\n    scope: [\n      \"keyword.operator.logical.php\",\n      \"keyword.operator.bitwise.php\",\n      \"keyword.operator.arithmetic.php\",\n    ],\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"keyword.operator.regexp.php\",\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: \"keyword.operator.comparison.php\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: [\"keyword.operator.heredoc.php\", \"keyword.operator.nowdoc.php\"],\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: \"variable.other.class.php\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: \"invalid.illegal.non-null-typehinted.php\",\n    settings: { foreground: \"#f44747\" },\n  },\n  {\n    scope: \"variable.other.generic-type.haskell\",\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: \"storage.type.haskell\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"storage.type.cs\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"entity.name.variable.local.cs\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: \"entity.name.label.cs\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: [\n      \"entity.name.scope-resolution.function.call\",\n      \"entity.name.scope-resolution.function.definition\",\n    ],\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: [\n      \"punctuation.definition.delayed.unison\",\n      \"punctuation.definition.list.begin.unison\",\n      \"punctuation.definition.list.end.unison\",\n      \"punctuation.definition.ability.begin.unison\",\n      \"punctuation.definition.ability.end.unison\",\n      \"punctuation.operator.assignment.as.unison\",\n      \"punctuation.separator.pipe.unison\",\n      \"punctuation.separator.delimiter.unison\",\n      \"punctuation.definition.hash.unison\",\n    ],\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: \"support.constant.edge\",\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: \"support.type.prelude.elm\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"support.constant.elm\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"entity.global.clojure\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"meta.symbol.clojure\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: \"constant.keyword.clojure\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: [\"meta.arguments.coffee\", \"variable.parameter.function.coffee\"],\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: \"storage.modifier.import.groovy\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"meta.method.groovy\",\n    settings: { foreground: \"#7b43f8\" },\n  },\n  {\n    scope: \"meta.definition.variable.name.groovy\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: \"meta.definition.class.inherited.classes.groovy\",\n    settings: { foreground: \"#199f43\" },\n  },\n  {\n    scope: \"support.variable.semantic.hlsl\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: [\n      \"support.type.texture.hlsl\",\n      \"support.type.sampler.hlsl\",\n      \"support.type.object.hlsl\",\n      \"support.type.object.rw.hlsl\",\n      \"support.type.fx.hlsl\",\n      \"support.type.object.hlsl\",\n    ],\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: [\"text.variable\", \"text.bracketed\"],\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: [\"support.type.swift\", \"support.type.vb.asp\"],\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"meta.scope.prerequisites.makefile\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: \"source.makefile\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"source.ini\",\n    settings: { foreground: \"#199f43\" },\n  },\n  {\n    scope: \"constant.language.symbol.ruby\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: [\"function.parameter.ruby\", \"function.parameter.cs\"],\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"constant.language.symbol.elixir\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope:\n      \"text.html.laravel-blade source.php.embedded.line.html entity.name.tag.laravel-blade\",\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope:\n      \"text.html.laravel-blade source.php.embedded.line.html support.constant.laravel-blade\",\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: \"entity.name.function.xi\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"entity.name.class.xi\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"constant.character.character-class.regexp.xi\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: \"constant.regexp.xi\",\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: \"keyword.control.xi\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"invalid.xi\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"beginning.punctuation.definition.quote.markdown.xi\",\n    settings: { foreground: \"#199f43\" },\n  },\n  {\n    scope: \"beginning.punctuation.definition.list.markdown.xi\",\n    settings: { foreground: \"#84848A\" },\n  },\n  {\n    scope: \"constant.character.xi\",\n    settings: { foreground: \"#7b43f8\" },\n  },\n  {\n    scope: \"accent.xi\",\n    settings: { foreground: \"#7b43f8\" },\n  },\n  {\n    scope: \"wikiword.xi\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"constant.other.color.rgb-value.xi\",\n    settings: { foreground: \"#ffffff\" },\n  },\n  {\n    scope: \"punctuation.definition.tag.xi\",\n    settings: { foreground: \"#84848A\" },\n  },\n  {\n    scope: [\n      \"support.constant.property-value.scss\",\n      \"support.constant.property-value.css\",\n    ],\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: [\n      \"keyword.operator.css\",\n      \"keyword.operator.scss\",\n      \"keyword.operator.less\",\n    ],\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: [\n      \"support.constant.color.w3c-standard-color-name.css\",\n      \"support.constant.color.w3c-standard-color-name.scss\",\n    ],\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"punctuation.separator.list.comma.css\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"support.type.vendored.property-name.css\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"support.type.property-name.css\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"support.type.property-name\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"support.constant.property-value\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"support.constant.font-name\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"entity.other.attribute-name.class.css\",\n    settings: {\n      foreground: \"#16a994\",\n      fontStyle: \"normal\",\n    },\n  },\n  {\n    scope: \"entity.other.attribute-name.id\",\n    settings: {\n      foreground: \"#7b43f8\",\n      fontStyle: \"normal\",\n    },\n  },\n  {\n    scope: [\n      \"entity.other.attribute-name.pseudo-element\",\n      \"entity.other.attribute-name.pseudo-class\",\n    ],\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"meta.selector\",\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: \"selector.sass\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: \"rgb-value\",\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"inline-color-decoration rgb-value\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"less rgb-value\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"control.elements\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"keyword.operator.less\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"entity.name.tag\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: \"entity.other.attribute-name\",\n    settings: {\n      foreground: \"#16a994\",\n      fontStyle: \"normal\",\n    },\n  },\n  {\n    scope: \"constant.character.entity\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: \"meta.tag\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"invalid.illegal.bad-ampersand.html\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"markup.heading\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: [\n      \"markup.heading punctuation.definition.heading\",\n      \"entity.name.section\",\n    ],\n    settings: { foreground: \"#7b43f8\" },\n  },\n  {\n    scope: \"entity.name.section.markdown\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: \"punctuation.definition.heading.markdown\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: \"markup.heading.setext\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: [\n      \"markup.heading.setext.1.markdown\",\n      \"markup.heading.setext.2.markdown\",\n    ],\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: [\"markup.bold\", \"todo.bold\"],\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"punctuation.definition.bold\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"punctuation.definition.bold.markdown\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: [\"markup.italic\", \"punctuation.definition.italic\", \"todo.emphasis\"],\n    settings: {\n      foreground: \"#fc2b73\",\n      fontStyle: \"italic\",\n    },\n  },\n  {\n    scope: \"emphasis md\",\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: \"markup.italic.markdown\",\n    settings: { fontStyle: \"italic\" },\n  },\n  {\n    scope: [\n      \"markup.underline.link.markdown\",\n      \"markup.underline.link.image.markdown\",\n    ],\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: [\n      \"string.other.link.title.markdown\",\n      \"string.other.link.description.markdown\",\n    ],\n    settings: { foreground: \"#7b43f8\" },\n  },\n  {\n    scope: \"punctuation.definition.metadata.markdown\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: [\"markup.inline.raw.markdown\", \"markup.inline.raw.string.markdown\"],\n    settings: { foreground: \"#199f43\" },\n  },\n  {\n    scope: \"punctuation.definition.list.begin.markdown\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: \"punctuation.definition.list.markdown\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: \"beginning.punctuation.definition.list.markdown\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: [\n      \"punctuation.definition.string.begin.markdown\",\n      \"punctuation.definition.string.end.markdown\",\n    ],\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: \"markup.quote.markdown\",\n    settings: { foreground: \"#84848A\" },\n  },\n  {\n    scope: \"keyword.other.unit\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: \"markup.changed.diff\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: [\n      \"meta.diff.header.from-file\",\n      \"meta.diff.header.to-file\",\n      \"punctuation.definition.from-file.diff\",\n      \"punctuation.definition.to-file.diff\",\n    ],\n    settings: { foreground: \"#7b43f8\" },\n  },\n  {\n    scope: \"markup.inserted.diff\",\n    settings: { foreground: \"#199f43\" },\n  },\n  {\n    scope: \"markup.deleted.diff\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: \"string.regexp\",\n    settings: { foreground: \"#17a5af\" },\n  },\n  {\n    scope: \"constant.other.character-class.regexp\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: \"keyword.operator.quantifier.regexp\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"constant.character.escape\",\n    settings: { foreground: \"#1ca1c7\" },\n  },\n  {\n    scope: \"source.json meta.structure.dictionary.json > string.quoted.json\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope:\n      \"source.json meta.structure.dictionary.json > string.quoted.json > punctuation.string\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: [\n      \"source.json meta.structure.dictionary.json > value.json > string.quoted.json\",\n      \"source.json meta.structure.array.json > value.json > string.quoted.json\",\n      \"source.json meta.structure.dictionary.json > value.json > string.quoted.json > punctuation\",\n      \"source.json meta.structure.array.json > value.json > string.quoted.json > punctuation\",\n    ],\n    settings: { foreground: \"#199f43\" },\n  },\n  {\n    scope: [\n      \"source.json meta.structure.dictionary.json > constant.language.json\",\n      \"source.json meta.structure.array.json > constant.language.json\",\n    ],\n    settings: { foreground: \"#08c0ef\" },\n  },\n  {\n    scope: \"support.type.property-name.json\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: \"support.type.property-name.json punctuation\",\n    settings: { foreground: \"#d52c36\" },\n  },\n  {\n    scope: \"punctuation.definition.block.sequence.item.yaml\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"block.scope.end\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"block.scope.begin\",\n    settings: { foreground: \"#79797F\" },\n  },\n  {\n    scope: \"token.info-token\",\n    settings: { foreground: \"#7b43f8\" },\n  },\n  {\n    scope: \"token.warn-token\",\n    settings: { foreground: \"#d5a910\" },\n  },\n  {\n    scope: \"token.error-token\",\n    settings: { foreground: \"#f44747\" },\n  },\n  {\n    scope: \"token.debug-token\",\n    settings: { foreground: \"#fc2b73\" },\n  },\n  {\n    scope: \"invalid.illegal\",\n    settings: { foreground: \"#ffffff\" },\n  },\n  {\n    scope: \"invalid.broken\",\n    settings: { foreground: \"#ffffff\" },\n  },\n  {\n    scope: \"invalid.deprecated\",\n    settings: { foreground: \"#ffffff\" },\n  },\n  {\n    scope: \"invalid.unimplemented\",\n    settings: { foreground: \"#ffffff\" },\n  },\n];\nvar semanticTokenColors = {\n  comment: \"#84848A\",\n  string: \"#199f43\",\n  number: \"#1ca1c7\",\n  regexp: \"#17a5af\",\n  keyword: \"#fc2b73\",\n  variable: \"#d47628\",\n  parameter: \"#79797F\",\n  property: \"#d47628\",\n  function: \"#7b43f8\",\n  method: \"#7b43f8\",\n  type: \"#c635e4\",\n  class: \"#c635e4\",\n  namespace: \"#d5a910\",\n  enumMember: \"#08c0ef\",\n  \"variable.constant\": \"#d5a910\",\n  \"variable.defaultLibrary\": \"#d5a910\",\n};\nvar pierre_light_default = {\n  name,\n  type,\n  colors,\n  tokenColors,\n  semanticTokenColors,\n};\n\n//#endregion\nexport {\n  colors,\n  pierre_light_default as default,\n  name,\n  semanticTokenColors,\n  tokenColors,\n  type,\n};\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/schema.ts",
    "content": "import { z } from \"zod\";\nimport type { ReactNode } from \"react\";\n\n/**\n * Tool UI conventions:\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\n * - Schema: `SerializableXSchema`\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\n * - Root attrs: `data-tool-ui-id` + `data-slot`\n */\n\n/**\n * Schema for tool UI identity.\n *\n * Every tool UI should have a unique identifier that:\n * - Is stable across re-renders\n * - Is meaningful (not auto-generated)\n * - Is unique within the conversation\n *\n * Format recommendation: `{component-type}-{semantic-identifier}`\n * Examples: \"data-table-expenses-q3\", \"option-list-deploy-target\"\n */\nexport const ToolUIIdSchema = z.string().min(1);\n\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\n\n/**\n * Primary role of a Tool UI surface in a chat context.\n */\nexport const ToolUIRoleSchema = z.enum([\n  \"information\",\n  \"decision\",\n  \"control\",\n  \"state\",\n  \"composite\",\n]);\n\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\n\nexport const ToolUIReceiptOutcomeSchema = z.enum([\n  \"success\",\n  \"partial\",\n  \"failed\",\n  \"cancelled\",\n]);\n\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\n\n/**\n * Optional receipt metadata: a durable summary of an outcome.\n */\nexport const ToolUIReceiptSchema = z.object({\n  outcome: ToolUIReceiptOutcomeSchema,\n  summary: z.string().min(1),\n  identifiers: z.record(z.string(), z.string()).optional(),\n  at: z.string().datetime(),\n});\n\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\n\n/**\n * Base schema for Tool UI payloads (id + optional role/receipt).\n */\nexport const ToolUISurfaceSchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  receipt: ToolUIReceiptSchema.optional(),\n});\n\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\n\nexport const ActionSchema = z.object({\n  id: z.string().min(1),\n  label: z.string().min(1),\n  /**\n   * Canonical narration the assistant can use after this action is taken.\n   *\n   * Example: \"I exported the table as CSV.\" / \"I opened the link in a new tab.\"\n   */\n  sentence: z.string().optional(),\n  confirmLabel: z.string().optional(),\n  variant: z\n    .enum([\"default\", \"destructive\", \"secondary\", \"ghost\", \"outline\"])\n    .optional(),\n  icon: z.custom<ReactNode>().optional(),\n  loading: z.boolean().optional(),\n  disabled: z.boolean().optional(),\n  shortcut: z.string().optional(),\n});\n\nexport type Action = z.infer<typeof ActionSchema>;\nexport type LocalAction = Action;\nexport type DecisionAction = Action;\n\nexport const DecisionResultSchema = z.object({\n  kind: z.literal(\"decision\"),\n  version: z.literal(1),\n  decisionId: z.string().min(1),\n  actionId: z.string().min(1),\n  actionLabel: z.string().min(1),\n  at: z.string().datetime(),\n  payload: z.record(z.string(), z.unknown()).optional(),\n});\n\nexport type DecisionResult<\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\n> = Omit<z.infer<typeof DecisionResultSchema>, \"payload\"> & {\n  payload?: TPayload;\n};\n\nexport function createDecisionResult<\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\n>(args: {\n  decisionId: string;\n  action: { id: string; label: string };\n  payload?: TPayload;\n}): DecisionResult<TPayload> {\n  return {\n    kind: \"decision\",\n    version: 1,\n    decisionId: args.decisionId,\n    actionId: args.action.id,\n    actionLabel: args.action.label,\n    at: new Date().toISOString(),\n    payload: args.payload,\n  };\n}\n\nexport const ActionButtonsPropsSchema = z.object({\n  actions: z.array(ActionSchema).min(1),\n  align: z.enum([\"left\", \"center\", \"right\"]).optional(),\n  confirmTimeout: z.number().positive().optional(),\n  className: z.string().optional(),\n});\n\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\n  actions: z.array(SerializableActionSchema),\n}).omit({ className: true });\n\nexport interface ActionsConfig {\n  items: Action[];\n  align?: \"left\" | \"center\" | \"right\";\n  confirmTimeout?: number;\n}\n\nexport const SerializableActionsConfigSchema = z.object({\n  items: z.array(SerializableActionSchema).min(1),\n  align: z.enum([\"left\", \"center\", \"right\"]).optional(),\n  confirmTimeout: z.number().positive().optional(),\n});\n\nexport type SerializableActionsConfig = z.infer<\n  typeof SerializableActionsConfigSchema\n>;\n\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/tool-ui-context.tsx",
    "content": "\"use client\";\n\nimport { createContext, use } from \"react\";\n\nexport interface ToolUIContextValue {\n  id: string;\n  surfaceMounted: boolean;\n  setSurfaceMounted: (mounted: boolean) => void;\n}\n\nexport const ToolUIContext = createContext<ToolUIContextValue | null>(null);\n\nexport function useOptionalToolUI(): ToolUIContextValue | null {\n  return use(ToolUIContext);\n}\n\nexport function useToolUI(): ToolUIContextValue {\n  const context = useOptionalToolUI();\n\n  if (!context) {\n    throw new Error(\n      \"ToolUI context is missing. Wrap LocalActions/DecisionActions with <ToolUI>.\",\n    );\n  }\n\n  return context;\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/tool-ui.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { cn } from \"./_adapter\";\nimport { ToolUIContext, useToolUI } from \"./tool-ui-context\";\nimport { LocalActions } from \"./local-actions\";\nimport { DecisionActions } from \"./decision-actions\";\n\nexport interface ToolUIProps {\n  id: string;\n  children: ReactNode;\n  className?: string;\n}\n\nfunction ToolUIRoot({ id, children, className }: ToolUIProps) {\n  const [surfaceMounted, setSurfaceMounted] = useState(false);\n\n  const value = useMemo(\n    () => ({ id, surfaceMounted, setSurfaceMounted }),\n    [id, surfaceMounted],\n  );\n\n  return (\n    <ToolUIContext.Provider value={value}>\n      <div\n        className={cn(\"flex flex-col gap-3\", className)}\n        data-slot=\"tool-ui\"\n        data-tool-ui-id={id}\n      >\n        {children}\n      </div>\n    </ToolUIContext.Provider>\n  );\n}\n\nexport interface ToolUISurfaceProps {\n  children: ReactNode;\n}\n\nfunction ToolUISurface({ children }: ToolUISurfaceProps) {\n  const { setSurfaceMounted } = useToolUI();\n\n  useEffect(() => {\n    setSurfaceMounted(true);\n    return () => setSurfaceMounted(false);\n  }, [setSurfaceMounted]);\n\n  return <>{children}</>;\n}\n\nexport interface ToolUIActionsProps {\n  children: ReactNode;\n  className?: string;\n  ariaLabel?: string;\n}\n\nfunction ToolUIActions({ children, className, ariaLabel }: ToolUIActionsProps) {\n  const { id, surfaceMounted } = useToolUI();\n\n  if (!surfaceMounted) {\n    return null;\n  }\n\n  return (\n    <div\n      className={cn(\"flex flex-col gap-2\", className)}\n      data-slot=\"tool-ui-actions\"\n      data-tool-ui-id={id}\n      role=\"group\"\n      aria-label={ariaLabel ?? \"Tool UI actions\"}\n    >\n      {children}\n    </div>\n  );\n}\n\ntype ToolUIComponent = typeof ToolUIRoot & {\n  Surface: typeof ToolUISurface;\n  Actions: typeof ToolUIActions;\n  LocalActions: typeof LocalActions;\n  DecisionActions: typeof DecisionActions;\n};\n\nexport const ToolUI = Object.assign(ToolUIRoot, {\n  Surface: ToolUISurface,\n  Actions: ToolUIActions,\n  LocalActions,\n  DecisionActions,\n}) as ToolUIComponent;\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/toolkit.tsx",
    "content": "import type { ReactNode } from \"react\";\n\nexport type ToolUiToolkitRenderContext = {\n  args?: unknown;\n  result?: unknown;\n  toolCallId?: string;\n  addResult?: (result: unknown) => Promise<void> | void;\n  [key: string]: unknown;\n};\n\nexport function createResultToolRenderer<T>(options: {\n  safeParse: (input: unknown) => T | null;\n  render: (parsed: T, ctx: ToolUiToolkitRenderContext) => ReactNode;\n}) {\n  return (ctx: ToolUiToolkitRenderContext) => {\n    const parsed = options.safeParse(ctx.result);\n    if (!parsed) return null;\n    return options.render(parsed, ctx);\n  };\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/use-action-buttons.tsx",
    "content": "\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { Action } from \"./schema\";\n\nexport type UseActionButtonsOptions = {\n  actions: Action[];\n  onAction: (actionId: string) => void | Promise<void>;\n  onBeforeAction?: (actionId: string) => boolean | Promise<boolean>;\n  confirmTimeout?: number;\n};\n\nexport type UseActionButtonsResult = {\n  actions: Array<\n    Action & {\n      currentLabel: string;\n      isConfirming: boolean;\n      isExecuting: boolean;\n      isDisabled: boolean;\n      isLoading: boolean;\n    }\n  >;\n  runAction: (actionId: string) => Promise<void>;\n  confirmingActionId: string | null;\n  executingActionId: string | null;\n};\n\ntype ActionExecutionLock = {\n  tryAcquire: () => boolean;\n  release: () => void;\n};\n\nexport function createActionExecutionLock(): ActionExecutionLock {\n  let locked = false;\n\n  return {\n    tryAcquire: () => {\n      if (locked) return false;\n      locked = true;\n      return true;\n    },\n    release: () => {\n      locked = false;\n    },\n  };\n}\n\nexport function useActionButtons(\n  options: UseActionButtonsOptions,\n): UseActionButtonsResult {\n  const { actions, onAction, onBeforeAction, confirmTimeout = 3000 } = options;\n\n  const [confirmingActionId, setConfirmingActionId] = useState<string | null>(\n    null,\n  );\n  const [executingActionId, setExecutingActionId] = useState<string | null>(\n    null,\n  );\n  const executionLockRef = useRef<ActionExecutionLock>(\n    createActionExecutionLock(),\n  );\n\n  useEffect(() => {\n    if (!confirmingActionId) return;\n    const id = setTimeout(() => setConfirmingActionId(null), confirmTimeout);\n    return () => clearTimeout(id);\n  }, [confirmingActionId, confirmTimeout]);\n\n  useEffect(() => {\n    if (!confirmingActionId) return;\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") {\n        setConfirmingActionId(null);\n      }\n    };\n\n    window.addEventListener(\"keydown\", handleKeyDown);\n    return () => window.removeEventListener(\"keydown\", handleKeyDown);\n  }, [confirmingActionId]);\n\n  const runAction = useCallback(\n    async (actionId: string) => {\n      const action = actions.find((a) => a.id === actionId);\n      if (!action) return;\n\n      const isAnyActionExecuting = executingActionId !== null;\n      if (action.disabled || action.loading || isAnyActionExecuting) {\n        return;\n      }\n\n      if (action.confirmLabel && confirmingActionId !== action.id) {\n        setConfirmingActionId(action.id);\n        return;\n      }\n\n      if (!executionLockRef.current.tryAcquire()) {\n        return;\n      }\n\n      if (onBeforeAction) {\n        const shouldProceed = await onBeforeAction(action.id);\n        if (!shouldProceed) {\n          setConfirmingActionId(null);\n          executionLockRef.current.release();\n          return;\n        }\n      }\n\n      try {\n        setExecutingActionId(action.id);\n        await onAction(action.id);\n      } finally {\n        executionLockRef.current.release();\n        setExecutingActionId(null);\n        setConfirmingActionId(null);\n      }\n    },\n    [actions, confirmingActionId, executingActionId, onAction, onBeforeAction],\n  );\n\n  const resolvedActions = useMemo(\n    () =>\n      actions.map((action) => {\n        const isConfirming = confirmingActionId === action.id;\n        const isThisActionExecuting = executingActionId === action.id;\n        const isLoading = action.loading || isThisActionExecuting;\n        const isDisabled =\n          action.disabled ||\n          (executingActionId !== null && !isThisActionExecuting);\n        const currentLabel =\n          isConfirming && action.confirmLabel\n            ? action.confirmLabel\n            : action.label;\n\n        return {\n          ...action,\n          currentLabel,\n          isConfirming,\n          isExecuting: isThisActionExecuting,\n          isDisabled,\n          isLoading,\n        };\n      }),\n    [actions, confirmingActionId, executingActionId],\n  );\n\n  return {\n    actions: resolvedActions,\n    runAction,\n    confirmingActionId,\n    executingActionId,\n  };\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/use-controllable-state.ts",
    "content": "\"use client\";\n\nimport { useCallback, useMemo, useRef, useState } from \"react\";\n\nexport type UseControllableStateOptions<T> = {\n  value?: T;\n  defaultValue: T;\n  onChange?: (next: T) => void;\n};\n\nexport function useControllableState<T>({\n  value,\n  defaultValue,\n  onChange,\n}: UseControllableStateOptions<T>) {\n  const [uncontrolled, setUncontrolled] = useState<T>(defaultValue);\n  const isControlled = value !== undefined;\n\n  const currentValue = useMemo(\n    () => (isControlled ? (value as T) : uncontrolled),\n    [isControlled, value, uncontrolled],\n  );\n  const currentValueRef = useRef(currentValue);\n  currentValueRef.current = currentValue;\n\n  const setValue = useCallback(\n    (next: T | ((prev: T) => T)) => {\n      const resolved =\n        typeof next === \"function\"\n          ? (next as (prev: T) => T)(currentValueRef.current)\n          : next;\n\n      currentValueRef.current = resolved;\n      if (!isControlled) {\n        setUncontrolled(resolved);\n      }\n\n      onChange?.(resolved);\n      return resolved;\n    },\n    [isControlled, onChange],\n  );\n\n  const setUncontrolledValue = useCallback((next: T) => {\n    setUncontrolled(next);\n  }, []);\n\n  return {\n    value: currentValue,\n    isControlled,\n    setValue,\n    setUncontrolledValue,\n  };\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/use-copy-to-clipboard.ts",
    "content": "\"use client\";\n\nimport { useCallback, useEffect, useState } from \"react\";\n\nfunction fallbackCopyToClipboard(text: string): boolean {\n  const textArea = document.createElement(\"textarea\");\n  try {\n    textArea.value = text;\n    textArea.setAttribute(\"readonly\", \"\");\n    textArea.style.position = \"fixed\";\n    textArea.style.top = \"-9999px\";\n    textArea.style.left = \"-9999px\";\n    document.body.appendChild(textArea);\n    textArea.select();\n    return document.execCommand(\"copy\");\n  } catch {\n    return false;\n  } finally {\n    if (textArea.parentNode) {\n      textArea.parentNode.removeChild(textArea);\n    }\n  }\n}\n\nexport function useCopyToClipboard(options?: { resetAfterMs?: number }): {\n  copiedId: string | null;\n  copy: (text: string, id?: string) => Promise<boolean>;\n} {\n  const resetAfterMs = options?.resetAfterMs ?? 2000;\n  const [copiedId, setCopiedId] = useState<string | null>(null);\n\n  const copy = useCallback(async (text: string, id: string = \"default\") => {\n    let ok = false;\n    try {\n      if (navigator.clipboard?.writeText) {\n        await navigator.clipboard.writeText(text);\n        ok = true;\n      } else {\n        ok = fallbackCopyToClipboard(text);\n      }\n    } catch {\n      ok = fallbackCopyToClipboard(text);\n    }\n\n    if (ok) {\n      setCopiedId(id);\n    }\n\n    return ok;\n  }, []);\n\n  useEffect(() => {\n    if (!copiedId) return;\n    const timeout = setTimeout(() => setCopiedId(null), resetAfterMs);\n    return () => clearTimeout(timeout);\n  }, [copiedId, resetAfterMs]);\n\n  return { copiedId, copy };\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/use-signature-reset.ts",
    "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nexport function useSignatureReset(\n  signature: string,\n  onSignatureChange: () => void,\n) {\n  const previousSignature = useRef(signature);\n\n  useEffect(() => {\n    if (previousSignature.current === signature) return;\n    previousSignature.current = signature;\n    onSignatureChange();\n  }, [signature, onSignatureChange]);\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/shared/utils.ts",
    "content": "export function formatRelativeTime(iso: string): string {\n  const seconds = Math.round((Date.now() - new Date(iso).getTime()) / 1000);\n  if (seconds < 60) return `${seconds}s`;\n  if (seconds < 3600) return `${Math.round(seconds / 60)}m`;\n  if (seconds < 86400) return `${Math.round(seconds / 3600)}h`;\n  if (seconds < 604800) return `${Math.round(seconds / 86400)}d`;\n  return `${Math.round(seconds / 604800)}w`;\n}\n\nexport function formatCount(count: number): string {\n  if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;\n  if (count >= 1_000) return `${(count / 1_000).toFixed(1)}K`;\n  return String(count);\n}\n\nexport function getDomain(url: string): string {\n  try {\n    return new URL(url).hostname.replace(/^www\\./, \"\");\n  } catch {\n    return \"\";\n  }\n}\n\nexport function prefersReducedMotion(): boolean {\n  return (\n    typeof window !== \"undefined\" &&\n    window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/stats-display/README.md",
    "content": "# Stats Display\n\nImplementation for the \"stats-display\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/stats-display/index.tsx\n- serializable schema + parse helpers: components/tool-ui/stats-display/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/stats-display/content.mdx\n- Preset payload: lib/presets/stats-display.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/stats-display/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn   → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Card → shadcn/ui Card\n */\n\nexport { cn } from \"@/lib/utils\";\nexport {\n  Card,\n  CardHeader,\n  CardTitle,\n  CardDescription,\n  CardContent,\n} from \"@/components/ui/card\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/stats-display/index.tsx",
    "content": "export { StatsDisplay } from \"./stats-display\";\nexport { Sparkline, type SparklineProps } from \"./sparkline\";\nexport {\n  type SerializableStatsDisplay,\n  type StatsDisplayProps,\n  type StatFormat,\n  type StatDiff,\n  type StatSparkline,\n  type StatItem,\n} from \"./schema\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/stats-display/schema.ts",
    "content": "import { z } from \"zod\";\nimport { defineToolUiContract } from \"../shared/contract\";\nimport { ToolUIIdSchema, ToolUIRoleSchema } from \"../shared/schema\";\n\nconst TextFormatSchema = z.object({\n  kind: z.literal(\"text\"),\n});\n\nconst NumberFormatSchema = z.object({\n  kind: z.literal(\"number\"),\n  decimals: z.number().int().min(0).optional(),\n  compact: z.boolean().optional(),\n});\n\nconst CurrencyFormatSchema = z.object({\n  kind: z.literal(\"currency\"),\n  currency: z.string().min(1),\n  decimals: z.number().int().min(0).optional(),\n});\n\nconst PercentFormatSchema = z.object({\n  kind: z.literal(\"percent\"),\n  decimals: z.number().int().min(0).optional(),\n  basis: z.enum([\"fraction\", \"unit\"]).optional(),\n});\n\nexport const StatFormatSchema = z.discriminatedUnion(\"kind\", [\n  TextFormatSchema,\n  NumberFormatSchema,\n  CurrencyFormatSchema,\n  PercentFormatSchema,\n]);\n\nexport type StatFormat = z.infer<typeof StatFormatSchema>;\n\nexport const StatDiffSchema = z.object({\n  value: z.number(),\n  decimals: z.number().int().min(0).optional(),\n  upIsPositive: z.boolean().optional(),\n  label: z.string().optional(),\n});\n\nexport type StatDiff = z.infer<typeof StatDiffSchema>;\n\nexport const StatSparklineSchema = z.object({\n  data: z.array(z.number()).min(2),\n  color: z.string().optional(),\n});\n\nexport type StatSparkline = z.infer<typeof StatSparklineSchema>;\n\nexport const StatItemSchema = z.object({\n  key: z.string().min(1),\n  label: z.string().min(1),\n  value: z.union([z.string(), z.number()]),\n  format: StatFormatSchema.optional(),\n  diff: StatDiffSchema.optional(),\n  sparkline: StatSparklineSchema.optional(),\n});\n\nexport type StatItem = z.infer<typeof StatItemSchema>;\n\nexport const SerializableStatsDisplaySchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  title: z.string().optional(),\n  description: z.string().optional(),\n  stats: z.array(StatItemSchema).min(1),\n});\n\nexport type SerializableStatsDisplay = z.infer<\n  typeof SerializableStatsDisplaySchema\n>;\n\nconst SerializableStatsDisplaySchemaContract = defineToolUiContract(\n  \"StatsDisplay\",\n  SerializableStatsDisplaySchema,\n);\n\nexport const parseSerializableStatsDisplay: (\n  input: unknown,\n) => SerializableStatsDisplay = SerializableStatsDisplaySchemaContract.parse;\n\nexport const safeParseSerializableStatsDisplay: (\n  input: unknown,\n) => SerializableStatsDisplay | null =\n  SerializableStatsDisplaySchemaContract.safeParse;\nexport interface StatsDisplayProps extends SerializableStatsDisplay {\n  className?: string;\n  locale?: string;\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/stats-display/sparkline.tsx",
    "content": "\"use client\";\n\nimport type { CSSProperties } from \"react\";\nimport { useId } from \"react\";\nimport { cn } from \"./_adapter\";\n\nexport interface SparklineProps {\n  data: number[];\n  color?: string;\n  width?: number;\n  height?: number;\n  className?: string;\n  style?: CSSProperties;\n  showFill?: boolean;\n  fillOpacity?: number;\n}\n\nexport function Sparkline({\n  data,\n  color = \"currentColor\",\n  width = 64,\n  height = 24,\n  className,\n  style,\n  showFill = false,\n  fillOpacity = 0.09,\n}: SparklineProps) {\n  const gradientId = useId();\n\n  if (data.length < 2) {\n    return null;\n  }\n\n  const minVal = Math.min(...data);\n  const maxVal = Math.max(...data);\n  const range = maxVal - minVal || 1;\n\n  const padding = 0;\n  const usableWidth = width;\n  const usableHeight = height;\n\n  const linePoints = data.map((value, index) => {\n    const x = padding + (index / (data.length - 1)) * usableWidth;\n    const y =\n      padding + usableHeight - ((value - minVal) / range) * usableHeight;\n    return { x, y };\n  });\n\n  const linePointsString = linePoints.map((p) => `${p.x},${p.y}`).join(\" \");\n\n  const areaPointsString = [\n    `${padding},${height}`,\n    ...linePoints.map((p) => `${p.x},${p.y}`),\n    `${width - padding},${height}`,\n  ].join(\" \");\n\n  const animationDelay = style?.animationDelay ?? \"0ms\";\n  const baseAnimationDelay =\n    typeof animationDelay === \"number\" ? `${animationDelay}ms` : animationDelay;\n  const secondaryAnimationDelay = `calc(${baseAnimationDelay} + 100ms)`;\n\n  return (\n    <svg\n      viewBox={`0 0 ${width} ${height}`}\n      aria-hidden=\"true\"\n      className={cn(\"h-full w-full shrink-0\", className)}\n      style={style}\n      preserveAspectRatio=\"none\"\n    >\n      {showFill && (\n        <>\n          <defs>\n            <linearGradient id={gradientId} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n              <stop offset=\"0%\" stopColor={color} stopOpacity={fillOpacity} />\n              <stop offset=\"100%\" stopColor={color} stopOpacity={0} />\n            </linearGradient>\n          </defs>\n          <polygon\n            points={areaPointsString}\n            fill={`url(#${gradientId})`}\n            className=\"animate-in fade-in duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] fill-mode-both\"\n            style={{ animationDelay }}\n          />\n        </>\n      )}\n      <polyline\n        points={linePointsString}\n        fill=\"none\"\n        stroke={color}\n        strokeWidth={1}\n        strokeOpacity={0.15}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        vectorEffect=\"non-scaling-stroke\"\n      />\n      <polyline\n        points={linePointsString}\n        fill=\"none\"\n        stroke={color}\n        strokeWidth={0.75}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        vectorEffect=\"non-scaling-stroke\"\n        pathLength={1}\n        strokeDasharray=\"0.36 0.64\"\n        strokeDashoffset={0}\n        strokeOpacity={0.2}\n        className=\"opacity-0 motion-safe:animate-in motion-safe:fade-in motion-safe:duration-700 motion-safe:ease-out motion-safe:fill-mode-both\"\n        style={{ animationDelay: baseAnimationDelay }}\n      />\n      <polyline\n        points={linePointsString}\n        fill=\"none\"\n        stroke={color}\n        strokeWidth={0.75}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        vectorEffect=\"non-scaling-stroke\"\n        pathLength={1}\n        strokeDasharray=\"0.24 0.76\"\n        strokeDashoffset={0}\n        strokeOpacity={0.65}\n        className=\"opacity-0 motion-safe:animate-in motion-safe:fade-in motion-safe:duration-500 motion-safe:ease-out motion-safe:fill-mode-both\"\n        style={{ animationDelay: secondaryAnimationDelay }}\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/stats-display/stats-display.tsx",
    "content": "\"use client\";\nimport {\n  cn,\n  Card,\n  CardHeader,\n  CardTitle,\n  CardDescription,\n  CardContent,\n} from \"./_adapter\";\nimport type {\n  StatsDisplayProps,\n  StatItem,\n  StatFormat,\n  StatDiff,\n} from \"./schema\";\nimport { Sparkline } from \"./sparkline\";\n\ninterface FormattedValueProps {\n  value: string | number;\n  format?: StatFormat;\n  locale?: string;\n}\n\nfunction FormattedValue({ value, format, locale }: FormattedValueProps) {\n  if (typeof value === \"string\" || !format) {\n    return <span className=\"font-light tabular-nums\">{String(value)}</span>;\n  }\n\n  switch (format.kind) {\n    case \"number\": {\n      const decimals = format.decimals ?? 0;\n      if (format.compact) {\n        const parts = new Intl.NumberFormat(locale, {\n          minimumFractionDigits: decimals,\n          maximumFractionDigits: decimals,\n          notation: \"compact\",\n        }).formatToParts(value);\n        const fullNumber = new Intl.NumberFormat(locale).format(value);\n        return (\n          <span className=\"font-light tabular-nums\" aria-label={fullNumber}>\n            {parts.map((part, i) =>\n              part.type === \"compact\" ? (\n                <span\n                  key={i}\n                  className=\"ml-0.5 text-[0.65em] opacity-80\"\n                  aria-hidden=\"true\"\n                >\n                  {part.value}\n                </span>\n              ) : (\n                <span key={i}>{part.value}</span>\n              ),\n            )}\n          </span>\n        );\n      }\n      const formatted = new Intl.NumberFormat(locale, {\n        minimumFractionDigits: decimals,\n        maximumFractionDigits: decimals,\n      }).format(value);\n      return <span className=\"font-light tabular-nums\">{formatted}</span>;\n    }\n    case \"currency\": {\n      const currency = format.currency;\n      const decimals = format.decimals ?? 2;\n      const formatted = new Intl.NumberFormat(locale, {\n        style: \"currency\",\n        currency,\n        minimumFractionDigits: decimals,\n        maximumFractionDigits: decimals,\n      }).format(value);\n      const spokenValue = new Intl.NumberFormat(locale, {\n        style: \"currency\",\n        currency,\n        currencyDisplay: \"name\",\n        minimumFractionDigits: decimals,\n        maximumFractionDigits: decimals,\n      }).format(value);\n      return (\n        <span className=\"font-light tabular-nums\" aria-label={spokenValue}>\n          {formatted}\n        </span>\n      );\n    }\n    case \"percent\": {\n      const decimals = format.decimals ?? 2;\n      const basis = format.basis ?? \"fraction\";\n      const numeric = basis === \"fraction\" ? value * 100 : value;\n      const formatted = numeric.toFixed(decimals);\n      return (\n        <span\n          className=\"font-light tabular-nums\"\n          aria-label={`${formatted} percent`}\n        >\n          {formatted}\n          <span className=\"ml-0.5 text-[0.65em] opacity-80\" aria-hidden=\"true\">\n            %\n          </span>\n        </span>\n      );\n    }\n    case \"text\":\n    default:\n      return <span className=\"font-light tabular-nums\">{String(value)}</span>;\n  }\n}\n\ninterface DeltaValueProps {\n  diff: StatDiff;\n}\n\nfunction DeltaValue({ diff }: DeltaValueProps) {\n  const { value, decimals = 1, upIsPositive = true, label } = diff;\n\n  const isPositive = value > 0;\n  const isNegative = value < 0;\n\n  const isGood = upIsPositive ? isPositive : isNegative;\n  const isBad = upIsPositive ? isNegative : isPositive;\n\n  const colorClass = isGood\n    ? \"text-green-600 dark:text-green-400\"\n    : isBad\n      ? \"text-red-600 dark:text-red-500\"\n      : \"text-muted-foreground\";\n\n  const bgClass = isGood\n    ? \"bg-green-500/10 dark:bg-green-600/15\"\n    : isBad\n      ? \"bg-red-500/10 dark:bg-red-500/15\"\n      : \"bg-muted\";\n\n  const formatted = Math.abs(value).toFixed(decimals);\n  const sign = isNegative ? \"−\" : \"+\";\n  const display = `${sign}${formatted}%`;\n\n  return (\n    <span\n      className={cn(\n        \"inline-flex items-center gap-1 rounded-full px-1.5 py-0.5 text-xs  tabular-nums\",\n        colorClass,\n        bgClass,\n      )}\n    >\n      {!upIsPositive && (\n        <span className=\"text-[0.9em]\">{isGood ? \"↓\" : \"↑\"}</span>\n      )}\n      {display}\n      {label && (\n        <span className=\"text-muted-foreground font-normal\">{label}</span>\n      )}\n    </span>\n  );\n}\n\ninterface StatCardProps {\n  stat: StatItem;\n  locale?: string;\n  isSingle?: boolean;\n  index?: number;\n}\n\nfunction StatCard({\n  stat,\n  locale,\n  isSingle = false,\n  index = 0,\n}: StatCardProps) {\n  const sparklineColor = stat.sparkline?.color ?? \"var(--muted-foreground)\";\n  const hasSparkline = Boolean(stat.sparkline);\n  const baseDelay = index * 175;\n\n  return (\n    <div\n      className={cn(\n        \"relative flex min-h-28 flex-col gap-1 px-6\",\n        isSingle ? \"justify-center\" : \"justify-end\",\n      )}\n    >\n      {hasSparkline && (\n        <Sparkline\n          data={stat.sparkline!.data}\n          color={sparklineColor}\n          showFill\n          fillOpacity={0.09}\n          className=\"pointer-events-none absolute inset-x-0 top-2 bottom-2 animate-in fade-in slide-in-from-bottom-12 duration-500 ease-[cubic-bezier(0.16,1,0.3,1)] fill-mode-both\"\n          style={{ animationDelay: `${baseDelay}ms` }}\n        />\n      )}\n      <span\n        className=\"text-muted-foreground relative text-xs font-normal tracking-wider uppercase opacity-90 animate-in fade-in slide-in-from-bottom-1 duration-500 ease-[cubic-bezier(0.16,1,0.3,1)] fill-mode-both\"\n        style={{ animationDelay: `${baseDelay + 75}ms` }}\n      >\n        {stat.label}\n      </span>\n      <div\n        className=\"relative flex items-baseline gap-2 pb-2 animate-in fade-in slide-in-from-bottom-2 duration-500 ease-[cubic-bezier(0.16,1,0.3,1)] fill-mode-both\"\n        style={{ animationDelay: `${baseDelay + 150}ms` }}\n      >\n        <span\n          className={cn(\n            \"font-light tracking-normal\",\n            isSingle ? \"text-5xl\" : \"text-3xl\",\n          )}\n        >\n          <FormattedValue\n            value={stat.value}\n            format={stat.format}\n            locale={locale}\n          />\n        </span>\n        {stat.diff && <DeltaValue diff={stat.diff} />}\n      </div>\n    </div>\n  );\n}\n\nexport function StatsDisplay({\n  id,\n  title,\n  description,\n  stats,\n  className,\n  locale: localeProp,\n}: StatsDisplayProps) {\n  const locale =\n    localeProp ??\n    (typeof navigator !== \"undefined\" ? navigator.language : undefined);\n  const hasHeader = Boolean(title || description);\n  const isSingle = stats.length === 1;\n\n  return (\n    <article\n      data-slot=\"stats-display\"\n      data-tool-ui-id={id}\n      className={cn(\n        \"w-full min-w-80 max-w-xl\",\n        isSingle && \"max-w-sm\",\n        className,\n      )}\n    >\n      <Card className={cn(\"overflow-clip !pb-0 !pt-2\", hasHeader && \"!gap-0\")}>\n        {hasHeader && (\n          <CardHeader className=\"border-b border-border !pt-3 !pb-4\">\n            {title && <CardTitle className=\"text-pretty\">{title}</CardTitle>}\n            {description && (\n              <CardDescription className=\"text-pretty\">\n                {description}\n              </CardDescription>\n            )}\n          </CardHeader>\n        )}\n        <CardContent className=\"@container overflow-hidden p-0\">\n          <div\n            className=\"grid @[440px]:-ml-px @[440px]:-mt-px\"\n            style={{\n              gridTemplateColumns: \"repeat(auto-fit, minmax(220px, 1fr))\",\n            }}\n          >\n            {stats.map((stat, index) => (\n              <div\n                key={stat.key}\n                className={cn(\n                  \"overflow-clip py-3 first:pt-0 @[440px]:py-3 @[440px]:first:pt-3 @[440px]:border-l @[440px]:border-t @[440px]:border-border\",\n                  index > 0 && \"border-border border-t\",\n                )}\n              >\n                <StatCard\n                  stat={stat}\n                  locale={locale}\n                  isSingle={isSingle}\n                  index={index}\n                />\n              </div>\n            ))}\n          </div>\n        </CardContent>\n      </Card>\n    </article>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/terminal/README.md",
    "content": "# Terminal\n\nImplementation for the \"terminal\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/terminal/index.tsx\n- serializable schema + parse helpers: components/tool-ui/terminal/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/terminal/content.mdx\n- Preset payload: lib/presets/terminal.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/terminal/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn          → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Button      → shadcn/ui Button\n *   Collapsible → shadcn/ui Collapsible\n */\n\nexport { cn } from \"@/lib/utils\";\nexport { Button } from \"@/components/ui/button\";\nexport { Collapsible, CollapsibleTrigger } from \"@/components/ui/collapsible\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/terminal/index.tsx",
    "content": "export { Terminal } from \"./terminal\";\nexport type { TerminalProps, SerializableTerminal } from \"./schema\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/terminal/schema.ts",
    "content": "import { z } from \"zod\";\nimport { defineToolUiContract } from \"../shared/contract\";\nimport {\n  ToolUIIdSchema,\n  ToolUIReceiptSchema,\n  ToolUIRoleSchema,\n} from \"../shared/schema\";\n\nexport const TerminalPropsSchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  receipt: ToolUIReceiptSchema.optional(),\n  command: z.string(),\n  stdout: z.string().optional(),\n  stderr: z.string().optional(),\n  exitCode: z.number().int().min(0),\n  durationMs: z.number().optional(),\n  cwd: z.string().optional(),\n  truncated: z.boolean().optional(),\n  maxCollapsedLines: z.number().min(1).optional(),\n  className: z.string().optional(),\n});\n\nexport type TerminalProps = z.infer<typeof TerminalPropsSchema>;\n\nexport const SerializableTerminalSchema = TerminalPropsSchema.omit({\n  className: true,\n});\n\nexport type SerializableTerminal = z.infer<typeof SerializableTerminalSchema>;\n\nconst SerializableTerminalSchemaContract = defineToolUiContract(\n  \"Terminal\",\n  SerializableTerminalSchema,\n);\n\nexport const parseSerializableTerminal: (\n  input: unknown,\n) => SerializableTerminal = SerializableTerminalSchemaContract.parse;\n\nexport const safeParseSerializableTerminal: (\n  input: unknown,\n) => SerializableTerminal | null = SerializableTerminalSchemaContract.safeParse;\n"
  },
  {
    "path": "apps/www/components/tool-ui/terminal/terminal.tsx",
    "content": "\"use client\";\n\nimport { useState, useCallback } from \"react\";\nimport Ansi from \"ansi-to-react\";\nimport {\n  Copy,\n  Check,\n  ChevronDown,\n  ChevronUp,\n  Terminal as TerminalIcon,\n} from \"lucide-react\";\nimport type { TerminalProps } from \"./schema\";\nimport { useCopyToClipboard } from \"../shared/use-copy-to-clipboard\";\n\nimport { Button, Collapsible, CollapsibleTrigger } from \"./_adapter\";\nimport { cn } from \"./_adapter\";\n\nconst COPY_ID = \"terminal-output\";\n\ntype TerminalControlledProps = {\n  expanded?: boolean;\n  defaultExpanded?: boolean;\n  onExpandedChange?: (expanded: boolean) => void;\n};\n\ntype TerminalRootProps = TerminalProps & TerminalControlledProps;\n\ntype TerminalHeaderProps = Pick<\n  TerminalProps,\n  \"command\" | \"cwd\" | \"exitCode\"\n> & {\n  formattedDuration: string | null;\n  hasOutput: boolean;\n  copiedId: string | null;\n  onCopy: () => void;\n};\n\ntype TerminalOutputProps = Pick<\n  TerminalProps,\n  \"stdout\" | \"stderr\" | \"truncated\"\n> & {\n  isCollapsed: boolean;\n  shouldCollapse: boolean;\n  lineCount: number;\n  onToggleCollapse: () => void;\n};\n\nfunction formatDuration(durationMs?: number): string | null {\n  if (durationMs == null) return null;\n  if (durationMs < 1000) return `${Math.round(durationMs)}ms`;\n  return `${(durationMs / 1000).toFixed(1)}s`;\n}\n\nfunction countOutputLines(output: string): number {\n  const trimmedTrailingNewlines = output.replace(/\\n+$/, \"\");\n  if (!trimmedTrailingNewlines) return 0;\n  return trimmedTrailingNewlines.split(\"\\n\").length;\n}\n\nfunction TerminalHeader({\n  command,\n  cwd,\n  exitCode,\n  formattedDuration,\n  hasOutput,\n  copiedId,\n  onCopy,\n}: TerminalHeaderProps) {\n  return (\n    <div className=\"bg-card flex items-center justify-between border-b px-4 py-2\">\n      <div className=\"flex items-center gap-2 overflow-hidden\">\n        <TerminalIcon className=\"text-muted-foreground h-4 w-4 shrink-0\" />\n        <code className=\"text-foreground truncate font-mono text-xs\">\n          {cwd && <span className=\"text-muted-foreground\">{cwd}$ </span>}\n          {command}\n        </code>\n      </div>\n      <div className=\"flex items-center gap-3\">\n        {formattedDuration && (\n          <span className=\"text-muted-foreground font-mono text-sm tabular-nums\">\n            {formattedDuration}\n          </span>\n        )}\n        <span\n          className={cn(\n            \"font-mono text-sm tabular-nums\",\n            exitCode === 0\n              ? \"text-muted-foreground\"\n              : \"text-red-600 dark:text-red-400\",\n          )}\n        >\n          {exitCode}\n        </span>\n        <Button\n          variant=\"ghost\"\n          size=\"sm\"\n          onClick={onCopy}\n          disabled={!hasOutput}\n          className=\"h-7 w-7 p-0\"\n          aria-label={\n            !hasOutput\n              ? \"No output to copy\"\n              : copiedId === COPY_ID\n                ? \"Copied\"\n                : \"Copy output\"\n          }\n        >\n          {hasOutput && copiedId === COPY_ID ? (\n            <Check className=\"h-4 w-4 text-green-700 dark:text-green-400\" />\n          ) : (\n            <Copy className=\"text-muted-foreground h-4 w-4\" />\n          )}\n        </Button>\n      </div>\n    </div>\n  );\n}\n\nfunction TerminalOutput({\n  stdout,\n  stderr,\n  truncated,\n  isCollapsed,\n  shouldCollapse,\n  lineCount,\n  onToggleCollapse,\n}: TerminalOutputProps) {\n  return (\n    <Collapsible open={!isCollapsed}>\n      <div\n        className={cn(\n          \"relative font-mono text-sm\",\n          isCollapsed && \"max-h-[200px] overflow-hidden\",\n        )}\n      >\n        <div className=\"overflow-x-auto p-4\">\n          {stdout && (\n            <div className=\"text-foreground whitespace-pre\">\n              <Ansi>{stdout}</Ansi>\n            </div>\n          )}\n          {stderr && (\n            <div className=\"mt-2 whitespace-pre text-red-500 dark:text-red-400\">\n              <Ansi>{stderr}</Ansi>\n            </div>\n          )}\n          {truncated && (\n            <div className=\"text-muted-foreground mt-2 text-xs italic\">\n              Output truncated...\n            </div>\n          )}\n        </div>\n\n        {isCollapsed && (\n          <div className=\"from-card absolute inset-x-0 bottom-0 h-16 bg-gradient-to-t to-transparent\" />\n        )}\n      </div>\n\n      {shouldCollapse && (\n        <CollapsibleTrigger asChild>\n          <Button\n            variant=\"ghost\"\n            onClick={onToggleCollapse}\n            className=\"text-muted-foreground w-full rounded-none border-t font-normal\"\n          >\n            {isCollapsed ? (\n              <>\n                <ChevronDown className=\"mr-1 size-4\" />\n                Show all {lineCount} lines\n              </>\n            ) : (\n              <>\n                <ChevronUp className=\"mr-1 size-4\" />\n                Collapse\n              </>\n            )}\n          </Button>\n        </CollapsibleTrigger>\n      )}\n    </Collapsible>\n  );\n}\n\nfunction TerminalEmpty() {\n  return (\n    <div className=\"text-muted-foreground px-4 py-3 font-mono text-sm italic\">\n      No output\n    </div>\n  );\n}\n\nfunction TerminalRoot({\n  id,\n  command,\n  stdout,\n  stderr,\n  exitCode,\n  durationMs,\n  cwd,\n  truncated,\n  maxCollapsedLines,\n  className,\n  expanded,\n  defaultExpanded = false,\n  onExpandedChange,\n}: TerminalRootProps) {\n  const [uncontrolledExpanded, setUncontrolledExpanded] =\n    useState(defaultExpanded);\n  const { copiedId, copy } = useCopyToClipboard();\n\n  const isExpanded = expanded ?? uncontrolledExpanded;\n  const hasOutput = Boolean(stdout || stderr);\n  const fullOutput = [stdout, stderr].filter(Boolean).join(\"\\n\");\n  const formattedDuration = formatDuration(durationMs);\n  const lineCount = countOutputLines(fullOutput);\n  const shouldCollapse =\n    maxCollapsedLines !== undefined && lineCount > maxCollapsedLines;\n  const isCollapsed = shouldCollapse && !isExpanded;\n\n  const setExpanded = useCallback(\n    (nextExpanded: boolean) => {\n      if (expanded === undefined) {\n        setUncontrolledExpanded(nextExpanded);\n      }\n      onExpandedChange?.(nextExpanded);\n    },\n    [expanded, onExpandedChange],\n  );\n\n  const handleCopy = useCallback(() => {\n    if (!hasOutput) return;\n    copy(fullOutput, COPY_ID);\n  }, [hasOutput, fullOutput, copy]);\n\n  return (\n    <div\n      className={cn(\n        \"@container flex w-full min-w-80 flex-col gap-3\",\n        className,\n      )}\n      data-tool-ui-id={id}\n      data-slot=\"terminal\"\n    >\n      <div className=\"border-border bg-card overflow-hidden rounded-lg border shadow-xs\">\n        <TerminalHeader\n          command={command}\n          cwd={cwd}\n          exitCode={exitCode}\n          formattedDuration={formattedDuration}\n          hasOutput={hasOutput}\n          copiedId={copiedId}\n          onCopy={handleCopy}\n        />\n\n        {hasOutput && (\n          <TerminalOutput\n            stdout={stdout}\n            stderr={stderr}\n            truncated={truncated}\n            isCollapsed={isCollapsed}\n            shouldCollapse={shouldCollapse}\n            lineCount={lineCount}\n            onToggleCollapse={() => setExpanded(!isExpanded)}\n          />\n        )}\n\n        {!hasOutput && <TerminalEmpty />}\n      </div>\n    </div>\n  );\n}\n\ntype TerminalComponent = typeof TerminalRoot & {\n  Header: typeof TerminalHeader;\n  Output: typeof TerminalOutput;\n  Empty: typeof TerminalEmpty;\n};\n\nexport const Terminal = Object.assign(TerminalRoot, {\n  Header: TerminalHeader,\n  Output: TerminalOutput,\n  Empty: TerminalEmpty,\n}) as TerminalComponent;\n"
  },
  {
    "path": "apps/www/components/tool-ui/video/README.md",
    "content": "# Video\n\nImplementation for the \"video\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/video/index.ts\n- serializable schema + parse helpers: components/tool-ui/video/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/video/content.mdx\n- Preset payload: lib/presets/video.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/video/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n */\n\"use client\";\n\nexport { cn } from \"@/lib/utils\";\nexport { Button } from \"@/components/ui/button\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/video/context.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\n\nexport interface VideoPlaybackState {\n  playing: boolean;\n  muted: boolean;\n}\n\nexport interface VideoContextValue {\n  state: VideoPlaybackState;\n  setState: (patch: Partial<VideoPlaybackState>) => void;\n  videoElement: HTMLVideoElement | null;\n  setVideoElement: (node: HTMLVideoElement | null) => void;\n}\n\nconst VideoContext = React.createContext<VideoContextValue | null>(null);\n\nexport function useVideo() {\n  const ctx = React.use(VideoContext);\n  if (!ctx) {\n    throw new Error(\"useVideo must be used within a <VideoProvider />\");\n  }\n  return ctx;\n}\n\nexport interface VideoProviderProps {\n  children: React.ReactNode;\n  defaultState?: Partial<VideoPlaybackState>;\n}\n\nexport function VideoProvider({ children, defaultState }: VideoProviderProps) {\n  const [state, setStateInternal] = React.useState<VideoPlaybackState>({\n    playing: defaultState?.playing ?? false,\n    muted: defaultState?.muted ?? true,\n  });\n\n  const [videoElement, setVideoElement] =\n    React.useState<HTMLVideoElement | null>(null);\n\n  const setState = React.useCallback((patch: Partial<VideoPlaybackState>) => {\n    setStateInternal((prev) => ({ ...prev, ...patch }));\n  }, []);\n\n  const value = React.useMemo(\n    () => ({ state, setState, videoElement, setVideoElement }),\n    [state, setState, videoElement],\n  );\n\n  return (\n    <VideoContext.Provider value={value}>{children}</VideoContext.Provider>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/video/index.ts",
    "content": "export { Video } from \"./video\";\nexport type { VideoProps } from \"./video\";\nexport type { SerializableVideo, Source } from \"./schema\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/video/schema.ts",
    "content": "import { z } from \"zod\";\nimport { defineToolUiContract } from \"../shared/contract\";\nimport {\n  ToolUIIdSchema,\n  ToolUIReceiptSchema,\n  ToolUIRoleSchema,\n} from \"../shared/schema\";\n\nimport { AspectRatioSchema, MediaFitSchema } from \"../shared/media\";\n\nexport const SourceSchema = z.object({\n  label: z.string(),\n  iconUrl: z.url().optional(),\n  url: z.url().optional(),\n});\n\nexport type Source = z.infer<typeof SourceSchema>;\n\nexport const SerializableVideoSchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  receipt: ToolUIReceiptSchema.optional(),\n  assetId: z.string(),\n  src: z.url(),\n  poster: z.url().optional(),\n  title: z.string().optional(),\n  description: z.string().optional(),\n  href: z.url().optional(),\n  domain: z.string().optional(),\n  durationMs: z.number().int().positive().optional(),\n  ratio: AspectRatioSchema.optional(),\n  fit: MediaFitSchema.optional(),\n  createdAt: z.string().datetime().optional(),\n  locale: z.string().optional(),\n  source: SourceSchema.optional(),\n});\n\nexport type SerializableVideo = z.infer<typeof SerializableVideoSchema>;\n\nconst SerializableVideoSchemaContract = defineToolUiContract(\n  \"Video\",\n  SerializableVideoSchema,\n);\n\nexport const parseSerializableVideo: (input: unknown) => SerializableVideo =\n  SerializableVideoSchemaContract.parse;\n\nexport const safeParseSerializableVideo: (\n  input: unknown,\n) => SerializableVideo | null = SerializableVideoSchemaContract.safeParse;\n"
  },
  {
    "path": "apps/www/components/tool-ui/video/video-helpers.ts",
    "content": "import type { SerializableVideo } from \"./schema\";\nimport { resolveSafeNavigationHref, sanitizeHref } from \"../shared/media\";\n\nexport type VideoMediaEvent = \"mute\" | \"unmute\";\n\nexport interface ResolvedVideoNavigation {\n  sanitizedHref: string | undefined;\n  sanitizedSourceUrl: string | undefined;\n  primaryHref: string | undefined;\n}\n\nexport function getMuteMediaEvent(\n  previousMuted: boolean,\n  nextMuted: boolean,\n): VideoMediaEvent | null {\n  if (previousMuted === nextMuted) {\n    return null;\n  }\n\n  return nextMuted ? \"mute\" : \"unmute\";\n}\n\nexport function resolveVideoNavigation(\n  rawHref: string | undefined,\n  rawSourceUrl: string | undefined,\n): ResolvedVideoNavigation {\n  const sanitizedHref = sanitizeHref(rawHref);\n  const sanitizedSourceUrl = sanitizeHref(rawSourceUrl);\n\n  return {\n    sanitizedHref,\n    sanitizedSourceUrl,\n    primaryHref: resolveSafeNavigationHref(sanitizedHref, sanitizedSourceUrl),\n  };\n}\n\nexport function normalizeVideoDataForCallback(\n  video: SerializableVideo,\n  normalized: {\n    ratio: NonNullable<SerializableVideo[\"ratio\"]>;\n    fit: NonNullable<SerializableVideo[\"fit\"]>;\n    locale: string;\n    sanitizedHref: string | undefined;\n    sanitizedSourceUrl: string | undefined;\n  },\n): SerializableVideo {\n  return {\n    ...video,\n    ratio: normalized.ratio,\n    fit: normalized.fit,\n    href: normalized.sanitizedHref,\n    source: video.source\n      ? { ...video.source, url: normalized.sanitizedSourceUrl }\n      : undefined,\n    locale: normalized.locale,\n  };\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/video/video.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ExternalLink, Play } from \"lucide-react\";\nimport { cn, Button } from \"./_adapter\";\n\nimport {\n  formatDuration,\n  getFitClass,\n  openSafeNavigationHref,\n  OVERLAY_GRADIENT,\n  RATIO_CLASS_MAP,\n} from \"../shared/media\";\nimport { VideoProvider, useVideo } from \"./context\";\nimport type { SerializableVideo } from \"./schema\";\nimport {\n  getMuteMediaEvent,\n  normalizeVideoDataForCallback,\n  resolveVideoNavigation,\n} from \"./video-helpers\";\n\nconst FALLBACK_LOCALE = \"en-US\";\n\nexport interface VideoProps extends SerializableVideo {\n  className?: string;\n  // Keep behavior flags intentionally minimal; prefer explicit variants over more booleans.\n  autoPlay?: boolean;\n  defaultMuted?: boolean;\n  onNavigate?: (href: string, video: SerializableVideo) => void;\n  onMediaEvent?: (type: \"play\" | \"pause\" | \"mute\" | \"unmute\") => void;\n}\n\nfunction VideoRoot(props: VideoProps) {\n  const { defaultMuted = true, ...rest } = props;\n\n  return (\n    <VideoProvider defaultState={{ muted: defaultMuted }}>\n      <VideoInner {...rest} />\n    </VideoProvider>\n  );\n}\n\nfunction VideoInner(props: Omit<VideoProps, \"defaultMuted\">) {\n  const {\n    className,\n    autoPlay = true,\n    onNavigate,\n    onMediaEvent,\n    ...serializable\n  } = props;\n\n  const {\n    id,\n    src,\n    poster,\n    title,\n    description,\n    href: rawHref,\n    domain,\n    durationMs,\n    ratio = \"16:9\",\n    fit = \"cover\",\n    createdAt,\n    source,\n    locale: providedLocale,\n  } = serializable;\n\n  const locale = providedLocale ?? FALLBACK_LOCALE;\n  const { sanitizedHref, sanitizedSourceUrl, primaryHref } =\n    resolveVideoNavigation(rawHref, source?.url);\n\n  const videoData: SerializableVideo = normalizeVideoDataForCallback(\n    serializable,\n    {\n      ratio,\n      fit,\n      locale,\n      sanitizedHref,\n      sanitizedSourceUrl,\n    },\n  );\n\n  const { state, setState, setVideoElement } = useVideo();\n  const videoRef = React.useRef<HTMLVideoElement | null>(null);\n  const previousMutedRef = React.useRef(state.muted);\n\n  React.useEffect(() => {\n    setVideoElement(videoRef.current);\n    return () => setVideoElement(null);\n  }, [setVideoElement]);\n\n  React.useEffect(() => {\n    const video = videoRef.current;\n    if (!video) return;\n    if (video.muted !== state.muted) {\n      video.muted = state.muted;\n    }\n  }, [state.muted]);\n\n  React.useEffect(() => {\n    const video = videoRef.current;\n    if (!video) return;\n    if (state.playing && video.paused) {\n      void video.play().catch(() => undefined);\n    } else if (!state.playing && !video.paused) {\n      video.pause();\n    }\n  }, [state.playing]);\n\n  const navigate = (targetHref: string) => {\n    if (onNavigate) {\n      onNavigate(targetHref, videoData);\n    } else {\n      openSafeNavigationHref(targetHref);\n    }\n  };\n\n  const handleWatch = (event: React.MouseEvent<HTMLButtonElement>) => {\n    event.preventDefault();\n    event.stopPropagation();\n    const video = videoRef.current;\n    if (!video) return;\n    if (video.paused) {\n      void video.play().catch(() => undefined);\n    } else {\n      video.pause();\n    }\n  };\n\n  const handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => {\n    event.preventDefault();\n    event.stopPropagation();\n    if (!primaryHref) return;\n    navigate(primaryHref);\n  };\n\n  const sourceLabel = source?.label;\n  const metadataDomain = domain && domain !== sourceLabel ? domain : undefined;\n  const hasMetadata = Boolean(\n    description || sourceLabel || metadataDomain || durationMs || createdAt,\n  );\n  const hasOverlay = Boolean(title || primaryHref);\n\n  return (\n    <article\n      className={cn(\"relative w-full min-w-80 max-w-md\", className)}\n      lang={locale}\n      data-tool-ui-id={id}\n      data-slot=\"video\"\n    >\n      <div\n        className={cn(\n          \"group @container relative isolate flex w-full min-w-0 flex-col overflow-hidden rounded-xl\",\n          \"border border-border bg-card text-sm shadow-xs\",\n        )}\n      >\n        <div\n          className={cn(\n            \"group relative w-full overflow-hidden bg-black\",\n            ratio !== \"auto\" ? RATIO_CLASS_MAP[ratio] : \"aspect-video\",\n          )}\n        >\n          <video\n            ref={videoRef}\n            className={cn(\n              \"relative z-10 h-full w-full transition-transform duration-200 group-hover:scale-[1.01]\",\n              getFitClass(fit),\n              ratio !== \"auto\" && \"absolute inset-0 h-full w-full\",\n            )}\n            src={src}\n            poster={poster}\n            controls\n            playsInline\n            autoPlay={autoPlay}\n            preload=\"metadata\"\n            muted={state.muted}\n            onPlay={() => {\n              setState({ playing: true });\n              onMediaEvent?.(\"play\");\n            }}\n            onPause={() => {\n              setState({ playing: false });\n              onMediaEvent?.(\"pause\");\n            }}\n            onVolumeChange={(event) => {\n              const target = event.currentTarget;\n              setState({ muted: target.muted });\n              const mediaEvent = getMuteMediaEvent(\n                previousMutedRef.current,\n                target.muted,\n              );\n              previousMutedRef.current = target.muted;\n              if (mediaEvent) {\n                onMediaEvent?.(mediaEvent);\n              }\n            }}\n          />\n          {hasOverlay && (\n            <>\n              <div\n                className=\"pointer-events-none absolute inset-x-0 top-0 z-20 h-32 opacity-0 transition-opacity duration-200 group-hover:opacity-100 group-focus-within:opacity-100\"\n                style={{ backgroundImage: OVERLAY_GRADIENT }}\n              />\n              <div className=\"absolute inset-x-0 top-0 z-30 flex items-start justify-between gap-2 px-5 pt-4 opacity-0 transition-opacity duration-200 group-hover:opacity-100 group-focus-within:opacity-100\">\n                {title ? (\n                  <div className=\"line-clamp-2 max-w-[70%] font-semibold text-white drop-shadow-sm\">\n                    {title}\n                  </div>\n                ) : (\n                  <span className=\"sr-only\">Video controls</span>\n                )}\n                <div className=\"flex items-center gap-2\">\n                  {primaryHref && (\n                    <Button\n                      variant=\"secondary\"\n                      size=\"sm\"\n                      onClick={handleOpen}\n                      className=\"bg-black/55 text-white hover:bg-black/70\"\n                    >\n                      <ExternalLink\n                        className=\"mr-1 h-4 w-4\"\n                        aria-hidden=\"true\"\n                      />\n                      Open\n                    </Button>\n                  )}\n                  <Button\n                    variant=\"default\"\n                    size=\"sm\"\n                    onClick={handleWatch}\n                    className=\"shadow-sm\"\n                  >\n                    <Play className=\"mr-1 h-4 w-4\" aria-hidden=\"true\" />\n                    Watch\n                  </Button>\n                </div>\n              </div>\n            </>\n          )}\n        </div>\n\n        {hasMetadata && (\n          <div className=\"flex flex-col gap-1.5 px-4 py-3\">\n            {description && (\n              <p className=\"text-foreground line-clamp-2 text-sm leading-snug\">\n                {description}\n              </p>\n            )}\n            <div className=\"text-muted-foreground flex flex-wrap items-center gap-x-3 gap-y-1 text-xs\">\n              {sourceLabel && <span>{sourceLabel}</span>}\n              {metadataDomain && <span>{metadataDomain}</span>}\n              {typeof durationMs === \"number\" && (\n                <span>{formatDuration(durationMs)}</span>\n              )}\n              {createdAt && (\n                <time dateTime={createdAt}>\n                  {formatCreatedAt(createdAt, locale)}\n                </time>\n              )}\n            </div>\n          </div>\n        )}\n      </div>\n    </article>\n  );\n}\n\nfunction formatCreatedAt(createdAt: string, locale: string): string {\n  const date = new Date(createdAt);\n  if (Number.isNaN(date.getTime())) {\n    return createdAt;\n  }\n\n  return new Intl.DateTimeFormat(locale, { dateStyle: \"medium\" }).format(date);\n}\n\ntype VideoComponent = typeof VideoRoot & {\n  Root: typeof VideoRoot;\n};\n\nexport const Video = Object.assign(VideoRoot, {\n  Root: VideoRoot,\n}) as VideoComponent;\n"
  },
  {
    "path": "apps/www/components/tool-ui/weather-widget/generated/weather-runtime-core.generated.ts",
    "content": "// @ts-nocheck\n// AUTO-GENERATED by `pnpm weather:compile`.\n// Source: lib/weather-authoring/weather-widget/weather-runtime-core.ts\n// DO NOT EDIT MANUALLY.\n\nimport{useMemo as ye,useState as ft,useEffect as dt}from\"react\";var Ze=8,j=new Set;function he(e){return j.has(e)?!0:j.size>=Ze?!1:(j.add(e),!0)}function ae(e){j.delete(e)}function ge(e,t){return t&&(ae(e),null)}import{useCallback as N,useEffect as Ke,useRef as v}from\"react\";var O=`#version 300 es\nin vec4 a_position;out vec2 v_uv;void main(){gl_Position=a_position;v_uv=a_position.xy*0.5+0.5;}`,se=`#version 300 es\nprecision highp float;in vec2 v_uv;out vec4 fragColor;uniform float u_time;uniform vec2 u_resolution;uniform float u_timeOfDay;uniform float u_moonPhase;uniform float u_starDensity;uniform vec2 u_celestialPos;uniform float u_sunSize;uniform float u_moonSize;uniform float u_sunGlowIntensity;uniform float u_sunGlowSize;uniform float u_sunRayCount;uniform float u_sunRayLength;uniform float u_sunRayIntensity;uniform float u_sunRayShimmer;uniform float u_sunRayShimmerSpeed;uniform float u_moonGlowIntensity;uniform float u_moonGlowSize;uniform float u_skyBrightness;uniform float u_skySaturation;uniform float u_skyContrast;uniform sampler2D u_moonTexture;uniform bool u_hasMoonTexture;\n#define PI 3.14159265359\n#define GODRAY_MAX_SAMPLES 32\n#define TAU 6.28318530718\nfloat hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453123);}vec2 hash2(vec2 p){p=vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3)));return fract(sin(p)*43758.5453);}float noise(vec2 p){vec2 i=floor(p);vec2 f=fract(p);f=f*f*(3.0-2.0*f);float a=hash(i);float b=hash(i+vec2(1.0,0.0));float c=hash(i+vec2(0.0,1.0));float d=hash(i+vec2(1.0,1.0));return mix(mix(a,b,f.x),mix(c,d,f.x),f.y);}float fbm(vec2 p,int octaves){float value=0.0;float amplitude=0.5;float frequency=1.0;for(int i=0;i<6;i++){if(i>=octaves)break;value+=amplitude*noise(p*frequency);frequency*=2.0;amplitude*=0.5;}return value;}float getSunY(float timeOfDay,float baseY){float belowHorizon=-0.25;float riseProgress=smoothstep(0.18,0.32,timeOfDay);float setProgress=smoothstep(0.68,0.82,timeOfDay);float visible=riseProgress*(1.0-setProgress);return mix(belowHorizon,baseY,visible);}float getMoonY(float timeOfDay,float baseY){float belowHorizon=-0.25;float risingEvening=smoothstep(0.74,0.88,timeOfDay);float settingMorning=1.0-smoothstep(0.12,0.26,timeOfDay);float visible=max(risingEvening,settingMorning);return mix(belowHorizon,baseY,visible);}float getHorizonFade(float y){return smoothstep(-0.2,0.0,y);}vec3 getSkyColor(vec2 uv,float timeOfDay){vec3 dayTop=vec3(0.4,0.6,0.9);vec3 dayHorizon=vec3(0.7,0.8,0.95);vec3 sunsetTop=vec3(0.2,0.2,0.4);vec3 sunsetHorizon=vec3(0.9,0.5,0.2);vec3 nightTop=vec3(0.02,0.02,0.05);vec3 nightHorizon=vec3(0.05,0.05,0.1);float dayAmount=smoothstep(0.25,0.4,timeOfDay)*smoothstep(0.75,0.6,timeOfDay);float sunsetAmount=max(smoothstep(0.2,0.3,timeOfDay)*smoothstep(0.4,0.3,timeOfDay),smoothstep(0.6,0.7,timeOfDay)*smoothstep(0.8,0.7,timeOfDay));float nightAmount=max(0.0,1.0-dayAmount-sunsetAmount);float gradientFactor=pow(1.0-uv.y,1.0);vec3 topColor=dayTop*dayAmount+sunsetTop*sunsetAmount+nightTop*nightAmount;vec3 horizonColor=dayHorizon*dayAmount+sunsetHorizon*sunsetAmount+nightHorizon*nightAmount;vec3 avgColor=(topColor+horizonColor)*0.5;topColor=mix(avgColor,topColor,u_skyContrast);horizonColor=mix(avgColor,horizonColor,u_skyContrast);vec3 color=mix(topColor,horizonColor,gradientFactor);color*=u_skyBrightness;float gray=dot(color,vec3(0.299,0.587,0.114));if(u_skySaturation<=1.0){color=mix(vec3(gray),color,u_skySaturation);}else{float boost=u_skySaturation-1.0;color=color+(color-vec3(gray))*boost;}return clamp(color,0.0,1.0);}float drawStars(vec2 uv,float density,float time){float stars=0.0;for(int layer=0;layer<3;layer++){float layerScale=100.0+float(layer)*50.0;vec2 gridUV=uv*layerScale;vec2 gridID=floor(gridUV);vec2 gridFract=fract(gridUV);vec2 starPos=hash2(gridID+float(layer)*100.0);float dist=length(gridFract-starPos);float starPresent=step(1.0-density*0.3,hash(gridID*(float(layer)+1.0)));float starSize=0.02+hash(gridID.yx)*0.03;float twinkle=sin(time*(2.0+hash(gridID)*3.0)+hash(gridID.yx)*TAU)*0.3+0.7;float star=smoothstep(starSize,0.0,dist)*starPresent*twinkle;star*=1.0-float(layer)*0.3;stars+=star;}return stars;}vec3 drawSun(vec2 uv,vec2 sunPos,float size){vec2 aspect=vec2(u_resolution.x/u_resolution.y,1.0);vec2 diff=(uv-sunPos)*aspect;float dist=length(diff);float angle=atan(diff.y,diff.x);float disc=1.0-smoothstep(size*0.9,size,dist);vec3 sunCore=vec3(1.0,1.0,0.95);vec3 sunEdge=vec3(1.0,0.9,0.4);float edgeFactor=clamp(dist/size,0.0,1.0);vec3 sunColor=mix(sunCore,sunEdge,edgeFactor);float limbDarkening=1.0-pow(clamp(dist/size,0.0,1.0),2.0)*0.3;sunColor*=limbDarkening;float glowSize=max(0.1,u_sunGlowSize);float scaledDist=dist/glowSize;float glow1=exp(-scaledDist*8.0)*0.5;float glow2=exp(-scaledDist*3.0)*0.3;float glow3=exp(-scaledDist*1.5)*0.15;vec3 glowColor=vec3(1.0,0.8,0.4);float glowTotal=(glow1+glow2+glow3)*u_sunGlowIntensity;vec3 result=sunColor*disc*2.0;result+=glowColor*glowTotal;float ringCenter=size*1.15;float ringWidth=max(size*0.35,0.001);float ringMask=smoothstep(size*0.85,size*1.05,dist);ringMask*=1.0-smoothstep(size*5.0,size*9.0,dist);float chromaShift=size*(0.012+u_sunRayIntensity*0.06);chromaShift*=smoothstep(size*0.9,size*2.4,dist);float ringR=exp(-pow((dist-chromaShift-ringCenter)/ringWidth,2.0));float ringG=exp(-pow((dist-ringCenter)/ringWidth,2.0));float ringB=exp(-pow((dist+chromaShift-ringCenter)/ringWidth,2.0));float ringT=clamp((dist-size)/(size*2.2),0.0,1.0);vec3 ringSpectral=0.55+0.45*cos(TAU*(ringT+vec3(0.0,0.33,0.67)));ringSpectral=clamp(ringSpectral,0.0,1.0);vec3 ringColor=mix(vec3(1.0),ringSpectral,0.45);float ringIntensity=(ringR+ringG+ringB)/3.0;ringIntensity*=ringMask*u_sunGlowIntensity*0.025;result+=ringColor*ringIntensity;if(u_sunRayCount>0.0&&u_sunRayIntensity>0.0){if(dist<size*3.6){float motion=clamp(u_sunRayShimmer,0.0,5.0);float t=u_time*max(0.0,u_sunRayShimmerSpeed);float rayPhase=angle*u_sunRayCount;float rayIndex=floor(rayPhase/PI+0.5);float raySeed=hash(vec2(rayIndex,19.17));float major=pow(abs(cos(rayPhase)),10.0);float minor=pow(abs(cos(rayPhase*2.0+raySeed*2.3)),22.0)*0.18;float rayShape=max(major,minor);float breathe=1.0+(noise(vec2(t*0.05,raySeed*7.0))-0.5)*(0.08*motion);float shimmer=1.0+(noise(vec2(dist*12.0-t*0.25,raySeed*23.0))-0.5)*(0.12*motion);float micro=1.0+(noise(vec2(t*0.6,rayPhase*0.8))-0.5)*(0.06*motion);float rayNoise=0.72+0.28*noise(vec2(rayPhase*0.35,t*0.12+raySeed*10.0));float rayPattern=rayShape*rayNoise;float rayStart=smoothstep(size*0.75,size*1.25,dist);float rayEnd=smoothstep(size*(3.0*breathe),size*1.5,dist);float rayLengthVar=0.75+raySeed*0.55;float maxRayDist=max(0.001,u_sunRayLength*0.15);float rayFalloff=exp(-dist*dist/(maxRayDist*maxRayDist*rayLengthVar*breathe));float rays=rayPattern*rayFalloff*rayStart*rayEnd*u_sunRayIntensity;rays*=shimmer*micro;float prismMask=smoothstep(size*1.05,size*2.6,dist);float rayChroma=size*(0.01+u_sunRayIntensity*0.05)*prismMask;float distR=max(0.0,dist-rayChroma);float distB=dist+rayChroma;float falloffR=exp(-distR*distR/(maxRayDist*maxRayDist*rayLengthVar*breathe));float falloffG=rayFalloff;float falloffB=exp(-distB*distB/(maxRayDist*maxRayDist*rayLengthVar*breathe));vec3 rayRGB=vec3(falloffR,falloffG,falloffB)*rayPattern*rayStart*rayEnd;float rayAvg=(rayRGB.r+rayRGB.g+rayRGB.b)/3.0;vec3 rayChromaColor=rayRGB/max(rayAvg,1e-4);float rayT=clamp((dist-size)/(size*2.6),0.0,1.0);vec3 raySpectral=0.55+0.45*cos(TAU*(rayT+vec3(0.0,0.33,0.67)));raySpectral=clamp(raySpectral,0.0,1.0);raySpectral=mix(vec3(1.0),raySpectral,0.28);vec3 rayWarm=vec3(1.0,0.92,0.7);float prismMix=clamp(0.08+u_sunRayIntensity*0.6,0.0,0.45)*prismMask;vec3 rayColor=mix(rayWarm,rayChromaColor,prismMix);rayColor=mix(rayColor,raySpectral,prismMix*0.65);result+=rayColor*rays;}}return result;}vec3 getSphereNormal(vec2 discUV){float r2=dot(discUV,discUV);if(r2>1.0)return vec3(0.0);float z=sqrt(1.0-r2);return normalize(vec3(discUV.x,discUV.y,z));}vec2 sphereToEquirectangular(vec3 normal){float longitude=atan(normal.x,normal.z);float u=longitude/TAU+0.5;float latitude=asin(clamp(normal.y,-1.0,1.0));float v=latitude/PI+0.5;return vec2(u,v);}vec3 getMoonSurfaceColor(vec3 normal,vec2 discUV){if(u_hasMoonTexture){vec2 texUV=sphereToEquirectangular(normal);return texture(u_moonTexture,texUV).rgb;}float brightness=0.7+fbm(discUV*5.0,3)*0.3;return vec3(brightness*0.85,brightness*0.83,brightness*0.8);}vec4 drawMoon(vec2 uv,vec2 moonPos,float size,float phase){vec2 aspect=vec2(u_resolution.x/u_resolution.y,1.0);vec2 diff=(uv-moonPos)*aspect;float dist=length(diff);vec2 discUV=diff/size;float discDist=length(discUV);float disc=1.0-smoothstep(0.95,1.0,discDist);float glowSize=max(0.1,u_moonGlowSize);float glowIntensity=u_moonGlowIntensity;if(disc<0.001){float scaledDist=dist/glowSize;float glow1=exp(-scaledDist*6.0)*0.15;float glow2=exp(-scaledDist*2.0)*0.06;vec3 glowColor=vec3(0.8,0.85,0.95);float phaseAngle=phase*TAU;vec3 sunDir=vec3(sin(phaseAngle),0.0,-cos(phaseAngle));float glowPhase=max(0.2,dot(normalize(vec3(discUV,0.5)),sunDir)*0.5+0.5);return vec4(glowColor*(glow1+glow2)*glowPhase*glowIntensity,0.0);}vec3 normal=getSphereNormal(discUV);float phaseAngle=phase*TAU;vec3 sunDir=vec3(sin(phaseAngle),0.0,-cos(phaseAngle));float NdotL=dot(normal,sunDir);float terminator=smoothstep(-0.02,0.08,NdotL);vec3 baseColor=getMoonSurfaceColor(normal,discUV);vec3 ambient=baseColor*0.03;vec3 lit=baseColor*terminator;vec3 moonSurface=ambient+lit;float limbDarkening=1.0-pow(discDist,3.0)*0.15;moonSurface*=limbDarkening;float rimLight=pow(1.0-abs(NdotL),4.0)*terminator*0.1;moonSurface+=vec3(1.0,0.98,0.95)*rimLight;float scaledDist=dist/glowSize;float glow1=exp(-scaledDist*6.0)*0.12;float glow2=exp(-scaledDist*2.0)*0.06;vec3 glowColor=vec3(0.8,0.85,0.95);float litAmount=max(0.1,terminator);vec3 glow=glowColor*(glow1+glow2)*litAmount*glowIntensity;return vec4(moonSurface*disc+glow,disc);}void main(){vec2 uv=v_uv;vec3 color=getSkyColor(uv,u_timeOfDay);float sunY=getSunY(u_timeOfDay,u_celestialPos.y);float moonY=getMoonY(u_timeOfDay,u_celestialPos.y);vec2 sunPos=vec2(u_celestialPos.x,sunY);vec2 moonPos=vec2(u_celestialPos.x,moonY);float moonFade=getHorizonFade(moonY);if(moonFade>0.01){float stars=drawStars(uv,u_starDensity,u_time);color+=vec3(stars)*moonFade;}float sunFade=getHorizonFade(sunY);if(sunFade>0.01){vec3 sun=drawSun(uv,sunPos,u_sunSize);color+=sun*sunFade;}if(moonFade>0.01){vec4 moon=drawMoon(uv,moonPos,u_moonSize,u_moonPhase);float alpha=moon.a*moonFade;color=mix(color,moon.rgb,alpha)+moon.rgb*(1.0-moon.a)*moonFade;}fragColor=vec4(color,0.0);}`,oe=`#version 300 es\nprecision highp float;in vec2 v_uv;out vec4 fragColor;uniform float u_time;uniform vec2 u_resolution;uniform sampler2D u_sceneTexture;uniform float u_timeOfDay;uniform float u_coverage;uniform float u_density;uniform float u_softness;uniform float u_windSpeed;uniform float u_windAngle;uniform float u_turbulence;uniform float u_lightIntensity;uniform float u_ambientDarkness;uniform int u_numLayers;uniform float u_cloudScale;uniform vec2 u_celestialPos;uniform float u_celestialSize;uniform float u_celestialBrightness;uniform float u_backlightIntensity;\n#define PI 3.14159265359\nfloat hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453123);}float noise(vec2 p){vec2 i=floor(p);vec2 f=fract(p);f=f*f*(3.0-2.0*f);float a=hash(i);float b=hash(i+vec2(1.0,0.0));float c=hash(i+vec2(0.0,1.0));float d=hash(i+vec2(1.0,1.0));return mix(mix(a,b,f.x),mix(c,d,f.x),f.y);}float fbm(vec2 p,int octaves){float value=0.0;float amplitude=0.5;for(int i=0;i<8;i++){if(i>=octaves)break;value+=amplitude*noise(p);p*=2.0;amplitude*=0.5;}return value;}float cloudLayer(vec2 uv,float time,float layerSeed,float speed,float turbAmount){vec2 wind=vec2(cos(u_windAngle),sin(u_windAngle))*speed*time;float layerScale=(1.8+hash(vec2(layerSeed,0.0))*1.2)*u_cloudScale;float layerRotation=hash(vec2(layerSeed,1.0))*0.5-0.25;vec2 layerOffset=vec2(hash(vec2(layerSeed,2.0))*100.0,hash(vec2(layerSeed,3.0))*100.0);float c=cos(layerRotation);float s=sin(layerRotation);vec2 rotatedUV=vec2(uv.x*c-uv.y*s,uv.x*s+uv.y*c);vec2 p=rotatedUV*layerScale+wind+layerOffset;float turbSeed=layerSeed*50.0;vec2 turbOffset=vec2(fbm(p*0.5+time*0.1+turbSeed,4),fbm(p*0.5+turbSeed+100.0+time*0.1,4))*turbAmount;float n=fbm(p+turbOffset,6);return n;}vec3 cloudLighting(float density,float heightInCloud,float sunAlt,float warmth,float nightFactor,vec2 uv){float daylight=smoothstep(-0.12,0.1,sunAlt);vec3 dayLitColor=vec3(1.0,0.98,0.96);vec3 sunsetLitColor=vec3(1.0,0.7,0.45);vec3 nightLitColor=vec3(0.12,0.14,0.2);vec3 litColor=mix(dayLitColor,sunsetLitColor,warmth);litColor=mix(litColor,nightLitColor,nightFactor);vec3 dayShadowColor=vec3(0.45,0.5,0.6);vec3 sunsetShadowColor=vec3(0.35,0.25,0.3);vec3 nightShadowColor=vec3(0.03,0.04,0.07);vec3 shadowColor=mix(dayShadowColor,sunsetShadowColor,warmth);shadowColor=mix(shadowColor,nightShadowColor,nightFactor);shadowColor*=(1.0-u_ambientDarkness*0.3);float topLight=heightInCloud*max(0.0,sunAlt);float sideLight=(1.0-abs(heightInCloud-0.5)*2.0)*(1.0-sunAlt*0.5);float bottomLight=(1.0-heightInCloud)*warmth*0.5;float ambientLight=mix(0.03,0.2,daylight);float lightAmount=(topLight*0.5+sideLight*0.3+bottomLight)*daylight+ambientLight;lightAmount=clamp(lightAmount*u_lightIntensity,0.0,1.0);vec3 cloudColor=mix(shadowColor,litColor,lightAmount);float rimLight=pow(density,0.5)*(1.0-density)*4.0;vec3 rimColor=mix(vec3(1.0,1.0,0.95),vec3(1.0,0.8,0.5),warmth);rimColor=mix(rimColor,vec3(0.15,0.18,0.25),nightFactor);float rimStrength=mix(0.1,0.3,daylight);cloudColor+=rimColor*rimLight*rimStrength*u_lightIntensity;float hotSpot=pow(max(0.0,lightAmount-0.6)*2.5,2.0)*warmth*daylight;cloudColor+=vec3(1.0,0.5,0.2)*hotSpot*0.4;float aspect=u_resolution.x/u_resolution.y;vec2 celestialUV=u_celestialPos;vec2 diff=(uv-celestialUV)*vec2(aspect,1.0);float distToCelestial=length(diff);float transmission=pow(1.0-density,2.0);float glowRadius=u_celestialSize*6.0;float proximityGlow=exp(-distToCelestial*distToCelestial/(glowRadius*glowRadius));float backlight=proximityGlow*transmission*u_celestialBrightness;float edgeDist=u_celestialSize*3.0;float nearCelestial=smoothstep(edgeDist*2.5,edgeDist*0.3,distToCelestial);float edgeFactor=density*(1.0-density)*4.0;float silverLining=nearCelestial*edgeFactor*u_celestialBrightness;vec3 backlightColor=mix(vec3(1.0,0.9,0.7),vec3(0.7,0.75,0.9),nightFactor);cloudColor+=backlightColor*(backlight*0.5+silverLining*0.8)*u_backlightIntensity;return cloudColor;}void main(){vec2 uv=v_uv;vec4 scene=texture(u_sceneTexture,uv);float sunAlt=u_timeOfDay<0.5?u_timeOfDay*2.0:2.0-u_timeOfDay*2.0;sunAlt=sunAlt*2.0-1.0;float warmth=1.0-smoothstep(0.0,0.4,sunAlt);warmth=warmth*warmth;float nightFactor=1.0-smoothstep(-0.12,0.02,sunAlt);float daylight=smoothstep(-0.12,0.1,sunAlt);vec3 color=scene.rgb;float accumulatedAlpha=0.0;for(int i=u_numLayers-1;i>=0;i--){float layerIdx=float(i);float layerDepth=layerIdx/max(1.0,float(u_numLayers)-1.0);float layerSeed=layerIdx*7.31+13.0;float layerSpeed=u_windSpeed*(0.6+hash(vec2(layerSeed,10.0))*0.8);float layerTurb=u_turbulence*(0.7+hash(vec2(layerSeed,11.0))*0.6);float cloud=cloudLayer(uv,u_time,layerSeed,layerSpeed,layerTurb);float threshold=1.0-u_coverage;cloud=smoothstep(threshold,threshold+u_softness,cloud);float heightInCloud=uv.y*0.6+cloud*0.4;vec3 cloudColor=cloudLighting(cloud,heightInCloud,sunAlt,warmth,nightFactor,uv);vec3 hazeColor=mix(vec3(0.05,0.06,0.1),vec3(0.6,0.7,0.85),daylight);float hazeAmount=layerDepth*layerDepth*0.5;cloudColor=mix(cloudColor,hazeColor,hazeAmount);float contrastReduction=1.0-layerDepth*0.3;cloudColor=mix(vec3(0.5),cloudColor,contrastReduction);float alpha=cloud*u_density*(0.6+(1.0-layerDepth)*0.4);color=mix(color,cloudColor,alpha*(1.0-accumulatedAlpha));accumulatedAlpha=accumulatedAlpha+alpha*(1.0-accumulatedAlpha);}fragColor=vec4(color,accumulatedAlpha);}`,ie=`#version 300 es\nprecision highp float;in vec2 v_uv;out vec4 fragColor;uniform float u_time;uniform vec2 u_resolution;uniform sampler2D u_sceneTexture;uniform float u_glassIntensity;uniform float u_glassZoom;uniform float u_fallingIntensity;uniform float u_fallingSpeed;uniform float u_fallingAngle;uniform float u_fallingStreakLength;uniform int u_fallingLayers;uniform float u_refractionStrength;\n#define S(a, b, t) smoothstep(a, b, t)\nvec3 N13(float p){vec3 p3=fract(vec3(p)*vec3(0.1031,0.11369,0.13787));p3+=dot(p3,p3.yzx+19.19);return fract(vec3((p3.x+p3.y)*p3.z,(p3.x+p3.z)*p3.y,(p3.y+p3.z)*p3.x));}float N(float t){return fract(sin(t*12345.564)*7658.76);}float Saw(float b,float t){return S(0.0,b,t)*S(1.0,b,t);}vec2 DropLayer(vec2 uv,float t){vec2 UV=uv;uv.y+=t*0.75;vec2 aspect=vec2(6.0,1.0);vec2 grid=aspect*2.0;vec2 id=floor(uv*grid);float colShift=N(id.x);uv.y+=colShift;id=floor(uv*grid);vec3 n=N13(id.x*35.2+id.y*2376.1);vec2 st=fract(uv*grid)-vec2(0.5,0.0);float x=n.x-0.5;float y=UV.y*20.0;float wiggle=sin(y+sin(y));x+=wiggle*(0.5-abs(x))*(n.z-0.5);x*=0.7;float ti=fract(t+n.z);y=(Saw(0.85,ti)-0.5)*0.9+0.5;vec2 p=vec2(x,y);float d=length((st-p)*aspect.yx);float mainDrop=S(0.4,0.0,d);float r=sqrt(S(1.0,y,st.y));float cd=abs(st.x-x);float trail=S(0.23*r,0.15*r*r,cd);float trailFront=S(-0.02,0.02,st.y-y);trail*=trailFront*r*r;float y2=fract(UV.y*10.0)+(st.y-0.5);float dd=length(st-vec2(x,y2));float droplets=S(0.3,0.0,dd);float m=mainDrop+droplets*r*trailFront;return vec2(m,trail);}float StaticDrops(vec2 uv,float t){uv*=40.0;vec2 id=floor(uv);uv=fract(uv)-0.5;vec3 n=N13(id.x*107.45+id.y*3543.654);vec2 p=(n.xy-0.5)*0.7;float d=length(uv-p);float fade=Saw(0.025,fract(t+n.z));float c=S(0.3,0.0,d)*fract(n.z*10.0)*fade;return c;}vec2 Drops(vec2 uv,float t,float l0,float l1,float l2){float s=StaticDrops(uv,t)*l0;vec2 m1=DropLayer(uv,t)*l1;vec2 m2=DropLayer(uv*1.85,t)*l2;float c=s+m1.x+m2.x;c=S(0.3,1.0,c);return vec2(c,max(m1.y*l0,m2.y*l1));}float hash12(vec2 p){vec3 p3=fract(vec3(p.xyx)*0.1031);p3+=dot(p3,p3.yzx+33.33);return fract((p3.x+p3.y)*p3.z);}vec2 FallingRainLayer(vec2 uv,float t,float speed,float angle,float streakLen,float scale,float density){vec2 offset=vec2(0.0);vec2 p=uv;p.x+=p.y*angle;p*=scale;p.y+=t*speed;vec2 id=floor(p);vec2 gv=fract(p)-0.5;for(int y=-1;y<=1;y++){for(int x=-1;x<=1;x++){vec2 offs=vec2(float(x),float(y));vec2 cellId=id+offs;float n1=hash12(cellId);if(n1>density)continue;vec2 n2=vec2(hash12(cellId*17.23),hash12(cellId*31.17));vec2 dropPos=offs+n2-0.5;vec2 localUV=gv-dropPos;float streakW=0.025+n1*0.02;float streakH=streakLen*(0.4+hash12(cellId*7.13)*0.6);float t_pos=(localUV.y+streakH)/(2.0*streakH);t_pos=clamp(t_pos,0.0,1.0);if(abs(localUV.y)>streakH*1.2)continue;float taper=mix(1.3,0.4,t_pos*t_pos);float width=streakW*taper;float core=S(width,width*0.2,abs(localUV.x));float vertFade=S(0.0,0.1,t_pos)*S(1.0,0.85,t_pos);float streak=core*vertFade;if(streak>0.001){offset.x+=localUV.x*streak*0.5;offset.y+=(n1-0.5)*streak*0.1;}}}return offset;}vec2 FallingRain(vec2 uv,float t){vec2 totalOffset=vec2(0.0);if(u_fallingIntensity<0.01)return totalOffset;float speed=u_fallingSpeed*5.0;float streakLen=u_fallingStreakLength*0.3;for(int i=0;i<6;i++){if(i>=u_fallingLayers)break;float layerIdx=float(i);float depth=layerIdx/float(max(u_fallingLayers-1,1));float layerScale=mix(6.0,30.0,depth);float layerSpeed=speed*mix(2.0,0.5,depth);float layerDensity=u_fallingIntensity*mix(0.8,0.3,depth);float layerStrength=mix(1.0,0.15,depth);float layerStreakLen=streakLen*mix(1.5,0.4,depth);float layerAngle=u_fallingAngle*mix(1.0,0.6,depth);vec2 layerOffset=vec2(sin(layerIdx*73.156)*3.0,cos(layerIdx*37.842)*3.0);vec2 layer=FallingRainLayer(uv+layerOffset,t+layerIdx*0.13,layerSpeed,layerAngle,layerStreakLen,layerScale,layerDensity);totalOffset+=layer*layerStrength;}return totalOffset*0.4;}void main(){vec2 uv=(gl_FragCoord.xy-0.5*u_resolution.xy)/u_resolution.y;vec2 UV=v_uv;uv*=u_glassZoom;float t=u_time*0.2;float rainAmount=u_glassIntensity;float staticDrops=S(-0.5,1.0,rainAmount)*2.0;float layer1=S(0.25,0.75,rainAmount);float layer2=S(0.0,0.5,rainAmount);vec2 c=Drops(uv,t,staticDrops,layer1,layer2);vec2 e=vec2(0.001,0.0);float cx=Drops(uv+e,t,staticDrops,layer1,layer2).x;float cy=Drops(uv+e.yx,t,staticDrops,layer1,layer2).x;vec2 glassNormal=vec2(cx-c.x,cy-c.x);vec2 fallingRainOffset=FallingRain(uv,u_time);vec2 totalRefraction=(glassNormal+fallingRainOffset)*u_refractionStrength;vec2 refractedUV=UV+totalRefraction;refractedUV=clamp(refractedUV,0.0,1.0);vec4 scene=texture(u_sceneTexture,refractedUV);vec3 color=scene.rgb;float rainMagnitude=length(fallingRainOffset);if(rainMagnitude>0.001){float brightness=dot(scene.rgb,vec3(0.299,0.587,0.114));float specular=rainMagnitude*15.0*(0.1+brightness*0.9);color+=vec3(0.8,0.85,0.95)*specular*0.3;}color+=vec3(0.1,0.12,0.15)*c.x*0.5;fragColor=vec4(color,scene.a);}`,re=`#version 300 es\nprecision highp float;in vec2 v_uv;out vec4 fragColor;uniform float u_time;uniform vec2 u_resolution;uniform sampler2D u_sceneTexture;uniform bool u_enabled;uniform float u_flashIntensity;uniform float u_branchDensity;uniform float u_sceneIllumination;uniform float u_lastFlashTime;uniform float u_strikeSeed;\n#define MAX_SEGMENTS 32\n#define MAX_BRANCHES 16\n#define PI 3.14159265359\nfloat easeOutSine(float t){return sin(t*PI*0.5);}float easeInSine(float t){return 1.0-cos(t*PI*0.5);}float easeInOutSine(float t){return-(cos(PI*t)-1.0)*0.5;}float easeOutQuad(float t){return 1.0-(1.0-t)*(1.0-t);}float easeOutCubic(float t){float inv=1.0-t;return 1.0-inv*inv*inv;}float hash11(float p){p=fract(p*0.1031);p*=p+33.33;p*=p+p;return fract(p);}float hash12(vec2 p){vec3 p3=fract(vec3(p.xyx)*0.1031);p3+=dot(p3,p3.yzx+33.33);return fract((p3.x+p3.y)*p3.z);}vec2 hash22(vec2 p){vec3 p3=fract(vec3(p.xyx)*vec3(0.1031,0.1030,0.0973));p3+=dot(p3,p3.yzx+33.33);return fract((p3.xx+p3.yz)*p3.zy);}float noise(vec2 p){vec2 i=floor(p);vec2 f=fract(p);f=f*f*(3.0-2.0*f);float a=hash12(i);float b=hash12(i+vec2(1.0,0.0));float c=hash12(i+vec2(0.0,1.0));float d=hash12(i+vec2(1.0,1.0));return mix(mix(a,b,f.x),mix(c,d,f.x),f.y);}float distToSegment(vec2 p,vec2 a,vec2 b){vec2 pa=p-a;vec2 ba=b-a;float h=clamp(dot(pa,ba)/dot(ba,ba),0.0,1.0);return length(pa-ba*h);}vec2 displacedPoint(vec2 start,vec2 end,float t,float seed,float displacementAmt){vec2 basePoint=mix(start,end,t);vec2 dir=end-start;vec2 perp=normalize(vec2(-dir.y,dir.x));float envelope=sin(t*PI);float n1=noise(vec2(t*8.0,seed*100.0))*2.0-1.0;float n2=noise(vec2(t*16.0,seed*100.0+50.0))*2.0-1.0;float n3=noise(vec2(t*32.0,seed*100.0+100.0))*2.0-1.0;float displacement=(n1*0.6+n2*0.3+n3*0.1)*envelope*displacementAmt;float targetBias=1.0-t*0.3;displacement*=targetBias;return basePoint+perp*displacement*length(dir);}float mainBoltDistance(vec2 uv,vec2 start,vec2 end,float seed,float displacementAmt){float minDist=999.0;vec2 prevPoint=start;for(int i=1;i<=MAX_SEGMENTS;i++){float t=float(i)/float(MAX_SEGMENTS);vec2 currPoint=displacedPoint(start,end,t,seed,displacementAmt);float d=distToSegment(uv,prevPoint,currPoint);minDist=min(minDist,d);prevPoint=currPoint;}return minDist;}float branchDistance(vec2 uv,vec2 branchStart,vec2 branchDir,float branchLen,float seed,float displacementAmt){vec2 branchEnd=branchStart+branchDir*branchLen;float minDist=999.0;vec2 prevPoint=branchStart;for(int i=1;i<=12;i++){float t=float(i)/12.0;vec2 currPoint=displacedPoint(branchStart,branchEnd,t,seed,displacementAmt*0.7);float d=distToSegment(uv,prevPoint,currPoint);minDist=min(minDist,d);prevPoint=currPoint;}return minDist;}vec2 branchesDistance(vec2 uv,vec2 start,vec2 end,float seed,float displacementAmt,float density){float minDist=999.0;float brightness=0.0;vec2 mainDir=normalize(end-start);float mainLen=length(end-start);for(int i=0;i<MAX_BRANCHES;i++){float idx=float(i);float branchT=0.15+hash11(seed+idx*7.31)*0.7;float branchProb=(1.0-branchT)*density;if(hash11(seed+idx*3.17)>branchProb)continue;vec2 branchStart=displacedPoint(start,end,branchT,seed,displacementAmt);float angleOffset=(hash11(seed+idx*11.13)*2.0-1.0)*0.6;float side=hash11(seed+idx*5.71)>0.5?1.0:-1.0;float angle=atan(mainDir.y,mainDir.x)+side*(0.3+abs(angleOffset)*0.5);vec2 branchDir=vec2(cos(angle),sin(angle));float branchLen=mainLen*(0.15+hash11(seed+idx*13.37)*0.25);float d=branchDistance(uv,branchStart,branchDir,branchLen,seed+idx*100.0,displacementAmt);if(d<minDist){minDist=d;brightness=0.5-branchT*0.2;}if(density>0.3&&hash11(seed+idx*17.19)<density*0.5){float subT=0.3+hash11(seed+idx*19.23)*0.4;vec2 subStart=branchStart+branchDir*branchLen*subT;float subAngle=angle+(hash11(seed+idx*23.29)*2.0-1.0)*0.5;vec2 subDir=vec2(cos(subAngle),sin(subAngle));float subLen=branchLen*0.4;float subD=branchDistance(uv,subStart,subDir,subLen,seed+idx*200.0,displacementAmt*0.5);if(subD<minDist){minDist=subD;brightness=0.25;}}}return vec2(minDist,brightness);}vec3 lightningGlow(float dist,float brightness,float intensity,float thickness){float scaledDist=dist/max(thickness,0.1);float core=smoothstep(0.003,0.0,scaledDist)*brightness;float innerGlow=exp(-scaledDist*150.0)*brightness;float outerGlow=exp(-dist*dist*3000.0)*brightness*thickness;vec3 coreColor=vec3(1.0,1.0,1.0);vec3 innerColor=vec3(0.7,0.8,1.0);vec3 outerColor=vec3(0.5,0.5,0.9);vec3 color=coreColor*core*2.0;color+=innerColor*innerGlow*0.8;color+=outerColor*outerGlow*0.5;return color*intensity;}float flashEnvelope(float timeSinceStrike,float duration){if(timeSinceStrike<0.0||timeSinceStrike>duration)return 0.0;float t=timeSinceStrike/duration;float attackT=clamp(t/0.03,0.0,1.0);float attack=easeOutCubic(attackT);float sustainT=clamp((t-0.05)/0.65,0.0,1.0);float sustain=1.0-easeInOutSine(sustainT);float decay=exp(-t*2.0);decay=mix(decay,easeOutSine(1.0-t),0.3);float endT=clamp((t-0.75)/0.25,0.0,1.0);float endFade=1.0-easeInSine(endT);return attack*max(sustain,decay*0.4)*endFade;}float restrikeEnvelope(float timeSinceStrike,float duration,float seed){float env=flashEnvelope(timeSinceStrike,duration*0.7);if(hash11(seed*7.7)>0.7){float restrike1=flashEnvelope(timeSinceStrike-duration*0.5,duration*0.3);env=max(env,restrike1*0.6);}if(hash11(seed*11.3)>0.85){float restrike2=flashEnvelope(timeSinceStrike-duration*0.75,duration*0.2);env=max(env,restrike2*0.4);}return env;}void main(){vec4 scene=texture(u_sceneTexture,v_uv);if(!u_enabled){fragColor=scene;return;}vec2 uv=v_uv;float aspect=u_resolution.x/u_resolution.y;uv.x*=aspect;float timeSinceStrike=u_time-u_lastFlashTime;float durationSec=0.8;float flash=restrikeEnvelope(timeSinceStrike,durationSec,u_strikeSeed);float afterimageDuration=durationSec*1.5;float afterimageT=clamp(timeSinceStrike/afterimageDuration,0.0,1.0);float afterimage=timeSinceStrike<0.0?0.0:(1.0-easeInSine(afterimageT));vec3 color=scene.rgb;if(flash>0.01||afterimage>0.01){vec2 strikeHash=hash22(vec2(u_strikeSeed*123.456,u_strikeSeed*789.012));vec2 boltStart=vec2((0.3+strikeHash.x*0.4)*aspect,1.05);vec2 boltEnd=vec2(boltStart.x+(strikeHash.x-0.5)*0.4,-0.05);float straightDist=distToSegment(uv,boltStart,boltEnd);float sourceGlow=exp(-length(uv-boltStart)*3.0);color+=vec3(0.4,0.45,0.6)*sourceGlow*afterimage*0.3;float distLimit=0.18+u_branchDensity*0.25+u_flashIntensity*0.05;float feather=0.08;float region=1.0-smoothstep(distLimit-feather,distLimit,straightDist);if(region<=0.0005){fragColor=vec4(color,scene.a);return;}float displacementAmt=0.15;float mainDist=mainBoltDistance(uv,boltStart,boltEnd,u_strikeSeed,displacementAmt);vec2 branchResult=branchesDistance(uv,boltStart,boltEnd,u_strikeSeed,displacementAmt,u_branchDensity);float branchDist=branchResult.x;float branchBrightness=branchResult.y;float mainThickness=mix(0.2,1.0,easeOutSine(sqrt(max(flash,0.0))));vec3 afterglowColor=vec3(0.5,0.45,0.7);vec3 mainCore=lightningGlow(mainDist,easeOutQuad(max(flash,0.0)),u_flashIntensity,mainThickness);float mainAfterglowDist=mainDist*0.6;float mainAfterglowStrength=exp(-mainAfterglowDist*50.0)*afterimage*0.5;vec3 mainAfterglow=afterglowColor*mainAfterglowStrength;float branchThickness=mix(0.15,1.0,easeOutSine(max(flash,0.0)));vec3 branchCore=lightningGlow(branchDist,branchBrightness*easeOutQuad(max(flash,0.0)),u_flashIntensity,branchThickness);float branchAfterglowDist=branchDist*0.7;float branchAfterglowStrength=exp(-branchAfterglowDist*80.0)*branchBrightness*afterimage*0.4;vec3 branchAfterglow=afterglowColor*branchAfterglowStrength;color+=(mainCore+branchCore)*max(flash,0.0)*region;color+=(mainAfterglow+branchAfterglow)*afterimage*region;}fragColor=vec4(color,scene.a);}`,le=`#version 300 es\nprecision highp float;in vec2 v_uv;out vec4 fragColor;uniform float u_time;uniform vec2 u_resolution;uniform sampler2D u_sceneTexture;uniform float u_intensity;uniform int u_layers;uniform float u_fallSpeed;uniform float u_windSpeed;uniform float u_windAngle;uniform float u_turbulence;uniform float u_drift;uniform float u_flutter;uniform float u_windShear;uniform float u_flakeSize;uniform float u_sizeVariation;uniform float u_opacity;uniform float u_glowAmount;uniform float u_sparkle;\n#define PI 3.14159265359\n#define MAX_LAYERS 6\nfloat hash12(vec2 p){vec3 p3=fract(vec3(p.xyx)*0.1031);p3+=dot(p3,p3.yzx+33.33);return fract((p3.x+p3.y)*p3.z);}vec2 hash22(vec2 p){vec3 p3=fract(vec3(p.xyx)*vec3(0.1031,0.1030,0.0973));p3+=dot(p3,p3.yzx+33.33);return fract((p3.xx+p3.yz)*p3.zy);}float noise(vec2 p){vec2 i=floor(p);vec2 f=fract(p);f=f*f*(3.0-2.0*f);float a=hash12(i);float b=hash12(i+vec2(1.0,0.0));float c=hash12(i+vec2(0.0,1.0));float d=hash12(i+vec2(1.0,1.0));return mix(mix(a,b,f.x),mix(c,d,f.x),f.y);}vec2 rotate2D(vec2 p,float angle){float c=cos(angle);float s=sin(angle);return vec2(p.x*c-p.y*s,p.x*s+p.y*c);}float snowflakeShape(vec2 uv,float size,float seed,float rotation){vec2 rotatedUV=rotate2D(uv,rotation);float dist=length(rotatedUV);float circle=smoothstep(size,size*0.3,dist);float angle=atan(rotatedUV.y,rotatedUV.x);float hexPattern=0.5+0.5*cos(angle*6.0);hexPattern=pow(hexPattern,2.0);float crystalAmount=smoothstep(0.02,0.05,size)*0.3;float shape=mix(circle,circle*(0.7+hexPattern*0.3),crystalAmount);float glow=exp(-dist*dist/(size*size*3.0))*u_glowAmount;return shape+glow*0.4;}vec2 getWind(float layerDepth){vec2 baseWind=vec2(cos(u_windAngle),0.0)*u_windSpeed;float windResponse=mix(0.3,1.0,1.0-layerDepth);float shearResponse=1.0+u_windShear*(1.0-layerDepth)*0.35;return baseWind*windResponse*shearResponse;}float sparkle(vec2 cellId,float time,float seed){float sparklePhase=hash12(cellId+vec2(seed*100.0,0.0))*100.0;float sparkleFreq=2.0+hash12(cellId+vec2(0.0,seed*100.0))*3.0;float sparkleWave=sin(time*sparkleFreq+sparklePhase);float sparkleIntensity=pow(max(0.0,sparkleWave),16.0);float sparkleProbability=hash12(cellId+vec2(floor(time*0.5),0.0));sparkleIntensity*=step(0.85,sparkleProbability);return sparkleIntensity*u_sparkle;}vec3 snowLayer(vec2 uv,float time,float layerIndex,float totalLayers){float depth=layerIndex/max(1.0,totalLayers-1.0);float layerScale=mix(8.0,40.0,depth);float layerSpeed=u_fallSpeed*mix(1.2,0.4,depth);float layerDensity=u_intensity*mix(1.0,0.5,depth);float layerFlakeSize=u_flakeSize*mix(1.5,0.3,depth);float layerOpacity=u_opacity*mix(1.0,0.4,depth);vec2 layerOffset=vec2(sin(layerIndex*73.156)*10.0,cos(layerIndex*37.842)*10.0);vec2 p=(uv+layerOffset)*layerScale;p.y+=time*layerSpeed*2.0;vec2 baseWind=getWind(depth);p.x+=time*baseWind.x*0.3;vec2 id=floor(p);vec2 gv=fract(p)-0.5;float snow=0.0;float sparkleAccum=0.0;for(int y=-1;y<=1;y++){for(int x=-1;x<=1;x++){vec2 offs=vec2(float(x),float(y));vec2 cellId=id+offs;float h1=hash12(cellId);vec2 h2=hash22(cellId);float h3=hash12(cellId+vec2(127.0,311.0));float h4=hash12(cellId+vec2(271.0,183.0));if(h1>layerDensity)continue;float sizeVar=1.0+(h3-0.5)*u_sizeVariation;float size=layerFlakeSize*sizeVar*0.04;vec2 flakePos=h2*0.8-0.4;float flutterPhase=h3*PI*2.0;float flutterAmp=u_flutter*0.15*(1.0-depth);flakePos.x+=sin(time*3.0+flutterPhase)*flutterAmp;flakePos.y+=cos(time*2.5+flutterPhase*1.3)*flutterAmp*0.5;float driftPhase=h4*PI*2.0+layerIndex*1.7;flakePos.x+=sin(time*0.55+driftPhase)*u_drift*0.18;float turbFreq=0.6+u_turbulence*1.4;vec2 turb=vec2(noise(cellId*0.17+time*turbFreq),noise(cellId.yx*0.17+time*turbFreq+17.0))-0.5;flakePos+=turb*(u_turbulence*0.22)*(1.0-depth);vec2 localUV=gv-offs-flakePos;float rotationSpeed=(1.5-sizeVar*0.5)*(0.5+h4*1.0);float rotationPhase=h4*PI*2.0;float rotation=time*rotationSpeed+rotationPhase;float flake=snowflakeShape(localUV,size,h1,rotation);float flakeSparkle=sparkle(cellId,time,h1)*flake;sparkleAccum+=flakeSparkle;snow+=flake*layerOpacity;}}return vec3(snow,sparkleAccum,depth);}void main(){vec4 scene=texture(u_sceneTexture,v_uv);vec2 uv=v_uv;float aspect=u_resolution.x/u_resolution.y;uv.x*=aspect;float snow=0.0;float totalSparkle=0.0;for(int i=u_layers-1;i>=0;i--){vec3 layerResult=snowLayer(uv,u_time,float(i),float(u_layers));snow+=layerResult.x;totalSparkle+=layerResult.y;}snow=clamp(snow,0.0,1.0);totalSparkle=clamp(totalSparkle,0.0,1.0);vec3 snowColor=vec3(0.75,0.78,0.85);vec3 sparkleColor=vec3(0.9,0.92,1.0);vec3 color=scene.rgb+snowColor*snow+sparkleColor*totalSparkle;fragColor=vec4(color,scene.a);}`,ue=`#version 300 es\nprecision highp float;in vec2 v_uv;out vec4 fragColor;uniform sampler2D u_sceneTexture;uniform float u_time;uniform vec2 u_resolution;uniform float u_timeOfDay;uniform vec2 u_sunPos;uniform float u_sunVisible;uniform float u_lastFlashTime;uniform float u_strikeSeed;uniform float u_lightningSceneIllumination;uniform bool u_postEnabled;uniform float u_haze;uniform float u_hazeHorizon;uniform float u_hazeDesaturation;uniform float u_hazeContrast;uniform float u_bloomIntensity;uniform float u_bloomThreshold;uniform float u_bloomKnee;uniform float u_bloomRadius;uniform float u_bloomTapScale;uniform float u_exposureIntensity;uniform float u_exposureDesaturation;uniform float u_exposureRecovery;uniform float u_godRayIntensity;uniform float u_godRayDecay;uniform float u_godRayDensity;uniform float u_godRayWeight;uniform int u_godRaySamples;\n#define PI 3.14159265359\n#define GODRAY_MAX_SAMPLES 32\nfloat saturate(float x){return clamp(x,0.0,1.0);}float luminance(vec3 c){return dot(c,vec3(0.299,0.587,0.114));}float easeOutSine(float t){return sin(t*PI*0.5);}float easeInSine(float t){return 1.0-cos(t*PI*0.5);}float easeInOutSine(float t){return-(cos(PI*t)-1.0)*0.5;}float easeOutQuad(float t){return 1.0-(1.0-t)*(1.0-t);}float easeOutCubic(float t){float inv=1.0-t;return 1.0-inv*inv*inv;}float hash11(float p){p=fract(p*0.1031);p*=p+33.33;p*=p+p;return fract(p);}float flashEnvelope(float timeSinceStrike,float duration){if(timeSinceStrike<0.0||timeSinceStrike>duration)return 0.0;float t=timeSinceStrike/duration;float attackT=clamp(t/0.03,0.0,1.0);float attack=easeOutCubic(attackT);float sustainT=clamp((t-0.05)/0.65,0.0,1.0);float sustain=1.0-easeInOutSine(sustainT);float decay=exp(-t*2.0);decay=mix(decay,easeOutSine(1.0-t),0.3);float endT=clamp((t-0.75)/0.25,0.0,1.0);float endFade=1.0-easeInSine(endT);return attack*max(sustain,decay*0.4)*endFade;}float restrikeEnvelope(float timeSinceStrike,float duration,float seed){float env=flashEnvelope(timeSinceStrike,duration*0.7);if(hash11(seed*7.7)>0.7){float restrike1=flashEnvelope(timeSinceStrike-duration*0.5,duration*0.3);env=max(env,restrike1*0.6);}if(hash11(seed*11.3)>0.85){float restrike2=flashEnvelope(timeSinceStrike-duration*0.75,duration*0.2);env=max(env,restrike2*0.4);}return env;}float getSunAltitudeFromTimeOfDay(float timeOfDay){float sunAlt=timeOfDay<0.5?timeOfDay*2.0:2.0-timeOfDay*2.0;return sunAlt*2.0-1.0;}vec3 applyHaze(vec3 color,vec2 uv){float haze=saturate(u_haze);if(haze<=0.0001)return color;float horizon=pow(1.0-uv.y,1.8);float hazeWeight=haze*mix(1.0,horizon,saturate(u_hazeHorizon));float sunAlt=getSunAltitudeFromTimeOfDay(u_timeOfDay);float daylight=smoothstep(-0.12,0.1,sunAlt);vec3 hazeDay=vec3(0.60,0.70,0.85);vec3 hazeNight=vec3(0.06,0.07,0.10);vec3 hazeColor=mix(hazeNight,hazeDay,daylight);color=mix(color,hazeColor,hazeWeight*0.55);float contrast=saturate(1.0-hazeWeight*saturate(u_hazeContrast));color=mix(vec3(0.5),color,contrast);float gray=luminance(color);float sat=saturate(1.0-hazeWeight*saturate(u_hazeDesaturation));color=mix(vec3(gray),color,sat);return color;}vec3 bloomTap(vec2 uv){vec3 c=texture(u_sceneTexture,clamp(uv,0.0,1.0)).rgb;float l=luminance(c);float knee=max(0.0001,u_bloomKnee);float m=smoothstep(u_bloomThreshold,u_bloomThreshold+knee,l);return c*m;}vec3 computeBloom(vec2 uv){float intensity=u_bloomIntensity;if(intensity<=0.0001)return vec3(0.0);vec2 texel=1.0/max(u_resolution,vec2(1.0));float radiusPx=max(0.0,u_bloomRadius)*(u_resolution.y*0.02);radiusPx*=max(0.25,u_bloomTapScale);vec2 d=texel*radiusPx;vec3 sum=vec3(0.0);sum+=bloomTap(uv)*0.20;sum+=bloomTap(uv+vec2(d.x,0.0))*0.12;sum+=bloomTap(uv+vec2(-d.x,0.0))*0.12;sum+=bloomTap(uv+vec2(0.0,d.y))*0.12;sum+=bloomTap(uv+vec2(0.0,-d.y))*0.12;sum+=bloomTap(uv+vec2(d.x,d.y))*0.08;sum+=bloomTap(uv+vec2(-d.x,d.y))*0.08;sum+=bloomTap(uv+vec2(d.x,-d.y))*0.08;sum+=bloomTap(uv+vec2(-d.x,-d.y))*0.08;return sum*intensity;}vec3 applyExposureResponse(vec3 color,float flashStrength){if(flashStrength<=0.0001)return color;float t=saturate(flashStrength);float gain=1.0+flashStrength*2.2;vec3 lifted=color*gain;vec3 tonemapped=1.0-exp(-lifted);vec3 outColor=mix(color,tonemapped,t);float gray=luminance(outColor);float desat=saturate(u_exposureDesaturation)*t;outColor=mix(outColor,vec3(gray),desat);outColor=mix(outColor,vec3(1.0),t*0.06);return outColor;}vec3 computeGodRays(vec2 uv){if(u_godRayIntensity<=0.0001)return vec3(0.0);if(u_godRaySamples<=0)return vec3(0.0);if(u_sunVisible<=0.001)return vec3(0.0);float sunAlt=getSunAltitudeFromTimeOfDay(u_timeOfDay);float daylight=smoothstep(-0.12,0.1,sunAlt);float lowSun=1.0-smoothstep(0.25,0.75,max(0.0,sunAlt));vec2 sunUV=clamp(u_sunPos,vec2(-0.25),vec2(1.25));vec2 delta=(uv-sunUV)*(u_godRayDensity/float(u_godRaySamples));vec2 coord=uv;float illuminationDecay=1.0;float accum=0.0;for(int i=0;i<GODRAY_MAX_SAMPLES;i++){if(i>=u_godRaySamples)break;coord-=delta;vec4 s=texture(u_sceneTexture,clamp(coord,0.0,1.0));float transmittance=1.0-saturate(s.a);float sampleLum=luminance(s.rgb);float brightMask=saturate((sampleLum-0.85)/0.15);brightMask*=brightMask;float raySample=transmittance*brightMask;accum+=raySample*illuminationDecay*u_godRayWeight;illuminationDecay*=u_godRayDecay;}vec3 rayColor=mix(vec3(0.7,0.72,0.8),vec3(1.0,0.92,0.75),daylight);float intensity=u_godRayIntensity*saturate(u_sunVisible)*daylight*lowSun;return rayColor*accum*intensity;}void main(){vec4 scene=texture(u_sceneTexture,v_uv);vec3 color=scene.rgb;if(!u_postEnabled){fragColor=vec4(color,1.0);return;}color+=computeGodRays(v_uv);color+=computeBloom(v_uv);float flashStrength=0.0;if(u_exposureIntensity>0.0001){float timeSinceStrike=u_time-u_lastFlashTime;float durationSec=0.8;float f=restrikeEnvelope(timeSinceStrike,durationSec,u_strikeSeed);float afterimageDuration=durationSec*1.5;float afterT=clamp((timeSinceStrike*max(0.05,u_exposureRecovery))/afterimageDuration,0.0,1.0);float afterimage=timeSinceStrike<0.0?0.0:(1.0-easeInSine(afterT));float sceneFlash=f*max(0.0,u_lightningSceneIllumination);color+=vec3(0.3,0.32,0.4)*sceneFlash;flashStrength=f*u_exposureIntensity;flashStrength=max(flashStrength,afterimage*u_exposureIntensity*0.12);color=applyExposureResponse(color,flashStrength);}color=applyHaze(color,v_uv);fragColor=vec4(clamp(color,0.0,1.0),1.0);}`;function pe(e,t,a){if(e.isContextLost())return null;let n=e.createShader(t);if(!n)return null;if(e.shaderSource(n,a),e.compileShader(n),!e.getShaderParameter(n,e.COMPILE_STATUS)){let s=e.getShaderInfoLog(n);if(!e.isContextLost()){let c=t===e.VERTEX_SHADER?\"vertex\":t===e.FRAGMENT_SHADER?\"fragment\":String(t);console.error(`Shader compile error (${c}):`,s??\"(no info log)\")}return e.deleteShader(n),null}return n}function B(e,t,a){if(e.isContextLost())return null;let n=pe(e,e.VERTEX_SHADER,t),s=pe(e,e.FRAGMENT_SHADER,a);if(!n||!s)return n&&e.deleteShader(n),s&&e.deleteShader(s),null;let c=e.createProgram();if(!c)return e.deleteShader(n),e.deleteShader(s),null;if(e.attachShader(c,n),e.attachShader(c,s),e.linkProgram(c),!e.getProgramParameter(c,e.LINK_STATUS)){let l=e.getProgramInfoLog(c);return e.isContextLost()||console.error(\"Program link error:\",l??\"(no info log)\"),e.deleteProgram(c),e.deleteShader(n),e.deleteShader(s),null}return e.detachShader(c,n),e.detachShader(c,s),e.deleteShader(n),e.deleteShader(s),c}function ce(e,t,a){let n=e.createTexture();if(!n)return null;e.bindTexture(e.TEXTURE_2D,n),e.texImage2D(e.TEXTURE_2D,0,e.RGBA,t,a,0,e.RGBA,e.UNSIGNED_BYTE,null),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE);let s=e.createFramebuffer();if(!s)return e.deleteTexture(n),null;e.bindFramebuffer(e.FRAMEBUFFER,s),e.framebufferTexture2D(e.FRAMEBUFFER,e.COLOR_ATTACHMENT0,e.TEXTURE_2D,n,0);let c=e.checkFramebufferStatus(e.FRAMEBUFFER);return c!==e.FRAMEBUFFER_COMPLETE?(e.isContextLost()||console.error(\"Framebuffer incomplete:\",c),e.deleteFramebuffer(s),e.deleteTexture(n),e.bindFramebuffer(e.FRAMEBUFFER,null),e.bindTexture(e.TEXTURE_2D,null),null):(e.bindFramebuffer(e.FRAMEBUFFER,null),e.bindTexture(e.TEXTURE_2D,null),{fbo:s,texture:n,width:t,height:a})}function fe(e,t,a,n){t.width===a&&t.height===n||(e.bindTexture(e.TEXTURE_2D,t.texture),e.texImage2D(e.TEXTURE_2D,0,e.RGBA,a,n,0,e.RGBA,e.UNSIGNED_BYTE,null),e.bindTexture(e.TEXTURE_2D,null),t.width=a,t.height=n)}function J(e,t,a){let n=Math.max(0,Math.min(1,(a-e)/(t-e)));return n*n*(3-2*n)}function Se(e){let t=e.timeOfDay,a=e.celestialY,n=-.25,s=J(.18,.32,t),c=J(.68,.82,t),l=s*(1-c),o=n+(a-n)*l,r=J(.74,.88,t),i=1-J(.12,.26,t),h=Math.max(r,i),f=n+(a-n)*h,p=l>h,b=p?o:f,R=p?e.sunSize:e.moonSize,C=p?Math.min(1,e.sunGlowIntensity*.3)*l:Math.min(.5,e.moonGlowIntensity*.15)*h;return{sunY:o,moonY:f,sunVisible:l,moonVisible:h,activeY:b,activeSize:R,activeBrightness:C}}function Z(e,t,a,n,s){e.bindFramebuffer(e.FRAMEBUFFER,a.fbo),e.viewport(0,0,n,s),e.useProgram(t)}function K(e,t,a,n){e.activeTexture(e.TEXTURE0),e.bindTexture(e.TEXTURE_2D,a),e.uniform1i(n(t,\"u_sceneTexture\"),0)}function ve(e,t,a,n){e.bindFramebuffer(e.FRAMEBUFFER,t.fbo),e.viewport(0,0,a,n),e.clearColor(0,0,0,0),e.clear(e.COLOR_BUFFER_BIT)}function be({gl:e,program:t,target:a,displayWidth:n,displayHeight:s,time:c,params:l,moonTexture:o,moonTextureLoaded:r,getUniformLocation:i}){Z(e,t,a,n,s),e.uniform1f(i(t,\"u_time\"),c),e.uniform2f(i(t,\"u_resolution\"),n,s),e.uniform1f(i(t,\"u_timeOfDay\"),l.timeOfDay),e.uniform1f(i(t,\"u_moonPhase\"),l.moonPhase),e.uniform1f(i(t,\"u_starDensity\"),l.starDensity),e.uniform2f(i(t,\"u_celestialPos\"),l.celestialX,l.celestialY),e.uniform1f(i(t,\"u_sunSize\"),l.sunSize),e.uniform1f(i(t,\"u_moonSize\"),l.moonSize),e.uniform1f(i(t,\"u_sunGlowIntensity\"),l.sunGlowIntensity),e.uniform1f(i(t,\"u_sunGlowSize\"),l.sunGlowSize),e.uniform1f(i(t,\"u_sunRayCount\"),l.sunRayCount),e.uniform1f(i(t,\"u_sunRayLength\"),l.sunRayLength),e.uniform1f(i(t,\"u_sunRayIntensity\"),l.sunRayIntensity),e.uniform1f(i(t,\"u_sunRayShimmer\"),l.sunRayShimmer),e.uniform1f(i(t,\"u_sunRayShimmerSpeed\"),l.sunRayShimmerSpeed),e.uniform1f(i(t,\"u_moonGlowIntensity\"),l.moonGlowIntensity),e.uniform1f(i(t,\"u_moonGlowSize\"),l.moonGlowSize),e.uniform1f(i(t,\"u_skyBrightness\"),l.skyBrightness),e.uniform1f(i(t,\"u_skySaturation\"),l.skySaturation),e.uniform1f(i(t,\"u_skyContrast\"),l.skyContrast),e.activeTexture(e.TEXTURE0),e.bindTexture(e.TEXTURE_2D,o),e.uniform1i(i(t,\"u_moonTexture\"),0),e.uniform1i(i(t,\"u_hasMoonTexture\"),r?1:0),e.drawArrays(e.TRIANGLES,0,6)}function we({gl:e,program:t,target:a,sceneTexture:n,displayWidth:s,displayHeight:c,time:l,params:o,celestial:r,getUniformLocation:i}){Z(e,t,a,s,c),K(e,t,n,i),e.uniform1f(i(t,\"u_time\"),l),e.uniform2f(i(t,\"u_resolution\"),s,c),e.uniform1f(i(t,\"u_timeOfDay\"),r.timeOfDay),e.uniform1f(i(t,\"u_coverage\"),o.coverage),e.uniform1f(i(t,\"u_density\"),o.density),e.uniform1f(i(t,\"u_softness\"),o.softness),e.uniform1f(i(t,\"u_windSpeed\"),o.windSpeed),e.uniform1f(i(t,\"u_windAngle\"),o.windAngle),e.uniform1f(i(t,\"u_turbulence\"),o.turbulence),e.uniform1f(i(t,\"u_lightIntensity\"),o.lightIntensity),e.uniform1f(i(t,\"u_ambientDarkness\"),o.ambientDarkness),e.uniform1i(i(t,\"u_numLayers\"),o.numLayers),e.uniform1f(i(t,\"u_cloudScale\"),o.cloudScale);let h=Se(r);e.uniform2f(i(t,\"u_celestialPos\"),r.celestialX,h.activeY),e.uniform1f(i(t,\"u_celestialSize\"),h.activeSize),e.uniform1f(i(t,\"u_celestialBrightness\"),h.activeBrightness),e.uniform1f(i(t,\"u_backlightIntensity\"),o.backlightIntensity),e.drawArrays(e.TRIANGLES,0,6)}function xe({gl:e,program:t,target:a,sceneTexture:n,displayWidth:s,displayHeight:c,time:l,params:o,interactions:r,getUniformLocation:i}){Z(e,t,a,s,c),K(e,t,n,i),e.uniform1f(i(t,\"u_time\"),l),e.uniform2f(i(t,\"u_resolution\"),s,c),e.uniform1f(i(t,\"u_glassIntensity\"),o.glassIntensity),e.uniform1f(i(t,\"u_glassZoom\"),o.glassZoom),e.uniform1f(i(t,\"u_fallingIntensity\"),o.fallingIntensity),e.uniform1f(i(t,\"u_fallingSpeed\"),o.fallingSpeed),e.uniform1f(i(t,\"u_fallingAngle\"),o.fallingAngle),e.uniform1f(i(t,\"u_fallingStreakLength\"),o.fallingStreakLength),e.uniform1i(i(t,\"u_fallingLayers\"),o.fallingLayers),e.uniform1f(i(t,\"u_refractionStrength\"),r.rainRefractionStrength),e.drawArrays(e.TRIANGLES,0,6)}function Ie(e,t,a,n,s){if(!e.lightning||!a||!t.enabled)return!1;let l=.8*1.5,o=n-s;return o>=0&&o<=l}function Re({gl:e,program:t,target:a,sceneTexture:n,displayWidth:s,displayHeight:c,time:l,params:o,interactions:r,lastFlashTime:i,strikeSeed:h,getUniformLocation:f}){Z(e,t,a,s,c),K(e,t,n,f),e.uniform1f(f(t,\"u_time\"),l),e.uniform2f(f(t,\"u_resolution\"),s,c),e.uniform1i(f(t,\"u_enabled\"),o.enabled?1:0),e.uniform1f(f(t,\"u_flashIntensity\"),o.flashIntensity),e.uniform1f(f(t,\"u_branchDensity\"),o.branchDensity),e.uniform1f(f(t,\"u_sceneIllumination\"),r.lightningSceneIllumination),e.uniform1f(f(t,\"u_lastFlashTime\"),i),e.uniform1f(f(t,\"u_strikeSeed\"),h),e.drawArrays(e.TRIANGLES,0,6)}function ke({gl:e,program:t,target:a,sceneTexture:n,displayWidth:s,displayHeight:c,time:l,params:o,getUniformLocation:r}){Z(e,t,a,s,c),K(e,t,n,r),e.uniform1f(r(t,\"u_time\"),l),e.uniform2f(r(t,\"u_resolution\"),s,c),e.uniform1f(r(t,\"u_intensity\"),o.intensity),e.uniform1i(r(t,\"u_layers\"),o.layers),e.uniform1f(r(t,\"u_fallSpeed\"),o.fallSpeed),e.uniform1f(r(t,\"u_windSpeed\"),o.windSpeed),e.uniform1f(r(t,\"u_windAngle\"),o.windAngle),e.uniform1f(r(t,\"u_turbulence\"),o.turbulence),e.uniform1f(r(t,\"u_drift\"),o.drift),e.uniform1f(r(t,\"u_flutter\"),o.flutter),e.uniform1f(r(t,\"u_windShear\"),o.windShear),e.uniform1f(r(t,\"u_flakeSize\"),o.flakeSize),e.uniform1f(r(t,\"u_sizeVariation\"),o.sizeVariation),e.uniform1f(r(t,\"u_opacity\"),o.opacity),e.uniform1f(r(t,\"u_glowAmount\"),o.glowAmount),e.uniform1f(r(t,\"u_sparkle\"),o.sparkle),e.drawArrays(e.TRIANGLES,0,6)}function _e({gl:e,program:t,sceneTexture:a,displayWidth:n,displayHeight:s,time:c,celestial:l,interactions:o,post:r,lastFlashTime:i,strikeSeed:h,getUniformLocation:f}){e.bindFramebuffer(e.FRAMEBUFFER,null),e.viewport(0,0,n,s),e.useProgram(t),K(e,t,a,f),e.uniform1f(f(t,\"u_time\"),c),e.uniform2f(f(t,\"u_resolution\"),n,s),e.uniform1f(f(t,\"u_timeOfDay\"),l.timeOfDay);let p=Se(l);e.uniform2f(f(t,\"u_sunPos\"),l.celestialX,p.sunY),e.uniform1f(f(t,\"u_sunVisible\"),p.sunVisible),e.uniform1f(f(t,\"u_lastFlashTime\"),i),e.uniform1f(f(t,\"u_strikeSeed\"),h),e.uniform1f(f(t,\"u_lightningSceneIllumination\"),o.lightningSceneIllumination),e.uniform1i(f(t,\"u_postEnabled\"),r.enabled?1:0),e.uniform1f(f(t,\"u_haze\"),r.haze),e.uniform1f(f(t,\"u_hazeHorizon\"),r.hazeHorizon),e.uniform1f(f(t,\"u_hazeDesaturation\"),r.hazeDesaturation),e.uniform1f(f(t,\"u_hazeContrast\"),r.hazeContrast),e.uniform1f(f(t,\"u_bloomIntensity\"),r.bloomIntensity),e.uniform1f(f(t,\"u_bloomThreshold\"),r.bloomThreshold),e.uniform1f(f(t,\"u_bloomKnee\"),r.bloomKnee),e.uniform1f(f(t,\"u_bloomRadius\"),r.bloomRadius),e.uniform1f(f(t,\"u_bloomTapScale\"),r.bloomTapScale),e.uniform1f(f(t,\"u_exposureIntensity\"),r.exposureIntensity),e.uniform1f(f(t,\"u_exposureDesaturation\"),r.exposureDesaturation),e.uniform1f(f(t,\"u_exposureRecovery\"),r.exposureRecovery),e.uniform1f(f(t,\"u_godRayIntensity\"),r.godRayIntensity),e.uniform1f(f(t,\"u_godRayDecay\"),r.godRayDecay),e.uniform1f(f(t,\"u_godRayDensity\"),r.godRayDensity),e.uniform1f(f(t,\"u_godRayWeight\"),r.godRayWeight),e.uniform1i(f(t,\"u_godRaySamples\"),Math.max(0,Math.min(32,Math.floor(r.godRaySamples)))),e.drawArrays(e.TRIANGLES,0,6)}function Ce(e){let t=v(null),a=v(null),n=v(0),s=v(0),c=v(-100),l=v(0),o=v(0),r=v(null),i=v(!1),h=v(null),f=v(new WeakMap),p=v(!1),b=v(!1),R=v(!1),C=v(!1),A=v(null),k=v(e);k.current=e;let w=v({celestial:null,cloud:null,rain:null,lightning:null,snow:null,composite:null}),T=v({a:null,b:null}),Q=N((d,u,y)=>{let g=f.current.get(u);g||(g=new Map,f.current.set(u,g));let m=g.get(y);if(m!==void 0)return m;let E=d.getUniformLocation(u,y);return g.set(y,E),E},[]),U=N(()=>{n.current&&(cancelAnimationFrame(n.current),n.current=0),b.current=!1},[]),$=N(d=>{d&&A.current&&ae(d),A.current=null},[]),G=N(()=>{U();let d=a.current,u=R.current;if(d&&!u){for(let y of Object.values(w.current))y&&d.deleteProgram(y);for(let y of[T.current.a,T.current.b])y&&(d.deleteFramebuffer(y.fbo),d.deleteTexture(y.texture));r.current&&d.deleteTexture(r.current),h.current&&d.deleteBuffer(h.current)}w.current={celestial:null,cloud:null,rain:null,lightning:null,snow:null,composite:null},T.current={a:null,b:null},r.current=null,i.current=!1,h.current=null,a.current=null,f.current=new WeakMap},[U]),z=N(({canvas:d,contextLost:u=!1,markInitFailed:y=!0,warnMessage:g,errorMessage:m})=>(u&&(R.current=!0),y&&(C.current=!0),m&&console.error(m),G(),A.current=ge(d,A.current),!1),[G]),V=N(()=>{if(C.current)return!1;let d=t.current;if(!d||A.current===!1)return!1;if(A.current===null){if(!he(d))return A.current=!1,!1;A.current=!0}G(),R.current=!1;let u=d.getContext(\"webgl2\");if(!u)return z({canvas:d,warnMessage:\"[WeatherEffectsCanvas] WebGL2 not supported; rendering without effects.\"});if(a.current=u,u.isContextLost())return z({canvas:d,contextLost:!0,markInitFailed:!1});if(w.current.celestial=B(u,O,se),w.current.cloud=B(u,O,oe),w.current.rain=B(u,O,ie),w.current.lightning=B(u,O,re),w.current.snow=B(u,O,le),w.current.composite=B(u,O,ue),!w.current.celestial||!w.current.composite)return u.isContextLost()?z({canvas:d,contextLost:!0,markInitFailed:!1}):z({canvas:d,errorMessage:\"Failed to create required WebGL programs\"});let y=k.current.dpr??window.devicePixelRatio,g=Math.max(1,Math.floor(d.clientWidth*y)),m=Math.max(1,Math.floor(d.clientHeight*y)),E=ce(u,g,m),x=ce(u,g,m);if(!E||!x)return u.isContextLost()?z({canvas:d,contextLost:!0,markInitFailed:!1}):z({canvas:d,errorMessage:\"Failed to create WebGL framebuffers\"});T.current.a=E,T.current.b=x;let I=u.createTexture();if(I){u.bindTexture(u.TEXTURE_2D,I),u.texImage2D(u.TEXTURE_2D,0,u.RGBA,1,1,0,u.RGBA,u.UNSIGNED_BYTE,new Uint8Array([128,128,128,255])),r.current=I;let _=new Image;_.crossOrigin=\"anonymous\",_.onload=()=>{let S=a.current;!S||r.current!==I||(S.bindTexture(u.TEXTURE_2D,I),S.texImage2D(u.TEXTURE_2D,0,u.RGBA,u.RGBA,u.UNSIGNED_BYTE,_),S.generateMipmap(u.TEXTURE_2D),S.texParameteri(u.TEXTURE_2D,u.TEXTURE_MIN_FILTER,u.LINEAR_MIPMAP_LINEAR),S.texParameteri(u.TEXTURE_2D,u.TEXTURE_MAG_FILTER,u.LINEAR),S.texParameteri(u.TEXTURE_2D,u.TEXTURE_WRAP_S,u.REPEAT),S.texParameteri(u.TEXTURE_2D,u.TEXTURE_WRAP_T,u.CLAMP_TO_EDGE),i.current=!0)},_.src=\"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAZABkAAD/4QB0RXhpZgAATU0AKgAAAAgABAEaAAUAAAABAAAAPgEbAAUAAAABAAAARgEoAAMAAAABAAIAAIdpAAQAAAABAAAATgAAAAAAAABkAAAAAQAAAGQAAAABAAKgAgAEAAAAAQAAAQCgAwAEAAAAAQAAAIAAAAAA/+0AOFBob3Rvc2hvcCAzLjAAOEJJTQQEAAAAAAAAOEJJTQQlAAAAAAAQ1B2M2Y8AsgTpgAmY7PhCfv/iAdhJQ0NfUFJPRklMRQABAQAAAcgAAAAABDAAAG1udHJSR0IgWFlaIAfgAAEAAQAAAAAAAGFjc3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD21gABAAAAANMtAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACWRlc2MAAADwAAAAJHJYWVoAAAEUAAAAFGdYWVoAAAEoAAAAFGJYWVoAAAE8AAAAFHd0cHQAAAFQAAAAFHJUUkMAAAFkAAAAKGdUUkMAAAFkAAAAKGJUUkMAAAFkAAAAKGNwcnQAAAGMAAAAPG1sdWMAAAAAAAAAAQAAAAxlblVTAAAACAAAABwAcwBSAEcAQlhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z1hZWiAAAAAAAAD21gABAAAAANMtcGFyYQAAAAAABAAAAAJmZgAA8qcAAA1ZAAAT0AAAClsAAAAAAAAAAG1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANv/AABEIAIABAAMBIgACEQEDEQH/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgv/xAC1EAACAQMDAgQDBQUEBAAAAX0BAgMABBEFEiExQQYTUWEHInEUMoGRoQgjQrHBFVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4eLj5OXm5+jp6vHy8/T19vf4+fr/xAAfAQADAQEBAQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/xAC1EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2wBDAAQEBAQEBAgEBAgLCAgICw8LCwsLDxIPDw8PDxIWEhISEhISFhYWFhYWFhYbGxsbGxsfHx8fHyMjIyMjIyMjIyP/2wBDAQUGBgkICQ8ICA8kGRQZJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCT/3QAEABD/2gAMAwEAAhEDEQA/APUAR3P5U8DPQ0n3uQKeAw5FYliiNj3pwh4yzCoiX7Gm7uxagCYhAetRniqxJJ45p4YdDQBIMnvilBb1pgJ6dqdszyOlAFlYpHHy81IInHUiqPzqecinjcxyQRQBa8l88HNJ5cg6ipI4ZWXPSl3SoMNk0AQ+U/emmNuxFPM6Z5U0/wA1W+6KAKxjYdaQKKvqV9BUyrAwGBzQBleTIRkYpRbSHuK0JWROBWe05zgUAOFu3cikMYHSoDO3bn6UwSs3DUATlcU3kHHFChugH51MsUj8KCfwoAj4xx1pOvWrS2kx6rQbSXnAP60AVghPIp2188Gpfs7A98UG3f7wz+NAERVwOWpu2SpAj5wacsj9h0oAi2MOtN2NntVmSaXaRswfXFVt8x42n8qAAr9KafbFO3L0kzmjeoOAM0Af/9D0uO9QBmjQyY564AqIXY5LIc+3IFWodPiEW6MhmIBIGR9QTVCVJN3lj5cHt/jWJRaghNy+6OTb9aq3kD28nlAkN6nimTRz2wUl1cHn5f5GoxLO5II4Y5DHmgZAGkHyknPX2pjSMgJBJzxyf5VbuJDGm4ckjnA4qGOFXBZjtXg4Pf6UAEUyrCGLnce2efyrRtbkOSBIFIHAP+NYrQyBmfB2/wANRIWi5b6daAN99Rtkz5hJPb0z70q6jA4wT+vFc3qE9tFhQfmJxuzjtnoe1ee6pqUUlztjcqV+UlTwfwoA9zOpbEwuBjuDn9Kz7jWAU2lsf7XpXkkPiC4SIRKwIAxyOeKqx3OrXc4Q/MOvBxkfiaAPSJ9ZJAVZATzkAiqj69cQgqULAjP4VnJZafHH9pvFVQBlhnge9Z8+saaUJDLtjHGBj6cHGaANA+JrpmBB2jH3c1cTxdNAyqqvk1yNtr0HnFRFGwK/K5+8D9elaOmanFJKEvnDHOfmG0EHoKLhY6seITcfvHJLemaZ/bUXkbQdpB5JNY91aQxkzQZKljwP5Cqy2SyI27dvz07fnQB0sOrQbsSTAY7j0rZsJLW4kKozEeleYPZbnxnJJwAPX3NdbpdutrEpDEnGAc8Z9qAPTUW3t0Xcxk3c4bjA+g71BLrIh5jUbM9gOnvXI3N/cQ2+6XJRxtGDyCDzii2uo5h8nzRKAeTkn2OadxWO0bxJb2sP2mcqI+hyas2viXQ73DW4yTxzwT+Ga87kWOUk3MYaPsjdP/11q28Vnbxg2sKgj06gfjRcLHpatbT8ggDHI71TuEtFiLK5LDqFx+FZVhcRso80lTnHByatzSxTzboQAeFPr+vamIx5XQAlWzWTcXSxE4IJ9fSte7tn80xuwBHGF5H5isebSXj/AHj9OceuKQxsd85+Zzk/7XQ0pvvXI5xwOBWbdCVwWVcAHjFVIpCWHmH5h17/AMqQzVeeR8spIHbpViG9hEbSsc44xms6TYj+WGzkfT+dO+wB4PNJ6YOOetAH/9H22KHZaiSFxuYYYcDHv6Vz9xAqS7d2Se471oXFnd7P3QyU/hB6g+5qa1s3dTNcKVbGADx071mO5mQWzs4D/dJq/eQ2kMPykMcYB9PpUltbpubPC46+/wCNUnUStszz+dIDGWZoWKj5QDlTVFrqaGUySKAW9eQfWti7ljKruUEg44OPzrPmBlRtqfMvIB6kUDHS6uv2UQyDLHPSsySNjGDnBByMdqSZhJEAwG/kEfy6U+G1d1YSbgMcfWkM5vUvMSMzTHOe9eXX1zFJIRbpsPRlznJ65r0DxTBcRweUgDc8k+h44/xrhBZQ2sf2iIOmM4Y4OT64PY0wuVYpHBEhOAD1qzZz+XcJcZ+63P0rGMjht2Tye/NdFpdhLfLI0S/LGpYge3WmI2bzWY/sjhTu3DZgn1rjJzNjaTleuK0JkSFgsmNvU468VVdfNO7Bwfun1x2oQBaPtwD92t7TtksnznocHFYUMa79r/KM9evauh0y1i2efGwd88j+lFgudxY3YfdDPxsBLZ4OAOtU28Q2d0Gt4ZGXBO3IwMeorm9YuHgtRcOGiMnylSOSK5SJwwyD8q8/hSGeg2uo4Z1bBI5bHOP6VtaTJ9tUtjDDkKDjI9a4Dw9fzLqJgwpjmHOeoFewxraWkYKlWUDsaVh3I9pknXePlA+6eaS5sUt/3bgjnzMDIGfSrEl7al/MhBJwODx2rON7aXsmTJlgCzAHoBQI2YLq3dApQ7vXt+RrQ6qp+UZ/CqFs0cwUKBnsRj9a2AtvAN0pDMWxjofp1xxTEXtLhF1ItsDyT8pJA596276xW2uxCrbnHX0zXnE15MzFogUO7a2e47kcc1q2F7cSLiVsunGWHJA757+lMRruLqKYhjtPas+7mnkcFGwcdK1o7qF0CTHb7nsfaufcP5nJJGePekMf5kiALJkj26USpYzAErgkcsOOlF9f3un6dJcRpuYIQF77Tzx7VxvhvUdUvb2Q6ivyHoG4x9PwoA7FbLfHvhG4KMinRLO58vBDYziiCd4bp1hG4HkAdvatm1VnidpwFLcAg96BH//S9nBkmugkZJXGD6Efp36VNJcO2+DcUCAFR7+mD+VUZrz7NJsixkd/X/8AXSAT6hOyD+6OeMisxmQ15dPJ5eB68Dt6cU6KVtv7vOV5NPmWSM7Y1xzyfXtV6G1jk+WNAu4cEUgMyC3NxIF3AHr83Hv3rTBiOOieh9ar3lrIk/lqCSVxn0PpWcs0Sv5Mm5iQev8ACaYFq+tooxvhZc4wcDFY8MkvmSLjIxycn9a33sxOFO8A8Ak1RvrGK3la3hkBGeWXJBHrSAw79Le5BBRfugN15z3rxnXrG4hlYfNtRsKPRT6ete3ReRG4acZTv71W1Gzt7/DMioG4B68DpzTGfNl3LLJIZpPyq5aX0vEaEgH1q7r9lDpl/JCuJADjHTGe9Yi7GXOMUxHThGubYzDl1Hp1rZ0/TjeWoctlVOMAfrmq2janaQ2TRPGCSD9citLTLlIDhSQDzjjb9KAGyaDJEMTNzjINULWbT9NuvMckSE4X06dutbWtarqLSxmYgj7ob2H0rmdPJ+2Pc6igZSTsIHA9OtAC3t6mtXAiuXVepBY8cfTvVC6t4rSx88ybfMJ+Qr6dMEdf6V6Lb+FrHVZkulUKTnnH5Vz/AIy0naisBhYzg47/AEoQHG2GpiyuBcgbgBjPQfUCu38N+JEuLuSC4ZSjYbDYG78a86is5biVYIh14Vfc/wCNaun2JtbkrcJjZy28cD1/GhgeseMhHDZxXFhu8thk/j1Bwe1eZ2p1O9vVgsc7znp04557YrpbbxXBb/6DeBnt+gYgcZ9fatX7dp/2fzdMZUP3V4wCTwPQ0hnQ2Ms1lp0cgGZMDIHbHBwKz5PFnk6g0F0D5XB4HzZ9MngGp9MmaHC3khdm5yB/X1PpWRrt3plrOC+5gDgjgjB5z9aQHUwX8GpOZLQ5U/d3DnNaFnJIDsJI55z0NYGkCK7aO4twscbAfh/9eutFsrW7zZ4Q8YHXnr6UAbr2iBNzZ5HTtz3qqYWiCn5Suc+vFdPpsLXumB5Tlhzk9SD2/CsSdWyykBlOeO/FMRmSXUflEOo3dm9vp0xVMxwyRqUO0k42gdvUn0q0kUUgLdMc4PpTPLAcY4J6n+lIZuQadGWVkP3uTxW3HY2bRM3mMhRTheoY+/pWNZyAJ5cj8DOw/wC0fX2q59sEsojcBM8Hb0FAj//T9MljFxOWUnr0rUms5reDejbRgYIrl7TWFtRtuCSMjgcnkVpnWXubVg5wcDYKzGXJJS8Cjk+/uPWoftQhbeCRx271jLqZiUgttLnketLPeW38QOOOaQzTn1Tz3z06ZH9cViTM3nBu5PBHvUTgTMrRMNvTJq3bwyCX1dBnPtQBuWtvcSwhpBlAMA1JLalE+ZcAdT61LZ3UkMJGAwXnBrktQ8ZXcF99kuEMYEgCLn74PQ0xE1+iMwQnHGQM9RWDefbrW3l/swh5iAAHJ2gd8e9bktpK7B9nJ/eLg/dyeR+lVvInfcsQGRznpSGebeKtF8y1S4jx5u0NIVyBuxz17ZrzSOFxFhlPXIbsfXmvo66sruK1ZZQG3/eBGQD7Zrh20ZJZRjaka9AeAO5NMDy2FdqhuQAfz9a6J9kFkJ1cbnbbtX+Zq1qd7pAjFpEN5V8nAIHPcGn2OmJAd14BINoZUJ7n+lMRPHLEdOaVPnKHJB5NdhajTLmxV1jxGQOOvP4+teevJJaBrf7pbKNtx/Ou48MwSSWX2iVsLjGBngDvz60hnWWKwiBkDhdoyPX6VpwWcE8Lm4QvwdgxwT3rHhEKbCdzZ6gDFaMU8kRBf7q9Ae9IRwg0s2es/bZYhD1K44A7ZqbW9Nku7DzYCJFwCcHkd+faux8Tqbyy860+VgD5nYfQVyWkrc2kElvPEQXA8vceGx1pjPLxZPJceWy8jr+Fej6XoE0NsLomMq5wqE5I96S7t45WWCKHG/AEmOnrmtL7JeWDxxNjGOcc5PfnpQBNBA6rnaOP/wBXSsa70CSW+WR4w6NjPbn/AOtXQ2SNKf3nBBAB9B61uRfZow0bvufjHHGPrSAy4bL7LAIyPkUbgAf84q7bSSTwGNGwPTPpVxoUvSBbnnONvrzWK8kUKvCQd/QYOKBnbeFdVjgne3u5PlxxUuqX6PcGWIDA44/nXLWtmRbiXcNzEj6Cn/a4prx7BCTLGhdhtONvAzn8aCS698SohwDQlyQyhyAqtzj0rOwPNV5Bt46j26U590UaTngvklRzgCgZu3UEd9D9pQFIw3y9j7UtoclGd84ODn+tY8eog4gQ/wD1h64qW9vfKh8lGXaCNrD7x9d2ffpQB//Uha/dIWBAyT0znp3zV2K+kazMjt90Y5NZv2FEtWEj5IXo3f2zVGFUi0zygDlGBwPQdqxLNwTSuVEfIGCc9f1rsLLy5Ysy4feDkDsa8wtphPdNeKkiHABBPy4HsD1rrNMvpYpQxPHUgdPSgLHUTpDbRGa2B2njp+dUzdTyMXLcHimy3akAAgw/1qE7G3FOMU7isdHY3cSR4Y81mmO1vrz7RdRxOEO4HYdynt1rHD7ejcVet7luYlOCeo9aAsdDPOhi+QbeMcd658S+XICg5zxnmpy0u0Kx6fyqB49i5P3X5/GgDVvtcmuYFjkCr5Y7cZ96yLuyiubYyROA2CHzg57dDUcwitLaSQ/vFIBBI5GOwrLjl+0Q+bKdqt1+XBH9aLgecz+GZZrgIMbT0OOvrWgtrCZhBC/+rIG5sndkcqPpXdWGnwGPEbkqTkZzkc9BV2CxjSbcqxnOexB+vemBwl34J1Dz87iVcgjHcevNei6D4PnsbMiQtIMjJHYe9dXp1qmxVPzZbAB46/yrfaVdrwvhTnBA5HFAjjJ7OCGT7Rt3ArgL0/Ks5bd5yZbgFUToP5V09zECCydF5+bp+VYUiRsgLuCWOOD0/CkMZPOrIscyjaBwo9feqIhBIuDjBBwOuP8ACppEiSTy13Oeh9PwxVmK1ZwBGeB2HX3oA0rPSLe9tTNwAuPTOe2KgvNIELLbSJgkZyey1HJeSafBhMu2eM9qyrnUZbqTzG3MSMdc9P5UAVLi3IudkAypOMD2qSeS2VBJEQsijkHnpwao3GoLbjefkJ4AYZzn0rlroySL50y4ZT8rA+2frzSGdHBqSyqZLZ+Aeo7Gqduly0rNIDM5J24PT/Gudtne5jLgksOuM8//AF66PTLnyJ8qDg4AByeT9aANuGO5ZNzuYyP4SO/XirUd08cTc5Vhg49RS3UxYBs8sOMfrWcy74QAASQRnOKLisawlikO8yhUx8zN2qnBdwXLC3RtwJ+929KbZaZLqdrJCnzooO72zwahisILC2KoQ204XGcj8+tMDPutStbHfFcEb87R1yQfTAqOS4XcVdQQB1ycAnvVG/kW4nG7JIx7ep6entTJ4VmiVUchjywBxx7d6LhY/9VJ3W6Kxr8pUHPPBx9axpV2wF3AGTjg8f5NdZNpuYQMAM/Uf/qpTpURsyFXJB9OKxNDiYUjEu1Mn6Vpxl1GR0HcHrWimkuCPLQ4P4VchsGg+eVDx0+tAxYrxGRQwGe4HetmNYg6yEjkZHqPY1lLZu7iRc1rJbuq7SCR60CJEiR8Lnn36CmSAq2yMKeeo6gd8Vowo6jYq5A7mphaRSOxA4XqcUCKEgkb5iSe2Cc1KAiwHglqufZj0HOcEZPI/GtKKCIoRjB75FAjkY1Z3IPyjvxmszUYDCpbBwfQYye1egLp0BIIY/SpGtI5P3Zj3Y78dKdgucJZRkKsnpjj+lblvHJNLnGB6ntWtPZx42ABQP7oH86kt7ZPLMeMf1+tAF6w2hSjEsg5LY6n/CrbTxIHUKDgcc/zqxZ2yC3KuQc44HpVW/hIYLAQFP5/jimSZ93LE8S9yRyMn8qwDbxM5ES7efWt5opEGTz+HH5VHLCUX5EAU9D9e9IZkIrW7HDYJ6fWrf2iW2BYsAWHBxStavjenT3FAtGkYB+MdD1oGYk4LzBrjcU7kHr9KqXLrDAPs8W45+8Tz+Vdf/ZJfAYg56CobizmiBVMEdsdqQHlGsTTrcp5m4qvzAkYU8cj35pdt9JceaMCJgOMEH1xzXpt1ZW8tuqyxrI6Hdn0/WuXvbS7kkCkM69if4fp3/KgZlCyRmWVcEnOVHA2/j/SrcVvFMcxAgKCBznH9DV+PQpZ4d0n3R8uC2fzB5/GrltpU9rJlJDIuO/IoAq2tvKUZ5I9wX1zxx7d6ikec/Jjan3vX2rr9NmntcsEUqx5Mg3D8qTVdMkZVuRt2HjKj+X/ANegLnM7H8tQjAcHp79qzGSRnKzg8fwgnH/666NbcxKfKRj6Fhz+dUJYLi6ZowG3nkn1P1oA5+eKBZRcSbBIRt+U7jj39DWeqk3ZdxuzyMjP61rS6GYiZpIzlxyDnj1x/wDrq1FZukJSBQnselAz/9b0o2jN8xaoZ7cLgE9eeK1fJc8bc054JD/BWJZjrbnpxUnljO09KvtbyHHy/pSGGYcBaAKWyJRwPyFMx6Zq+YG/uH8qPJJ42H8qAKwQuMck1KEkU8jH1FTCGRfuqfyNKY58520AMXOeTj6UpJXlSc0FJP7lO2SY+7QBNHdNHx14xzQ8xcj+QqHy5M5CU0pJ0xQA7gnBJNAWTPXApojkqQIR97NAFhfMUD5qP3hPbBquQxOQDT18wHjNAEwtmPJOakMLnHPSoC7Be9O+0MBhQ35UAPMORnPIqJkYnnOKDNIeMH8qZh2GDmgBCWH3CRVciVyScmrGxsYCZ/OmhJgfu4oArmEvxnn3pHtXPIq7sk/u0bZP4hmgDIktmAwCTTxEQowea0jHnquKhMGTwpIoAriNjyPzqU71Xb94elPMD54WgRygcjNAEId8bPuimBFJzzmrDRyN/CfwpFidf4TQA9RvXYfzqm9qoyODV8L/ALJpfKJOQMUAf//Z\"}let P=new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]),L=u.createBuffer();if(!L)return u.isContextLost()?z({canvas:d,contextLost:!0,markInitFailed:!1}):z({canvas:d,errorMessage:\"Failed to create WebGL buffer\"});h.current=L,u.bindBuffer(u.ARRAY_BUFFER,L),u.bufferData(u.ARRAY_BUFFER,P,u.STATIC_DRAW);for(let _ of Object.values(w.current)){if(!_)continue;let S=u.getAttribLocation(_,\"a_position\");S>=0&&(u.enableVertexAttribArray(S),u.vertexAttribPointer(S,2,u.FLOAT,!1,0,0))}return s.current=performance.now(),!0},[G,z]),F=N(()=>{let d=a.current,u=t.current,y=w.current,g=T.current,m=k.current;if(R.current||!p.current){b.current=!1,n.current=0;return}if(!d||!u||!g.a||!g.b){b.current=!1;return}let E=m.dpr??window.devicePixelRatio,x=Math.max(1,Math.floor(u.clientWidth*E)),I=Math.max(1,Math.floor(u.clientHeight*E));(u.width!==x||u.height!==I)&&(u.width=x,u.height=I,fe(d,g.a,x,I),fe(d,g.b,x,I));let P=(performance.now()-s.current)/1e3,L=(ne,$e)=>Q(d,ne,$e);m.layers.lightning&&m.lightning.enabled&&m.lightning.autoMode&&P>=l.current&&(c.current=P,o.current=Math.random(),l.current=P+m.lightning.autoInterval*(.5+Math.random()));let _=g.a,S=g.b,X=()=>{let ne=_;_=S,S=ne};m.layers.celestial&&y.celestial?(be({gl:d,program:y.celestial,target:S,displayWidth:x,displayHeight:I,time:P,params:m.celestial,moonTexture:r.current,moonTextureLoaded:i.current,getUniformLocation:L}),X()):(ve(d,S,x,I),X()),m.layers.clouds&&y.cloud&&(we({gl:d,program:y.cloud,target:S,sceneTexture:_.texture,displayWidth:x,displayHeight:I,time:P,params:m.cloud,celestial:m.celestial,getUniformLocation:L}),X()),m.layers.rain&&y.rain&&(xe({gl:d,program:y.rain,target:S,sceneTexture:_.texture,displayWidth:x,displayHeight:I,time:P,params:m.rain,interactions:m.interactions,getUniformLocation:L}),X()),Ie(m.layers,m.lightning,y.lightning,P,c.current)&&y.lightning&&(Re({gl:d,program:y.lightning,target:S,sceneTexture:_.texture,displayWidth:x,displayHeight:I,time:P,params:m.lightning,interactions:m.interactions,lastFlashTime:c.current,strikeSeed:o.current,getUniformLocation:L}),X()),m.layers.snow&&y.snow&&(ke({gl:d,program:y.snow,target:S,sceneTexture:_.texture,displayWidth:x,displayHeight:I,time:P,params:m.snow,getUniformLocation:L}),X()),y.composite&&_e({gl:d,program:y.composite,sceneTexture:_.texture,displayWidth:x,displayHeight:I,time:P,celestial:m.celestial,interactions:m.interactions,post:m.post,lastFlashTime:c.current,strikeSeed:o.current,getUniformLocation:L}),p.current&&!R.current?(b.current=!0,n.current=requestAnimationFrame(F)):(b.current=!1,n.current=0)},[Q]);return Ke(()=>{let d=t.current;if(!d)return;let u=m=>{m.preventDefault(),R.current=!0,G()},y=()=>{R.current=!1,C.current=!1,V()&&p.current&&(b.current=!0,F())};d.addEventListener(\"webglcontextlost\",u,{passive:!1}),d.addEventListener(\"webglcontextrestored\",y);let g=typeof IntersectionObserver<\"u\"?new IntersectionObserver(m=>{let x=!!m[0]?.isIntersecting;if(p.current=x,!x){U(),G(),$(d);return}!b.current&&!R.current&&(a.current&&T.current.a&&T.current.b||V())&&(b.current=!0,F())},{threshold:0}):null;return g?g.observe(d):p.current=!0,!g&&V()&&p.current&&(b.current=!0,F()),()=>{g?.disconnect(),d.removeEventListener(\"webglcontextlost\",u),d.removeEventListener(\"webglcontextrestored\",y),G(),$(d)}},[G,V,$,F,U]),t}var De={celestial:!0,clouds:!0,rain:!1,lightning:!1,snow:!1},Te={timeOfDay:.5,moonPhase:.5,starDensity:.5,celestialX:.74,celestialY:.78,sunSize:.14,moonSize:.17,sunGlowIntensity:3.05,sunGlowSize:.3,sunRayCount:6,sunRayLength:3,sunRayIntensity:.1,sunRayShimmer:1,sunRayShimmerSpeed:1,moonGlowIntensity:3.45,moonGlowSize:.94,skyBrightness:1,skySaturation:1,skyContrast:1},Pe={coverage:.5,density:.7,softness:.5,cloudScale:1,windSpeed:.3,windAngle:0,turbulence:.3,lightIntensity:1,ambientDarkness:.2,backlightIntensity:.5,numLayers:3},ze={glassIntensity:.5,glassZoom:1,fallingIntensity:.6,fallingSpeed:2,fallingAngle:.1,fallingStreakLength:1,fallingLayers:4},Ee={enabled:!1,autoMode:!0,autoInterval:8,flashIntensity:1,branchDensity:.5},Ae={intensity:.5,layers:4,fallSpeed:.6,windSpeed:.3,windAngle:.2,turbulence:.3,drift:.5,flutter:.5,windShear:.5,flakeSize:1,sizeVariation:.5,opacity:.5,glowAmount:.25,sparkle:.25},Ge={rainRefractionStrength:1,lightningSceneIllumination:.6},Le={enabled:!0,haze:0,hazeHorizon:.8,hazeDesaturation:.35,hazeContrast:.6,bloomIntensity:0,bloomThreshold:.82,bloomKnee:.35,bloomRadius:1.2,bloomTapScale:1,exposureIntensity:0,exposureDesaturation:.25,exposureRecovery:1,godRayIntensity:0,godRayDecay:.965,godRayDensity:.9,godRayWeight:.35,godRaySamples:16};function M(e,t){if(!t)return{...e};let a={...e};for(let n of Object.keys(t)){let s=t[n];s!==void 0&&(a[n]=s)}return a}function We(e){return{layers:M(De,e.layers),celestial:M(Te,e.celestial),cloud:M(Pe,e.cloud),rain:M(ze,e.rain),lightning:M(Ee,e.lightning),snow:M(Ae,e.snow),interactions:M(Ge,e.interactions),post:M(Le,e.post),dpr:e.dpr}}import{jsx as qe}from\"react/jsx-runtime\";function Fe({className:e,dpr:t,layers:a,celestial:n,cloud:s,rain:c,lightning:l,snow:o,interactions:r,post:i}){let h=Ce(We({dpr:t,layers:a,celestial:n,cloud:s,rain:c,lightning:l,snow:o,interactions:r,post:i}));return qe(\"canvas\",{ref:h,className:e,style:{width:\"100%\",height:\"100%\"}})}var de={clear:{dawn:{celestial:{celestialX:.69,skyBrightness:1,starDensity:.36,sunGlowIntensity:3.47,sunGlowSize:.49,sunRayCount:4,sunRayShimmer:5,sunRayShimmerSpeed:5},glass:{blur:4.5,brightness:.95}},dusk:{celestial:{celestialX:.69,skyBrightness:1,starDensity:.36,sunGlowIntensity:3.84,sunGlowSize:.43,sunRayCount:4,sunRayIntensity:.24,sunRayLength:1.37},glass:{brightness:.9}},midnight:{celestial:{celestialY:.71,moonGlowIntensity:3.23,moonPhase:.51,skyBrightness:.44,skyContrast:1,skySaturation:2,starDensity:1.72,sunGlowIntensity:1.59}},noon:{celestial:{celestialY:.71,moonPhase:.2421,skyBrightness:.8,skySaturation:1.9,starDensity:.14,sunGlowIntensity:1.72,sunGlowSize:.69,sunRayCount:4,sunRayIntensity:.19,sunRayShimmer:5,sunRayShimmerSpeed:5},glass:{brightness:1}}},cloudy:{dawn:{celestial:{celestialX:.69,celestialY:.74,skyBrightness:1,skyContrast:1.1,skySaturation:.88,starDensity:.36,sunGlowIntensity:2.4,sunGlowSize:.38,sunRayShimmer:5,sunRayShimmerSpeed:5},cloud:{ambientDarkness:1,backlightIntensity:1.07,cloudScale:1.73,coverage:.81,density:1.1,lightIntensity:.67,numLayers:1,softness:.42,windSpeed:.04},glass:{blur:3,brightness:.95},post:{bloomIntensity:.99,bloomKnee:.58,bloomRadius:5.5,bloomTapScale:1.39,bloomThreshold:.78,exposureIntensity:1.14,exposureRecovery:1.95,godRayDecay:.857,godRayDensity:.79,godRayIntensity:1.08,godRaySamples:121,godRayWeight:.42}},dusk:{celestial:{celestialX:.69,celestialY:.74,skyContrast:.99,skySaturation:1.38,starDensity:.11,sunGlowIntensity:3.21,sunGlowSize:.47,sunRayShimmer:5,sunRayShimmerSpeed:5},cloud:{ambientDarkness:.54,backlightIntensity:.78,cloudScale:1.73,coverage:.81,density:.9,lightIntensity:.78,numLayers:1,softness:.42,windSpeed:.04},glass:{brightness:.9},post:{bloomIntensity:1.11,bloomKnee:.58,bloomRadius:5.5,bloomTapScale:1.39,bloomThreshold:.78,exposureIntensity:1.14,exposureRecovery:1.95,godRayDecay:.857,godRayDensity:.79,godRayIntensity:1.08,godRaySamples:121,godRayWeight:.42}},midnight:{celestial:{celestialX:.74,celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,starDensity:1.72,sunRayShimmer:5,sunRayShimmerSpeed:5},cloud:{cloudScale:1.73,coverage:.81,density:1.02,numLayers:1,softness:.42,windSpeed:.04},post:{bloomIntensity:1.11,bloomKnee:.58,bloomRadius:5.5,bloomTapScale:1.39,bloomThreshold:.78,exposureIntensity:1.14,exposureRecovery:1.95,godRayDecay:.857,godRayDensity:.79,godRayIntensity:1.08,godRaySamples:121,godRayWeight:.42}},noon:{celestial:{celestialX:.74,celestialY:.71,moonPhase:.2421,skyBrightness:.84,skyContrast:1.31,skySaturation:1.44,starDensity:.14,sunGlowIntensity:1.35,sunGlowSize:.61,sunRayCount:4,sunRayIntensity:.22,sunRayShimmer:5,sunRayShimmerSpeed:5},cloud:{ambientDarkness:0,backlightIntensity:.45,cloudScale:1.73,coverage:.81,density:1.06,lightIntensity:.44,numLayers:1,softness:.42,windSpeed:.04},glass:{brightness:1},post:{bloomIntensity:.42,bloomKnee:.58,bloomRadius:5.5,bloomTapScale:1.39,bloomThreshold:.76,exposureIntensity:1.14,exposureRecovery:1.95,godRayDecay:.884,godRayDensity:.79,godRayIntensity:1.33,godRaySamples:121,godRayWeight:.42}}},drizzle:{dawn:{celestial:{celestialX:.69,celestialY:.74,moonGlowIntensity:3.55,moonGlowSize:.97,skyBrightness:.96,skyContrast:.84,skySaturation:.94,starDensity:1.72},cloud:{ambientDarkness:.7,backlightIntensity:.82,cloudScale:1.21,coverage:.61,density:.98,lightIntensity:0,numLayers:1,softness:.17,turbulence:.16,windSpeed:.02},glass:{brightness:.95},rain:{fallingAngle:.02,fallingIntensity:.13,fallingSpeed:2.24,fallingStreakLength:.32,glassIntensity:.29,glassZoom:.65}},dusk:{celestial:{celestialX:.69,celestialY:.74,moonGlowIntensity:3.55,moonGlowSize:.97,skyBrightness:.77,skyContrast:.84,skySaturation:.85,starDensity:1.72,sunGlowIntensity:2.73,sunRayLength:1.45},cloud:{ambientDarkness:.7,backlightIntensity:.82,cloudScale:1.21,coverage:.61,density:.98,lightIntensity:0,numLayers:1,softness:.17,turbulence:.16,windSpeed:.02},glass:{brightness:.9},rain:{fallingAngle:.02,fallingIntensity:.13,fallingSpeed:2.24,fallingStreakLength:.32,glassIntensity:.29,glassZoom:.65}},midnight:{celestial:{celestialX:.74,celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,skyBrightness:.77,skyContrast:.84,skySaturation:.85,starDensity:1.72,sunGlowIntensity:2.73,sunRayLength:1.45},cloud:{ambientDarkness:.7,backlightIntensity:.82,cloudScale:1.21,coverage:.61,density:.98,lightIntensity:0,numLayers:1,softness:.17,turbulence:.16,windSpeed:.02},rain:{fallingAngle:.02,fallingIntensity:.13,fallingSpeed:2.24,fallingStreakLength:.32,glassIntensity:.2,glassZoom:.5}},noon:{celestial:{celestialX:.74,celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,skyBrightness:.77,skyContrast:.84,skySaturation:.85,starDensity:1.72,sunGlowIntensity:2.73,sunRayLength:1.45},cloud:{ambientDarkness:.7,backlightIntensity:.82,cloudScale:1.21,coverage:.61,density:.98,lightIntensity:0,numLayers:1,softness:.17,turbulence:.16,windSpeed:.02},glass:{brightness:1},rain:{fallingAngle:.02,fallingIntensity:.13,fallingSpeed:2.24,fallingStreakLength:.32,glassIntensity:.29,glassZoom:.65}}},fog:{dawn:{celestial:{celestialY:.71,skyBrightness:1.05,skyContrast:.41,skySaturation:1.25,starDensity:.11,sunGlowIntensity:2.42,sunGlowSize:.38,sunRayIntensity:.05,sunRayLength:1.59},cloud:{ambientDarkness:1,backlightIntensity:.54,cloudScale:.64,density:1.5,numLayers:6,softness:.77,turbulence:.67,windSpeed:.02},glass:{brightness:.95}},dusk:{celestial:{celestialY:.71,skyBrightness:1.02,skyContrast:.62,skySaturation:.88,starDensity:.11,sunGlowIntensity:2.42,sunGlowSize:.38,sunRayIntensity:.05,sunRayLength:1.59},cloud:{ambientDarkness:0,backlightIntensity:.54,cloudScale:.93,numLayers:6,softness:.5,turbulence:1,windSpeed:.03},glass:{brightness:.9}},midnight:{celestial:{celestialY:.71,moonGlowIntensity:2.74,moonGlowSize:1.43,moonPhase:.5,skyBrightness:1.02,skyContrast:.62,skySaturation:.88,starDensity:.11,sunGlowIntensity:2.42,sunGlowSize:.38,sunRayIntensity:.05,sunRayLength:1.59},cloud:{ambientDarkness:0,backlightIntensity:1.4,cloudScale:.91,density:1.27,lightIntensity:2,numLayers:6,softness:.63,turbulence:.86,windSpeed:.03}},noon:{celestial:{celestialY:.71,skyBrightness:.76,skyContrast:.47,skySaturation:.6,starDensity:.11,sunGlowIntensity:.92,sunGlowSize:.84,sunRayCount:0,sunRayIntensity:0,sunRayLength:0},cloud:{ambientDarkness:1,backlightIntensity:.72,cloudScale:.83,density:1.5,numLayers:6,softness:.84,turbulence:.86,windSpeed:.03},glass:{brightness:1}}},hail:{dawn:{celestial:{celestialX:.69,celestialY:.74,skyBrightness:.42,skySaturation:.74,starDensity:.36,sunGlowIntensity:5,sunGlowSize:.36,sunRayShimmer:5,sunRayShimmerSpeed:5},cloud:{ambientDarkness:.75,backlightIntensity:.64,density:.83,lightIntensity:.55,numLayers:1,turbulence:.55,windAngle:-3.14,windSpeed:.14},glass:{brightness:.95},interactions:{rainRefractionStrength:.96},post:{bloomIntensity:.13,bloomThreshold:.64,godRayIntensity:.56,haze:.11,hazeHorizon:1},rain:{fallingIntensity:.42,fallingLayers:1,fallingSpeed:3,fallingStreakLength:.56,glassIntensity:1,glassZoom:.69},snow:{drift:.63,fallSpeed:8,flakeSize:.77,flutter:.79,glowAmount:.29,intensity:.81,layers:8,opacity:1,sizeVariation:.48,sparkle:.44,turbulence:.91,windAngle:2.23,windShear:.78,windSpeed:.99}},dusk:{celestial:{celestialX:.69,celestialY:.74,skyBrightness:.47,skyContrast:1.39,skySaturation:1.42,starDensity:.11,sunGlowIntensity:3.21,sunGlowSize:.51,sunRayShimmer:5,sunRayShimmerSpeed:5},cloud:{ambientDarkness:.75,backlightIntensity:.64,density:.83,lightIntensity:.79,numLayers:1,turbulence:.55,windAngle:-3.14,windSpeed:.14},glass:{brightness:.9},interactions:{rainRefractionStrength:.96},post:{bloomIntensity:.13,bloomThreshold:.64,godRayIntensity:.56,haze:.15,hazeHorizon:1},rain:{fallingIntensity:.42,fallingLayers:1,fallingSpeed:3,fallingStreakLength:.56,glassIntensity:.72,glassZoom:.69},snow:{drift:.63,fallSpeed:8,flakeSize:.77,flutter:.79,glowAmount:.71,intensity:.47,layers:8,opacity:1,sizeVariation:.77,sparkle:.1,turbulence:.91,windAngle:2.23,windShear:.78,windSpeed:1}},midnight:{celestial:{celestialX:.74,celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,skyBrightness:.42,skySaturation:.74,starDensity:1.72,sunRayShimmer:5,sunRayShimmerSpeed:5},cloud:{ambientDarkness:.75,backlightIntensity:.64,density:.83,lightIntensity:1.21,numLayers:1,turbulence:.55,windAngle:-3.14,windSpeed:.14},interactions:{rainRefractionStrength:.96},post:{bloomIntensity:.13,bloomThreshold:.64,godRayIntensity:.56,haze:.14,hazeHorizon:1},rain:{fallingIntensity:.42,fallingLayers:1,fallingSpeed:3,fallingStreakLength:.56,glassIntensity:.72,glassZoom:.69},snow:{drift:.63,fallSpeed:8,flakeSize:.77,flutter:.79,glowAmount:.71,intensity:.47,layers:8,opacity:1,sizeVariation:.85,sparkle:.1,turbulence:.87,windAngle:2.23,windShear:.78,windSpeed:1.11}},noon:{celestial:{celestialX:.74,celestialY:.71,moonPhase:.2421,skyBrightness:.56,skyContrast:1.03,skySaturation:.62,starDensity:.14,sunGlowIntensity:2.5,sunGlowSize:.54,sunRayCount:3,sunRayShimmer:5,sunRayShimmerSpeed:5},cloud:{ambientDarkness:1,backlightIntensity:.21,coverage:.74,density:.95,lightIntensity:.6,numLayers:1,softness:.36,turbulence:.55,windAngle:-3.14,windSpeed:.14},glass:{brightness:1},interactions:{rainRefractionStrength:.96},post:{bloomIntensity:.13,bloomThreshold:.64,godRayIntensity:.56,haze:.23,hazeHorizon:1},rain:{fallingIntensity:.42,fallingLayers:1,fallingSpeed:3,fallingStreakLength:.56,glassIntensity:.72,glassZoom:.69},snow:{drift:.63,fallSpeed:8,flakeSize:.77,flutter:.79,glowAmount:.71,intensity:.8,layers:8,opacity:1,sizeVariation:.77,sparkle:.1,turbulence:.91,windAngle:2.23,windShear:.78,windSpeed:.89}}},\"heavy-rain\":{dawn:{celestial:{celestialX:.69,celestialY:.74,moonGlowIntensity:3.55,moonGlowSize:.97,moonPhase:.3776,skyBrightness:.87,skySaturation:.56,starDensity:1.72,sunGlowIntensity:3.75,sunGlowSize:.43},cloud:{ambientDarkness:1,backlightIntensity:1.35,cloudScale:1.52,coverage:.53,density:1.17,lightIntensity:.06,numLayers:1,softness:.25,turbulence:.45,windSpeed:.05},glass:{brightness:.95},interactions:{rainRefractionStrength:1},post:{bloomIntensity:.36,godRayDecay:.838,godRayDensity:.56,godRayIntensity:1.11,godRaySamples:35,godRayWeight:.66,haze:.14,hazeHorizon:1},rain:{fallingAngle:.3,fallingLayers:6,fallingSpeed:3,fallingStreakLength:2,glassIntensity:.98,glassZoom:.58}},dusk:{celestial:{celestialX:.69,celestialY:.74,moonGlowIntensity:3.55,moonGlowSize:.97,moonPhase:.4115,moonSize:.17,skyBrightness:.96,skyContrast:.99,skySaturation:.72,starDensity:1.72,sunGlowIntensity:4.15,sunGlowSize:.35,sunRayCount:6,sunRayIntensity:.1,sunRayLength:3,sunSize:.14},cloud:{ambientDarkness:1,backlightIntensity:1.8,cloudScale:2.02,coverage:.72,density:.94,lightIntensity:0,numLayers:1,softness:.17,turbulence:.51,windAngle:0,windSpeed:.05},glass:{brightness:.9},interactions:{rainRefractionStrength:1},post:{bloomIntensity:.28,godRayWeight:.55,haze:.21,hazeHorizon:1},rain:{fallingAngle:.3,fallingIntensity:1,fallingLayers:6,fallingSpeed:3,fallingStreakLength:2,glassIntensity:.98,glassZoom:.58}},midnight:{celestial:{celestialX:.74,celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,moonPhase:.3776,moonSize:.17,skyBrightness:1.11,skyContrast:1,skySaturation:.78,starDensity:1.72,sunGlowIntensity:3.05,sunGlowSize:.3,sunRayCount:6,sunRayIntensity:.1,sunRayLength:3,sunSize:.14},cloud:{ambientDarkness:1,backlightIntensity:.81,cloudScale:2.16,coverage:.72,density:1.5,lightIntensity:0,numLayers:1,softness:.37,turbulence:.55,windAngle:0,windSpeed:.05},glass:{blur:1,brightness:1.15},interactions:{rainRefractionStrength:1},rain:{fallingAngle:.3,fallingIntensity:1,fallingLayers:6,fallingSpeed:3,fallingStreakLength:2,glassIntensity:.98,glassZoom:.58}},noon:{celestial:{celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,moonPhase:.3776,skyBrightness:.81,skyContrast:.54,skySaturation:.36,starDensity:1.68,sunGlowIntensity:3.7,sunGlowSize:.22,sunRayCount:0,sunRayIntensity:0,sunRayLength:0},cloud:{ambientDarkness:1,backlightIntensity:.59,cloudScale:1.8,coverage:.58,density:.87,lightIntensity:0,numLayers:1,softness:.2,turbulence:.24,windSpeed:.05},glass:{brightness:1},interactions:{rainRefractionStrength:1},post:{godRayIntensity:1.24,godRaySamples:0,godRayWeight:.6},rain:{fallingAngle:.3,fallingSpeed:3,glassIntensity:.98,glassZoom:.58}}},overcast:{dawn:{celestial:{celestialY:.71,moonGlowIntensity:1.91,moonGlowSize:.32,moonPhase:.5,skyBrightness:1.36,skyContrast:.73,skySaturation:1.6,starDensity:.11,sunGlowIntensity:5.1,sunGlowSize:.27,sunRayCount:6,sunRayIntensity:.05,sunRayLength:.6},cloud:{ambientDarkness:0,backlightIntensity:1.41,cloudScale:1.9,density:.3,lightIntensity:.75,numLayers:8,softness:.77,turbulence:.66,windSpeed:.04},glass:{brightness:.95},post:{bloomThreshold:.81,godRayIntensity:.32,godRayWeight:.3,haze:.15,hazeContrast:0,hazeHorizon:1}},dusk:{celestial:{celestialY:.71,moonGlowIntensity:1.91,moonGlowSize:.32,moonPhase:.5,skyBrightness:1.12,skyContrast:.83,skySaturation:2,starDensity:.11,sunGlowIntensity:4.15,sunGlowSize:.28,sunRayCount:6,sunRayIntensity:.05,sunRayLength:.6},cloud:{ambientDarkness:0,backlightIntensity:1.41,cloudScale:1.9,density:.3,lightIntensity:.78,numLayers:8,softness:.77,turbulence:.66,windSpeed:.04},glass:{brightness:.9},post:{bloomThreshold:.81,godRayIntensity:.32,godRayWeight:.3,haze:.15,hazeContrast:0,hazeHorizon:1}},midnight:{celestial:{celestialY:.71,moonGlowIntensity:2.75,moonGlowSize:1.54,moonPhase:.5,skyBrightness:1.58,skyContrast:.84,skySaturation:1.15,starDensity:.11,sunGlowIntensity:2.42,sunGlowSize:.38,sunRayCount:6,sunRayIntensity:.05,sunRayLength:1.59},cloud:{ambientDarkness:0,backlightIntensity:1.41,cloudScale:1.9,coverage:.9,density:1.16,lightIntensity:0,numLayers:8,softness:.87,turbulence:.66,windSpeed:.04},post:{bloomThreshold:.81,godRayIntensity:.02,godRayWeight:.15,haze:.35,hazeContrast:0,hazeHorizon:.96}},noon:{celestial:{celestialY:.71,moonGlowIntensity:1.91,moonGlowSize:.32,moonPhase:.5,skyBrightness:1.16,skyContrast:.53,skySaturation:.88,starDensity:.11,sunGlowIntensity:5.2,sunGlowSize:.38,sunRayCount:6,sunRayIntensity:.05,sunRayLength:1.59},cloud:{ambientDarkness:0,backlightIntensity:.9,cloudScale:1.9,coverage:.95,density:.3,lightIntensity:.08,numLayers:8,softness:.77,turbulence:.66,windSpeed:.04},glass:{brightness:1},post:{bloomThreshold:.81,godRayIntensity:.12,godRayWeight:.2,haze:.15,hazeContrast:0,hazeHorizon:1}}},\"partly-cloudy\":{dawn:{celestial:{celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,moonPhase:.5,starDensity:1.72,sunGlowIntensity:3.89,sunGlowSize:.38,sunRayCount:4,sunRayIntensity:.17},cloud:{ambientDarkness:.42,coverage:.37,density:1.24,lightIntensity:1.42,softness:.28,windSpeed:.01},glass:{blur:3,brightness:.95}},dusk:{celestial:{celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,moonPhase:.5,starDensity:1.72,sunGlowIntensity:3.89,sunGlowSize:.38,sunRayCount:4,sunRayIntensity:.17},cloud:{ambientDarkness:.42,cloudScale:1.68,coverage:.37,density:1.24,lightIntensity:1.42,softness:.26,windSpeed:.01},glass:{brightness:.9}},midnight:{celestial:{celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,moonPhase:.5,starDensity:1.72},cloud:{ambientDarkness:.42,backlightIntensity:1.23,cloudScale:1.11,coverage:.37,density:1.24,lightIntensity:1.42,numLayers:5,softness:.21,turbulence:.18,windSpeed:.01}},noon:{celestial:{celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,moonPhase:.5,starDensity:1.72},cloud:{ambientDarkness:.42,backlightIntensity:0,cloudScale:1.86,coverage:.48,density:1.71,lightIntensity:.67,numLayers:2,softness:.41,windSpeed:.01},glass:{brightness:1}}},rain:{dawn:{celestial:{celestialX:.69,celestialY:.74,moonGlowIntensity:3.55,moonGlowSize:.97,skyBrightness:.96,skyContrast:.84,skySaturation:.94,starDensity:1.72},cloud:{ambientDarkness:.7,backlightIntensity:.82,cloudScale:1.13,coverage:.72,density:.94,lightIntensity:0,numLayers:1,softness:.17,turbulence:.34,windSpeed:.05},glass:{brightness:.95},interactions:{rainRefractionStrength:.76},rain:{fallingAngle:0,fallingIntensity:.84,fallingLayers:4,fallingSpeed:3,fallingStreakLength:.54,glassIntensity:.38,glassZoom:.5}},dusk:{celestial:{celestialX:.69,celestialY:.74,moonGlowIntensity:3.55,moonGlowSize:.97,moonPhase:.4115,moonSize:.17,skyBrightness:.96,skyContrast:.84,skySaturation:.94,starDensity:1.72,sunGlowIntensity:3.05,sunGlowSize:.3,sunRayCount:6,sunRayIntensity:.1,sunRayLength:3,sunSize:.14},cloud:{ambientDarkness:1,backlightIntensity:.82,cloudScale:1.13,coverage:.72,density:.94,lightIntensity:0,numLayers:1,softness:.17,turbulence:.34,windAngle:0,windSpeed:.05},glass:{brightness:.9},interactions:{rainRefractionStrength:.76},lightning:{enabled:!0},rain:{fallingAngle:0,fallingIntensity:.84,fallingLayers:5,fallingSpeed:3,fallingStreakLength:.54,glassIntensity:.38,glassZoom:.5}},midnight:{celestial:{celestialX:.74,celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,moonPhase:.3776,moonSize:.17,skyBrightness:1.11,skyContrast:1,skySaturation:.78,starDensity:1.72,sunGlowIntensity:3.05,sunGlowSize:.3,sunRayCount:6,sunRayIntensity:.1,sunRayLength:3,sunSize:.14},cloud:{ambientDarkness:1,backlightIntensity:.77,cloudScale:1.13,coverage:.72,density:1,lightIntensity:0,numLayers:1,softness:.23,turbulence:.34,windAngle:0,windSpeed:.05},interactions:{rainRefractionStrength:.76},rain:{fallingAngle:0,fallingIntensity:.84,fallingLayers:5,fallingSpeed:3,fallingStreakLength:.54,glassIntensity:.38,glassZoom:.5}},noon:{celestial:{celestialX:.74,celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,moonPhase:.3776,moonSize:.17,skyBrightness:1.12,skyContrast:.49,skySaturation:.59,starDensity:1.72,sunGlowIntensity:3.05,sunGlowSize:.22,sunRayCount:0,sunRayIntensity:0,sunRayLength:0,sunSize:.14},cloud:{ambientDarkness:1,backlightIntensity:.59,cloudScale:1.13,coverage:.79,density:1.08,lightIntensity:0,numLayers:1,softness:.23,turbulence:.34,windAngle:0,windSpeed:.05},glass:{brightness:1},interactions:{rainRefractionStrength:.76},lightning:{enabled:!0},rain:{fallingAngle:0,fallingIntensity:.84,fallingLayers:5,fallingSpeed:3,fallingStreakLength:.54,glassIntensity:.38,glassZoom:.5}}},sleet:{dawn:{celestial:{celestialX:.69,celestialY:.74,skyBrightness:.82,starDensity:.36,sunGlowIntensity:5},cloud:{ambientDarkness:1,backlightIntensity:1.27,turbulence:.56,windSpeed:.03},glass:{brightness:.95},rain:{fallingAngle:.31,fallingIntensity:.22,fallingSpeed:2.07,fallingStreakLength:2,glassIntensity:.41,glassZoom:.85},snow:{drift:1,fallSpeed:8,flakeSize:.85,flutter:.98,glowAmount:.09,intensity:.34,layers:8,opacity:.78,sizeVariation:1,sparkle:.48,turbulence:.92,windAngle:1.76,windShear:1,windSpeed:1.8}},dusk:{celestial:{celestialX:.69,celestialY:.74,starDensity:.36,sunGlowIntensity:5},cloud:{turbulence:.56,windSpeed:.03},glass:{brightness:.9},lightning:{enabled:!0},rain:{fallingAngle:.31,fallingIntensity:.22,fallingSpeed:2.07,fallingStreakLength:2,glassIntensity:.41,glassZoom:.85},snow:{drift:1,fallSpeed:8,flakeSize:.85,flutter:.98,glowAmount:.09,intensity:.34,layers:8,opacity:.78,sizeVariation:1,sparkle:.48,windAngle:1.76,windShear:1,windSpeed:1.8}},midnight:{celestial:{celestialX:.69,celestialY:.74,starDensity:.36,sunGlowIntensity:5},cloud:{turbulence:.56,windSpeed:.03},lightning:{enabled:!0},rain:{fallingAngle:.31,fallingIntensity:.22,fallingSpeed:2.07,fallingStreakLength:2,glassIntensity:.41,glassZoom:.85},snow:{drift:1,fallSpeed:8,flakeSize:.85,flutter:.98,glowAmount:.09,intensity:.34,layers:8,opacity:.78,sizeVariation:1,sparkle:.48,windAngle:1.76,windShear:1,windSpeed:1.8}},noon:{celestial:{celestialX:.69,celestialY:.74,starDensity:.36,sunGlowIntensity:5},cloud:{turbulence:.56,windSpeed:.03},glass:{brightness:1},lightning:{enabled:!0},rain:{fallingAngle:.31,fallingIntensity:.22,fallingSpeed:2.07,fallingStreakLength:2,glassIntensity:.41,glassZoom:.85},snow:{drift:1,fallSpeed:8,flakeSize:.85,flutter:.98,glowAmount:.09,intensity:.34,layers:8,opacity:.78,sizeVariation:1,sparkle:.48,turbulence:1,windAngle:1.76,windShear:1,windSpeed:1.8}}},snow:{dawn:{celestial:{celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,skyBrightness:.6,skyContrast:0,skySaturation:1.5,starDensity:1.72},cloud:{ambientDarkness:.94,backlightIntensity:1.14,density:1.19,lightIntensity:1.73,turbulence:.3,windSpeed:.04},glass:{brightness:.95,chromaticAberration:2,depth:8,strength:10},post:{godRayIntensity:.4,haze:.03,hazeContrast:.05,hazeDesaturation:0,hazeHorizon:1},snow:{drift:1,fallSpeed:1.5,flakeSize:1.05,flutter:1,glowAmount:.44,intensity:.53,layers:8,opacity:.4,sizeVariation:.63,sparkle:.97,turbulence:.59,windAngle:1.95,windShear:.8,windSpeed:2}},dusk:{celestial:{celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,skyBrightness:1.01,skyContrast:.86,skySaturation:2,starDensity:1.72,sunGlowIntensity:4.9},cloud:{ambientDarkness:.9,backlightIntensity:1.53,cloudScale:1.82,coverage:.62,density:1.19,lightIntensity:.55,softness:.31,turbulence:.3,windSpeed:.04},glass:{blur:2,chromaticAberration:2,depth:8,strength:10},post:{godRayIntensity:.4,haze:.03,hazeContrast:.05,hazeDesaturation:0,hazeHorizon:1},snow:{drift:1,fallSpeed:1.5,flakeSize:1.05,flutter:1,glowAmount:.88,intensity:.96,layers:8,opacity:.34,sizeVariation:.63,sparkle:.97,turbulence:.59,windAngle:1.95,windShear:.8,windSpeed:2}},midnight:{celestial:{celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,moonPhase:.5,skyBrightness:1.07,skyContrast:.59,skySaturation:1.57,starDensity:1.72},cloud:{ambientDarkness:.04,cloudScale:1.15,coverage:.5,density:1.19,lightIntensity:1.29,softness:.14,turbulence:.3,windAngle:-.08,windSpeed:.04},glass:{blur:1,chromaticAberration:2,depth:8,strength:10},post:{godRayIntensity:.4,haze:.03,hazeContrast:.05,hazeDesaturation:0,hazeHorizon:1},snow:{drift:1,fallSpeed:1.5,flakeSize:1.05,flutter:1,glowAmount:.88,intensity:.53,layers:8,opacity:.34,sizeVariation:.63,sparkle:.97,turbulence:.59,windAngle:1.95,windShear:.8,windSpeed:1.44}},noon:{celestial:{celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,skyBrightness:.35,skyContrast:.69,skySaturation:.73,starDensity:1.72,sunGlowIntensity:3.45,sunGlowSize:.29,sunRayCount:0},cloud:{ambientDarkness:1,backlightIntensity:1.01,density:1.19,lightIntensity:.57,turbulence:.3,windSpeed:.04},glass:{blur:1,brightness:1.15,chromaticAberration:2,depth:8,saturation:1.4,strength:10},post:{godRayIntensity:.4,haze:.03,hazeContrast:.05,hazeDesaturation:0,hazeHorizon:1},snow:{drift:1,fallSpeed:1.5,flakeSize:1.28,flutter:1,glowAmount:.47,intensity:.53,layers:8,opacity:.34,sizeVariation:.63,sparkle:1,turbulence:.59,windAngle:1.95,windShear:.8,windSpeed:2}}},thunderstorm:{dawn:{celestial:{celestialX:.69,celestialY:.74,moonGlowIntensity:0,moonGlowSize:0,skyBrightness:.78,skyContrast:1.03,skySaturation:.94,starDensity:1.72,sunGlowIntensity:5.1,sunRayCount:5,sunRayIntensity:.13,sunRayLength:1.7,sunRayShimmer:5,sunRayShimmerSpeed:5},cloud:{backlightIntensity:.55,cloudScale:1.56,coverage:.71,density:.97,lightIntensity:0,numLayers:1,softness:.28,turbulence:.36,windSpeed:.04},glass:{blur:1,brightness:.9,chromaticAberration:2,strength:45},interactions:{lightningSceneIllumination:.83,rainRefractionStrength:2},lightning:{autoInterval:8,branchDensity:.83,enabled:!0,flashIntensity:1.57},post:{bloomIntensity:.39,exposureIntensity:.95,exposureRecovery:4.5,godRayDecay:.894,godRayDensity:.91,godRayIntensity:.26,godRaySamples:39,godRayWeight:.36,haze:.07,hazeContrast:0,hazeHorizon:1},rain:{fallingAngle:.02,fallingLayers:12,fallingSpeed:3,fallingStreakLength:1.46,glassIntensity:1.71,glassZoom:.51}},dusk:{celestial:{celestialX:.69,celestialY:.74,moonGlowIntensity:0,moonGlowSize:0,skyBrightness:.91,skyContrast:1.03,skySaturation:1.19,starDensity:1.72,sunGlowIntensity:5.05,sunRayShimmer:5,sunRayShimmerSpeed:5},cloud:{ambientDarkness:1,backlightIntensity:.55,cloudScale:1.56,coverage:.71,density:.97,lightIntensity:0,numLayers:1,softness:.28,turbulence:.36,windSpeed:.04},glass:{blur:1,brightness:.9,chromaticAberration:2,strength:45},interactions:{lightningSceneIllumination:.83,rainRefractionStrength:2},lightning:{branchDensity:.83,enabled:!0,flashIntensity:1.57},post:{bloomIntensity:.44,exposureIntensity:1.1,exposureRecovery:4.5,haze:.07,hazeContrast:0,hazeHorizon:1},rain:{fallingAngle:.02,fallingLayers:12,fallingSpeed:3,fallingStreakLength:1.46,glassIntensity:1.32,glassZoom:.51}},midnight:{celestial:{celestialY:.71,moonGlowIntensity:4.55,moonGlowSize:1.25,moonPhase:.5,skyBrightness:.78,skyContrast:1.03,skySaturation:.74,starDensity:1.72,sunRayShimmer:5,sunRayShimmerSpeed:5},cloud:{ambientDarkness:.88,backlightIntensity:.55,cloudScale:1.56,coverage:.71,density:.97,lightIntensity:0,numLayers:1,softness:.28,turbulence:.36,windSpeed:.04},glass:{blur:1,brightness:.9,chromaticAberration:2,strength:45},interactions:{lightningSceneIllumination:.83,rainRefractionStrength:2},lightning:{autoInterval:8.5,branchDensity:.89,enabled:!0,flashIntensity:1.57},post:{bloomIntensity:.44,exposureIntensity:1.1,exposureRecovery:4.5,haze:.07,hazeContrast:0,hazeHorizon:1},rain:{fallingAngle:.02,fallingLayers:12,fallingSpeed:3,fallingStreakLength:1.46,glassIntensity:1.32,glassZoom:.51}},noon:{celestial:{celestialY:.71,moonGlowIntensity:0,moonGlowSize:0,skyBrightness:.74,skyContrast:1.07,skySaturation:.55,starDensity:1.72,sunGlowIntensity:2.7,sunGlowSize:.51,sunRayCount:0,sunRayIntensity:0,sunRayLength:0,sunRayShimmer:5,sunRayShimmerSpeed:5},cloud:{ambientDarkness:1,backlightIntensity:.55,cloudScale:1.56,coverage:.71,density:.97,lightIntensity:0,numLayers:1,softness:.28,turbulence:.36,windSpeed:.04},glass:{blur:1,brightness:.9,chromaticAberration:2,strength:45},interactions:{lightningSceneIllumination:.83,rainRefractionStrength:2},lightning:{enabled:!0,flashIntensity:1.57},post:{bloomIntensity:.44,exposureIntensity:1.1,exposureRecovery:4.5,haze:.07,hazeContrast:0,hazeHorizon:1},rain:{fallingAngle:.02,fallingLayers:12,fallingSpeed:3,fallingStreakLength:1.46,glassIntensity:1.32,glassZoom:.51}}},windy:{dawn:{celestial:{celestialX:.69,celestialY:.74,skyBrightness:1,starDensity:.36,sunGlowIntensity:5},cloud:{cloudScale:.85,coverage:.4,windSpeed:.17},glass:{brightness:.95}},dusk:{celestial:{celestialX:.69,celestialY:.74,starDensity:.11,sunGlowIntensity:3.21,sunGlowSize:.51},cloud:{cloudScale:.85,coverage:.4,windSpeed:.17},glass:{brightness:.9}},midnight:{celestial:{celestialX:.74,celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,starDensity:1.72},cloud:{cloudScale:.85,coverage:.4,windSpeed:.17}},noon:{celestial:{celestialX:.74,celestialY:.71,moonPhase:.2421,starDensity:.14,sunGlowIntensity:1.59,sunGlowSize:.55},cloud:{cloudScale:.85,coverage:.4,windSpeed:.17},glass:{brightness:1}}}};function H(e){if(!e)return .5;let t=new Date(e),a=t.getUTCHours(),n=t.getUTCMinutes();return(a+n/60)/24}function Qe(e){if(!e)return .5;let t=new Date(e);t.setUTCHours(0,0,0,0);let a=new Date(\"2000-01-06T00:00:00Z\"),n=(t.getTime()-a.getTime())/(1e3*60*60*24),s=29.530588853;return(n%s+s)%s/s}function je(e){return e<0}var Je={clear:1,\"partly-cloudy\":.9,cloudy:.8,overcast:.65,fog:.7,drizzle:.7,rain:.6,\"heavy-rain\":.45,thunderstorm:.3,snow:.8,sleet:.65,hail:.5,windy:.9};function et(e,t){return e<.35?\"dark\":e>.45?\"light\":t??\"dark\"}function Oe(e){let t=e*24;return t<6?-1+t/6:t<12?(t-6)/6:t<18?1-(t-12)/6:-(t-18)/6}function tt(e,t=\"clear\"){let a=Oe(e),n;a<0?n=.05+(1+a)*.1:n=.15+a*.85;let s=Je[t],c=n*s;return Math.max(0,Math.min(1,c))}function nt(e=0){return e<=10?e/10*.3:e<=25?.3+(e-10)/15*.4:.7+Math.min((e-25)/25,.3)}function at(e){switch(e){case\"light\":return .3;case\"moderate\":return .6;case\"heavy\":return 1;default:return 0}}function st(e=10){return e>=10?0:e>=5?(10-e)/5*.3:.3+(5-e)/5*.7}function te(e){return Math.max(0,Math.min(1,e))}function ee(e,t,a){let n=te((a-e)/(t-e));return n*n*(3-2*n)}var D={x:.74,y:.78,sunSize:.14,moonSize:.17,starDensity:2,sunGlowIntensity:3.05,sunGlowSize:.3,sunRayCount:6,sunRayLength:3,sunRayIntensity:.1,moonGlowIntensity:3.45,moonGlowSize:.94},ot={clear:D,\"partly-cloudy\":D,cloudy:D,overcast:D,fog:D,drizzle:D,rain:D,\"heavy-rain\":D,thunderstorm:D,snow:D,sleet:D,hail:D,windy:D},it={clear:{cloud:{coverage:.1,speed:.3,darkness:0,turbulence:.2}},\"partly-cloudy\":{cloud:{coverage:.4,speed:.4,darkness:.1,turbulence:.3}},cloudy:{cloud:{coverage:.7,speed:.4,darkness:.2,turbulence:.3}},overcast:{cloud:{coverage:.95,speed:.3,darkness:.35,turbulence:.25}},fog:{cloud:{coverage:.6,speed:.15,darkness:.15,turbulence:.1}},drizzle:{cloud:{coverage:.75,speed:.35,darkness:.3,turbulence:.3},rain:{intensity:.25,glassDrops:!0,fallingRain:!0,angle:3}},rain:{cloud:{coverage:.85,speed:.5,darkness:.4,turbulence:.4},rain:{intensity:.6,glassDrops:!0,fallingRain:!0,angle:5}},\"heavy-rain\":{cloud:{coverage:.95,speed:.6,darkness:.55,turbulence:.5},rain:{intensity:1,glassDrops:!0,fallingRain:!0,angle:8}},thunderstorm:{cloud:{coverage:1,speed:.7,darkness:.7,turbulence:.6},rain:{intensity:1,glassDrops:!0,fallingRain:!0,angle:15},lightning:{enabled:!0,autoTrigger:!0,intervalMin:4,intervalMax:12}},snow:{cloud:{coverage:.7,speed:.25,darkness:.2,turbulence:.2},snow:{intensity:.7,windDrift:.3}},sleet:{cloud:{coverage:.8,speed:.4,darkness:.35,turbulence:.35},rain:{intensity:.5,glassDrops:!0,fallingRain:!0,angle:10},snow:{intensity:.3,windDrift:.4}},hail:{cloud:{coverage:.9,speed:.6,darkness:.5,turbulence:.5},rain:{intensity:.7,glassDrops:!0,fallingRain:!0,angle:5},snow:{intensity:.3,windDrift:.4}},windy:{cloud:{coverage:.5,speed:1,darkness:.1,turbulence:.6}}};function Me(e){let{conditionCode:t,windSpeed:a,precipitationLevel:n,visibility:s,timestamp:c,timeOfDay:l}=e,o=it[t],r=l??H(c),i=Oe(r),h=nt(a),f=at(n),p=st(s),b=je(i),R={sunAltitude:i,haze:Math.max(p,(o.cloud?.darkness??0)*.3),starVisibility:b&&!o.cloud?1:b?1-(o.cloud?.coverage??0):0},C={atmosphere:R};if(o.cloud&&(C.cloud={...o.cloud,speed:o.cloud.speed*(1+h*.5),turbulence:o.cloud.turbulence*(1+h*.3)}),o.rain){let E=f>0?f:o.rain.intensity;C.rain={...o.rain,intensity:E,angle:o.rain.angle+h*10}}o.lightning&&(C.lightning={...o.lightning}),o.snow&&(C.snow={...o.snow,windDrift:o.snow.windDrift+h*.3});let A=Qe(c),k=ot[t];C.celestial={timeOfDay:r,moonPhase:A,starDensity:b?k.starDensity:0,celestialX:k.x,celestialY:k.y,sunSize:k.sunSize,moonSize:k.moonSize,sunGlowIntensity:k.sunGlowIntensity,sunGlowSize:k.sunGlowSize,sunRayCount:k.sunRayCount,sunRayLength:k.sunRayLength,sunRayIntensity:k.sunRayIntensity,moonGlowIntensity:k.moonGlowIntensity,moonGlowSize:k.moonGlowSize};let w=te(R.haze),T=C.cloud?.coverage??0,Q=T>.001,$=te(.04+(t===\"fog\"?.18:t===\"thunderstorm\"?.12:t===\"heavy-rain\"?.1:t===\"overcast\"?.08:t===\"cloudy\"||t===\"partly-cloudy\"?.06:.04)+w*.22),G=1.1+w*1.2,z=C.lightning?.enabled?.85:0,V=ee(-.05,.08,i),F=1-ee(.18,.7,Math.max(0,i)),d=ee(.25,.85,T),u=1-ee(.97,1,T),y=.35+w*.65,g=Q?te(V*F*d*u*y*.6):0,m={enabled:!0,haze:w,bloomIntensity:$,bloomRadius:G,exposureIntensity:z,godRayIntensity:g};return C.post=m,C}function rt(e){let t=Me(e),a=e.timeOfDay??t.celestial?.timeOfDay??H(e.timestamp),n=t.cloud!==void 0,s=t.rain!==void 0,c=t.lightning!==void 0,l=t.snow!==void 0,o=t.lightning?.intervalMin??4,r=t.lightning?.intervalMax??12;return{layers:{celestial:!0,clouds:n,rain:s,lightning:c,snow:l},celestial:{timeOfDay:a,moonPhase:t.celestial?.moonPhase??.5,starDensity:t.celestial?.starDensity??.5,celestialX:t.celestial?.celestialX??.5,celestialY:t.celestial?.celestialY??.72,sunSize:t.celestial?.sunSize??.06,moonSize:t.celestial?.moonSize??.05,sunGlowIntensity:t.celestial?.sunGlowIntensity??1,sunGlowSize:t.celestial?.sunGlowSize??.3,sunRayCount:t.celestial?.sunRayCount??12,sunRayLength:t.celestial?.sunRayLength??.5,sunRayIntensity:t.celestial?.sunRayIntensity??.4,sunRayShimmer:1,sunRayShimmerSpeed:1,moonGlowIntensity:t.celestial?.moonGlowIntensity??1,moonGlowSize:t.celestial?.moonGlowSize??.2,skyBrightness:1,skySaturation:1,skyContrast:1},cloud:{cloudScale:1.5,coverage:t.cloud?.coverage??.5,density:.7,softness:.3,windSpeed:t.cloud?.speed??.5,windAngle:0,turbulence:t.cloud?.turbulence??.5,lightIntensity:1,ambientDarkness:t.cloud?.darkness??.3,backlightIntensity:.5,numLayers:3},rain:{glassIntensity:s?(t.rain?.intensity??.5)*.7:0,zoom:1,fallingIntensity:s?t.rain?.intensity??.6:0,fallingSpeed:1,fallingAngle:s?(t.rain?.angle??5)*.02:.1,fallingStreakLength:.8,fallingLayers:3,fallingRefraction:.3},lightning:{autoMode:c?t.lightning?.autoTrigger??!0:!1,autoInterval:c?(o+r)/2:8,glowIntensity:.8,branchDensity:.6,sceneIllumination:.6},snow:{intensity:l?t.snow?.intensity??.7:0,layers:4,fallSpeed:.5,windSpeed:l?t.snow?.windDrift??.3:.3,windAngle:0,turbulence:.3,drift:l?t.snow?.windDrift??.3:.3,flutter:.5,windShear:.2,flakeSize:1,sizeVariation:.5,opacity:.8,glowAmount:.3,sparkle:.2},post:{enabled:!0,haze:0,hazeHorizon:.5,hazeDesaturation:.3,hazeContrast:.2,bloomIntensity:0,bloomThreshold:.8,bloomKnee:.5,bloomRadius:8,bloomTapScale:1,exposureIntensity:0,exposureDesaturation:.3,exposureRecovery:2,godRayIntensity:0,godRayDecay:.96,godRayDensity:.5,godRayWeight:.3,godRaySamples:60}}}function lt(e){return{layers:e.layers,celestial:{timeOfDay:e.celestial.timeOfDay,moonPhase:e.celestial.moonPhase,starDensity:e.celestial.starDensity,celestialX:e.celestial.celestialX,celestialY:e.celestial.celestialY,sunSize:e.celestial.sunSize,moonSize:e.celestial.moonSize,sunGlowIntensity:e.celestial.sunGlowIntensity,sunGlowSize:e.celestial.sunGlowSize,sunRayCount:e.celestial.sunRayCount,sunRayLength:e.celestial.sunRayLength,sunRayIntensity:e.celestial.sunRayIntensity,sunRayShimmer:e.celestial.sunRayShimmer,sunRayShimmerSpeed:e.celestial.sunRayShimmerSpeed,moonGlowIntensity:e.celestial.moonGlowIntensity,moonGlowSize:e.celestial.moonGlowSize,skyBrightness:e.celestial.skyBrightness,skySaturation:e.celestial.skySaturation,skyContrast:e.celestial.skyContrast},cloud:{coverage:e.cloud.coverage,density:e.cloud.density,softness:e.cloud.softness,cloudScale:e.cloud.cloudScale,windSpeed:e.cloud.windSpeed,windAngle:e.cloud.windAngle,turbulence:e.cloud.turbulence,lightIntensity:e.cloud.lightIntensity,ambientDarkness:e.cloud.ambientDarkness,backlightIntensity:e.cloud.backlightIntensity,numLayers:e.cloud.numLayers},rain:{glassIntensity:e.rain.glassIntensity,glassZoom:e.rain.zoom,fallingIntensity:e.rain.fallingIntensity,fallingSpeed:e.rain.fallingSpeed,fallingAngle:e.rain.fallingAngle,fallingStreakLength:e.rain.fallingStreakLength,fallingLayers:e.rain.fallingLayers},lightning:{enabled:e.layers.lightning,autoMode:e.lightning.autoMode,autoInterval:e.lightning.autoInterval,flashIntensity:e.lightning.glowIntensity,branchDensity:e.lightning.branchDensity},snow:{intensity:e.snow.intensity,layers:e.snow.layers,fallSpeed:e.snow.fallSpeed,windSpeed:e.snow.windSpeed,windAngle:e.snow.windAngle,turbulence:e.snow.turbulence,drift:e.snow.drift,flutter:e.snow.flutter,windShear:e.snow.windShear,flakeSize:e.snow.flakeSize,sizeVariation:e.snow.sizeVariation,opacity:e.snow.opacity,glowAmount:e.snow.glowAmount,sparkle:e.snow.sparkle},interactions:{rainRefractionStrength:e.rain.fallingRefraction,lightningSceneIllumination:e.lightning.sceneIllumination},post:e.post}}function Be(e){return lt(rt(e))}var me={dawn:.25,noon:.5,dusk:.75,midnight:0},ut=[\"dawn\",\"noon\",\"dusk\",\"midnight\"];function q(e){let t=(e%1+1)%1,a=\"noon\",n=1/0;for(let s of ut){let c=me[s],l=Math.abs(t-c);l>.5&&(l=1-l);let o=Math.abs(l-n)<=Number.EPSILON;(l<n||o&&s===\"midnight\")&&(n=l,a=s)}return a}function W(e,t){if(!(!e&&!t))return{...e,...t}}function Ne(e,t){return{...e,layers:W(e.layers,t.layers),celestial:W(e.celestial,t.celestial),cloud:W(e.cloud,t.cloud),rain:W(e.rain,t.rain),lightning:W(e.lightning,t.lightning),snow:W(e.snow,t.snow),glass:W(e.glass,t.glass),interactions:W(e.interactions,t.interactions),post:W(e.post,t.post)}}function Ye(e){let t=Be(e),a=e.timeOfDay;typeof a==\"number\"&&t.celestial&&(t.celestial.timeOfDay=a);let n=e.tunedPresets?.[e.conditionCode],s=a??t.celestial?.timeOfDay;if(!n||s===void 0)return t;let c=q(s);return Ne(t,n[c])}function ct(){if(typeof window>\"u\")return\"high\";let e=window.devicePixelRatio||1,t=window.innerWidth*window.innerHeight*e*e,a=typeof navigator<\"u\"?navigator.hardwareConcurrency:void 0,n=typeof navigator<\"u\"?navigator.deviceMemory:void 0,s=window.innerWidth<768;return typeof n==\"number\"&&n<=4||typeof a==\"number\"&&a<=4||t>25e5?s?\"low\":\"medium\":s?\"medium\":\"high\"}function Ve(e){return e===\"low\"||e===\"medium\"||e===\"high\"?e:ct()}function Xe(e){if(typeof window>\"u\")return;let t=window.devicePixelRatio||1;return Math.max(1,Math.min(t,e===\"low\"?1:e===\"medium\"?1.5:2))}import{jsx as He}from\"react/jsx-runtime\";var mt=de;function yt({conditionCode:e,windSpeed:t,precipitationLevel:a,visibility:n,timestamp:s,timeOfDay:c,settings:l,className:o}){let[r,i]=ft(!1),h=l?.enabled!==!1,f=l?.reducedMotion??!1,p=ye(()=>Ve(l?.quality??\"auto\"),[l?.quality]);dt(()=>{i(!0)},[]);let b=ye(()=>Xe(p),[p]),R=ye(()=>!h||f?null:Ye({conditionCode:e,windSpeed:t,precipitationLevel:a,visibility:n,timestamp:s,timeOfDay:c,tunedPresets:mt}),[h,f,e,t,a,n,s,c]);return!r||!h||f||!R?null:He(\"div\",{className:o,style:{position:\"absolute\",inset:0,overflow:\"hidden\",pointerEvents:\"none\",borderRadius:\"inherit\"},\"aria-hidden\":\"true\",children:He(Fe,{className:\"absolute inset-0\",dpr:b,...R})})}function ht({glassStyles:e,blurAmount:t}){if(!!e.backdropFilter)return e;let n=`blur(${t}px)`;return{backdropFilter:n,WebkitBackdropFilter:n}}import{useEffect as gt,useMemo as pt,useState as St}from\"react\";var Y={depth:12,radius:12,strength:40,chromaticAberration:8,blur:2,brightness:1.05,saturation:1.2};function vt({width:e,height:t,radius:a,depth:n}){let s=Math.ceil(a/t*15),c=Math.ceil(a/e*15),l=Math.max(0,e-2*n),o=Math.max(0,t-2*n),r=`<svg height=\"${t}\" width=\"${e}\" viewBox=\"0 0 ${e} ${t}\" xmlns=\"http://www.w3.org/2000/svg\">\n    <style>.mix { mix-blend-mode: screen; }</style>\n    <defs>\n      <linearGradient id=\"Y\" x1=\"0\" x2=\"0\" y1=\"${s}%\" y2=\"${100-s}%\">\n        <stop offset=\"0%\" stop-color=\"#0F0\" />\n        <stop offset=\"100%\" stop-color=\"#000\" />\n      </linearGradient>\n      <linearGradient id=\"X\" x1=\"${c}%\" x2=\"${100-c}%\" y1=\"0\" y2=\"0\">\n        <stop offset=\"0%\" stop-color=\"#F00\" />\n        <stop offset=\"100%\" stop-color=\"#000\" />\n      </linearGradient>\n    </defs>\n    <rect x=\"0\" y=\"0\" height=\"${t}\" width=\"${e}\" fill=\"#808080\" />\n    <g filter=\"blur(2px)\">\n      <rect x=\"0\" y=\"0\" height=\"${t}\" width=\"${e}\" fill=\"#000080\" />\n      <rect x=\"0\" y=\"0\" height=\"${t}\" width=\"${e}\" fill=\"url(#Y)\" class=\"mix\" />\n      <rect x=\"0\" y=\"0\" height=\"${t}\" width=\"${e}\" fill=\"url(#X)\" class=\"mix\" />\n      <rect x=\"${n}\" y=\"${n}\" height=\"${o}\" width=\"${l}\" fill=\"#808080\" rx=\"${a}\" ry=\"${a}\" filter=\"blur(${n}px)\" />\n    </g>\n  </svg>`;return\"data:image/svg+xml;utf8,\"+encodeURIComponent(r)}function bt({width:e,height:t,radius:a,depth:n,strength:s,chromaticAberration:c}){let l=vt({width:e,height:t,radius:a,depth:n}),o=`<feImage x=\"0\" y=\"0\" height=\"${t}\" width=\"${e}\" href=\"${l}\" result=\"displacementMap\" />`,r;if(c===0)r=`\n      ${o}\n      <feDisplacementMap in=\"SourceGraphic\" in2=\"displacementMap\" scale=\"${s}\" xChannelSelector=\"R\" yChannelSelector=\"G\" />\n    `;else{let h=s+c*2,f=s+c;r=`\n      ${o}\n      <feDisplacementMap in=\"SourceGraphic\" in2=\"displacementMap\" scale=\"${h}\" xChannelSelector=\"R\" yChannelSelector=\"G\" />\n      <feColorMatrix type=\"matrix\" values=\"1 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1 0\" result=\"displacedR\" />\n      <feDisplacementMap in=\"SourceGraphic\" in2=\"displacementMap\" scale=\"${f}\" xChannelSelector=\"R\" yChannelSelector=\"G\" />\n      <feColorMatrix type=\"matrix\" values=\"0 0 0 0 0  0 1 0 0 0  0 0 0 0 0  0 0 0 1 0\" result=\"displacedG\" />\n      <feDisplacementMap in=\"SourceGraphic\" in2=\"displacementMap\" scale=\"${s}\" xChannelSelector=\"R\" yChannelSelector=\"G\" />\n      <feColorMatrix type=\"matrix\" values=\"0 0 0 0 0  0 0 0 0 0  0 0 1 0 0  0 0 0 1 0\" result=\"displacedB\" />\n      <feBlend in=\"displacedR\" in2=\"displacedG\" mode=\"screen\"/>\n      <feBlend in2=\"displacedB\" mode=\"screen\"/>\n    `}let i=`<svg height=\"${t}\" width=\"${e}\" viewBox=\"0 0 ${e} ${t}\" xmlns=\"http://www.w3.org/2000/svg\">\n    <defs>\n      <filter id=\"displace\" color-interpolation-filters=\"sRGB\">${r}</filter>\n    </defs>\n  </svg>`;return\"data:image/svg+xml;utf8,\"+encodeURIComponent(i)+\"#displace\"}function wt({filterUrl:e,blur:t,brightness:a,saturation:n}){return`blur(${t/2}px) url('${e}') blur(${t}px) brightness(${a}) saturate(${n})`}function xt(){let[e,t]=St(!0);return gt(()=>{let a=CSS.supports(\"backdrop-filter\",\"blur(1px)\")||CSS.supports(\"-webkit-backdrop-filter\",\"blur(1px)\");t(a)},[]),e}function It({width:e,height:t,depth:a=Y.depth,radius:n=Y.radius,strength:s=Y.strength,chromaticAberration:c=Y.chromaticAberration,blur:l=Y.blur,brightness:o=Y.brightness,saturation:r=Y.saturation,enabled:i=!0}){let h=xt();return pt(()=>{if(!i||!h||e<=0||t<=0)return{};let f=bt({width:e,height:t,radius:n,depth:a,strength:s,chromaticAberration:c}),p=wt({filterUrl:f,blur:l,brightness:o,saturation:r});return{backdropFilter:p,WebkitBackdropFilter:p}},[e,t,a,n,s,c,l,o,r,i,h])}function Ue(e){return(e%1+1)%1}function Rt(e){let t=Ue(e),a=q(t);return me[a]}function kt(e){return((Math.floor(e)%12+12)%12+.5)/12}function _t(e){let{time:t,updatedAt:a}=e;return typeof t?.timeBucket==\"number\"?{timeOfDay:kt(t.timeBucket),source:\"timeBucket\"}:typeof t?.localTimeOfDay==\"number\"?{timeOfDay:Ue(t.localTimeOfDay),source:\"localTimeOfDay\"}:typeof a==\"string\"?{timeOfDay:H(a),source:\"updatedAt\"}:{timeOfDay:.5,source:\"defaultNoon\"}}export{yt as EffectCompositorRuntime,de as TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES,q as getNearestCheckpoint,tt as getSceneBrightnessFromTimeOfDay,H as getTimeOfDay,et as getWeatherTheme,ht as resolveGlassBackdropFilterStyles,_t as resolveWeatherTime,Rt as snapTimeOfDayToNearestCheckpoint,It as useGlassStyles};\n"
  },
  {
    "path": "apps/www/components/tool-ui/weather-widget/runtime.ts",
    "content": "/**\n * Start here when wiring the Weather Widget in your app.\n *\n * Import `WeatherWidget` and the runtime types from this file for production use.\n * Reach for `index.tsx` only if you're actively working on authoring/debug internals.\n */\nexport { WeatherWidget } from \"./weather-widget-container\";\nexport type {\n  WeatherWidgetPayload,\n  WeatherWidgetRuntimeProps as WeatherWidgetProps,\n  WeatherWidgetCurrent,\n  WeatherWidgetTime,\n  WeatherWidgetLocation,\n  WeatherConditionCode,\n  ForecastDay,\n  TemperatureUnit,\n  PrecipitationLevel,\n} from \"./schema-runtime\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/weather-widget/schema-runtime.ts",
    "content": "export type WeatherConditionCode =\n  | \"clear\"\n  | \"partly-cloudy\"\n  | \"cloudy\"\n  | \"overcast\"\n  | \"fog\"\n  | \"drizzle\"\n  | \"rain\"\n  | \"heavy-rain\"\n  | \"thunderstorm\"\n  | \"snow\"\n  | \"sleet\"\n  | \"hail\"\n  | \"windy\";\n\nexport type TemperatureUnit = \"celsius\" | \"fahrenheit\";\n\nexport type PrecipitationLevel = \"none\" | \"light\" | \"moderate\" | \"heavy\";\n\nexport type TimeBucket = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11;\n\nexport interface ForecastDay {\n  label: string;\n  conditionCode: WeatherConditionCode;\n  tempMin: number;\n  tempMax: number;\n}\n\nexport interface WeatherWidgetCurrent {\n  conditionCode: WeatherConditionCode;\n  temperature: number;\n  tempMin: number;\n  tempMax: number;\n  windSpeed?: number;\n  precipitationLevel?: PrecipitationLevel;\n  visibility?: number;\n}\n\nexport interface WeatherWidgetTime {\n  timeBucket?: TimeBucket;\n  localTimeOfDay?: number;\n}\n\nexport interface WeatherWidgetLocation {\n  name: string;\n}\n\nexport interface WeatherWidgetPayload {\n  version: \"3.1\";\n  id: string;\n  location: WeatherWidgetLocation;\n  units: {\n    temperature: TemperatureUnit;\n  };\n  current: WeatherWidgetCurrent;\n  forecast: ForecastDay[];\n  time: WeatherWidgetTime;\n  updatedAt?: string;\n}\n\nexport type EffectQuality = \"low\" | \"medium\" | \"high\" | \"auto\";\n\nexport interface EffectSettings {\n  enabled?: boolean;\n  quality?: EffectQuality;\n  reducedMotion?: boolean;\n}\n\nexport interface WeatherWidgetRuntimeProps extends WeatherWidgetPayload {\n  className?: string;\n  effects?: EffectSettings;\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/weather-widget/weather-data-overlay.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useRef, useState, useCallback } from \"react\";\nimport {\n  Sun,\n  Cloud,\n  CloudSun,\n  CloudFog,\n  CloudDrizzle,\n  CloudRain,\n  CloudLightning,\n  Snowflake,\n  CloudHail,\n  Wind,\n  type LucideIcon,\n} from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport type {\n  ForecastDay,\n  TemperatureUnit,\n  WeatherConditionCode,\n} from \"./schema-runtime\";\nimport {\n  getSceneBrightnessFromTimeOfDay,\n  getTimeOfDay,\n  getWeatherTheme,\n} from \"./generated/weather-runtime-core.generated\";\nimport {\n  resolveGlassBackdropFilterStyles,\n  useGlassStyles,\n} from \"./generated/weather-runtime-core.generated\";\n\ntype WeatherTheme = \"light\" | \"dark\";\n\nfunction getPeakIntensity(timeOfDay: number): number {\n  const noonDistance = Math.abs(timeOfDay - 0.5);\n  const midnightDistance = Math.min(timeOfDay, 1 - timeOfDay);\n  const minDistance = Math.min(noonDistance, midnightDistance);\n  return Math.max(0, 1 - minDistance * 4);\n}\n\nfunction sineEasedGradient(\n  x: number,\n  y: number,\n  radius: number,\n  peakOpacity: number,\n  steps = 8,\n): string {\n  const stops: string[] = [];\n  for (let i = 0; i <= steps; i++) {\n    const t = i / steps;\n    const eased = Math.sin((t * Math.PI) / 2);\n    const opacity = peakOpacity * (1 - eased);\n    const position = t * 100;\n    stops.push(\n      `rgba(255,255,255,${opacity.toFixed(4)}) ${position.toFixed(1)}%`,\n    );\n  }\n  return `radial-gradient(circle ${radius}px at ${x}px ${y}px, ${stops.join(\", \")})`;\n}\n\nconst conditionIcons: Record<WeatherConditionCode, LucideIcon> = {\n  clear: Sun,\n  \"partly-cloudy\": CloudSun,\n  cloudy: Cloud,\n  overcast: Cloud,\n  fog: CloudFog,\n  drizzle: CloudDrizzle,\n  rain: CloudRain,\n  \"heavy-rain\": CloudRain,\n  thunderstorm: CloudLightning,\n  snow: Snowflake,\n  sleet: CloudHail,\n  hail: CloudHail,\n  windy: Wind,\n};\n\nexport interface GlassEffectParams {\n  enabled?: boolean;\n  depth?: number;\n  strength?: number;\n  chromaticAberration?: number;\n  blur?: number;\n  brightness?: number;\n  saturation?: number;\n}\n\nexport interface WeatherDataOverlayProps {\n  location: string;\n  conditionCode: WeatherConditionCode;\n  temperature: number;\n  tempHigh: number;\n  tempLow: number;\n  forecast?: ForecastDay[];\n  unit?: TemperatureUnit;\n  theme?: WeatherTheme;\n  /**\n   * Provide either `timeOfDay` (0-1) or a `timestamp` ISO string.\n   * If neither is provided, defaults to noon (0.5).\n   */\n  timeOfDay?: number;\n  timestamp?: string | undefined;\n  className?: string;\n  reducedMotion?: boolean;\n  /**\n   * Glass refraction effect parameters for the forecast card.\n   * When enabled, applies SVG displacement filter for realistic glass distortion.\n   */\n  glassParams?: GlassEffectParams | undefined;\n}\n\ninterface GlowState {\n  x: number;\n  y: number;\n  intensity: number;\n}\n\nexport function observeCardDimensions(\n  element: HTMLDivElement | null,\n  onResize: () => void,\n): () => void {\n  if (!element || typeof ResizeObserver !== \"function\") {\n    return () => {};\n  }\n\n  const observer = new ResizeObserver(onResize);\n  observer.observe(element);\n  return () => observer.disconnect();\n}\n\nexport function WeatherDataOverlay({\n  location,\n  conditionCode,\n  temperature,\n  tempHigh,\n  tempLow,\n  forecast = [],\n  unit = \"fahrenheit\",\n  theme: themeProp,\n  timeOfDay: timeOfDayProp,\n  timestamp,\n  className,\n  reducedMotion = false,\n  glassParams,\n}: WeatherDataOverlayProps) {\n  const timeOfDay =\n    typeof timeOfDayProp === \"number\"\n      ? timeOfDayProp\n      : typeof timestamp === \"string\"\n        ? getTimeOfDay(timestamp)\n        : 0.5;\n\n  const [glowState, setGlowState] = useState<GlowState>({\n    x: 0,\n    y: 0,\n    intensity: 0,\n  });\n  const [cardDimensions, setCardDimensions] = useState({ width: 0, height: 0 });\n  const cardRef = useRef<HTMLDivElement>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const pendingGlowStateRef = useRef<GlowState | null>(null);\n  const pendingGlowFrameRef = useRef<number | null>(null);\n\n  // Glass effect styles applied directly to forecast container.\n  // Enabled by default - falls back to simple blur if SVG filter unsupported.\n  const glassEnabled = glassParams?.enabled !== false;\n  const glassStyles = useGlassStyles({\n    width: cardDimensions.width,\n    height: cardDimensions.height,\n    depth: glassParams?.depth ?? 3,\n    radius: 12,\n    strength: glassParams?.strength ?? 75,\n    chromaticAberration: glassParams?.chromaticAberration ?? 6,\n    blur: glassParams?.blur ?? 1.5,\n    brightness: glassParams?.brightness ?? 0.8,\n    saturation: glassParams?.saturation ?? 1.3,\n    enabled: glassEnabled,\n  });\n\n  // Track forecast card dimensions for glass effect\n  const updateCardDimensions = useCallback(() => {\n    if (cardRef.current) {\n      const rect = cardRef.current.getBoundingClientRect();\n      setCardDimensions({\n        width: Math.round(rect.width),\n        height: Math.round(rect.height),\n      });\n    }\n  }, []);\n  const hasForecastStrip = forecast.length > 0;\n\n  useEffect(() => {\n    updateCardDimensions();\n    return observeCardDimensions(cardRef.current, updateCardDimensions);\n  }, [hasForecastStrip, updateCardDimensions]);\n\n  const theme =\n    themeProp ??\n    getWeatherTheme(\n      getSceneBrightnessFromTimeOfDay(timeOfDay, conditionCode),\n      undefined,\n    );\n\n  const commitGlowState = useCallback((nextState: GlowState) => {\n    setGlowState((prevState) => {\n      if (\n        prevState.x === nextState.x &&\n        prevState.y === nextState.y &&\n        prevState.intensity === nextState.intensity\n      ) {\n        return prevState;\n      }\n\n      return nextState;\n    });\n  }, []);\n\n  const cancelPendingGlowFrame = useCallback(() => {\n    pendingGlowStateRef.current = null;\n\n    if (\n      pendingGlowFrameRef.current !== null &&\n      typeof window !== \"undefined\" &&\n      typeof window.cancelAnimationFrame === \"function\"\n    ) {\n      window.cancelAnimationFrame(pendingGlowFrameRef.current);\n    }\n\n    pendingGlowFrameRef.current = null;\n  }, []);\n\n  const scheduleGlowState = useCallback(\n    (nextState: GlowState) => {\n      pendingGlowStateRef.current = nextState;\n\n      if (pendingGlowFrameRef.current !== null) {\n        return;\n      }\n\n      if (\n        typeof window === \"undefined\" ||\n        typeof window.requestAnimationFrame !== \"function\"\n      ) {\n        pendingGlowStateRef.current = null;\n        commitGlowState(nextState);\n        return;\n      }\n\n      pendingGlowFrameRef.current = window.requestAnimationFrame(() => {\n        pendingGlowFrameRef.current = null;\n        const pendingState = pendingGlowStateRef.current;\n        pendingGlowStateRef.current = null;\n\n        if (pendingState) {\n          commitGlowState(pendingState);\n        }\n      });\n    },\n    [commitGlowState],\n  );\n\n  const clearGlowIntensity = useCallback(() => {\n    cancelPendingGlowFrame();\n    setGlowState((prevState) => {\n      if (prevState.intensity === 0) {\n        return prevState;\n      }\n\n      return { ...prevState, intensity: 0 };\n    });\n  }, [cancelPendingGlowFrame]);\n\n  useEffect(() => {\n    if (reducedMotion) {\n      clearGlowIntensity();\n      return;\n    }\n\n    const container = containerRef.current;\n    if (!container) return;\n\n    const handleMouseMove = (e: MouseEvent) => {\n      if (!cardRef.current) return;\n      const cardRect = cardRef.current.getBoundingClientRect();\n\n      const clampedX = Math.max(\n        cardRect.left,\n        Math.min(e.clientX, cardRect.right),\n      );\n      const clampedY = Math.max(\n        cardRect.top,\n        Math.min(e.clientY, cardRect.bottom),\n      );\n\n      const distanceX =\n        e.clientX < cardRect.left\n          ? cardRect.left - e.clientX\n          : e.clientX > cardRect.right\n            ? e.clientX - cardRect.right\n            : 0;\n      const distanceY =\n        e.clientY < cardRect.top\n          ? cardRect.top - e.clientY\n          : e.clientY > cardRect.bottom\n            ? e.clientY - cardRect.bottom\n            : 0;\n      const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);\n\n      const maxDistance = 150;\n      const intensity = Math.max(0, 1 - distance / maxDistance);\n\n      scheduleGlowState({\n        x: clampedX - cardRect.left,\n        y: clampedY - cardRect.top,\n        intensity,\n      });\n    };\n\n    const handleMouseLeave = () => {\n      clearGlowIntensity();\n    };\n\n    container.addEventListener(\"mousemove\", handleMouseMove);\n    container.addEventListener(\"mouseleave\", handleMouseLeave);\n\n    return () => {\n      container.removeEventListener(\"mousemove\", handleMouseMove);\n      container.removeEventListener(\"mouseleave\", handleMouseLeave);\n      cancelPendingGlowFrame();\n    };\n  }, [\n    reducedMotion,\n    clearGlowIntensity,\n    scheduleGlowState,\n    cancelPendingGlowFrame,\n  ]);\n\n  const roundedTemperature = Math.round(temperature);\n  const unitSymbol = unit === \"celsius\" ? \"C\" : \"F\";\n  const spokenUnit = unit === \"celsius\" ? \"Celsius\" : \"Fahrenheit\";\n  const peakIntensity = getPeakIntensity(timeOfDay);\n\n  const isDark = theme === \"dark\";\n  const textPrimary = isDark ? \"text-white\" : \"text-black\";\n  const textPrimarySoft = isDark ? \"text-white/90\" : \"text-black/85\";\n  const textSecondary = isDark ? \"text-white/80\" : \"text-black/80\";\n  const textSubtle = isDark ? \"text-white/40\" : \"text-black/40\";\n\n  const baseBgOpacity = isDark ? 0.04 : 0.04;\n  const bgOpacity = baseBgOpacity * (1 - peakIntensity * 0.7);\n  const midnightDistance = Math.min(timeOfDay, 1 - timeOfDay);\n  const baseBlur = isDark ? 2 + midnightDistance * 38 : 24;\n  const blurAmount = isDark\n    ? baseBlur\n    : baseBlur - peakIntensity * (baseBlur - 8);\n\n  // Dawn intensity peaks around timeOfDay 0.2-0.3 (morning transition)\n  const isDawn = timeOfDay > 0.1 && timeOfDay < 0.4;\n  const dawnIntensity = isDawn ? 1 - Math.abs(timeOfDay - 0.25) * 4 : 0;\n  const forecastTextShadow =\n    dawnIntensity > 0\n      ? `0 0.5px 1px rgba(0,0,0,${(dawnIntensity * 0.4).toFixed(2)})`\n      : undefined;\n\n  const shadowStyle = isDark\n    ? \"0 1px 8px rgba(0,0,0,0.3)\"\n    : \"0 1px 8px rgba(255,255,255,0.3)\";\n\n  // Fluid type scales with the widget container size. (Requires container-type:size.)\n  const locationFontSize = \"clamp(13px, 7.5cqmin, 17px)\";\n  const temperatureFontSize = \"clamp(48px, 32cqmin, 72px)\";\n  const degreeFontSize = \"clamp(18px, 12cqmin, 28px)\";\n  const hiLoFontSize = \"clamp(11px, 6.5cqmin, 15px)\";\n  const forecastFontFamily =\n    '\"SF Pro Text\", Inter, \"Noto Sans\", system-ui, sans-serif';\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\n        \"pointer-events-auto absolute inset-0 z-10 flex select-none flex-col\",\n        className,\n      )}\n    >\n      {/* Current weather (more inset than forecast strip) */}\n      <div className=\"px-6 pt-6\">\n        <div className=\"flex flex-col items-start\">\n          <h2\n            className={cn(\n              \"font-medium leading-[1.08] tracking-tight\",\n              textSecondary,\n            )}\n            style={{\n              fontSize: locationFontSize,\n              fontFamily: forecastFontFamily,\n              textShadow: shadowStyle,\n            }}\n          >\n            {location}\n          </h2>\n\n          <div className=\"-mt-0.5 flex items-start gap-1\">\n            <span\n              className={cn(\n                \"font-[250] tabular-nums leading-[1.02] tracking-[-0.015em]\",\n                textPrimarySoft,\n              )}\n              style={{\n                fontSize: temperatureFontSize,\n                fontFamily: forecastFontFamily,\n                fontFeatureSettings: '\"tnum\" 1, \"case\" 1',\n                textShadow: isDark\n                  ? \"0 2px 20px rgba(0,0,0,0.25)\"\n                  : \"0 2px 20px rgba(255,255,255,0.3)\",\n              }}\n              aria-hidden=\"true\"\n            >\n              {roundedTemperature}\n            </span>\n            <span\n              className={cn(\"mt-2 font-[250] tabular-nums\", textSecondary)}\n              style={{\n                fontSize: degreeFontSize,\n                fontFamily: forecastFontFamily,\n                fontFeatureSettings: '\"tnum\" 1, \"case\" 1',\n              }}\n              aria-hidden=\"true\"\n            >\n              °{unitSymbol}\n            </span>\n            <span className=\"sr-only\">\n              {roundedTemperature} degrees {spokenUnit}\n            </span>\n          </div>\n\n          <div\n            className=\"mt-0.5 flex items-center gap-3\"\n            style={{\n              fontFamily: forecastFontFamily,\n              fontFeatureSettings: '\"tnum\" 1, \"case\" 1',\n            }}\n          >\n            <span\n              className=\"font-medium tabular-nums\"\n              style={{ fontSize: hiLoFontSize }}\n            >\n              <span className={textSubtle}>H </span>\n              <span className={textPrimary}>{Math.round(tempHigh)}°</span>\n            </span>\n            <span\n              className=\"font-medium tabular-nums\"\n              style={{ fontSize: hiLoFontSize }}\n            >\n              <span className={textSubtle}>L </span>\n              <span className={textPrimary}>{Math.round(tempLow)}°</span>\n            </span>\n          </div>\n        </div>\n      </div>\n\n      {/* Spacer */}\n      <div className=\"flex-1\" />\n\n      {/* Forecast strip - hidden at small container sizes (less inset than header) */}\n      {forecast.length > 0 && (\n        <div className=\"px-3 pb-3\">\n          {/* Show the strip earlier, but progressively reduce content as height shrinks. */}\n          <div ref={cardRef} className=\"weather-forecast-strip relative hidden\">\n            {/* Edge shine - outside overflow-hidden so it aligns with border */}\n            <div\n              className=\"pointer-events-none absolute inset-0 z-10 rounded-xl transition-opacity duration-300 ease-out\"\n              style={{\n                opacity: glowState.intensity,\n                background: sineEasedGradient(\n                  glowState.x,\n                  glowState.y,\n                  100,\n                  isDark ? 0.6 : 1,\n                ),\n                mask: \"linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)\",\n                maskComposite: \"exclude\",\n                WebkitMaskComposite: \"xor\",\n                padding: \"0.5px\",\n              }}\n            />\n            <div\n              className=\"relative overflow-hidden rounded-xl px-3 py-2.5\"\n              style={{\n                backgroundColor: `rgba(255, 255, 255, ${bgOpacity})`,\n                ...resolveGlassBackdropFilterStyles({\n                  glassStyles,\n                  blurAmount,\n                }),\n              }}\n            >\n              {/* Inner glow */}\n              <div\n                className=\"pointer-events-none absolute inset-0 mix-blend-color-dodge transition-opacity duration-300 ease-out\"\n                style={{\n                  opacity: glowState.intensity,\n                  background: sineEasedGradient(\n                    glowState.x,\n                    glowState.y,\n                    120,\n                    isDark ? 0.06 : 0.15,\n                  ),\n                }}\n              />\n              <div className=\"relative flex items-center justify-between\">\n                {forecast.slice(0, 5).map((day, index) => {\n                  const DayIcon = conditionIcons[day.conditionCode];\n                  return (\n                    <div\n                      key={`${day.label}-${index}`}\n                      className=\"flex flex-1 flex-col items-center gap-0.5\"\n                      style={{\n                        fontFamily: forecastFontFamily,\n                        fontFeatureSettings: '\"tnum\" 1, \"case\" 1',\n                        textShadow: forecastTextShadow,\n                      }}\n                    >\n                      <span\n                        className={cn(\n                          \"text-[10px] uppercase tracking-[0.08em]\",\n                          index === 0 ? \"font-semibold\" : \"font-medium\",\n                          textPrimary,\n                        )}\n                      >\n                        {day.label}\n                      </span>\n                      <DayIcon\n                        className={cn(\n                          \"my-0.5 size-5\",\n                          textPrimary,\n                          index === 0 ? \"opacity-100\" : \"opacity-70\",\n                          // At shorter containers (but still showing the strip),\n                          // omit the icon to preserve legibility.\n                          \"weather-forecast-icon hidden\",\n                        )}\n                        strokeWidth={1.5}\n                        aria-hidden=\"true\"\n                      />\n                      <div className=\"flex flex-col items-center gap-0.5\">\n                        <span\n                          className={cn(\n                            \"text-[15px] tabular-nums leading-[1.2] tracking-[-0.01em]\",\n                            index === 0 ? \"font-semibold\" : \"font-medium\",\n                            textPrimary,\n                          )}\n                        >\n                          {Math.round(day.tempMax)}°\n                        </span>\n                        <span\n                          className={cn(\n                            \"font-normal text-[12px] tabular-nums leading-[1.3]\",\n                            textPrimary,\n                          )}\n                        >\n                          {Math.round(day.tempMin)}°\n                        </span>\n                      </div>\n                    </div>\n                  );\n                })}\n              </div>\n            </div>\n          </div>\n        </div>\n      )}\n\n      {/* Height-based container queries (requires container-type:size on the weather container). */}\n      <style>{`\n        @container weather (min-height: 245px) {\n          .weather-forecast-strip {\n            display: block !important;\n          }\n        }\n        @container weather (min-height: 280px) {\n          .weather-forecast-icon {\n            display: block !important;\n          }\n        }\n      `}</style>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/weather-widget/weather-widget-container.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  EffectCompositorRuntime,\n  getNearestCheckpoint,\n  getSceneBrightnessFromTimeOfDay,\n  getWeatherTheme,\n  resolveWeatherTime,\n  snapTimeOfDayToNearestCheckpoint,\n  TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES,\n} from \"./generated/weather-runtime-core.generated\";\nimport type { WeatherWidgetRuntimeProps } from \"./schema-runtime\";\nimport { WeatherDataOverlay } from \"./weather-data-overlay\";\n\ntype TimeCheckpoint = \"dawn\" | \"noon\" | \"dusk\" | \"midnight\";\n\nexport function WeatherWidget({\n  version: _version,\n  id,\n  location,\n  units,\n  current,\n  forecast,\n  time,\n  updatedAt,\n  className,\n  effects,\n}: WeatherWidgetRuntimeProps) {\n  const [prefersReducedMotion, setPrefersReducedMotion] = useState(() => {\n    if (typeof window === \"undefined\") {\n      return false;\n    }\n\n    return (\n      window.matchMedia?.(\"(prefers-reduced-motion: reduce)\")?.matches ?? false\n    );\n  });\n\n  useEffect(() => {\n    if (\n      typeof window === \"undefined\" ||\n      typeof window.matchMedia !== \"function\"\n    ) {\n      return;\n    }\n\n    const mediaQueryList = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\",\n    );\n    setPrefersReducedMotion(mediaQueryList.matches);\n\n    const handleMotionPreferenceChange = (event: MediaQueryListEvent) => {\n      setPrefersReducedMotion(event.matches);\n    };\n\n    if (typeof mediaQueryList.addEventListener === \"function\") {\n      mediaQueryList.addEventListener(\"change\", handleMotionPreferenceChange);\n      return () => {\n        mediaQueryList.removeEventListener(\n          \"change\",\n          handleMotionPreferenceChange,\n        );\n      };\n    }\n\n    mediaQueryList.addListener(handleMotionPreferenceChange);\n    return () => {\n      mediaQueryList.removeListener(handleMotionPreferenceChange);\n    };\n  }, []);\n\n  const reducedMotion = effects?.reducedMotion ?? prefersReducedMotion;\n  const effectsEnabled = effects?.enabled !== false && !reducedMotion;\n\n  const resolvedTime = resolveWeatherTime({\n    time,\n    updatedAt,\n  });\n  const timeOfDay = snapTimeOfDayToNearestCheckpoint(resolvedTime.timeOfDay);\n  const tunedOverrides =\n    TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES[current.conditionCode];\n  const checkpoint = getNearestCheckpoint(timeOfDay) as TimeCheckpoint;\n  const checkpointOverrides = tunedOverrides?.[checkpoint];\n  const glassParams =\n    checkpointOverrides && \"glass\" in checkpointOverrides\n      ? checkpointOverrides.glass\n      : undefined;\n  const brightness = getSceneBrightnessFromTimeOfDay(\n    timeOfDay,\n    current.conditionCode,\n  );\n  const weatherTheme = getWeatherTheme(brightness, undefined);\n  const isWeatherDark = weatherTheme === \"dark\";\n  const backgroundClass = isWeatherDark\n    ? \"bg-gradient-to-b from-zinc-950 via-zinc-900/70 to-zinc-950\"\n    : \"bg-gradient-to-b from-sky-50 via-sky-100/70 to-white\";\n\n  return (\n    <article\n      data-slot=\"weather-widget\"\n      data-tool-ui-id={id}\n      className={cn(\"isolate w-full max-w-md\", className)}\n    >\n      <div\n        data-slot=\"card\"\n        className={cn(\n          \"@container/weather relative aspect-[4/3] overflow-clip rounded-2xl border-0 p-0 shadow-none [container-type:size]\",\n          backgroundClass,\n        )}\n      >\n        {effectsEnabled && (\n          <EffectCompositorRuntime\n            className=\"absolute inset-0\"\n            conditionCode={current.conditionCode}\n            windSpeed={current.windSpeed}\n            precipitationLevel={current.precipitationLevel}\n            visibility={current.visibility}\n            timestamp={updatedAt}\n            timeOfDay={timeOfDay}\n            settings={effects}\n          />\n        )}\n\n        <WeatherDataOverlay\n          location={location.name}\n          conditionCode={current.conditionCode}\n          temperature={current.temperature}\n          tempHigh={current.tempMax}\n          tempLow={current.tempMin}\n          forecast={forecast}\n          unit={units.temperature}\n          theme={weatherTheme}\n          timeOfDay={timeOfDay}\n          timestamp={updatedAt}\n          glassParams={glassParams}\n          reducedMotion={reducedMotion}\n        />\n      </div>\n    </article>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/tool-ui/x-post/README.md",
    "content": "# X Post\n\nImplementation for the \"x-post\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/x-post/index.ts\n- serializable schema + parse helpers: components/tool-ui/x-post/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/social-post/content.mdx\n- Preset payload: lib/presets/x-post.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/components/tool-ui/x-post/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn      → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Button  → shadcn/ui Button\n *   Tooltip → shadcn/ui Tooltip\n */\n\nexport { cn } from \"@/lib/utils\";\nexport { Button } from \"@/components/ui/button\";\nexport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/x-post/index.ts",
    "content": "export { XPost } from \"./x-post\";\nexport type { XPostProps } from \"./x-post\";\nexport type {\n  XPostData,\n  XPostAuthor,\n  XPostMedia,\n  XPostLinkPreview,\n  XPostStats,\n} from \"./schema\";\n"
  },
  {
    "path": "apps/www/components/tool-ui/x-post/schema.ts",
    "content": "import { z } from \"zod\";\nimport { defineToolUiContract } from \"../shared/contract\";\n\nexport const XPostAuthorSchema = z.object({\n  name: z.string(),\n  handle: z.string(),\n  avatarUrl: z.url(),\n  verified: z.boolean().optional(),\n});\n\nexport const XPostMediaSchema = z.object({\n  type: z.enum([\"image\", \"video\"]),\n  url: z.url(),\n  alt: z.string(),\n  aspectRatio: z.enum([\"1:1\", \"4:3\", \"16:9\", \"9:16\"]).optional(),\n});\n\nexport const XPostLinkPreviewSchema = z.object({\n  url: z.url(),\n  title: z.string().optional(),\n  description: z.string().optional(),\n  imageUrl: z.url().optional(),\n  domain: z.string().optional(),\n});\n\nexport const XPostStatsSchema = z.object({\n  likes: z.number().optional(),\n  isLiked: z.boolean().optional(),\n  isReposted: z.boolean().optional(),\n  isBookmarked: z.boolean().optional(),\n});\n\nexport interface XPostData {\n  id: string;\n  author: z.infer<typeof XPostAuthorSchema>;\n  text?: string;\n  media?: z.infer<typeof XPostMediaSchema>;\n  linkPreview?: z.infer<typeof XPostLinkPreviewSchema>;\n  quotedPost?: XPostData;\n  stats?: z.infer<typeof XPostStatsSchema>;\n  createdAt?: string;\n}\n\nexport const SerializableXPostSchema: z.ZodType<XPostData> = z.object({\n  id: z.string(),\n  author: XPostAuthorSchema,\n  text: z.string().optional(),\n  media: XPostMediaSchema.optional(),\n  linkPreview: XPostLinkPreviewSchema.optional(),\n  quotedPost: z.lazy(() => SerializableXPostSchema).optional(),\n  stats: XPostStatsSchema.optional(),\n  createdAt: z.string().optional(),\n});\nexport type XPostAuthor = z.infer<typeof XPostAuthorSchema>;\nexport type XPostMedia = z.infer<typeof XPostMediaSchema>;\nexport type XPostLinkPreview = z.infer<typeof XPostLinkPreviewSchema>;\nexport type XPostStats = z.infer<typeof XPostStatsSchema>;\n\nconst SerializableXPostSchemaContract = defineToolUiContract(\n  \"XPost\",\n  SerializableXPostSchema,\n);\n\nexport const parseSerializableXPost: (input: unknown) => XPostData =\n  SerializableXPostSchemaContract.parse;\n\nexport const safeParseSerializableXPost: (input: unknown) => XPostData | null =\n  SerializableXPostSchemaContract.safeParse;\n"
  },
  {
    "path": "apps/www/components/tool-ui/x-post/x-post.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Heart, Share } from \"lucide-react\";\nimport {\n  cn,\n  Button,\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"./_adapter\";\nimport { formatCount, formatRelativeTime, getDomain } from \"../shared/utils\";\n\nimport { resolveSafeNavigationHref } from \"../shared/media\";\nimport type { XPostData, XPostMedia, XPostLinkPreview } from \"./schema\";\n\nexport interface XPostProps {\n  post: XPostData;\n  className?: string;\n  onAction?: (action: string, post: XPostData) => void;\n}\n\nfunction Avatar({ src, alt }: { src: string; alt: string }) {\n  return (\n    <img\n      src={src}\n      alt={alt}\n      width={40}\n      height={40}\n      className=\"size-10 shrink-0 rounded-full object-cover\"\n    />\n  );\n}\n\nfunction XLogo({ className }: { className?: string }) {\n  return (\n    <svg\n      viewBox=\"0 0 300 271\"\n      className={className}\n      role=\"img\"\n      aria-label=\"X (formerly Twitter) logo\"\n    >\n      <path\n        fill=\"currentColor\"\n        d=\"m236 0h46l-101 115 118 156h-92.6l-72.5-94.8-83 94.8h-46l107-123-113-148h94.9l65.5 86.6zm-16.1 244h25.5l-165-218h-27.4z\"\n      />\n    </svg>\n  );\n}\n\nfunction VerifiedBadge({ className }: { className?: string }) {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      className={className}\n      role=\"img\"\n      aria-label=\"Verified account\"\n    >\n      <path\n        fill=\"currentColor\"\n        d=\"M22.5 12.5c0-1.58-.875-2.95-2.148-3.6.154-.435.238-.905.238-1.4 0-2.21-1.71-3.998-3.818-3.998-.47 0-.92.084-1.336.25C14.818 2.415 13.51 1.5 12 1.5s-2.816.917-3.437 2.25c-.415-.165-.866-.25-1.336-.25-2.11 0-3.818 1.79-3.818 4 0 .495.083.965.238 1.4-1.272.65-2.147 2.018-2.147 3.6 0 1.495.782 2.798 1.942 3.486-.02.17-.032.34-.032.514 0 2.21 1.708 4 3.818 4 .47 0 .92-.086 1.335-.25.62 1.334 1.926 2.25 3.437 2.25 1.512 0 2.818-.916 3.437-2.25.415.163.865.248 1.336.248 2.11 0 3.818-1.79 3.818-4 0-.174-.012-.344-.033-.513 1.158-.687 1.943-1.99 1.943-3.484zm-6.616-3.334l-4.334 6.5c-.145.217-.382.334-.625.334-.143 0-.288-.04-.416-.126l-.115-.094-2.415-2.415c-.293-.293-.293-.768 0-1.06s.768-.294 1.06 0l1.77 1.767 3.825-5.74c.23-.345.696-.436 1.04-.207.346.23.44.696.21 1.04z\"\n      />\n    </svg>\n  );\n}\n\nfunction AuthorInfo({\n  name,\n  handle,\n  verified,\n  createdAt,\n}: {\n  name: string;\n  handle: string;\n  verified?: boolean;\n  createdAt?: string;\n}) {\n  return (\n    <div className=\"flex min-w-0 items-center gap-1\">\n      <span className=\"truncate font-semibold\">{name}</span>\n      {verified && (\n        <VerifiedBadge className=\"size-[18px] shrink-0 text-blue-500\" />\n      )}\n      <span className=\"text-muted-foreground truncate\">@{handle}</span>\n      {createdAt && (\n        <>\n          <span className=\"text-muted-foreground\">·</span>\n          <span className=\"text-muted-foreground\">\n            {formatRelativeTime(createdAt)}\n          </span>\n        </>\n      )}\n    </div>\n  );\n}\n\nfunction PostBody({ text }: { text?: string }) {\n  if (!text) return null;\n  return (\n    <p className=\"text-[15px] leading-normal text-pretty wrap-break-word whitespace-pre-wrap\">\n      {text}\n    </p>\n  );\n}\n\nfunction PostMedia({\n  media,\n  onOpen,\n}: {\n  media: XPostMedia;\n  onOpen?: () => void;\n}) {\n  const aspectRatio =\n    media.aspectRatio === \"1:1\"\n      ? \"1\"\n      : media.aspectRatio === \"4:3\"\n        ? \"4/3\"\n        : \"16/9\";\n\n  return (\n    <button\n      type=\"button\"\n      className=\"bg-muted mt-2 w-full overflow-hidden rounded-xl\"\n      style={{ aspectRatio }}\n      onClick={() => onOpen?.()}\n    >\n      {media.type === \"image\" ? (\n        <img\n          src={media.url}\n          alt={media.alt}\n          className=\"size-full object-cover\"\n          loading=\"lazy\"\n        />\n      ) : (\n        <video\n          src={media.url}\n          controls\n          playsInline\n          className=\"size-full object-contain\"\n        />\n      )}\n    </button>\n  );\n}\n\nfunction PostLinkPreview({ preview }: { preview: XPostLinkPreview }) {\n  const href = resolveSafeNavigationHref(preview.url);\n  const domain = preview.domain ?? getDomain(preview.url);\n  const content = (\n    <>\n      {preview.imageUrl && (\n        <img\n          src={preview.imageUrl}\n          alt=\"\"\n          className=\"h-48 w-full object-cover\"\n          loading=\"lazy\"\n        />\n      )}\n      <div className=\"p-3\">\n        {domain && (\n          <div className=\"text-muted-foreground text-xs\">{domain}</div>\n        )}\n        {preview.title && (\n          <div className=\"font-medium text-pretty\">{preview.title}</div>\n        )}\n        {preview.description && (\n          <div className=\"text-muted-foreground line-clamp-2 text-sm text-pretty\">\n            {preview.description}\n          </div>\n        )}\n      </div>\n    </>\n  );\n\n  if (!href) {\n    return (\n      <div className=\"mt-2 block overflow-hidden rounded-xl border\">\n        {content}\n      </div>\n    );\n  }\n\n  return (\n    <a\n      href={href}\n      target=\"_blank\"\n      rel=\"noopener noreferrer\"\n      className=\"hover:bg-muted/50 mt-2 block overflow-hidden rounded-xl border transition-colors\"\n    >\n      {content}\n    </a>\n  );\n}\n\nfunction QuotedPostCard({ post }: { post: XPostData }) {\n  return (\n    <div className=\"hover:bg-muted/30 mt-2 rounded-xl border p-3 transition-colors\">\n      <div className=\"flex min-w-0 items-center gap-1\">\n        <img\n          src={post.author.avatarUrl}\n          alt={`${post.author.name} avatar`}\n          width={16}\n          height={16}\n          className=\"size-4 rounded-full object-cover\"\n        />\n        <span className=\"truncate font-semibold\">{post.author.name}</span>\n        {post.author.verified && (\n          <VerifiedBadge className=\"size-3.5 shrink-0 text-blue-500\" />\n        )}\n        <span className=\"text-muted-foreground truncate\">\n          @{post.author.handle}\n        </span>\n        {post.createdAt && (\n          <>\n            <span className=\"text-muted-foreground shrink-0\">·</span>\n            <span className=\"text-muted-foreground shrink-0\">\n              {formatRelativeTime(post.createdAt)}\n            </span>\n          </>\n        )}\n      </div>\n      {post.text && <p className=\"mt-1.5\">{post.text}</p>}\n      {post.media && (\n        <img\n          src={post.media.url}\n          alt={post.media.alt}\n          className=\"mt-2 rounded-lg\"\n        />\n      )}\n    </div>\n  );\n}\n\nfunction ActionButton({\n  icon: Icon,\n  label,\n  count,\n  active,\n  hoverColor,\n  activeColor,\n  onClick,\n}: {\n  icon: React.ComponentType<{ className?: string }>;\n  label: string;\n  count?: number;\n  active?: boolean;\n  hoverColor: string;\n  activeColor?: string;\n  onClick: () => void;\n}) {\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>\n        <Button\n          variant=\"ghost\"\n          size=\"sm\"\n          onClick={(e) => {\n            e.stopPropagation();\n            onClick();\n          }}\n          className={cn(\n            \"h-auto gap-1.5 px-2 py-1\",\n            hoverColor,\n            active && activeColor,\n          )}\n          aria-label={label}\n        >\n          <Icon className=\"size-4\" />\n          {count !== undefined && (\n            <span className=\"text-sm\">{formatCount(count)}</span>\n          )}\n        </Button>\n      </TooltipTrigger>\n      <TooltipContent>{label}</TooltipContent>\n    </Tooltip>\n  );\n}\n\nfunction PostActions({\n  stats,\n  onAction,\n}: {\n  stats?: XPostData[\"stats\"];\n  onAction: (action: string) => void;\n}) {\n  return (\n    <TooltipProvider delayDuration={300}>\n      <div className=\"mt-3 flex items-center gap-4\">\n        <ActionButton\n          icon={Heart}\n          label=\"Like\"\n          count={stats?.likes}\n          active={stats?.isLiked}\n          hoverColor=\"hover:text-pink-500 hover:bg-pink-500/10\"\n          activeColor=\"text-pink-500 fill-pink-500\"\n          onClick={() => onAction(\"like\")}\n        />\n        <ActionButton\n          icon={Share}\n          label=\"Share\"\n          hoverColor=\"hover:text-blue-500 hover:bg-blue-500/10\"\n          onClick={() => onAction(\"share\")}\n        />\n      </div>\n    </TooltipProvider>\n  );\n}\n\nexport function XPost({ post, className, onAction }: XPostProps) {\n  return (\n    <div\n      className={cn(\"flex max-w-xl flex-col gap-3\", className)}\n      data-tool-ui-id={post.id}\n      data-slot=\"x-post\"\n    >\n      <article className=\"bg-card rounded-xl border p-3 shadow-sm\">\n        <div className=\"flex gap-3\">\n          <Avatar\n            src={post.author.avatarUrl}\n            alt={`${post.author.name} avatar`}\n          />\n          <div className=\"min-w-0 flex-1\">\n            <div className=\"flex items-start justify-between gap-2\">\n              <AuthorInfo\n                name={post.author.name}\n                handle={post.author.handle}\n                verified={post.author.verified}\n                createdAt={post.createdAt}\n              />\n              <XLogo className=\"text-muted-foreground/40 size-4\" />\n            </div>\n            <PostBody text={post.text} />\n            {post.media && <PostMedia media={post.media} />}\n            {post.quotedPost && <QuotedPostCard post={post.quotedPost} />}\n            {post.linkPreview && !post.quotedPost && (\n              <PostLinkPreview preview={post.linkPreview} />\n            )}\n            <PostActions\n              stats={post.stats}\n              onAction={(action) => onAction?.(action, post)}\n            />\n          </div>\n        </div>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/ui/accordion.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\";\nimport { ChevronDownIcon } from \"lucide-react\";\n\nimport { cn } from \"@/lib/ui/cn\";\n\nfunction Accordion({\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Root>) {\n  return <AccordionPrimitive.Root data-slot=\"accordion\" {...props} />;\n}\n\nfunction AccordionItem({\n  className,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Item>) {\n  return (\n    <AccordionPrimitive.Item\n      data-slot=\"accordion-item\"\n      className={cn(\"border-b last:border-b-0\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction AccordionTrigger({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {\n  return (\n    <AccordionPrimitive.Header className=\"flex\">\n      <AccordionPrimitive.Trigger\n        data-slot=\"accordion-trigger\"\n        className={cn(\n          \"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:scale-y-[-1]\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        <ChevronDownIcon className=\"text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200\" />\n      </AccordionPrimitive.Trigger>\n    </AccordionPrimitive.Header>\n  );\n}\n\nfunction AccordionContent({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Content>) {\n  return (\n    <AccordionPrimitive.Content\n      data-slot=\"accordion-content\"\n      className=\"group data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-y-clip overflow-x-visible text-sm\"\n      {...props}\n    >\n      <div\n        className={cn(\n          \"pt-0 pb-4\",\n          \"group-data-[state=open]:animate-in group-data-[state=open]:fade-in-0\",\n          \"group-data-[state=closed]:animate-out group-data-[state=closed]:fade-out-0\",\n          \"duration-200\",\n          className,\n        )}\n      >\n        {children}\n      </div>\n    </AccordionPrimitive.Content>\n  );\n}\n\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent };\n"
  },
  {
    "path": "apps/www/components/ui/alert.tsx",
    "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/ui/cn\";\n\nconst alertVariants = cva(\n  \"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-card text-card-foreground\",\n        destructive:\n          \"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90\",\n        warning:\n          \"border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-900 dark:bg-amber-950/50 dark:text-amber-200 [&>svg]:text-amber-600 dark:[&>svg]:text-amber-200\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nfunction Alert({\n  className,\n  variant,\n  ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof alertVariants>) {\n  return (\n    <div\n      data-slot=\"alert\"\n      role=\"alert\"\n      className={cn(alertVariants({ variant }), className)}\n      {...props}\n    />\n  );\n}\n\nfunction AlertTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"alert-title\"\n      className={cn(\n        \"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction AlertDescription({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"alert-description\"\n      className={cn(\n        \"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Alert, AlertTitle, AlertDescription };\n"
  },
  {
    "path": "apps/www/components/ui/avatar.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\";\n\nimport { cn } from \"@/lib/ui/cn\";\n\nfunction Avatar({\n  className,\n  ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Root>) {\n  return (\n    <AvatarPrimitive.Root\n      data-slot=\"avatar\"\n      className={cn(\n        \"relative flex size-8 shrink-0 overflow-hidden rounded-full\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction AvatarImage({\n  className,\n  ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Image>) {\n  return (\n    <AvatarPrimitive.Image\n      data-slot=\"avatar-image\"\n      className={cn(\"aspect-square size-full\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction AvatarFallback({\n  className,\n  ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {\n  return (\n    <AvatarPrimitive.Fallback\n      data-slot=\"avatar-fallback\"\n      className={cn(\n        \"bg-muted flex size-full items-center justify-center rounded-full\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Avatar, AvatarImage, AvatarFallback };\n"
  },
  {
    "path": "apps/www/components/ui/badge.tsx",
    "content": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/ui/cn\";\n\nconst badgeVariants = cva(\n  \"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90\",\n        secondary:\n          \"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90\",\n        destructive:\n          \"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n        outline:\n          \"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n        warning:\n          \"border-transparent bg-amber-500/10 text-amber-700 dark:bg-amber-500/20 dark:text-amber-400 [a&]:hover:bg-amber-500/20 dark:[a&]:hover:bg-amber-500/30\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nfunction Badge({\n  className,\n  variant,\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"span\"> &\n  VariantProps<typeof badgeVariants> & { asChild?: boolean }) {\n  const Comp = asChild ? Slot : \"span\";\n\n  return (\n    <Comp\n      data-slot=\"badge\"\n      className={cn(badgeVariants({ variant }), className)}\n      {...props}\n    />\n  );\n}\n\nexport { Badge, badgeVariants };\n"
  },
  {
    "path": "apps/www/components/ui/button-group.tsx",
    "content": "import { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/ui/cn\";\nimport { Separator } from \"@/components/ui/separator\";\n\nconst buttonGroupVariants = cva(\n  \"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2\",\n  {\n    variants: {\n      orientation: {\n        horizontal:\n          \"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none\",\n        vertical:\n          \"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none\",\n      },\n    },\n    defaultVariants: {\n      orientation: \"horizontal\",\n    },\n  },\n);\n\nfunction ButtonGroup({\n  className,\n  orientation,\n  ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof buttonGroupVariants>) {\n  return (\n    <div\n      role=\"group\"\n      data-slot=\"button-group\"\n      data-orientation={orientation}\n      className={cn(buttonGroupVariants({ orientation }), className)}\n      {...props}\n    />\n  );\n}\n\nfunction ButtonGroupText({\n  className,\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  asChild?: boolean;\n}) {\n  const Comp = asChild ? Slot : \"div\";\n\n  return (\n    <Comp\n      className={cn(\n        \"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ButtonGroupSeparator({\n  className,\n  orientation = \"vertical\",\n  ...props\n}: React.ComponentProps<typeof Separator>) {\n  return (\n    <Separator\n      data-slot=\"button-group-separator\"\n      orientation={orientation}\n      className={cn(\n        \"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport {\n  ButtonGroup,\n  ButtonGroupSeparator,\n  ButtonGroupText,\n  buttonGroupVariants,\n};\n"
  },
  {
    "path": "apps/www/components/ui/button.tsx",
    "content": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/ui/cn\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n        outline:\n          \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost:\n          \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n        sm: \"h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5\",\n        lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n        icon: \"size-9\",\n        \"icon-sm\": \"size-8\",\n        \"icon-lg\": \"size-10\",\n        homeCTA: \"rounded-full px-6 py-3 text-lg w-fit text-start\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nfunction Button({\n  className,\n  variant = \"default\",\n  size = \"default\",\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"button\"> &\n  VariantProps<typeof buttonVariants> & {\n    asChild?: boolean;\n  }) {\n  const Comp = asChild ? Slot : \"button\";\n\n  return (\n    <Comp\n      data-slot=\"button\"\n      data-variant={variant}\n      data-size={size}\n      className={cn(buttonVariants({ variant, size, className }))}\n      {...props}\n    />\n  );\n}\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "apps/www/components/ui/card.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/ui/cn\";\n\nfunction Card({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card\"\n      className={cn(\n        \"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-header\"\n      className={cn(\n        \"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-title\"\n      className={cn(\"leading-none font-semibold\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardDescription({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardAction({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-action\"\n      className={cn(\n        \"col-start-2 row-span-2 row-start-1 self-start justify-self-end\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-content\"\n      className={cn(\"px-6\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-footer\"\n      className={cn(\"flex items-center px-6 [.border-t]:pt-6\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Card,\n  CardHeader,\n  CardFooter,\n  CardTitle,\n  CardAction,\n  CardDescription,\n  CardContent,\n};\n"
  },
  {
    "path": "apps/www/components/ui/chart.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as RechartsPrimitive from \"recharts\";\n\nimport { cn } from \"@/lib/ui/cn\";\n\n// Format: { THEME_NAME: CSS_SELECTOR }\nconst THEMES = { light: \"\", dark: \".dark\" } as const;\n\nexport type ChartConfig = {\n  [k in string]: {\n    label?: React.ReactNode;\n    icon?: React.ComponentType;\n  } & (\n    | { color?: string; theme?: never }\n    | { color?: never; theme: Record<keyof typeof THEMES, string> }\n  );\n};\n\ntype ChartContextProps = {\n  config: ChartConfig;\n};\n\nconst ChartContext = React.createContext<ChartContextProps | null>(null);\n\nfunction useChart() {\n  const context = React.useContext(ChartContext);\n\n  if (!context) {\n    throw new Error(\"useChart must be used within a <ChartContainer />\");\n  }\n\n  return context;\n}\n\nfunction ChartContainer({\n  id,\n  className,\n  children,\n  config,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  config: ChartConfig;\n  children: React.ComponentProps<\n    typeof RechartsPrimitive.ResponsiveContainer\n  >[\"children\"];\n}) {\n  const uniqueId = React.useId();\n  const chartId = `chart-${id || uniqueId.replace(/:/g, \"\")}`;\n\n  return (\n    <ChartContext.Provider value={{ config }}>\n      <div\n        data-slot=\"chart\"\n        data-chart={chartId}\n        className={cn(\n          \"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden\",\n          className,\n        )}\n        {...props}\n      >\n        <ChartStyle id={chartId} config={config} />\n        <RechartsPrimitive.ResponsiveContainer>\n          {children}\n        </RechartsPrimitive.ResponsiveContainer>\n      </div>\n    </ChartContext.Provider>\n  );\n}\n\nconst ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {\n  const colorConfig = Object.entries(config).filter(\n    ([, config]) => config.theme || config.color,\n  );\n\n  if (!colorConfig.length) {\n    return null;\n  }\n\n  return (\n    <style\n      dangerouslySetInnerHTML={{\n        __html: Object.entries(THEMES)\n          .map(\n            ([theme, prefix]) => `\n${prefix} [data-chart=${id}] {\n${colorConfig\n  .map(([key, itemConfig]) => {\n    const color =\n      itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||\n      itemConfig.color;\n    return color ? `  --color-${key}: ${color};` : null;\n  })\n  .join(\"\\n\")}\n}\n`,\n          )\n          .join(\"\\n\"),\n      }}\n    />\n  );\n};\n\nconst ChartTooltip = RechartsPrimitive.Tooltip;\n\nfunction ChartTooltipContent({\n  active,\n  payload,\n  className,\n  indicator = \"dot\",\n  hideLabel = false,\n  hideIndicator = false,\n  label,\n  labelFormatter,\n  labelClassName,\n  formatter,\n  color,\n  nameKey,\n  labelKey,\n}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &\n  React.ComponentProps<\"div\"> & {\n    hideLabel?: boolean;\n    hideIndicator?: boolean;\n    indicator?: \"line\" | \"dot\" | \"dashed\";\n    nameKey?: string;\n    labelKey?: string;\n  }) {\n  const { config } = useChart();\n\n  const tooltipLabel = React.useMemo(() => {\n    if (hideLabel || !payload?.length) {\n      return null;\n    }\n\n    const [item] = payload;\n    const key = `${labelKey || item?.dataKey || item?.name || \"value\"}`;\n    const itemConfig = getPayloadConfigFromPayload(config, item, key);\n    const value =\n      !labelKey && typeof label === \"string\"\n        ? config[label as keyof typeof config]?.label || label\n        : itemConfig?.label;\n\n    if (labelFormatter) {\n      return (\n        <div className={cn(\"font-medium\", labelClassName)}>\n          {labelFormatter(value, payload)}\n        </div>\n      );\n    }\n\n    if (!value) {\n      return null;\n    }\n\n    return <div className={cn(\"font-medium\", labelClassName)}>{value}</div>;\n  }, [\n    label,\n    labelFormatter,\n    payload,\n    hideLabel,\n    labelClassName,\n    config,\n    labelKey,\n  ]);\n\n  if (!active || !payload?.length) {\n    return null;\n  }\n\n  const nestLabel = payload.length === 1 && indicator !== \"dot\";\n\n  return (\n    <div\n      className={cn(\n        \"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl\",\n        className,\n      )}\n    >\n      {!nestLabel ? tooltipLabel : null}\n      <div className=\"grid gap-1.5\">\n        {payload\n          .filter((item) => item.type !== \"none\")\n          .map((item, index) => {\n            const key = `${nameKey || item.name || item.dataKey || \"value\"}`;\n            const itemConfig = getPayloadConfigFromPayload(config, item, key);\n            const indicatorColor = color || item.payload.fill || item.color;\n\n            return (\n              <div\n                key={item.dataKey}\n                className={cn(\n                  \"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5\",\n                  indicator === \"dot\" && \"items-center\",\n                )}\n              >\n                {formatter && item?.value !== undefined && item.name ? (\n                  formatter(item.value, item.name, item, index, item.payload)\n                ) : (\n                  <>\n                    {itemConfig?.icon ? (\n                      <itemConfig.icon />\n                    ) : (\n                      !hideIndicator && (\n                        <div\n                          className={cn(\n                            \"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)\",\n                            {\n                              \"h-2.5 w-2.5\": indicator === \"dot\",\n                              \"w-1\": indicator === \"line\",\n                              \"w-0 border-[1.5px] border-dashed bg-transparent\":\n                                indicator === \"dashed\",\n                              \"my-0.5\": nestLabel && indicator === \"dashed\",\n                            },\n                          )}\n                          style={\n                            {\n                              \"--color-bg\": indicatorColor,\n                              \"--color-border\": indicatorColor,\n                            } as React.CSSProperties\n                          }\n                        />\n                      )\n                    )}\n                    <div\n                      className={cn(\n                        \"flex flex-1 justify-between leading-none\",\n                        nestLabel ? \"items-end\" : \"items-center\",\n                      )}\n                    >\n                      <div className=\"grid gap-1.5\">\n                        {nestLabel ? tooltipLabel : null}\n                        <span className=\"text-muted-foreground\">\n                          {itemConfig?.label || item.name}\n                        </span>\n                      </div>\n                      {item.value && (\n                        <span className=\"text-foreground font-mono font-medium tabular-nums\">\n                          {item.value.toLocaleString()}\n                        </span>\n                      )}\n                    </div>\n                  </>\n                )}\n              </div>\n            );\n          })}\n      </div>\n    </div>\n  );\n}\n\nconst ChartLegend = RechartsPrimitive.Legend;\n\nfunction ChartLegendContent({\n  className,\n  hideIcon = false,\n  payload,\n  verticalAlign = \"bottom\",\n  nameKey,\n}: React.ComponentProps<\"div\"> &\n  Pick<RechartsPrimitive.LegendProps, \"payload\" | \"verticalAlign\"> & {\n    hideIcon?: boolean;\n    nameKey?: string;\n  }) {\n  const { config } = useChart();\n\n  if (!payload?.length) {\n    return null;\n  }\n\n  return (\n    <div\n      className={cn(\n        \"flex items-center justify-center gap-4\",\n        verticalAlign === \"top\" ? \"pb-3\" : \"pt-3\",\n        className,\n      )}\n    >\n      {payload\n        .filter((item) => item.type !== \"none\")\n        .map((item) => {\n          const key = `${nameKey || item.dataKey || \"value\"}`;\n          const itemConfig = getPayloadConfigFromPayload(config, item, key);\n\n          return (\n            <div\n              key={item.value}\n              className={cn(\n                \"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3\",\n              )}\n            >\n              {itemConfig?.icon && !hideIcon ? (\n                <itemConfig.icon />\n              ) : (\n                <div\n                  className=\"h-2 w-2 shrink-0 rounded-[2px]\"\n                  style={{\n                    backgroundColor: item.color,\n                  }}\n                />\n              )}\n              {itemConfig?.label}\n            </div>\n          );\n        })}\n    </div>\n  );\n}\n\n// Helper to extract item config from a payload.\nfunction getPayloadConfigFromPayload(\n  config: ChartConfig,\n  payload: unknown,\n  key: string,\n) {\n  if (typeof payload !== \"object\" || payload === null) {\n    return undefined;\n  }\n\n  const payloadPayload =\n    \"payload\" in payload &&\n    typeof payload.payload === \"object\" &&\n    payload.payload !== null\n      ? payload.payload\n      : undefined;\n\n  let configLabelKey: string = key;\n\n  if (\n    key in payload &&\n    typeof payload[key as keyof typeof payload] === \"string\"\n  ) {\n    configLabelKey = payload[key as keyof typeof payload] as string;\n  } else if (\n    payloadPayload &&\n    key in payloadPayload &&\n    typeof payloadPayload[key as keyof typeof payloadPayload] === \"string\"\n  ) {\n    configLabelKey = payloadPayload[\n      key as keyof typeof payloadPayload\n    ] as string;\n  }\n\n  return configLabelKey in config\n    ? config[configLabelKey]\n    : config[key as keyof typeof config];\n}\n\nexport {\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent,\n  ChartLegend,\n  ChartLegendContent,\n  ChartStyle,\n};\n"
  },
  {
    "path": "apps/www/components/ui/code-block.tsx",
    "content": "\"use client\";\n\nimport { cn } from \"@/lib/ui/cn\";\nimport { useTheme } from \"next-themes\";\nimport React, { useEffect, useState } from \"react\";\nimport { codeToHtml } from \"shiki\";\n\nexport type CodeBlockProps = {\n  children?: React.ReactNode;\n  className?: string;\n} & React.HTMLProps<HTMLDivElement>;\n\nfunction CodeBlock({ children, className, ...props }: CodeBlockProps) {\n  return (\n    <div\n      className={cn(\n        \"not-prose flex w-full flex-col overflow-clip border\",\n        \"border-border bg-card text-card-foreground rounded\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport type CodeBlockCodeProps = {\n  code: string;\n  language?: string;\n  theme?: string;\n  className?: string;\n} & React.HTMLProps<HTMLDivElement>;\n\nfunction CodeBlockCode({\n  code,\n  language = \"tsx\",\n  className,\n  ...props\n}: CodeBlockCodeProps) {\n  const { theme: browserTheme } = useTheme();\n\n  const [highlightedHtml, setHighlightedHtml] = useState<string | null>(null);\n\n  const theme = browserTheme === \"dark\" ? \"github-dark\" : \"github-light\";\n\n  useEffect(() => {\n    async function highlight() {\n      if (!code) {\n        setHighlightedHtml(\"<pre><code></code></pre>\");\n        return;\n      }\n\n      const html = await codeToHtml(code, { lang: language, theme });\n      setHighlightedHtml(html);\n    }\n    highlight();\n  }, [code, language, theme]);\n\n  const classNames = cn(\n    \"w-full overflow-x-auto text-[13px] [&>pre]:px-4 [&>pre]:py-4\",\n    className,\n  );\n\n  // SSR fallback: render plain code if not hydrated yet\n  return highlightedHtml ? (\n    <div\n      className={classNames}\n      dangerouslySetInnerHTML={{ __html: highlightedHtml }}\n      {...props}\n    />\n  ) : (\n    <div className={classNames} {...props}>\n      <pre>\n        <code>{code}</code>\n      </pre>\n    </div>\n  );\n}\n\nexport type CodeBlockGroupProps = React.HTMLAttributes<HTMLDivElement>;\n\nfunction CodeBlockGroup({\n  children,\n  className,\n  ...props\n}: CodeBlockGroupProps) {\n  return (\n    <div\n      className={cn(\"flex items-center justify-between\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport { CodeBlockGroup, CodeBlockCode, CodeBlock };\n"
  },
  {
    "path": "apps/www/components/ui/collapsible.tsx",
    "content": "\"use client\";\n\nimport * as CollapsiblePrimitive from \"@radix-ui/react-collapsible\";\nimport { cn } from \"@/lib/ui/cn\";\n\nfunction Collapsible({\n  ...props\n}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {\n  return <CollapsiblePrimitive.Root data-slot=\"collapsible\" {...props} />;\n}\n\nfunction CollapsibleTrigger({\n  ...props\n}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {\n  return (\n    <CollapsiblePrimitive.CollapsibleTrigger\n      data-slot=\"collapsible-trigger\"\n      {...props}\n    />\n  );\n}\n\nfunction CollapsibleContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {\n  return (\n    <CollapsiblePrimitive.CollapsibleContent\n      data-slot=\"collapsible-content\"\n      className={cn(\n        \"overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Collapsible, CollapsibleTrigger, CollapsibleContent };\n"
  },
  {
    "path": "apps/www/components/ui/dialog.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { XIcon } from \"lucide-react\";\n\nimport { cn } from \"@/lib/ui/cn\";\n\nfunction Dialog({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Root>) {\n  return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />;\n}\n\nfunction DialogTrigger({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n  return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />;\n}\n\nfunction DialogPortal({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n  return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />;\n}\n\nfunction DialogClose({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Close>) {\n  return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />;\n}\n\nfunction DialogOverlay({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {\n  return (\n    <DialogPrimitive.Overlay\n      data-slot=\"dialog-overlay\"\n      className={cn(\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DialogContent({\n  className,\n  children,\n  showCloseButton = true,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Content> & {\n  showCloseButton?: boolean;\n}) {\n  return (\n    <DialogPortal data-slot=\"dialog-portal\">\n      <DialogOverlay />\n      <DialogPrimitive.Content\n        data-slot=\"dialog-content\"\n        className={cn(\n          \"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        {showCloseButton && (\n          <DialogPrimitive.Close\n            data-slot=\"dialog-close\"\n            className=\"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n          >\n            <XIcon />\n            <span className=\"sr-only\">Close</span>\n          </DialogPrimitive.Close>\n        )}\n      </DialogPrimitive.Content>\n    </DialogPortal>\n  );\n}\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"dialog-header\"\n      className={cn(\"flex flex-col gap-2 text-center sm:text-left\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DialogFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"dialog-footer\"\n      className={cn(\n        \"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DialogTitle({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Title>) {\n  return (\n    <DialogPrimitive.Title\n      data-slot=\"dialog-title\"\n      className={cn(\"text-lg leading-none font-semibold\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DialogDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Description>) {\n  return (\n    <DialogPrimitive.Description\n      data-slot=\"dialog-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogOverlay,\n  DialogPortal,\n  DialogTitle,\n  DialogTrigger,\n};\n"
  },
  {
    "path": "apps/www/components/ui/dropdown-menu.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\";\nimport { CheckIcon, ChevronRightIcon, CircleIcon } from \"lucide-react\";\n\nimport { cn } from \"@/lib/ui/cn\";\n\nfunction DropdownMenu({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {\n  return <DropdownMenuPrimitive.Root data-slot=\"dropdown-menu\" {...props} />;\n}\n\nfunction DropdownMenuPortal({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {\n  return (\n    <DropdownMenuPrimitive.Portal data-slot=\"dropdown-menu-portal\" {...props} />\n  );\n}\n\nfunction DropdownMenuTrigger({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {\n  return (\n    <DropdownMenuPrimitive.Trigger\n      data-slot=\"dropdown-menu-trigger\"\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuContent({\n  className,\n  sideOffset = 4,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {\n  return (\n    <DropdownMenuPrimitive.Portal>\n      <DropdownMenuPrimitive.Content\n        data-slot=\"dropdown-menu-content\"\n        sideOffset={sideOffset}\n        className={cn(\n          \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md\",\n          className,\n        )}\n        {...props}\n      />\n    </DropdownMenuPrimitive.Portal>\n  );\n}\n\nfunction DropdownMenuGroup({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {\n  return (\n    <DropdownMenuPrimitive.Group data-slot=\"dropdown-menu-group\" {...props} />\n  );\n}\n\nfunction DropdownMenuItem({\n  className,\n  inset,\n  variant = \"default\",\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {\n  inset?: boolean;\n  variant?: \"default\" | \"destructive\";\n}) {\n  return (\n    <DropdownMenuPrimitive.Item\n      data-slot=\"dropdown-menu-item\"\n      data-inset={inset}\n      data-variant={variant}\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuCheckboxItem({\n  className,\n  children,\n  checked,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {\n  return (\n    <DropdownMenuPrimitive.CheckboxItem\n      data-slot=\"dropdown-menu-checkbox-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      checked={checked}\n      {...props}\n    >\n      <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n        <DropdownMenuPrimitive.ItemIndicator>\n          <CheckIcon className=\"size-4\" />\n        </DropdownMenuPrimitive.ItemIndicator>\n      </span>\n      {children}\n    </DropdownMenuPrimitive.CheckboxItem>\n  );\n}\n\nfunction DropdownMenuRadioGroup({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {\n  return (\n    <DropdownMenuPrimitive.RadioGroup\n      data-slot=\"dropdown-menu-radio-group\"\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuRadioItem({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {\n  return (\n    <DropdownMenuPrimitive.RadioItem\n      data-slot=\"dropdown-menu-radio-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    >\n      <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n        <DropdownMenuPrimitive.ItemIndicator>\n          <CircleIcon className=\"size-2 fill-current\" />\n        </DropdownMenuPrimitive.ItemIndicator>\n      </span>\n      {children}\n    </DropdownMenuPrimitive.RadioItem>\n  );\n}\n\nfunction DropdownMenuLabel({\n  className,\n  inset,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {\n  inset?: boolean;\n}) {\n  return (\n    <DropdownMenuPrimitive.Label\n      data-slot=\"dropdown-menu-label\"\n      data-inset={inset}\n      className={cn(\n        \"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {\n  return (\n    <DropdownMenuPrimitive.Separator\n      data-slot=\"dropdown-menu-separator\"\n      className={cn(\"bg-border -mx-1 my-1 h-px\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuShortcut({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"dropdown-menu-shortcut\"\n      className={cn(\n        \"text-muted-foreground ml-auto text-xs tracking-widest\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuSub({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {\n  return <DropdownMenuPrimitive.Sub data-slot=\"dropdown-menu-sub\" {...props} />;\n}\n\nfunction DropdownMenuSubTrigger({\n  className,\n  inset,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {\n  inset?: boolean;\n}) {\n  return (\n    <DropdownMenuPrimitive.SubTrigger\n      data-slot=\"dropdown-menu-sub-trigger\"\n      data-inset={inset}\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <ChevronRightIcon className=\"ml-auto size-4\" />\n    </DropdownMenuPrimitive.SubTrigger>\n  );\n}\n\nfunction DropdownMenuSubContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {\n  return (\n    <DropdownMenuPrimitive.SubContent\n      data-slot=\"dropdown-menu-sub-content\"\n      className={cn(\n        \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport {\n  DropdownMenu,\n  DropdownMenuPortal,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuLabel,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuSub,\n  DropdownMenuSubTrigger,\n  DropdownMenuSubContent,\n};\n"
  },
  {
    "path": "apps/www/components/ui/input-group.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/ui/cn\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Textarea } from \"@/components/ui/textarea\";\n\nfunction InputGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"input-group\"\n      role=\"group\"\n      className={cn(\n        \"group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none\",\n        \"h-9 min-w-0 has-[>textarea]:h-auto\",\n\n        // Variants based on alignment.\n        \"has-[>[data-align=inline-start]]:[&>input]:pl-2\",\n        \"has-[>[data-align=inline-end]]:[&>input]:pr-2\",\n        \"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3\",\n        \"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3\",\n\n        // Focus state.\n        \"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]\",\n\n        // Error state.\n        \"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40\",\n\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nconst inputGroupAddonVariants = cva(\n  \"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50\",\n  {\n    variants: {\n      align: {\n        \"inline-start\":\n          \"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]\",\n        \"inline-end\":\n          \"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]\",\n        \"block-start\":\n          \"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5\",\n        \"block-end\":\n          \"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5\",\n      },\n    },\n    defaultVariants: {\n      align: \"inline-start\",\n    },\n  },\n);\n\nfunction InputGroupAddon({\n  className,\n  align = \"inline-start\",\n  ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof inputGroupAddonVariants>) {\n  return (\n    <div\n      role=\"group\"\n      data-slot=\"input-group-addon\"\n      data-align={align}\n      className={cn(inputGroupAddonVariants({ align }), className)}\n      onClick={(e) => {\n        if ((e.target as HTMLElement).closest(\"button\")) {\n          return;\n        }\n        e.currentTarget.parentElement?.querySelector(\"input\")?.focus();\n      }}\n      {...props}\n    />\n  );\n}\n\nconst inputGroupButtonVariants = cva(\n  \"text-sm shadow-none flex gap-2 items-center\",\n  {\n    variants: {\n      size: {\n        xs: \"h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2\",\n        sm: \"h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5\",\n        \"icon-xs\":\n          \"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0\",\n        \"icon-sm\": \"size-8 p-0 has-[>svg]:p-0\",\n      },\n    },\n    defaultVariants: {\n      size: \"xs\",\n    },\n  },\n);\n\nfunction InputGroupButton({\n  className,\n  type = \"button\",\n  variant = \"ghost\",\n  size = \"xs\",\n  ...props\n}: Omit<React.ComponentProps<typeof Button>, \"size\"> &\n  VariantProps<typeof inputGroupButtonVariants>) {\n  return (\n    <Button\n      type={type}\n      data-size={size}\n      variant={variant}\n      className={cn(inputGroupButtonVariants({ size }), className)}\n      {...props}\n    />\n  );\n}\n\nfunction InputGroupText({ className, ...props }: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      className={cn(\n        \"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction InputGroupInput({\n  className,\n  ...props\n}: React.ComponentProps<\"input\">) {\n  return (\n    <Input\n      data-slot=\"input-group-control\"\n      className={cn(\n        \"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction InputGroupTextarea({\n  className,\n  ...props\n}: React.ComponentProps<\"textarea\">) {\n  return (\n    <Textarea\n      data-slot=\"input-group-control\"\n      className={cn(\n        \"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupButton,\n  InputGroupText,\n  InputGroupInput,\n  InputGroupTextarea,\n};\n"
  },
  {
    "path": "apps/www/components/ui/input.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/ui/cn\";\n\nfunction Input({ className, type, ...props }: React.ComponentProps<\"input\">) {\n  return (\n    <input\n      type={type}\n      data-slot=\"input\"\n      className={cn(\n        \"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n        \"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]\",\n        \"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Input };\n"
  },
  {
    "path": "apps/www/components/ui/item.tsx",
    "content": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/ui/cn\";\nimport { Separator } from \"@/components/ui/separator\";\n\nfunction ItemGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      role=\"list\"\n      data-slot=\"item-group\"\n      className={cn(\"group/item-group flex flex-col\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction ItemSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof Separator>) {\n  return (\n    <Separator\n      data-slot=\"item-separator\"\n      orientation=\"horizontal\"\n      className={cn(\"my-0\", className)}\n      {...props}\n    />\n  );\n}\n\nconst itemVariants = cva(\n  \"group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-transparent\",\n        outline: \"border-border\",\n        muted: \"bg-muted/50\",\n      },\n      size: {\n        default: \"p-4 gap-4 \",\n        sm: \"py-3 px-4 gap-2.5\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nfunction Item({\n  className,\n  variant = \"default\",\n  size = \"default\",\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"div\"> &\n  VariantProps<typeof itemVariants> & { asChild?: boolean }) {\n  const Comp = asChild ? Slot : \"div\";\n  return (\n    <Comp\n      data-slot=\"item\"\n      data-variant={variant}\n      data-size={size}\n      className={cn(itemVariants({ variant, size, className }))}\n      {...props}\n    />\n  );\n}\n\nconst itemMediaVariants = cva(\n  \"flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-transparent\",\n        icon: \"size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4\",\n        image:\n          \"size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nfunction ItemMedia({\n  className,\n  variant = \"default\",\n  ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof itemMediaVariants>) {\n  return (\n    <div\n      data-slot=\"item-media\"\n      data-variant={variant}\n      className={cn(itemMediaVariants({ variant, className }))}\n      {...props}\n    />\n  );\n}\n\nfunction ItemContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"item-content\"\n      className={cn(\n        \"flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ItemTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"item-title\"\n      className={cn(\n        \"flex w-fit items-center gap-2 text-sm leading-snug font-medium\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ItemDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n  return (\n    <p\n      data-slot=\"item-description\"\n      className={cn(\n        \"text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance\",\n        \"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ItemActions({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"item-actions\"\n      className={cn(\"flex items-center gap-2\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction ItemHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"item-header\"\n      className={cn(\n        \"flex basis-full items-center justify-between gap-2\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ItemFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"item-footer\"\n      className={cn(\n        \"flex basis-full items-center justify-between gap-2\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Item,\n  ItemMedia,\n  ItemContent,\n  ItemActions,\n  ItemGroup,\n  ItemSeparator,\n  ItemTitle,\n  ItemDescription,\n  ItemHeader,\n  ItemFooter,\n};\n"
  },
  {
    "path": "apps/www/components/ui/label.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as LabelPrimitive from \"@radix-ui/react-label\";\n\nimport { cn } from \"@/lib/ui/cn\";\n\nfunction Label({\n  className,\n  ...props\n}: React.ComponentProps<typeof LabelPrimitive.Root>) {\n  return (\n    <LabelPrimitive.Root\n      data-slot=\"label\"\n      className={cn(\n        \"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Label };\n"
  },
  {
    "path": "apps/www/components/ui/logo.tsx",
    "content": "import * as React from \"react\";\n\nexport function LogoMark({\n  className,\n  ...props\n}: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 24 24\"\n      fill=\"currentColor\"\n      className={className}\n      {...props}\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M12 1L21.526 6.5V17.5L12 23L2.474 17.5V6.5L12 1ZM12 7C9.239 7 7 9.239 7 12C7 14.761 9.239 17 12 17C14.761 17 17 14.761 17 12C17 9.239 14.761 7 12 7Z\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "apps/www/components/ui/popover.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\";\n\nimport { cn } from \"@/lib/ui/cn\";\n\nfunction Popover({\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Root>) {\n  return <PopoverPrimitive.Root data-slot=\"popover\" {...props} />;\n}\n\nfunction PopoverTrigger({\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {\n  return <PopoverPrimitive.Trigger data-slot=\"popover-trigger\" {...props} />;\n}\n\nfunction PopoverAnchor({\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {\n  return <PopoverPrimitive.Anchor data-slot=\"popover-anchor\" {...props} />;\n}\n\nfunction PopoverContent({\n  className,\n  align = \"center\",\n  sideOffset = 4,\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Content>) {\n  return (\n    <PopoverPrimitive.Portal>\n      <PopoverPrimitive.Content\n        data-slot=\"popover-content\"\n        align={align}\n        sideOffset={sideOffset}\n        className={cn(\n          \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md outline-hidden\",\n          className,\n        )}\n        {...props}\n      />\n    </PopoverPrimitive.Portal>\n  );\n}\n\nexport { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };\n"
  },
  {
    "path": "apps/www/components/ui/radio-group.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\";\nimport { CircleIcon } from \"lucide-react\";\n\nimport { cn } from \"@/lib/ui/cn\";\n\nfunction RadioGroup({\n  className,\n  ...props\n}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {\n  return (\n    <RadioGroupPrimitive.Root\n      data-slot=\"radio-group\"\n      className={cn(\"grid gap-3\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction RadioGroupItem({\n  className,\n  ...props\n}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {\n  return (\n    <RadioGroupPrimitive.Item\n      data-slot=\"radio-group-item\"\n      className={cn(\n        \"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    >\n      <RadioGroupPrimitive.Indicator\n        data-slot=\"radio-group-indicator\"\n        className=\"relative flex items-center justify-center\"\n      >\n        <CircleIcon className=\"fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2\" />\n      </RadioGroupPrimitive.Indicator>\n    </RadioGroupPrimitive.Item>\n  );\n}\n\nexport { RadioGroup, RadioGroupItem };\n"
  },
  {
    "path": "apps/www/components/ui/select.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport { CheckIcon, ChevronDownIcon, ChevronUpIcon } from \"lucide-react\";\n\nimport { cn } from \"@/lib/ui/cn\";\n\nfunction Select({\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Root>) {\n  return <SelectPrimitive.Root data-slot=\"select\" {...props} />;\n}\n\nfunction SelectGroup({\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Group>) {\n  return <SelectPrimitive.Group data-slot=\"select-group\" {...props} />;\n}\n\nfunction SelectValue({\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Value>) {\n  return <SelectPrimitive.Value data-slot=\"select-value\" {...props} />;\n}\n\nfunction SelectTrigger({\n  className,\n  size = \"default\",\n  children,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {\n  size?: \"sm\" | \"default\";\n}) {\n  return (\n    <SelectPrimitive.Trigger\n      data-slot=\"select-trigger\"\n      data-size={size}\n      className={cn(\n        \"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <SelectPrimitive.Icon asChild>\n        <ChevronDownIcon className=\"size-4 opacity-50\" />\n      </SelectPrimitive.Icon>\n    </SelectPrimitive.Trigger>\n  );\n}\n\nfunction SelectContent({\n  className,\n  children,\n  position = \"popper\",\n  align = \"center\",\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Content>) {\n  return (\n    <SelectPrimitive.Portal>\n      <SelectPrimitive.Content\n        data-slot=\"select-content\"\n        className={cn(\n          \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md\",\n          position === \"popper\" &&\n            \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n          className,\n        )}\n        position={position}\n        align={align}\n        {...props}\n      >\n        <SelectScrollUpButton />\n        <SelectPrimitive.Viewport\n          className={cn(\n            \"p-1\",\n            position === \"popper\" &&\n              \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1\",\n          )}\n        >\n          {children}\n        </SelectPrimitive.Viewport>\n        <SelectScrollDownButton />\n      </SelectPrimitive.Content>\n    </SelectPrimitive.Portal>\n  );\n}\n\nfunction SelectLabel({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Label>) {\n  return (\n    <SelectPrimitive.Label\n      data-slot=\"select-label\"\n      className={cn(\"text-muted-foreground px-2 py-1.5 text-xs\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SelectItem({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Item>) {\n  return (\n    <SelectPrimitive.Item\n      data-slot=\"select-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2\",\n        className,\n      )}\n      {...props}\n    >\n      <span className=\"absolute right-2 flex size-3.5 items-center justify-center\">\n        <SelectPrimitive.ItemIndicator>\n          <CheckIcon className=\"size-4\" />\n        </SelectPrimitive.ItemIndicator>\n      </span>\n      <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n    </SelectPrimitive.Item>\n  );\n}\n\nfunction SelectSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Separator>) {\n  return (\n    <SelectPrimitive.Separator\n      data-slot=\"select-separator\"\n      className={cn(\"bg-border pointer-events-none -mx-1 my-1 h-px\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SelectScrollUpButton({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {\n  return (\n    <SelectPrimitive.ScrollUpButton\n      data-slot=\"select-scroll-up-button\"\n      className={cn(\n        \"flex cursor-default items-center justify-center py-1\",\n        className,\n      )}\n      {...props}\n    >\n      <ChevronUpIcon className=\"size-4\" />\n    </SelectPrimitive.ScrollUpButton>\n  );\n}\n\nfunction SelectScrollDownButton({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {\n  return (\n    <SelectPrimitive.ScrollDownButton\n      data-slot=\"select-scroll-down-button\"\n      className={cn(\n        \"flex cursor-default items-center justify-center py-1\",\n        className,\n      )}\n      {...props}\n    >\n      <ChevronDownIcon className=\"size-4\" />\n    </SelectPrimitive.ScrollDownButton>\n  );\n}\n\nexport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectLabel,\n  SelectScrollDownButton,\n  SelectScrollUpButton,\n  SelectSeparator,\n  SelectTrigger,\n  SelectValue,\n};\n"
  },
  {
    "path": "apps/www/components/ui/separator.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\";\n\nimport { cn } from \"@/lib/ui/cn\";\n\nfunction Separator({\n  className,\n  orientation = \"horizontal\",\n  decorative = true,\n  ...props\n}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {\n  return (\n    <SeparatorPrimitive.Root\n      data-slot=\"separator\"\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        \"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Separator };\n"
  },
  {
    "path": "apps/www/components/ui/sheet.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SheetPrimitive from \"@radix-ui/react-dialog\";\nimport { XIcon } from \"lucide-react\";\n\nimport { cn } from \"@/lib/ui/cn\";\n\nfunction Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {\n  return <SheetPrimitive.Root data-slot=\"sheet\" {...props} />;\n}\n\nfunction SheetTrigger({\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {\n  return <SheetPrimitive.Trigger data-slot=\"sheet-trigger\" {...props} />;\n}\n\nfunction SheetClose({\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Close>) {\n  return <SheetPrimitive.Close data-slot=\"sheet-close\" {...props} />;\n}\n\nfunction SheetPortal({\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Portal>) {\n  return <SheetPrimitive.Portal data-slot=\"sheet-portal\" {...props} />;\n}\n\nfunction SheetOverlay({\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {\n  return (\n    <SheetPrimitive.Overlay\n      data-slot=\"sheet-overlay\"\n      className={cn(\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction SheetContent({\n  className,\n  children,\n  side = \"right\",\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Content> & {\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n}) {\n  return (\n    <SheetPortal>\n      <SheetOverlay />\n      <SheetPrimitive.Content\n        data-slot=\"sheet-content\"\n        className={cn(\n          \"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500\",\n          side === \"right\" &&\n            \"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm\",\n          side === \"left\" &&\n            \"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm\",\n          side === \"top\" &&\n            \"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b\",\n          side === \"bottom\" &&\n            \"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        <SheetPrimitive.Close className=\"ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none\">\n          <XIcon className=\"size-4\" />\n          <span className=\"sr-only\">Close</span>\n        </SheetPrimitive.Close>\n      </SheetPrimitive.Content>\n    </SheetPortal>\n  );\n}\n\nfunction SheetHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sheet-header\"\n      className={cn(\"flex flex-col gap-1.5 p-4\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SheetFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sheet-footer\"\n      className={cn(\"mt-auto flex flex-col gap-2 p-4\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SheetTitle({\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Title>) {\n  return (\n    <SheetPrimitive.Title\n      data-slot=\"sheet-title\"\n      className={cn(\"text-foreground font-semibold\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SheetDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Description>) {\n  return (\n    <SheetPrimitive.Description\n      data-slot=\"sheet-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Sheet,\n  SheetTrigger,\n  SheetClose,\n  SheetContent,\n  SheetHeader,\n  SheetFooter,\n  SheetTitle,\n  SheetDescription,\n};\n"
  },
  {
    "path": "apps/www/components/ui/sidebar.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { PanelLeftIcon } from \"lucide-react\";\n\nimport { useIsMobile } from \"@/hooks/use-mobile\";\nimport { cn } from \"@/lib/ui/cn\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Separator } from \"@/components/ui/separator\";\nimport {\n  Sheet,\n  SheetContent,\n  SheetDescription,\n  SheetHeader,\n  SheetTitle,\n} from \"@/components/ui/sheet\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\n\nconst SIDEBAR_COOKIE_NAME = \"sidebar_state\";\nconst SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;\nconst SIDEBAR_WIDTH = \"16rem\";\nconst SIDEBAR_WIDTH_MOBILE = \"18rem\";\nconst SIDEBAR_WIDTH_ICON = \"3rem\";\nconst SIDEBAR_KEYBOARD_SHORTCUT = \"b\";\n\ntype SidebarContextProps = {\n  state: \"expanded\" | \"collapsed\";\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  openMobile: boolean;\n  setOpenMobile: (open: boolean) => void;\n  isMobile: boolean;\n  toggleSidebar: () => void;\n};\n\nconst SidebarContext = React.createContext<SidebarContextProps | null>(null);\n\nfunction useSidebar() {\n  const context = React.useContext(SidebarContext);\n  if (!context) {\n    throw new Error(\"useSidebar must be used within a SidebarProvider.\");\n  }\n\n  return context;\n}\n\nfunction SidebarProvider({\n  defaultOpen = true,\n  open: openProp,\n  onOpenChange: setOpenProp,\n  className,\n  style,\n  children,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  defaultOpen?: boolean;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}) {\n  const isMobile = useIsMobile();\n  const [openMobile, setOpenMobile] = React.useState(false);\n\n  // This is the internal state of the sidebar.\n  // We use openProp and setOpenProp for control from outside the component.\n  const [_open, _setOpen] = React.useState(defaultOpen);\n  const open = openProp ?? _open;\n  const setOpen = React.useCallback(\n    (value: boolean | ((value: boolean) => boolean)) => {\n      const openState = typeof value === \"function\" ? value(open) : value;\n      if (setOpenProp) {\n        setOpenProp(openState);\n      } else {\n        _setOpen(openState);\n      }\n\n      // This sets the cookie to keep the sidebar state.\n      document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;\n    },\n    [setOpenProp, open],\n  );\n\n  // Helper to toggle the sidebar.\n  const toggleSidebar = React.useCallback(() => {\n    return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);\n  }, [isMobile, setOpen, setOpenMobile]);\n\n  // Adds a keyboard shortcut to toggle the sidebar.\n  React.useEffect(() => {\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (\n        event.key === SIDEBAR_KEYBOARD_SHORTCUT &&\n        (event.metaKey || event.ctrlKey)\n      ) {\n        event.preventDefault();\n        toggleSidebar();\n      }\n    };\n\n    window.addEventListener(\"keydown\", handleKeyDown);\n    return () => window.removeEventListener(\"keydown\", handleKeyDown);\n  }, [toggleSidebar]);\n\n  // We add a state so that we can do data-state=\"expanded\" or \"collapsed\".\n  // This makes it easier to style the sidebar with Tailwind classes.\n  const state = open ? \"expanded\" : \"collapsed\";\n\n  const contextValue = React.useMemo<SidebarContextProps>(\n    () => ({\n      state,\n      open,\n      setOpen,\n      isMobile,\n      openMobile,\n      setOpenMobile,\n      toggleSidebar,\n    }),\n    [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],\n  );\n\n  return (\n    <SidebarContext.Provider value={contextValue}>\n      <TooltipProvider delayDuration={0}>\n        <div\n          data-slot=\"sidebar-wrapper\"\n          style={\n            {\n              \"--sidebar-width\": SIDEBAR_WIDTH,\n              \"--sidebar-width-icon\": SIDEBAR_WIDTH_ICON,\n              ...style,\n            } as React.CSSProperties\n          }\n          className={cn(\n            \"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full\",\n            className,\n          )}\n          {...props}\n        >\n          {children}\n        </div>\n      </TooltipProvider>\n    </SidebarContext.Provider>\n  );\n}\n\nfunction Sidebar({\n  side = \"left\",\n  variant = \"sidebar\",\n  collapsible = \"offcanvas\",\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  side?: \"left\" | \"right\";\n  variant?: \"sidebar\" | \"floating\" | \"inset\";\n  collapsible?: \"offcanvas\" | \"icon\" | \"none\";\n}) {\n  const { isMobile, state, openMobile, setOpenMobile } = useSidebar();\n\n  if (collapsible === \"none\") {\n    return (\n      <div\n        data-slot=\"sidebar\"\n        className={cn(\n          \"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </div>\n    );\n  }\n\n  if (isMobile) {\n    return (\n      <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>\n        <SheetContent\n          data-sidebar=\"sidebar\"\n          data-slot=\"sidebar\"\n          data-mobile=\"true\"\n          className=\"bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden\"\n          style={\n            {\n              \"--sidebar-width\": SIDEBAR_WIDTH_MOBILE,\n            } as React.CSSProperties\n          }\n          side={side}\n        >\n          <SheetHeader className=\"sr-only\">\n            <SheetTitle>Sidebar</SheetTitle>\n            <SheetDescription>Displays the mobile sidebar.</SheetDescription>\n          </SheetHeader>\n          <div className=\"flex h-full w-full flex-col\">{children}</div>\n        </SheetContent>\n      </Sheet>\n    );\n  }\n\n  return (\n    <div\n      className=\"group peer text-sidebar-foreground hidden md:block\"\n      data-state={state}\n      data-collapsible={state === \"collapsed\" ? collapsible : \"\"}\n      data-variant={variant}\n      data-side={side}\n      data-slot=\"sidebar\"\n    >\n      {/* This is what handles the sidebar gap on desktop */}\n      <div\n        data-slot=\"sidebar-gap\"\n        className={cn(\n          \"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear\",\n          \"group-data-[collapsible=offcanvas]:w-0\",\n          \"group-data-[side=right]:rotate-180\",\n          variant === \"floating\" || variant === \"inset\"\n            ? \"group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]\"\n            : \"group-data-[collapsible=icon]:w-(--sidebar-width-icon)\",\n        )}\n      />\n      <div\n        data-slot=\"sidebar-container\"\n        className={cn(\n          \"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex\",\n          side === \"left\"\n            ? \"left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]\"\n            : \"right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]\",\n          // Adjust the padding for floating and inset variants.\n          variant === \"floating\" || variant === \"inset\"\n            ? \"p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]\"\n            : \"group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l\",\n          className,\n        )}\n        {...props}\n      >\n        <div\n          data-sidebar=\"sidebar\"\n          data-slot=\"sidebar-inner\"\n          className=\"bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm\"\n        >\n          {children}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction SidebarTrigger({\n  className,\n  onClick,\n  ...props\n}: React.ComponentProps<typeof Button>) {\n  const { toggleSidebar } = useSidebar();\n\n  return (\n    <Button\n      data-sidebar=\"trigger\"\n      data-slot=\"sidebar-trigger\"\n      variant=\"ghost\"\n      size=\"icon\"\n      className={cn(\"size-7\", className)}\n      onClick={(event) => {\n        onClick?.(event);\n        toggleSidebar();\n      }}\n      {...props}\n    >\n      <PanelLeftIcon />\n      <span className=\"sr-only\">Toggle Sidebar</span>\n    </Button>\n  );\n}\n\nfunction SidebarRail({ className, ...props }: React.ComponentProps<\"button\">) {\n  const { toggleSidebar } = useSidebar();\n\n  return (\n    <button\n      data-sidebar=\"rail\"\n      data-slot=\"sidebar-rail\"\n      aria-label=\"Toggle Sidebar\"\n      tabIndex={-1}\n      onClick={toggleSidebar}\n      title=\"Toggle Sidebar\"\n      className={cn(\n        \"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex\",\n        \"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize\",\n        \"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize\",\n        \"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full\",\n        \"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2\",\n        \"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarInset({ className, ...props }: React.ComponentProps<\"main\">) {\n  return (\n    <main\n      data-slot=\"sidebar-inset\"\n      className={cn(\n        \"bg-background relative flex w-full flex-1 flex-col\",\n        \"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarInput({\n  className,\n  ...props\n}: React.ComponentProps<typeof Input>) {\n  return (\n    <Input\n      data-slot=\"sidebar-input\"\n      data-sidebar=\"input\"\n      className={cn(\"bg-background h-8 w-full shadow-none\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sidebar-header\"\n      data-sidebar=\"header\"\n      className={cn(\"flex flex-col gap-2 p-2\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sidebar-footer\"\n      data-sidebar=\"footer\"\n      className={cn(\"flex flex-col gap-2 p-2\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof Separator>) {\n  return (\n    <Separator\n      data-slot=\"sidebar-separator\"\n      data-sidebar=\"separator\"\n      className={cn(\"bg-sidebar-border mx-2 w-auto\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sidebar-content\"\n      data-sidebar=\"content\"\n      className={cn(\n        \"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sidebar-group\"\n      data-sidebar=\"group\"\n      className={cn(\"relative flex w-full min-w-0 flex-col p-2\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarGroupLabel({\n  className,\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"div\"> & { asChild?: boolean }) {\n  const Comp = asChild ? Slot : \"div\";\n\n  return (\n    <Comp\n      data-slot=\"sidebar-group-label\"\n      data-sidebar=\"group-label\"\n      className={cn(\n        \"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0\",\n        \"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarGroupAction({\n  className,\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"button\"> & { asChild?: boolean }) {\n  const Comp = asChild ? Slot : \"button\";\n\n  return (\n    <Comp\n      data-slot=\"sidebar-group-action\"\n      data-sidebar=\"group-action\"\n      className={cn(\n        \"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0\",\n        // Increases the hit area of the button on mobile.\n        \"after:absolute after:-inset-2 md:after:hidden\",\n        \"group-data-[collapsible=icon]:hidden\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarGroupContent({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sidebar-group-content\"\n      data-sidebar=\"group-content\"\n      className={cn(\"w-full text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarMenu({ className, ...props }: React.ComponentProps<\"ul\">) {\n  return (\n    <ul\n      data-slot=\"sidebar-menu\"\n      data-sidebar=\"menu\"\n      className={cn(\"flex w-full min-w-0 flex-col gap-1\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarMenuItem({ className, ...props }: React.ComponentProps<\"li\">) {\n  return (\n    <li\n      data-slot=\"sidebar-menu-item\"\n      data-sidebar=\"menu-item\"\n      className={cn(\"group/menu-item relative\", className)}\n      {...props}\n    />\n  );\n}\n\nconst sidebarMenuButtonVariants = cva(\n  \"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0\",\n  {\n    variants: {\n      variant: {\n        default: \"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground\",\n        outline:\n          \"bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]\",\n      },\n      size: {\n        default: \"h-8 text-sm\",\n        sm: \"h-7 text-xs\",\n        lg: \"h-12 text-sm group-data-[collapsible=icon]:p-0!\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nfunction SidebarMenuButton({\n  asChild = false,\n  isActive = false,\n  variant = \"default\",\n  size = \"default\",\n  tooltip,\n  className,\n  ...props\n}: React.ComponentProps<\"button\"> & {\n  asChild?: boolean;\n  isActive?: boolean;\n  tooltip?: string | React.ComponentProps<typeof TooltipContent>;\n} & VariantProps<typeof sidebarMenuButtonVariants>) {\n  const Comp = asChild ? Slot : \"button\";\n  const { isMobile, state } = useSidebar();\n\n  const button = (\n    <Comp\n      data-slot=\"sidebar-menu-button\"\n      data-sidebar=\"menu-button\"\n      data-size={size}\n      data-active={isActive}\n      className={cn(sidebarMenuButtonVariants({ variant, size }), className)}\n      {...props}\n    />\n  );\n\n  if (!tooltip) {\n    return button;\n  }\n\n  if (typeof tooltip === \"string\") {\n    tooltip = {\n      children: tooltip,\n    };\n  }\n\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>{button}</TooltipTrigger>\n      <TooltipContent\n        side=\"right\"\n        align=\"center\"\n        hidden={state !== \"collapsed\" || isMobile}\n        {...tooltip}\n      />\n    </Tooltip>\n  );\n}\n\nfunction SidebarMenuAction({\n  className,\n  asChild = false,\n  showOnHover = false,\n  ...props\n}: React.ComponentProps<\"button\"> & {\n  asChild?: boolean;\n  showOnHover?: boolean;\n}) {\n  const Comp = asChild ? Slot : \"button\";\n\n  return (\n    <Comp\n      data-slot=\"sidebar-menu-action\"\n      data-sidebar=\"menu-action\"\n      className={cn(\n        \"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0\",\n        // Increases the hit area of the button on mobile.\n        \"after:absolute after:-inset-2 md:after:hidden\",\n        \"peer-data-[size=sm]/menu-button:top-1\",\n        \"peer-data-[size=default]/menu-button:top-1.5\",\n        \"peer-data-[size=lg]/menu-button:top-2.5\",\n        \"group-data-[collapsible=icon]:hidden\",\n        showOnHover &&\n          \"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarMenuBadge({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sidebar-menu-badge\"\n      data-sidebar=\"menu-badge\"\n      className={cn(\n        \"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none\",\n        \"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground\",\n        \"peer-data-[size=sm]/menu-button:top-1\",\n        \"peer-data-[size=default]/menu-button:top-1.5\",\n        \"peer-data-[size=lg]/menu-button:top-2.5\",\n        \"group-data-[collapsible=icon]:hidden\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarMenuSkeleton({\n  className,\n  showIcon = false,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  showIcon?: boolean;\n}) {\n  // Fixed width between 50 to 90% to avoid impure function warning\n  const width = \"75%\";\n\n  return (\n    <div\n      data-slot=\"sidebar-menu-skeleton\"\n      data-sidebar=\"menu-skeleton\"\n      className={cn(\"flex h-8 items-center gap-2 rounded-md px-2\", className)}\n      {...props}\n    >\n      {showIcon && (\n        <Skeleton\n          className=\"size-4 rounded-md\"\n          data-sidebar=\"menu-skeleton-icon\"\n        />\n      )}\n      <Skeleton\n        className=\"h-4 max-w-(--skeleton-width) flex-1\"\n        data-sidebar=\"menu-skeleton-text\"\n        style={\n          {\n            \"--skeleton-width\": width,\n          } as React.CSSProperties\n        }\n      />\n    </div>\n  );\n}\n\nfunction SidebarMenuSub({ className, ...props }: React.ComponentProps<\"ul\">) {\n  return (\n    <ul\n      data-slot=\"sidebar-menu-sub\"\n      data-sidebar=\"menu-sub\"\n      className={cn(\n        \"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5\",\n        \"group-data-[collapsible=icon]:hidden\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarMenuSubItem({\n  className,\n  ...props\n}: React.ComponentProps<\"li\">) {\n  return (\n    <li\n      data-slot=\"sidebar-menu-sub-item\"\n      data-sidebar=\"menu-sub-item\"\n      className={cn(\"group/menu-sub-item relative\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SidebarMenuSubButton({\n  asChild = false,\n  size = \"md\",\n  isActive = false,\n  className,\n  ...props\n}: React.ComponentProps<\"a\"> & {\n  asChild?: boolean;\n  size?: \"sm\" | \"md\";\n  isActive?: boolean;\n}) {\n  const Comp = asChild ? Slot : \"a\";\n\n  return (\n    <Comp\n      data-slot=\"sidebar-menu-sub-button\"\n      data-sidebar=\"menu-sub-button\"\n      data-size={size}\n      data-active={isActive}\n      className={cn(\n        \"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0\",\n        \"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground\",\n        size === \"sm\" && \"text-xs\",\n        size === \"md\" && \"text-sm\",\n        \"group-data-[collapsible=icon]:hidden\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Sidebar,\n  SidebarContent,\n  SidebarFooter,\n  SidebarGroup,\n  SidebarGroupAction,\n  SidebarGroupContent,\n  SidebarGroupLabel,\n  SidebarHeader,\n  SidebarInput,\n  SidebarInset,\n  SidebarMenu,\n  SidebarMenuAction,\n  SidebarMenuBadge,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  SidebarMenuSkeleton,\n  SidebarMenuSub,\n  SidebarMenuSubButton,\n  SidebarMenuSubItem,\n  SidebarProvider,\n  SidebarRail,\n  SidebarSeparator,\n  SidebarTrigger,\n  useSidebar,\n};\n"
  },
  {
    "path": "apps/www/components/ui/skeleton.tsx",
    "content": "import { cn } from \"@/lib/ui/cn\";\n\nfunction Skeleton({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"skeleton\"\n      className={cn(\"bg-accent animate-pulse rounded-md\", className)}\n      {...props}\n    />\n  );\n}\n\nexport { Skeleton };\n"
  },
  {
    "path": "apps/www/components/ui/slider.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SliderPrimitive from \"@radix-ui/react-slider\";\n\nimport { cn } from \"@/lib/ui/cn\";\n\nfunction Slider({\n  className,\n  defaultValue,\n  value,\n  min = 0,\n  max = 100,\n  ...props\n}: React.ComponentProps<typeof SliderPrimitive.Root>) {\n  const _values = React.useMemo(\n    () =>\n      Array.isArray(value)\n        ? value\n        : Array.isArray(defaultValue)\n          ? defaultValue\n          : [min, max],\n    [value, defaultValue, min, max],\n  );\n\n  return (\n    <SliderPrimitive.Root\n      data-slot=\"slider\"\n      defaultValue={defaultValue}\n      value={value}\n      min={min}\n      max={max}\n      className={cn(\n        \"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col\",\n        className,\n      )}\n      {...props}\n    >\n      <SliderPrimitive.Track\n        data-slot=\"slider-track\"\n        className={cn(\n          \"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5\",\n        )}\n      >\n        <SliderPrimitive.Range\n          data-slot=\"slider-range\"\n          className={cn(\n            \"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full\",\n          )}\n        />\n      </SliderPrimitive.Track>\n      {Array.from({ length: _values.length }, (_, index) => (\n        <SliderPrimitive.Thumb\n          data-slot=\"slider-thumb\"\n          key={index}\n          className=\"border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50\"\n        />\n      ))}\n    </SliderPrimitive.Root>\n  );\n}\n\nexport { Slider };\n"
  },
  {
    "path": "apps/www/components/ui/switch.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SwitchPrimitive from \"@radix-ui/react-switch\";\n\nimport { cn } from \"@/lib/ui/cn\";\n\nfunction Switch({\n  className,\n  ...props\n}: React.ComponentProps<typeof SwitchPrimitive.Root>) {\n  return (\n    <SwitchPrimitive.Root\n      data-slot=\"switch\"\n      className={cn(\n        \"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    >\n      <SwitchPrimitive.Thumb\n        data-slot=\"switch-thumb\"\n        className={cn(\n          \"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0\",\n        )}\n      />\n    </SwitchPrimitive.Root>\n  );\n}\n\nexport { Switch };\n"
  },
  {
    "path": "apps/www/components/ui/table.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/ui/cn\";\n\nfunction Table({ className, ...props }: React.ComponentProps<\"table\">) {\n  return (\n    <div\n      data-slot=\"table-container\"\n      className=\"relative w-full overflow-x-auto\"\n    >\n      <table\n        data-slot=\"table\"\n        className={cn(\"w-full caption-bottom text-sm\", className)}\n        {...props}\n      />\n    </div>\n  );\n}\n\nfunction TableHeader({ className, ...props }: React.ComponentProps<\"thead\">) {\n  return (\n    <thead\n      data-slot=\"table-header\"\n      className={cn(\"[&_tr]:border-b\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction TableBody({ className, ...props }: React.ComponentProps<\"tbody\">) {\n  return (\n    <tbody\n      data-slot=\"table-body\"\n      className={cn(\"[&_tr:last-child]:border-0\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction TableRow({ className, ...props }: React.ComponentProps<\"tr\">) {\n  return (\n    <tr\n      data-slot=\"table-row\"\n      className={cn(\n        \"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableHead({ className, ...props }: React.ComponentProps<\"th\">) {\n  return (\n    <th\n      data-slot=\"table-head\"\n      className={cn(\n        \"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableCell({ className, ...props }: React.ComponentProps<\"td\">) {\n  return (\n    <td\n      data-slot=\"table-cell\"\n      className={cn(\n        \"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Table, TableHeader, TableBody, TableHead, TableRow, TableCell };\n"
  },
  {
    "path": "apps/www/components/ui/tabs.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\n\nimport { cn } from \"@/lib/ui/cn\";\n\nfunction Tabs({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Root>) {\n  return (\n    <TabsPrimitive.Root\n      data-slot=\"tabs\"\n      className={cn(\"flex flex-col gap-2\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction TabsList({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.List>) {\n  return (\n    <TabsPrimitive.List\n      data-slot=\"tabs-list\"\n      className={cn(\n        \"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TabsTrigger({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {\n  return (\n    <TabsPrimitive.Trigger\n      data-slot=\"tabs-trigger\"\n      className={cn(\n        \"data-[state=active]:bg-background data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-muted-foreground hover:text-primary inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TabsContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Content>) {\n  return (\n    <TabsPrimitive.Content\n      data-slot=\"tabs-content\"\n      className={cn(\"flex-1 outline-none\", className)}\n      {...props}\n    />\n  );\n}\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent };\n"
  },
  {
    "path": "apps/www/components/ui/textarea.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/ui/cn\";\n\nfunction Textarea({ className, ...props }: React.ComponentProps<\"textarea\">) {\n  return (\n    <textarea\n      data-slot=\"textarea\"\n      className={cn(\n        \"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Textarea };\n"
  },
  {
    "path": "apps/www/components/ui/toggle-group.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as ToggleGroupPrimitive from \"@radix-ui/react-toggle-group\";\nimport { type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/ui/cn\";\nimport { toggleVariants } from \"@/components/ui/toggle\";\n\nconst ToggleGroupContext = React.createContext<\n  VariantProps<typeof toggleVariants> & {\n    spacing?: number;\n  }\n>({\n  size: \"default\",\n  variant: \"default\",\n  spacing: 0,\n});\n\nfunction ToggleGroup({\n  className,\n  variant,\n  size,\n  spacing = 0,\n  children,\n  ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &\n  VariantProps<typeof toggleVariants> & {\n    spacing?: number;\n  }) {\n  return (\n    <ToggleGroupPrimitive.Root\n      data-slot=\"toggle-group\"\n      data-variant={variant}\n      data-size={size}\n      data-spacing={spacing}\n      style={{ \"--gap\": spacing } as React.CSSProperties}\n      className={cn(\n        \"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs\",\n        className,\n      )}\n      {...props}\n    >\n      <ToggleGroupContext.Provider value={{ variant, size, spacing }}>\n        {children}\n      </ToggleGroupContext.Provider>\n    </ToggleGroupPrimitive.Root>\n  );\n}\n\nfunction ToggleGroupItem({\n  className,\n  children,\n  variant,\n  size,\n  ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &\n  VariantProps<typeof toggleVariants>) {\n  const context = React.useContext(ToggleGroupContext);\n\n  return (\n    <ToggleGroupPrimitive.Item\n      data-slot=\"toggle-group-item\"\n      data-variant={context.variant || variant}\n      data-size={context.size || size}\n      data-spacing={context.spacing}\n      className={cn(\n        toggleVariants({\n          variant: context.variant || variant,\n          size: context.size || size,\n        }),\n        \"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10\",\n        \"data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </ToggleGroupPrimitive.Item>\n  );\n}\n\nexport { ToggleGroup, ToggleGroupItem };\n"
  },
  {
    "path": "apps/www/components/ui/toggle.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TogglePrimitive from \"@radix-ui/react-toggle\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/ui/cn\";\n\nconst toggleVariants = cva(\n  \"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-transparent\",\n        outline:\n          \"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground\",\n      },\n      size: {\n        default: \"h-9 px-2 min-w-9\",\n        sm: \"h-8 px-1.5 min-w-8\",\n        lg: \"h-10 px-2.5 min-w-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nfunction Toggle({\n  className,\n  variant,\n  size,\n  ...props\n}: React.ComponentProps<typeof TogglePrimitive.Root> &\n  VariantProps<typeof toggleVariants>) {\n  return (\n    <TogglePrimitive.Root\n      data-slot=\"toggle\"\n      className={cn(toggleVariants({ variant, size, className }))}\n      {...props}\n    />\n  );\n}\n\nexport { Toggle, toggleVariants };\n"
  },
  {
    "path": "apps/www/components/ui/tooltip.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\n\nimport { cn } from \"@/lib/ui/cn\";\n\nfunction TooltipProvider({\n  delayDuration = 0,\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {\n  return (\n    <TooltipPrimitive.Provider\n      data-slot=\"tooltip-provider\"\n      delayDuration={delayDuration}\n      {...props}\n    />\n  );\n}\n\nfunction Tooltip({\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Root>) {\n  return (\n    <TooltipProvider>\n      <TooltipPrimitive.Root data-slot=\"tooltip\" {...props} />\n    </TooltipProvider>\n  );\n}\n\nfunction TooltipTrigger({\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {\n  return <TooltipPrimitive.Trigger data-slot=\"tooltip-trigger\" {...props} />;\n}\n\nfunction TooltipContent({\n  className,\n  sideOffset = 0,\n  children,\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Content>) {\n  return (\n    <TooltipPrimitive.Portal>\n      <TooltipPrimitive.Content\n        data-slot=\"tooltip-content\"\n        sideOffset={sideOffset}\n        className={cn(\n          \"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        <TooltipPrimitive.Arrow className=\"bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]\" />\n      </TooltipPrimitive.Content>\n    </TooltipPrimitive.Portal>\n  );\n}\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };\n"
  },
  {
    "path": "apps/www/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"\",\n    \"css\": \"app/styles/globals.css\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"iconLibrary\": \"lucide\",\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  },\n  \"registries\": {\n    \"@assistant-ui\": \"https://r.assistant-ui.com/{name}.json\",\n    \"@tool-ui\": \"https://tool-ui.com/r/{name}.json\"\n  }\n}\n"
  },
  {
    "path": "apps/www/css.d.ts",
    "content": "// CSS module imports should always expose a class-name map, even when\n// typechecking outside Next.js's normal bundler pipeline.\ndeclare module \"*.module.css\" {\n  const classes: Readonly<Record<string, string>>;\n  export default classes;\n}\n\n// Plain CSS files are imported for side-effects (e.g. globals, Leaflet CSS).\ndeclare module \"*.css\" {}\n"
  },
  {
    "path": "apps/www/docs/changelog.md",
    "content": "# Changelog System\n\nAutomated changelog for `app/docs/changelog/content.mdx`. This is a copy/paste component library (shadcn model), not an npm package — there is no semver contract.\n\n## Commands\n\n```bash\npnpm changelog:generate   # Generate entries from git history via agent inference\npnpm changelog:check      # Validate structure (runs in CI via verify:ci)\n```\n\n## Entry Structure\n\nThe changelog is a single MDX file. Each release is a `## YYYY-MM-DD` section with subsections in this order:\n\n1. `### Breaking changes` (optional) — bullet list\n2. `### Changes` (required) — bullet list\n3. `### Migration prompt` (optional) — must contain a markdown code fence\n\nNo prose between the `##` heading and first `###` subsection. MDX comments are allowed (see Marker below).\n\n### Marker comment\n\nThe top of the file may include an MDX comment tracking the last generated commit ref:\n\n```mdx\n{/* changelog-generated-to: <short-sha> */}\n```\n\nUse MDX comment syntax `{/* */}`, not HTML `<!-- -->`.\n\n## Breaking Changes vs Component Updates\n\n- **Breaking change** = a cross-cutting change affecting ALL components at once (e.g., enforcing `/schema` entrypoints repo-wide, migrating the action model across all components)\n- **Component update** = individual component evolution — existing copies still work, users upgrade via `npx shadcn@latest add`. NOT a breaking change.\n\n## Migration Prompt\n\nA prompt users copy-paste into their **coding agent** (e.g., Claude Code). Can exist with or without breaking changes.\n\n### Voice and structure\n\n- Write in imperative, agent-directed voice\n- Structure: opening directive → numbered Goals → bulleted Steps → verification\n- Reference the `2026-02-12` entry for detailed style, `2026-02-17` for upgrade-only style\n\n### Required content\n\n- Include `npx shadcn@latest add @tool-ui/{name}` commands\n- End steps with: lint, typecheck, tests; fix breakages\n- End with: validate UI rendering\n\n### Formatting\n\n- Wrap the entire prompt in a ` ```text ` code fence\n- The validator (`lib/changelog/changelog.ts`) rejects migration prompts without a code fence\n\n## Writing Style\n\n- New components: `New component: [Name](/docs/name) — short description.`\n- Component names in inline code when mentioned in prose: `` `Code Block` ``\n- Use markdown links to doc routes when introducing a component: `[Code Block](/docs/code-block)`\n- Group related changes; lead with the most significant\n- Use \"shared theme tokens\" (never \"pierre theme tokens\")\n- Only user-facing changes — no internal fixes (terminal wrapping, docs preview clipping, gallery exports, registry closure fixes)\n\n## Validation Rules\n\n`lib/changelog/changelog.ts` exports `validateChangelogStructure`. Rules enforced:\n\n- Each `##` section must have a valid `YYYY-MM-DD` heading\n- `### Changes` is required in every release\n- `### Breaking changes` must appear before `### Changes`\n- `### Migration prompt` must appear after `### Changes`\n- No duplicate `### Migration prompt` headings\n- Migration prompt body must contain a markdown code fence\n- No unsupported `###` subsection headings\n- No prose between `##` heading and first `###` subsection (MDX comments OK)\n\n## Architecture\n\nThe `lib/changelog/` directory contains three modules:\n\n- **`changelog.ts`** — Validation (`validateChangelogStructure`) and rendering (inserts new sections into the MDX file)\n- **`git.ts`** — Commit range resolution (last release tag to HEAD)\n- **`inference.ts`** — LLM inference (gathers commit evidence, asks a coding agent for structured release notes)\n\n### Maintainer flow\n\n1. `release-please` opens/updates release PR\n2. Run `pnpm changelog:generate`\n3. Review/edit generated section in `app/docs/changelog/content.mdx`\n4. CI runs `pnpm changelog:check`\n5. Merge release PR\n"
  },
  {
    "path": "apps/www/docs/tests.md",
    "content": "# Test guide\n\nTool UI tests use Vitest with node environment and strict console guards.\n\n## Run tests\n\n```bash\npnpm test\npnpm test:watch\n```\n\n## Test locations\n\n- Tool UI contracts: `lib/tests/tool-ui/**`\n- Registry artifacts: `lib/tests/registry/**`\n- Playground logic: `lib/playground/**/*.test.ts`\n\n## Console policy\n\n`lib/tests/setup/console-guard.ts` fails tests on unexpected `console.warn` / `console.error`.\nIf a test expects a warning, add an explicit allow pattern there.\n\n## What to test for new components\n\n1. Schema parse/safeParse contracts\n2. State transitions and action semantics\n3. Accessibility ids/roles for interactive surfaces\n4. Registry artifact inclusion after `pnpm registry:build`\n"
  },
  {
    "path": "apps/www/eslint.config.ts",
    "content": "import { defineConfig, globalIgnores } from \"eslint/config\";\nimport tsPlugin from \"@typescript-eslint/eslint-plugin\";\nimport oxlint from \"eslint-plugin-oxlint\";\nimport reactHooks from \"eslint-plugin-react-hooks\";\nimport tsParser from \"@typescript-eslint/parser\";\nimport { toolUiActionModelPlugin } from \"./lib/eslint/tool-ui-action-model-plugin\";\n\n// ESLint is retained only for rules Oxlint cannot handle:\n//   1. no-restricted-syntax (custom AST selectors)\n//   2. no-restricted-imports (complex overrides with ignores)\n//   3. Custom tool-ui/* plugin rules (JS AST rules)\n//   4. React Compiler hook rules (eslint-plugin-react-hooks)\n// All standard lint rules are handled by Oxlint (see .oxlintrc.json).\n\nconst eslintConfig = defineConfig([\n  // Inline disable comments for rules now handled by Oxlint\n  // appear unused to ESLint — suppress those warnings.\n  {\n    linterOptions: {\n      reportUnusedDisableDirectives: \"off\",\n    },\n  },\n\n  globalIgnores([\n    \"**/dist/**\",\n    \"**/node_modules/**\",\n    \"**/.next/**\",\n    \"**/out/**\",\n    \"**/next-env.d.ts\",\n    \"components/tool-ui/weather-widget/generated/**\",\n  ]),\n\n  // TypeScript parser + plugin (plugin registered for disable comment\n  // recognition — all TS rules are enforced by Oxlint, not ESLint)\n  {\n    files: [\"**/*.ts\", \"**/*.tsx\"],\n    languageOptions: {\n      parser: tsParser,\n    },\n    plugins: {\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any -- plugin type mismatch\n      \"@typescript-eslint\": tsPlugin as any,\n    },\n  },\n\n  // React hooks / React Compiler rules\n  {\n    files: [\"**/*.ts\", \"**/*.tsx\"],\n    plugins: {\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any -- configs shape mismatch\n      \"react-hooks\": reactHooks as any,\n    },\n    rules: {\n      \"react-hooks/rules-of-hooks\": \"error\",\n      // Disable strict React Compiler rules that don't fit this codebase\n      \"react-hooks/refs\": \"off\",\n      \"react-hooks/immutability\": \"off\",\n      \"react-hooks/set-state-in-effect\": \"off\",\n      \"react-hooks/static-components\": \"off\",\n    },\n  },\n\n  // Oxlint can't handle custom AST selectors\n  {\n    files: [\"components/tool-ui/**/*.ts\", \"components/tool-ui/**/*.tsx\"],\n    ignores: [\"components/tool-ui/shared/media/safe-navigation.ts\"],\n    rules: {\n      \"no-restricted-syntax\": [\n        \"error\",\n        {\n          selector:\n            \"CallExpression[callee.object.name='window'][callee.property.name='open']\",\n          message:\n            \"Use openSafeNavigationHref from components/tool-ui/shared/media/safe-navigation instead of window.open.\",\n        },\n      ],\n    },\n  },\n\n  // Oxlint can't handle no-restricted-imports with complex overrides\n  {\n    files: [\"components/tool-ui/**/*.ts\", \"components/tool-ui/**/*.tsx\"],\n    ignores: [\n      \"components/tool-ui/shared/**/*.ts\",\n      \"components/tool-ui/shared/**/*.tsx\",\n    ],\n    rules: {\n      \"no-restricted-imports\": [\n        \"error\",\n        {\n          paths: [\n            {\n              name: \"../shared\",\n              message:\n                \"Import from direct shared modules (for example '../shared/schema' or '../shared/contract') instead of the shared barrel.\",\n            },\n          ],\n        },\n      ],\n    },\n  },\n  {\n    files: [\"components/tool-ui/**/*.ts\", \"components/tool-ui/**/*.tsx\"],\n    ignores: [\n      \"components/tool-ui/**/_adapter.tsx\",\n      \"components/tool-ui/shared/**/*.ts\",\n      \"components/tool-ui/shared/**/*.tsx\",\n    ],\n    rules: {\n      \"no-restricted-imports\": [\n        \"error\",\n        {\n          patterns: [\n            {\n              group: [\"@/components/ui/*\", \"@/lib/ui/cn\"],\n              message:\n                \"Import UI primitives and cn from './_adapter' to keep tool-ui components portable.\",\n            },\n          ],\n        },\n      ],\n    },\n  },\n\n  // Custom tool-ui plugin rules (JS AST rules, can't run in Oxlint)\n  {\n    files: [\n      \"components/tool-ui/audio/**/*.{ts,tsx}\",\n      \"components/tool-ui/citation/**/*.{ts,tsx}\",\n      \"components/tool-ui/code-block/**/*.{ts,tsx}\",\n      \"components/tool-ui/data-table/**/*.{ts,tsx}\",\n      \"components/tool-ui/image/**/*.{ts,tsx}\",\n      \"components/tool-ui/instagram-post/**/*.{ts,tsx}\",\n      \"components/tool-ui/link-preview/**/*.{ts,tsx}\",\n      \"components/tool-ui/linkedin-post/**/*.{ts,tsx}\",\n      \"components/tool-ui/order-summary/**/*.{ts,tsx}\",\n      \"components/tool-ui/plan/**/*.{ts,tsx}\",\n      \"components/tool-ui/terminal/**/*.{ts,tsx}\",\n      \"components/tool-ui/video/**/*.{ts,tsx}\",\n      \"components/tool-ui/x-post/**/*.{ts,tsx}\",\n    ],\n    plugins: {\n      \"tool-ui\": toolUiActionModelPlugin,\n    },\n    rules: {\n      \"tool-ui/no-embedded-response-actions\": \"error\",\n    },\n  },\n  {\n    files: [\n      \"app/**/*.{ts,tsx}\",\n      \"components/**/*.{ts,tsx}\",\n      \"lib/**/*.{ts,tsx}\",\n    ],\n    plugins: {\n      \"tool-ui\": toolUiActionModelPlugin,\n    },\n    rules: {\n      \"tool-ui/no-add-result-in-local-actions\": \"error\",\n      \"tool-ui/decision-actions-require-envelope\": \"error\",\n    },\n  },\n\n  // Disable core ESLint rules that Oxlint already handles.\n  // Only include configs for plugins still installed in ESLint.\n  ...oxlint.configs[\"flat/eslint\"],\n  ...oxlint.configs[\"flat/react-hooks\"],\n]);\n\nexport default eslintConfig;\n"
  },
  {
    "path": "apps/www/hooks/use-extract-headings.ts",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { usePathname } from \"next/navigation\";\n\nexport type Heading = {\n  id: string;\n  text: string;\n};\n\nexport function useExtractHeadings(container: HTMLElement | null): Heading[] {\n  const [headings, setHeadings] = useState<Heading[]>([]);\n  const pathname = usePathname();\n\n  useEffect(() => {\n    if (!container) return;\n\n    const timer = setTimeout(() => {\n      const h2ElementsWithId = container.querySelectorAll(\"h2[id]\");\n      const extracted = Array.from(h2ElementsWithId).map((el) => ({\n        id: el.id,\n        text: el.textContent || \"\",\n      }));\n      setHeadings(extracted);\n    }, 100);\n\n    return () => clearTimeout(timer);\n  }, [container, pathname]);\n\n  return headings;\n}\n"
  },
  {
    "path": "apps/www/hooks/use-headings-observer.ts",
    "content": "\"use client\";\n\nimport { useEffect, useState, useCallback } from \"react\";\nimport type { Heading } from \"./use-extract-headings\";\n\nexport function useHeadingsObserver(\n  headings: Heading[],\n  container: HTMLElement | null,\n): string | null {\n  const [activeId, setActiveId] = useState<string | null>(null);\n\n  const calculateActiveHeading = useCallback(() => {\n    if (!container || headings.length === 0) return;\n\n    const stickyHeader = container.querySelector('[role=\"tablist\"]');\n    const offset = stickyHeader\n      ? stickyHeader.getBoundingClientRect().height + 40\n      : 100;\n\n    const scrollTop = container.scrollTop + offset;\n\n    let active = headings[0];\n    for (const heading of headings) {\n      const el = document.getElementById(heading.id);\n      if (el && el.offsetTop <= scrollTop) {\n        active = heading;\n      } else {\n        break;\n      }\n    }\n\n    const scrollHeight = container.scrollHeight;\n    const clientHeight = container.clientHeight;\n    const maxScroll = scrollHeight - clientHeight;\n    const isAtMaxScroll =\n      maxScroll > 0 && container.scrollTop >= maxScroll - 10;\n\n    if (isAtMaxScroll) {\n      const lastHeading = headings[headings.length - 1];\n      const lastHeadingEl = document.getElementById(lastHeading.id);\n      if (lastHeadingEl) {\n        const lastHeadingTop = lastHeadingEl.getBoundingClientRect().top;\n        const containerTop = container.getBoundingClientRect().top;\n        const relativeTop = lastHeadingTop - containerTop;\n\n        if (relativeTop < clientHeight * 0.6) {\n          setActiveId(lastHeading.id);\n          return;\n        }\n      }\n    }\n\n    setActiveId(active.id);\n  }, [container, headings]);\n\n  useEffect(() => {\n    if (!container || headings.length === 0) return;\n\n    const timeoutId = setTimeout(calculateActiveHeading, 50);\n\n    return () => clearTimeout(timeoutId);\n  }, [container, headings, calculateActiveHeading]);\n\n  useEffect(() => {\n    if (!container) return;\n\n    let frameId: number | null = null;\n\n    const handleScroll = () => {\n      if (frameId !== null) {\n        return;\n      }\n\n      frameId = window.requestAnimationFrame(() => {\n        frameId = null;\n        calculateActiveHeading();\n      });\n    };\n\n    container.addEventListener(\"scroll\", handleScroll, { passive: true });\n\n    return () => {\n      container.removeEventListener(\"scroll\", handleScroll);\n      if (frameId !== null) {\n        window.cancelAnimationFrame(frameId);\n      }\n    };\n  }, [container, calculateActiveHeading]);\n\n  return activeId;\n}\n"
  },
  {
    "path": "apps/www/hooks/use-mobile.ts",
    "content": "import * as React from \"react\";\n\nconst MOBILE_BREAKPOINT = 768;\n\nexport function useIsMobile() {\n  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(\n    undefined,\n  );\n\n  React.useEffect(() => {\n    const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);\n    const onChange = () => {\n      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\n    };\n    mql.addEventListener(\"change\", onChange);\n    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\n    return () => mql.removeEventListener(\"change\", onChange);\n  }, []);\n\n  return !!isMobile;\n}\n"
  },
  {
    "path": "apps/www/hooks/use-preset-param.ts",
    "content": "\"use client\";\n\nimport { useCallback, useMemo } from \"react\";\nimport { useRouter, usePathname, useSearchParams } from \"next/navigation\";\n\ninterface UsePresetParamOptions<T extends string> {\n  presets: Record<T, unknown>;\n  defaultPreset: T;\n  paramName?: string;\n}\n\ninterface UsePresetParamReturn<T extends string> {\n  currentPreset: T;\n  setPreset: (preset: T) => void;\n}\n\nexport function usePresetParam<T extends string>({\n  presets,\n  defaultPreset,\n  paramName = \"preset\",\n}: UsePresetParamOptions<T>): UsePresetParamReturn<T> {\n  const router = useRouter();\n  const pathname = usePathname();\n  const searchParams = useSearchParams();\n\n  const presetKeys = useMemo(() => new Set(Object.keys(presets)), [presets]);\n\n  const currentPreset = useMemo(() => {\n    const paramValue = searchParams.get(paramName);\n    if (paramValue !== null && presetKeys.has(paramValue)) {\n      return paramValue as T;\n    }\n    return defaultPreset;\n  }, [searchParams, paramName, presetKeys, defaultPreset]);\n\n  const setPreset = useCallback(\n    (preset: T) => {\n      const currentParamValue = searchParams.get(paramName);\n      if (currentParamValue === preset) return;\n\n      const params = new URLSearchParams(searchParams.toString());\n      params.set(paramName, preset);\n      router.push(`${pathname}?${params.toString()}`, { scroll: false });\n    },\n    [router, pathname, searchParams, paramName],\n  );\n\n  return { currentPreset, setPreset };\n}\n"
  },
  {
    "path": "apps/www/hooks/use-reduced-motion.ts",
    "content": "\"use client\";\n\nimport { useState, useEffect } from \"react\";\n\nexport function useReducedMotion(): boolean {\n  const [reducedMotion, setReducedMotion] = useState(false);\n\n  useEffect(() => {\n    if (typeof window === \"undefined\") return;\n\n    const mediaQuery = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    setReducedMotion(mediaQuery.matches);\n\n    const handleChange = () => setReducedMotion(mediaQuery.matches);\n    mediaQuery.addEventListener?.(\"change\", handleChange);\n    return () => mediaQuery.removeEventListener?.(\"change\", handleChange);\n  }, []);\n\n  return reducedMotion;\n}\n"
  },
  {
    "path": "apps/www/hooks/use-responsive-preview.ts",
    "content": "\"use client\";\n\nimport { useCallback, useRef } from \"react\";\nimport type { ImperativePanelGroupHandle } from \"react-resizable-panels\";\n\ninterface UseResponsivePreviewOptions {\n  minWidth: number;\n  maxWidth: number;\n  tolerance?: number;\n}\n\ninterface UseResponsivePreviewReturn {\n  panelGroupRef: React.RefObject<ImperativePanelGroupHandle | null>;\n  handleLayout: (sizes: number[]) => void;\n}\n\ntype PreviewLayoutSizes = [\n  leftGutter: number,\n  previewWidth: number,\n  rightGutter: number,\n];\n\nconst layoutMatchesTarget = (\n  actual: number[],\n  target: number[],\n  tolerance: number,\n): boolean => {\n  if (actual.length !== target.length) return false;\n  return actual.every((val, i) => Math.abs(val - target[i]) < tolerance);\n};\n\nexport function useResponsivePreview({\n  minWidth,\n  maxWidth,\n  tolerance = 0.5,\n}: UseResponsivePreviewOptions): UseResponsivePreviewReturn {\n  const panelGroupRef = useRef<ImperativePanelGroupHandle | null>(null);\n  const isSyncing = useRef(false);\n\n  const handleLayout = useCallback(\n    (sizes: number[]) => {\n      if (!panelGroupRef.current) return;\n\n      if (isSyncing.current) {\n        isSyncing.current = false;\n        return;\n      }\n\n      if (sizes.length !== 3) return;\n      const [, previewWidth] = sizes as PreviewLayoutSizes;\n\n      const clampedWidth = Math.min(maxWidth, Math.max(minWidth, previewWidth));\n      const gutter = Math.max(0, (100 - clampedWidth) / 2);\n      const targetLayout: PreviewLayoutSizes = [gutter, clampedWidth, gutter];\n\n      if (!layoutMatchesTarget(sizes, targetLayout, tolerance)) {\n        isSyncing.current = true;\n        panelGroupRef.current.setLayout(targetLayout);\n      }\n    },\n    [minWidth, maxWidth, tolerance],\n  );\n\n  return { panelGroupRef, handleLayout };\n}\n"
  },
  {
    "path": "apps/www/hooks/use-tab-search-param.ts",
    "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef, type RefObject } from \"react\";\nimport { useQueryState } from \"nuqs\";\n\ninterface UseTabSearchParamOptions<T extends string> {\n  paramName?: string;\n  defaultTab: T;\n  validTabs: readonly T[];\n  scrollTargetRef?: RefObject<HTMLElement | null>;\n  hashTrigger?: string;\n}\n\ninterface UseTabSearchParamReturn<T extends string> {\n  activeTab: T;\n  setActiveTab: (tab: T) => void;\n}\n\nexport function resolveTabFromSearchParam<T extends string>(\n  rawTab: string | null,\n  defaultTab: T,\n  validTabs: readonly T[],\n): T {\n  return rawTab !== null && validTabs.includes(rawTab as T)\n    ? (rawTab as T)\n    : defaultTab;\n}\n\nexport function useTabSearchParam<T extends string>({\n  paramName = \"tab\",\n  defaultTab,\n  validTabs,\n  scrollTargetRef,\n  hashTrigger,\n}: UseTabSearchParamOptions<T>): UseTabSearchParamReturn<T> {\n  const isInitialMount = useRef(true);\n\n  const [rawTab, setRawTab] = useQueryState(paramName);\n  const activeTab = resolveTabFromSearchParam(rawTab, defaultTab, validTabs);\n\n  // Handle hash trigger (e.g., #examples in URL)\n  useEffect(() => {\n    if (!hashTrigger || typeof window === \"undefined\") return;\n\n    const hash = window.location.hash;\n    if (hash === hashTrigger) {\n      const hashTab = hashTrigger.replace(\"#\", \"\") as T;\n      if (validTabs.includes(hashTab) && rawTab !== hashTab) {\n        setRawTab(hashTab);\n      }\n    }\n  }, [hashTrigger, rawTab, setRawTab, validTabs]);\n\n  // Handle scroll to target when switching to hash-triggered tab\n  useEffect(() => {\n    if (\n      scrollTargetRef?.current &&\n      hashTrigger &&\n      activeTab === hashTrigger.replace(\"#\", \"\") &&\n      !isInitialMount.current\n    ) {\n      scrollTargetRef.current.scrollIntoView({\n        behavior: \"smooth\",\n        block: \"start\",\n      });\n    }\n    isInitialMount.current = false;\n  }, [activeTab, hashTrigger, scrollTargetRef]);\n\n  const setActiveTab = useCallback(\n    (newTab: T) => {\n      if (newTab === activeTab) return;\n      setRawTab(newTab);\n    },\n    [activeTab, setRawTab],\n  );\n\n  return { activeTab, setActiveTab };\n}\n"
  },
  {
    "path": "apps/www/hooks/use-toc-keyboard-nav.ts",
    "content": "\"use client\";\n\nimport { useEffect, useState, useRef } from \"react\";\nimport type { Heading } from \"./use-extract-headings\";\n\nexport function useTocKeyboardNav(\n  headings: Heading[],\n  onNavigate: (id: string) => void,\n) {\n  const [focusIndex, setFocusIndex] = useState(-1);\n  const linkRefs = useRef<(HTMLAnchorElement | null)[]>([]);\n\n  useEffect(() => {\n    linkRefs.current = linkRefs.current.slice(0, headings.length);\n  }, [headings]);\n\n  useEffect(() => {\n    if (focusIndex >= 0 && focusIndex < linkRefs.current.length) {\n      linkRefs.current[focusIndex]?.focus();\n    }\n  }, [focusIndex]);\n\n  const handleKeyDown = (e: React.KeyboardEvent) => {\n    if (e.key === \"ArrowDown\") {\n      e.preventDefault();\n      setFocusIndex((prev) => (prev < headings.length - 1 ? prev + 1 : prev));\n    } else if (e.key === \"ArrowUp\") {\n      e.preventDefault();\n      setFocusIndex((prev) => (prev > 0 ? prev - 1 : prev));\n    } else if (e.key === \"Enter\" && focusIndex >= 0) {\n      e.preventDefault();\n      onNavigate(headings[focusIndex].id);\n    }\n  };\n\n  const setLinkRef = (index: number) => (el: HTMLAnchorElement | null) => {\n    linkRefs.current[index] = el;\n  };\n\n  return {\n    handleKeyDown,\n    setLinkRef,\n    focusIndex,\n  };\n}\n"
  },
  {
    "path": "apps/www/lib/analytics.ts",
    "content": "type AnalyticsProperties = Record<string, string | number | boolean>;\n\nconst trackEvent = (event: string, properties?: AnalyticsProperties) => {\n  if (typeof window === \"undefined\") {\n    return;\n  }\n\n  void loadPostHog().then((posthog) => {\n    posthog?.capture?.(event, properties);\n  });\n\n  void loadVercelTrack().then((track) => {\n    track?.(event, properties);\n  });\n};\n\nlet posthogLoader: Promise<\n  (typeof import(\"posthog-js\"))[\"default\"] | null\n> | null = null;\nlet vercelTrackLoader: Promise<\n  (typeof import(\"@vercel/analytics\"))[\"track\"] | null\n> | null = null;\n\nfunction loadPostHog() {\n  posthogLoader ??= import(\"posthog-js\")\n    .then((module) => module.default)\n    .catch(() => null);\n  return posthogLoader;\n}\n\nfunction loadVercelTrack() {\n  vercelTrackLoader ??= import(\"@vercel/analytics\")\n    .then((module) => module.track)\n    .catch(() => null);\n  return vercelTrackLoader;\n}\n\nexport const analytics = {\n  // ============================================================\n  // TIER 1: Core Value Metrics (Adoption signals)\n  // These measure if people are getting value from tool-ui\n  // ============================================================\n\n  component: {\n    /** PRIMARY CONVERSION: User copied component code */\n    codeCopied: (componentName: string, codeType: \"full\" | \"snippet\") =>\n      trackEvent(\"component_code_copied\", {\n        component: componentName,\n        code_type: codeType,\n      }),\n\n    /** Which preset configurations resonate */\n    presetSelected: (componentName: string, preset: string) =>\n      trackEvent(\"component_preset_selected\", {\n        component: componentName,\n        preset,\n      }),\n\n    /** Demand signal: which components get attention */\n    viewed: (componentName: string, source: \"gallery\" | \"direct\" | \"search\") =>\n      trackEvent(\"component_viewed\", {\n        component: componentName,\n        source,\n      }),\n\n    /** Deep engagement: interacting with live preview */\n    previewInteracted: (componentName: string, action: string) =>\n      trackEvent(\"component_preview_interacted\", {\n        component: componentName,\n        action,\n      }),\n\n    /** Tab navigation within component docs */\n    tabSwitched: (componentName: string, tab: string) =>\n      trackEvent(\"component_tab_switched\", {\n        component: componentName,\n        tab,\n      }),\n  },\n\n  // ============================================================\n  // TIER 2: Content & Discovery (Gap analysis)\n  // These help identify what's missing or needs improvement\n  // ============================================================\n\n  search: {\n    /** How people discover content */\n    opened: (source: \"header\" | \"keyboard\" | \"empty_state\") =>\n      trackEvent(\"search_opened\", { source }),\n\n    /** What people are looking for */\n    querySubmitted: (query: string, resultsCount: number) =>\n      trackEvent(\"search_query_submitted\", {\n        query,\n        results_count: resultsCount,\n      }),\n\n    /** CRITICAL: Content gaps - what doesn't exist yet */\n    noResults: (query: string) => trackEvent(\"search_no_results\", { query }),\n\n    /** Which search results convert to visits */\n    resultClicked: (query: string, url: string, position: number) =>\n      trackEvent(\"search_result_clicked\", { query, url, position }),\n  },\n\n  gallery: {\n    /** Gallery landing page view */\n    pageViewed: () => trackEvent(\"gallery_page_viewed\"),\n\n    /** Which preview cards are shown in the gallery */\n    componentPreviewed: (componentName: string) =>\n      trackEvent(\"gallery_component_previewed\", { component: componentName }),\n\n    /** Which components get clicks from gallery overview */\n    componentClicked: (componentName: string) =>\n      trackEvent(\"gallery_component_clicked\", { component: componentName }),\n\n    /** Category filtering patterns */\n    categoryFiltered: (category: string) =>\n      trackEvent(\"gallery_category_filtered\", { category }),\n  },\n\n  docs: {\n    /** Navigation patterns through docs */\n    navigationClicked: (pageName: string, pageUrl: string) =>\n      trackEvent(\"doc_navigation_clicked\", {\n        page_name: pageName,\n        page_url: pageUrl,\n      }),\n\n    /** TOC engagement */\n    tocLinkClicked: (headingTitle: string, headingDepth: number) =>\n      trackEvent(\"toc_link_clicked\", {\n        heading_title: headingTitle,\n        heading_depth: headingDepth,\n      }),\n\n    /** Installation command copy intent (high-conversion docs action) */\n    installSnippetCopied: (\n      snippetType: \"skills\" | \"registry\" | \"package_manager\" | \"tool_agent\",\n      location: \"docs_code_block\" | \"docs_header\",\n    ) =>\n      trackEvent(\"install_snippet_copied\", {\n        snippet_type: snippetType,\n        location,\n      }),\n  },\n\n  code: {\n    /** Generic code block copy (non-component code) */\n    blockCopied: (language: string, source: string) =>\n      trackEvent(\"code_block_copied\", { language, source }),\n  },\n\n  // ============================================================\n  // TIER 3: Acquisition & Conversion (Growth signals)\n  // These track how visitors become users\n  // ============================================================\n\n  cta: {\n    /** CTA engagement */\n    clicked: (cta: string, location: string) =>\n      trackEvent(\"cta_clicked\", { cta, location }),\n  },\n\n  /** External link clicks (GitHub, npm, etc.) */\n  external: {\n    linkClicked: (\n      destination: \"github\" | \"npm\" | \"docs\" | \"other\",\n      url: string,\n    ) => trackEvent(\"external_link_clicked\", { destination, url }),\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/changelog/changelog.ts",
    "content": "import fs from \"node:fs\";\nimport path from \"node:path\";\n\nexport type InferredReleaseNotes = {\n  breakingChanges: string[];\n  changes: string[];\n  migrationPrompt: string | null;\n};\n\ntype RenderReleaseSectionInput = {\n  date: string;\n  notes: InferredReleaseNotes;\n  generatedToRef?: string | null;\n};\n\ntype UpsertReleaseSectionInput = {\n  content: string;\n  date: string;\n  section: string;\n};\n\nexport type ChangelogValidationResult = {\n  ok: boolean;\n  errors: string[];\n};\n\ntype SectionBounds = {\n  heading: string;\n  start: number;\n  end: number;\n};\n\ntype SubsectionBounds = {\n  heading: string;\n  headingEnd: number;\n  start: number;\n  end: number;\n};\n\nfunction normalizeItemList(items: string[]): string[] {\n  return items\n    .map((item) => item.trim())\n    .filter(Boolean)\n    .map((item) => item.replace(/\\s+/g, \" \"));\n}\n\nfunction listToBullets(items: string[]): string {\n  return items.map((item) => `- ${item}`).join(\"\\n\");\n}\n\nfunction parseReleaseSectionBounds(content: string): SectionBounds[] {\n  const sections: SectionBounds[] = [];\n  const headingRegex = /^##\\s+([^\\n]+)$/gm;\n  const matches = Array.from(content.matchAll(headingRegex));\n\n  for (let index = 0; index < matches.length; index += 1) {\n    const match = matches[index];\n    const start = match.index ?? 0;\n    const end = matches[index + 1]?.index ?? content.length;\n    sections.push({\n      heading: match[1]?.trim() ?? \"\",\n      start,\n      end,\n    });\n  }\n\n  return sections;\n}\n\nfunction parseSubsectionBounds(sectionContent: string): SubsectionBounds[] {\n  const headingRegex = /^###\\s+([^\\n]+)$/gm;\n  const matches = Array.from(sectionContent.matchAll(headingRegex));\n\n  return matches.map((match, index) => {\n    const start = match.index ?? 0;\n    const headingText = match[0] ?? \"\";\n    const heading = match[1]?.trim() ?? \"\";\n    const headingEnd = start + headingText.length;\n    const end = matches[index + 1]?.index ?? sectionContent.length;\n    return {\n      heading,\n      headingEnd,\n      start,\n      end,\n    };\n  });\n}\n\nfunction hasMarkdownCodeFence(input: string): boolean {\n  const normalized = input.trim();\n  if (!normalized) return false;\n\n  return /(^|\\n)(`{3,}|~{3,})[^\\n]*\\n[\\s\\S]*?\\n\\2(?=\\n|$)/m.test(normalized);\n}\n\nfunction toMarkdownCodeFence(input: string, language: string): string {\n  const normalized = input.replace(/\\r\\n/g, \"\\n\").trimEnd();\n  const backtickRuns = Array.from(\n    normalized.matchAll(/`+/g),\n    (match) => match[0].length,\n  );\n  const maxBacktickRun =\n    backtickRuns.length > 0 ? Math.max(...backtickRuns) : 0;\n  const fence = \"`\".repeat(Math.max(3, maxBacktickRun + 1));\n  return `${fence}${language}\\n${normalized}\\n${fence}`;\n}\n\nfunction extractGeneratedToRef(content: string): string | null {\n  const markerMatch = content.match(\n    /(?:<!--\\s*changelog-generated-to:\\s*([^\\s>][^>]*)\\s*-->|\\{\\/\\*\\s*changelog-generated-to:\\s*(\\S+)\\s*\\*\\/\\})/,\n  );\n  const marker = (markerMatch?.[1] ?? markerMatch?.[2])?.trim();\n  return marker && marker.length > 0 ? marker : null;\n}\n\nfunction extractReleaseDateFromHeading(heading: string): string | null {\n  const normalizedHeading = heading.trim();\n  if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(normalizedHeading)) {\n    return null;\n  }\n\n  return normalizedHeading;\n}\n\nfunction introAreaContainsOnlyAllowedComments(introArea: string): boolean {\n  const stripped = introArea\n    .replace(/<!--[\\s\\S]*?-->/g, \"\")\n    .replace(/\\{\\/\\*[\\s\\S]*?\\*\\/\\}/g, \"\")\n    .trim();\n  return stripped.length === 0;\n}\n\nexport function createInitialChangelogContent(): string {\n  return `import { DocsHeader } from \"../_components/docs-header\";\n\n<DocsHeader\n  title=\"Changelog\"\n  mdxPath=\"app/docs/changelog/content.mdx\"\n/>\\n`;\n}\n\nexport function ensureChangelogFileExists(filePath: string): void {\n  if (fs.existsSync(filePath)) {\n    return;\n  }\n\n  fs.mkdirSync(path.dirname(filePath), { recursive: true });\n  fs.writeFileSync(filePath, createInitialChangelogContent(), \"utf8\");\n}\n\nexport function renderReleaseSection({\n  date,\n  notes,\n  generatedToRef,\n}: RenderReleaseSectionInput): string {\n  const breakingChanges = normalizeItemList(notes.breakingChanges);\n  const changes = normalizeItemList(notes.changes);\n  const migrationPrompt = notes.migrationPrompt?.trim() ?? null;\n  const normalizedGeneratedToRef = generatedToRef?.trim() ?? null;\n\n  if (changes.length === 0) {\n    throw new Error(\"Changelog rendering requires at least one change.\");\n  }\n\n  const lines: string[] = [`## ${date}`, \"\"];\n\n  if (normalizedGeneratedToRef) {\n    lines.push(\n      `{/* changelog-generated-to: ${normalizedGeneratedToRef} */}`,\n      \"\",\n    );\n  }\n\n  if (breakingChanges.length > 0) {\n    lines.push(\"### Breaking changes\", \"\", listToBullets(breakingChanges), \"\");\n  }\n\n  lines.push(\"### Changes\", \"\", listToBullets(changes), \"\");\n\n  if (migrationPrompt) {\n    lines.push(\n      \"### Migration prompt\",\n      \"\",\n      toMarkdownCodeFence(migrationPrompt, \"text\"),\n    );\n  }\n  return `${lines.join(\"\\n\").trimEnd()}\\n`;\n}\n\nexport function upsertReleaseSection({\n  content,\n  date,\n  section,\n}: UpsertReleaseSectionInput): string {\n  const nextSection = section.trim();\n  const sections = parseReleaseSectionBounds(content);\n  const heading = date.trim();\n\n  const existing = sections.find((candidate) => candidate.heading === heading);\n  if (existing) {\n    const before = content.slice(0, existing.start).trimEnd();\n    const after = content.slice(existing.end).replace(/^\\s+/, \"\");\n    return (\n      `${before}\\n\\n${nextSection}\\n${after ? `\\n${after}` : \"\"}`.trimEnd() +\n      \"\\n\"\n    );\n  }\n\n  const insertAt = sections[0]?.start ?? content.length;\n  const before = content.slice(0, insertAt).trimEnd();\n  const after = content.slice(insertAt).replace(/^\\s+/, \"\");\n  return (\n    `${before}\\n\\n${nextSection}\\n${after ? `\\n${after}` : \"\"}`.trimEnd() + \"\\n\"\n  );\n}\n\nexport function validateChangelogStructure(\n  content: string,\n): ChangelogValidationResult {\n  const errors: string[] = [];\n  const sections = parseReleaseSectionBounds(content);\n\n  if (sections.length === 0) {\n    errors.push(\n      \"Changelog must include at least one release section (## YYYY-MM-DD).\",\n    );\n    return { ok: false, errors };\n  }\n\n  for (const section of sections) {\n    const sectionContent = content.slice(section.start, section.end);\n    const subsectionBounds = parseSubsectionBounds(sectionContent);\n    const subsectionNames = subsectionBounds.map(\n      (subsection) => subsection.heading,\n    );\n    const migrationPromptCount = subsectionNames.filter(\n      (name) => name === \"Migration prompt\",\n    ).length;\n    const firstSubheadingMatch = sectionContent.match(/^###\\s+[^\\n]+$/m);\n\n    if (!firstSubheadingMatch) {\n      errors.push(\n        `Release \"${section.heading}\" must include subsection headings.`,\n      );\n      continue;\n    }\n\n    const headingLine = sectionContent.match(/^##\\s+[^\\n]+/)?.[0] ?? \"\";\n    const introArea = sectionContent\n      .slice(\n        sectionContent.indexOf(headingLine) + headingLine.length,\n        firstSubheadingMatch.index,\n      )\n      .trim();\n\n    if (\n      introArea.length > 0 &&\n      !introAreaContainsOnlyAllowedComments(introArea)\n    ) {\n      errors.push(\n        `Release \"${section.heading}\" contains prose before the first subsection heading.`,\n      );\n    }\n\n    const idxBreaking = subsectionNames.indexOf(\"Breaking changes\");\n    const idxMigration = subsectionNames.indexOf(\"Migration prompt\");\n    const idxChanges = subsectionNames.indexOf(\"Changes\");\n\n    if (idxChanges === -1) {\n      errors.push(`Release \"${section.heading}\" is missing \"### Changes\".`);\n    }\n\n    if (idxBreaking !== -1 && idxChanges !== -1 && idxBreaking > idxChanges) {\n      errors.push(\n        `Release \"${section.heading}\" must place \"### Breaking changes\" before \"### Changes\".`,\n      );\n    }\n\n    if (idxMigration !== -1 && idxChanges !== -1 && idxMigration < idxChanges) {\n      errors.push(\n        `Release \"${section.heading}\" must place \"### Migration prompt\" after \"### Changes\".`,\n      );\n    }\n\n    if (migrationPromptCount > 1) {\n      errors.push(\n        `Release \"${section.heading}\" contains duplicate \"### Migration prompt\" headings.`,\n      );\n    }\n\n    if (migrationPromptCount === 1) {\n      const migrationPromptSection = subsectionBounds.find(\n        (subsection) => subsection.heading === \"Migration prompt\",\n      );\n      const migrationPromptBody = migrationPromptSection\n        ? sectionContent.slice(\n            migrationPromptSection.headingEnd,\n            migrationPromptSection.end,\n          )\n        : \"\";\n\n      if (!hasMarkdownCodeFence(migrationPromptBody)) {\n        errors.push(\n          `Release \"${section.heading}\" has a migration prompt but is missing the required Fumadocs code-block markdown fence.`,\n        );\n      }\n    }\n\n    for (const subsectionName of subsectionNames) {\n      if (\n        subsectionName !== \"Breaking changes\" &&\n        subsectionName !== \"Migration prompt\" &&\n        subsectionName !== \"Changes\"\n      ) {\n        errors.push(\n          `Release \"${section.heading}\" contains unsupported subsection heading \"### ${subsectionName}\".`,\n        );\n      }\n    }\n  }\n\n  return { ok: errors.length === 0, errors };\n}\n\nexport function readLatestReleaseGeneratedToRef(\n  content: string,\n): string | null {\n  const sections = parseReleaseSectionBounds(content);\n  const latestSection = sections[0];\n  if (!latestSection) {\n    return null;\n  }\n\n  const latestSectionContent = content.slice(\n    latestSection.start,\n    latestSection.end,\n  );\n  const firstSubheadingMatch = latestSectionContent.match(/^###\\s+[^\\n]+$/m);\n  const markerSearchContent = firstSubheadingMatch\n    ? latestSectionContent.slice(0, firstSubheadingMatch.index)\n    : latestSectionContent;\n  return extractGeneratedToRef(markerSearchContent);\n}\n\nexport function readLatestReleaseDate(content: string): string | null {\n  const sections = parseReleaseSectionBounds(content);\n  const latestSection = sections[0];\n  if (!latestSection) {\n    return null;\n  }\n\n  return extractReleaseDateFromHeading(latestSection.heading);\n}\n"
  },
  {
    "path": "apps/www/lib/changelog/git.ts",
    "content": "import { execFileSync } from \"node:child_process\";\n\nexport type ReleaseGitContext = {\n  lastTag: string | null;\n  range: string;\n  commits: Array<{\n    hash: string;\n    subject: string;\n    body: string;\n    files: string[];\n  }>;\n  changedFiles: string[];\n};\n\nexport type ReleaseGitContextOptions = {\n  fromRef?: string;\n  fromDate?: string;\n  toRef?: string;\n  fromChangelogPath?: string;\n};\n\nfunction isToolUiComponentPath(filePath: string): boolean {\n  return filePath.startsWith(\"components/tool-ui/\");\n}\n\nfunction filterToolUiComponentFiles(files: string[]): string[] {\n  return files.filter((filePath) => isToolUiComponentPath(filePath));\n}\n\nfunction runGit(projectRoot: string, args: string[]): string {\n  return execFileSync(\"git\", [\"-C\", projectRoot, ...args], {\n    encoding: \"utf8\",\n    stdio: [\"ignore\", \"pipe\", \"pipe\"],\n  }).trim();\n}\n\nfunction tryRunGit(projectRoot: string, args: string[]): string | null {\n  try {\n    return runGit(projectRoot, args);\n  } catch {\n    return null;\n  }\n}\n\nfunction collectCommitFiles(projectRoot: string, hash: string): string[] {\n  const output = runGit(projectRoot, [\n    \"show\",\n    \"--name-only\",\n    \"--pretty=format:\",\n    hash,\n  ]);\n\n  return output\n    .split(\"\\n\")\n    .map((line) => line.trim())\n    .filter(Boolean);\n}\n\nfunction resolveReleaseRange(\n  projectRoot: string,\n  options: ReleaseGitContextOptions,\n): { lastTag: string | null; range: string } {\n  const normalizedFromRef = options.fromRef?.trim();\n  const normalizedFromDate = options.fromDate?.trim();\n  const normalizedToRef = options.toRef?.trim() || \"HEAD\";\n  const normalizedFromChangelogPath = options.fromChangelogPath?.trim();\n\n  const baselineOptionsCount = [\n    normalizedFromRef,\n    normalizedFromDate,\n    normalizedFromChangelogPath,\n  ].filter(Boolean).length;\n\n  if (baselineOptionsCount > 1) {\n    throw new Error(\n      \"Invalid changelog range options. Provide only one baseline selector: fromRef, fromDate, or fromChangelogPath.\",\n    );\n  }\n\n  if (normalizedFromRef) {\n    return {\n      lastTag: null,\n      range: `${normalizedFromRef}..${normalizedToRef}`,\n    };\n  }\n\n  if (normalizedFromDate) {\n    if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(normalizedFromDate)) {\n      throw new Error(\n        `Invalid fromDate \"${normalizedFromDate}\". Use YYYY-MM-DD.`,\n      );\n    }\n\n    const baselineCommit = tryRunGit(projectRoot, [\n      \"rev-list\",\n      \"-n\",\n      \"1\",\n      `--before=${normalizedFromDate}T23:59:59`,\n      normalizedToRef,\n    ]);\n\n    if (!baselineCommit) {\n      return {\n        lastTag: null,\n        range: normalizedToRef,\n      };\n    }\n\n    return {\n      lastTag: null,\n      range: `${baselineCommit}..${normalizedToRef}`,\n    };\n  }\n\n  if (normalizedFromChangelogPath) {\n    const changelogBaselineCommit = tryRunGit(projectRoot, [\n      \"log\",\n      \"-n\",\n      \"1\",\n      \"--pretty=format:%H\",\n      \"--\",\n      normalizedFromChangelogPath,\n    ]);\n\n    if (!changelogBaselineCommit) {\n      throw new Error(\n        [\n          `No git history found for changelog path \"${normalizedFromChangelogPath}\".`,\n          \"Ensure the changelog file exists and has been committed at least once.\",\n        ].join(\" \"),\n      );\n    }\n\n    return {\n      lastTag: null,\n      range: `${changelogBaselineCommit}..${normalizedToRef}`,\n    };\n  }\n\n  const lastTag = tryRunGit(projectRoot, [\"describe\", \"--tags\", \"--abbrev=0\"]);\n  if (!lastTag) {\n    throw new Error(\n      [\n        \"No git release tag found. Changelog generation requires a tagged baseline.\",\n        'Create and push an annotated tag first (example: git tag -a v2026.2.13 -m \"Release v2026.2.13\" && git push origin v2026.2.13).',\n      ].join(\"\\n\"),\n    );\n  }\n\n  return {\n    lastTag,\n    range: `${lastTag}..HEAD`,\n  };\n}\n\nexport function collectReleaseGitContext(\n  projectRoot: string,\n  options: ReleaseGitContextOptions = {},\n): ReleaseGitContext {\n  const { lastTag, range } = resolveReleaseRange(projectRoot, options);\n  const rawCommits = runGit(projectRoot, [\n    \"log\",\n    \"--no-merges\",\n    \"--pretty=format:%H%x1f%s%x1f%b%x1e\",\n    range,\n  ]);\n\n  const commits = rawCommits\n    .split(\"\\x1e\")\n    .map((entry) => entry.trim())\n    .filter(Boolean)\n    .map((entry) => {\n      const [hash, subject, body] = entry.split(\"\\x1f\");\n      const files = filterToolUiComponentFiles(\n        collectCommitFiles(projectRoot, hash),\n      );\n      return {\n        hash,\n        subject: subject?.trim() ?? \"\",\n        body: body?.trim() ?? \"\",\n        files,\n      };\n    })\n    .filter((commit) => commit.files.length > 0);\n\n  if (commits.length === 0) {\n    throw new Error(\n      [\n        `No tool-ui component commits found for release range \"${range}\".`,\n        \"Changelog inference only includes component source changes under components/tool-ui/.\",\n      ].join(\" \"),\n    );\n  }\n\n  const changedFiles = Array.from(\n    new Set(commits.flatMap((commit) => commit.files)),\n  ).sort((a, b) => a.localeCompare(b));\n\n  return {\n    lastTag,\n    range,\n    commits,\n    changedFiles,\n  };\n}\n\nexport function formatCommitSummary(\n  context: ReleaseGitContext,\n  maxCommits = 120,\n): string {\n  const commits = context.commits.slice(0, maxCommits);\n  return commits\n    .map((commit) => {\n      const lines = [`- ${commit.hash.slice(0, 7)} ${commit.subject}`];\n      if (commit.body) {\n        lines.push(`  body: ${commit.body.replace(/\\s+/g, \" \").trim()}`);\n      }\n\n      if (commit.files.length > 0) {\n        const fileList = commit.files.slice(0, 12).join(\", \");\n        const extra =\n          commit.files.length > 12\n            ? ` (+${commit.files.length - 12} more)`\n            : \"\";\n        lines.push(`  files: ${fileList}${extra}`);\n      }\n\n      return lines.join(\"\\n\");\n    })\n    .join(\"\\n\");\n}\n"
  },
  {
    "path": "apps/www/lib/changelog/inference.ts",
    "content": "import { anthropic } from \"@ai-sdk/anthropic\";\nimport { openai } from \"@ai-sdk/openai\";\nimport { generateText } from \"ai\";\nimport { z } from \"zod\";\nimport type { InferredReleaseNotes } from \"./changelog\";\n\nconst InferredReleaseNotesSchema = z.object({\n  breakingChanges: z.array(z.string().min(1)).default([]),\n  changes: z.array(z.string().min(1)).min(1),\n  migrationPrompt: z.string().min(1).nullable(),\n});\n\ntype InferReleaseNotesInput = {\n  releaseDate: string;\n  commitSummary: string;\n  changedFiles: string[];\n  changelogTemplateContext: string;\n};\n\ntype CriticalMigrationEvidence = Pick<\n  InferReleaseNotesInput,\n  \"commitSummary\" | \"changedFiles\"\n>;\n\nconst RELEASE_NOTES_SYSTEM_PROMPT =\n  \"You are generating release notes for a public changelog. Return strict JSON only.\";\n\ntype CriticalTheme = {\n  id: string;\n  heading: string;\n  summarySignals: RegExp[];\n  fileSignals: RegExp[];\n  breakingSummarySignals: RegExp[];\n  inferBreakingFromFiles?: (changedFiles: string[]) => boolean;\n  breakingChangeLine: string;\n  changeLine: string;\n  migrationSteps: string[];\n};\n\nconst ACTION_MODEL_THEME: CriticalTheme = {\n  id: \"action-model\",\n  heading: \"Action model\",\n  summarySignals: [\n    /action model/i,\n    /local\\s*actions?/i,\n    /decision\\s*actions?/i,\n    /response-action/i,\n    /toolui\\.(localactions|decisionactions)/i,\n  ],\n  fileSignals: [\n    /components\\/tool-ui\\/shared\\/local-actions\\.tsx$/i,\n    /components\\/tool-ui\\/shared\\/decision-actions\\.tsx$/i,\n    /components\\/tool-ui\\/shared\\/tool-ui(-context)?\\.tsx$/i,\n    /components\\/tool-ui\\/shared\\/schema\\.ts$/i,\n  ],\n  breakingSummarySignals: [\n    /\\bbreaking\\b/i,\n    /\\bremove(d)?\\b/i,\n    /\\bmigrat(e|ed|ion)\\b/i,\n    /\\bcut[- ]?over\\b/i,\n    /\\breplace(d)?\\b/i,\n    /\\bdeprecat(e|ed|ion)\\b/i,\n    /\\bdrop(ped)?\\b/i,\n    /\\blegacy\\b/i,\n  ],\n  inferBreakingFromFiles: (changedFiles) => {\n    const hasLocalActions = changedFiles.some((pathValue) =>\n      /components\\/tool-ui\\/shared\\/local-actions\\.tsx$/i.test(pathValue),\n    );\n    const hasDecisionActions = changedFiles.some((pathValue) =>\n      /components\\/tool-ui\\/shared\\/decision-actions\\.tsx$/i.test(pathValue),\n    );\n    return hasLocalActions && hasDecisionActions;\n  },\n  breakingChangeLine:\n    \"Tool UI action model moved to compound action surfaces (`ToolUI.LocalActions` / `ToolUI.DecisionActions`) and removed legacy inline response-action patterns. See [Actions](/docs/actions).\",\n  changeLine:\n    \"Tool UI action-surface composition was updated around `ToolUI.LocalActions` / `ToolUI.DecisionActions`. See [Actions](/docs/actions).\",\n  migrationSteps: [\n    \"Migrate action handling to `ToolUI.LocalActions` / `ToolUI.DecisionActions` for consequential workflows.\",\n    \"Replace legacy inline response-action handlers with action-surface composition where needed.\",\n    \"Update tests and docs to reflect action-surface and receipt semantics.\",\n  ],\n};\n\nconst CRITICAL_THEMES: CriticalTheme[] = [ACTION_MODEL_THEME];\n\nfunction extractJsonPayload(text: string): string {\n  const fencedMatch = text.match(/```json\\s*([\\s\\S]*?)```/i);\n  if (fencedMatch?.[1]) {\n    return fencedMatch[1].trim();\n  }\n\n  const firstBrace = text.indexOf(\"{\");\n  const lastBrace = text.lastIndexOf(\"}\");\n  if (firstBrace === -1 || lastBrace === -1 || lastBrace <= firstBrace) {\n    throw new Error(\"No JSON object found in inferred changelog output.\");\n  }\n\n  return text.slice(firstBrace, lastBrace + 1).trim();\n}\n\nfunction normalizeInferredNotes(\n  notes: InferredReleaseNotes,\n): InferredReleaseNotes {\n  const normalizeText = (text: string): string =>\n    text\n      .replace(/\\\\n/g, \"\\n\")\n      .replace(/\\\\t/g, \"\\t\")\n      .replace(/\\r\\n/g, \"\\n\")\n      .trim();\n\n  const breakingChanges = notes.breakingChanges\n    .map((item) => normalizeText(item))\n    .filter(Boolean);\n  const changes = notes.changes\n    .map((item) => normalizeText(item))\n    .filter(Boolean);\n\n  const isGlobalSchemaBoundaryLine = (item: string): boolean => {\n    const hasToolUiScope =\n      /tool ui/i.test(item) &&\n      (/component entrypoints?/i.test(item) ||\n        /across all components|across tool ui components|all tool ui components|repo-wide/i.test(\n          item,\n        ));\n    const hasSchemaBoundarySignal = /schema|entrypoint|boundary/i.test(item);\n    return hasToolUiScope && hasSchemaBoundarySignal;\n  };\n\n  const dedupeSchemaScope = (items: string[]): string[] => {\n    const hasGlobalSchemaBoundaryLine = items.some((item) =>\n      isGlobalSchemaBoundaryLine(item),\n    );\n\n    if (!hasGlobalSchemaBoundaryLine) {\n      return items;\n    }\n\n    return items.filter(\n      (item) =>\n        !(/datatable/i.test(item) && /schema|entrypoint|boundary/i.test(item)),\n    );\n  };\n\n  const normalizedBreakingChanges = dedupeSchemaScope(breakingChanges);\n  const normalizedChanges = dedupeSchemaScope(changes);\n  const hasGlobalSchemaBoundaryLine =\n    normalizedBreakingChanges.some((item) =>\n      isGlobalSchemaBoundaryLine(item),\n    ) || normalizedChanges.some((item) => isGlobalSchemaBoundaryLine(item));\n\n  if (normalizedChanges.length === 0) {\n    throw new Error(\n      \"Inferred changelog output must include at least one change.\",\n    );\n  }\n\n  const migrationPrompt = notes.migrationPrompt\n    ? normalizeText(notes.migrationPrompt)\n    : null;\n  const normalizedMigrationPrompt =\n    hasGlobalSchemaBoundaryLine && migrationPrompt\n      ? migrationPrompt.replace(\n          /\\btool ui and datatable schemas\\b/gi,\n          \"Tool UI schemas\",\n        )\n      : migrationPrompt;\n\n  return {\n    breakingChanges: normalizedBreakingChanges,\n    changes: normalizedChanges,\n    migrationPrompt: normalizedMigrationPrompt,\n  };\n}\n\nfunction textHasAnySignal(text: string, signals: RegExp[]): boolean {\n  return signals.some((signal) => signal.test(text));\n}\n\nfunction filesHaveAnySignal(filePaths: string[], signals: RegExp[]): boolean {\n  return filePaths.some((filePath) =>\n    signals.some((signal) => signal.test(filePath)),\n  );\n}\n\nfunction appendThemeMigrationSteps(\n  prompt: string | null,\n  theme: CriticalTheme,\n): string {\n  if (!prompt || prompt.trim().length === 0) {\n    return [\n      theme.migrationSteps[0] ?? \"\",\n      \"\",\n      \"Steps:\",\n      ...theme.migrationSteps.slice(1).map((step) => `- ${step}`),\n    ].join(\"\\n\");\n  }\n\n  if (textHasAnySignal(prompt, theme.summarySignals)) {\n    return prompt;\n  }\n\n  return [\n    prompt.trimEnd(),\n    \"\",\n    `${theme.heading} steps:`,\n    ...theme.migrationSteps.map((step) => `- ${step}`),\n  ].join(\"\\n\");\n}\n\nexport function ensureCriticalMigrationCoverage(\n  notes: InferredReleaseNotes,\n  evidence: CriticalMigrationEvidence,\n): InferredReleaseNotes {\n  let next: InferredReleaseNotes = {\n    breakingChanges: [...notes.breakingChanges],\n    changes: [...notes.changes],\n    migrationPrompt: notes.migrationPrompt,\n  };\n\n  for (const theme of CRITICAL_THEMES) {\n    const hasThemeEvidence =\n      textHasAnySignal(evidence.commitSummary, theme.summarySignals) ||\n      filesHaveAnySignal(evidence.changedFiles, theme.fileSignals);\n\n    if (!hasThemeEvidence) {\n      continue;\n    }\n\n    const isBreakingEvidence =\n      textHasAnySignal(evidence.commitSummary, theme.breakingSummarySignals) ||\n      (theme.inferBreakingFromFiles?.(evidence.changedFiles) ?? false);\n\n    const hasThemeChange = next.changes.some((line) =>\n      textHasAnySignal(line, theme.summarySignals),\n    );\n    if (!hasThemeChange) {\n      next.changes.push(theme.changeLine);\n    }\n\n    const hasThemeBreaking = next.breakingChanges.some((line) =>\n      textHasAnySignal(line, theme.summarySignals),\n    );\n    if (isBreakingEvidence && !hasThemeBreaking) {\n      next.breakingChanges.push(theme.breakingChangeLine);\n    }\n\n    const shouldAppendMigrationSteps =\n      isBreakingEvidence ||\n      next.breakingChanges.some((line) =>\n        textHasAnySignal(line, theme.summarySignals),\n      );\n    if (shouldAppendMigrationSteps) {\n      next = {\n        ...next,\n        migrationPrompt: appendThemeMigrationSteps(next.migrationPrompt, theme),\n      };\n    }\n  }\n\n  return next;\n}\n\nexport function parseInferredReleaseNotes(text: string): InferredReleaseNotes {\n  let parsedJson: unknown;\n  try {\n    parsedJson = JSON.parse(extractJsonPayload(text));\n  } catch (error) {\n    throw new Error(\n      `Invalid inferred changelog payload: ${(error as Error).message}`,\n    );\n  }\n\n  try {\n    return normalizeInferredNotes(InferredReleaseNotesSchema.parse(parsedJson));\n  } catch (error) {\n    throw new Error(\n      `Invalid inferred changelog payload: ${(error as Error).message}`,\n    );\n  }\n}\n\nfunction selectModel() {\n  if (process.env.ANTHROPIC_API_KEY) {\n    return anthropic(\"claude-sonnet-4-5-20250929\");\n  }\n\n  if (process.env.OPENAI_API_KEY) {\n    return openai(\"gpt-5-nano\");\n  }\n\n  throw new Error(\n    \"Missing model credentials. Set OPENAI_API_KEY or ANTHROPIC_API_KEY.\",\n  );\n}\n\nexport function buildInferencePrompt(input: InferReleaseNotesInput): string {\n  return [\n    \"Infer release notes from commit evidence.\",\n    `Release date: ${input.releaseDate}`,\n    \"\",\n    \"Output JSON schema:\",\n    '{ \"breakingChanges\": string[], \"changes\": string[], \"migrationPrompt\": string | null }',\n    \"\",\n    \"Rules:\",\n    \"- Include a migrationPrompt when components have been updated and users may want to re-install. A migration prompt can exist with or without breaking changes.\",\n    \"- Keep migrationPrompt as an imperative migration checklist for coding agents.\",\n    \"- Keep entries concise and user-facing.\",\n    \"- Include markdown links to relevant docs routes when confidence is high (for example [Actions](/docs/actions) for action-model changes).\",\n    \"- Do not include markdown code fences in field values.\",\n    \"- If a change has propagated across Tool UI components, use repo-wide wording.\",\n    \"- Avoid over-indexing on a seed component (for example DataTable) when scope is cross-component.\",\n    \"- Mention a specific component only when evidence indicates scope is actually component-local.\",\n    \"\",\n    \"Scenario examples:\",\n    \"1) Cross-component schema migration (global wording)\",\n    \"Evidence: DataTable commit appears first, then similar schema-entrypoint changes across multiple Tool UI components.\",\n    'Good breakingChanges example: [\"Tool UI component entrypoints now enforce /schema boundaries across components.\"]',\n    'Bad breakingChanges example: [\"DataTable moved to /schema entrypoint.\"]',\n    \"\",\n    \"2) Component-local fix (specific wording)\",\n    \"Evidence: only option-list files changed, with no matching changes in other components.\",\n    'Good changes example: [\"Option List receipt preset generation no longer includes onConfirm in receipt mode.\"]',\n    \"\",\n    \"3) Migration prompt scope\",\n    \"When breaking scope is global, migrationPrompt should describe repo-wide migration steps.\",\n    \"When breaking scope is local, migrationPrompt may mention the affected component directly.\",\n    \"\",\n    \"Existing changelog style sample:\",\n    input.changelogTemplateContext,\n    \"\",\n    \"Changed files:\",\n    input.changedFiles.map((file) => `- ${file}`).join(\"\\n\"),\n    \"\",\n    \"Commit evidence:\",\n    input.commitSummary,\n    \"\",\n    \"Return JSON only.\",\n  ].join(\"\\n\");\n}\n\nexport async function inferReleaseNotes(\n  input: InferReleaseNotesInput,\n): Promise<InferredReleaseNotes> {\n  const model = selectModel();\n\n  const { text } = await generateText({\n    model,\n    system: RELEASE_NOTES_SYSTEM_PROMPT,\n    prompt: buildInferencePrompt(input),\n  });\n\n  const parsedNotes = parseInferredReleaseNotes(text);\n  return ensureCriticalMigrationCoverage(parsedNotes, {\n    commitSummary: input.commitSummary,\n    changedFiles: input.changedFiles,\n  });\n}\n"
  },
  {
    "path": "apps/www/lib/demo-chat/toolkit.tsx",
    "content": "\"use client\";\n\nimport type { Toolkit } from \"@assistant-ui/react\";\nimport { Plan } from \"@/components/tool-ui/plan\";\nimport { DataTable } from \"@/components/tool-ui/data-table\";\nimport { StatsDisplay } from \"@/components/tool-ui/stats-display\";\nimport { Terminal } from \"@/components/tool-ui/terminal\";\nimport { safeParseSerializablePlan } from \"@/components/tool-ui/plan/schema\";\nimport { safeParseSerializableDataTable } from \"@/components/tool-ui/data-table/schema\";\nimport { safeParseSerializableStatsDisplay } from \"@/components/tool-ui/stats-display/schema\";\nimport { safeParseSerializableTerminal } from \"@/components/tool-ui/terminal/schema\";\nimport { ToolUI } from \"@/components/tool-ui/shared\";\nimport { ToolFallback } from \"@/app/components/assistant-ui/tool-fallback\";\n\nexport const DEMO_CHAT_TOOLKIT: Toolkit = {\n  show_plan: {\n    type: \"backend\",\n    render: ({ result }) => {\n      const parsed = safeParseSerializablePlan(result);\n      if (!parsed) return null;\n      return (\n        <ToolUI id={parsed.id}>\n          <ToolUI.Surface>\n            <Plan {...parsed} />\n          </ToolUI.Surface>\n          <ToolUI.Actions>\n            <ToolUI.LocalActions\n              actions={[\n                { id: \"approve\", label: \"Looks good\" },\n                { id: \"revise\", label: \"Request changes\", variant: \"outline\" },\n              ]}\n              onAction={() => {}}\n            />\n          </ToolUI.Actions>\n        </ToolUI>\n      );\n    },\n  },\n  get_tasks: {\n    type: \"backend\",\n    render: ({ result }) => {\n      const parsed = safeParseSerializableDataTable(result);\n      if (!parsed) return null;\n      return (\n        <div className=\"not-prose\">\n          <DataTable.Table\n            id={parsed.id}\n            rowIdKey={parsed.rowIdKey ?? \"id\"}\n            columns={parsed.columns}\n            data={parsed.data}\n            defaultSort={parsed.defaultSort}\n          />\n        </div>\n      );\n    },\n  },\n  show_stats: {\n    type: \"backend\",\n    render: ({ result }) => {\n      const parsed = safeParseSerializableStatsDisplay(result);\n      if (!parsed) return null;\n      return <StatsDisplay {...parsed} />;\n    },\n  },\n  show_terminal: {\n    type: \"backend\",\n    render: ({ result }) => {\n      const parsed = safeParseSerializableTerminal(result);\n      if (!parsed) return null;\n      return <Terminal {...parsed} />;\n    },\n  },\n};\n\nexport const DEMO_CHAT_TOOL_FALLBACK = ToolFallback;\n"
  },
  {
    "path": "apps/www/lib/docs/auto-link.tsx",
    "content": "import * as React from \"react\";\nimport { componentsRegistry } from \"@/lib/docs/component-registry\";\n\ntype ReactNode = React.ReactNode;\n\ntype Pattern = { match: string; href: string };\n\n// Static patterns for external libraries\nconst externalPatterns: Pattern[] = [\n  { match: \"shadcn/ui\", href: \"https://ui.shadcn.com\" },\n  {\n    match: \"assistant-ui\",\n    href: \"https://www.assistant-ui.com/\",\n  },\n  { match: \"radix\", href: \"https://www.radix-ui.com\" },\n  { match: \"Zod\", href: \"https://zod.dev\" },\n];\n\n// Build patterns for label (e.g., \"Link Preview\"), camel (\"LinkPreview\"), and id (\"link-preview\").\n// Only include kebab patterns with hyphens to avoid matching lowercase words like \"audio\" or \"video\".\nconst patterns: Pattern[] = [\n  ...externalPatterns,\n  ...componentsRegistry\n    .flatMap((c) => {\n      const label = c.label;\n      const camel = label.replace(/\\s+/g, \"\");\n      const kebab = c.id;\n      const result = [\n        { match: label, href: c.path },\n        { match: camel, href: c.path },\n      ];\n      if (kebab.includes(\"-\")) {\n        result.push({ match: kebab, href: c.path });\n      }\n      return result;\n    })\n    .sort((a, b) => b.match.length - a.match.length),\n];\n\nfunction isWordChar(ch?: string) {\n  return !!ch && /[A-Za-z0-9_]/.test(ch);\n}\n\nfunction isBoundary(prev: string | undefined, next: string | undefined) {\n  // Disallow adjoining word characters or hyphen to avoid partial matches\n  const prevIsWord = isWordChar(prev) || prev === \"-\";\n  const nextIsWord = isWordChar(next) || next === \"-\";\n  return !prevIsWord && !nextIsWord;\n}\n\nfunction processText(value: string): ReactNode[] {\n  const out: ReactNode[] = [];\n  let i = 0;\n  while (i < value.length) {\n    let bestIdx = Infinity;\n    let best: Pattern | null = null;\n    let bestLen = 0;\n\n    for (const p of patterns) {\n      const idx = value.indexOf(p.match, i);\n      if (idx !== -1 && idx < bestIdx) {\n        bestIdx = idx;\n        best = p;\n        bestLen = p.match.length;\n      }\n    }\n\n    if (!best) {\n      if (i < value.length) out.push(value.slice(i));\n      break;\n    }\n\n    const start = bestIdx;\n    const end = bestIdx + bestLen;\n    const prev = start > 0 ? value[start - 1] : undefined;\n    const next = end < value.length ? value[end] : undefined;\n    const ok = isBoundary(prev, next);\n\n    if (!ok) {\n      out.push(value.slice(i, start + 1));\n      i = start + 1;\n      continue;\n    }\n\n    if (start > i) out.push(value.slice(i, start));\n    out.push(\n      React.createElement(\n        \"a\",\n        { href: best.href, key: `${best.href}-${start}` },\n        value.slice(start, end),\n      ),\n    );\n    i = end;\n  }\n  return out;\n}\n\nconst SKIP_TYPES = new Set([\"a\", \"code\", \"pre\", \"kbd\", \"samp\"]);\n\nfunction transform(node: ReactNode): ReactNode {\n  if (typeof node === \"string\") {\n    return processText(node);\n  }\n  if (Array.isArray(node)) {\n    return node.map((n, i) =>\n      React.isValidElement(n) ? cloneWithChildren(n, i) : transform(n),\n    );\n  }\n  if (React.isValidElement(node)) {\n    return cloneWithChildren(node);\n  }\n  return node;\n}\n\nfunction cloneWithChildren(\n  el: React.ReactElement,\n  key?: React.Key,\n): React.ReactElement {\n  const type = el.type;\n  const hasHrefProp =\n    typeof type !== \"string\" &&\n    (el.props as { href?: unknown } | undefined)?.href !== undefined;\n  const skip =\n    (typeof type === \"string\" && SKIP_TYPES.has(type)) || hasHrefProp;\n  const hasChildren = Object.prototype.hasOwnProperty.call(\n    el.props as object,\n    \"children\",\n  );\n  if (\n    skip ||\n    !hasChildren ||\n    (el.props as { children?: ReactNode }).children == null\n  ) {\n    return key != null ? React.cloneElement(el, { key }) : el;\n  }\n  const nextChildren = transform(\n    (el.props as { children?: ReactNode }).children as ReactNode,\n  );\n  if (key != null) return React.cloneElement(el, { key }, nextChildren);\n  return React.cloneElement(el, undefined, nextChildren);\n}\n\nexport function AutoLinkChildren({ children }: { children: ReactNode }) {\n  return <>{transform(children)}</>;\n}\n\nexport function withAutoLink<E extends React.ElementType>(Base: E) {\n  type Props = React.ComponentPropsWithoutRef<E> & { children?: ReactNode };\n  type WithoutChildren = Omit<Props, \"children\">;\n\n  const AutoLinked: React.FC<Props> = (props) => {\n    const { children, ...rest } = props;\n    return React.createElement(\n      Base,\n      rest as WithoutChildren,\n      <AutoLinkChildren>{children}</AutoLinkChildren>,\n    );\n  };\n  return AutoLinked as React.FC<React.ComponentPropsWithoutRef<E>>;\n}\n"
  },
  {
    "path": "apps/www/lib/docs/component-ids.ts",
    "content": "export const componentIds = [\n  \"approval-card\",\n  \"chart\",\n  \"citation\",\n  \"code-block\",\n  \"code-diff\",\n  \"data-table\",\n  \"geo-map\",\n  \"image\",\n  \"image-gallery\",\n  \"video\",\n  \"audio\",\n  \"instagram-post\",\n  \"link-preview\",\n  \"linkedin-post\",\n  \"message-draft\",\n  \"item-carousel\",\n  \"option-list\",\n  \"order-summary\",\n  \"parameter-slider\",\n  \"plan\",\n  \"preferences-panel\",\n  \"progress-tracker\",\n  \"stats-display\",\n  \"terminal\",\n  \"question-flow\",\n  \"weather-widget\",\n  \"x-post\",\n] as const;\n\nexport type ComponentId = (typeof componentIds)[number];\n\nexport function isComponentId(\n  value: string | undefined | null,\n): value is ComponentId {\n  if (typeof value !== \"string\") {\n    return false;\n  }\n\n  return componentIds.includes(value as ComponentId);\n}\n"
  },
  {
    "path": "apps/www/lib/docs/component-registry.ts",
    "content": "export type ComponentCategory =\n  | \"media\"\n  | \"artifacts\"\n  | \"display\"\n  | \"input\"\n  | \"confirmation\"\n  | \"progress\";\n\nexport interface CategoryMeta {\n  label: string;\n  order: number;\n}\n\nexport const CATEGORY_META: Record<ComponentCategory, CategoryMeta> = {\n  progress: { label: \"Progress\", order: 1 },\n  input: { label: \"Input\", order: 2 },\n  display: { label: \"Display\", order: 3 },\n  artifacts: { label: \"Artifacts\", order: 4 },\n  confirmation: { label: \"Confirmation\", order: 5 },\n  media: { label: \"Media\", order: 6 },\n};\n\nexport interface ComponentMeta {\n  id: string;\n  label: string;\n  description: string;\n  path: string;\n  category: ComponentCategory;\n  /** Prompt for npx tool-agent to integrate this component */\n  toolAgentPrompt: string;\n}\n\nexport const componentsRegistry: ComponentMeta[] = [\n  {\n    id: \"approval-card\",\n    label: \"Approval Card\",\n    description: \"Binary confirmation for agent actions\",\n    path: \"/docs/approval-card\",\n    category: \"confirmation\",\n    toolAgentPrompt:\n      \"integrate the approval card component for binary confirmation of agent actions\",\n  },\n  {\n    id: \"chart\",\n    label: \"Chart\",\n    description: \"Visualize data with interactive charts\",\n    path: \"/docs/chart\",\n    category: \"artifacts\",\n    toolAgentPrompt:\n      \"integrate the chart component to visualize data with interactive charts\",\n  },\n  {\n    id: \"citation\",\n    label: \"Citation\",\n    description: \"Display source references with attribution\",\n    path: \"/docs/citation\",\n    category: \"display\",\n    toolAgentPrompt:\n      \"integrate the citation component to display source references with attribution\",\n  },\n  {\n    id: \"code-block\",\n    label: \"Code Block\",\n    description: \"Display syntax-highlighted code snippets\",\n    path: \"/docs/code-block\",\n    category: \"artifacts\",\n    toolAgentPrompt:\n      \"integrate the code block component for syntax-highlighted code snippets\",\n  },\n  {\n    id: \"code-diff\",\n    label: \"Code Diff\",\n    description: \"Compare code changes with syntax-highlighted diffs\",\n    path: \"/docs/code-diff\",\n    category: \"artifacts\",\n    toolAgentPrompt:\n      \"integrate the code diff component to compare code changes with syntax-highlighted diffs\",\n  },\n  {\n    id: \"data-table\",\n    label: \"Data Table\",\n    description: \"Present structured data in sortable tables\",\n    path: \"/docs/data-table\",\n    category: \"artifacts\",\n    toolAgentPrompt:\n      \"integrate the data table component to present structured data in sortable tables\",\n  },\n  {\n    id: \"geo-map\",\n    label: \"Geo Map\",\n    description: \"Display geolocated entities and fleet positions\",\n    path: \"/docs/geo-map\",\n    category: \"display\",\n    toolAgentPrompt:\n      \"integrate the geo map component to display geolocated entities and fleet positions\",\n  },\n  {\n    id: \"image\",\n    label: \"Image\",\n    description: \"Display images with metadata and attribution\",\n    path: \"/docs/image\",\n    category: \"media\",\n    toolAgentPrompt:\n      \"integrate the image component to display images with metadata and attribution\",\n  },\n  {\n    id: \"image-gallery\",\n    label: \"Image Gallery\",\n    description: \"Masonry grid with fullscreen lightbox viewer\",\n    path: \"/docs/image-gallery\",\n    category: \"media\",\n    toolAgentPrompt:\n      \"integrate the image gallery component with masonry grid and lightbox viewer\",\n  },\n  {\n    id: \"video\",\n    label: \"Video\",\n    description: \"Video playback with controls and poster\",\n    path: \"/docs/video\",\n    category: \"media\",\n    toolAgentPrompt:\n      \"integrate the video component for video playback with controls and poster\",\n  },\n  {\n    id: \"audio\",\n    label: \"Audio\",\n    description: \"Audio playback with artwork and metadata\",\n    path: \"/docs/audio\",\n    category: \"media\",\n    toolAgentPrompt:\n      \"integrate the audio component for audio playback with artwork and metadata\",\n  },\n  {\n    id: \"link-preview\",\n    label: \"Link Preview\",\n    description: \"Rich link previews with Open Graph data\",\n    path: \"/docs/link-preview\",\n    category: \"display\",\n    toolAgentPrompt:\n      \"integrate the link preview component for rich link previews with Open Graph data\",\n  },\n  {\n    id: \"message-draft\",\n    label: \"Message Draft\",\n    description: \"Review and approve messages before sending\",\n    path: \"/docs/message-draft\",\n    category: \"artifacts\",\n    toolAgentPrompt:\n      \"integrate the message draft component to review and approve messages before sending\",\n  },\n  {\n    id: \"option-list\",\n    label: \"Option List\",\n    description: \"Let users select from multiple choices\",\n    path: \"/docs/option-list\",\n    category: \"input\",\n    toolAgentPrompt:\n      \"integrate the option list component to let users select from multiple choices\",\n  },\n  {\n    id: \"order-summary\",\n    label: \"Order Summary\",\n    description: \"Display purchases with itemized pricing\",\n    path: \"/docs/order-summary\",\n    category: \"confirmation\",\n    toolAgentPrompt:\n      \"integrate the order summary component to display purchases with itemized pricing\",\n  },\n  {\n    id: \"parameter-slider\",\n    label: \"Parameter Slider\",\n    description: \"Numeric parameter adjustment controls\",\n    path: \"/docs/parameter-slider\",\n    category: \"input\",\n    toolAgentPrompt:\n      \"integrate the parameter slider component for numeric parameter adjustment controls\",\n  },\n  {\n    id: \"plan\",\n    label: \"Plan\",\n    description: \"Step-by-step task workflows with status tracking\",\n    path: \"/docs/plan\",\n    category: \"progress\",\n    toolAgentPrompt:\n      \"integrate the plan component for step-by-step task workflows with status tracking\",\n  },\n  {\n    id: \"preferences-panel\",\n    label: \"Preferences Panel\",\n    description: \"Compact settings panel for user preferences\",\n    path: \"/docs/preferences-panel\",\n    category: \"input\",\n    toolAgentPrompt:\n      \"integrate the preferences panel component for compact user settings\",\n  },\n  {\n    id: \"progress-tracker\",\n    label: \"Progress Tracker\",\n    description: \"Real-time status feedback for multi-step operations\",\n    path: \"/docs/progress-tracker\",\n    category: \"progress\",\n    toolAgentPrompt:\n      \"integrate the progress tracker component for real-time status feedback on multi-step operations\",\n  },\n  {\n    id: \"item-carousel\",\n    label: \"Item Carousel\",\n    description: \"Horizontal carousel for browsing collections\",\n    path: \"/docs/item-carousel\",\n    category: \"display\",\n    toolAgentPrompt:\n      \"integrate the item carousel component for horizontal browsing of collections\",\n  },\n  {\n    id: \"instagram-post\",\n    label: \"Instagram Post\",\n    description: \"Render Instagram post previews\",\n    path: \"/docs/instagram-post\",\n    category: \"artifacts\",\n    toolAgentPrompt:\n      \"integrate the instagram post component to render Instagram post previews\",\n  },\n  {\n    id: \"linkedin-post\",\n    label: \"LinkedIn Post\",\n    description: \"Render LinkedIn post previews\",\n    path: \"/docs/linkedin-post\",\n    category: \"artifacts\",\n    toolAgentPrompt:\n      \"integrate the linkedin post component to render LinkedIn post previews\",\n  },\n  {\n    id: \"x-post\",\n    label: \"X Post\",\n    description: \"Render X post previews\",\n    path: \"/docs/x-post\",\n    category: \"artifacts\",\n    toolAgentPrompt:\n      \"integrate the x post component to render X/Twitter post previews\",\n  },\n  {\n    id: \"stats-display\",\n    label: \"Stats Display\",\n    description: \"Key metrics and KPIs in a visual grid\",\n    path: \"/docs/stats-display\",\n    category: \"display\",\n    toolAgentPrompt:\n      \"integrate the stats display component for key metrics and KPIs in a visual grid\",\n  },\n  {\n    id: \"terminal\",\n    label: \"Terminal\",\n    description: \"Show command-line output and logs\",\n    path: \"/docs/terminal\",\n    category: \"display\",\n    toolAgentPrompt:\n      \"integrate the terminal component to show command-line output and logs\",\n  },\n  {\n    id: \"question-flow\",\n    label: \"Question Flow\",\n    description: \"Multi-step guided questions with branching\",\n    path: \"/docs/question-flow\",\n    category: \"input\",\n    toolAgentPrompt:\n      \"integrate the question flow component for multi-step guided questions with branching\",\n  },\n  {\n    id: \"weather-widget\",\n    label: \"Weather Widget\",\n    description: \"Probably more weather widget than anyone asked for\",\n    path: \"/docs/weather-widget\",\n    category: \"display\",\n    toolAgentPrompt:\n      \"integrate the weather widget component for weather display with forecasts and conditions\",\n  },\n];\n\nexport const componentsByCategory = new Map<\n  ComponentCategory,\n  ComponentMeta[]\n>();\nfor (const component of componentsRegistry) {\n  const arr = componentsByCategory.get(component.category) || [];\n  arr.push(component);\n  componentsByCategory.set(component.category, arr);\n}\n"
  },
  {
    "path": "apps/www/lib/docs/gallery-component-docs.ts",
    "content": "export const galleryComponentDocs = {\n  \"approval-card\": {\n    name: \"Approval Card\",\n    docsHref: \"/docs/approval-card\",\n  },\n  audio: {\n    name: \"Audio\",\n    docsHref: \"/docs/audio\",\n  },\n  chart: {\n    name: \"Chart\",\n    docsHref: \"/docs/chart\",\n  },\n  citation: {\n    name: \"Citation\",\n    docsHref: \"/docs/citation\",\n  },\n  \"code-block\": {\n    name: \"Code Block\",\n    docsHref: \"/docs/code-block\",\n  },\n  \"code-diff\": {\n    name: \"Code Diff\",\n    docsHref: \"/docs/code-diff\",\n  },\n  \"data-table\": {\n    name: \"Data Table\",\n    docsHref: \"/docs/data-table\",\n  },\n  \"geo-map\": {\n    name: \"Geo Map\",\n    docsHref: \"/docs/geo-map\",\n  },\n  image: {\n    name: \"Image\",\n    docsHref: \"/docs/image\",\n  },\n  \"instagram-post\": {\n    name: \"Instagram Post\",\n    docsHref: \"/docs/instagram-post\",\n  },\n  \"image-gallery\": {\n    name: \"Image Gallery\",\n    docsHref: \"/docs/image-gallery\",\n  },\n  \"item-carousel\": {\n    name: \"Item Carousel\",\n    docsHref: \"/docs/item-carousel\",\n  },\n  \"link-preview\": {\n    name: \"Link Preview\",\n    docsHref: \"/docs/link-preview\",\n  },\n  \"linkedin-post\": {\n    name: \"LinkedIn Post\",\n    docsHref: \"/docs/linkedin-post\",\n  },\n  \"message-draft\": {\n    name: \"Message Draft\",\n    docsHref: \"/docs/message-draft\",\n  },\n  \"option-list\": {\n    name: \"Option List\",\n    docsHref: \"/docs/option-list\",\n  },\n  \"order-summary\": {\n    name: \"Order Summary\",\n    docsHref: \"/docs/order-summary\",\n  },\n  \"parameter-slider\": {\n    name: \"Parameter Slider\",\n    docsHref: \"/docs/parameter-slider\",\n  },\n  plan: {\n    name: \"Plan\",\n    docsHref: \"/docs/plan\",\n  },\n  \"preferences-panel\": {\n    name: \"Preferences Panel\",\n    docsHref: \"/docs/preferences-panel\",\n  },\n  \"progress-tracker\": {\n    name: \"Progress Tracker\",\n    docsHref: \"/docs/progress-tracker\",\n  },\n  \"question-flow\": {\n    name: \"Question Flow\",\n    docsHref: \"/docs/question-flow\",\n  },\n  \"stats-display\": {\n    name: \"Stats Display\",\n    docsHref: \"/docs/stats-display\",\n  },\n  terminal: {\n    name: \"Terminal\",\n    docsHref: \"/docs/terminal\",\n  },\n  video: {\n    name: \"Video\",\n    docsHref: \"/docs/video\",\n  },\n  \"weather-widget\": {\n    name: \"Weather Widget\",\n    docsHref: \"/docs/weather-widget\",\n  },\n  \"x-post\": {\n    name: \"X Post\",\n    docsHref: \"/docs/x-post\",\n  },\n} as const satisfies Record<\n  string,\n  { name: string; docsHref: `/docs/${string}` }\n>;\n\nexport type GalleryComponentDocId = keyof typeof galleryComponentDocs;\n"
  },
  {
    "path": "apps/www/lib/docs/gallery-layout.ts",
    "content": "export const GALLERY_LAYOUT_CLASS =\n  \"mx-auto w-full min-w-0 max-w-full overflow-x-hidden pb-20\";\n\nexport const GALLERY_MOBILE_STACK_CLASS = \"flex flex-col gap-0 lg:hidden\";\n\nexport const GALLERY_DESKTOP_GRID_CLASS =\n  \"hidden lg:grid lg:grid-cols-2 lg:items-start lg:gap-5\";\n\nexport const GALLERY_COLUMN_STACK_CLASS = \"flex flex-col gap-0\";\n\nexport const GALLERY_STANDARD_PREVIEW_WRAPPER_CLASS =\n  \"mx-auto flex w-full max-w-[680px] justify-center\";\n"
  },
  {
    "path": "apps/www/lib/docs/gallery-usage-code.ts",
    "content": "import { getPreviewConfig } from \"./preview-config\";\nimport type { ComponentId } from \"./component-ids\";\nimport type { GalleryComponentDocId } from \"./gallery-component-docs\";\n\nfunction toPascalCase(kebab: string): string {\n  return kebab\n    .split(\"-\")\n    .map((s) => s.charAt(0).toUpperCase() + s.slice(1))\n    .join(\"\");\n}\n\nconst IMPORT_OVERRIDES: Partial<Record<GalleryComponentDocId, string>> = {\n  citation: 'import { CitationList } from \"@/components/tool-ui/citation\"',\n  \"weather-widget\":\n    'import { WeatherWidget } from \"@/components/tool-ui/weather-widget/runtime\"',\n};\n\n/**\n * Returns the import statement for a Tool UI component. Use when displaying\n * code examples so users can copy a complete snippet.\n */\nexport function getImportLine(\n  componentId: GalleryComponentDocId | ComponentId,\n): string {\n  const override = IMPORT_OVERRIDES[componentId as GalleryComponentDocId];\n  if (override) return override;\n  const name = toPascalCase(componentId);\n  return `import { ${name} } from \"@/components/tool-ui/${componentId}\"`;\n}\n\n/**\n * Returns usage code (import + example) for a gallery component, or null if not available.\n */\nexport function getGalleryUsageCode(\n  componentId: GalleryComponentDocId,\n): string | null {\n  const config = getPreviewConfig(componentId as ComponentId);\n  if (!config) return null;\n\n  const preset = config.presets[config.defaultPreset];\n  if (!preset || typeof preset.generateExampleCode !== \"function\") return null;\n\n  const body = preset.generateExampleCode(preset.data);\n  const importLine = getImportLine(componentId);\n\n  return `${importLine}\\n\\nexport function Example() {\\n  return (\\n    ${body}\\n  )\\n}`;\n}\n"
  },
  {
    "path": "apps/www/lib/docs/install-snippet-analytics.ts",
    "content": "export type InstallSnippetType =\n  | \"skills\"\n  | \"registry\"\n  | \"package_manager\"\n  | \"tool_agent\";\n\nconst SKILLS_INSTALL_PATTERN = /(?:^|\\n)\\s*npx\\s+skills\\s+add\\b/i;\nconst TOOL_AGENT_PATTERN = /(?:^|\\n)\\s*npx\\s+tool-agent\\b/i;\nconst REGISTRY_INSTALL_PATTERN = /(?:^|\\n)\\s*npx\\s+shadcn@latest\\s+add\\b/i;\nconst PACKAGE_INSTALL_PATTERN =\n  /(?:^|\\n)\\s*(?:npm|pnpm|yarn|bun)\\s+(?:install|add)\\b/i;\n\nexport function detectInstallSnippetType(\n  code: string,\n): InstallSnippetType | null {\n  if (SKILLS_INSTALL_PATTERN.test(code)) {\n    return \"skills\";\n  }\n\n  if (TOOL_AGENT_PATTERN.test(code)) {\n    return \"tool_agent\";\n  }\n\n  if (REGISTRY_INSTALL_PATTERN.test(code)) {\n    return \"registry\";\n  }\n\n  if (PACKAGE_INSTALL_PATTERN.test(code)) {\n    return \"package_manager\";\n  }\n\n  return null;\n}\n\nexport function getDocsCodeCopySource(\n  installSnippetType: InstallSnippetType | null,\n): \"docs_installation\" | \"docs_code_block\" {\n  return installSnippetType ? \"docs_installation\" : \"docs_code_block\";\n}\n"
  },
  {
    "path": "apps/www/lib/docs/preview-config.tsx",
    "content": "\"use client\";\n\nimport dynamic from \"next/dynamic\";\nimport { useState } from \"react\";\nimport type { ComponentProps, ComponentType, ReactNode } from \"react\";\nimport type { PresetWithCodeGen } from \"@/lib/presets/types\";\nimport type { ComponentId } from \"@/lib/docs/component-ids\";\n\nimport type { ApprovalCard } from \"@/components/tool-ui/approval-card\";\nimport type { Chart } from \"@/components/tool-ui/chart\";\nimport type { Citation } from \"@/components/tool-ui/citation\";\nimport type { CodeBlockComposedProps } from \"@/components/tool-ui/code-block\";\nimport { CodeBlock } from \"@/components/tool-ui/code-block\";\nimport type { CodeDiffComposedProps } from \"@/components/tool-ui/code-diff\";\nimport type { DataTable } from \"@/components/tool-ui/data-table\";\nimport type { GeoMap } from \"@/components/tool-ui/geo-map\";\nimport type { Image } from \"@/components/tool-ui/image\";\nimport type { ImageGallery } from \"@/components/tool-ui/image-gallery\";\nimport type { Video } from \"@/components/tool-ui/video\";\nimport type { Audio } from \"@/components/tool-ui/audio\";\nimport type { InstagramPost } from \"@/components/tool-ui/instagram-post\";\nimport type { LinkPreview } from \"@/components/tool-ui/link-preview\";\nimport type { LinkedInPost } from \"@/components/tool-ui/linkedin-post\";\nimport type { MessageDraft } from \"@/components/tool-ui/message-draft\";\nimport type { ItemCarousel } from \"@/components/tool-ui/item-carousel\";\nimport type { OptionList } from \"@/components/tool-ui/option-list\";\nimport type { OrderSummary } from \"@/components/tool-ui/order-summary\";\nimport type { ParameterSlider } from \"@/components/tool-ui/parameter-slider\";\nimport type { SerializablePlan } from \"@/components/tool-ui/plan\";\nimport type {\n  SerializablePreferencesPanel,\n  SerializablePreferencesPanelReceipt,\n} from \"@/components/tool-ui/preferences-panel\";\nimport type { ProgressTracker } from \"@/components/tool-ui/progress-tracker\";\nimport type { StatsDisplay } from \"@/components/tool-ui/stats-display\";\nimport type { Terminal } from \"@/components/tool-ui/terminal\";\nimport type { QuestionFlow } from \"@/components/tool-ui/question-flow\";\nimport type { WeatherWidget } from \"@/components/tool-ui/weather-widget/runtime\";\nimport type { XPost } from \"@/components/tool-ui/x-post\";\nimport {\n  ToolUI,\n  createDecisionResult,\n  type Action,\n  type DecisionAction,\n} from \"@/components/tool-ui/shared\";\n\nimport {\n  approvalCardPresets,\n  type ApprovalCardPresetName,\n} from \"@/lib/presets/approval-card\";\nimport { chartPresets, type ChartPresetName } from \"@/lib/presets/chart\";\nimport {\n  citationPresets,\n  type CitationPresetName,\n} from \"@/lib/presets/citation\";\nimport {\n  codeBlockPresets,\n  type CodeBlockPresetName,\n} from \"@/lib/presets/code-block\";\nimport {\n  codeDiffPresets,\n  type CodeDiffPresetName,\n} from \"@/lib/presets/code-diff\";\nimport {\n  dataTablePresets,\n  type DataTablePresetName,\n  type SortState,\n} from \"@/lib/presets/data-table\";\nimport { geoMapPresets, type GeoMapPresetName } from \"@/lib/presets/geo-map\";\nimport { imagePresets, type ImagePresetName } from \"@/lib/presets/image\";\nimport {\n  imageGalleryPresets,\n  type ImageGalleryPresetName,\n} from \"@/lib/presets/image-gallery\";\nimport { videoPresets, type VideoPresetName } from \"@/lib/presets/video\";\nimport { audioPresets, type AudioPresetName } from \"@/lib/presets/audio\";\nimport {\n  instagramPostPresets,\n  type InstagramPostPresetName,\n} from \"@/lib/presets/instagram-post\";\nimport {\n  linkPreviewPresets,\n  type LinkPreviewPresetName,\n} from \"@/lib/presets/link-preview\";\nimport {\n  linkedInPostPresets,\n  type LinkedInPostPresetName,\n} from \"@/lib/presets/linkedin-post\";\nimport {\n  messageDraftPresets,\n  type MessageDraftPresetName,\n} from \"@/lib/presets/message-draft\";\nimport {\n  itemCarouselPresets,\n  type ItemCarouselPresetName,\n} from \"@/lib/presets/item-carousel\";\nimport {\n  optionListPresets,\n  type OptionListPresetName,\n} from \"@/lib/presets/option-list\";\nimport {\n  orderSummaryPresets,\n  type OrderSummaryPresetName,\n} from \"@/lib/presets/order-summary\";\nimport {\n  parameterSliderPresets,\n  type ParameterSliderPresetName,\n} from \"@/lib/presets/parameter-slider\";\nimport { planPresets, type PlanPresetName } from \"@/lib/presets/plan\";\nimport {\n  preferencesPanelPresets,\n  type PreferencesPanelPresetName,\n} from \"@/lib/presets/preferences-panel\";\nimport {\n  progressTrackerPresets,\n  type ProgressTrackerPresetName,\n} from \"@/lib/presets/progress-tracker\";\nimport {\n  statsDisplayPresets,\n  type StatsDisplayPresetName,\n} from \"@/lib/presets/stats-display\";\nimport {\n  terminalPresets,\n  type TerminalPresetName,\n} from \"@/lib/presets/terminal\";\nimport {\n  questionFlowPresets,\n  type QuestionFlowPresetName,\n} from \"@/lib/presets/question-flow\";\nimport {\n  weatherWidgetPresets,\n  type WeatherWidgetPresetName,\n} from \"@/lib/presets/weather-widget\";\nimport { xPostPresets, type XPostPresetName } from \"@/lib/presets/x-post\";\nimport type { SerializableUpfrontMode } from \"@/components/tool-ui/question-flow\";\n\nconst DynamicApprovalCard = dynamic(() =>\n  import(\"@/components/tool-ui/approval-card\").then((m) => m.ApprovalCard),\n);\nconst DynamicChart = dynamic(() =>\n  import(\"@/components/tool-ui/chart\").then((m) => m.Chart),\n);\nconst DynamicCitation = dynamic(() =>\n  import(\"@/components/tool-ui/citation\").then((m) => m.Citation),\n);\nconst DynamicCitationList = dynamic(() =>\n  import(\"@/components/tool-ui/citation\").then((m) => m.CitationList),\n);\nconst DynamicCodeDiff = dynamic(() =>\n  import(\"@/components/tool-ui/code-diff\").then((m) => m.CodeDiff),\n);\nconst DynamicDataTable = dynamic(() =>\n  import(\"@/components/tool-ui/data-table\").then((m) => m.DataTable),\n);\nconst DynamicGeoMap = dynamic(\n  () => import(\"@/components/tool-ui/geo-map\").then((m) => m.GeoMap),\n  {\n    ssr: false,\n  },\n);\nconst DynamicImage = dynamic(() =>\n  import(\"@/components/tool-ui/image\").then((m) => m.Image),\n);\nconst DynamicImageGallery = dynamic(() =>\n  import(\"@/components/tool-ui/image-gallery\").then((m) => m.ImageGallery),\n);\nconst DynamicVideo = dynamic(() =>\n  import(\"@/components/tool-ui/video\").then((m) => m.Video),\n);\nconst DynamicAudio = dynamic(() =>\n  import(\"@/components/tool-ui/audio\").then((m) => m.Audio),\n);\nconst DynamicInstagramPost = dynamic(() =>\n  import(\"@/components/tool-ui/instagram-post\").then((m) => m.InstagramPost),\n);\nconst DynamicLinkPreview = dynamic(() =>\n  import(\"@/components/tool-ui/link-preview\").then((m) => m.LinkPreview),\n);\nconst DynamicLinkedInPost = dynamic(() =>\n  import(\"@/components/tool-ui/linkedin-post\").then((m) => m.LinkedInPost),\n);\nconst DynamicMessageDraft = dynamic(() =>\n  import(\"@/components/tool-ui/message-draft\").then((m) => m.MessageDraft),\n);\nconst DynamicItemCarousel = dynamic(() =>\n  import(\"@/components/tool-ui/item-carousel\").then((m) => m.ItemCarousel),\n);\nconst DynamicOptionList = dynamic(() =>\n  import(\"@/components/tool-ui/option-list\").then((m) => m.OptionList),\n);\nconst DynamicOrderSummary = dynamic(() =>\n  import(\"@/components/tool-ui/order-summary\").then((m) => m.OrderSummary),\n);\nconst DynamicParameterSlider = dynamic(() =>\n  import(\"@/components/tool-ui/parameter-slider\").then(\n    (m) => m.ParameterSlider,\n  ),\n);\nconst DynamicPlan = dynamic(() =>\n  import(\"@/components/tool-ui/plan\").then((m) => m.Plan),\n);\nconst DynamicPlanCompact = dynamic(() =>\n  import(\"@/components/tool-ui/plan\").then((m) => m.PlanCompact),\n);\nconst DynamicPreferencesPanel = dynamic(() =>\n  import(\"@/components/tool-ui/preferences-panel\").then(\n    (m) => m.PreferencesPanel,\n  ),\n);\nconst DynamicPreferencesPanelReceipt = dynamic(() =>\n  import(\"@/components/tool-ui/preferences-panel\").then(\n    (m) => m.PreferencesPanelReceipt,\n  ),\n);\nconst DynamicProgressTracker = dynamic(() =>\n  import(\"@/components/tool-ui/progress-tracker\").then(\n    (m) => m.ProgressTracker,\n  ),\n);\nconst DynamicStatsDisplay = dynamic(() =>\n  import(\"@/components/tool-ui/stats-display\").then((m) => m.StatsDisplay),\n);\nconst DynamicTerminal = dynamic(() =>\n  import(\"@/components/tool-ui/terminal\").then((m) => m.Terminal),\n);\nconst DynamicQuestionFlow = dynamic(() =>\n  import(\"@/components/tool-ui/question-flow\").then((m) => m.QuestionFlow),\n);\nconst DynamicWeatherWidget = dynamic(() =>\n  import(\"@/components/tool-ui/weather-widget/runtime\").then(\n    (m) => m.WeatherWidget,\n  ),\n);\nconst DynamicXPost = dynamic(() =>\n  import(\"@/components/tool-ui/x-post\").then((m) => m.XPost),\n);\n\nfunction QuestionFlowUpfrontWithReceipt({\n  data,\n}: {\n  data: SerializableUpfrontMode;\n}) {\n  const [completedAnswers, setCompletedAnswers] = useState<Record<\n    string,\n    string[]\n  > | null>(null);\n\n  if (completedAnswers) {\n    const summary = data.steps.map((step) => {\n      const selectedIds = completedAnswers[step.id] ?? [];\n      const selectedLabels = selectedIds\n        .map((id) => step.options.find((opt) => opt.id === id)?.label ?? id)\n        .join(\", \");\n      return {\n        label: step.title.replace(/^(Select|Choose)\\s+(a\\s+|your\\s+)?/i, \"\"),\n        value: selectedLabels || \"None\",\n      };\n    });\n\n    const title = data.id\n      .replace(/^question-flow-?/, \"\")\n      .replace(/-/g, \" \")\n      .replace(/\\b\\w/g, (c) => c.toUpperCase());\n\n    return (\n      <DynamicQuestionFlow\n        id={data.id}\n        choice={{\n          title,\n          summary,\n        }}\n      />\n    );\n  }\n\n  return (\n    <DynamicQuestionFlow\n      {...data}\n      onComplete={(answers: Record<string, string[]>) =>\n        setCompletedAnswers(answers)\n      }\n    />\n  );\n}\n\nexport interface ChatContext {\n  userMessage: string;\n  preamble?: string;\n}\n\nexport interface PreviewConfig<TData, TPresetName extends string> {\n  presets: Record<TPresetName, PresetWithCodeGen<TData>>;\n  defaultPreset: TPresetName;\n  renderComponent: (props: {\n    data: TData;\n    presetName: TPresetName;\n    state: PreviewState;\n    setState: (state: Partial<PreviewState>) => void;\n  }) => ReactNode;\n  wrapper?: ComponentType<{ children: ReactNode }>;\n  chatContext: ChatContext;\n}\n\nexport interface PreviewState {\n  sort?: SortState;\n  selection?: unknown;\n}\n\nfunction toRecord(value: unknown): Record<string, unknown> | null {\n  return typeof value === \"object\" && value !== null\n    ? (value as Record<string, unknown>)\n    : null;\n}\n\nfunction resolveActionItems(actions: unknown): Action[] | null {\n  if (Array.isArray(actions)) {\n    return actions as Action[];\n  }\n\n  const record = toRecord(actions);\n  const items = record?.[\"items\"];\n  return Array.isArray(items) ? (items as Action[]) : null;\n}\n\nfunction resolveLocalActionItems(data: unknown): Action[] | null {\n  const record = toRecord(data);\n  if (!record) return null;\n  return resolveActionItems(record[\"localActions\"]);\n}\n\nfunction renderWithLocalActions(\n  id: string,\n  surface: ReactNode,\n  localActions: Action[] | null,\n) {\n  if (!localActions || localActions.length === 0) {\n    return surface;\n  }\n\n  return (\n    <ToolUI id={id}>\n      <ToolUI.Surface>{surface}</ToolUI.Surface>\n      <ToolUI.Actions>\n        <ToolUI.LocalActions\n          actions={localActions}\n          onAction={(actionId) => console.log(\"Local action:\", actionId)}\n        />\n      </ToolUI.Actions>\n    </ToolUI>\n  );\n}\n\nfunction MaxWidthWrapper({ children }: { children: ReactNode }) {\n  return <div className=\"mx-auto w-full max-w-md\">{children}</div>;\n}\n\nfunction MaxWidthSmWrapper({ children }: { children: ReactNode }) {\n  return <div className=\"mx-auto w-full max-w-sm\">{children}</div>;\n}\n\nfunction MaxWidthSmStartWrapper({ children }: { children: ReactNode }) {\n  return <div className=\"w-full max-w-sm\">{children}</div>;\n}\n\nfunction MaxWidthStartWrapper({ children }: { children: ReactNode }) {\n  return <div className=\"w-full max-w-md\">{children}</div>;\n}\n\nexport const previewConfigs: Record<\n  ComponentId,\n  PreviewConfig<unknown, string>\n> = {\n  \"approval-card\": {\n    presets: approvalCardPresets as Record<string, PresetWithCodeGen<unknown>>,\n    defaultPreset: \"deploy\" satisfies ApprovalCardPresetName,\n    wrapper: MaxWidthSmWrapper,\n    chatContext: {\n      userMessage: \"Deploy the latest changes to production\",\n      preamble: \"I'll need your confirmation before proceeding:\",\n    },\n    renderComponent: ({ data, state, setState }) => {\n      const cardData = data as ComponentProps<typeof ApprovalCard>;\n      const choice = state.selection as \"approved\" | \"denied\" | undefined;\n      return (\n        <DynamicApprovalCard\n          {...cardData}\n          choice={cardData.choice ?? choice}\n          onConfirm={() => setState({ selection: \"approved\" })}\n          onCancel={() => setState({ selection: \"denied\" })}\n        />\n      );\n    },\n  },\n  chart: {\n    presets: chartPresets as Record<string, PresetWithCodeGen<unknown>>,\n    defaultPreset: \"revenue\" satisfies ChartPresetName,\n    chatContext: {\n      userMessage: \"Show me the revenue data for this quarter\",\n      preamble: \"Here's your data visualization:\",\n    },\n    renderComponent: ({ data, presetName }) => {\n      const chartData = data as Omit<Parameters<typeof Chart>[0], \"id\">;\n      return <DynamicChart id={`chart-${presetName}`} {...chartData} />;\n    },\n  },\n  \"geo-map\": {\n    presets: geoMapPresets as Record<string, PresetWithCodeGen<unknown>>,\n    defaultPreset: \"fleet\" satisfies GeoMapPresetName,\n    chatContext: {\n      userMessage: \"Where are our active trucks right now?\",\n      preamble: \"Here's the current location map:\",\n    },\n    renderComponent: ({ data, presetName }) => {\n      const geoMapData = data as Omit<Parameters<typeof GeoMap>[0], \"id\">;\n      return <DynamicGeoMap id={`geo-map-${presetName}`} {...geoMapData} />;\n    },\n  },\n  citation: {\n    presets: citationPresets as Record<string, PresetWithCodeGen<unknown>>,\n    defaultPreset: \"stacked\" satisfies CitationPresetName,\n    chatContext: {\n      userMessage: \"What does the documentation say about this?\",\n      preamble: \"According to the source:\",\n    },\n    renderComponent: ({ data, presetName }) => {\n      const { citations, variant, maxVisible } = data as {\n        citations: Parameters<typeof Citation>[0][];\n        variant?: Parameters<typeof Citation>[0][\"variant\"];\n        maxVisible?: number;\n      };\n      const localActionItems = resolveLocalActionItems(data);\n\n      const wrapperClass =\n        variant === \"inline\" ? \"mx-auto max-w-xl\" : \"mx-auto max-w-lg\";\n\n      // Single citation without list\n      if (citations.length === 1 && !maxVisible) {\n        const citation = citations[0];\n\n        return renderWithLocalActions(\n          citation.id,\n          <div className=\"mx-auto flex w-full max-w-md flex-col gap-3\">\n            <DynamicCitation {...citation} variant={variant} />\n          </div>,\n          localActionItems,\n        );\n      }\n\n      // Multiple citations or truncated list\n      return (\n        <div className={wrapperClass}>\n          <DynamicCitationList\n            id={`citation-list-${presetName}`}\n            citations={citations}\n            variant={variant}\n            maxVisible={maxVisible}\n          />\n        </div>\n      );\n    },\n  },\n  \"code-block\": {\n    presets: codeBlockPresets as Record<string, PresetWithCodeGen<unknown>>,\n    defaultPreset: \"typescript\" satisfies CodeBlockPresetName,\n    chatContext: {\n      userMessage: \"Write me a utility function for this\",\n      preamble: \"Here's the code:\",\n    },\n    renderComponent: ({ data }) => {\n      const codeBlock = data as CodeBlockComposedProps;\n      return <CodeBlock {...codeBlock} />;\n    },\n  },\n  \"code-diff\": {\n    presets: codeDiffPresets as Record<string, PresetWithCodeGen<unknown>>,\n    defaultPreset: \"refactor\" satisfies CodeDiffPresetName,\n    chatContext: {\n      userMessage: \"Can you make fetchUser return null instead of throwing?\",\n      preamble: \"Here's the updated function:\",\n    },\n    renderComponent: ({ data }) => {\n      const diffData = data as CodeDiffComposedProps;\n      return <DynamicCodeDiff {...diffData} />;\n    },\n  },\n  \"data-table\": {\n    presets: dataTablePresets as Record<string, PresetWithCodeGen<unknown>>,\n    defaultPreset: \"stocks\" satisfies DataTablePresetName,\n    chatContext: {\n      userMessage: \"Show me the data in a table\",\n      preamble: \"Here's what I found:\",\n    },\n    renderComponent: ({ data, state, setState }) => {\n      const tableData = data as Parameters<typeof DataTable>[0];\n      const localActionItems = resolveLocalActionItems(data);\n      return renderWithLocalActions(\n        tableData.id,\n        <div className=\"flex w-full flex-col gap-4\">\n          <DynamicDataTable\n            {...tableData}\n            sort={state.sort as Parameters<typeof DataTable>[0][\"sort\"]}\n            onSortChange={(sort) => setState({ sort })}\n          />\n        </div>,\n        localActionItems,\n      );\n    },\n  },\n  image: {\n    presets: imagePresets as Record<string, PresetWithCodeGen<unknown>>,\n    defaultPreset: \"with-source\" satisfies ImagePresetName,\n    wrapper: MaxWidthWrapper,\n    chatContext: {\n      userMessage: \"Generate an image of a sunset over mountains\",\n      preamble: \"Here's what I created:\",\n    },\n    renderComponent: ({ data }) => {\n      const { image } = data as {\n        image: Parameters<typeof Image>[0];\n      };\n      const localActionItems = resolveLocalActionItems(data);\n      return renderWithLocalActions(\n        image.id,\n        <div className=\"flex flex-col gap-3\">\n          <DynamicImage {...image} />\n        </div>,\n        localActionItems,\n      );\n    },\n  },\n  \"image-gallery\": {\n    presets: imageGalleryPresets as Record<string, PresetWithCodeGen<unknown>>,\n    defaultPreset: \"search-results\" satisfies ImageGalleryPresetName,\n    chatContext: {\n      userMessage: \"Find me some reference images\",\n      preamble: \"Here are some images I found:\",\n    },\n    renderComponent: ({ data }) => {\n      const galleryData = data as Parameters<typeof ImageGallery>[0];\n      return (\n        <DynamicImageGallery\n          {...galleryData}\n          onImageClick={(id, image) => console.log(\"Image clicked:\", id, image)}\n        />\n      );\n    },\n  },\n  video: {\n    presets: videoPresets as Record<string, PresetWithCodeGen<unknown>>,\n    defaultPreset: \"with-poster\" satisfies VideoPresetName,\n    wrapper: MaxWidthWrapper,\n    chatContext: {\n      userMessage: \"Find that video tutorial\",\n      preamble: \"Here's the video:\",\n    },\n    renderComponent: ({ data }) => {\n      const { video } = data as {\n        video: Parameters<typeof Video>[0];\n      };\n      const localActionItems = resolveLocalActionItems(data);\n      return renderWithLocalActions(\n        video.id,\n        <div className=\"flex flex-col gap-3\">\n          <DynamicVideo {...video} />\n        </div>,\n        localActionItems,\n      );\n    },\n  },\n  audio: {\n    presets: audioPresets as Record<string, PresetWithCodeGen<unknown>>,\n    defaultPreset: \"full\" satisfies AudioPresetName,\n    wrapper: MaxWidthSmWrapper,\n    chatContext: {\n      userMessage: \"Play that song we talked about\",\n      preamble: \"Here it is:\",\n    },\n    renderComponent: ({ data }) => {\n      const { audio, variant } = data as {\n        audio: Parameters<typeof Audio>[0];\n        variant?: \"full\" | \"compact\";\n      };\n      const localActionItems = resolveLocalActionItems(data);\n      return renderWithLocalActions(\n        audio.id,\n        <div className=\"flex flex-col gap-3\">\n          <DynamicAudio {...audio} variant={variant} />\n        </div>,\n        localActionItems,\n      );\n    },\n  },\n  \"instagram-post\": {\n    presets: instagramPostPresets as Record<string, PresetWithCodeGen<unknown>>,\n    defaultPreset: \"basic\" satisfies InstagramPostPresetName,\n    wrapper: MaxWidthSmWrapper,\n    chatContext: {\n      userMessage: \"Draft an Instagram post for this launch\",\n      preamble: \"Here's the post preview:\",\n    },\n    renderComponent: ({ data }) => {\n      const instagramData = data as {\n        post: Parameters<typeof InstagramPost>[0][\"post\"];\n      };\n      const localActionItems = resolveLocalActionItems(data);\n      return renderWithLocalActions(\n        instagramData.post.id,\n        <div className=\"flex flex-col gap-3\">\n          <DynamicInstagramPost post={instagramData.post} />\n        </div>,\n        localActionItems,\n      );\n    },\n  },\n  \"link-preview\": {\n    presets: linkPreviewPresets as Record<string, PresetWithCodeGen<unknown>>,\n    defaultPreset: \"with-image\" satisfies LinkPreviewPresetName,\n    wrapper: MaxWidthWrapper,\n    chatContext: {\n      userMessage: \"Find that article from earlier\",\n      preamble: \"Was it this one?\",\n    },\n    renderComponent: ({ data }) => {\n      const { linkPreview } = data as {\n        linkPreview: Parameters<typeof LinkPreview>[0];\n      };\n      const localActionItems = resolveLocalActionItems(data);\n      return renderWithLocalActions(\n        linkPreview.id,\n        <div className=\"flex flex-col gap-3\">\n          <DynamicLinkPreview {...linkPreview} />\n        </div>,\n        localActionItems,\n      );\n    },\n  },\n  \"linkedin-post\": {\n    presets: linkedInPostPresets as Record<string, PresetWithCodeGen<unknown>>,\n    defaultPreset: \"basic\" satisfies LinkedInPostPresetName,\n    wrapper: MaxWidthSmWrapper,\n    chatContext: {\n      userMessage: \"Create a LinkedIn update about the release\",\n      preamble: \"Here's the LinkedIn post preview:\",\n    },\n    renderComponent: ({ data }) => {\n      const linkedInData = data as {\n        post: Parameters<typeof LinkedInPost>[0][\"post\"];\n      };\n      const localActionItems = resolveLocalActionItems(data);\n      return renderWithLocalActions(\n        linkedInData.post.id,\n        <div className=\"flex flex-col gap-3\">\n          <DynamicLinkedInPost post={linkedInData.post} />\n        </div>,\n        localActionItems,\n      );\n    },\n  },\n  \"message-draft\": {\n    presets: messageDraftPresets as Record<string, PresetWithCodeGen<unknown>>,\n    defaultPreset: \"email\" satisfies MessageDraftPresetName,\n    wrapper: MaxWidthStartWrapper,\n    chatContext: {\n      userMessage: \"Send Marcus the updated proposal\",\n      preamble: \"I've drafted this message for your review:\",\n    },\n    renderComponent: ({ data }) => {\n      const draftData = data as Parameters<typeof MessageDraft>[0];\n      return (\n        <DynamicMessageDraft\n          {...draftData}\n          onSend={() => console.log(\"Message sent\")}\n          onUndo={() => console.log(\"Send undone\")}\n          onCancel={() => console.log(\"Message cancelled\")}\n        />\n      );\n    },\n  },\n  \"option-list\": {\n    presets: optionListPresets as Record<string, PresetWithCodeGen<unknown>>,\n    defaultPreset: \"max-selections\" satisfies OptionListPresetName,\n    wrapper: MaxWidthWrapper,\n    chatContext: {\n      userMessage: \"Help me pick between these options\",\n      preamble: \"What sounds good?\",\n    },\n    renderComponent: ({ data, state, setState }) => {\n      const listData = data as Parameters<typeof OptionList>[0];\n      return (\n        <DynamicOptionList\n          {...listData}\n          id=\"option-list-preview\"\n          value={state.selection as Parameters<typeof OptionList>[0][\"value\"]}\n          onChange={(selection) => setState({ selection })}\n          onAction={(actionId, selection) => {\n            if (actionId !== \"confirm\") return;\n            console.log(\"OptionList confirmed:\", selection);\n            alert(`Selection confirmed: ${JSON.stringify(selection)}`);\n          }}\n        />\n      );\n    },\n  },\n  \"order-summary\": {\n    presets: orderSummaryPresets as Record<string, PresetWithCodeGen<unknown>>,\n    defaultPreset: \"default\" satisfies OrderSummaryPresetName,\n    wrapper: MaxWidthWrapper,\n    chatContext: {\n      userMessage: \"Place that order we discussed\",\n      preamble: \"Here's the order summary for your review:\",\n    },\n    renderComponent: ({ data, state, setState }) => {\n      const orderData = data as Parameters<typeof OrderSummary>[0];\n      const decisionActions = (resolveActionItems(\n        toRecord(data)?.[\"decisionActions\"],\n      ) as DecisionAction[] | null) ?? [\n        { id: \"cancel\", label: \"Cancel\", variant: \"outline\" },\n        { id: \"confirm\", label: \"Purchase\", variant: \"default\" },\n      ];\n      const receiptChoice =\n        (state.selection as\n          | Parameters<typeof OrderSummary>[0][\"choice\"]\n          | undefined) ?? undefined;\n\n      if (orderData.choice ?? receiptChoice) {\n        return (\n          <DynamicOrderSummary\n            {...orderData}\n            choice={orderData.choice ?? receiptChoice}\n          />\n        );\n      }\n\n      return (\n        <ToolUI id={orderData.id}>\n          <ToolUI.Surface>\n            <DynamicOrderSummary {...orderData} />\n          </ToolUI.Surface>\n          <ToolUI.Actions>\n            <ToolUI.DecisionActions\n              actions={decisionActions}\n              onAction={(action) =>\n                createDecisionResult({\n                  decisionId: `${orderData.id}-decision`,\n                  action,\n                })\n              }\n              onCommit={(result) => {\n                if (result.actionId === \"confirm\") {\n                  setState({\n                    selection: {\n                      action: \"confirm\",\n                      orderId: `ORD-${Date.now().toString().slice(-6)}`,\n                      confirmedAt: result.at,\n                    },\n                  });\n                  return;\n                }\n\n                setState({ selection: null });\n              }}\n            />\n          </ToolUI.Actions>\n        </ToolUI>\n      );\n    },\n  },\n  \"parameter-slider\": {\n    presets: parameterSliderPresets as Record<\n      string,\n      PresetWithCodeGen<unknown>\n    >,\n    defaultPreset: \"audio-eq\" satisfies ParameterSliderPresetName,\n    wrapper: MaxWidthSmStartWrapper,\n    chatContext: {\n      userMessage: \"Boost the bass a bit on this track\",\n      preamble: \"Here are the current EQ settings:\",\n    },\n    renderComponent: ({ data, presetName }) => {\n      const sliderData = data as Parameters<typeof ParameterSlider>[0];\n      const isAudioEq = presetName === \"audio-eq\";\n\n      // Apply per-slider neon theming for audio-eq preset\n      const themedSliders = isAudioEq\n        ? sliderData.sliders.map((slider, i) => ({\n            ...slider,\n            fillClassName: [\n              \"bg-cyan-500/40\",\n              \"bg-fuchsia-500/40\",\n              \"bg-amber-500/40\",\n            ][i],\n            handleClassName: [\"bg-cyan-400\", \"bg-fuchsia-400\", \"bg-amber-400\"][\n              i\n            ],\n          }))\n        : sliderData.sliders;\n\n      return (\n        <DynamicParameterSlider\n          {...sliderData}\n          sliders={themedSliders}\n          onAction={(actionId, values) =>\n            console.log(\"Action:\", actionId, \"Values:\", values)\n          }\n        />\n      );\n    },\n  },\n  plan: {\n    presets: planPresets as Record<string, PresetWithCodeGen<unknown>>,\n    defaultPreset: \"simple\" satisfies PlanPresetName,\n    chatContext: {\n      userMessage: \"Help me plan out this project\",\n      preamble: \"Here's what I'm working on:\",\n    },\n    renderComponent: ({ data, presetName }) => {\n      const planData = data as SerializablePlan;\n      if (presetName === \"compact\") {\n        return <DynamicPlanCompact {...planData} />;\n      }\n      return <DynamicPlan {...planData} />;\n    },\n  },\n  \"preferences-panel\": {\n    presets: preferencesPanelPresets as Record<\n      string,\n      PresetWithCodeGen<unknown>\n    >,\n    defaultPreset: \"notifications\" satisfies PreferencesPanelPresetName,\n    wrapper: MaxWidthStartWrapper,\n    chatContext: {\n      userMessage: \"Update my notification settings\",\n      preamble: \"Here are your current preferences:\",\n    },\n    renderComponent: ({ data, state, setState }) => {\n      const panelData = data as\n        | SerializablePreferencesPanel\n        | SerializablePreferencesPanelReceipt;\n\n      if (\"choice\" in panelData) {\n        return <DynamicPreferencesPanelReceipt {...panelData} />;\n      }\n\n      return (\n        <DynamicPreferencesPanel\n          {...panelData}\n          value={\n            state.selection as Record<string, string | boolean> | undefined\n          }\n          onChange={(value) =>\n            setState({\n              selection: value as unknown as string[] | string | null,\n            })\n          }\n          onAction={async (actionId, values) =>\n            console.log(\"Action:\", actionId, \"Values:\", values)\n          }\n        />\n      );\n    },\n  },\n  \"progress-tracker\": {\n    presets: progressTrackerPresets as Record<\n      string,\n      PresetWithCodeGen<unknown>\n    >,\n    defaultPreset: \"in-progress\" satisfies ProgressTrackerPresetName,\n    wrapper: MaxWidthSmStartWrapper,\n    chatContext: {\n      userMessage: \"Deploy the application to production\",\n      preamble: \"Starting deployment process:\",\n    },\n    renderComponent: ({ data }) => {\n      const trackerData = data as Parameters<typeof ProgressTracker>[0];\n      return <DynamicProgressTracker {...trackerData} />;\n    },\n  },\n  \"stats-display\": {\n    presets: statsDisplayPresets as Record<string, PresetWithCodeGen<unknown>>,\n    defaultPreset: \"business-metrics\" satisfies StatsDisplayPresetName,\n    chatContext: {\n      userMessage: \"Show me the key metrics for this quarter\",\n      preamble: \"Here's your performance summary:\",\n    },\n    renderComponent: ({ data }) => {\n      const statsData = data as Parameters<typeof StatsDisplay>[0];\n      return <DynamicStatsDisplay {...statsData} />;\n    },\n  },\n  \"item-carousel\": {\n    presets: itemCarouselPresets as Record<string, PresetWithCodeGen<unknown>>,\n    defaultPreset: \"recommendations\" satisfies ItemCarouselPresetName,\n    chatContext: {\n      userMessage: \"What should I listen to right now?\",\n      preamble: \"Here are some recommendations:\",\n    },\n    renderComponent: ({ data }) => (\n      <DynamicItemCarousel\n        {...(data as Parameters<typeof ItemCarousel>[0])}\n        onItemClick={(itemId) => console.log(\"Item clicked:\", itemId)}\n        onItemAction={(itemId, actionId) =>\n          console.log(\"Item action:\", itemId, actionId)\n        }\n      />\n    ),\n  },\n  terminal: {\n    presets: terminalPresets as Record<string, PresetWithCodeGen<unknown>>,\n    defaultPreset: \"success\" satisfies TerminalPresetName,\n    chatContext: {\n      userMessage: \"Run the tests\",\n      preamble: \"Running tests...\",\n    },\n    renderComponent: ({ data }) => {\n      const terminalData = data as Parameters<typeof Terminal>[0];\n      const localActionItems = resolveLocalActionItems(data);\n\n      return renderWithLocalActions(\n        terminalData.id,\n        <div className=\"flex w-full flex-col gap-3\">\n          <DynamicTerminal {...terminalData} />\n        </div>,\n        localActionItems,\n      );\n    },\n  },\n  \"question-flow\": {\n    presets: questionFlowPresets as Record<string, PresetWithCodeGen<unknown>>,\n    defaultPreset: \"upfront\" satisfies QuestionFlowPresetName,\n    wrapper: MaxWidthWrapper,\n    chatContext: {\n      userMessage: \"Help me set up a new project\",\n      preamble: \"Let's configure your project step by step:\",\n    },\n    renderComponent: ({ data, presetName }) => {\n      const flowData = data as Parameters<typeof QuestionFlow>[0];\n      const isUpfront = \"steps\" in flowData && flowData.steps !== undefined;\n      const isReceipt = \"choice\" in flowData && flowData.choice !== undefined;\n\n      if (isReceipt) {\n        return <DynamicQuestionFlow key={presetName} {...flowData} />;\n      }\n\n      if (isUpfront) {\n        return (\n          <QuestionFlowUpfrontWithReceipt\n            key={presetName}\n            data={flowData as SerializableUpfrontMode}\n          />\n        );\n      }\n\n      return (\n        <DynamicQuestionFlow\n          key={presetName}\n          {...flowData}\n          onSelect={(ids: string[]) => console.log(\"Selected:\", ids)}\n          onBack={() => console.log(\"Go back\")}\n        />\n      );\n    },\n  },\n  \"weather-widget\": {\n    presets: weatherWidgetPresets as Record<string, PresetWithCodeGen<unknown>>,\n    defaultPreset: \"thunderstorm\" satisfies WeatherWidgetPresetName,\n    wrapper: MaxWidthSmStartWrapper,\n    chatContext: {\n      userMessage: \"What's the weather like in San Diego?\",\n      preamble: \"Here's the current weather:\",\n    },\n    renderComponent: ({ data }) => (\n      <DynamicWeatherWidget\n        {...(data as Parameters<typeof WeatherWidget>[0])}\n      />\n    ),\n  },\n  \"x-post\": {\n    presets: xPostPresets as Record<string, PresetWithCodeGen<unknown>>,\n    defaultPreset: \"basic\" satisfies XPostPresetName,\n    wrapper: MaxWidthSmWrapper,\n    chatContext: {\n      userMessage: \"Write a post for X about today's launch\",\n      preamble: \"Here's the X post preview:\",\n    },\n    renderComponent: ({ data }) => {\n      const xPostData = data as {\n        post: Parameters<typeof XPost>[0][\"post\"];\n      };\n      const localActionItems = resolveLocalActionItems(data);\n      return renderWithLocalActions(\n        xPostData.post.id,\n        <div className=\"flex flex-col gap-3\">\n          <DynamicXPost post={xPostData.post} />\n        </div>,\n        localActionItems,\n      );\n    },\n  },\n};\n\nexport function getPreviewConfig(componentId: ComponentId) {\n  return previewConfigs[componentId];\n}\n\nexport type { ComponentId } from \"@/lib/docs/component-ids\";\n"
  },
  {
    "path": "apps/www/lib/eslint/tool-ui-action-model-plugin.ts",
    "content": "import type { Rule } from \"eslint\";\n\nconst RESPONSE_ACTION_IDENTIFIERS = new Set([\n  \"responseActions\",\n  \"onResponseAction\",\n  \"onBeforeResponseAction\",\n]);\n\nconst LOCAL_ACTION_ELEMENTS = new Set([\"LocalActions\", \"ToolUI.LocalActions\"]);\nconst DECISION_ACTION_ELEMENTS = new Set([\n  \"DecisionActions\",\n  \"ToolUI.DecisionActions\",\n]);\n\nfunction jsxNameToString(\n  name:\n    | {\n        type: \"JSXIdentifier\";\n        name: string;\n      }\n    | {\n        type: \"JSXMemberExpression\";\n        object: unknown;\n        property: unknown;\n      }\n    | {\n        type: \"JSXNamespacedName\";\n      },\n): string {\n  if (name.type === \"JSXIdentifier\") {\n    return name.name;\n  }\n\n  if (name.type === \"JSXMemberExpression\") {\n    const object =\n      name.object &&\n      typeof name.object === \"object\" &&\n      \"name\" in (name.object as Record<string, unknown>)\n        ? String((name.object as Record<string, unknown>).name)\n        : \"\";\n    const property =\n      name.property &&\n      typeof name.property === \"object\" &&\n      \"name\" in (name.property as Record<string, unknown>)\n        ? String((name.property as Record<string, unknown>).name)\n        : \"\";\n    return `${object}.${property}`;\n  }\n\n  return \"\";\n}\n\nconst noEmbeddedResponseActionsRule: Rule.RuleModule = {\n  meta: {\n    type: \"problem\",\n    docs: {\n      description:\n        \"Disallow legacy embedded action props on display component surfaces.\",\n      recommended: true,\n      url: \"https://tool-ui.com/docs/actions\",\n    },\n    schema: [],\n    messages: {\n      forbidden:\n        \"Embedded '{{name}}' is deprecated. Use sibling LocalActions or DecisionActions surfaces instead.\",\n    },\n  },\n  create(context) {\n    return {\n      Identifier(node) {\n        if (RESPONSE_ACTION_IDENTIFIERS.has(node.name)) {\n          context.report({\n            node,\n            messageId: \"forbidden\",\n            data: { name: node.name },\n          });\n        }\n      },\n    };\n  },\n};\n\nconst noAddResultInLocalActionsRule: Rule.RuleModule = {\n  meta: {\n    type: \"problem\",\n    docs: {\n      description:\n        \"Disallow addResult(...) calls inside LocalActions onAction handlers.\",\n      recommended: true,\n      url: \"https://tool-ui.com/docs/actions\",\n    },\n    schema: [],\n    messages: {\n      forbidden:\n        \"LocalActions onAction handlers must not call addResult(...). Use DecisionActions for consequential commits.\",\n    },\n  },\n  create(context) {\n    const sourceCode = context.sourceCode;\n\n    return {\n      JSXAttribute(node) {\n        if (node.name.name !== \"onAction\") return;\n        const openingElement = node.parent;\n        if (\n          !openingElement ||\n          openingElement.type !== \"JSXOpeningElement\" ||\n          !LOCAL_ACTION_ELEMENTS.has(jsxNameToString(openingElement.name))\n        ) {\n          return;\n        }\n\n        if (\n          !node.value ||\n          node.value.type !== \"JSXExpressionContainer\" ||\n          !node.value.expression ||\n          (node.value.expression.type !== \"ArrowFunctionExpression\" &&\n            node.value.expression.type !== \"FunctionExpression\")\n        ) {\n          return;\n        }\n\n        const expressionText = sourceCode.getText(node.value.expression);\n        if (/\\baddResult\\s*\\(/.test(expressionText)) {\n          context.report({ node, messageId: \"forbidden\" });\n        }\n      },\n    };\n  },\n};\n\nconst decisionActionsRequireEnvelopeRule: Rule.RuleModule = {\n  meta: {\n    type: \"problem\",\n    docs: {\n      description:\n        \"Require DecisionActions onAction to return a createDecisionResult(...) envelope.\",\n      recommended: true,\n      url: \"https://tool-ui.com/docs/actions\",\n    },\n    schema: [],\n    messages: {\n      envelope:\n        \"DecisionActions onAction must create a typed envelope with createDecisionResult(...).\",\n    },\n  },\n  create(context) {\n    const sourceCode = context.sourceCode;\n\n    return {\n      JSXAttribute(node) {\n        if (node.name.name !== \"onAction\") return;\n        const openingElement = node.parent;\n        if (\n          !openingElement ||\n          openingElement.type !== \"JSXOpeningElement\" ||\n          !DECISION_ACTION_ELEMENTS.has(jsxNameToString(openingElement.name))\n        ) {\n          return;\n        }\n\n        if (\n          !node.value ||\n          node.value.type !== \"JSXExpressionContainer\" ||\n          !node.value.expression ||\n          (node.value.expression.type !== \"ArrowFunctionExpression\" &&\n            node.value.expression.type !== \"FunctionExpression\")\n        ) {\n          return;\n        }\n\n        const expressionText = sourceCode.getText(node.value.expression);\n        if (!/\\bcreateDecisionResult\\s*\\(/.test(expressionText)) {\n          context.report({ node, messageId: \"envelope\" });\n        }\n      },\n    };\n  },\n};\n\nexport const toolUiActionModelPlugin = {\n  rules: {\n    \"no-embedded-response-actions\": noEmbeddedResponseActionsRule,\n    \"no-add-result-in-local-actions\": noAddResultInLocalActionsRule,\n    \"decision-actions-require-envelope\": decisionActionsRequireEnvelopeRule,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/integrations/freestyle/create-chat.ts",
    "content": "\"use server\";\n\nimport { Freestyle, type Responses } from \"freestyle-sandboxes\";\n\nif (!process.env.FREESTYLE_API_KEY) {\n  console.warn(\n    \"FREESTYLE_API_KEY is not set. Freestyle features will not work.\",\n  );\n}\n\nconst freestyle = new Freestyle({\n  apiKey: process.env.FREESTYLE_API_KEY!,\n});\n\nasync function requestDevServerInternal(\n  repoId: string,\n): Promise<Responses.ResponsePostEphemeralV1DevServers200> {\n  const response = await freestyle.fetch(\"/ephemeral/v1/dev-servers\", {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({\n      devServer: { repoId },\n      preset: \"nextJs\",\n    }),\n  });\n\n  if (!response.ok) {\n    throw new Error(\n      `Failed to request dev server (${response.status} ${response.statusText})`,\n    );\n  }\n\n  return (await response.json()) as Responses.ResponsePostEphemeralV1DevServers200;\n}\n\nexport async function createChat() {\n  const { repoId } = await freestyle.git.repos.create({\n    name: `Tool UI Builder`,\n    public: true,\n    source: {\n      url: \"https://github.com/assistant-ui/tool-ui-sandbox-template\",\n    },\n    devServers: {\n      preset: \"nextJs\",\n    },\n  });\n\n  const { ephemeralUrl, mcpEphemeralUrl } =\n    await requestDevServerInternal(repoId);\n\n  return {\n    repoId,\n    ephemeralUrl,\n    mcpEphemeralUrl,\n  };\n}\n\nexport async function requestDevServer({ repoId }: { repoId: string }) {\n  const devServer = await requestDevServerInternal(repoId);\n\n  // Only return serializable data needed by the client\n  return {\n    ephemeralUrl: devServer.ephemeralUrl ?? devServer.url,\n    mcpEphemeralUrl: devServer.mcpEphemeralUrl ?? null,\n    devCommandRunning: devServer.devCommandRunning,\n    installCommandRunning: devServer.installCommandRunning,\n  };\n}\n"
  },
  {
    "path": "apps/www/lib/integrations/freestyle/get-code.ts",
    "content": "\"use server\";\n\nimport { Freestyle } from \"freestyle-sandboxes\";\n\nif (!process.env.FREESTYLE_API_KEY) {\n  console.warn(\n    \"FREESTYLE_API_KEY is not set. Freestyle features will not work.\",\n  );\n}\n\nconst freestyle = new Freestyle({\n  apiKey: process.env.FREESTYLE_API_KEY!,\n});\n\nasync function getFileFromRepo(\n  repoId: string,\n  filePath: string,\n): Promise<string> {\n  try {\n    const repo = freestyle.git.repos.ref({ repoId });\n    const response = await repo.contents.get({ path: filePath });\n\n    if (response.type !== \"file\") {\n      throw new Error(`Expected file at ${filePath}, got ${response.type}.`);\n    }\n\n    return Buffer.from(response.content, \"base64\").toString(\"utf-8\");\n  } catch (error) {\n    console.error(\"Failed to get file from repo:\", error);\n    throw new Error(\n      `Failed to read ${filePath} from repo: ${error instanceof Error ? error.message : String(error)}`,\n    );\n  }\n}\n\nexport async function getComponentCode(\n  repoId: string,\n  filePath: string = \"app/page.tsx\",\n): Promise<string> {\n  try {\n    return await getFileFromRepo(repoId, filePath);\n  } catch (error) {\n    console.error(\"Failed to get component code:\", error);\n    throw error;\n  }\n}\n"
  },
  {
    "path": "apps/www/lib/integrations/rate-limit/upstash.ts",
    "content": "import { Ratelimit } from \"@upstash/ratelimit\";\nimport { Redis } from \"@upstash/redis\";\n\n// Create rate limiter instance (optional - requires Upstash Redis)\nconst redis =\n  process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN\n    ? new Redis({\n        url: process.env.UPSTASH_REDIS_REST_URL,\n        token: process.env.UPSTASH_REDIS_REST_TOKEN,\n      })\n    : null;\n\nconst ratelimit = redis\n  ? new Ratelimit({\n      redis,\n      limiter: Ratelimit.slidingWindow(10, \"10 m\"), // 10 requests per 10 minutes per IP\n      analytics: true,\n      prefix: \"ai-chat\",\n    })\n  : null;\n\n/**\n * Check if a request should be rate limited\n * @param identifier - Usually IP address or user ID\n * @returns Object with success boolean and reset timestamp\n */\nexport async function checkRateLimit(identifier: string) {\n  if (!ratelimit) {\n    // Rate limiting not configured, allow all requests\n    console.warn(\n      \"Rate limiting is not configured. Set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN to enable.\",\n    );\n    return { success: true, limit: 0, remaining: 0, reset: 0 };\n  }\n\n  const result = await ratelimit.limit(identifier);\n\n  return result;\n}\n"
  },
  {
    "path": "apps/www/lib/mocks/chat-showcase-data.ts",
    "content": "import type { Column } from \"@/components/tool-ui/data-table\";\nimport type { SerializableLinkPreview } from \"@/components/tool-ui/link-preview\";\nimport type { SerializableChart } from \"@/components/tool-ui/chart\";\nimport type { XPostData } from \"@/components/tool-ui/x-post\";\nimport type { OptionListOption } from \"@/components/tool-ui/option-list\";\nimport type { SerializableTerminal } from \"@/components/tool-ui/terminal\";\nimport type { SerializableCodeBlock } from \"@/components/tool-ui/code-block\";\nimport type { SerializableItemCarousel } from \"@/components/tool-ui/item-carousel\";\nimport type { SerializableCitation } from \"@/components/tool-ui/citation\";\nimport type { SerializableParameterSlider } from \"@/components/tool-ui/parameter-slider\";\nimport type { SerializableStatsDisplay } from \"@/components/tool-ui/stats-display\";\nimport type { SerializableProgressTracker } from \"@/components/tool-ui/progress-tracker\";\n\nexport type Flight = {\n  id: string;\n  airline: string;\n  route: string;\n  departure: string;\n  duration: string;\n  stops: \"Nonstop\" | \"1 stop\" | \"2 stops\";\n  price: string;\n};\n\ntype BadgeColor = \"danger\" | \"warning\" | \"info\" | \"success\" | \"neutral\";\n\nexport const TABLE_COLUMNS: Column<Flight>[] = [\n  { key: \"airline\", label: \"Airline\", sortable: true, priority: \"primary\" },\n  { key: \"route\", label: \"Route\", sortable: false, priority: \"primary\" },\n  { key: \"departure\", label: \"Departs\", sortable: true, priority: \"secondary\" },\n  { key: \"duration\", label: \"Duration\", sortable: true, priority: \"secondary\" },\n  {\n    key: \"stops\",\n    label: \"Stops\",\n    sortable: true,\n    priority: \"primary\",\n    format: {\n      kind: \"badge\",\n      colorMap: {\n        Nonstop: \"success\",\n        \"1 stop\": \"warning\",\n        \"2 stops\": \"neutral\",\n      } as Record<string, BadgeColor>,\n    },\n  },\n  { key: \"price\", label: \"Price\", sortable: true, priority: \"primary\" },\n];\n\nexport const TABLE_DATA: Flight[] = [\n  {\n    id: \"fl-1\",\n    airline: \"ANA\",\n    route: \"LAX → NRT\",\n    departure: \"Mar 15, 11:30am\",\n    duration: \"11h 45m\",\n    stops: \"Nonstop\",\n    price: \"$892\",\n  },\n  {\n    id: \"fl-2\",\n    airline: \"JAL\",\n    route: \"LAX → HND\",\n    departure: \"Mar 15, 1:15pm\",\n    duration: \"12h 10m\",\n    stops: \"Nonstop\",\n    price: \"$945\",\n  },\n  {\n    id: \"fl-3\",\n    airline: \"United\",\n    route: \"LAX → NRT\",\n    departure: \"Mar 15, 10:45am\",\n    duration: \"14h 20m\",\n    stops: \"1 stop\",\n    price: \"$724\",\n  },\n  {\n    id: \"fl-4\",\n    airline: \"Delta\",\n    route: \"LAX → HND\",\n    departure: \"Mar 15, 9:00am\",\n    duration: \"16h 55m\",\n    stops: \"1 stop\",\n    price: \"$689\",\n  },\n];\n\nexport const LINK_PREVIEW: SerializableLinkPreview = {\n  id: \"chat-showcase-link-preview\",\n  href: \"https://www.quantamagazine.org/the-year-in-physics-20251217/\",\n  title: \"The Year in Physics\",\n  description:\n    \"Physicists spotted a new black hole, doubled down on weakening dark energy, and debated the meaning of quantum mechanics.\",\n  image:\n    \"https://www.quantamagazine.org/wp-content/uploads/2025/12/Year-in-review-2025-Physics-cr-Carlos-Arrojo-Lede-1-1720x968.webp\",\n  domain: \"quantamagazine.org\",\n  ratio: \"16:9\",\n  createdAt: \"2025-12-17T10:00:00.000Z\",\n};\n\nexport const X_POST: XPostData = {\n  id: \"chat-showcase-x-post\",\n  author: {\n    name: \"Alex Chen\",\n    handle: \"alexchendev\",\n    avatarUrl:\n      \"https://images.unsplash.com/photo-1599566150163-29194dcaad36?auto=format&fit=crop&q=80&w=1200\",\n  },\n  text: \"Just shipped something I've been working on for a while: react-aria-tree\\n\\nA headless tree view component with full keyboard navigation, multi-select, drag & drop, and virtualization built in.\\n\\nNo styling opinions. Accessible by default.\\n\\ngithub.com/alexchen/react-aria-tree\",\n  createdAt: \"2025-11-10T14:30:00.000Z\",\n};\n\nexport const X_POST_ACTIONS = [\n  { id: \"cancel\", label: \"Discard\", variant: \"ghost\" as const },\n  { id: \"edit\", label: \"Revise\", variant: \"outline\" as const },\n  { id: \"send\", label: \"Post Now\", variant: \"default\" as const },\n];\n\nconst SPENDING_DATA = [\n  { category: \"Groceries\", amount: 284 },\n  { category: \"Dining\", amount: 156 },\n  { category: \"Transport\", amount: 89 },\n  { category: \"Entertainment\", amount: 67 },\n  { category: \"Shopping\", amount: 124 },\n];\n\nexport const SPENDING_CHART: Omit<SerializableChart, \"id\"> = {\n  type: \"bar\",\n  title: \"Weekly Spending\",\n  data: SPENDING_DATA,\n  xKey: \"category\",\n  series: [{ key: \"amount\", label: \"Amount\" }],\n  showLegend: false,\n};\n\nexport const PLAN_TODO_LABELS = [\n  \"Checking Sarah's interests\",\n  \"Searching gift ideas\",\n  \"Comparing top options\",\n  \"Finalizing recommendations\",\n];\n\nexport const TERMINAL_DATA: Omit<SerializableTerminal, \"id\"> = {\n  command: \"pnpm test auth\",\n  stdout: `✓ login flow handles invalid credentials\n✓ session tokens refresh correctly\n✓ logout clears all cookies\n\nTests: 3 passed, 3 total\nTime: 1.24s`,\n  exitCode: 0,\n  durationMs: 1243,\n};\n\nexport const CODE_BLOCK_DATA: Omit<SerializableCodeBlock, \"id\"> = {\n  language: \"typescript\",\n  lineNumbers: \"visible\",\n  filename: \"use-debounce.ts\",\n  code: `import { useEffect, useState } from \"react\";\n\nexport function useDebounce<T>(value: T, delay = 250): T {\n  const [debounced, setDebounced] = useState(value);\n\n  useEffect(() => {\n    // Normalize delay (covers NaN/Infinity/negative)\n    const d = Number.isFinite(delay) ? Math.max(0, delay) : 0;\n\n    // If no delay, update immediately.\n    if (d === 0) {\n      if (!Object.is(debounced, value)) setDebounced(value);\n      return;\n    }\n\n    const id = setTimeout(() => {\n      if (!Object.is(debounced, value)) setDebounced(value);\n    }, d);\n\n    return () => clearTimeout(id);\n  }, [value, delay, debounced]);\n\n  return debounced;\n}`,\n};\n\nexport const OPTION_LIST_OPTIONS: OptionListOption[] = [\n  {\n    id: \"comedy\",\n    label: \"Something funny\",\n    description: \"I need a good laugh\",\n  },\n  {\n    id: \"thriller\",\n    label: \"Edge-of-seat thriller\",\n    description: \"Keep me guessing\",\n  },\n  {\n    id: \"comfort\",\n    label: \"Feel-good classic\",\n    description: \"Cozy and familiar\",\n  },\n  { id: \"scifi\", label: \"Mind-bending sci-fi\", description: \"Make me think\" },\n];\n\nexport const OPTION_LIST_CONFIRMED = [\"comedy\", \"comfort\"];\n\nexport const ITEM_CAROUSEL_DATA: Omit<SerializableItemCarousel, \"id\"> = {\n  items: [\n    {\n      id: \"ambient-1\",\n      name: \"Music for Airports\",\n      subtitle: \"Brian Eno · Ambient\",\n      image:\n        \"https://is1-ssl.mzstatic.com/image/thumb/Music125/v4/ee/71/42/ee71425d-6bc9-3df8-c90b-8539f59144ab/00724386649553.rgb.jpg/600x600bb.jpg\",\n      actions: [{ id: \"play\", label: \"Play\", variant: \"default\" }],\n    },\n    {\n      id: \"rock-1\",\n      name: \"In Rainbows\",\n      subtitle: \"Radiohead · Alt Rock\",\n      image:\n        \"https://is1-ssl.mzstatic.com/image/thumb/Music126/v4/dd/50/c7/dd50c790-99ac-d3d0-5ab8-e3891fb8fd52/634904032463.png/600x600bb.jpg\",\n      actions: [{ id: \"play\", label: \"Play\", variant: \"default\" }],\n    },\n    {\n      id: \"electronic-1\",\n      name: \"Async\",\n      subtitle: \"Ryuichi Sakamoto · Electronic\",\n      image:\n        \"https://is1-ssl.mzstatic.com/image/thumb/Music123/v4/82/e0/7b/82e07b9a-1d98-bbf4-d1e2-fb94312bbea2/731383683060.jpg/600x600bb.jpg\",\n      actions: [{ id: \"play\", label: \"Play\", variant: \"default\" }],\n    },\n    {\n      id: \"jazz-1\",\n      name: \"Kind of Blue\",\n      subtitle: \"Miles Davis · Jazz\",\n      image:\n        \"https://is1-ssl.mzstatic.com/image/thumb/Music/7f/9f/d6/mzi.vtnaewef.jpg/600x600bb.jpg\",\n      actions: [{ id: \"play\", label: \"Play\", variant: \"default\" }],\n    },\n    {\n      id: \"psychedelic-1\",\n      name: \"Currents\",\n      subtitle: \"Tame Impala · Psychedelic\",\n      image:\n        \"https://is1-ssl.mzstatic.com/image/thumb/Music115/v4/a8/2e/b4/a82eb490-f30a-a321-461a-0383c88fec95/15UMGIM23316.rgb.jpg/600x600bb.jpg\",\n      actions: [{ id: \"play\", label: \"Play\", variant: \"default\" }],\n    },\n  ],\n};\n\nexport const PARAMETER_SLIDER_DATA: Omit<SerializableParameterSlider, \"id\"> = {\n  sliders: [\n    {\n      id: \"bass\",\n      label: \"Bass\",\n      min: -12,\n      max: 12,\n      step: 1,\n      value: 4,\n      unit: \"dB\",\n    },\n    {\n      id: \"mid\",\n      label: \"Mid\",\n      min: -12,\n      max: 12,\n      step: 1,\n      value: -1,\n      unit: \"dB\",\n    },\n    {\n      id: \"treble\",\n      label: \"Treble\",\n      min: -12,\n      max: 12,\n      step: 1,\n      value: 3,\n      unit: \"dB\",\n    },\n  ],\n  actions: [\n    { id: \"reset\", label: \"Flat\", variant: \"ghost\" },\n    { id: \"apply\", label: \"Apply\", variant: \"default\" },\n  ],\n};\n\nfunction favicon(domain: string, size = 32): string {\n  return `https://www.google.com/s2/favicons?domain=${domain}&sz=${size}`;\n}\n\nexport const LLM_CITATIONS: SerializableCitation[] = [\n  {\n    id: \"llm-citation-1\",\n    href: \"https://en.wikipedia.org/wiki/Large_language_model\",\n    title: \"Large language model - Wikipedia\",\n    snippet:\n      \"A large language model is a type of machine learning model designed for natural language processing tasks such as language generation.\",\n    domain: \"wikipedia.org\",\n    favicon: favicon(\"wikipedia.org\"),\n    type: \"document\",\n  },\n  {\n    id: \"llm-citation-2\",\n    href: \"https://arxiv.org/abs/1706.03762\",\n    title: \"Attention Is All You Need\",\n    snippet:\n      \"The dominant sequence transduction models are based on complex recurrent or convolutional neural networks.\",\n    domain: \"arxiv.org\",\n    favicon: favicon(\"arxiv.org\"),\n    type: \"article\",\n  },\n  {\n    id: \"llm-citation-3\",\n    href: \"https://openai.com/research/gpt-2\",\n    title: \"Better Language Models - OpenAI\",\n    snippet:\n      \"We've trained a large-scale unsupervised language model which generates coherent paragraphs of text.\",\n    domain: \"openai.com\",\n    favicon: favicon(\"openai.com\"),\n    type: \"article\",\n  },\n  {\n    id: \"llm-citation-4\",\n    href: \"https://ai.google/research/pubs/pub46201\",\n    title: \"BERT: Pre-training of Deep Bidirectional Transformers\",\n    snippet:\n      \"We introduce a new language representation model called BERT, which stands for Bidirectional Encoder Representations from Transformers.\",\n    domain: \"ai.google\",\n    favicon: favicon(\"ai.google\"),\n    type: \"article\",\n  },\n];\n\nexport const STATS_DISPLAY_DATA: Omit<SerializableStatsDisplay, \"id\"> = {\n  title: \"Q4 Performance\",\n  stats: [\n    {\n      key: \"revenue\",\n      label: \"Revenue\",\n      value: 847300,\n      format: { kind: \"currency\", currency: \"USD\", decimals: 0 },\n      sparkline: {\n        data: [\n          72000, 68000, 74000, 81000, 78000, 85000, 89000, 91000, 86000, 94000,\n          97000, 102000,\n        ],\n        color: \"var(--chart-1)\",\n      },\n      diff: { value: 12.4, decimals: 1 },\n    },\n    {\n      key: \"active-users\",\n      label: \"Active Users\",\n      value: 24890,\n      format: { kind: \"number\", compact: true },\n      sparkline: {\n        data: [\n          18200, 19100, 19800, 20400, 21200, 21900, 22600, 23100, 23800, 24200,\n          24500, 24890,\n        ],\n        color: \"var(--chart-3)\",\n      },\n      diff: { value: 8.2, decimals: 1 },\n    },\n    {\n      key: \"churn\",\n      label: \"Churn Rate\",\n      value: 2.1,\n      format: { kind: \"percent\", decimals: 1, basis: \"unit\" },\n      sparkline: {\n        data: [3.2, 3.0, 2.8, 2.9, 2.7, 2.5, 2.4, 2.3, 2.2, 2.1, 2.1, 2.1],\n        color: \"var(--chart-4)\",\n      },\n      diff: { value: -0.8, decimals: 1, upIsPositive: false },\n    },\n    {\n      key: \"nps\",\n      label: \"NPS Score\",\n      value: 72,\n      format: { kind: \"number\" },\n      sparkline: {\n        data: [58, 61, 64, 62, 65, 68, 66, 69, 70, 71, 71, 72],\n        color: \"var(--chart-5)\",\n      },\n      diff: { value: 5.0, decimals: 0 },\n    },\n  ],\n};\n\nexport const PROGRESS_TRACKER_DATA: Omit<SerializableProgressTracker, \"id\"> = {\n  steps: [\n    {\n      id: \"build\",\n      label: \"Building\",\n      description: \"Compiling TypeScript and bundling assets\",\n      status: \"completed\",\n    },\n    {\n      id: \"test\",\n      label: \"Running Tests\",\n      description: \"84 tests across 12 suites\",\n      status: \"in-progress\",\n    },\n    {\n      id: \"deploy\",\n      label: \"Deploy to Production\",\n      description: \"Upload to edge nodes\",\n      status: \"pending\",\n    },\n  ],\n  elapsedTime: 38400,\n};\n"
  },
  {
    "path": "apps/www/lib/mocks/stocks.ts",
    "content": "/**\n * Mock stock data service\n * Structured for easy replacement with real API later\n */\n\nexport interface Stock {\n  symbol: string;\n  name: string;\n  price: number;\n  change: number;\n  changePercent: number;\n  volume: number;\n  marketCap: number;\n  pe?: number;\n  eps?: number;\n}\n\nconst mockStockData: Stock[] = [\n  {\n    symbol: \"AAPL\",\n    name: \"Apple Inc.\",\n    price: 178.25,\n    change: 2.35,\n    changePercent: 1.34,\n    volume: 48532100,\n    marketCap: 2780000000000,\n    pe: 29.5,\n    eps: 6.05,\n  },\n  {\n    symbol: \"MSFT\",\n    name: \"Microsoft Corporation\",\n    price: 378.91,\n    change: -1.24,\n    changePercent: -0.33,\n    volume: 22451800,\n    marketCap: 2810000000000,\n    pe: 35.2,\n    eps: 10.76,\n  },\n  {\n    symbol: \"GOOGL\",\n    name: \"Alphabet Inc.\",\n    price: 139.67,\n    change: 0.89,\n    changePercent: 0.64,\n    volume: 19234500,\n    marketCap: 1750000000000,\n    pe: 26.8,\n    eps: 5.21,\n  },\n  {\n    symbol: \"AMZN\",\n    name: \"Amazon.com Inc.\",\n    price: 145.32,\n    change: 3.12,\n    changePercent: 2.19,\n    volume: 35678900,\n    marketCap: 1500000000000,\n    pe: 58.4,\n    eps: 2.49,\n  },\n  {\n    symbol: \"TSLA\",\n    name: \"Tesla Inc.\",\n    price: 242.18,\n    change: -5.67,\n    changePercent: -2.29,\n    volume: 89234100,\n    marketCap: 768000000000,\n    pe: 73.2,\n    eps: 3.31,\n  },\n  {\n    symbol: \"NVDA\",\n    name: \"NVIDIA Corporation\",\n    price: 485.62,\n    change: 8.45,\n    changePercent: 1.77,\n    volume: 41256700,\n    marketCap: 1190000000000,\n    pe: 115.8,\n    eps: 4.19,\n  },\n  {\n    symbol: \"META\",\n    name: \"Meta Platforms Inc.\",\n    price: 325.78,\n    change: 4.23,\n    changePercent: 1.32,\n    volume: 15234800,\n    marketCap: 825000000000,\n    pe: 28.6,\n    eps: 11.39,\n  },\n  {\n    symbol: \"BRK.B\",\n    name: \"Berkshire Hathaway Inc.\",\n    price: 362.45,\n    change: -0.87,\n    changePercent: -0.24,\n    volume: 2145300,\n    marketCap: 790000000000,\n    pe: 8.9,\n    eps: 40.73,\n  },\n];\n\nexport interface GetStocksParams {\n  symbols?: string[];\n  limit?: number;\n  sort?: {\n    by?: \"symbol\" | \"price\" | \"change\" | \"marketCap\";\n    direction?: \"asc\" | \"desc\";\n  };\n}\n\n/**\n * Get stock data\n *\n * In production, replace this with actual API calls:\n * - Use Alpha Vantage, Finnhub, or similar API\n * - Add authentication and API key management\n * - Implement caching for rate limit compliance\n * - Add error handling for API failures\n */\nexport async function getStocks(\n  params: GetStocksParams = {},\n): Promise<Stock[]> {\n  const { symbols, limit, sort } = params;\n  const sortBy = sort?.by ?? \"symbol\";\n  const sortDirection = sort?.direction ?? \"asc\";\n\n  // Simulate API delay\n  await new Promise((resolve) => setTimeout(resolve, 300));\n\n  let stocks = [...mockStockData];\n\n  // Filter by symbols if provided\n  if (symbols && symbols.length > 0) {\n    const upperSymbols = symbols.map((s) => s.toUpperCase());\n    stocks = stocks.filter((stock) => upperSymbols.includes(stock.symbol));\n  }\n\n  // Sort\n  stocks.sort((a, b) => {\n    const aVal = a[sortBy];\n    const bVal = b[sortBy];\n\n    if (typeof aVal === \"string\" && typeof bVal === \"string\") {\n      return sortDirection === \"asc\"\n        ? aVal.localeCompare(bVal)\n        : bVal.localeCompare(aVal);\n    }\n\n    if (typeof aVal === \"number\" && typeof bVal === \"number\") {\n      return sortDirection === \"asc\" ? aVal - bVal : bVal - aVal;\n    }\n\n    return 0;\n  });\n\n  // Limit results\n  if (limit && limit > 0) {\n    stocks = stocks.slice(0, limit);\n  }\n\n  return stocks;\n}\n"
  },
  {
    "path": "apps/www/lib/mocks/tasks.ts",
    "content": "export type Task = {\n  id: string;\n  customer: string;\n  issue: string;\n  priority: \"high\" | \"medium\" | \"low\";\n  status: \"open\" | \"in-progress\" | \"waiting\" | \"done\";\n  assignee: string;\n  created: string; // ISO string\n};\n\nconst ALL_TASKS: Task[] = [\n  {\n    id: \"T-1042\",\n    customer: \"Acme Co.\",\n    issue: \"Checkout failing intermittently on mobile\",\n    priority: \"high\",\n    status: \"open\",\n    assignee: \"J. Patel\",\n    created: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), // 2h ago\n  },\n  {\n    id: \"T-1041\",\n    customer: \"Globex\",\n    issue: \"SLA report shows incorrect totals\",\n    priority: \"medium\",\n    status: \"in-progress\",\n    assignee: \"M. Chen\",\n    created: new Date(Date.now() - 26 * 60 * 60 * 1000).toISOString(), // 26h ago\n  },\n  {\n    id: \"T-1039\",\n    customer: \"Umbrella Corp\",\n    issue: \"2FA backup codes not accepted\",\n    priority: \"low\",\n    status: \"open\",\n    assignee: \"A. Gomez\",\n    created: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(),\n  },\n  {\n    id: \"T-1035\",\n    customer: \"Hooli\",\n    issue: \"Attachments fail on Safari 17\",\n    priority: \"high\",\n    status: \"in-progress\",\n    assignee: \"R. Davis\",\n    created: new Date(Date.now() - 4 * 60 * 60 * 1000).toISOString(),\n  },\n];\n\nexport async function getMockTasks({\n  assignee,\n}: { assignee?: string } = {}): Promise<Task[]> {\n  let items = ALL_TASKS;\n  if (assignee)\n    items = items.filter((t) =>\n      t.assignee.toLowerCase().includes(assignee.toLowerCase()),\n    );\n  // Sort by urgency (high → medium → low), then by created desc (newest first)\n  const rank = { high: 1, medium: 2, low: 3 } as const;\n  return [...items].sort((a, b) => {\n    const byUrgency = rank[a.priority] - rank[b.priority];\n    if (byUrgency !== 0) return byUrgency;\n    return new Date(b.created).getTime() - new Date(a.created).getTime();\n  });\n}\n"
  },
  {
    "path": "apps/www/lib/og/og-image.tsx",
    "content": "import { ImageResponse } from \"next/og\";\nimport { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nexport const size = {\n  width: 1200,\n  height: 630,\n};\n\nexport const contentType = \"image/png\";\n\nconst ZINC_950 = \"#09090b\";\nconst ZINC_400 = \"#a1a1aa\";\n\nasync function loadGeistFont(weight: \"Regular\" | \"Bold\") {\n  // Per Next.js docs, `process.cwd()` is the Next.js project directory.\n  return readFile(join(process.cwd(), `assets/fonts/Geist-${weight}.ttf`));\n}\n\nexport async function generateOgImage(title: string, description?: string) {\n  const [geistRegular, geistBold] = await Promise.all([\n    loadGeistFont(\"Regular\"),\n    loadGeistFont(\"Bold\"),\n  ]);\n\n  return new ImageResponse(\n    <div\n      style={{\n        width: \"100%\",\n        height: \"100%\",\n        display: \"flex\",\n        flexDirection: \"column\",\n        alignItems: \"flex-start\",\n        justifyContent: \"space-between\",\n        backgroundColor: ZINC_950,\n        padding: \"85px\",\n      }}\n    >\n      <svg width=\"150\" height=\"150\" viewBox=\"0 0 24 24\" fill=\"white\">\n        <path\n          fillRule=\"evenodd\"\n          clipRule=\"evenodd\"\n          d=\"M12 1L21.526 6.5V17.5L12 23L2.474 17.5V6.5L12 1ZM12 7C9.239 7 7 9.239 7 12C7 14.761 9.239 17 12 17C14.761 17 17 14.761 17 12C17 9.239 14.761 7 12 7Z\"\n        />\n      </svg>\n\n      <div\n        style={{\n          display: \"flex\",\n          flexDirection: \"column\",\n          alignItems: \"flex-start\",\n        }}\n      >\n        <div\n          style={{\n            display: \"flex\",\n            fontSize: \"120px\",\n            fontFamily: \"Geist Bold\",\n            fontWeight: 700,\n            color: \"white\",\n            marginBottom: \"12px\",\n            letterSpacing: \"-0.02em\",\n          }}\n        >\n          {title}\n        </div>\n\n        {description && (\n          <div\n            style={{\n              display: \"flex\",\n              fontSize: \"50px\",\n              fontFamily: \"Geist Regular\",\n              fontWeight: 400,\n              color: ZINC_400,\n            }}\n          >\n            {description}\n          </div>\n        )}\n      </div>\n    </div>,\n    {\n      ...size,\n      fonts: [\n        {\n          name: \"Geist Regular\",\n          data: geistRegular,\n          style: \"normal\",\n          weight: 400,\n        },\n        {\n          name: \"Geist Bold\",\n          data: geistBold,\n          style: \"normal\",\n          weight: 700,\n        },\n      ],\n    },\n  );\n}\n\nexport function createOgImageHandler(title: string, description?: string) {\n  return async function Image() {\n    return generateOgImage(title, description);\n  };\n}\n"
  },
  {
    "path": "apps/www/lib/playground/constants.ts",
    "content": "export const PROTOTYPE_SLUG_HEADER = \"x-prototype-slug\";\n"
  },
  {
    "path": "apps/www/lib/playground/index.ts",
    "content": "export * from \"./types\";\nexport * from \"./registry\";\nexport * from \"./runtime\";\nexport * from \"./tool-uis\";\nexport * from \"./mocks\";\n"
  },
  {
    "path": "apps/www/lib/playground/mocks.ts",
    "content": "const sleep = (latencyMs: number) =>\n  new Promise<void>((resolve) => {\n    setTimeout(resolve, latencyMs);\n  });\n\nexport function mockTool<TResult>(\n  result: TResult,\n  latencyMs: number = 0,\n): (args: unknown) => Promise<TResult> {\n  return async () => {\n    if (latencyMs > 0) {\n      await sleep(latencyMs);\n    }\n    return result;\n  };\n}\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/food-ordering/get-menu.ts",
    "content": "import { MENU } from \"./shared\";\n\ntype GetMenuArgs = {\n  restaurantId: string;\n};\n\ntype GetMenuResult = typeof MENU;\n\nexport const getMenu = async (_args: GetMenuArgs): Promise<GetMenuResult> => {\n  return MENU;\n};\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/food-ordering/get-restaurant-details.ts",
    "content": "import { RESTAURANT_DETAILS } from \"./shared\";\n\ntype GetRestaurantDetailsArgs = {\n  restaurantId: string;\n};\n\ntype GetRestaurantDetailsResult = typeof RESTAURANT_DETAILS;\n\nexport const getRestaurantDetails = async (\n  _args: GetRestaurantDetailsArgs,\n): Promise<GetRestaurantDetailsResult> => {\n  return RESTAURANT_DETAILS;\n};\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/food-ordering/index.ts",
    "content": "import { z } from \"zod\";\n\nimport { searchRestaurants } from \"./search-restaurants\";\nimport { getRestaurantDetails } from \"./get-restaurant-details\";\nimport { getMenu } from \"./get-menu\";\nimport { placeOrder } from \"./place-order\";\nimport type { Prototype } from \"../../types\";\n\nconst searchRestaurantsInput = z.object({\n  query: z.string().optional(),\n  location: z.string().optional(),\n});\n\nconst getRestaurantDetailsInput = z.object({\n  restaurantId: z.string(),\n});\n\nconst getMenuInput = z.object({\n  restaurantId: z.string(),\n});\n\nconst placeOrderInput = z.object({\n  restaurantId: z.string(),\n  items: z\n    .array(\n      z.object({\n        itemId: z.string(),\n        name: z.string(),\n        quantity: z.number().int().min(1),\n        price: z.number().min(0),\n      }),\n    )\n    .min(1),\n  orderType: z.enum([\"delivery\", \"pickup\"]),\n  deliveryAddress: z.string().optional(),\n  customerName: z.string(),\n  customerPhone: z.string(),\n});\n\nexport const foodOrderingPrototype: Prototype = {\n  slug: \"food-ordering\",\n  title: \"Food Ordering Assistant\",\n  summary: \"Search restaurants, browse menus, and place orders\",\n  systemPrompt: `You are a helpful food ordering assistant. Help users find restaurants, browse menus, and place orders.\n\nKey capabilities:\n1. Search for restaurants by cuisine type or name\n2. Get detailed menu information for restaurants\n3. Place orders for delivery or pickup\n4. Get restaurant details like hours, ratings, and delivery info\n\nBe conversational, friendly, and helpful. Ask clarifying questions when needed.`,\n  tools: [\n    {\n      name: \"search_restaurants\",\n      description: \"Search for restaurants by cuisine, name, or location.\",\n      uiId: \"fallback\",\n      input: searchRestaurantsInput,\n      execute: async (rawArgs: unknown) =>\n        searchRestaurants(searchRestaurantsInput.parse(rawArgs ?? {})),\n    },\n    {\n      name: \"get_restaurant_details\",\n      description: \"Retrieve detailed information about a specific restaurant.\",\n      uiId: \"fallback\",\n      input: getRestaurantDetailsInput,\n      execute: async (rawArgs: unknown) =>\n        getRestaurantDetails(getRestaurantDetailsInput.parse(rawArgs)),\n    },\n    {\n      name: \"get_menu\",\n      description: \"Fetch the menu for a specific restaurant.\",\n      uiId: \"fallback\",\n      input: getMenuInput,\n      execute: async (rawArgs: unknown) => getMenu(getMenuInput.parse(rawArgs)),\n    },\n    {\n      name: \"place_order\",\n      description: \"Place a delivery or pickup order and receive a summary.\",\n      uiId: \"fallback\",\n      input: placeOrderInput,\n      execute: async (rawArgs: unknown) =>\n        placeOrder(placeOrderInput.parse(rawArgs)),\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/food-ordering/place-order.ts",
    "content": "type OrderItem = {\n  itemId: string;\n  name: string;\n  quantity: number;\n  price: number;\n};\n\ntype PlaceOrderArgs = {\n  restaurantId: string;\n  items: OrderItem[];\n  orderType: \"delivery\" | \"pickup\";\n  deliveryAddress?: string;\n  customerName: string;\n  customerPhone: string;\n};\n\ntype PlaceOrderResult = {\n  orderId: string;\n  status: \"confirmed\";\n  restaurantId: string;\n  items: OrderItem[];\n  orderType: \"delivery\" | \"pickup\";\n  deliveryAddress?: string;\n  customer: {\n    name: string;\n    phone: string;\n  };\n  subtotal: number;\n  tax: number;\n  deliveryFee: number;\n  total: number;\n  estimatedReadyTime: string;\n  estimatedDeliveryTime?: string;\n};\n\nconst formatReadyTime = (orderType: \"delivery\" | \"pickup\") =>\n  orderType === \"pickup\" ? \"20-25 min\" : \"30-35 min\";\n\nconst DELIVERY_FEE = 2.99;\nconst TAX_RATE = 0.0875;\n\nexport const placeOrder = async (\n  args: PlaceOrderArgs,\n): Promise<PlaceOrderResult> => {\n  const subtotal = args.items.reduce<number>(\n    (sum, item) => sum + item.price * item.quantity,\n    0,\n  );\n  const tax = subtotal * TAX_RATE;\n  const deliveryFee = args.orderType === \"delivery\" ? DELIVERY_FEE : 0;\n  const total = subtotal + tax + deliveryFee;\n\n  return {\n    orderId: `ord_${Date.now()}`,\n    status: \"confirmed\",\n    restaurantId: args.restaurantId,\n    items: args.items,\n    orderType: args.orderType,\n    deliveryAddress: args.deliveryAddress,\n    customer: {\n      name: args.customerName,\n      phone: args.customerPhone,\n    },\n    subtotal,\n    tax,\n    deliveryFee,\n    total,\n    estimatedReadyTime: formatReadyTime(args.orderType),\n    estimatedDeliveryTime:\n      args.orderType === \"delivery\" ? \"35-45 min\" : undefined,\n  };\n};\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/food-ordering/search-restaurants.ts",
    "content": "import { RESTAURANT_SEARCH_RESULTS } from \"./shared\";\n\ntype SearchRestaurantsArgs = {\n  query?: string;\n  location?: string;\n};\n\ntype SearchRestaurantsResult = typeof RESTAURANT_SEARCH_RESULTS;\n\nexport const searchRestaurants = async (\n  _args: SearchRestaurantsArgs,\n): Promise<SearchRestaurantsResult> => {\n  return RESTAURANT_SEARCH_RESULTS;\n};\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/food-ordering/shared.ts",
    "content": "export const RESTAURANT_SEARCH_RESULTS = {\n  restaurants: [\n    {\n      id: \"rest_001\",\n      name: \"Tony's Pizza Palace\",\n      cuisine: \"Italian, Pizza\",\n      rating: 4.7,\n      distance: \"0.3 mi\",\n      deliveryTime: \"20-30 min\",\n      deliveryFee: 2.99,\n      minOrder: 15,\n    },\n    {\n      id: \"rest_002\",\n      name: \"Golden Dragon Chinese\",\n      cuisine: \"Chinese, Asian\",\n      rating: 4.5,\n      distance: \"0.5 mi\",\n      deliveryTime: \"25-35 min\",\n      deliveryFee: 1.99,\n      minOrder: 12,\n    },\n    {\n      id: \"rest_003\",\n      name: \"Sushi Master\",\n      cuisine: \"Japanese, Sushi\",\n      rating: 4.8,\n      distance: \"0.4 mi\",\n      deliveryTime: \"30-40 min\",\n      deliveryFee: 3.99,\n      minOrder: 20,\n    },\n  ],\n  count: 3,\n} as const;\n\nexport const RESTAURANT_DETAILS = {\n  id: \"rest_001\",\n  name: \"Tony's Pizza Palace\",\n  description:\n    \"Family-owned pizzeria serving authentic Italian pizza for over 20 years\",\n  hours: {\n    monday: \"11:00 AM - 10:00 PM\",\n    tuesday: \"11:00 AM - 10:00 PM\",\n    wednesday: \"11:00 AM - 10:00 PM\",\n    thursday: \"11:00 AM - 11:00 PM\",\n    friday: \"11:00 AM - 12:00 AM\",\n    saturday: \"11:00 AM - 12:00 AM\",\n    sunday: \"12:00 PM - 10:00 PM\",\n  },\n  address: \"123 Main St, San Francisco, CA 94102\",\n  phone: \"(415) 555-0123\",\n  rating: 4.7,\n  reviewCount: 1247,\n  specialties: [\"Margherita Pizza\", \"Pepperoni Classic\", \"Garlic Knots\"],\n  dietaryOptions: [\"Vegetarian\", \"Vegan\", \"Gluten-free\"],\n  pickupAvailable: true,\n  deliveryAvailable: true,\n} as const;\n\nexport const MENU = {\n  restaurantId: \"rest_001\",\n  categories: [\n    {\n      name: \"Appetizers\",\n      items: [\n        {\n          id: \"m1\",\n          name: \"Garlic Knots (6 pcs)\",\n          price: 5.99,\n          description: \"Fresh baked knots with garlic butter and herbs\",\n        },\n        {\n          id: \"m2\",\n          name: \"Mozzarella Sticks (8 pcs)\",\n          price: 8.99,\n          description: \"Golden fried mozzarella with marinara sauce\",\n        },\n      ],\n    },\n    {\n      name: \"Pizza\",\n      items: [\n        {\n          id: \"m3\",\n          name: \"Margherita Pizza\",\n          price: 14.99,\n          description: \"Fresh mozzarella, basil, and tomato sauce\",\n        },\n        {\n          id: \"m4\",\n          name: \"Pepperoni Classic\",\n          price: 16.99,\n          description: \"Classic pepperoni with mozzarella cheese\",\n        },\n        {\n          id: \"m5\",\n          name: \"BBQ Chicken Pizza\",\n          price: 18.99,\n          description: \"Grilled chicken, BBQ sauce, red onions, cilantro\",\n        },\n      ],\n    },\n    {\n      name: \"Pasta\",\n      items: [\n        {\n          id: \"m6\",\n          name: \"Spaghetti Carbonara\",\n          price: 13.99,\n          description: \"Creamy pasta with bacon, parmesan, and eggs\",\n        },\n        {\n          id: \"m7\",\n          name: \"Fettuccine Alfredo\",\n          price: 12.99,\n          description: \"Classic creamy parmesan sauce\",\n        },\n      ],\n    },\n  ],\n} as const;\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/index.ts",
    "content": "export { foodOrderingPrototype } from \"./food-ordering\";\nexport { waymoPrototype } from \"./waymo\";\nexport { waymoV2Prototype } from \"./waymo-v2\";\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/README.md",
    "content": "# Waymo Prototype (v0)\n\nA reference implementation of Tool UIs for ride booking that follows the [Collaboration Guidelines](../../../../COLLAB_GUIDELINES.md).\n\n## Quick Start\n\nNavigate to `/playground/waymo-demo` to test the golden path:\n\n1. Click \"Start Demo: 'I need a ride home'\"\n2. See ride quote card\n3. Click \"Confirm Ride\"\n4. See booking confirmation\n\n## Architecture\n\nThis prototype strictly follows the three-layer architecture:\n\n### 1. Tools (`tools.ts`)\n\n4 pure functions that return typed mock data:\n\n- `getRiderContext()` - Returns user's home, work, payment methods\n- `getPickupLocation({ hint })` - Returns pickup location with confidence\n- `getQuote({ pickup, dropoff })` - Returns ride quote with price/ETA\n- `bookTrip({ quoteId, paymentMethodId? })` - Books ride and returns confirmation\n\n### 2. Tool UIs (`components/`)\n\n2 React components that only render:\n\n- `RideQuote` - Interactive (confirm button) → Receipt (confirmed state)\n- `BookingConfirmation` - Receipt only (trip details + actions)\n\n### 3. Orchestrator (`WaymoDemo.tsx`)\n\nManages the entire flow:\n\n- Calls tools in sequence\n- Updates message state\n- Decides when to show Tool UIs\n- Handles user interactions\n\n## File Structure\n\n```\nwaymo/\n├── types.ts                    # Shared domain types\n├── tools.ts                    # 4 core tools (mock data)\n├── components/\n│   ├── RideQuote.tsx          # Interactive → Receipt UI\n│   ├── BookingConfirmation.tsx # Receipt UI\n│   └── index.tsx              # Exports\n├── WaymoDemo.tsx              # Orchestrator\n├── README.md                  # This file\n└── index.tsx                  # Main export\n```\n\n## Tool UI Message Format\n\n```typescript\ninterface ToolUIMessage {\n  type: \"tool-ui\";\n  component: \"RideQuote\" | \"BookingConfirmation\";\n  props: RideQuoteProps | BookingConfirmationProps;\n}\n```\n\n## Golden Path Flow\n\n1. **User**: \"I need a ride home\"\n2. **Silent tools**: `getRiderContext` → `getPickupLocation` → `getQuote`\n3. **Show**: Text + RideQuote UI (interactive)\n4. **User clicks**: \"Confirm Ride\"\n5. **Call**: `bookTrip`\n6. **Update**: RideQuote → receipt, add BookingConfirmation\n\n## Guidelines Compliance\n\nThis prototype demonstrates:\n\n- ✅ **3-layer architecture** - Tools, UI components, orchestrator\n- ✅ **4 tools only** - Chunky capabilities, not conversational steps\n- ✅ **2 Tool UIs** - Minimal set for golden path\n- ✅ **Standard message format** - Consistent ToolUIMessage shape\n- ✅ **No tool calls in components** - All async logic in orchestrator\n- ✅ **Typed domain objects** - Shared types in `types.ts`\n\n## Extending\n\nTo add a friction variant (after golden path works):\n\n1. Keep the same 4 tools\n2. Reuse existing Tool UIs\n3. Add branch logic in `WaymoDemo.tsx`\n4. Test that golden path still works\n\n## Important\n\n**Do not:**\n\n- Add more tools (stay at 4)\n- Call tools from components\n- Change ToolUIMessage format\n- Break the golden path\n\n**Always refer to [COLLAB_GUIDELINES.md](../../../../COLLAB_GUIDELINES.md) before making changes.**\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/WaymoDemo.tsx",
    "content": "/**\n * WaymoDemo - Orchestrator for the Waymo ride booking prototype\n *\n * This file follows the three-layer architecture defined in COLLAB_GUIDELINES.md:\n * - Calls tools (never done in components)\n * - Manages message state\n * - Renders Tool UIs based on ToolUIMessage format\n *\n * @see ../../../../COLLAB_GUIDELINES.md\n */\n\n\"use client\";\n\nimport { useState, useCallback } from \"react\";\nimport { RideQuote, BookingConfirmation } from \"./components\";\nimport {\n  getRiderContext,\n  getPickupLocation,\n  getQuote,\n  bookTrip,\n  resolveAddress,\n} from \"./tools\";\nimport type {\n  RiderContext,\n  RideQuote as RideQuoteType,\n  ToolUIMessage,\n  RideQuoteProps,\n  BookingConfirmationProps,\n} from \"./types\";\n\ninterface Message {\n  role: \"user\" | \"assistant\";\n  content: string | ToolUIMessage;\n}\n\nexport function WaymoDemo() {\n  const [messages, setMessages] = useState<Message[]>([]);\n  const [isProcessing, setIsProcessing] = useState(false);\n  const [currentQuote, setCurrentQuote] = useState<RideQuoteType | null>(null);\n  const [riderContext, setRiderContext] = useState<RiderContext | null>(null);\n  const [awaitingDestination, setAwaitingDestination] = useState(false);\n  const [destinationInput, setDestinationInput] = useState(\"\");\n\n  const handleGoldenPath = useCallback(async () => {\n    setIsProcessing(true);\n    setMessages([{ role: \"user\", content: \"I need a ride home\" }]);\n\n    try {\n      // Step 1: Get rider context (silent)\n      const context = await getRiderContext();\n      setRiderContext(context);\n\n      if (!context.home) {\n        // Handle no home case in future iteration\n        setMessages((prev) => [\n          ...prev,\n          {\n            role: \"assistant\",\n            content:\n              \"I notice you don't have a home address saved. Where would you like to go?\",\n          },\n        ]);\n        setIsProcessing(false);\n        return;\n      }\n\n      // Step 2: Get pickup location (silent)\n      const pickupResult = await getPickupLocation({\n        hint: \"current_location\",\n      });\n\n      if (pickupResult.confidence !== \"high\") {\n        // Handle low confidence in future iteration\n        setMessages((prev) => [\n          ...prev,\n          {\n            role: \"assistant\",\n            content: `I'm having trouble confirming your location. Are you near ${pickupResult.resolvedLocation.name}?`,\n          },\n        ]);\n        setIsProcessing(false);\n        return;\n      }\n\n      // Step 3: Get quote\n      const quote = await getQuote({\n        pickup: pickupResult.resolvedLocation,\n        dropoff: context.home,\n      });\n      setCurrentQuote(quote);\n\n      // Step 4: Show assistant response with RideQuote UI\n      setMessages((prev) => [\n        ...prev,\n        {\n          role: \"assistant\",\n          content: `I can get you home from ${pickupResult.resolvedLocation.name}.`,\n        },\n        {\n          role: \"assistant\",\n          content: {\n            type: \"tool-ui\",\n            component: \"RideQuote\",\n            props: {\n              state: \"interactive\",\n              quote,\n              paymentMethod: context.defaultPaymentMethod,\n            },\n          },\n        },\n      ]);\n    } catch (error) {\n      console.error(\"Error in golden path:\", error);\n      setMessages((prev) => [\n        ...prev,\n        {\n          role: \"assistant\",\n          content:\n            \"I'm sorry, I encountered an error while getting your ride quote. Please try again.\",\n        },\n      ]);\n    } finally {\n      setIsProcessing(false);\n    }\n  }, []);\n\n  const handleConfirmRide = useCallback(async () => {\n    if (!currentQuote || !riderContext) return;\n\n    setIsProcessing(true);\n\n    try {\n      // Book the trip\n      const trip = await bookTrip({\n        quoteId: currentQuote.quoteId,\n        paymentMethodId: riderContext.defaultPaymentMethod?.id,\n      });\n\n      // Update the RideQuote to receipt state\n      setMessages((prev) => {\n        const newMessages = [...prev];\n        // Find and update the RideQuote message\n        for (let i = newMessages.length - 1; i >= 0; i--) {\n          const msg = newMessages[i];\n          if (\n            msg.role === \"assistant\" &&\n            typeof msg.content === \"object\" &&\n            msg.content.component === \"RideQuote\"\n          ) {\n            // Update to receipt state\n            newMessages[i] = {\n              ...msg,\n              content: {\n                ...msg.content,\n                props: {\n                  ...msg.content.props,\n                  state: \"receipt\",\n                },\n              },\n            };\n            break;\n          }\n        }\n        return newMessages;\n      });\n\n      // Add booking confirmation\n      setMessages((prev) => [\n        ...prev,\n        {\n          role: \"assistant\",\n          content: {\n            type: \"tool-ui\",\n            component: \"BookingConfirmation\",\n            props: {\n              state: \"receipt\",\n              trip,\n            },\n          },\n        },\n      ]);\n    } catch (error) {\n      console.error(\"Error booking trip:\", error);\n      setMessages((prev) => [\n        ...prev,\n        {\n          role: \"assistant\",\n          content:\n            \"I'm sorry, I couldn't complete your booking. Please try again.\",\n        },\n      ]);\n    } finally {\n      setIsProcessing(false);\n    }\n  }, [currentQuote, riderContext]);\n\n  // Friction variant: No saved home address\n  const handleFrictionPath = useCallback(async () => {\n    setIsProcessing(true);\n    setMessages([{ role: \"user\", content: \"I need a ride home\" }]);\n\n    try {\n      // Get rider context with no home\n      const context = await getRiderContext({ noHome: true });\n      setRiderContext(context);\n\n      // No home address - ask for destination\n      setMessages((prev) => [\n        ...prev,\n        {\n          role: \"assistant\",\n          content:\n            \"I notice you don't have a home address saved. Where would you like to go?\",\n        },\n      ]);\n      setAwaitingDestination(true);\n    } catch (error) {\n      console.error(\"Error in friction path:\", error);\n      setMessages((prev) => [\n        ...prev,\n        {\n          role: \"assistant\",\n          content: \"I'm sorry, I encountered an error. Please try again.\",\n        },\n      ]);\n    } finally {\n      setIsProcessing(false);\n    }\n  }, []);\n\n  // Handle manual destination input\n  const handleDestinationSubmit = useCallback(async () => {\n    if (!destinationInput.trim() || !riderContext) return;\n\n    setAwaitingDestination(false);\n    setIsProcessing(true);\n\n    // Add user message\n    setMessages((prev) => [\n      ...prev,\n      { role: \"user\", content: destinationInput },\n    ]);\n\n    try {\n      // Resolve the address\n      const dropoff = await resolveAddress(destinationInput);\n      setDestinationInput(\"\");\n\n      // Get pickup location\n      const pickupResult = await getPickupLocation({\n        hint: \"current_location\",\n      });\n\n      // Get quote\n      const quote = await getQuote({\n        pickup: pickupResult.resolvedLocation,\n        dropoff,\n      });\n      setCurrentQuote(quote);\n\n      // Show quote\n      setMessages((prev) => [\n        ...prev,\n        {\n          role: \"assistant\",\n          content: `I can get you to ${dropoff.name} from ${pickupResult.resolvedLocation.name}.`,\n        },\n        {\n          role: \"assistant\",\n          content: {\n            type: \"tool-ui\",\n            component: \"RideQuote\",\n            props: {\n              state: \"interactive\",\n              quote,\n              paymentMethod: riderContext.defaultPaymentMethod,\n            },\n          },\n        },\n      ]);\n    } catch (error) {\n      console.error(\"Error processing destination:\", error);\n      setMessages((prev) => [\n        ...prev,\n        {\n          role: \"assistant\",\n          content:\n            \"I couldn't find that address. Please try again with a different address.\",\n        },\n      ]);\n      setAwaitingDestination(true);\n    } finally {\n      setIsProcessing(false);\n    }\n  }, [destinationInput, riderContext]);\n\n  const renderMessage = (message: Message, index: number) => {\n    if (message.role === \"user\") {\n      return (\n        <div key={index} className=\"flex justify-end\">\n          <div className=\"bg-primary text-primary-foreground max-w-[80%] rounded-lg px-4 py-2\">\n            {message.content as string}\n          </div>\n        </div>\n      );\n    }\n\n    // Assistant message\n    if (typeof message.content === \"string\") {\n      return (\n        <div key={index} className=\"flex justify-start\">\n          <div className=\"bg-muted max-w-[80%] rounded-lg px-4 py-2\">\n            {message.content}\n          </div>\n        </div>\n      );\n    }\n\n    // Tool UI message\n    const toolUI = message.content as ToolUIMessage;\n    if (toolUI.component === \"RideQuote\") {\n      const props = toolUI.props as RideQuoteProps;\n      return (\n        <div key={index} className=\"flex justify-start\">\n          <div className=\"w-full max-w-lg\">\n            <RideQuote {...props} onConfirm={handleConfirmRide} />\n          </div>\n        </div>\n      );\n    }\n\n    if (toolUI.component === \"BookingConfirmation\") {\n      const props = toolUI.props as BookingConfirmationProps;\n      return (\n        <div key={index} className=\"flex justify-start\">\n          <div className=\"w-full max-w-lg\">\n            <BookingConfirmation {...props} />\n          </div>\n        </div>\n      );\n    }\n\n    return null;\n  };\n\n  return (\n    <div className=\"mx-auto max-w-4xl p-6\">\n      <div className=\"bg-card mb-6 rounded-lg border p-4\">\n        <h1 className=\"text-2xl font-bold\">Waymo Demo (v0)</h1>\n        <p className=\"text-muted-foreground mt-2\">\n          Golden path implementation: &ldquo;I need a ride home&rdquo; → 1 click\n          booking\n        </p>\n      </div>\n\n      {/* Messages */}\n      <div className=\"space-y-4\">\n        {messages.map((message, index) => renderMessage(message, index))}\n\n        {isProcessing && (\n          <div className=\"flex justify-start\">\n            <div className=\"bg-muted rounded-lg px-4 py-2\">\n              <div className=\"flex items-center gap-2\">\n                <div className=\"bg-foreground/50 h-2 w-2 animate-pulse rounded-full\" />\n                <div className=\"bg-foreground/50 animation-delay-200 h-2 w-2 animate-pulse rounded-full\" />\n                <div className=\"bg-foreground/50 animation-delay-400 h-2 w-2 animate-pulse rounded-full\" />\n              </div>\n            </div>\n          </div>\n        )}\n      </div>\n\n      {/* Destination input for friction path */}\n      {awaitingDestination && !isProcessing && (\n        <div className=\"mt-4\">\n          <div className=\"flex gap-2\">\n            <input\n              type=\"text\"\n              value={destinationInput}\n              onChange={(e) => setDestinationInput(e.target.value)}\n              onKeyDown={(e) => {\n                if (e.key === \"Enter\") {\n                  handleDestinationSubmit();\n                }\n              }}\n              placeholder=\"Enter destination address...\"\n              className=\"bg-background border-input flex-1 rounded-lg border px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500\"\n              autoFocus\n            />\n            <button\n              onClick={handleDestinationSubmit}\n              disabled={!destinationInput.trim()}\n              className=\"bg-primary text-primary-foreground hover:bg-primary/90 disabled:bg-muted disabled:text-muted-foreground rounded-lg px-4 py-2 font-medium transition-colors\"\n            >\n              Send\n            </button>\n          </div>\n        </div>\n      )}\n\n      {/* Start buttons */}\n      {messages.length === 0 && !isProcessing && (\n        <div className=\"mt-8 space-y-4 text-center\">\n          <div>\n            <button\n              onClick={handleGoldenPath}\n              className=\"bg-primary text-primary-foreground hover:bg-primary/90 rounded-lg px-6 py-3 font-medium transition-colors\"\n            >\n              Golden Path: &ldquo;I need a ride home&rdquo;\n            </button>\n          </div>\n          <div>\n            <button\n              onClick={handleFrictionPath}\n              className=\"bg-muted text-muted-foreground hover:bg-muted/80 rounded-lg px-6 py-3 font-medium transition-colors\"\n            >\n              Friction: No Saved Address\n            </button>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/check-ride-prices.ts",
    "content": "import { MOCK_NOW, RIDE_OPTIONS } from \"./shared\";\n\ntype CheckRidePricesArgs = {\n  pickup: string;\n  destination: string;\n  departureTime?: string;\n  services?: string[];\n};\n\ntype RideOption = (typeof RIDE_OPTIONS)[number];\n\ntype CheckRidePricesResult = {\n  asOf: string;\n  options: readonly RideOption[];\n};\n\nexport const checkRidePrices = async ({\n  services,\n}: CheckRidePricesArgs): Promise<CheckRidePricesResult> => {\n  const options =\n    services && services.length > 0\n      ? RIDE_OPTIONS.filter((option) => services.includes(option.service))\n      : RIDE_OPTIONS;\n\n  return {\n    asOf: MOCK_NOW,\n    options,\n  };\n};\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/components/BookingConfirmation.tsx",
    "content": "/**\n * BookingConfirmation Tool UI Component\n *\n * ARCHITECTURE LAYER 2: Tool UI\n * - Receives props and renders UI\n * - No tool calls, no async logic\n * - Read-only receipt component\n *\n * @see ../../../../../COLLAB_GUIDELINES.md\n */\n\n\"use client\";\n\nimport type { BookingConfirmationProps } from \"../types\";\nimport {\n  CheckCircle,\n  MapPin,\n  Clock,\n  CreditCard,\n  Car,\n  Share2,\n  Map,\n} from \"lucide-react\";\n\nexport function BookingConfirmation({ trip }: BookingConfirmationProps) {\n  return (\n    <div className=\"rounded-lg border bg-card shadow-sm\">\n      {/* Success header */}\n      <div className=\"border-b bg-green-50 px-6 py-4 dark:bg-green-950/30\">\n        <div className=\"flex items-center gap-3\">\n          <CheckCircle className=\"h-6 w-6 text-green-600 dark:text-green-400\" />\n          <div>\n            <h3 className=\"text-lg font-semibold text-foreground\">\n              Your Waymo is on the way!\n            </h3>\n            <p className=\"text-sm text-muted-foreground\">\n              Trip ID: {trip.tripId}\n            </p>\n          </div>\n        </div>\n      </div>\n\n      <div className=\"p-6\">\n        {/* Route summary */}\n        <div className=\"mb-6 space-y-3\">\n          <div className=\"flex items-start gap-3\">\n            <div className=\"mt-1\">\n              <div className=\"flex h-2 w-2 rounded-full bg-blue-600\" />\n            </div>\n            <div className=\"flex-1\">\n              <div className=\"font-medium\">{trip.pickup.name}</div>\n              <div className=\"text-sm text-muted-foreground\">\n                {trip.pickup.address}\n              </div>\n            </div>\n          </div>\n\n          <div className=\"ml-1 h-6 w-0 border-l-2 border-dashed border-muted-foreground/30\" />\n\n          <div className=\"flex items-start gap-3\">\n            <MapPin className=\"mt-1 h-4 w-4 text-muted-foreground\" />\n            <div className=\"flex-1\">\n              <div className=\"font-medium\">{trip.dropoff.name}</div>\n              <div className=\"text-sm text-muted-foreground\">\n                {trip.dropoff.address}\n              </div>\n            </div>\n          </div>\n        </div>\n\n        {/* Trip details */}\n        <div className=\"mb-6 space-y-3 rounded-lg bg-muted/50 p-4\">\n          <div className=\"flex items-center justify-between\">\n            <div className=\"flex items-center gap-2\">\n              <Clock className=\"h-4 w-4 text-muted-foreground\" />\n              <span className=\"text-sm text-muted-foreground\">ETA</span>\n            </div>\n            <span className=\"font-medium\">{trip.etaMinutes} minutes</span>\n          </div>\n\n          <div className=\"flex items-center justify-between\">\n            <div className=\"flex items-center gap-2\">\n              <CreditCard className=\"h-4 w-4 text-muted-foreground\" />\n              <span className=\"text-sm text-muted-foreground\">Payment</span>\n            </div>\n            <span className=\"text-sm\">{trip.paymentSummary}</span>\n          </div>\n\n          {trip.vehicle && (\n            <div className=\"flex items-center justify-between\">\n              <div className=\"flex items-center gap-2\">\n                <Car className=\"h-4 w-4 text-muted-foreground\" />\n                <span className=\"text-sm text-muted-foreground\">Vehicle</span>\n              </div>\n              <div className=\"text-right\">\n                <div className=\"text-sm font-medium\">\n                  {trip.vehicle.color} {trip.vehicle.make} {trip.vehicle.model}\n                </div>\n                <div className=\"text-xs text-muted-foreground\">\n                  License: {trip.vehicle.plate}\n                </div>\n              </div>\n            </div>\n          )}\n        </div>\n\n        {/* Action buttons */}\n        <div className=\"flex gap-3\">\n          <button className=\"flex flex-1 items-center justify-center gap-2 rounded-lg bg-primary px-4 py-2.5 font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2\">\n            <Map className=\"h-4 w-4\" />\n            Track Ride\n          </button>\n          <button className=\"flex flex-1 items-center justify-center gap-2 rounded-lg border bg-background px-4 py-2.5 font-medium text-foreground transition-colors hover:bg-muted focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2\">\n            <Share2 className=\"h-4 w-4\" />\n            Share Trip\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/components/RideQuote.tsx",
    "content": "/**\n * RideQuote Tool UI Component\n *\n * ARCHITECTURE LAYER 2: Tool UI\n * - Receives props and renders UI\n * - No tool calls, no async logic\n * - Callbacks passed from orchestrator\n * - States: \"interactive\" | \"receipt\"\n *\n * @see ../../../../../COLLAB_GUIDELINES.md\n */\n\n\"use client\";\n\nimport type { RideQuoteProps } from \"../types\";\nimport { MapPin, Clock, DollarSign, CheckCircle } from \"lucide-react\";\n\nexport function RideQuote({\n  state,\n  quote,\n  paymentMethod,\n  onConfirm,\n}: RideQuoteProps) {\n  const formatPrice = (amount: number, currency: string) => {\n    if (currency === \"USD\") return `$${amount.toFixed(2)}`;\n    return `${amount} ${currency}`;\n  };\n\n  const formatPayment = () => {\n    if (!paymentMethod) return \"No payment method\";\n\n    if (paymentMethod.type === \"apple_pay\") return \"Apple Pay\";\n    if (paymentMethod.type === \"google_pay\") return \"Google Pay\";\n    if (paymentMethod.type === \"card\" && paymentMethod.brand) {\n      const brand =\n        paymentMethod.brand.charAt(0).toUpperCase() +\n        paymentMethod.brand.slice(1);\n      return `${brand} (...${paymentMethod.last4})`;\n    }\n\n    return \"Payment method\";\n  };\n\n  if (state === \"receipt\") {\n    // Receipt mode - collapsed summary\n    return (\n      <div className=\"rounded-lg border border-green-200 bg-green-50 p-4 dark:border-green-800 dark:bg-green-950\">\n        <div className=\"flex items-center gap-2\">\n          <CheckCircle className=\"h-5 w-5 text-green-600 dark:text-green-400\" />\n          <span className=\"font-medium text-green-900 dark:text-green-100\">\n            Ride confirmed\n          </span>\n        </div>\n        <div className=\"mt-2 text-sm text-green-800 dark:text-green-200\">\n          <div>\n            {quote.pickup.name} → {quote.dropoff.name}\n          </div>\n          <div className=\"mt-1 text-xs text-green-700 dark:text-green-300\">\n            {quote.etaMinutes} min •{\" \"}\n            {formatPrice(quote.price.amount, quote.price.currency)}\n          </div>\n        </div>\n      </div>\n    );\n  }\n\n  // Interactive mode - full card\n  return (\n    <div className=\"bg-card rounded-lg border p-6 shadow-sm\">\n      {/* Header */}\n      <div className=\"mb-4\">\n        <h3 className=\"text-foreground text-lg font-semibold\">\n          Waymo Ride Quote\n        </h3>\n      </div>\n\n      {/* Route */}\n      <div className=\"mb-4 space-y-3\">\n        <div className=\"flex items-start gap-3\">\n          <div className=\"mt-1\">\n            <div className=\"flex h-2 w-2 rounded-full bg-blue-600\" />\n          </div>\n          <div className=\"flex-1\">\n            <div className=\"text-muted-foreground text-sm\">Pickup</div>\n            <div className=\"font-medium\">{quote.pickup.name}</div>\n            <div className=\"text-muted-foreground text-sm\">\n              {quote.pickup.address}\n            </div>\n          </div>\n        </div>\n\n        <div className=\"border-muted-foreground/30 ml-1 h-8 w-0 border-l-2 border-dashed\" />\n\n        <div className=\"flex items-start gap-3\">\n          <div className=\"mt-1\">\n            <MapPin className=\"text-muted-foreground h-4 w-4\" />\n          </div>\n          <div className=\"flex-1\">\n            <div className=\"text-muted-foreground text-sm\">Dropoff</div>\n            <div className=\"font-medium\">{quote.dropoff.name}</div>\n            <div className=\"text-muted-foreground text-sm\">\n              {quote.dropoff.address}\n            </div>\n          </div>\n        </div>\n      </div>\n\n      {/* Details */}\n      <div className=\"bg-muted/50 mb-4 rounded-lg p-4\">\n        <div className=\"flex items-center justify-between\">\n          <div className=\"flex items-center gap-4\">\n            <div className=\"flex items-center gap-2\">\n              <Clock className=\"text-muted-foreground h-4 w-4\" />\n              <span className=\"font-medium\">{quote.etaMinutes} minutes</span>\n            </div>\n            <div className=\"flex items-center gap-2\">\n              <DollarSign className=\"text-muted-foreground h-4 w-4\" />\n              <span className=\"font-medium\">\n                {formatPrice(quote.price.amount, quote.price.currency)}\n                {quote.price.isEstimate && (\n                  <span className=\"text-muted-foreground ml-1 text-xs\">\n                    (estimate)\n                  </span>\n                )}\n              </span>\n            </div>\n          </div>\n        </div>\n        <div className=\"text-muted-foreground mt-2 text-sm\">\n          {quote.vehicleInfo.type} • Seats {quote.vehicleInfo.capacity}\n        </div>\n      </div>\n\n      {/* Payment */}\n      <div className=\"bg-background mb-6 flex items-center justify-between rounded-lg border p-3\">\n        <span className=\"text-muted-foreground text-sm\">Payment</span>\n        <span className=\"text-sm font-medium\">{formatPayment()}</span>\n      </div>\n\n      {/* Confirm button */}\n      <button\n        onClick={onConfirm}\n        className=\"bg-primary text-primary-foreground hover:bg-primary/90 focus:ring-primary w-full rounded-lg px-4 py-3 font-medium transition-colors focus:ring-2 focus:ring-offset-2 focus:outline-none\"\n      >\n        Confirm Ride\n      </button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/components/index.tsx",
    "content": "export { RideQuote } from \"./RideQuote\";\nexport { BookingConfirmation } from \"./BookingConfirmation\";\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/confirm-ride-booking.ts",
    "content": "import { MOCK_NOW } from \"./shared\";\nimport type { RideSummary } from \"./shared\";\n\ntype ConfirmRideBookingArgs = RideSummary;\n\ntype ConfirmRideBookingResult = {\n  confirmation: {\n    rideId: string;\n    provider: string;\n    service: string;\n    etaMinutes: number;\n    price: number;\n    currency: string;\n    pickup: string;\n    destination: string;\n    status: \"scheduled\";\n    pickupWindow: string;\n    bookedAt: string;\n  };\n};\n\nconst buildRideId = (provider: string, service: string) => {\n  const base = `${provider}-${service}`.toUpperCase();\n  const suffix = base.replace(/[^A-Z0-9]/gi, \"\").slice(0, 5) || \"RIDE\";\n  return `RIDE-MOCK-${suffix}`;\n};\n\nexport const confirmRideBooking = async ({\n  provider,\n  service,\n  etaMinutes,\n  price,\n  currency,\n  pickup,\n  destination,\n}: ConfirmRideBookingArgs): Promise<ConfirmRideBookingResult> => {\n  return {\n    confirmation: {\n      rideId: buildRideId(provider, service),\n      provider,\n      service,\n      etaMinutes,\n      price,\n      currency: currency ?? \"USD\",\n      pickup,\n      destination,\n      status: \"scheduled\",\n      pickupWindow: \"2024-06-01T12:06:00Z–12:11:00Z\",\n      bookedAt: MOCK_NOW,\n    },\n  };\n};\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/confirm-user-payment.ts",
    "content": "import { MOCK_NOW } from \"./shared\";\n\ntype ConfirmUserPaymentArgs = {\n  amount: number;\n  currency?: string;\n};\n\ntype ConfirmUserPaymentResult = {\n  status: \"authorized\";\n  amount: number;\n  currency: string;\n  authId: string;\n  authorizedAt: string;\n};\n\nexport const confirmUserPayment = async ({\n  amount,\n  currency,\n}: ConfirmUserPaymentArgs): Promise<ConfirmUserPaymentResult> => {\n  return {\n    status: \"authorized\",\n    amount,\n    currency: currency ?? \"USD\",\n    authId: \"AUTH-MOCK-0001\",\n    authorizedAt: MOCK_NOW,\n  };\n};\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/get-profile-context.ts",
    "content": "import {\n  DEFAULT_FAVORITES,\n  DEFAULT_PICKUP,\n  DEFAULT_RECENTS,\n  MOCK_NOW,\n} from \"./shared\";\n\ntype GetProfileContextResult = {\n  asOf: string;\n  defaultPickup: typeof DEFAULT_PICKUP;\n  favorites: typeof DEFAULT_FAVORITES;\n  recents: typeof DEFAULT_RECENTS;\n  defaultPayment: { type: \"card\"; brand: \"Visa\"; last4: \"4242\" };\n  prefs: { departMode: \"now\" };\n};\n\nexport const getProfileContext = async (): Promise<GetProfileContextResult> => {\n  return {\n    asOf: MOCK_NOW,\n    defaultPickup: DEFAULT_PICKUP,\n    favorites: DEFAULT_FAVORITES,\n    recents: DEFAULT_RECENTS,\n    defaultPayment: { type: \"card\", brand: \"Visa\", last4: \"4242\" },\n    prefs: { departMode: \"now\" },\n  };\n};\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/get-user-destination.ts",
    "content": "import { DESTINATIONS } from \"./shared\";\n\ntype GetUserDestinationArgs = {\n  promptHint?: string;\n};\n\ntype Destination = typeof DESTINATIONS.home | typeof DESTINATIONS.ferry;\n\ntype GetUserDestinationResult = {\n  destination: Destination;\n};\n\nexport const getUserDestination = async ({\n  promptHint,\n}: GetUserDestinationArgs): Promise<GetUserDestinationResult> => {\n  const useHome =\n    promptHint !== undefined && promptHint.toLowerCase().includes(\"home\");\n  const destination = useHome ? DESTINATIONS.home : DESTINATIONS.ferry;\n  return { destination };\n};\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/get-user-location.ts",
    "content": "import { DEFAULT_PICKUP } from \"./shared\";\n\ntype GetUserLocationArgs = {\n  allow_gps?: boolean;\n};\n\ntype GetUserLocationResult = {\n  pickup: typeof DEFAULT_PICKUP;\n};\n\nexport const getUserLocation = async (\n  _args: GetUserLocationArgs,\n): Promise<GetUserLocationResult> => {\n  return { pickup: DEFAULT_PICKUP };\n};\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/index.ts",
    "content": "import { z } from \"zod\";\n\nimport { WAYMO_SYSTEM_MESSAGE_V2 } from \"./system-message-v2\";\n\nimport {\n  candidateArraySchema,\n  paymentMethodEnum,\n  rideSummarySchema,\n} from \"./shared\";\nimport { getUserLocation } from \"./get-user-location\";\nimport { toggleGps } from \"./toggle-gps\";\nimport { getUserDestination } from \"./get-user-destination\";\nimport { checkRidePrices } from \"./check-ride-prices\";\nimport { requestPaymentMethod } from \"./request-payment-method\";\nimport { confirmUserPayment } from \"./confirm-user-payment\";\nimport { showRideDetails } from \"./show-ride-details\";\nimport { getProfileContext } from \"./get-profile-context\";\nimport { searchPlaces } from \"./search-places\";\nimport { precheckPrices } from \"./precheck-prices\";\nimport { scheduleRide } from \"./schedule-ride\";\nimport { showRideOptions } from \"./show-ride-options\";\nimport { confirmRideBooking } from \"./confirm-ride-booking\";\nimport type { Prototype } from \"../../types\";\n\nconst getUserLocationInput = z.object({\n  allow_gps: z.boolean().optional(),\n});\n\nconst toggleGpsInput = z.object({\n  isEnabled: z.boolean().optional(),\n});\n\nconst getUserDestinationInput = z.object({\n  promptHint: z.string().optional(),\n});\n\nconst checkRidePricesInput = z.object({\n  pickup: z.string().min(1),\n  destination: z.string().min(1),\n  departureTime: z.string().optional(),\n  services: z.array(z.string().min(1)).min(1).optional(),\n});\n\nconst requestPaymentMethodInput = z.object({\n  preferred: paymentMethodEnum.optional(),\n});\n\nconst confirmUserPaymentInput = z.object({\n  amount: z.number().positive(),\n  currency: z.string().optional(),\n});\n\nconst rideSummaryInput = rideSummarySchema;\n\nconst searchPlacesInput = z.object({\n  query: z.string().min(1),\n  near: z.string().optional(),\n});\n\nconst precheckPricesInput = z.object({\n  pickup: z.string().min(1),\n  candidates: candidateArraySchema,\n});\n\nconst showRideOptionsInput = z.object({\n  pickup: z.string().min(1),\n  candidates: candidateArraySchema,\n});\n\nconst getProfileContextInput = z.object({});\n\nexport const waymoPrototype: Prototype = {\n  slug: \"waymo-booking\",\n  title: \"Waymo Booking Assistant\",\n  summary: \"Guide riders through booking Waymo autonomous rides\",\n  systemPrompt: WAYMO_SYSTEM_MESSAGE_V2,\n  tools: [\n    {\n      name: \"get_user_location\",\n      description:\n        \"Collect or confirm the user's pickup location before proposing destinations.\",\n      uiId: \"fallback\",\n      input: getUserLocationInput,\n      execute: async (rawArgs: unknown) =>\n        getUserLocation(getUserLocationInput.parse(rawArgs ?? {})),\n    },\n    {\n      name: \"toggle_gps\",\n      description:\n        \"Toggle GPS usage for the rider and confirm the latest state to the assistant.\",\n      uiId: \"fallback\",\n      input: toggleGpsInput,\n      execute: async (rawArgs: unknown) =>\n        toggleGps(toggleGpsInput.parse(rawArgs ?? {})),\n    },\n    {\n      name: \"get_user_destination\",\n      description:\n        \"Collect or confirm the rider's destination before quoting prices.\",\n      uiId: \"fallback\",\n      input: getUserDestinationInput,\n      execute: async (rawArgs: unknown) =>\n        getUserDestination(getUserDestinationInput.parse(rawArgs ?? {})),\n    },\n    {\n      name: \"check_ride_prices\",\n      description:\n        \"Return estimated ETAs and fares for available Waymo ride options.\",\n      uiId: \"fallback\",\n      input: checkRidePricesInput,\n      execute: async (rawArgs: unknown) =>\n        checkRidePrices(checkRidePricesInput.parse(rawArgs)),\n    },\n    {\n      name: \"request_payment_method\",\n      description:\n        \"Confirm the payment instrument the rider would like to use.\",\n      uiId: \"fallback\",\n      input: requestPaymentMethodInput,\n      execute: async (rawArgs: unknown) =>\n        requestPaymentMethod(requestPaymentMethodInput.parse(rawArgs ?? {})),\n    },\n    {\n      name: \"confirm_user_payment\",\n      description:\n        \"Authorize the quoted fare before booking the ride with Waymo.\",\n      uiId: \"fallback\",\n      input: confirmUserPaymentInput,\n      execute: async (rawArgs: unknown) =>\n        confirmUserPayment(confirmUserPaymentInput.parse(rawArgs)),\n    },\n    {\n      name: \"show_ride_details\",\n      description:\n        \"Echo back the selected ride details and provide a summary receipt.\",\n      uiId: \"fallback\",\n      input: rideSummaryInput,\n      execute: async (rawArgs: unknown) =>\n        showRideDetails(rideSummaryInput.parse(rawArgs)),\n    },\n    {\n      name: \"get_profile_context\",\n      description:\n        \"Retrieve rider favorites, recents, and default pickups to power anticipatory suggestions.\",\n      uiId: \"fallback\",\n      input: getProfileContextInput,\n      execute: async (rawArgs: unknown) => {\n        getProfileContextInput.parse(rawArgs ?? {});\n        return getProfileContext();\n      },\n    },\n    {\n      name: \"search_places\",\n      description:\n        \"Search for places near a context to provide escape hatch destination selection.\",\n      uiId: \"fallback\",\n      input: searchPlacesInput,\n      execute: async (rawArgs: unknown) =>\n        searchPlaces(searchPlacesInput.parse(rawArgs)),\n    },\n    {\n      name: \"precheck_prices\",\n      description:\n        \"Pre-check ETAs and fares for suggested destinations to power anticipatory prompts.\",\n      uiId: \"fallback\",\n      input: precheckPricesInput,\n      execute: async (rawArgs: unknown) =>\n        precheckPrices(precheckPricesInput.parse(rawArgs)),\n    },\n    {\n      name: \"schedule_ride\",\n      description:\n        \"Schedule the selected ride after payment authorization and return a confirmation receipt.\",\n      uiId: \"fallback\",\n      input: rideSummaryInput,\n      execute: async (rawArgs: unknown) =>\n        scheduleRide(rideSummaryInput.parse(rawArgs)),\n    },\n    {\n      name: \"show_ride_options\",\n      description:\n        \"Display anticipatory ride options for quick booking based on profile and search data.\",\n      uiId: \"fallback\",\n      input: showRideOptionsInput,\n      execute: async (rawArgs: unknown) =>\n        showRideOptions(showRideOptionsInput.parse(rawArgs)),\n    },\n    {\n      name: \"confirm_ride_booking\",\n      description:\n        \"Finalize the ride after scheduling and present a final confirmation receipt.\",\n      uiId: \"fallback\",\n      input: rideSummaryInput,\n      execute: async (rawArgs: unknown) =>\n        confirmRideBooking(rideSummaryInput.parse(rawArgs)),\n    },\n  ],\n};\n\nexport { WaymoDemo } from \"./WaymoDemo\";\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/precheck-prices.ts",
    "content": "import { MOCK_NOW, SUGGESTION_REASONS, SUGGESTION_SERVICES } from \"./shared\";\nimport type { Candidate } from \"./shared\";\n\ntype Suggestion = {\n  id: string;\n  label: string;\n  address: string;\n  topService: {\n    service: (typeof SUGGESTION_SERVICES)[number];\n    etaMinutes: number;\n    price: number;\n    currency: \"USD\";\n  };\n  reasonTag: (typeof SUGGESTION_REASONS)[number];\n};\n\ntype PrecheckPricesArgs = {\n  pickup: string;\n  candidates: Candidate[];\n};\n\ntype PrecheckPricesResult = {\n  asOf: string;\n  suggestions: Suggestion[];\n};\n\nexport const precheckPrices = async ({\n  candidates,\n}: PrecheckPricesArgs): Promise<PrecheckPricesResult> => {\n  const suggestions = candidates.slice(0, 3).map((candidate, index) => {\n    const service = SUGGESTION_SERVICES[index % SUGGESTION_SERVICES.length];\n    const eta = [3, 7, 12][index % 3];\n    const price = [17.5, 22.25, 32.0][index % 3];\n    const reason = SUGGESTION_REASONS[index % SUGGESTION_REASONS.length];\n\n    return {\n      id: candidate.id,\n      label: candidate.label,\n      address: candidate.address,\n      topService: {\n        service,\n        etaMinutes: eta,\n        price,\n        currency: \"USD\" as const,\n      },\n      reasonTag: reason,\n    };\n  });\n\n  return {\n    asOf: MOCK_NOW,\n    suggestions,\n  };\n};\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/request-payment-method.ts",
    "content": "import type { PaymentMethod } from \"./shared\";\n\ntype RequestPaymentMethodArgs = {\n  preferred?: PaymentMethod;\n};\n\ntype PaymentInstrument =\n  | { type: \"card\"; brand: \"Visa\"; last4: \"4242\" }\n  | { type: Exclude<PaymentMethod, \"card\"> };\n\ntype RequestPaymentMethodResult = {\n  selected: PaymentMethod;\n  instrument: PaymentInstrument;\n};\n\nexport const requestPaymentMethod = async ({\n  preferred,\n}: RequestPaymentMethodArgs): Promise<RequestPaymentMethodResult> => {\n  const selected = preferred ?? \"card\";\n  const instrument: PaymentInstrument =\n    selected === \"card\"\n      ? { type: \"card\", brand: \"Visa\", last4: \"4242\" }\n      : { type: selected };\n\n  return {\n    selected,\n    instrument,\n  };\n};\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/schedule-ride.ts",
    "content": "import type { RideSummary } from \"./shared\";\n\ntype ScheduleRideArgs = RideSummary;\n\ntype ScheduleRideResult = {\n  confirmation: {\n    rideId: string;\n    provider: string;\n    service: string;\n    etaMinutes: number;\n    price: number;\n    currency: string;\n    pickup: string;\n    destination: string;\n    status: \"scheduled\";\n    pickupWindow: string;\n  };\n};\n\nexport const scheduleRide = async ({\n  provider,\n  service,\n  etaMinutes,\n  price,\n  currency,\n  pickup,\n  destination,\n}: ScheduleRideArgs): Promise<ScheduleRideResult> => {\n  return {\n    confirmation: {\n      rideId: \"RIDE-MOCK-23456\",\n      provider,\n      service,\n      etaMinutes,\n      price,\n      currency: currency ?? \"USD\",\n      pickup,\n      destination,\n      status: \"scheduled\",\n      pickupWindow: \"2024-06-01T12:06:00Z–12:11:00Z\",\n    },\n  };\n};\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/search-places.ts",
    "content": "import { PLACE_CHOICES } from \"./shared\";\n\ntype SearchPlacesArgs = {\n  query: string;\n  near?: string;\n};\n\ntype PlaceChoice = (typeof PLACE_CHOICES)[number];\n\ntype SearchPlacesResult = {\n  near: string | null;\n  choices: PlaceChoice[];\n};\n\nexport const searchPlaces = async ({\n  query,\n  near,\n}: SearchPlacesArgs): Promise<SearchPlacesResult> => {\n  const normalized = query.toLowerCase();\n  const choices = PLACE_CHOICES.filter(\n    (choice) =>\n      choice.label.toLowerCase().includes(normalized) ||\n      choice.address.toLowerCase().includes(normalized),\n  );\n\n  return {\n    near: near ?? null,\n    choices,\n  };\n};\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/select-frequent-location-tool.tsx",
    "content": "\"use client\";\n\nimport type { ToolDefinition } from \"@assistant-ui/react\";\nimport { z } from \"zod\";\nimport {\n  FrequentLocationSelector,\n  SelectFrequentLocationResult,\n} from \"./wip-tool-uis/FrequentLocationSelector\";\n\nexport const SelectFrequentLocationTool: ToolDefinition<\n  Record<string, never>,\n  SelectFrequentLocationResult\n> = {\n  description:\n    \"Present the user's frequent locations (favorites like Home and Work, plus recents) when they request a ride without specifying a destination. Use this to show a visual location picker UI. When the tool result includes `selectedLocation`, treat that as the rider's confirmed destination and continue without calling this tool again.\",\n  parameters: z.object({}),\n  type: \"human\",\n  render: (props) => <FrequentLocationSelector {...props} />,\n};\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/shared.ts",
    "content": "import { z } from \"zod\";\n\nexport const MOCK_NOW = \"2024-06-01T12:00:00.000Z\";\n\nexport const DEFAULT_PICKUP = {\n  address: \"500 Howard St, San Francisco, CA 94105\",\n  label: \"Current location\",\n  lat: 37.78825,\n  lng: -122.39716,\n  placeId: \"mock-pickup-1\",\n} as const;\n\nexport const DEFAULT_FAVORITES = [\n  {\n    id: \"fav-home\",\n    label: \"Home\",\n    address: \"123 Market St, San Francisco, CA 94103\",\n  },\n  {\n    id: \"fav-work\",\n    label: \"Work\",\n    address: \"1 Market St, San Francisco, CA 94105\",\n  },\n] as const;\n\nexport const DEFAULT_RECENTS = [\n  {\n    id: \"recent-airport\",\n    label: \"SFO\",\n    address: \"San Francisco International Airport, CA 94128\",\n  },\n] as const;\n\nexport const RIDE_OPTIONS = [\n  {\n    provider: \"Waymo\",\n    service: \"Standard\",\n    etaMinutes: 3,\n    price: 17.5,\n    currency: \"USD\",\n  },\n  {\n    provider: \"Waymo\",\n    service: \"XL\",\n    etaMinutes: 4,\n    price: 24.8,\n    currency: \"USD\",\n  },\n  {\n    provider: \"Waymo\",\n    service: \"Premium\",\n    etaMinutes: 5,\n    price: 32.0,\n    currency: \"USD\",\n  },\n] as const;\n\nexport const DESTINATIONS = {\n  home: {\n    address: \"123 Market St, San Francisco, CA 94103\",\n    lat: 37.78223,\n    lng: -122.40904,\n    label: \"Home\",\n    placeId: \"mock-dest-home-1\",\n  },\n  ferry: {\n    address: \"1 Ferry Building, San Francisco, CA 94111\",\n    lat: 37.7955,\n    lng: -122.3937,\n    label: \"Ferry Building\",\n    placeId: \"mock-dest-1\",\n  },\n} as const;\n\nexport const PLACE_CHOICES = [\n  {\n    id: \"plc-1\",\n    label: \"Ferry Building\",\n    address: \"1 Ferry Building, San Francisco, CA 94111\",\n  },\n  {\n    id: \"plc-2\",\n    label: \"Oracle Park\",\n    address: \"24 Willie Mays Plaza, San Francisco, CA 94107\",\n  },\n  {\n    id: \"plc-3\",\n    label: \"Chase Center\",\n    address: \"1 Warriors Way, San Francisco, CA 94158\",\n  },\n] as const;\n\nexport const SUGGESTION_SERVICES = [\"Standard\", \"XL\", \"Premium\"] as const;\n\nexport const SUGGESTION_REASONS = [\"frequent\", \"workday\", \"recent\"] as const;\n\nexport const PAYMENT_METHODS = [\"apple_pay\", \"google_pay\", \"card\"] as const;\n\nexport type PaymentMethod = (typeof PAYMENT_METHODS)[number];\n\nexport const paymentMethodEnum = z.enum(PAYMENT_METHODS);\n\nexport const candidateSchema = z.object({\n  id: z.string().min(1),\n  label: z.string().min(1),\n  address: z.string().min(1),\n});\n\nexport const candidateArraySchema = z.array(candidateSchema).min(1);\n\nexport type Candidate = z.infer<typeof candidateSchema>;\n\nexport const rideSummarySchema = z.object({\n  provider: z.string().min(1),\n  service: z.string().min(1),\n  etaMinutes: z.number().min(0),\n  price: z.number().min(0),\n  currency: z.string().optional(),\n  pickup: z.string().min(1),\n  destination: z.string().min(1),\n});\n\nexport type RideSummary = z.infer<typeof rideSummarySchema>;\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/show-ride-details.ts",
    "content": "import type { RideSummary } from \"./shared\";\n\ntype ShowRideDetailsArgs = RideSummary;\n\ntype ShowRideDetailsResult = {\n  confirmation: {\n    rideId: string;\n    provider: string;\n    service: string;\n    etaMinutes: number;\n    price: number;\n    currency: string;\n    pickup: string;\n    destination: string;\n    status: \"scheduled\";\n    pickupWindow: string;\n  };\n};\n\nexport const showRideDetails = async ({\n  provider,\n  service,\n  etaMinutes,\n  price,\n  currency,\n  pickup,\n  destination,\n}: ShowRideDetailsArgs): Promise<ShowRideDetailsResult> => {\n  return {\n    confirmation: {\n      rideId: \"RIDE-MOCK-12345\",\n      provider,\n      service,\n      etaMinutes,\n      price,\n      currency: currency ?? \"USD\",\n      pickup,\n      destination,\n      status: \"scheduled\",\n      pickupWindow: \"2024-06-01T12:05:00Z–12:10:00Z\",\n    },\n  };\n};\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/show-ride-options.ts",
    "content": "import {\n  DEFAULT_PICKUP,\n  MOCK_NOW,\n  SUGGESTION_REASONS,\n  SUGGESTION_SERVICES,\n} from \"./shared\";\nimport type { Candidate } from \"./shared\";\n\ntype ShowRideOptionsArgs = {\n  pickup: string;\n  candidates: Candidate[];\n};\n\ntype RideSuggestionsResult = {\n  asOf: string;\n  pickup: {\n    address: string;\n    label: string;\n  };\n  suggestions: Array<{\n    id: string;\n    label: string;\n    address: string;\n    topService: {\n      service: (typeof SUGGESTION_SERVICES)[number];\n      etaMinutes: number;\n      price: number;\n      currency: \"USD\";\n    };\n    reasonTag: (typeof SUGGESTION_REASONS)[number];\n  }>;\n  defaultPayment: { type: \"card\"; brand: \"Visa\"; last4: \"4242\" };\n};\n\nexport const showRideOptions = async ({\n  pickup,\n  candidates,\n}: ShowRideOptionsArgs): Promise<RideSuggestionsResult> => {\n  const suggestions = candidates.slice(0, 3).map((candidate, index) => {\n    const service = SUGGESTION_SERVICES[index % SUGGESTION_SERVICES.length];\n    const eta = [3, 7, 12][index % 3];\n    const price = [17.5, 22.25, 45.0][index % 3];\n    const reason = SUGGESTION_REASONS[index % SUGGESTION_REASONS.length];\n\n    return {\n      id: candidate.id,\n      label: candidate.label,\n      address: candidate.address,\n      topService: {\n        service,\n        etaMinutes: eta,\n        price,\n        currency: \"USD\" as const,\n      },\n      reasonTag: reason,\n    };\n  });\n\n  return {\n    asOf: MOCK_NOW,\n    pickup: {\n      address: pickup,\n      label: DEFAULT_PICKUP.label,\n    },\n    suggestions,\n    defaultPayment: { type: \"card\", brand: \"Visa\", last4: \"4242\" },\n  };\n};\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/system-message-v2.ts",
    "content": "export const WAYMO_SYSTEM_MESSAGE_V2 =\n  `You are a helpful assistant that guides riders through booking Waymo autonomous rides with clarity and safety.\n\nYour goals:\n- Help the rider confirm their pickup location and destination.\n- Explain available Waymo ride options, ETAs, and prices.\n- Walk the rider through payment authorization and booking.\n- Clearly summarize key details (pickup, destination, ETA, price) before and after booking.\n\nGuidelines:\n- Ask focused clarifying questions when required to proceed (e.g. missing pickup or destination).\n- Prefer concise, structured responses over long paragraphs.\n- When using tools, rely on their outputs as the source of truth for ride details.\n- If something is ambiguous or unsupported by tool data, say so explicitly instead of guessing.\n\nTool Usage:\n- When the user requests a ride WITHOUT specifying a destination (e.g., \"I need a ride\", \"Book me a Waymo\"), first call select_frequent_location to present their saved locations (Home, Work, etc.) in a visual UI picker. Once that tool's result includes \\`selectedLocation\\`, acknowledge the choice and continue the flow without calling the picker again.\n- If the user mentions a specific destination (e.g., \"Take me home\", \"I need to go to SFO\"), use get_user_destination instead to confirm that specific location.\n- Use get_profile_context to retrieve favorites and recents data for internal context, but use select_frequent_location when you want to show the user a visual picker UI.` as const;\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/toggle-gps.ts",
    "content": "type ToggleGpsArgs = {\n  isEnabled?: boolean;\n};\n\ntype ToggleGpsResult = {\n  isEnabled: boolean;\n};\n\nexport const toggleGps = async ({\n  isEnabled = false,\n}: ToggleGpsArgs): Promise<ToggleGpsResult> => {\n  return { isEnabled };\n};\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/tools.ts",
    "content": "/**\n * Core Waymo tools implementation (v0)\n *\n * ARCHITECTURE LAYER 1: Tools\n * - Pure functions that return typed mock data\n * - No React, no UI concerns\n * - Exactly 4 tools following COLLAB_GUIDELINES.md\n *\n * @see ../../../../COLLAB_GUIDELINES.md\n */\n\nimport type {\n  RiderContext,\n  Location,\n  PaymentMethod,\n  RideQuote,\n  BookedTrip,\n  GetPickupLocationInput,\n  GetPickupLocationOutput,\n  GetQuoteInput,\n  BookTripInput,\n} from \"./types\";\n\n// Mock data\nconst MOCK_HOME: Location = {\n  address: \"123 Main Street, San Francisco, CA 94105\",\n  lat: 37.7749,\n  lng: -122.4194,\n  name: \"Home\",\n};\n\nconst MOCK_WORK: Location = {\n  address: \"456 Office Blvd, San Francisco, CA 94103\",\n  lat: 37.7751,\n  lng: -122.418,\n  name: \"Work\",\n};\n\nconst MOCK_COFFEE_SHOP: Location = {\n  address: \"789 Downtown Ave, San Francisco, CA 94102\",\n  lat: 37.7755,\n  lng: -122.4186,\n  name: \"Downtown Coffee Shop\",\n};\n\nconst MOCK_DEFAULT_PAYMENT: PaymentMethod = {\n  id: \"pm_1\",\n  type: \"apple_pay\",\n  isDefault: true,\n};\n\nconst MOCK_PAYMENT_METHODS: PaymentMethod[] = [\n  MOCK_DEFAULT_PAYMENT,\n  {\n    id: \"pm_2\",\n    type: \"card\",\n    brand: \"visa\",\n    last4: \"4242\",\n    isDefault: false,\n  },\n];\n\n// Tool 1: Get rider context\nexport async function getRiderContext(options?: {\n  noHome?: boolean;\n}): Promise<RiderContext> {\n  // Simulate network delay\n  await new Promise((resolve) => setTimeout(resolve, 300));\n\n  // For friction variant: return no home address\n  if (options?.noHome) {\n    return {\n      home: null,\n      work: MOCK_WORK,\n      frequentDestinations: [MOCK_WORK],\n      recentDestinations: [\n        {\n          address: \"1 Market St, San Francisco, CA\",\n          lat: 37.7946,\n          lng: -122.3999,\n          name: \"Ferry Building\",\n        },\n      ],\n      defaultPaymentMethod: MOCK_DEFAULT_PAYMENT,\n      paymentMethods: MOCK_PAYMENT_METHODS,\n    };\n  }\n\n  return {\n    home: MOCK_HOME,\n    work: MOCK_WORK,\n    frequentDestinations: [MOCK_HOME, MOCK_WORK],\n    recentDestinations: [\n      {\n        address: \"1 Market St, San Francisco, CA\",\n        lat: 37.7946,\n        lng: -122.3999,\n        name: \"Ferry Building\",\n      },\n    ],\n    defaultPaymentMethod: MOCK_DEFAULT_PAYMENT,\n    paymentMethods: MOCK_PAYMENT_METHODS,\n  };\n}\n\n// Helper to resolve a manual address input\nexport async function resolveAddress(query: string): Promise<Location> {\n  // Simulate network delay\n  await new Promise((resolve) => setTimeout(resolve, 400));\n\n  // Mock geocoding - in reality this would hit a geocoding API\n  return {\n    address: query,\n    lat: 37.7858,\n    lng: -122.4064,\n    name: query.split(\",\")[0], // Use first part as name\n  };\n}\n\n// Tool 2: Get pickup location\nexport async function getPickupLocation(\n  _input: GetPickupLocationInput,\n): Promise<GetPickupLocationOutput> {\n  // Simulate network delay\n  await new Promise((resolve) => setTimeout(resolve, 200));\n\n  // For v0, always return high confidence coffee shop location\n  // Note: In a real implementation, we'd use the input parameter\n  return {\n    resolvedLocation: MOCK_COFFEE_SHOP,\n    confidence: \"high\",\n  };\n}\n\n// Tool 3: Get quote\nlet quoteCounter = 0;\nexport async function getQuote(input: GetQuoteInput): Promise<RideQuote> {\n  // Simulate network delay\n  await new Promise((resolve) => setTimeout(resolve, 500));\n\n  quoteCounter++;\n  const expiresAt = new Date(Date.now() + 2 * 60 * 1000).toISOString(); // 2 minutes\n\n  return {\n    quoteId: `quote_${quoteCounter}`,\n    pickup: input.pickup,\n    dropoff: input.dropoff,\n    etaMinutes: 5,\n    price: {\n      amount: 12.5,\n      currency: \"USD\",\n      isEstimate: false,\n    },\n    vehicleInfo: {\n      type: \"Waymo One\",\n      capacity: 4,\n    },\n    expiresAt,\n  };\n}\n\n// Tool 4: Book trip\nlet tripCounter = 0;\nexport async function bookTrip(input: BookTripInput): Promise<BookedTrip> {\n  // Simulate network delay\n  await new Promise((resolve) => setTimeout(resolve, 800));\n\n  tripCounter++;\n\n  // Get the payment method to use\n  const paymentMethodId = input.paymentMethodId || MOCK_DEFAULT_PAYMENT.id;\n  const paymentMethod =\n    MOCK_PAYMENT_METHODS.find((pm) => pm.id === paymentMethodId) ||\n    MOCK_DEFAULT_PAYMENT;\n\n  // Format payment summary\n  let paymentSummary = \"$12.50 charged to \";\n  if (paymentMethod.type === \"apple_pay\") {\n    paymentSummary += \"Apple Pay\";\n  } else if (paymentMethod.type === \"google_pay\") {\n    paymentSummary += \"Google Pay\";\n  } else if (paymentMethod.type === \"card\") {\n    paymentSummary += `${paymentMethod.brand?.charAt(0).toUpperCase()}${paymentMethod.brand?.slice(1)} (...${paymentMethod.last4})`;\n  }\n\n  return {\n    tripId: `trip_${tripCounter}`,\n    status: \"requested\",\n    pickup: MOCK_COFFEE_SHOP,\n    dropoff: MOCK_HOME,\n    etaMinutes: 5,\n    price: {\n      amount: 12.5,\n      currency: \"USD\",\n    },\n    paymentSummary,\n    vehicle: {\n      make: \"Jaguar\",\n      model: \"I-PACE\",\n      color: \"White\",\n      plate: \"8ABC123\",\n    },\n  };\n}\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/types.ts",
    "content": "/**\n * Shared domain types for Waymo prototype\n *\n * These types are used consistently across all three architecture layers:\n * - Tools return these types\n * - Components receive these as props\n * - Orchestrator passes these between layers\n *\n * @see ../../../../COLLAB_GUIDELINES.md\n */\n\n// Core domain types\n\nexport interface Location {\n  address: string;\n  lat: number;\n  lng: number;\n  name: string;\n}\n\nexport interface PaymentMethod {\n  id: string;\n  type: \"card\" | \"apple_pay\" | \"google_pay\";\n  brand?: string; // \"visa\", \"mastercard\", etc\n  last4?: string; // last 4 digits\n  isDefault: boolean;\n}\n\nexport interface RiderContext {\n  home: Location | null;\n  work: Location | null;\n  frequentDestinations: Location[];\n  recentDestinations: Location[];\n  defaultPaymentMethod: PaymentMethod | null;\n  paymentMethods: PaymentMethod[];\n}\n\nexport interface RideQuote {\n  quoteId: string;\n  pickup: Location;\n  dropoff: Location;\n  etaMinutes: number;\n  price: {\n    amount: number;\n    currency: string;\n    isEstimate: boolean;\n  };\n  vehicleInfo: {\n    type: string; // \"Waymo One\"\n    capacity: number;\n  };\n  expiresAt: string; // ISO8601\n}\n\nexport interface BookedTrip {\n  tripId: string;\n  status: \"requested\";\n  pickup: Location;\n  dropoff: Location;\n  etaMinutes: number;\n  price: {\n    amount: number;\n    currency: string;\n  };\n  paymentSummary: string; // \"$12.50 charged to Apple Pay (...4242)\"\n  vehicle?: {\n    make: string;\n    model: string;\n    color: string;\n    plate: string;\n  };\n}\n\n// Tool-specific types\n\nexport interface GetPickupLocationInput {\n  hint: \"current_location\" | string | { lat: number; lng: number };\n}\n\nexport interface GetPickupLocationOutput {\n  resolvedLocation: Location;\n  confidence: \"high\" | \"medium\" | \"low\";\n}\n\nexport interface GetQuoteInput {\n  pickup: Location;\n  dropoff: Location;\n}\n\nexport interface BookTripInput {\n  quoteId: string;\n  paymentMethodId?: string; // uses default if not specified\n}\n\n// Tool UI message format\n\nexport interface ToolUIMessage {\n  type: \"tool-ui\";\n  component: \"RideQuote\" | \"BookingConfirmation\";\n  props: RideQuoteProps | BookingConfirmationProps;\n}\n\n// Tool UI component props\n\nexport interface RideQuoteProps {\n  state: \"interactive\" | \"receipt\";\n  quote: RideQuote;\n  paymentMethod: PaymentMethod | null;\n  onConfirm?: () => void;\n}\n\nexport interface BookingConfirmationProps {\n  state: \"receipt\";\n  trip: BookedTrip;\n}\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/wip-tool-uis/FrequentLocationSelector.tsx",
    "content": "\"use client\";\n\nimport type { ToolCallMessagePartProps } from \"@assistant-ui/react\";\nimport { Card } from \"@/components/ui/card\";\nimport { Button } from \"@/components/ui/button\";\nimport { Home, Briefcase, MapPin, Clock, CheckCircle2 } from \"lucide-react\";\nimport { DEFAULT_FAVORITES, DEFAULT_RECENTS } from \"../shared\";\n\ntype FrequentLocation = {\n  id: string;\n  label: string;\n  address: string;\n  category: \"favorite\" | \"recent\";\n};\n\nexport type SelectFrequentLocationResult = {\n  selectedLocation?: FrequentLocation;\n};\n\nconst getLocationIcon = (label: string) => {\n  const lower = label.toLowerCase();\n  if (lower === \"home\") return <Home className=\"h-5 w-5\" />;\n  if (lower === \"work\") return <Briefcase className=\"h-5 w-5\" />;\n  return <MapPin className=\"h-5 w-5\" />;\n};\n\nconst getFavoritesAndRecents = () => {\n  const favorites: FrequentLocation[] = DEFAULT_FAVORITES.map((fav) => ({\n    id: fav.id,\n    label: fav.label,\n    address: fav.address,\n    category: \"favorite\" as const,\n  }));\n\n  const recents: FrequentLocation[] = DEFAULT_RECENTS.map((recent) => ({\n    id: recent.id,\n    label: recent.label,\n    address: recent.address,\n    category: \"recent\" as const,\n  }));\n  return {\n    locations: [...favorites, ...recents],\n    prompt: \"Where would you like to go?\",\n  };\n};\n\nexport const FrequentLocationSelector = ({\n  result,\n  addResult,\n}: ToolCallMessagePartProps<\n  Record<string, never>,\n  SelectFrequentLocationResult\n>) => {\n  // Receipt Mode - Show what was selected\n  const selectedLocation = result?.selectedLocation;\n  if (selectedLocation) {\n    return (\n      <Card className=\"max-w-md p-4\">\n        <div className=\"flex items-start gap-3\">\n          <div className=\"mt-0.5 text-green-600\">\n            <CheckCircle2 className=\"h-5 w-5\" />\n          </div>\n          <div className=\"flex-1\">\n            <div className=\"mb-1 flex items-center gap-2\">\n              <div className=\"text-muted-foreground\">\n                {getLocationIcon(selectedLocation.label)}\n              </div>\n              <span className=\"font-semibold\">{selectedLocation.label}</span>\n            </div>\n            <div className=\"text-muted-foreground text-sm\">\n              {selectedLocation.address}\n            </div>\n          </div>\n        </div>\n      </Card>\n    );\n  }\n\n  // Interactive Mode - Show location options\n  const { locations, prompt } = getFavoritesAndRecents();\n  const favorites = locations.filter((loc) => loc.category === \"favorite\");\n  const recents = locations.filter((loc) => loc.category === \"recent\");\n\n  return (\n    <Card className=\"max-w-md space-y-4 p-4\">\n      <div className=\"space-y-1\">\n        <h3 className=\"text-lg font-semibold\">{prompt}</h3>\n        <p className=\"text-muted-foreground text-sm\">\n          Select a frequent location or search for a new one\n        </p>\n      </div>\n\n      {favorites.length > 0 && (\n        <div className=\"space-y-2\">\n          <div className=\"text-muted-foreground flex items-center gap-2 text-sm font-medium\">\n            <MapPin className=\"h-3.5 w-3.5\" />\n            <span>Favorites</span>\n          </div>\n          <div className=\"space-y-2\">\n            {favorites.map((location) => (\n              <Button\n                key={location.id}\n                variant=\"outline\"\n                className=\"hover:bg-accent h-auto w-full justify-start px-4 py-3 text-left\"\n                onClick={() =>\n                  addResult({\n                    selectedLocation: location,\n                  })\n                }\n              >\n                <div className=\"flex w-full items-start gap-3\">\n                  <div className=\"text-muted-foreground mt-0.5\">\n                    {getLocationIcon(location.label)}\n                  </div>\n                  <div className=\"min-w-0 flex-1\">\n                    <div className=\"font-medium\">{location.label}</div>\n                    <div className=\"text-muted-foreground truncate text-sm\">\n                      {location.address}\n                    </div>\n                  </div>\n                </div>\n              </Button>\n            ))}\n          </div>\n        </div>\n      )}\n\n      {recents.length > 0 && (\n        <div className=\"space-y-2\">\n          <div className=\"text-muted-foreground flex items-center gap-2 text-sm font-medium\">\n            <Clock className=\"h-3.5 w-3.5\" />\n            <span>Recent</span>\n          </div>\n          <div className=\"space-y-2\">\n            {recents.map((location) => (\n              <Button\n                key={location.id}\n                variant=\"outline\"\n                className=\"hover:bg-accent h-auto w-full justify-start px-4 py-3 text-left\"\n                onClick={() =>\n                  addResult({\n                    selectedLocation: location,\n                  })\n                }\n              >\n                <div className=\"flex w-full items-start gap-3\">\n                  <div className=\"text-muted-foreground mt-0.5\">\n                    {getLocationIcon(location.label)}\n                  </div>\n                  <div className=\"min-w-0 flex-1\">\n                    <div className=\"font-medium\">{location.label}</div>\n                    <div className=\"text-muted-foreground truncate text-sm\">\n                      {location.address}\n                    </div>\n                  </div>\n                </div>\n              </Button>\n            ))}\n          </div>\n        </div>\n      )}\n\n      <div className=\"text-muted-foreground text-center text-sm\">\n        or type a different location\n      </div>\n    </Card>\n  );\n};\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/wip-tool-uis/README.md",
    "content": "# WIP Tool UIs\n\nThis directory contains **Work-In-Progress Tool UI components** for the Waymo Booking Assistant prototype. These are experimental UI components being developed and tested to determine:\n\n1. Which tools benefit from custom UI components\n2. What interaction patterns work best for different use cases\n3. Which Tool UIs should be promoted to the main Tool UI library\n\n## Purpose\n\nThe playground serves as a sandbox for:\n\n- Prototyping Tool UIs for specific AI assistant use cases\n- Experimenting with different interaction patterns\n- Validating design decisions before committing to the main library\n- Testing tool calling behavior and user flows\n\n## Current Components\n\n### FrequentLocationSelector\n\nA shadcn/ui-based component that displays a user's frequent locations (favorites and recents) as an interactive picker.\n\n**Tool:** `select_frequent_location`\n\n**Use Case:** When a user requests a ride without specifying a destination, this UI presents their most frequently used locations (Home, Work, etc.) for quick selection.\n\n**Features:**\n\n- Categorizes locations into Favorites and Recents\n- Uses contextual icons (Home, Work, generic location)\n- Provides a fallback option to search for different locations\n- Built entirely with shadcn/ui components for consistency\n\n## Promotion Criteria\n\nTool UIs in this directory may be promoted to the main Tool UI library when they:\n\n- Demonstrate clear value across multiple use cases\n- Have stable APIs and interaction patterns\n- Pass usability testing\n- Show generic applicability beyond just Waymo\n\n## Structure\n\n```\nwip-tool-uis/\n├── README.md                      # This file\n├── index.tsx                      # Exports all WIP components\n└── FrequentLocationSelector.tsx  # Location picker component\n```\n\n## Development Guidelines\n\n1. Use shadcn/ui components exclusively\n2. Follow the existing project's TypeScript and styling conventions\n3. Document the tool and use case clearly\n4. Keep components focused and composable\n5. Consider accessibility from the start\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/wip-tool-uis/TOOL-UI-PATTERNS.md",
    "content": "# Tool UI Patterns & Receipts Guide\n\nThis document outlines the patterns for creating interactive Tool UIs with receipt transformations in the Tool UI Playground.\n\n## Overview\n\n**Goal**: Create Tool UIs that transition from interactive mode → receipt mode, showing what the user selected without injecting fake messages.\n\n**Key Principle**: Tool UIs are **stateless** - their mode (interactive vs receipt) is derived from the `result` prop, not component state.\n\nThis document focuses on the frontend Toolkit/`Tools()` pattern and the interactive → receipt transformation used in the Waymo prototype. For repo-wide architecture and collaboration guidelines that apply to this and other prototypes, see `COLLAB_GUIDELINES.md` at the project root.\n\n---\n\n## Architecture\n\n### Frontend Tool Registration\n\nTools are defined as `ToolDefinition` entries and registered via `Tools({ toolkit })`. This differs from backend-only tools.\n\n**File Structure:**\n\n```\nlib/playground/prototypes/waymo/\n├── wip-tool-uis/\n│   ├── FrequentLocationSelector.tsx  # UI Component\n│   └── README.md\n├── select-frequent-location-tool.tsx  # Tool + UI Definition\n└── index.ts                           # Backend tool registry (legacy)\n```\n\n### Backend Integration\n\nFrontend tools are automatically forwarded to the backend via `AssistantChatTransport` and merged with backend tools using the `frontendTools()` helper.\n\n**Backend Flow:**\n\n1. Client sends tool definitions in request body (`tools` field)\n2. API route extracts `clientTools` from body\n3. `frontendTools(clientTools)` converts to AI SDK format\n4. Merged with prototype tools: `{ ...frontendTools(clientTools), ...prototypeTools }`\n5. Model sees all tools and can call frontend tools\n\n**Key Files:**\n\n- `app/api/playground/chat/route.ts` - Extracts client tools\n- `lib/playground/runtime.ts` - Merges frontend + backend tools\n\n---\n\n## Pattern: Interactive → Receipt Tool UI\n\n### 1. Define the Tool Definition\n\n**File**: `lib/playground/prototypes/waymo/select-frequent-location-tool.tsx`\n\n```tsx\n\"use client\";\n\nimport type { ToolDefinition } from \"@assistant-ui/react\";\nimport { z } from \"zod\";\nimport { FrequentLocationSelector } from \"./wip-tool-uis/FrequentLocationSelector\";\n\ntype FrequentLocation = {\n  id: string;\n  label: string;\n  address: string;\n  category: \"favorite\" | \"recent\";\n};\n\ntype SelectFrequentLocationResult = {\n  locations: FrequentLocation[];\n  prompt: string;\n  selectedLocation?: FrequentLocation; // ← Key: optional field for receipt mode\n};\n\nexport const SelectFrequentLocationTool: ToolDefinition<\n  Record<string, never>, // Input args (empty for this tool)\n  SelectFrequentLocationResult // Result type\n> = {\n  description:\n    \"Present user's frequent locations when they request a ride without specifying a destination.\",\n  parameters: z.object({}), // Empty params\n\n  execute: async () => {\n    // Return initial data (interactive mode)\n    return {\n      locations: [...favorites, ...recents],\n      prompt: \"Where would you like to go?\",\n      // selectedLocation is undefined here\n    };\n  },\n\n  render: (props) => <FrequentLocationSelector {...props} />,\n};\n```\n\n**Key Points:**\n\n- Result type includes **optional** `selectedLocation` field\n- `execute` returns data for interactive mode (no selection yet)\n- `render` receives full props including `result` and `addResult`\n\n---\n\n### 2. Create the UI Component\n\n**File**: `lib/playground/prototypes/waymo/wip-tool-uis/FrequentLocationSelector.tsx`\n\n```tsx\n\"use client\";\n\nimport type { ToolCallMessagePartProps } from \"@assistant-ui/react\";\nimport { Card } from \"@/components/ui/card\";\nimport { Button } from \"@/components/ui/button\";\nimport { CheckCircle2 } from \"lucide-react\";\n\ntype SelectFrequentLocationResult = {\n  locations: FrequentLocation[];\n  prompt: string;\n  selectedLocation?: FrequentLocation;\n};\n\nexport const FrequentLocationSelector = ({\n  result,\n  addResult,\n}: ToolCallMessagePartProps<\n  Record<string, never>,\n  SelectFrequentLocationResult\n>) => {\n  if (!result) return null;\n\n  // RECEIPT MODE - Show what was selected\n  if (result.selectedLocation) {\n    return (\n      <Card className=\"p-4\">\n        <div className=\"flex items-start gap-3\">\n          <CheckCircle2 className=\"h-5 w-5 text-green-600\" />\n          <div>\n            <div className=\"font-semibold\">{result.selectedLocation.label}</div>\n            <div className=\"text-sm text-muted-foreground\">\n              {result.selectedLocation.address}\n            </div>\n          </div>\n        </div>\n      </Card>\n    );\n  }\n\n  // INTERACTIVE MODE - Show clickable options\n  const { locations, prompt } = result;\n\n  return (\n    <Card className=\"p-4 space-y-4\">\n      <h3 className=\"font-semibold\">{prompt}</h3>\n\n      {locations.map((location) => (\n        <Button\n          key={location.id}\n          variant=\"outline\"\n          className=\"w-full\"\n          onClick={() =>\n            addResult({\n              ...result,\n              selectedLocation: location, // ← Add selection to result\n            })\n          }\n        >\n          <div className=\"flex flex-col items-start\">\n            <span className=\"font-medium\">{location.label}</span>\n            <span className=\"text-sm text-muted-foreground\">\n              {location.address}\n            </span>\n          </div>\n        </Button>\n      ))}\n    </Card>\n  );\n};\n```\n\n**Key Points:**\n\n- **Props**: Use `ToolCallMessagePartProps<TArgs, TResult>` type\n- **Receipt Detection**: Check `if (result.selectedLocation)`\n- **Transition**: `addResult({ ...result, selectedLocation: location })`\n- **Stateless**: Mode derived from `result` prop, not `useState`\n\n---\n\n### 3. Mount the Tool Component\n\n**File**: `app/playground/chat-pane.tsx`\n\n```tsx\nimport { SelectFrequentLocationTool } from \"@/lib/playground/prototypes/waymo/select-frequent-location-tool\";\n\nexport const ChatPane = forwardRef<ChatPaneRef, ChatPaneProps>(\n  ({ prototype }, ref) => {\n    const { slug } = prototype;\n    // ... runtime setup ...\n\n    return (\n      <AssistantRuntimeProvider runtime={runtime}>\n        {/* Mount Waymo-specific tools */}\n        {slug === \"waymo-booking\" && <SelectFrequentLocationTool />}\n\n        <ThreadPrimitive.Root>{/* ... chat UI ... */}</ThreadPrimitive.Root>\n      </AssistantRuntimeProvider>\n    );\n  },\n);\n```\n\n**Key Points:**\n\n- Tool component must be inside `AssistantRuntimeProvider`\n- Component returns `null` (doesn't render, just registers tool)\n- Conditionally mount based on prototype slug\n\n---\n\n## Flow Diagram\n\n```\nUser: \"I need a ride\"\n       ↓\nModel calls: select_frequent_location()\n       ↓\nexecute() runs → Returns { locations: [...], prompt: \"...\" }\n       ↓\nUI renders (Interactive Mode)\n  - result.selectedLocation is undefined\n  - Shows clickable location buttons\n       ↓\nUser clicks \"Home\"\n       ↓\naddResult({ ...result, selectedLocation: homeLocation })\n       ↓\nUI re-renders (Receipt Mode)\n  - result.selectedLocation exists\n  - Shows ✓ Home with checkmark\n       ↓\nModel automatically continues with selection data\n```\n\n---\n\n## Key APIs\n\n### `ToolDefinition` + toolkit key\n\n```tsx\nconst toolkit: Toolkit = {\n  some_tool_name: {\n    description: string, // Shown to model\n    parameters: ZodSchema, // Input validation\n    execute: (args, context) => TResult | Promise<TResult>,\n    render: (props: ToolCallMessagePartProps) => ReactNode,\n  },\n};\n```\n\n### `ToolCallMessagePartProps<TArgs, TResult>`\n\nAvailable props in `render` function:\n\n```tsx\n{\n  toolName: string,\n  toolCallId: string,\n  args: TArgs,                // Tool input arguments\n  argsText: string,           // JSON string of args\n  result: TResult | undefined, // Tool result (initial + updates)\n  status: {\n    type: \"running\" | \"complete\" | \"incomplete\" | \"requires_action\"\n  },\n  addResult: (result: TResult) => void,  // ← Key for receipts!\n  artifact: unknown,\n  isError: boolean | undefined\n}\n```\n\n### `addResult()`\n\n**Purpose**: Complete tool execution with a result and automatically continue the conversation.\n\n**Usage**:\n\n```tsx\nonClick={() => addResult({\n  ...result,        // Preserve initial data\n  selectedField: value  // Add user's selection\n})}\n```\n\n**Effects**:\n\n- Tool status changes to \"complete\"\n- `result` prop updates with new data\n- Component re-renders (receipt mode)\n- Model automatically continues with result data\n\n---\n\n## Backend Integration Details\n\n### API Route Pattern\n\n**File**: `app/api/playground/chat/route.ts`\n\n```tsx\nimport { frontendTools } from \"@assistant-ui/react-ai-sdk\";\n\nexport async function POST(request: NextRequest) {\n  const body = await request.json();\n  const messages = body.messages;\n  const clientTools = body.tools; // ← Frontend tools sent here\n\n  const result = streamPrototypeResponse(prototype, messages, clientTools);\n  return result.toUIMessageStreamResponse();\n}\n```\n\n### Runtime Pattern\n\n**File**: `lib/playground/runtime.ts`\n\n```tsx\nimport { frontendTools } from \"@assistant-ui/react-ai-sdk\";\n\nexport const streamPrototypeResponse = (\n  prototype: Prototype,\n  messages: UIMessage[],\n  clientTools?: unknown,\n) => {\n  const prototypeTools = buildToolSet(prototype);\n\n  // Merge frontend + backend tools\n  const tools = clientTools\n    ? {\n        ...(frontendTools(clientTools as any) as any),\n        ...prototypeTools,\n      }\n    : prototypeTools;\n\n  return streamText({\n    model,\n    system: prototype.systemPrompt,\n    messages: convertToModelMessages(messages),\n    tools, // ← Combined toolset\n    stopWhen: stepCountIs(100),\n  });\n};\n```\n\n---\n\n## Alternative: Human-in-the-Loop Pattern\n\nFor multi-step wizards or when `execute` needs user input:\n\n```tsx\nexport const MultiStepTool: ToolDefinition = {\n  execute: async (args, { human }) => {\n    // Step 1: Get user selection\n    const selection = await human({\n      type: \"location_picker\",\n      locations: [...],\n    });\n\n    // Step 2: Use selection in further logic\n    const validated = await validateLocation(selection);\n\n    return { selection, validated };\n  },\n\n  render: ({ interrupt, resume }) => {\n    if (interrupt) {\n      // Interactive mode - user is making selection\n      return <Picker\n        data={interrupt.payload}\n        onSelect={(loc) => resume(loc)}\n      />;\n    }\n    // Receipt mode\n    return <Receipt />;\n  }\n};\n```\n\n**When to use `context.human()` instead of `addResult`:**\n\n- Multi-step flows (call `human()` multiple times)\n- Execute function needs the user's selection\n- Approval workflows\n- Complex validation before proceeding\n\n---\n\n## System Prompt Guidelines\n\nUpdate the system prompt to guide when the model should call the tool:\n\n```typescript\nexport const WAYMO_SYSTEM_MESSAGE_V2 = `...\n\nTool Usage:\n- When the user requests a ride WITHOUT specifying a destination\n  (e.g., \"I need a ride\", \"Book me a Waymo\"), first call\n  select_frequent_location to present their saved locations in a visual UI.\n- If the user mentions a specific destination (e.g., \"Take me home\"),\n  use get_user_destination instead.\n` as const;\n```\n\n---\n\n## Testing Checklist\n\n- [ ] Tool appears in model's available tools\n- [ ] Interactive UI renders when tool is called\n- [ ] Clicking triggers `addResult`\n- [ ] Receipt UI renders with selection\n- [ ] Model continues conversation with selection data\n- [ ] No duplicate/fake user messages injected\n- [ ] Works after thread reset\n- [ ] TypeScript compiles without errors\n\n---\n\n## Common Issues\n\n### Tool not being called by model\n\n- **Cause**: Frontend tools not forwarded to backend\n- **Fix**: Verify `frontendTools()` is used in `runtime.ts` and `clientTools` extracted in API route\n\n### UI shows fallback instead of custom component\n\n- **Cause**: Tool component not mounted\n- **Fix**: Ensure tool component is inside `AssistantRuntimeProvider`\n\n### Receipt mode not showing\n\n- **Cause**: `addResult` not updating the field checked for receipt mode\n- **Fix**: Ensure you spread `...result` and add selection field\n\n### Receipt state not persisting across page reloads ⚠️ CRITICAL\n\n**Problem**: When users interact with a Tool UI (e.g., clicking a location), the receipt state displays correctly, but disappears after refreshing the page.\n\n**Root Cause**: The assistant-ui `ThreadHistoryAdapter` only persists **new messages**, not **updates to existing messages**. When `addResult()` is called, it updates an existing tool call message part, which doesn't trigger the history adapter's `append()` method. The adapter tracks message IDs and skips re-persisting messages it has already seen.\n\n**Why This Happens**:\n\n1. Tool is called → `execute()` returns initial result → Message created with tool call\n2. History adapter saves this message (message ID added to `historyIds` set)\n3. User interacts → `addResult()` updates the tool result in the **same message**\n4. History adapter checks: \"Already seen this message ID? Skip persistence\"\n5. Updated result never gets saved to localStorage\n6. On page reload → Old result (without user's selection) is loaded\n\n**Format Differences Between Runtime and Storage**:\n\n- **Runtime messages** (from `useAssistantState`):\n  - Use `content` array\n  - Tool results stored in `result` field\n  - Tool parts have generic type: `\"tool-call\"`\n- **Storage messages** (in localStorage):\n  - Use `parts` array\n  - Tool results stored in `output` field\n  - Tool parts have specific type: `\"tool-select_frequent_location\"`, `\"tool-get_user_destination\"`, etc.\n- **Message structure differences**:\n  - Runtime may exclude `step-start` parts (count: 1)\n  - Storage includes all parts like `step-start` (count: 2+)\n  - **Must match by `toolCallId`, not array index!**\n\n**Solution**: Add a `ToolResultPersistence` component that:\n\n1. Listens to runtime message changes via `useAssistantState`\n2. Compares runtime tool results with stored results in localStorage\n3. Detects when `addResult()` has added data (e.g., `selectedLocation`)\n4. Surgically updates **only** the changed tool result's `output` field\n5. Preserves all other message data (especially user messages)\n\n**Implementation**:\n\n```tsx\n// Component to sync tool call results to localStorage\nconst ToolResultPersistence = ({ slug }: { slug: string }) => {\n  const messages = useAssistantState(({ thread }) => thread.messages);\n\n  useEffect(() => {\n    if (!messages || messages.length === 0) return;\n\n    // Only update assistant messages with tool calls in storage\n    const repo = readThreadRepo(slug);\n    let hasChanges = false;\n\n    for (const runtimeMsg of messages) {\n      if (runtimeMsg.role !== \"assistant\") continue;\n\n      // Find this message in storage\n      const storedMsgIndex = repo.messages.findIndex(\n        (entry) => entry.message.id === runtimeMsg.id,\n      );\n\n      if (storedMsgIndex === -1) continue;\n\n      const storedMsg = repo.messages[storedMsgIndex].message;\n\n      // ⚠️ CRITICAL: Storage uses \"parts\", runtime uses \"content\"\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      const storedParts = (storedMsg as any).parts;\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      const runtimeParts = (runtimeMsg as any).content;\n\n      if (\n        storedMsg.role === \"assistant\" &&\n        Array.isArray(storedParts) &&\n        Array.isArray(runtimeParts)\n      ) {\n        // Match tool calls by toolCallId, NOT by array index\n        for (const runtimePart of runtimeParts) {\n          if (\n            runtimePart &&\n            typeof runtimePart === \"object\" &&\n            \"type\" in runtimePart &&\n            runtimePart.type === \"tool-call\" &&\n            \"result\" in runtimePart &&\n            \"toolCallId\" in runtimePart\n          ) {\n            // ⚠️ CRITICAL: Storage uses specific tool names like \"tool-select_frequent_location\"\n            // Runtime uses generic \"tool-call\". Match by toolCallId instead!\n            // eslint-disable-next-line @typescript-eslint/no-explicit-any\n            const storedPart = storedParts.find(\n              // eslint-disable-next-line @typescript-eslint/no-explicit-any\n              (part: any) =>\n                part?.type?.startsWith?.(\"tool-\") &&\n                part?.toolCallId === runtimePart.toolCallId,\n            );\n\n            if (storedPart) {\n              // ⚠️ CRITICAL: Storage uses \"output\", runtime uses \"result\"\n              // eslint-disable-next-line @typescript-eslint/no-explicit-any\n              const storedResult = (storedPart as any).output;\n              const runtimeResult = runtimePart.result;\n\n              if (\n                JSON.stringify(storedResult) !== JSON.stringify(runtimeResult)\n              ) {\n                // eslint-disable-next-line @typescript-eslint/no-explicit-any\n                (storedPart as any).output = runtimeResult;\n                hasChanges = true;\n              }\n            }\n          }\n        }\n      }\n    }\n\n    if (hasChanges) {\n      writeThreadRepo(slug, repo);\n    }\n  }, [messages, slug]);\n\n  return null;\n};\n\n// Mount inside AssistantRuntimeProvider:\n<AssistantRuntimeProvider runtime={runtime}>\n  <ToolResultPersistence slug={slug} />\n  {/* rest of your UI */}\n</AssistantRuntimeProvider>;\n```\n\n**Critical Implementation Notes**:\n\n1. **Use `useAssistantState`**, not deprecated `useThread`:\n\n   ```tsx\n   const messages = useAssistantState(({ thread }) => thread.messages);\n   ```\n\n2. **Match by `toolCallId`, NEVER by array index**:\n   - Runtime might have `[tool-call]` (1 part)\n   - Storage might have `[step-start, tool-select_frequent_location]` (2 parts)\n   - Indices don't align! Match by ID.\n\n3. **Check type with `startsWith(\"tool-\")`**, not exact equality:\n\n   ```tsx\n   part?.type?.startsWith?.(\"tool-\"); // ✅ Matches \"tool-select_frequent_location\"\n   part?.type === \"tool-call\"; // ❌ Won't match storage\n   ```\n\n4. **Access correct field names**:\n   | Field | Runtime | Storage |\n   |-------|---------|---------|\n   | Parts container | `content` | `parts` |\n   | Tool result | `result` | `output` |\n   | Tool type | `\"tool-call\"` | `\"tool-{toolName}\"` |\n\n5. **Handle type safety with eslint-disable**:\n   - Storage format isn't properly typed\n   - Use `as any` with `// eslint-disable-next-line @typescript-eslint/no-explicit-any`\n\n**Testing Receipt Persistence**:\n\n- [ ] Call tool → interact → `addResult()` triggered\n- [ ] Receipt shows correctly before refresh\n- [ ] **Refresh page** → receipt still shows (CRITICAL)\n- [ ] Check localStorage → `output` contains selection\n- [ ] No console errors about missing fields\n- [ ] Works with multiple tool calls in same message\n- [ ] Thread reset clears persisted data\n\n### Type errors with `frontendTools`\n\n- **Cause**: Type mismatch between frontend tool format and AI SDK ToolSet\n- **Fix**: Use type assertions: `frontendTools(clientTools as any) as any`\n\n---\n\n## Future Considerations\n\n- **Tool UI Library Promotion**: When a WIP Tool UI is stable, move it to main library\n- **Reusable Patterns**: Extract common receipt patterns (selection, confirmation, multi-step)\n- **Accessibility**: Add keyboard navigation, ARIA labels, focus management\n- **Animation**: Smooth transitions between interactive → receipt modes\n- **Mobile**: Test touch interactions, responsive sizing\n\n---\n\n## References\n\n- [assistant-ui Tool Guide](https://www.assistant-ui.com/docs/guides/Tools)\n- [Tools Guide](https://www.assistant-ui.com/docs/guides/tools)\n- [AI SDK Integration](https://www.assistant-ui.com/docs/runtimes/ai-sdk/use-chat)\n- [Human-in-the-Loop](https://www.assistant-ui.com/docs/runtimes/langgraph/tutorial/part-2)\n\n---\n\n**Last Updated**: 2025-01-17\n**Pattern Status**: ✅ Tested & Working (Waymo FrequentLocationSelector)\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/wip-tool-uis/index.tsx",
    "content": "export { FrequentLocationSelector } from \"./FrequentLocationSelector\";\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/wip-tool-uis/ux-v3.md",
    "content": "# Waymo Tool UI Library Design (v3)\n\n## Goal\n\nBuild a **library-grade** Tool UI system for LLM assistants, using Waymo as the demo vertical.\n\n**Key principles:**\n\n- Tool UIs are reusable primitives with 4 states (loading/interactive/receipt/error)\n- Clean separation: tools return data, UIs take presentation props\n- Explicit protocol for how LLMs invoke UIs\n- First-class support for correction and failure recovery\n\n---\n\n## The Tool UI Protocol\n\n### How LLMs Request Tool UIs\n\nThe assistant returns structured messages that the host interprets:\n\n```typescript\ninterface ToolUIMessage {\n  type: \"tool_ui\";\n  component: string;           // \"RideQuote\", \"TripStatusTracker\", etc\n  props: Record<string, any>;  // Presentation-layer props\n  state: ToolUIState;          // \"loading\" | \"interactive\" | \"receipt\" | \"error\"\n  toolCallId?: string;         // Links to underlying tool call\n}\n\n// Assistant message format\n{\n  role: \"assistant\",\n  content: [\n    { type: \"text\", text: \"I can get you home from Downtown Coffee.\" },\n    {\n      type: \"tool_ui\",\n      component: \"RideQuote\",\n      state: \"interactive\",\n      props: { /* presentation props */ }\n    }\n  ]\n}\n```\n\n### Tool → UI Mapping\n\nEach tool has a default UI component and prop mapper:\n\n```typescript\nconst toolUIRegistry = {\n  \"rides.get_quote\": {\n    component: \"RideQuote\",\n    mapProps: (toolResult: Quote) => RideQuoteProps,\n  },\n  \"rides.book_trip\": {\n    component: \"BookingConfirmation\",\n    mapProps: (toolResult: BookedTrip) => BookingConfirmationProps,\n  },\n  \"rides.get_trip_status\": {\n    component: \"TripStatusTracker\",\n    mapProps: (toolResult: TripStatus) => TripStatusTrackerProps,\n  },\n};\n```\n\n---\n\n## Universal Tool UI States\n\n**Every Tool UI component must handle 4 states:**\n\n```typescript\ntype ToolUIState = \"loading\" | \"interactive\" | \"receipt\" | \"error\";\n\ninterface BaseToolUIProps {\n  state: ToolUIState;\n  errorMessage?: string;\n  onRetry?: () => void;\n}\n```\n\n### State Behaviors\n\n1. **loading** - Tool call in progress\n   - Show skeleton/shimmer\n   - Disable interactions\n   - Indicate \"thinking\"\n\n2. **interactive** - Ready for user action\n   - Full UI rendered\n   - Buttons/inputs enabled\n   - Clear CTAs\n\n3. **receipt** - Action completed\n   - Collapsed summary\n   - Shows what was selected\n   - May have edit affordances\n\n4. **error** - Tool failed\n   - Clear error message\n   - Retry option if applicable\n   - Graceful degradation\n\n---\n\n## v3 Tool Contracts\n\n### Core Tools (Tier 0)\n\n#### 1. `rides.get_rider_context`\n\n**Output with error handling:**\n\n```typescript\n{\n  home: Location | null,\n  work: Location | null,\n  frequent_destinations: Location[],\n  recent_destinations: Location[],\n  payment: {\n    default_method: PaymentMethod | null,\n    methods: PaymentMethod[],\n    error?: \"unavailable\" | \"not_configured\"\n  },\n  flags: {\n    has_multiple_payment_methods: boolean,\n    is_new_rider: boolean\n  }\n}\n```\n\n---\n\n#### 2. `rides.get_pickup_location`\n\n**Output with source tracking:**\n\n```typescript\n{\n  resolved_location: {\n    address: string,\n    lat: number,\n    lng: number,\n    name: string\n  },\n  confidence: \"high\" | \"medium\" | \"low\",\n  source: \"gps\" | \"user_search\" | \"user_adjusted\" | \"fallback\",\n  nearby_landmarks?: string[],\n  error?: \"gps_unavailable\" | \"location_services_off\"\n}\n```\n\n---\n\n#### 3. `rides.get_quote`\n\n**Output with expiry and metadata:**\n\n```typescript\n{\n  quote_id: string,\n  eta_minutes: number,\n  price: {\n    amount: number,\n    currency: string,\n    is_estimate: boolean,\n    surge_multiplier?: number,\n    expires_at: string  // ISO8601\n  },\n  pickup: Location,\n  dropoff: Location,\n  vehicle_info: {\n    type: \"Waymo One\",\n    capacity: number\n  },\n  meta: {\n    reason_estimate?: \"surge\" | \"traffic\" | \"distance\",\n    message?: string  // \"Price may change due to high demand\"\n  }\n}\n```\n\n---\n\n#### 4. `rides.book_trip`\n\n**Output with failure cases:**\n\n```typescript\n{\n  success: boolean,\n  trip_id?: string,\n  status: \"requested\" | \"failed\",\n  failure_reason?: \"quote_expired\" | \"price_changed\" | \"payment_failed\" | \"no_vehicles\",\n  updated_quote?: Quote,  // When price changed\n  trip?: {\n    pickup: Location,\n    dropoff: Location,\n    eta_minutes: number,\n    price: Price,\n    payment_summary: string,\n    vehicle?: Vehicle\n  }\n}\n```\n\n---\n\n#### 5. `rides.get_trip_status`\n\n**Output with progress tracking:**\n\n```typescript\n{\n  trip_id: string,\n  status: \"searching\" | \"assigned\" | \"en_route\" | \"arrived\" | \"in_trip\" | \"completed\" | \"canceled\",\n  progress_percent?: number,  // 0-100 for en_route/in_trip\n  eta_minutes?: number,\n  timestamps: {\n    requested_at: string,\n    assigned_at?: string,\n    pickup_arrival_at?: string,\n    trip_start_at?: string,\n    completed_at?: string\n  },\n  vehicle?: {\n    make: string,\n    model: string,\n    color: string,\n    plate: string,\n    driver_name?: string,\n    current_location?: { lat: number, lng: number }\n  },\n  next_actions: string[],\n  cancel_reason?: string  // If canceled\n}\n```\n\n---\n\n## Library-Grade Tool UI Components\n\n### Base Primitives (Generic)\n\n#### 1. LoadingCard\n\n```typescript\ninterface LoadingCardProps {\n  lines?: number; // Number of skeleton lines\n  showImage?: boolean;\n  message?: string; // \"Getting your quote...\"\n}\n```\n\n#### 2. ErrorCard\n\n```typescript\ninterface ErrorCardProps {\n  message: string;\n  detail?: string;\n  onRetry?: () => void;\n  onDismiss?: () => void;\n}\n```\n\n#### 3. OptionCardSelector\n\n```typescript\ninterface OptionCardSelectorProps extends BaseToolUIProps {\n  options: Array<{\n    id: string;\n    title: string;\n    subtitle?: string;\n    primaryMetric?: string;\n    secondaryMetric?: string;\n    badge?: string;\n    disabled?: boolean;\n  }>;\n  selectedId?: string;\n  onSelect: (id: string) => void;\n  layout?: \"horizontal\" | \"grid\";\n}\n```\n\n#### 4. TimelineStatusCard\n\n```typescript\ninterface TimelineStatusCardProps extends BaseToolUIProps {\n  steps: Array<{\n    id: string;\n    label: string;\n    status: \"pending\" | \"active\" | \"completed\" | \"error\";\n    timestamp?: string;\n    description?: string;\n  }>;\n  currentStepId?: string;\n}\n```\n\n---\n\n### Waymo-Specific Components (Built on Primitives)\n\n#### 1. RideQuote (Hero Component)\n\n**Presentation-focused props (decoupled from Quote):**\n\n```typescript\ninterface RideQuoteProps extends BaseToolUIProps {\n  // Display data\n  routeLabel: string; // \"Downtown Coffee → 123 Main St\"\n  etaLabel: string; // \"Arrives in 5 minutes\"\n  priceLabel: string; // \"$12.50\"\n  priceBadge?: string; // \"Estimate\" | \"Surge pricing\"\n  vehicleLabel?: string; // \"Waymo One\"\n  expiresLabel?: string; // \"Price valid for 2 minutes\"\n  paymentSummary?: string; // \"Apple Pay (...4242)\"\n\n  // Actions\n  onConfirm?: () => void;\n  onEditPickup?: () => void;\n  onEditDropoff?: () => void;\n  onChangePayment?: () => void;\n}\n```\n\n**State behaviors:**\n\n- **loading**: Skeleton with route, shimmer for price/ETA\n- **interactive**: Full card with Confirm button\n- **receipt**: \"✓ Ride confirmed\" with summary\n- **error**: \"Couldn't get quote\" with retry\n\n---\n\n#### 2. TripStatusTracker (The Wow Factor)\n\n**Presentation-focused props:**\n\n```typescript\ninterface TripStatusTrackerProps extends BaseToolUIProps {\n  tripId: string;\n  currentStatus: string;\n  statusLabel: string; // \"Vehicle on the way\"\n  progressPercent?: number; // For progress bar\n  etaLabel?: string; // \"3 minutes away\"\n\n  timeline: Array<{\n    step: string;\n    label: string;\n    status: \"pending\" | \"active\" | \"completed\";\n    timestamp?: string;\n  }>;\n\n  vehicleInfo?: {\n    description: string; // \"White Jaguar I-PACE\"\n    plate: string;\n    imageUrl?: string;\n  };\n\n  mapUrl?: string;\n\n  // Actions based on current state\n  actions?: Array<{\n    label: string;\n    action: \"cancel\" | \"find_car\" | \"contact\" | \"share\";\n    onClick: () => void;\n  }>;\n}\n```\n\n**State behaviors:**\n\n- **loading**: Skeleton timeline\n- **interactive**: Live timeline with actions\n- **receipt**: Completed trip summary\n- **error**: \"Can't load trip status\" with retry\n\n---\n\n#### 3. PickupConfirmCard\n\n**Props:**\n\n```typescript\ninterface PickupConfirmCardProps extends BaseToolUIProps {\n  addressLabel: string;\n  confidenceBadge?: \"High\" | \"Medium\" | \"Low\";\n  nearbyContext?: string; // \"Near City Hall, Metro Station\"\n  mapThumbnailUrl?: string;\n\n  onConfirm?: () => void;\n  onChangeLocation?: () => void;\n  onUseCurrentLocation?: () => void;\n}\n```\n\n---\n\n#### 4. BookingConfirmation\n\n**Props:**\n\n```typescript\ninterface BookingConfirmationProps extends BaseToolUIProps {\n  title: string; // \"Your Waymo is on the way!\"\n  routeLabel: string;\n  etaLabel: string;\n  priceLabel: string;\n  paymentLabel: string;\n  tripId: string;\n\n  onTrackRide?: () => void;\n  onShareTrip?: () => void;\n  onCancelRide?: () => void;\n  onChangePayment?: () => void;\n}\n```\n\n---\n\n## Correction & Recovery Flows\n\n### Post-Decision Edits\n\nEvery interactive Tool UI should support correction via callbacks:\n\n```typescript\n// RideQuote correction flow\nUser: \"Actually, pick me up at the back entrance\"\n→ RideQuote.onEditPickup() triggered\n→ Show PickupConfirmCard\n→ User adjusts location\n→ Call rides.get_quote with new pickup\n→ Re-render RideQuote with updated data\n\n// BookingConfirmation correction flow\nUser: \"Use my business card instead\"\n→ BookingConfirmation.onChangePayment() triggered\n→ Show PaymentMethodPicker\n→ User selects different payment\n→ Update payment in backend\n→ Re-render confirmation\n```\n\n### Failure Recovery\n\nHandle common failures gracefully:\n\n```typescript\n// Quote expired\nrides.book_trip returns {\n  success: false,\n  failure_reason: \"quote_expired\",\n  updated_quote: { /* new quote */ }\n}\n→ Re-render RideQuote with:\n  - \"Price updated\" badge\n  - New price highlighted\n  - Auto-focused Confirm button\n\n// Payment failed\nrides.book_trip returns {\n  success: false,\n  failure_reason: \"payment_failed\"\n}\n→ Show ErrorCard: \"Payment failed\"\n→ Trigger PaymentMethodPicker\n→ Retry booking with new payment\n```\n\n---\n\n## Demo Implementation Priority\n\n### Phase 0: Foundation (Day 1-2)\n\n**Goal:** Tool UI protocol and base components\n\n1. **Define protocol:**\n   - ToolUIMessage format\n   - Tool → UI registry\n   - Props mapping functions\n\n2. **Build base components:**\n   - LoadingCard\n   - ErrorCard\n   - BaseToolUIProps implementation\n\n3. **Implement state machine:**\n   - 4-state handling in all components\n   - State transition animations\n\n---\n\n### Phase 1: Golden Path (Day 3-4)\n\n**Goal:** Perfect \"I need a ride home\" → 1 click flow\n\n**Tools:**\n\n- rides.get_rider_context\n- rides.get_pickup_location\n- rides.get_quote\n- rides.book_trip\n\n**Tool UIs:**\n\n- RideQuote (all 4 states)\n- BookingConfirmation\n- TripStatusTracker (basic)\n\n**Demo:**\n\n1. User: \"I need a ride home\"\n2. Loading → RideQuote (interactive)\n3. Click Confirm → Receipt → BookingConfirmation\n4. Auto-transition → TripStatusTracker\n\n---\n\n### Phase 2: Ambiguity & Correction (Day 5-6)\n\n**Goal:** Handle edge cases and post-decision edits\n\n**Add Tools:**\n\n- rides.search_places\n- rides.get_trip_status (with progress)\n\n**Add Tool UIs:**\n\n- PickupConfirmCard\n- DestinationPicker\n\n**Flows:**\n\n- Low confidence GPS → PickupConfirmCard\n- No home saved → DestinationPicker\n- \"Actually, change destination\" → Edit flow\n\n---\n\n### Phase 3: Polish (Day 7+)\n\n**Goal:** Delight features\n\n- PaymentMethodPicker\n- MapPreviewCard integration\n- Real-time status animations\n- Multi-destination comparison\n\n---\n\n## Success Metrics\n\n### Library Quality\n\n✅ Components work with any backend (presentation props)\n✅ All 4 states handled gracefully\n✅ Clear protocol for LLM → UI invocation\n✅ Correction flows are first-class\n\n### Demo Impact\n\n✅ Golden path feels instant (< 2s to quote)\n✅ Loading states prevent UI pop-in\n✅ Errors recover gracefully\n✅ TripStatusTracker creates \"wow\" moment\n\n### Developer Experience\n\n✅ Components compose naturally\n✅ Props are self-documenting (TypeScript)\n✅ State transitions are predictable\n✅ Can build new verticals without forking\n\n---\n\n## Key Design Decisions\n\n### 1. Why 4 states not 2?\n\nLoading and error states are critical for perceived quality. Users need to see the assistant \"thinking\" and failures need graceful recovery.\n\n### 2. Why presentation props?\n\nLibrary components shouldn't know about Waymo's Quote type. They should work for Lyft, DoorDash, or any vertical with similar UI needs.\n\n### 3. Why explicit protocol?\n\nWithout a clear contract for how LLMs invoke UIs, every implementation becomes ad-hoc. The protocol enables tooling, testing, and cross-model portability.\n\n### 4. Why correction callbacks?\n\nLLM assistants should support \"actually, change X\" naturally. Post-decision edits are where conversational UI shines versus traditional forms.\n\n---\n\n## Next Steps\n\n1. **Implement Phase 0** - Protocol and base components\n2. **Build RideQuote** with all 4 states\n3. **Build TripStatusTracker** with live updates\n4. **Wire up golden path** in playground\n5. **Add correction flows** once basics work\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo/wip-tool-uis/ux.md",
    "content": "# Waymo Booking UX Design (v2)\n\n## Goal\n\nDesign a compelling end-to-end demo of booking a ride home via Waymo, focusing on:\n\n- Minimal, high-impact tool set (80/20 principle)\n- Clean separation: tools = operations, Tool UIs = presentation + interaction\n- Post-booking tracking where Tool UIs really shine\n- Handle ambiguity and correction, not just the golden path\n\n---\n\n## Architecture: Three Layers\n\n### 1. Domain Tools (Backend-ish)\n\nWhat the ride service actually does: resolve context, quote rides, book rides, fetch trip status.\n\n### 2. Tool UI Primitives\n\nReusable UI building blocks: selectors, pickers, receipts, timelines, maps.\n\n### 3. Conversation Patterns\n\nDifferent flows (power, guided, exploratory) are just different sequences over the same tools + UIs.\n\n---\n\n## Core User Story\n\n**\"I need a ride home\"**\n\nDecision points:\n\n1. **Where are you now?** (pickup location + confidence)\n2. **Where is home?** (destination from context or user input)\n3. **When?** (now vs scheduled)\n4. **Payment** (default or selection)\n5. **Confirm & book**\n6. **Track ride** (the \"wow\" moment)\n\n**Note:** Waymo offers a single autonomous vehicle type, which simplifies the flow.\n\n---\n\n## v2 Tool Set\n\n### Tier 0 – Core Tools (4) — The True 80/20\n\nThese four are enough to: resolve context, get quotes, book, and track.\n\n#### 1. `rides.get_rider_context`\n\n**Purpose:** Silent context fetch\n\n**Input:** none\n\n**Output:**\n\n```typescript\n{\n  home: Location | null,\n  work: Location | null,\n  frequent_destinations: Location[],\n  recent_destinations: Location[],\n  default_payment_method: PaymentMethod,\n  payment_methods: PaymentMethod[],\n  flags: {\n    has_multiple_payment_methods: boolean,\n    is_new_rider: boolean\n  }\n}\n```\n\n**Use:** Right after \"I need a ride home\" to avoid asking for known info.\n\n**UI:** None (silent background call)\n\n---\n\n#### 2. `rides.get_pickup_location`\n\n**Purpose:** Normalize/confirm where to pick up\n\n**Input:**\n\n```typescript\n{\n  hint: \"current_location\" | string | { lat: number, lng: number },\n  allow_approximate?: boolean\n}\n```\n\n**Output:**\n\n```typescript\n{\n  resolved_location: {\n    address: string,\n    lat: number,\n    lng: number,\n    name: string\n  },\n  confidence: \"high\" | \"medium\" | \"low\",\n  nearby_landmarks?: string[]\n}\n```\n\n**Use:**\n\n- If `confidence === \"high\"` → auto-confirm with text\n- If `confidence !== \"high\"` → show **PickupConfirmCard** UI\n\n**UI:** PickupConfirmCard (conditional)\n\n---\n\n#### 3. `rides.get_quote`\n\n**Purpose:** Get ride quote for pickup → destination\n\n**Input:**\n\n```typescript\n{\n  pickup: Location,\n  dropoff: Location,\n  departure_time?: \"now\" | ISO8601\n}\n```\n\n**Output:**\n\n```typescript\n{\n  quote_id: string,\n  eta_minutes: number,\n  price: {\n    amount: number,\n    currency: string,\n    is_estimate: boolean,\n    surge_multiplier?: number\n  },\n  pickup: Location,\n  dropoff: Location,\n  vehicle_info: {\n    type: \"Waymo One\",\n    capacity: number\n  }\n}\n```\n\n**Use:** Main interactive tool - presents the ride option for confirmation\n\n**UI:** **RideQuote** ⭐ (Hero Tool UI)\n\n---\n\n#### 4. `rides.book_trip`\n\n**Purpose:** Actually book the ride\n\n**Input:**\n\n```typescript\n{\n  quote_id: string,\n  payment_method_id?: string  // uses default if not specified\n}\n```\n\n**Output:**\n\n```typescript\n{\n  trip_id: string,\n  status: \"requested\",\n  pickup: Location,\n  dropoff: Location,\n  eta_minutes: number,\n  price: {\n    amount: number,\n    currency: string\n  },\n  payment_summary: string,  // \"$12.50 charged to Apple Pay (...4242)\"\n  vehicle?: {\n    make: string,\n    model: string,\n    color: string,\n    plate: string\n  }\n}\n```\n\n**Use:** Execute booking, then immediately show tracking\n\n**UI:** **BookingConfirmation** → **TripStatusTracker** ⭐\n\n---\n\n### Tier 1 – Support Tools (3)\n\n#### 5. `rides.search_places`\n\n**Purpose:** Resolve arbitrary text destination\n\n**Input:**\n\n```typescript\n{\n  query: string,\n  near?: { lat: number, lng: number }\n}\n```\n\n**Output:**\n\n```typescript\n{\n  results: Array<{\n    name: string;\n    address: string;\n    lat: number;\n    lng: number;\n    type: string; // \"airport\", \"restaurant\", etc.\n  }>;\n}\n```\n\n**Use:** When \"home\"/\"work\"/recents don't match user's request\n\n**UI:** DestinationPicker (search results)\n\n---\n\n#### 6. `payments.list_methods`\n\n**Purpose:** Expose payment options when needed\n\n**Input:** none\n\n**Output:**\n\n```typescript\n{\n  methods: Array<{\n    id: string;\n    type: \"card\" | \"apple_pay\" | \"google_pay\";\n    brand?: string;\n    last4?: string;\n    is_default: boolean;\n  }>;\n}\n```\n\n**Use:** Only for \"change payment method\" flows\n\n**UI:** PaymentMethodPicker (conditional)\n\n---\n\n#### 7. `rides.get_trip_status`\n\n**Purpose:** Track ride after booking\n\n**Input:**\n\n```typescript\n{\n  trip_id: string;\n}\n```\n\n**Output:**\n\n```typescript\n{\n  trip_id: string,\n  status: \"searching\" | \"assigned\" | \"en_route\" | \"arrived\" | \"in_trip\" | \"completed\" | \"canceled\",\n  eta_minutes?: number,\n  vehicle?: {\n    make: string,\n    model: string,\n    color: string,\n    plate: string,\n    current_location?: { lat: number, lng: number }\n  },\n  map_snapshot_url?: string,\n  next_actions: string[]  // [\"cancel\", \"contact_support\", \"share_eta\"]\n}\n```\n\n**Use:** Animate status transitions through repeated calls\n\n**UI:** **TripStatusTracker** ⭐ (Where Tool UIs really shine)\n\n---\n\n## Tool UI Primitives (Generic, Reusable)\n\nThese are library-level; Waymo is just a preset.\n\n### 1. OptionCardSelector\n\n- Horizontally scrollable or grid of options\n- Each card: title, subtitle, primary metric, secondary metric, badges, CTA\n- Modes: `select` (clickable) | `receipt` (selected highlighted)\n- Used for: ride options, payment methods, destination picks\n\n### 2. InlineChoiceChips\n\n- Row of pill buttons (\"Now\", \"In 15 min\", \"Tonight\")\n- Great for quick time selection in chat bubble\n\n### 3. SummaryReceiptCard\n\n- Compact summary: icon, title, key info lines, sibling action surface\n- Used for: booking confirmation, post-selection receipts\n\n### 4. TimelineStatusCard\n\n- Vertical stepper with states\n- Each step can be highlighted with timestamps/ETAs\n- Used for: trip status tracking\n\n### 5. MapPreviewCard (optional)\n\n- Map thumbnail + pickup/dropoff labels\n- Actions: \"Open in maps\", \"Change pickup\"\n\n---\n\n## Waymo-Specific Tool UIs (Built from Primitives)\n\n### 1. RideQuote ⭐ (Hero Tool UI)\n\n**Built on:** SummaryReceiptCard + custom styling\n\n**Props:**\n\n```typescript\n{\n  quote: Quote,  // from rides.get_quote\n  payment_method: PaymentMethod,\n  mode: \"interactive\" | \"receipt\"\n}\n```\n\n**Interactive mode:**\n\n- Shows: pickup → destination route\n- Displays: ETA, price, Waymo vehicle visualization\n- Large \"Confirm Ride\" button\n- Payment method shown (with \"Change\" option)\n\n**Receipt mode:**\n\n- \"✓ Ride confirmed\"\n- Collapsed summary of route, ETA, price\n\n---\n\n### 2. BookingConfirmation ⭐\n\n**Built on:** SummaryReceiptCard\n\n**Props:**\n\n```typescript\n{\n  trip: BookedTrip,  // from rides.book_trip\n}\n```\n\n**Shows:**\n\n- Title: \"Your Waymo is on the way\"\n- Lines: pickup → dropoff, ETA, price, payment\n- Actions: \"Track ride\", \"Share trip\", \"Cancel\"\n\n---\n\n### 3. TripStatusTracker ⭐ (The Wow Factor)\n\n**Built on:** TimelineStatusCard + MapPreviewCard\n\n**Props:**\n\n```typescript\n{\n  trip_status: TripStatus,  // from rides.get_trip_status\n}\n```\n\n**Timeline steps:**\n\n1. Requested\n2. Vehicle assigned (shows vehicle details)\n3. Vehicle en route (shows ETA countdown)\n4. Vehicle arrived (pickup instructions)\n5. In trip (live progress)\n6. Completed (receipt/rating)\n\n**Features:**\n\n- Each status update re-renders with new data\n- Map preview shows vehicle location\n- Context-appropriate actions at each step\n\n---\n\n### 4. DestinationPicker\n\n**Built on:** InlineChoiceChips + search input + list\n\n**Props:**\n\n```typescript\n{\n  home: Location | null,\n  work: Location | null,\n  recents: Location[],\n  search_results?: Location[],\n  mode: \"interactive\" | \"receipt\"\n}\n```\n\n**Interactive mode:**\n\n- Top: chips for Home, Work (if available)\n- Middle: recent locations\n- Bottom: search box → results from `rides.search_places`\n\n**Receipt mode:**\n\n- \"✓ [Location Name]\" with address\n\n---\n\n### 5. PickupConfirmCard\n\n**Built on:** MapPreviewCard + buttons\n\n**Props:**\n\n```typescript\n{\n  location: Location,\n  confidence: \"high\" | \"medium\" | \"low\",\n  nearby_landmarks?: string[],\n  mode: \"interactive\" | \"receipt\"\n}\n```\n\n**Interactive mode:**\n\n- Map preview with marker\n- Address + landmark context\n- Buttons: \"Confirm pickup here\" / \"Change location\"\n- If `confidence !== \"high\"`, emphasize \"Change location\"\n\n**Receipt mode:**\n\n- \"✓ Pickup at [Address]\"\n\n---\n\n### 6. PaymentMethodPicker\n\n**Built on:** OptionCardSelector\n\n**Props:**\n\n```typescript\n{\n  methods: PaymentMethod[],\n  selected_id?: string,\n  mode: \"interactive\" | \"receipt\"\n}\n```\n\n**When shown:**\n\n- User explicitly asks to change payment\n- `has_multiple_payment_methods && no_default`\n\n---\n\n## Demo Flows\n\n### Flow 1: Golden Path — \"I need a ride home\" (1 click)\n\n**User:** \"I need a ride home\"\n\n**Step 1:** Silent context + location resolution\n\n```\nTools called:\n- rides.get_rider_context → has home, has default payment\n- rides.get_pickup_location({ hint: \"current_location\" }) → confidence: \"high\"\n```\n\n**Step 2:** Assistant responds + shows RideQuote\n\n```\nAssistant: \"I can get you home from Downtown Coffee Shop.\"\n```\n\n**RideQuote UI (interactive mode):**\n\n```\n┌─────────────────────────────────────┐\n│  Downtown Coffee → 123 Main St      │\n│                                     │\n│  [Waymo vehicle illustration]       │\n│                                     │\n│     Arrives in 5 minutes            │\n│           $12.50                    │\n│                                     │\n│    ┌─────────────────────┐          │\n│    │   Confirm Ride      │          │\n│    └─────────────────────┘          │\n│                                     │\n│  Apple Pay (...4242)  [Change]      │\n└─────────────────────────────────────┘\n```\n\n**Step 3:** User clicks \"Confirm Ride\"\n\n- RideQuote transitions to receipt mode: \"✓ Ride confirmed\"\n- Tool called: `rides.book_trip`\n\n**Step 4:** BookingConfirmation + TripStatusTracker\n\n```\n┌─────────────────────────────────────┐\n│  Your Waymo is on the way! ✓        │\n├─────────────────────────────────────┤\n│  Downtown Coffee → 123 Main St      │\n│  5 min • $12.50 • Apple Pay         │\n│                                     │\n│  Trip ID: WMO-2024-ABC123           │\n│                                     │\n│  [Track Ride] [Share] [Cancel]      │\n└─────────────────────────────────────┘\n\n┌─────────────────────────────────────┐\n│  Trip Status                        │\n├─────────────────────────────────────┤\n│  ● Requested                        │\n│  ○ Vehicle assigned                 │\n│  ○ En route to pickup               │\n│  ○ Arrived at pickup                │\n│  ○ Trip in progress                 │\n│  ○ Completed                        │\n└─────────────────────────────────────┘\n```\n\n**Total:**\n\n- 1 user message\n- 1 click\n- 3 tools called\n- 3 Tool UIs (RideQuote → BookingConfirmation → TripStatusTracker)\n\n---\n\n### Flow 2: New User — \"I need a ride home\" (guided)\n\n**User:** \"I need a ride home\"\n\n**Step 1:** Context reveals no home saved\n\n```\nTools called:\n- rides.get_rider_context → home: null\n```\n\n**Step 2:** Assistant asks + shows DestinationPicker\n\n```\nAssistant: \"I can help with that. Where's home for you?\"\n```\n\n**DestinationPicker UI:**\n\n```\n┌─────────────────────────────────────┐\n│  Where to?                          │\n├─────────────────────────────────────┤\n│  [Work: 456 Office Blvd]            │\n│                                     │\n│  Recent:                            │\n│  • SFO Airport                      │\n│  • Downtown Target                  │\n│                                     │\n│  ┌─────────────────────────────┐    │\n│  │ Search for address...       │    │\n│  └─────────────────────────────┘    │\n└─────────────────────────────────────┘\n```\n\n**Step 3:** User searches \"123 Main St\"\n\n- Tool called: `rides.search_places({ query: \"123 Main St\" })`\n- Results appear, user selects\n\n**Step 4:** Continue with Flow 1\n\n- `rides.get_pickup_location`\n- `rides.get_quote`\n- RideQuote → Confirm → BookingConfirmation → TripStatusTracker\n\n---\n\n### Flow 3: Low Confidence Pickup — GPS Issues\n\n**User:** \"I need a ride to work\"\n\n**Step 1:** Location resolution with low confidence\n\n```\nTools called:\n- rides.get_rider_context → has work\n- rides.get_pickup_location → confidence: \"low\"\n```\n\n**Step 2:** Show PickupConfirmCard\n\n```\nAssistant: \"I'm having trouble pinpointing your exact location.\"\n```\n\n**PickupConfirmCard UI:**\n\n```\n┌─────────────────────────────────────┐\n│  Confirm Pickup Location            │\n├─────────────────────────────────────┤\n│  [Map preview with marker]          │\n│                                     │\n│  Near: Downtown Coffee Shop         │\n│  123 Main St                        │\n│                                     │\n│  Nearby: City Hall, Metro Station   │\n│                                     │\n│  [Confirm Here]  [Change Location]  │\n└─────────────────────────────────────┘\n```\n\n**Step 3:** User confirms or adjusts → Continue with quote + booking\n\n---\n\n### Flow 4: Comparison — \"Should I go home or to the office?\"\n\n**User:** \"Should I go home or to the office?\"\n\n**Step 1:** Get both quotes\n\n```\nTools called:\n- rides.get_rider_context → home + work\n- rides.get_pickup_location\n- rides.get_quote({ dropoff: home })\n- rides.get_quote({ dropoff: work })\n```\n\n**Step 2:** Show comparison\n\n```\nAssistant: \"Here's how both trips compare:\"\n```\n\n**Comparison UI:**\n\n```\n┌─────────────────────────────────────┐\n│  To Home                            │\n│  123 Main St                        │\n│  5 min • $12.50                     │\n│  [Select]                           │\n├─────────────────────────────────────┤\n│  To Work                            │\n│  456 Office Blvd                    │\n│  9 min • $18.00                     │\n│  [Select]                           │\n└─────────────────────────────────────┘\n```\n\n**Step 3:** User selects → Book → Confirm → Track\n\n---\n\n### Flow 5: Post-Booking Updates — The Wow Factor\n\nAfter booking, the TripStatusTracker updates as status changes:\n\n**Status: \"assigned\"**\n\n```\n┌─────────────────────────────────────┐\n│  Trip Status                        │\n├─────────────────────────────────────┤\n│  ✓ Requested                        │\n│  ● Vehicle assigned                 │\n│    White Jaguar I-PACE              │\n│    License: 8ABC123                 │\n│  ○ En route (3 min away)            │\n│  ...                                │\n└─────────────────────────────────────┘\n```\n\n**Status: \"arrived\"**\n\n```\n┌─────────────────────────────────────┐\n│  Your Waymo has arrived!            │\n├─────────────────────────────────────┤\n│  [Map with vehicle at pickup]       │\n│                                     │\n│  White Jaguar I-PACE                │\n│  License: 8ABC123                   │\n│                                     │\n│  Look for your vehicle near the     │\n│  main entrance on Oak Street.       │\n│                                     │\n│  [Get Directions] [Can't Find It]   │\n└─────────────────────────────────────┘\n```\n\n**Status: \"completed\"**\n\n```\n┌─────────────────────────────────────┐\n│  Trip Complete ✓                    │\n├─────────────────────────────────────┤\n│  Downtown Coffee → 123 Main St      │\n│  12 min trip • $12.50               │\n│                                     │\n│  How was your ride?                 │\n│  [😊] [😐] [😞]                     │\n│                                     │\n│  [Get Receipt] [Report Issue]       │\n└─────────────────────────────────────┘\n```\n\n---\n\n## Build Priority\n\n### Phase 1: Minimal End-to-End (First Prototype)\n\n**Tools (4):**\n\n1. `rides.get_rider_context`\n2. `rides.get_pickup_location`\n3. `rides.get_quote`\n4. `rides.book_trip`\n\n**Tool UIs (3):**\n\n1. **RideQuote** - The hero (interactive + receipt modes)\n2. **BookingConfirmation** - The payoff\n3. **TripStatusTracker** - The wow factor\n\n**Flows:**\n\n- Golden path: \"I need a ride home\" → 1 click\n\n---\n\n### Phase 2: Handle Edge Cases\n\n**Add Tools:** 5. `rides.search_places` 6. `rides.get_trip_status`\n\n**Add Tool UIs:** 4. **DestinationPicker** - For unknown destinations 5. **PickupConfirmCard** - For low confidence locations\n\n**Flows:**\n\n- New user with no home saved\n- GPS issues / location correction\n\n---\n\n### Phase 3: Polish & Delight\n\n**Add Tools:** 7. `payments.list_methods`\n\n**Add Tool UIs:** 6. **PaymentMethodPicker** 7. **MapPreviewCard** integration\n\n**Features:**\n\n- Multi-destination comparison\n- Scheduled rides\n- Real-time status animations\n\n---\n\n## Design Principles\n\n### 1. Confidence-Based UI Decisions\n\nLet `confidence` scores from tools drive UI choices:\n\n- `high` → auto-confirm with text\n- `medium/low` → show interactive picker\n\n### 2. Progressive Enhancement\n\n- Start with text confirmations\n- Add interactive UIs where they reduce friction\n- Maps and animations are polish, not core\n\n### 3. Receipt States Everywhere\n\nEvery interactive Tool UI should have a collapsed receipt state showing what was selected.\n\n### 4. Post-Booking is the Payoff\n\nThe demo isn't complete at booking. TripStatusTracker with animated state transitions is where Tool UIs really shine.\n\n### 5. Same Tools, Different Flows\n\nThe LLM orchestrates different conversation patterns using the same tool set. Don't add tools for every flow variant.\n\n---\n\n## Open Questions\n\n### 1. Prices before or after location confirmation?\n\n**Recommendation:** Always price after concrete pickup + dropoff.\n\n- If `confidence === \"high\"` and destination known → show real prices\n- Otherwise → resolve locations first, then quote\n\n### 2. Handling surge / availability changes?\n\n**Design for:**\n\n- `price.is_estimate = true` during surge\n- `book_trip` returns `price_confirmed` or `price_changed`\n- Small change → inline toast\n- Large change → re-render RideQuote with \"Price changed\" badge\n\n### 3. How much detail in RideQuote?\n\n**Implement density modes:**\n\n- `variant=\"compact\"` → ETA, price, confirm (default)\n- `variant=\"detailed\"` → add capacity, accessibility, carbon, etc.\n\n### 4. Payment selection: separate step or bundled?\n\n**Recommendation:** Bundled by default.\n\n- Only show PaymentMethodPicker if:\n  - User asks to change payment\n  - Multiple methods + no default\n\n### 5. Map preview in RideQuote?\n\n**Trade-offs:**\n\n- Pro: Visual confirmation of route\n- Con: More complex, slower to render\n- Recommendation: Add in Phase 3 as polish\n\n### 6. Multi-destination comparison?\n\n**For v1:** Support in Flow 4 without new tools\n**Defer:** Complex multi-stop trips to future iteration\n\n---\n\n## Next Steps\n\n1. **Define TypeScript schemas** for tool inputs/outputs\n2. **Design RideQuote component** - The hero UI\n3. **Design TripStatusTracker** - The wow factor\n4. **Wire up golden path flow** in the playground\n5. **Iterate on edge cases** based on testing\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo-v2/DESIGN.md",
    "content": "# Waymo v2 Prototype Design\n\n## Purpose\n\nThis prototype exists to **research and demonstrate Tool UI patterns** using ride booking as a concrete domain. The goal isn't a production Waymo clone - it's to use this vertical as a lens for understanding:\n\n1. When Tool UIs add value vs. plain text\n2. What fundamental patterns exist across domains\n3. How Tool UIs compose in conversations\n4. What data contracts work between LLMs and UIs\n\n## Relationship to Design Guidelines\n\nThis prototype and the [Design Guidelines](/docs/design-guidelines) evolve together:\n\n- **Guidelines → Prototype**: We follow the principles (triadic loop, receipts, anti-patterns)\n- **Prototype → Guidelines**: Friction we hit here informs new guidelines\n\n### Learnings Fed Back to Guidelines\n\n| Discovery                                                          | Guideline Update                                                                                                                                                                                                                                                                         |\n| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Assistant said \"Where to?\" and UI had \"Where to?\" header           | Expanded \"Redundant narration\" to be bidirectional — neither assistant nor surface should echo the other                                                                                                                                                                                 |\n| Pickup location change needed dedicated UI, not inline edit        | Secondary actions that require selection should spawn new Tool UIs rather than inline editing                                                                                                                                                                                            |\n| Changing a term of a \"contract\" UI (like RideQuote) invalidates it | New pattern: **Contract with Revocation**. Some Tool UIs represent a contract (offer with specific terms). Changing any term revokes the contract and requires a fresh one. The revoked UI shows \"superseded\" state, a new Tool UI appears with updated terms.                           |\n| User can type \"take me home\" instead of clicking the picker        | Design decision: **Skip redundant selection UIs**. When destination is implicit in user's message, skip the picker and go straight to the quote. The quote shows the destination anyway, so a separate confirmation is redundant. Selection UIs are for clarification, not confirmation. |\n\n---\n\n## The Triadic Loop\n\nFrom the [Design Guidelines](/docs/design-guidelines): The assistant, the surface, and the user form a **collaborative triad**.\n\n```\n    User\n   ↙    ↘\ncontrols  observes\n   ↓        ↓\n Surface ←→ Assistant\n  mediates   narrates\n```\n\n**The loop in action:**\n\n1. **Assistant introduces** the surface with context\n2. **User interacts** with the surface\n3. **Surface mediates** the result back\n4. **Assistant narrates** what happened and continues\n\nThis is NOT just \"show UI, wait for click.\" The assistant must:\n\n- **Introduce** each surface (\"Where would you like to go?\")\n- **Acknowledge** user actions (\"Great, Home it is.\")\n- **Reference** surface data in follow-ups (\"Your 5-minute ride to Home...\")\n- **Contextualize** what comes next (\"Let me get you a quote.\")\n\n### Anti-pattern: Silent Surfaces\n\n❌ **Wrong:**\n\n```\nUser: \"I need a ride\"\n[DestinationPicker appears silently]\nUser: clicks Home\n[RideQuote appears silently]\n```\n\n✅ **Right:**\n\n```\nUser: \"I need a ride\"\nAssistant: \"Where would you like to go?\"\n[DestinationPicker appears]\nUser: clicks Home\nAssistant: \"Home it is! Here's your quote for the 5-minute ride.\"\n[RideQuote appears]\n```\n\n### Implementation: How the Loop Flows\n\nWith Toolkit tool definitions and `type: \"human\"`:\n\n1. **Assistant calls tool** → Tool UI renders in interactive state\n2. **User interacts** → Component calls `addResult({ ...data, selectedLocation })`\n3. **Result sent to model** → Assistant sees the selection in the tool result\n4. **Assistant generates response** → Can reference the selection and call next tool\n\nThe model sees the full result object, so it knows:\n\n- What the user selected (`selectedLocation.label === \"Home\"`)\n- Any metadata (ETA, price from quote, etc.)\n\nThis enables natural acknowledgment: \"Home it is! That's a 5-minute ride for $12.50.\"\n\n---\n\n## Core User Experience\n\n### User Mental Model\n\nWhen someone says \"I need a ride\", they're thinking:\n\n- **Where am I?** (usually known/assumed)\n- **Where am I going?** (sometimes known, sometimes vague)\n- **How much / how long?** (want to know before committing)\n- **Is this confirmed?** (need clear feedback)\n\n### UX Principles\n\n1. **Minimize required input** - Use context (GPS, saved locations) to reduce questions\n2. **Show, don't ask** - Present options visually rather than asking open-ended questions\n3. **Single clear action** - Each Tool UI should have one obvious next step\n4. **Recoverable** - User can always correct/change their mind\n5. **Progressive disclosure** - Don't overwhelm with details upfront\n\n---\n\n## Conversation Flow\n\n### Entry Points\n\n| Entry          | Example                       | Handling                              |\n| -------------- | ----------------------------- | ------------------------------------- |\n| No destination | \"I need a ride\"               | Show DestinationPicker                |\n| Known intent   | \"Take me home\"                | Resolve from profile, show RideQuote  |\n| Price check    | \"How much to SFO?\"            | Show RideQuote (no immediate booking) |\n| Full intent    | \"Book a Waymo to 123 Main St\" | Resolve address, show RideQuote       |\n\n### The Golden Path\n\n```\nUser: \"I need a ride\"\n    │\n    ▼\n┌──────────────────────────────────────┐\n│  TOOL UI: DestinationPicker          │  ← Pattern: SELECTION\n│  ─────────────────────────────────── │\n│  \"Where to?\"                         │\n│                                      │\n│  [🏠 Home]  [💼 Work]                │\n│  [📍 Recent: Ferry Building]         │\n│                                      │\n│  ─────────────────────────────────── │\n│  User clicks \"Home\"                  │\n│  → UI transforms to receipt:         │\n│  \"✓ Home - 123 Main St\"              │\n└──────────────────────────────────────┘\n    │\n    ▼\n┌──────────────────────────────────────┐\n│  TOOL UI: RideQuote                  │  ← Pattern: CONFIRMATION\n│  ─────────────────────────────────── │\n│  Downtown Coffee → Home              │\n│  5 min • $12.50 • Waymo One          │\n│  Payment: Apple Pay                  │\n│                                      │\n│  [ Confirm Ride ]                    │\n│                                      │\n│  ─────────────────────────────────── │\n│  User clicks \"Confirm\"               │\n│  → UI transforms to receipt:         │\n│  \"✓ Ride confirmed • 5 min away\"     │\n└──────────────────────────────────────┘\n    │\n    ▼\n┌──────────────────────────────────────┐\n│  TOOL UI: TripStatus                 │  ← Pattern: PROGRESS\n│  ─────────────────────────────────── │\n│  ✓ Requested                         │\n│  ● Vehicle on the way (3 min)        │\n│  ○ Pickup                            │\n│  ○ Dropoff                           │\n│                                      │\n│  White Jaguar I-PACE • 8ABC123       │\n│                                      │\n│  [Cancel Ride]                       │\n│  ─────────────────────────────────── │\n│  (Updates live as status changes)    │\n│  → Eventually becomes receipt:       │\n│  \"✓ Trip completed • $12.50\"         │\n└──────────────────────────────────────┘\n```\n\n---\n\n## Tool UI Patterns\n\nThis prototype exercises three fundamental patterns that generalize across domains:\n\n### 1. Selection Pattern (DestinationPicker)\n\n**Purpose:** User chooses from discrete options\n\n**Behavior:**\n\n- Displays options visually\n- User clicks to select\n- Transforms to receipt showing choice\n\n**Generalizes to:** Payment methods, ride types, menu items, time slots, any multi-choice input\n\n**Library needs:**\n\n- `addResult()` for capturing user choice\n- Receipt state detection from result data\n- Option rendering primitives\n\n#### When to Skip Selection UIs\n\nSelection tools are for **clarification**, not confirmation. If the user's intent is already clear from their message, skip the selection UI and proceed to the next step.\n\n```\nUser: \"I need a ride\"           → Show DestinationPicker (need clarification)\nUser: \"Take me home\"            → Skip picker, go to RideQuote (destination clear)\nUser: \"How much to the Ferry?\"  → Skip picker, go to RideQuote (destination clear)\n```\n\nThe key insight: Don't show a UI that asks a question the user already answered. If the downstream UI (RideQuote) displays the destination anyway, a separate selection receipt is redundant.\n\n---\n\n### 2. Confirmation / Contract Pattern (RideQuote)\n\n**Purpose:** Present a contract (offer with specific terms) for user approval\n\n**Behavior:**\n\n- Shows key details in scannable format\n- Primary action: Accept (Confirm)\n- Secondary actions: Modify terms (e.g., \"Change pickup location\")\n- **If terms change → contract is revoked**, UI transitions to \"superseded\" state\n- New contract must be presented with updated terms\n- Transforms to receipt on acceptance\n\n**States:**\n\n- `interactive` → User reviewing the contract\n- `revoked` → User changed a term, contract superseded (dimmed, shows \"Quote updated\")\n- `confirmed` → User accepted, contract fulfilled (receipt)\n\n**Generalizes to:** Order summaries, booking confirmations, transaction approvals, pricing quotes, any offer that can be modified before acceptance\n\n**Key insight:** A contract is a snapshot of specific terms. You can't \"edit\" a contract in place—you revoke and replace it. This maps naturally to conversation flow where each Tool UI is a discrete event.\n\n**Library needs:**\n\n- Structured card layouts\n- Primary + secondary action buttons\n- Revoked/superseded state styling\n- Receipt state for accepted contracts\n\n---\n\n### 3. Progress Pattern (TripStatus)\n\n**Purpose:** Show live-updating state over time\n\n**Behavior:**\n\n- Timeline/stepper visualization\n- Updates as external state changes\n- Actions available at certain states (e.g., Cancel before pickup)\n- Eventually reaches terminal state → receipt\n\n**Generalizes to:** Order tracking, file uploads, deployment status, any long-running task\n\n**Library needs:**\n\n- Polling/subscription mechanism for updates\n- State machine visualization components\n- Conditional action availability\n- Terminal state detection\n\n---\n\n## Tool UI Components\n\n| Component             | Pattern   | States                                          |\n| --------------------- | --------- | ----------------------------------------------- |\n| **DestinationPicker** | Selection | interactive → receipt                           |\n| **PickupPicker**      | Selection | interactive → receipt                           |\n| **RideQuote**         | Contract  | interactive → revoked \\| confirmed              |\n| **TripStatus**        | Progress  | live (multiple phases) → completed \\| cancelled |\n\n---\n\n## Research Questions\n\n### When does a Tool UI add value?\n\n- Visual location picker vs. \"type your destination\"\n- Showing a card vs. just saying \"Your ride is $12.50\"\n- Hypothesis: Tool UIs win when there's **structured data** or **discrete choices**\n\n### How do Tool UIs compose?\n\n- One tool call → one UI, or chained?\n- How do receipts work with multiple UIs in thread?\n- What happens when a UI needs to update after user action?\n\n### What's the right data contract?\n\n- Should LLM pass presentation-ready strings or raw data?\n- How much should UI \"know\" vs. just render props?\n- Where does formatting/localization happen?\n\n---\n\n## Open Design Questions\n\n1. **Zero-input vs. confirmation**\n   - If we know home + GPS, can \"ride home\" skip DestinationPicker?\n   - Or should we always show a confirmation step?\n\n2. **LLM value-add**\n   - LLM excels at ambiguous intent (\"somewhere for lunch nearby\")\n   - Tool UIs excel at structured selection\n   - Where are the handoff points?\n\n3. **Mid-flow corrections**\n   - User confirms, then says \"wait, wrong address\"\n   - Cancel and restart? Inline edit?\n\n---\n\n## Next Steps\n\n1. Define component interfaces for the three Tool UIs\n2. Build minimal implementations that exercise the patterns\n3. Identify library primitives needed to support these patterns\n4. Document learnings for Tool UI design guidelines\n\n---\n\n_Last updated: 2025-01-25_\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo-v2/components/DestinationPicker.tsx",
    "content": "\"use client\";\n\n/**\n * DestinationPicker - Selection Pattern\n *\n * Shows saved locations and lets user pick one.\n * Transforms to receipt state showing what was selected.\n *\n * Note: Only shown when destination is unknown. If user already said\n * where they want to go, the assistant skips this and goes to RideQuote.\n */\n\nimport type { ToolCallMessagePartProps } from \"@assistant-ui/react\";\nimport { Card } from \"@/components/ui/card\";\nimport { Button } from \"@/components/ui/button\";\nimport { Home, Briefcase, MapPin, Clock, CheckCircle2 } from \"lucide-react\";\nimport type { Location, SelectDestinationResult } from \"../types\";\nimport { MOCK_LOCATIONS } from \"../types\";\n\nconst getLocationIcon = (location: Location) => {\n  if (location.label.toLowerCase() === \"home\") {\n    return <Home className=\"h-5 w-5\" />;\n  }\n  if (location.label.toLowerCase() === \"work\") {\n    return <Briefcase className=\"h-5 w-5\" />;\n  }\n  return <MapPin className=\"h-5 w-5\" />;\n};\n\nconst getCategoryIcon = (type: Location[\"type\"]) => {\n  if (type === \"favorite\") {\n    return <MapPin className=\"h-3.5 w-3.5\" />;\n  }\n  return <Clock className=\"h-3.5 w-3.5\" />;\n};\n\nexport function DestinationPicker({\n  result,\n  addResult,\n}: ToolCallMessagePartProps<Record<string, never>, SelectDestinationResult>) {\n  const locations = MOCK_LOCATIONS;\n\n  // Receipt state - show what was selected\n  if (result?.selectedLocation) {\n    const selected = result.selectedLocation;\n    return (\n      <Card className=\"max-w-md p-4\">\n        <div className=\"flex items-start gap-3\">\n          <div className=\"mt-0.5 text-green-600\">\n            <CheckCircle2 className=\"h-5 w-5\" />\n          </div>\n          <div className=\"flex-1\">\n            <div className=\"mb-1 flex items-center gap-2\">\n              <div className=\"text-muted-foreground\">\n                {getLocationIcon(selected)}\n              </div>\n              <span className=\"font-semibold\">{selected.label}</span>\n            </div>\n            <div className=\"text-muted-foreground text-sm\">\n              {selected.address}\n            </div>\n          </div>\n        </div>\n      </Card>\n    );\n  }\n\n  // Interactive state - show picker\n  const favorites = locations.filter((loc) => loc.type === \"favorite\");\n  const recents = locations.filter((loc) => loc.type === \"recent\");\n\n  const handleSelect = (location: Location) => {\n    addResult({\n      locations,\n      selectedLocation: location,\n    });\n  };\n\n  return (\n    <Card className=\"max-w-md space-y-4 p-4\">\n      {favorites.length > 0 && (\n        <div className=\"space-y-2\">\n          <div className=\"text-muted-foreground flex items-center gap-2 text-sm font-medium\">\n            {getCategoryIcon(\"favorite\")}\n            <span>Favorites</span>\n          </div>\n          <div className=\"space-y-2\">\n            {favorites.map((location) => (\n              <Button\n                key={location.id}\n                variant=\"outline\"\n                className=\"hover:bg-accent h-auto w-full justify-start px-4 py-3 text-left\"\n                onClick={() => handleSelect(location)}\n              >\n                <div className=\"flex w-full items-start gap-3\">\n                  <div className=\"text-muted-foreground mt-0.5\">\n                    {getLocationIcon(location)}\n                  </div>\n                  <div className=\"min-w-0 flex-1\">\n                    <div className=\"font-medium\">{location.label}</div>\n                    <div className=\"text-muted-foreground truncate text-sm\">\n                      {location.address}\n                    </div>\n                  </div>\n                </div>\n              </Button>\n            ))}\n          </div>\n        </div>\n      )}\n\n      {recents.length > 0 && (\n        <div className=\"space-y-2\">\n          <div className=\"text-muted-foreground flex items-center gap-2 text-sm font-medium\">\n            {getCategoryIcon(\"recent\")}\n            <span>Recent</span>\n          </div>\n          <div className=\"space-y-2\">\n            {recents.map((location) => (\n              <Button\n                key={location.id}\n                variant=\"outline\"\n                className=\"hover:bg-accent h-auto w-full justify-start px-4 py-3 text-left\"\n                onClick={() => handleSelect(location)}\n              >\n                <div className=\"flex w-full items-start gap-3\">\n                  <div className=\"text-muted-foreground mt-0.5\">\n                    {getLocationIcon(location)}\n                  </div>\n                  <div className=\"min-w-0 flex-1\">\n                    <div className=\"font-medium\">{location.label}</div>\n                    <div className=\"text-muted-foreground truncate text-sm\">\n                      {location.address}\n                    </div>\n                  </div>\n                </div>\n              </Button>\n            ))}\n          </div>\n        </div>\n      )}\n    </Card>\n  );\n}\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo-v2/components/PickupPicker.tsx",
    "content": "\"use client\";\n\n/**\n * PickupPicker - Selection Pattern\n *\n * Shows pickup location options: current GPS location or saved places.\n * Transforms to receipt state showing what was selected.\n */\n\nimport type { ToolCallMessagePartProps } from \"@assistant-ui/react\";\nimport { Card } from \"@/components/ui/card\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Navigation,\n  Home,\n  Briefcase,\n  MapPin,\n  CheckCircle2,\n} from \"lucide-react\";\nimport type { SelectPickupResult } from \"../types\";\nimport { MOCK_LOCATIONS, MOCK_PICKUP } from \"../types\";\n\ninterface PickupOption {\n  id: string;\n  label: string;\n  address: string;\n  type: \"current\" | \"saved\";\n}\n\nconst getOptionIcon = (option: PickupOption) => {\n  if (option.type === \"current\") {\n    return <Navigation className=\"h-5 w-5\" />;\n  }\n  if (option.label.toLowerCase() === \"home\") {\n    return <Home className=\"h-5 w-5\" />;\n  }\n  if (option.label.toLowerCase() === \"work\") {\n    return <Briefcase className=\"h-5 w-5\" />;\n  }\n  return <MapPin className=\"h-5 w-5\" />;\n};\n\n// Build pickup options from current location + saved locations\nconst buildPickupOptions = (): PickupOption[] => {\n  const options: PickupOption[] = [\n    {\n      id: \"current\",\n      label: MOCK_PICKUP.label,\n      address: MOCK_PICKUP.address,\n      type: \"current\",\n    },\n  ];\n\n  // Add saved locations as alternative pickup points\n  for (const loc of MOCK_LOCATIONS.filter((l) => l.type === \"favorite\")) {\n    options.push({\n      id: loc.id,\n      label: loc.label,\n      address: loc.address,\n      type: \"saved\",\n    });\n  }\n\n  return options;\n};\n\nexport function PickupPicker({\n  result,\n  addResult,\n}: ToolCallMessagePartProps<Record<string, never>, SelectPickupResult>) {\n  // Receipt state - show what was selected\n  if (result?.selectedPickup) {\n    const selected = result.selectedPickup;\n    return (\n      <Card className=\"max-w-md p-4\">\n        <div className=\"flex items-start gap-3\">\n          <div className=\"mt-0.5 text-green-600\">\n            <CheckCircle2 className=\"h-5 w-5\" />\n          </div>\n          <div className=\"flex-1\">\n            <div className=\"mb-1 flex items-center gap-2\">\n              <div className=\"text-muted-foreground\">\n                {getOptionIcon(selected)}\n              </div>\n              <span className=\"font-semibold\">{selected.label}</span>\n            </div>\n            <div className=\"text-muted-foreground text-sm\">\n              {selected.address}\n            </div>\n          </div>\n        </div>\n      </Card>\n    );\n  }\n\n  // Interactive state - show picker\n  const options = buildPickupOptions();\n\n  const handleSelect = (option: PickupOption) => {\n    addResult({\n      selectedPickup: option,\n    });\n  };\n\n  const currentLocation = options.filter((opt) => opt.type === \"current\");\n  const savedPlaces = options.filter((opt) => opt.type === \"saved\");\n\n  return (\n    <Card className=\"max-w-md space-y-4 p-4\">\n      {currentLocation.length > 0 && (\n        <div className=\"space-y-2\">\n          <div className=\"text-muted-foreground flex items-center gap-2 text-sm font-medium\">\n            <Navigation className=\"h-3.5 w-3.5\" />\n            <span>Current Location</span>\n          </div>\n          <div className=\"space-y-2\">\n            {currentLocation.map((option) => (\n              <Button\n                key={option.id}\n                variant=\"outline\"\n                className=\"hover:bg-accent h-auto w-full justify-start px-4 py-3 text-left\"\n                onClick={() => handleSelect(option)}\n              >\n                <div className=\"flex w-full items-start gap-3\">\n                  <div className=\"text-muted-foreground mt-0.5\">\n                    {getOptionIcon(option)}\n                  </div>\n                  <div className=\"min-w-0 flex-1\">\n                    <div className=\"font-medium\">{option.label}</div>\n                    <div className=\"text-muted-foreground truncate text-sm\">\n                      {option.address}\n                    </div>\n                  </div>\n                </div>\n              </Button>\n            ))}\n          </div>\n        </div>\n      )}\n\n      {savedPlaces.length > 0 && (\n        <div className=\"space-y-2\">\n          <div className=\"text-muted-foreground flex items-center gap-2 text-sm font-medium\">\n            <MapPin className=\"h-3.5 w-3.5\" />\n            <span>Saved Places</span>\n          </div>\n          <div className=\"space-y-2\">\n            {savedPlaces.map((option) => (\n              <Button\n                key={option.id}\n                variant=\"outline\"\n                className=\"hover:bg-accent h-auto w-full justify-start px-4 py-3 text-left\"\n                onClick={() => handleSelect(option)}\n              >\n                <div className=\"flex w-full items-start gap-3\">\n                  <div className=\"text-muted-foreground mt-0.5\">\n                    {getOptionIcon(option)}\n                  </div>\n                  <div className=\"min-w-0 flex-1\">\n                    <div className=\"font-medium\">{option.label}</div>\n                    <div className=\"text-muted-foreground truncate text-sm\">\n                      {option.address}\n                    </div>\n                  </div>\n                </div>\n              </Button>\n            ))}\n          </div>\n        </div>\n      )}\n    </Card>\n  );\n}\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo-v2/components/RideQuote.tsx",
    "content": "\"use client\";\n\n/**\n * RideQuote - Confirmation Pattern with Inline Pickup Selection\n *\n * Shows ride details (route, ETA, price) with a Confirm button.\n * Handles pickup location changes inline without requiring a separate tool call.\n *\n * Transforms to receipt state after confirmation.\n */\n\nimport type { ToolCallMessagePartProps } from \"@assistant-ui/react\";\nimport { Card } from \"@/components/ui/card\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  MapPin,\n  Clock,\n  CreditCard,\n  Car,\n  CheckCircle2,\n  Loader2,\n  Navigation,\n  Home,\n  Briefcase,\n  ArrowLeft,\n} from \"lucide-react\";\nimport { useState, useEffect, useCallback } from \"react\";\nimport type { GetRideQuoteResult, RideQuote as RideQuoteType } from \"../types\";\nimport { MOCK_LOCATIONS, MOCK_PICKUP } from \"../types\";\n\n// Pickup option for inline selection\ninterface PickupOption {\n  id: string;\n  label: string;\n  address: string;\n  type: \"current\" | \"saved\";\n}\n\nconst formatPrice = (amount: number, currency: string) => {\n  if (currency === \"USD\") {\n    return `$${amount.toFixed(2)}`;\n  }\n  return `${amount} ${currency}`;\n};\n\n// Build pickup options for inline selection\nconst buildPickupOptions = (): PickupOption[] => {\n  const options: PickupOption[] = [\n    {\n      id: \"current\",\n      label: MOCK_PICKUP.label,\n      address: MOCK_PICKUP.address,\n      type: \"current\",\n    },\n  ];\n\n  for (const loc of MOCK_LOCATIONS.filter((l) => l.type === \"favorite\")) {\n    options.push({\n      id: loc.id,\n      label: loc.label,\n      address: loc.address,\n      type: \"saved\",\n    });\n  }\n\n  return options;\n};\n\nconst getPickupIcon = (option: PickupOption) => {\n  if (option.type === \"current\") {\n    return <Navigation className=\"h-5 w-5\" />;\n  }\n  if (option.label.toLowerCase() === \"home\") {\n    return <Home className=\"h-5 w-5\" />;\n  }\n  if (option.label.toLowerCase() === \"work\") {\n    return <Briefcase className=\"h-5 w-5\" />;\n  }\n  return <MapPin className=\"h-5 w-5\" />;\n};\n\n// Generate a mock quote based on destination and pickup\nconst generateQuote = (\n  destinationId: string,\n  pickupOverride?: { label: string; address: string },\n): RideQuoteType => {\n  const destination = MOCK_LOCATIONS.find((loc) => loc.id === destinationId);\n\n  const destInfo = destination ?? {\n    label: \"Destination\",\n    address: \"Unknown address\",\n  };\n\n  // Base prices - adjust slightly based on pickup location\n  const basePrices: Record<string, number> = {\n    home: 12.5,\n    work: 18.75,\n    \"ferry-building\": 15.0,\n  };\n\n  const baseEtas: Record<string, number> = {\n    home: 5,\n    work: 8,\n    \"ferry-building\": 6,\n  };\n\n  // Slight variation if pickup is not current location\n  const priceMultiplier = pickupOverride ? 1.1 : 1.0;\n  const etaAddition = pickupOverride ? 2 : 0;\n\n  return {\n    quoteId: `quote_${Date.now()}`,\n    pickup: pickupOverride ?? MOCK_PICKUP,\n    destination: {\n      label: destInfo.label,\n      address: destInfo.address,\n    },\n    etaMinutes: (baseEtas[destinationId] ?? 7) + etaAddition,\n    price: {\n      amount: (basePrices[destinationId] ?? 14.0) * priceMultiplier,\n      currency: \"USD\",\n    },\n    vehicle: {\n      type: \"Waymo One\",\n    },\n    payment: {\n      method: \"Apple Pay\",\n    },\n    expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(),\n  };\n};\n\nexport function RideQuote({\n  args,\n  result,\n  addResult,\n}: ToolCallMessagePartProps<{ destinationId: string }, GetRideQuoteResult>) {\n  const [isConfirming, setIsConfirming] = useState(false);\n  const [isPriceLoading, setIsPriceLoading] = useState(true);\n  const [mode, setMode] = useState<\"quote\" | \"selecting-pickup\">(\"quote\");\n  const [currentPickup, setCurrentPickup] = useState<{\n    label: string;\n    address: string;\n  } | null>(null);\n\n  // Generate quote based on current pickup\n  const quote =\n    result?.quote ??\n    generateQuote(args.destinationId, currentPickup ?? undefined);\n\n  // Simulate async price/ETA calculation\n  useEffect(() => {\n    // Skip loading simulation if we already have a result\n    if (result) {\n      setIsPriceLoading(false);\n      return;\n    }\n\n    setIsPriceLoading(true);\n    const timer = setTimeout(() => {\n      setIsPriceLoading(false);\n    }, 800);\n\n    return () => clearTimeout(timer);\n  }, [result, currentPickup]); // Re-run when pickup changes\n\n  // Handle pickup selection\n  const handleSelectPickup = useCallback((option: PickupOption) => {\n    setCurrentPickup({ label: option.label, address: option.address });\n    setMode(\"quote\");\n  }, []);\n\n  // Receipt state - show confirmation\n  if (result?.confirmed) {\n    return (\n      <Card className=\"max-w-md p-4\">\n        <div className=\"flex items-start gap-3\">\n          <div className=\"mt-0.5 text-green-600\">\n            <CheckCircle2 className=\"h-5 w-5\" />\n          </div>\n          <div className=\"flex-1\">\n            <div className=\"font-semibold\">Ride confirmed</div>\n            <div className=\"text-muted-foreground mt-1 text-sm\">\n              {quote.pickup.label} → {quote.destination.label}\n            </div>\n            <div className=\"text-muted-foreground mt-1 text-xs\">\n              {quote.etaMinutes} min •{\" \"}\n              {formatPrice(quote.price.amount, quote.price.currency)}\n            </div>\n          </div>\n        </div>\n      </Card>\n    );\n  }\n\n  const handleConfirm = () => {\n    setIsConfirming(true);\n    // Simulate brief delay for booking\n    setTimeout(() => {\n      addResult({\n        quote,\n        confirmed: true,\n      });\n    }, 800);\n  };\n\n  // Pickup selection mode\n  if (mode === \"selecting-pickup\") {\n    const pickupOptions = buildPickupOptions();\n    const currentOptions = pickupOptions.filter((o) => o.type === \"current\");\n    const savedOptions = pickupOptions.filter((o) => o.type === \"saved\");\n\n    return (\n      <Card className=\"max-w-md p-4\">\n        {/* Header with back button */}\n        <div className=\"mb-4 flex items-center gap-2\">\n          <Button\n            variant=\"ghost\"\n            size=\"sm\"\n            className=\"text-muted-foreground -ml-2 h-8 w-8 p-0\"\n            onClick={() => setMode(\"quote\")}\n          >\n            <ArrowLeft className=\"h-4 w-4\" />\n          </Button>\n          <span className=\"text-sm font-medium\">Select pickup location</span>\n        </div>\n\n        {/* Current Location */}\n        {currentOptions.length > 0 && (\n          <div className=\"mb-3 space-y-2\">\n            <div className=\"text-muted-foreground flex items-center gap-2 text-xs font-medium\">\n              <Navigation className=\"h-3.5 w-3.5\" />\n              <span>Current Location</span>\n            </div>\n            {currentOptions.map((option) => (\n              <Button\n                key={option.id}\n                variant=\"outline\"\n                className=\"hover:bg-accent h-auto w-full justify-start px-3 py-2.5 text-left\"\n                onClick={() => handleSelectPickup(option)}\n              >\n                <div className=\"flex w-full items-start gap-3\">\n                  <div className=\"text-muted-foreground mt-0.5\">\n                    {getPickupIcon(option)}\n                  </div>\n                  <div className=\"min-w-0 flex-1\">\n                    <div className=\"text-sm font-medium\">{option.label}</div>\n                    <div className=\"text-muted-foreground truncate text-xs\">\n                      {option.address}\n                    </div>\n                  </div>\n                </div>\n              </Button>\n            ))}\n          </div>\n        )}\n\n        {/* Saved Places */}\n        {savedOptions.length > 0 && (\n          <div className=\"space-y-2\">\n            <div className=\"text-muted-foreground flex items-center gap-2 text-xs font-medium\">\n              <MapPin className=\"h-3.5 w-3.5\" />\n              <span>Saved Places</span>\n            </div>\n            {savedOptions.map((option) => (\n              <Button\n                key={option.id}\n                variant=\"outline\"\n                className=\"hover:bg-accent h-auto w-full justify-start px-3 py-2.5 text-left\"\n                onClick={() => handleSelectPickup(option)}\n              >\n                <div className=\"flex w-full items-start gap-3\">\n                  <div className=\"text-muted-foreground mt-0.5\">\n                    {getPickupIcon(option)}\n                  </div>\n                  <div className=\"min-w-0 flex-1\">\n                    <div className=\"text-sm font-medium\">{option.label}</div>\n                    <div className=\"text-muted-foreground truncate text-xs\">\n                      {option.address}\n                    </div>\n                  </div>\n                </div>\n              </Button>\n            ))}\n          </div>\n        )}\n      </Card>\n    );\n  }\n\n  return (\n    <Card className=\"max-w-md p-5\">\n      {/* Route */}\n      <div className=\"mb-4 space-y-3\">\n        {/* Pickup */}\n        <div className=\"flex items-start gap-3\">\n          <div className=\"mt-1.5\">\n            <div className=\"h-2.5 w-2.5 rounded-full bg-blue-600\" />\n          </div>\n          <div className=\"min-w-0 flex-1\">\n            <div className=\"text-muted-foreground text-xs uppercase tracking-wide\">\n              Pickup\n            </div>\n            <div className=\"font-medium\">{quote.pickup.label}</div>\n            <div className=\"text-muted-foreground truncate text-sm\">\n              {quote.pickup.address}\n            </div>\n          </div>\n        </div>\n\n        <div className=\"border-muted-foreground/30 ml-[5px] h-6 w-0 border-l-2 border-dashed\" />\n\n        {/* Destination */}\n        <div className=\"flex items-start gap-3\">\n          <div className=\"mt-1\">\n            <MapPin className=\"text-muted-foreground h-4 w-4\" />\n          </div>\n          <div className=\"flex-1\">\n            <div className=\"text-muted-foreground text-xs uppercase tracking-wide\">\n              Destination\n            </div>\n            <div className=\"font-medium\">{quote.destination.label}</div>\n            <div className=\"text-muted-foreground text-sm\">\n              {quote.destination.address}\n            </div>\n          </div>\n        </div>\n      </div>\n\n      {/* Details */}\n      <div className=\"bg-muted/50 mb-4 rounded-lg p-4\">\n        <div className=\"flex items-center justify-between\">\n          <div className=\"flex items-center gap-4\">\n            {isPriceLoading ? (\n              <>\n                <div className=\"bg-muted-foreground/20 h-5 w-16 animate-pulse rounded\" />\n                <div className=\"bg-muted-foreground/20 h-6 w-20 animate-pulse rounded\" />\n              </>\n            ) : (\n              <>\n                <div className=\"flex items-center gap-2\">\n                  <Clock className=\"text-muted-foreground h-4 w-4\" />\n                  <span className=\"font-medium\">{quote.etaMinutes} min</span>\n                </div>\n                <div className=\"flex items-center gap-2\">\n                  <span className=\"text-lg font-semibold\">\n                    {formatPrice(quote.price.amount, quote.price.currency)}\n                  </span>\n                </div>\n              </>\n            )}\n          </div>\n        </div>\n        <div className=\"text-muted-foreground mt-2 flex items-center gap-2 text-sm\">\n          <Car className=\"h-4 w-4\" />\n          <span>{quote.vehicle.type}</span>\n        </div>\n      </div>\n\n      {/* Payment */}\n      <div className=\"mb-5 flex items-center justify-between rounded-lg border p-3\">\n        <div className=\"flex items-center gap-2\">\n          <CreditCard className=\"text-muted-foreground h-4 w-4\" />\n          <span className=\"text-muted-foreground text-sm\">Payment</span>\n        </div>\n        <span className=\"text-sm font-medium\">{quote.payment.method}</span>\n      </div>\n\n      {/* Actions */}\n      <div className=\"space-y-2\">\n        <Button\n          onClick={handleConfirm}\n          disabled={isPriceLoading || isConfirming}\n          className=\"w-full\"\n          size=\"lg\"\n        >\n          {isPriceLoading ? (\n            <>\n              <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n              Calculating...\n            </>\n          ) : isConfirming ? (\n            <>\n              <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n              Confirming...\n            </>\n          ) : (\n            \"Confirm Ride\"\n          )}\n        </Button>\n        <Button\n          variant=\"ghost\"\n          onClick={() => setMode(\"selecting-pickup\")}\n          disabled={isPriceLoading || isConfirming}\n          className=\"text-muted-foreground w-full\"\n          size=\"sm\"\n        >\n          Change pickup location\n        </Button>\n      </div>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo-v2/components/TripStatus.tsx",
    "content": "\"use client\";\n\n/**\n * TripStatus - Progress Pattern\n *\n * Shows live trip progress with a timeline.\n * Updates as the trip progresses through phases.\n */\n\nimport type { ToolCallMessagePartProps } from \"@assistant-ui/react\";\nimport { Card } from \"@/components/ui/card\";\nimport { Button } from \"@/components/ui/button\";\nimport { CheckCircle2, Circle, Car, XCircle } from \"lucide-react\";\nimport type {\n  GetTripStatusResult,\n  TripStep,\n  TripStatus as TripStatusType,\n} from \"../types\";\nimport { MOCK_VEHICLE } from \"../types\";\n\nconst StepIcon = ({ status }: { status: TripStep[\"status\"] }) => {\n  if (status === \"completed\") {\n    return <CheckCircle2 className=\"h-5 w-5 text-green-600\" />;\n  }\n  if (status === \"current\") {\n    return (\n      <div className=\"relative\">\n        <Circle className=\"h-5 w-5 text-blue-600 fill-blue-600\" />\n        <div className=\"absolute inset-0 animate-ping\">\n          <Circle className=\"h-5 w-5 text-blue-600 opacity-50\" />\n        </div>\n      </div>\n    );\n  }\n  return <Circle className=\"text-muted-foreground/40 h-5 w-5\" />;\n};\n\n// Generate initial trip status\nconst generateTripStatus = (tripId: string): TripStatusType => {\n  return {\n    tripId,\n    currentPhase: \"vehicle_assigned\",\n    steps: [\n      {\n        phase: \"requested\",\n        label: \"Ride requested\",\n        status: \"completed\",\n        timestamp: \"Just now\",\n      },\n      {\n        phase: \"vehicle_assigned\",\n        label: \"Vehicle assigned\",\n        status: \"current\",\n      },\n      {\n        phase: \"en_route_to_pickup\",\n        label: \"On the way to you\",\n        status: \"pending\",\n      },\n      {\n        phase: \"arrived_at_pickup\",\n        label: \"Arrived at pickup\",\n        status: \"pending\",\n      },\n      {\n        phase: \"trip_in_progress\",\n        label: \"Trip in progress\",\n        status: \"pending\",\n      },\n      {\n        phase: \"arrived_at_destination\",\n        label: \"Arrived at destination\",\n        status: \"pending\",\n      },\n    ],\n    vehicle: MOCK_VEHICLE,\n    etaMinutes: 3,\n    canCancel: true,\n  };\n};\n\nexport function TripStatus({\n  args,\n  result,\n  addResult,\n}: ToolCallMessagePartProps<{ tripId: string }, GetTripStatusResult>) {\n  // Generate status from args\n  const status = result?.status ?? generateTripStatus(args.tripId);\n\n  // Cancelled state\n  if (result?.cancelled) {\n    return (\n      <Card className=\"max-w-md p-4\">\n        <div className=\"flex items-start gap-3\">\n          <div className=\"mt-0.5 text-red-600\">\n            <XCircle className=\"h-5 w-5\" />\n          </div>\n          <div className=\"flex-1\">\n            <div className=\"font-semibold\">Ride cancelled</div>\n            <div className=\"text-muted-foreground mt-1 text-sm\">\n              Your ride has been cancelled. No charge.\n            </div>\n          </div>\n        </div>\n      </Card>\n    );\n  }\n\n  // Completed state - receipt\n  if (status.currentPhase === \"completed\") {\n    return (\n      <Card className=\"max-w-md p-4\">\n        <div className=\"flex items-start gap-3\">\n          <div className=\"mt-0.5 text-green-600\">\n            <CheckCircle2 className=\"h-5 w-5\" />\n          </div>\n          <div className=\"flex-1\">\n            <div className=\"font-semibold\">Trip completed</div>\n            <div className=\"text-muted-foreground mt-1 text-sm\">\n              Thanks for riding with Waymo!\n            </div>\n          </div>\n        </div>\n      </Card>\n    );\n  }\n\n  const handleCancel = () => {\n    addResult({\n      status,\n      cancelled: true,\n    });\n  };\n\n  return (\n    <Card className=\"max-w-md p-5\">\n      <div className=\"mb-4\">\n        <h3 className=\"text-lg font-semibold\">Your ride</h3>\n        {status.etaMinutes && (\n          <p className=\"text-muted-foreground text-sm\">\n            {status.etaMinutes} min away\n          </p>\n        )}\n      </div>\n\n      {/* Timeline */}\n      <div className=\"mb-5 space-y-0\">\n        {status.steps.map((step, index) => (\n          <div key={step.phase} className=\"flex gap-3\">\n            <div className=\"flex flex-col items-center\">\n              <StepIcon status={step.status} />\n              {index < status.steps.length - 1 && (\n                <div\n                  className={`w-0.5 flex-1 my-1 ${\n                    step.status === \"completed\"\n                      ? \"bg-green-600\"\n                      : \"bg-muted-foreground/20\"\n                  }`}\n                />\n              )}\n            </div>\n            <div className=\"flex-1 pb-4\">\n              <div\n                className={`font-medium ${\n                  step.status === \"pending\"\n                    ? \"text-muted-foreground\"\n                    : \"text-foreground\"\n                }`}\n              >\n                {step.label}\n              </div>\n              {step.timestamp && (\n                <div className=\"text-muted-foreground text-xs\">\n                  {step.timestamp}\n                </div>\n              )}\n            </div>\n          </div>\n        ))}\n      </div>\n\n      {/* Vehicle info */}\n      {status.vehicle && (\n        <div className=\"bg-muted/50 mb-4 flex items-center gap-3 rounded-lg p-3\">\n          <Car className=\"text-muted-foreground h-5 w-5\" />\n          <div>\n            <div className=\"text-sm font-medium\">\n              {status.vehicle.color} {status.vehicle.make}{\" \"}\n              {status.vehicle.model}\n            </div>\n            <div className=\"text-muted-foreground text-xs\">\n              {status.vehicle.plate}\n            </div>\n          </div>\n        </div>\n      )}\n\n      {/* Cancel button */}\n      {status.canCancel && (\n        <Button\n          variant=\"outline\"\n          onClick={handleCancel}\n          className=\"w-full text-red-600 hover:bg-red-50 hover:text-red-700\"\n        >\n          Cancel Ride\n        </Button>\n      )}\n    </Card>\n  );\n}\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo-v2/components/index.ts",
    "content": "export { DestinationPicker } from \"./DestinationPicker\";\nexport { PickupPicker } from \"./PickupPicker\";\nexport { RideQuote } from \"./RideQuote\";\nexport { TripStatus } from \"./TripStatus\";\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo-v2/index.ts",
    "content": "/**\n * Waymo v2 Prototype\n *\n * A minimal ride booking flow demonstrating three Tool UI patterns:\n * - Selection (DestinationPicker)\n * - Confirmation (RideQuote)\n * - Progress (TripStatus)\n */\n\nimport type { Prototype } from \"../../types\";\nimport { WAYMO_V2_SYSTEM_PROMPT } from \"./system-prompt\";\n\n// Export tools for mounting in chat-pane\nexport {\n  SelectDestinationTool,\n  SelectPickupTool,\n  GetRideQuoteTool,\n  GetTripStatusTool,\n} from \"./tools\";\n\n// Export components\nexport {\n  DestinationPicker,\n  PickupPicker,\n  RideQuote,\n  TripStatus,\n} from \"./components\";\n\n/**\n * The waymo-v2 prototype definition.\n *\n * Note: This prototype uses frontend tools registered via Toolkit/Tools().\n * The tools array is empty because runtime registration happens in ChatPane.\n */\nexport const waymoV2Prototype: Prototype = {\n  slug: \"waymo-v2\",\n  title: \"Waymo v2 (Minimal Flow)\",\n  summary: \"Three-pattern ride booking: Selection, Confirmation, Progress\",\n  systemPrompt: WAYMO_V2_SYSTEM_PROMPT,\n  tools: [], // Tools are registered via Toolkit/Tools()\n};\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo-v2/system-prompt.ts",
    "content": "/**\n * Waymo v2 System Prompt\n *\n * Emphasizes the triadic loop: Assistant narrates, Surface mediates, User controls.\n */\n\nexport const WAYMO_V2_SYSTEM_PROMPT =\n  `You are a Waymo ride booking assistant. Help users book autonomous vehicle rides with minimal friction.\n\n## The Triadic Loop\n\nYou, the user, and the Tool UIs form a collaborative triad. Your job is to:\n\n1. **Introduce** each Tool UI with brief context\n2. **Acknowledge** what the user selected in the UI\n3. **Narrate** transitions between steps\n4. **Reference** specific data from the UI in your responses\n\nNever show a Tool UI silently. Always introduce it or acknowledge the user's interaction with it.\n\n## Your Tools\n\nYou have four tools that present visual UIs:\n\n1. **select_destination** - Shows interactive picker for user to choose destination. **Only use when destination is unknown.** If user already said where (\"take me home\"), skip this and go straight to get_ride_quote.\n\n2. **select_pickup** - Shows pickup location options (current GPS location, saved places). Use when user wants to change pickup. The result includes \\`selectedPickup\\` when user picks one.\n\n3. **get_ride_quote** - Shows a contract: route, ETA, price with Confirm button. User can change pickup location inline within the UI. Takes \\`destinationId\\` (valid: \"home\", \"work\", \"ferry-building\"). Result: \\`confirmed: true\\` when user confirms the ride. Call get_trip_status after confirmation.\n\n4. **get_trip_status** - Shows live trip timeline with vehicle info. Takes \\`tripId\\`.\n\n## When to Show Which Tool\n\n| User says | You do |\n|-----------|--------|\n| \"I need a ride\" (no destination) | Show select_destination picker |\n| \"Take me home\" / \"Ride to work\" | Skip picker, go straight to get_ride_quote |\n| \"How much to the Ferry Building?\" | Skip picker, show get_ride_quote |\n\nThe key insight: Don't show a destination picker if the destination is already clear. The quote UI shows the destination anyway, so a separate confirmation receipt is redundant.\n\n## Example Flows\n\n**Destination unknown:**\nUser: \"I need a ride\"\nYou: \"Where would you like to go?\"\n[Call select_destination]\n--- User clicks \"Home\" ---\nYou: \"Home it is! Here's your quote.\"\n[Call get_ride_quote with destinationId: \"home\"]\n\n**Destination known:**\nUser: \"Take me home\"\nYou: \"Home it is! Here's your quote.\"\n[Call get_ride_quote with destinationId: \"home\"]\n\n--- User reviews quote, optionally changes pickup, then clicks \"Confirm Ride\" ---\n\nYou: \"You're all set! Your Waymo is on the way.\"\n[Call get_trip_status with tripId from the quote]\n\n## Key Behaviors\n\n- **Always introduce**: Before showing a Tool UI, say something brief like \"Where to?\" or \"Here's your quote.\"\n\n- **Always acknowledge**: After the user interacts with a Tool UI, acknowledge their choice: \"Home it is!\" or \"Great, confirmed!\"\n\n- **Reference specifics**: Use data from the UI in your narration: \"Your 5-minute ride to Home will cost $12.50.\"\n\n- **Keep it brief**: The Tool UI shows the details. Your job is to provide context and continuity, not repeat everything.\n\n- **Don't over-explain**: If the UI is self-explanatory, a simple \"Here you go:\" suffices.\n\n## Anti-patterns to Avoid\n\n❌ Showing a Tool UI with no introduction\n❌ Ignoring the user's selection and moving on silently\n❌ Repeating every detail the UI already displays\n❌ Asking clarifying questions when the UI can capture input\n` as const;\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo-v2/tools/get-ride-quote.tsx",
    "content": "\"use client\";\n\nimport type { ToolDefinition } from \"@assistant-ui/react\";\nimport { z } from \"zod\";\nimport { RideQuote } from \"../components\";\nimport type { GetRideQuoteResult } from \"../types\";\n\nexport const GetRideQuoteTool: ToolDefinition<\n  { destinationId: string },\n  GetRideQuoteResult\n> = {\n  description:\n    \"Show ride quote with route, ETA, price, and payment. User can change pickup location inline. When confirmed=true, the ride is booked—call get_trip_status next.\",\n  parameters: z.object({\n    destinationId: z\n      .string()\n      .describe(\n        \"The ID of the destination (e.g., 'home', 'work', 'ferry-building')\",\n      ),\n  }),\n  type: \"human\",\n  render: (props) => <RideQuote {...props} />,\n};\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo-v2/tools/get-trip-status.tsx",
    "content": "\"use client\";\n\nimport type { ToolDefinition } from \"@assistant-ui/react\";\nimport { z } from \"zod\";\nimport { TripStatus } from \"../components\";\nimport type { GetTripStatusResult } from \"../types\";\n\nexport const GetTripStatusTool: ToolDefinition<\n  { tripId: string },\n  GetTripStatusResult\n> = {\n  description:\n    \"Show live trip timeline with vehicle info. Use after ride is confirmed. Introduce with something like 'Your Waymo is on the way!' If user cancels (result includes cancelled: true), acknowledge.\",\n  parameters: z.object({\n    tripId: z.string().describe(\"The trip ID from the confirmed booking\"),\n  }),\n  type: \"human\",\n  render: (props) => <TripStatus {...props} />,\n};\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo-v2/tools/index.ts",
    "content": "export { SelectDestinationTool } from \"./select-destination\";\nexport { SelectPickupTool } from \"./select-pickup\";\nexport { GetRideQuoteTool } from \"./get-ride-quote\";\nexport { GetTripStatusTool } from \"./get-trip-status\";\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo-v2/tools/select-destination.tsx",
    "content": "\"use client\";\n\nimport type { ToolDefinition } from \"@assistant-ui/react\";\nimport { z } from \"zod\";\nimport { DestinationPicker } from \"../components\";\nimport type { SelectDestinationResult } from \"../types\";\n\n/**\n * Selection Tool - Destination Picker\n *\n * Only use when destination is unknown. If user already specified where\n * they want to go (\"take me home\"), skip this and call get_ride_quote directly.\n * The quote UI shows the destination, so a separate picker would be redundant.\n */\nexport const SelectDestinationTool: ToolDefinition<\n  Record<string, never>,\n  SelectDestinationResult\n> = {\n  description:\n    \"Show destination picker with saved locations (Home, Work) and recents. Only use when destination is unknown. If user already said where ('take me home'), skip this and call get_ride_quote directly.\",\n  parameters: z.object({}),\n  type: \"human\",\n  render: (props) => <DestinationPicker {...props} />,\n};\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo-v2/tools/select-pickup.tsx",
    "content": "\"use client\";\n\nimport type { ToolDefinition } from \"@assistant-ui/react\";\nimport { z } from \"zod\";\nimport { PickupPicker } from \"../components/PickupPicker\";\nimport type { SelectPickupResult } from \"../types\";\n\nexport const SelectPickupTool: ToolDefinition<\n  Record<string, never>,\n  SelectPickupResult\n> = {\n  description:\n    \"Show a picker for selecting pickup location. Options include current GPS location and saved places (Home, Work). Use when user wants to change their pickup location. After selection, show an updated ride quote.\",\n  parameters: z.object({}),\n  type: \"human\",\n  render: (props) => <PickupPicker {...props} />,\n};\n"
  },
  {
    "path": "apps/www/lib/playground/prototypes/waymo-v2/types.ts",
    "content": "/**\n * Waymo v2 - Minimal Flow Types\n *\n * Focused types for the three-pattern prototype:\n * - Selection (DestinationPicker)\n * - Confirmation (RideQuote)\n * - Progress (TripStatus)\n */\n\n// ============================================================================\n// Domain Types\n// ============================================================================\n\nexport interface Location {\n  id: string;\n  label: string; // \"Home\", \"Work\", \"Ferry Building\"\n  address: string;\n  type: \"favorite\" | \"recent\";\n}\n\nexport interface Price {\n  amount: number;\n  currency: string;\n}\n\nexport interface Vehicle {\n  type: string; // \"Waymo One\"\n  make: string;\n  model: string;\n  color: string;\n  plate: string;\n}\n\n// ============================================================================\n// Tool: select_destination\n// Pattern: Selection\n// ============================================================================\n\nexport interface SelectDestinationArgs {\n  // No args - only used when destination is unknown\n}\n\nexport interface SelectDestinationResult {\n  locations: Location[];\n  selectedLocation?: Location; // Set when user makes selection\n}\n\n// ============================================================================\n// Tool: select_pickup\n// Pattern: Selection\n// ============================================================================\n\nexport interface PickupOption {\n  id: string;\n  label: string;\n  address: string;\n  type: \"current\" | \"saved\";\n}\n\nexport interface SelectPickupResult {\n  selectedPickup?: PickupOption; // Set when user makes selection\n}\n\n// ============================================================================\n// Tool: get_ride_quote\n// Pattern: Confirmation\n// ============================================================================\n\nexport interface GetRideQuoteArgs {\n  destinationId: string;\n}\n\nexport interface RideQuote {\n  quoteId: string;\n  pickup: {\n    label: string;\n    address: string;\n  };\n  destination: {\n    label: string;\n    address: string;\n  };\n  etaMinutes: number;\n  price: Price;\n  vehicle: {\n    type: string;\n  };\n  payment: {\n    method: string; // \"Apple Pay\", \"Visa (...4242)\"\n  };\n  expiresAt: string; // ISO8601\n}\n\nexport interface GetRideQuoteResult {\n  quote: RideQuote;\n  confirmed?: boolean; // Set when user confirms the ride\n}\n\n// ============================================================================\n// Tool: get_trip_status\n// Pattern: Progress\n// ============================================================================\n\nexport type TripPhase =\n  | \"requested\"\n  | \"vehicle_assigned\"\n  | \"en_route_to_pickup\"\n  | \"arrived_at_pickup\"\n  | \"trip_in_progress\"\n  | \"arrived_at_destination\"\n  | \"completed\"\n  | \"cancelled\";\n\nexport interface TripStep {\n  phase: TripPhase;\n  label: string;\n  status: \"completed\" | \"current\" | \"pending\";\n  timestamp?: string;\n}\n\nexport interface GetTripStatusArgs {\n  tripId: string;\n}\n\nexport interface TripStatus {\n  tripId: string;\n  currentPhase: TripPhase;\n  steps: TripStep[];\n  vehicle?: Vehicle;\n  etaMinutes?: number;\n  canCancel: boolean;\n}\n\nexport interface GetTripStatusResult {\n  status: TripStatus;\n  cancelled?: boolean; // Set if user cancels\n}\n\n// ============================================================================\n// Mock Data\n// ============================================================================\n\nexport const MOCK_LOCATIONS: Location[] = [\n  {\n    id: \"home\",\n    label: \"Home\",\n    address: \"123 Main Street, San Francisco, CA 94105\",\n    type: \"favorite\",\n  },\n  {\n    id: \"work\",\n    label: \"Work\",\n    address: \"456 Market Street, San Francisco, CA 94103\",\n    type: \"favorite\",\n  },\n  {\n    id: \"ferry-building\",\n    label: \"Ferry Building\",\n    address: \"1 Ferry Building, San Francisco, CA 94111\",\n    type: \"recent\",\n  },\n];\n\nexport const MOCK_PICKUP = {\n  label: \"Current Location\",\n  address: \"789 Downtown Ave, San Francisco, CA 94102\",\n};\n\nexport const MOCK_VEHICLE: Vehicle = {\n  type: \"Waymo One\",\n  make: \"Jaguar\",\n  model: \"I-PACE\",\n  color: \"White\",\n  plate: \"8ABC123\",\n};\n"
  },
  {
    "path": "apps/www/lib/playground/registry.ts",
    "content": "import type { Prototype } from \"./types\";\n\nimport {\n  foodOrderingPrototype,\n  waymoPrototype,\n  waymoV2Prototype,\n} from \"./prototypes\";\n\nexport const PROTOTYPES: Prototype[] = [\n  waymoV2Prototype,\n  foodOrderingPrototype,\n  waymoPrototype,\n];\n\nexport const listPrototypes = (): Prototype[] => PROTOTYPES;\n\nexport const findPrototype = (slug: string): Prototype | undefined =>\n  PROTOTYPES.find((prototype) => prototype.slug === slug);\n"
  },
  {
    "path": "apps/www/lib/playground/runtime.ts",
    "content": "import { anthropic } from \"@ai-sdk/anthropic\";\nimport { openai } from \"@ai-sdk/openai\";\nimport {\n  convertToModelMessages,\n  stepCountIs,\n  streamText,\n  tool,\n  type LanguageModel,\n  type ToolSet,\n  type UIMessage,\n} from \"ai\";\nimport { z } from \"zod\";\nimport { frontendTools } from \"@assistant-ui/react-ai-sdk\";\n\nimport type { Prototype } from \"./types\";\nimport { hasToolUi } from \"./tool-uis\";\n\nconst DEFAULT_MODEL = \"openai/gpt-4.1-mini\";\n\nconst PROVIDERS: Record<string, (modelId: string) => LanguageModel> = {\n  openai,\n  anthropic,\n};\n\nconst normalizeToolInputSchema = (\n  schema: z.ZodTypeAny | undefined,\n  toolName: string,\n) => {\n  if (!schema) {\n    return z.object({});\n  }\n\n  if (schema instanceof z.ZodObject) {\n    return schema;\n  }\n\n  console.warn(\n    `Tool \"${toolName}\" supplied a non-object schema. Falling back to an empty object schema.`,\n  );\n  return z.object({});\n};\n\nconst resolveModel = (identifier: string) => {\n  const [provider, ...rest] = identifier.split(\"/\");\n  if (!provider) {\n    throw new Error(\n      \"DEFAULT_MODEL must include a provider segment, e.g. openai/gpt-4.1-mini\",\n    );\n  }\n  const modelId = rest.join(\"/\");\n  if (!modelId) {\n    throw new Error(\n      `Model identifier \"${identifier}\" is missing the model name`,\n    );\n  }\n  const resolver = PROVIDERS[provider];\n  if (!resolver) {\n    throw new Error(`Unsupported model provider \"${provider}\"`);\n  }\n  return resolver(modelId);\n};\n\nexport const buildToolSet = (prototype: Prototype): ToolSet => {\n  const seenNames = new Set<string>();\n  const entries: Array<[string, ToolSet[string]]> = [];\n\n  for (const definition of prototype.tools) {\n    if (seenNames.has(definition.name)) {\n      console.warn(\n        `Duplicate tool name \"${definition.name}\" found in prototype \"${prototype.slug}\". Only the first occurrence will be used.`,\n      );\n      continue;\n    }\n    seenNames.add(definition.name);\n\n    if (definition.uiId && !hasToolUi(definition.uiId)) {\n      console.warn(\n        `Tool \"${definition.name}\" references unknown UI \"${definition.uiId}\". Falling back to default UI.`,\n      );\n    }\n\n    const inputSchema = normalizeToolInputSchema(\n      definition.input,\n      definition.name,\n    );\n\n    const builtTool = tool<unknown, unknown>({\n      description: definition.description,\n      inputSchema,\n      async execute(args: unknown) {\n        return definition.execute(args);\n      },\n    });\n\n    entries.push([definition.name, builtTool]);\n  }\n\n  return Object.fromEntries(entries) as ToolSet;\n};\n\nexport const streamPrototypeResponse = async (\n  prototype: Prototype,\n  messages: UIMessage[],\n  clientTools?: unknown,\n) => {\n  const model = resolveModel(DEFAULT_MODEL);\n  const prototypeTools = buildToolSet(prototype);\n\n  // Merge frontend tools with prototype tools\n  const tools = clientTools\n    ? {\n        // eslint-disable-next-line @typescript-eslint/no-explicit-any\n        ...(frontendTools(clientTools as any) as any),\n        ...prototypeTools,\n      }\n    : prototypeTools;\n\n  return streamText({\n    model,\n    system: prototype.systemPrompt,\n    messages: await convertToModelMessages(messages),\n    tools,\n    stopWhen: stepCountIs(100),\n  });\n};\n"
  },
  {
    "path": "apps/www/lib/playground/tool-uis.ts",
    "content": "import type { ToolCallMessagePartComponent } from \"@assistant-ui/react\";\n\nimport type { ToolUiId } from \"./types\";\n\nconst registry: Record<ToolUiId, () => Promise<ToolCallMessagePartComponent>> =\n  {\n    fallback: () =>\n      import(\"@/app/components/assistant-ui/tool-fallback\").then(\n        (mod) => mod.ToolFallback,\n      ),\n    \"waymo-location-selector\": () =>\n      import(\"@/app/components/assistant-ui/tool-fallback\").then(\n        (mod) => mod.ToolFallback,\n      ),\n  };\n\nexport const TOOL_UI_REGISTRY = registry;\n\nexport const hasToolUi = (uiId: ToolUiId): boolean => uiId in registry;\n\nexport const getToolUiLoader = (\n  uiId: ToolUiId,\n): (() => Promise<ToolCallMessagePartComponent>) =>\n  registry[uiId] ?? registry.fallback;\n"
  },
  {
    "path": "apps/www/lib/playground/types.ts",
    "content": "import type { ZodTypeAny } from \"zod\";\n\nexport type ToolUiId = \"fallback\" | \"waymo-location-selector\";\n\nexport type ToolExecute = (args: unknown) => Promise<unknown>;\n\nexport type Tool = {\n  name: string;\n  description: string;\n  uiId?: ToolUiId;\n  execute: ToolExecute;\n  input?: ZodTypeAny;\n  output?: ZodTypeAny;\n};\n\nexport type Prototype = {\n  slug: string;\n  title: string;\n  summary?: string;\n  systemPrompt: string;\n  tools: Tool[];\n};\n"
  },
  {
    "path": "apps/www/lib/playground/weather-tuning/has-any-tuning-delta.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\n\nimport { hasAnyTuningDelta } from \"@/app/sandbox/weather-tuning/lib/has-any-tuning-delta\";\n\ndescribe(\"hasAnyTuningDelta\", () => {\n  it(\"returns false for empty overrides\", () => {\n    expect(hasAnyTuningDelta({})).toBe(false);\n    expect(\n      hasAnyTuningDelta({\n        clear: { dawn: {}, noon: {}, dusk: {}, midnight: {} },\n      }),\n    ).toBe(false);\n  });\n\n  it(\"returns true when any checkpoint contains override groups\", () => {\n    expect(\n      hasAnyTuningDelta({\n        clear: {\n          dawn: { glass: { depth: 8 } },\n          noon: {},\n          dusk: {},\n          midnight: {},\n        },\n      }),\n    ).toBe(true);\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/playground/weather-tuning/list-updated-params.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\n\nimport { listUpdatedParams } from \"@/app/sandbox/weather-tuning/lib/list-updated-params\";\n\ndescribe(\"listUpdatedParams\", () => {\n  it(\"returns all updated parameter paths\", () => {\n    const result = listUpdatedParams({\n      clear: {\n        dawn: {\n          glass: {\n            depth: 12,\n            strength: 90,\n          },\n          rain: {\n            zoom: 1.1,\n          },\n        },\n        noon: {},\n        dusk: {},\n        midnight: {},\n      },\n    });\n\n    expect(result).toEqual([\n      \"clear.dawn.glass.depth\",\n      \"clear.dawn.glass.strength\",\n      \"clear.dawn.rain.zoom\",\n    ]);\n  });\n\n  it(\"returns empty for no deltas\", () => {\n    const result = listUpdatedParams({\n      clear: { dawn: {}, noon: {}, dusk: {}, midnight: {} },\n    });\n    expect(result).toEqual([]);\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/playground/weather-tuning/parameter-definitions-coverage.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\n\nimport { getRawBaseParamsForCondition } from \"@/app/sandbox/weather-compositor/presets\";\nimport { PARAMETER_GROUPS } from \"@/app/sandbox/weather-tuning/components/parameter-definitions\";\n\nfunction getNumericKeys(value: unknown): string[] {\n  if (!value || typeof value !== \"object\") return [];\n  return Object.entries(value as Record<string, unknown>)\n    .filter(([, v]) => typeof v === \"number\")\n    .map(([k]) => k);\n}\n\ndescribe(\"tuning studio parameter definitions\", () => {\n  test(\"covers all numeric params exposed by compositor presets (excluding timeOfDay)\", () => {\n    const base = getRawBaseParamsForCondition(\n      \"clear\",\n      new Date().toISOString(),\n    );\n\n    const definedByLayer = new Map<string, Set<string>>();\n    for (const group of PARAMETER_GROUPS) {\n      const set = definedByLayer.get(group.layer) ?? new Set<string>();\n      for (const p of group.params) set.add(p.key);\n      definedByLayer.set(group.layer, set);\n    }\n\n    const layers: Array<keyof typeof base> = [\n      \"celestial\",\n      \"cloud\",\n      \"rain\",\n      \"lightning\",\n      \"snow\",\n      \"glass\",\n      \"post\",\n    ];\n\n    const missing: Record<string, string[]> = {};\n\n    for (const layer of layers) {\n      const numericKeys = getNumericKeys(base[layer]).filter(\n        (k) => !(layer === \"celestial\" && k === \"timeOfDay\"),\n      );\n      const defined = definedByLayer.get(layer) ?? new Set<string>();\n      const layerMissing = numericKeys.filter((k) => !defined.has(k)).sort();\n      if (layerMissing.length > 0) missing[layer] = layerMissing;\n    }\n\n    expect(missing).toEqual({});\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/playground/weather-tuning/rain-param-ranges.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\n\nimport { PARAMETER_GROUPS } from \"@/app/sandbox/weather-tuning/components/parameter-definitions\";\nimport { RAIN_PARAM_LIMITS } from \"@/app/sandbox/weather-tuning/lib/constants\";\n\ndescribe(\"rain parameter ranges\", () => {\n  it(\"uses widened rain limits for all rain params\", () => {\n    const rainGroup = PARAMETER_GROUPS.find((group) => group.layer === \"rain\");\n    expect(rainGroup).toBeDefined();\n\n    const byKey = new Map(rainGroup?.params.map((param) => [param.key, param]));\n\n    expect(byKey.get(\"glassIntensity\")?.max).toBe(\n      RAIN_PARAM_LIMITS.glassIntensity.max,\n    );\n    expect(byKey.get(\"zoom\")?.max).toBe(RAIN_PARAM_LIMITS.zoom.max);\n    expect(byKey.get(\"fallingIntensity\")?.max).toBe(\n      RAIN_PARAM_LIMITS.fallingIntensity.max,\n    );\n    expect(byKey.get(\"fallingSpeed\")?.max).toBe(\n      RAIN_PARAM_LIMITS.fallingSpeed.max,\n    );\n    expect(byKey.get(\"fallingAngle\")?.min).toBe(\n      RAIN_PARAM_LIMITS.fallingAngle.min,\n    );\n    expect(byKey.get(\"fallingAngle\")?.max).toBe(\n      RAIN_PARAM_LIMITS.fallingAngle.max,\n    );\n    expect(byKey.get(\"fallingStreakLength\")?.max).toBe(\n      RAIN_PARAM_LIMITS.fallingStreakLength.max,\n    );\n    expect(byKey.get(\"fallingLayers\")?.max).toBe(\n      RAIN_PARAM_LIMITS.fallingLayers.max,\n    );\n    expect(byKey.get(\"fallingRefraction\")?.max).toBe(\n      RAIN_PARAM_LIMITS.fallingRefraction.max,\n    );\n    expect(byKey.get(\"fallingWaviness\")?.max).toBe(\n      RAIN_PARAM_LIMITS.fallingWaviness.max,\n    );\n    expect(byKey.get(\"fallingThicknessVar\")?.max).toBe(\n      RAIN_PARAM_LIMITS.fallingThicknessVar.max,\n    );\n  });\n\n  it(\"provides substantially more headroom than legacy caps\", () => {\n    expect(RAIN_PARAM_LIMITS.glassIntensity.max).toBeGreaterThan(1);\n    expect(RAIN_PARAM_LIMITS.zoom.max).toBeGreaterThan(2);\n    expect(RAIN_PARAM_LIMITS.fallingIntensity.max).toBeGreaterThan(1);\n    expect(RAIN_PARAM_LIMITS.fallingSpeed.max).toBeGreaterThan(3);\n    expect(RAIN_PARAM_LIMITS.fallingStreakLength.max).toBeGreaterThan(2);\n    expect(RAIN_PARAM_LIMITS.fallingLayers.max).toBeGreaterThan(6);\n    expect(RAIN_PARAM_LIMITS.fallingRefraction.max).toBeGreaterThan(1);\n    expect(RAIN_PARAM_LIMITS.fallingWaviness.max).toBeGreaterThan(1);\n    expect(RAIN_PARAM_LIMITS.fallingThicknessVar.max).toBeGreaterThan(1);\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/playground/weather-tuning/recover-repo-overrides.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\n\nimport { recoverRepoCheckpointOverrides } from \"@/app/sandbox/weather-tuning/lib/recover-repo-overrides\";\n\ndescribe(\"recoverRepoCheckpointOverrides\", () => {\n  it(\"returns checkpoint overrides from a successful recover response\", async () => {\n    const fetchMock: typeof fetch = (async () =>\n      ({\n        ok: true,\n        json: async () => ({\n          checkpointOverrides: {\n            clear: {\n              dawn: { glass: { depth: 12 } },\n              noon: {},\n              dusk: {},\n              midnight: {},\n            },\n          },\n        }),\n      }) as Response) as typeof fetch;\n\n    const result = await recoverRepoCheckpointOverrides(fetchMock);\n    expect(result?.clear?.dawn?.glass?.depth).toBe(12);\n  });\n\n  it(\"returns null on non-ok response\", async () => {\n    const fetchMock: typeof fetch = (async () =>\n      ({\n        ok: false,\n        json: async () => ({}),\n      }) as Response) as typeof fetch;\n\n    const result = await recoverRepoCheckpointOverrides(fetchMock);\n    expect(result).toBeNull();\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/playground/weather-tuning/resolve-params.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\n\nimport { TIME_CHECKPOINTS } from \"@/lib/weather-authoring/weather-widget/effects/tuning\";\nimport type { TimeCheckpoint } from \"@/lib/weather-authoring/weather-widget/effects/tuning\";\nimport type { FullCompositorParams } from \"@/app/sandbox/weather-compositor/presets\";\nimport { mergeWithOverrides } from \"@/app/sandbox/weather-compositor/presets\";\nimport type { CheckpointOverrides } from \"@/app/sandbox/weather-compositor/interpolation\";\nimport { resolveCompositorParamsAtTime } from \"@/app/sandbox/weather-tuning/lib/resolve-params\";\n\nfunction makeBase(timeOfDay: number): FullCompositorParams {\n  // Only a few fields are relevant to this test; the rest are just required for mergeWithOverrides.\n  return {\n    layers: {\n      celestial: true,\n      clouds: false,\n      rain: false,\n      lightning: false,\n      snow: false,\n    },\n    celestial: {\n      timeOfDay,\n      moonPhase: 0.5,\n      starDensity: 0.5,\n      celestialX: 0.5,\n      celestialY: 0.72,\n      sunSize: 0.06,\n      moonSize: 0.05,\n      sunGlowIntensity: 1.0,\n      sunGlowSize: 0.3,\n      sunRayCount: 12,\n      sunRayLength: 0.5,\n      sunRayIntensity: 0.4,\n      sunRayShimmer: 1.0,\n      sunRayShimmerSpeed: 1.0,\n      moonGlowIntensity: 1.0,\n      moonGlowSize: 0.2,\n      skyBrightness: 1.0,\n      skySaturation: 1.0,\n      skyContrast: 1.0,\n    },\n    cloud: {} as unknown as FullCompositorParams[\"cloud\"],\n    rain: {} as unknown as FullCompositorParams[\"rain\"],\n    lightning: {} as unknown as FullCompositorParams[\"lightning\"],\n    snow: {} as unknown as FullCompositorParams[\"snow\"],\n    glass: {\n      enabled: true,\n      depth: 3,\n      strength: 75,\n      chromaticAberration: 6,\n      blur: 1.5,\n      brightness: 0.8,\n      saturation: 1.3,\n    },\n    post: { enabled: true } as unknown as FullCompositorParams[\"post\"],\n  };\n}\n\nfunction makeEmptyCheckpointOverrides(): CheckpointOverrides {\n  return {\n    dawn: {},\n    noon: {},\n    dusk: {},\n    midnight: {},\n  };\n}\n\ndescribe(\"resolveCompositorParamsAtTime\", () => {\n  test(\"adopting an applied delta as repo defaults preserves the resolved output\", () => {\n    const timeOfDay = TIME_CHECKPOINTS.midnight;\n\n    const repo1: CheckpointOverrides = {\n      ...makeEmptyCheckpointOverrides(),\n      midnight: { celestial: { skyContrast: 1.0 } },\n    };\n\n    const userDelta: CheckpointOverrides = {\n      ...makeEmptyCheckpointOverrides(),\n      midnight: { celestial: { skyContrast: 1.04 } },\n    };\n\n    const repo2: CheckpointOverrides = {\n      ...makeEmptyCheckpointOverrides(),\n      midnight: { celestial: { skyContrast: 1.04 } },\n    };\n\n    const getRawBaseForCheckpoint = (cp: TimeCheckpoint) =>\n      makeBase(TIME_CHECKPOINTS[cp]);\n\n    const getRepoBaseForCheckpoint1 = (cp: TimeCheckpoint) => {\n      const raw = getRawBaseForCheckpoint(cp);\n      const o = repo1[cp];\n      return mergeWithOverrides(raw, o);\n    };\n\n    const beforeApply = resolveCompositorParamsAtTime({\n      timeOfDay,\n      rawBaseAtTime: makeBase(timeOfDay),\n      getRawBaseForCheckpoint,\n      repoCheckpointOverrides: repo1,\n      getRepoBaseForCheckpoint: getRepoBaseForCheckpoint1,\n      userCheckpointOverrides: userDelta,\n    });\n\n    const getRepoBaseForCheckpoint2 = (cp: TimeCheckpoint) => {\n      const raw = getRawBaseForCheckpoint(cp);\n      const o = repo2[cp];\n      return mergeWithOverrides(raw, o);\n    };\n\n    const afterApply = resolveCompositorParamsAtTime({\n      timeOfDay,\n      rawBaseAtTime: makeBase(timeOfDay),\n      getRawBaseForCheckpoint,\n      repoCheckpointOverrides: repo2,\n      getRepoBaseForCheckpoint: getRepoBaseForCheckpoint2,\n      userCheckpointOverrides: undefined,\n    });\n\n    expect(beforeApply.celestial.skyContrast).toBe(1.04);\n    expect(afterApply.celestial.skyContrast).toBe(1.04);\n  });\n\n  test(\"repo defaults interpolate across time when only one endpoint is overridden\", () => {\n    // Halfway between midnight (0.0) and dawn (0.25).\n    const timeOfDay = 0.125;\n\n    const repo: CheckpointOverrides = {\n      ...makeEmptyCheckpointOverrides(),\n      midnight: { celestial: { skyContrast: 1.04 } },\n    };\n\n    const getRawBaseForCheckpoint = (cp: TimeCheckpoint) =>\n      makeBase(TIME_CHECKPOINTS[cp]);\n    const getRepoBaseForCheckpoint = (cp: TimeCheckpoint) =>\n      mergeWithOverrides(getRawBaseForCheckpoint(cp), repo[cp]);\n\n    const resolved = resolveCompositorParamsAtTime({\n      timeOfDay,\n      rawBaseAtTime: makeBase(timeOfDay),\n      getRawBaseForCheckpoint,\n      repoCheckpointOverrides: repo,\n      getRepoBaseForCheckpoint,\n      userCheckpointOverrides: undefined,\n    });\n\n    // Should land between base (1.0) at dawn and override (1.04) at midnight.\n    expect(resolved.celestial.skyContrast).toBeGreaterThan(1.0);\n    expect(resolved.celestial.skyContrast).toBeLessThan(1.04);\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/playground/weather-tuning/snow-fall-speed-range.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\n\nimport { PARAMETER_GROUPS } from \"@/app/sandbox/weather-tuning/components/parameter-definitions\";\nimport { SNOW_FALL_SPEED_MAX } from \"@/app/sandbox/weather-tuning/lib/constants\";\n\ndescribe(\"snow fall-speed tuning range\", () => {\n  it(\"exposes extended max fall speed for sleet/hail tuning\", () => {\n    const snowGroup = PARAMETER_GROUPS.find((group) => group.layer === \"snow\");\n    expect(snowGroup).toBeDefined();\n\n    const fallSpeed = snowGroup?.params.find(\n      (param) => param.key === \"fallSpeed\",\n    );\n    expect(fallSpeed).toBeDefined();\n    expect(fallSpeed?.max).toBe(SNOW_FALL_SPEED_MAX);\n    expect(SNOW_FALL_SPEED_MAX).toBeGreaterThan(3);\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/playground/weather-tuning/studio-timestamp.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\n\nimport { createStudioTimestamp } from \"@/app/sandbox/weather-tuning/lib/studio-timestamp\";\n\ndescribe(\"studio timestamp generation\", () => {\n  it(\"is date-invariant for a given time-of-day\", () => {\n    const january = createStudioTimestamp(\n      0.5,\n      new Date(\"2025-01-15T00:00:00.000Z\"),\n    );\n    const july = createStudioTimestamp(\n      0.5,\n      new Date(\"2025-07-15T00:00:00.000Z\"),\n    );\n\n    expect(january).toBe(july);\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/playground/weather-tuning/tool-ui-export.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\n\nimport {\n  buildCanonicalToolUiPresetsForEditedConditions,\n  mergeTunedPresets,\n  replaceEditedConditions,\n  toToolUiDelta,\n} from \"@/app/sandbox/weather-tuning/lib/tool-ui-export\";\nimport { mapToolUiPresetsToCompositor } from \"@/app/sandbox/weather-tuning/lib/tool-ui-import\";\nimport {\n  getRawBaseParamsForCondition,\n  type CheckpointOverrides,\n} from \"@/app/sandbox/weather-compositor/presets\";\nimport { createStudioTimestamp } from \"@/app/sandbox/weather-tuning/lib/studio-timestamp\";\n\ndescribe(\"weather-tuning tool-ui export\", () => {\n  test(\"mergeTunedPresets preserves untouched conditions\", () => {\n    const base = {\n      drizzle: {\n        dawn: { rain: { glassIntensity: 0.2 } },\n        noon: {},\n        dusk: {},\n        midnight: {},\n      },\n      clear: {\n        dawn: { celestial: { starDensity: 0.1, moonPhase: 0.25 } },\n        noon: {},\n        dusk: {},\n        midnight: {},\n      },\n    } as const;\n\n    const delta = {\n      clear: {\n        dawn: { celestial: { starDensity: 0.5 } },\n        noon: {},\n        dusk: {},\n        midnight: {},\n      },\n    } as const;\n\n    const merged = mergeTunedPresets(base, delta);\n\n    // Unrelated condition remains.\n    expect(merged.drizzle?.dawn.rain?.glassIntensity).toBe(0.2);\n\n    // Deep merge within a group: updated field overrides, untouched field remains.\n    expect(merged.clear?.dawn.celestial?.starDensity).toBe(0.5);\n    expect(merged.clear?.dawn.celestial?.moonPhase).toBe(0.25);\n  });\n\n  test(\"toToolUiDelta maps rain.zoom -> rain.glassZoom\", () => {\n    const delta = toToolUiDelta({\n      clear: {\n        dawn: {},\n        noon: {\n          rain: {\n            zoom: 0.75,\n          },\n        },\n        dusk: {},\n        midnight: {},\n      },\n    });\n\n    expect(delta.clear?.noon.rain?.glassZoom).toBe(0.75);\n  });\n\n  test(\"canonical export replaces edited condition output and removes stale keys\", () => {\n    const base = {\n      clear: {\n        dawn: {},\n        noon: {\n          celestial: {\n            moonPhase: 0.1234,\n            starDensity: 0.4,\n          },\n        },\n        dusk: {},\n        midnight: {},\n      },\n      rain: {\n        dawn: { rain: { glassIntensity: 0.8 } },\n        noon: {},\n        dusk: {},\n        midnight: {},\n      },\n    } as const;\n\n    const repoCheckpointOverrides = mapToolUiPresetsToCompositor(base);\n\n    const rawNoon = getRawBaseParamsForCondition(\n      \"clear\",\n      createStudioTimestamp(0.5, new Date(\"2026-01-01T00:00:00Z\")),\n    );\n\n    const editedConditionOverrides: Partial<\n      Record<\"clear\", CheckpointOverrides>\n    > = {\n      clear: {\n        dawn: {},\n        noon: {\n          celestial: {\n            // Revert this parameter to raw-base value, which should delete\n            // the previously exported moonPhase override.\n            moonPhase: rawNoon.celestial.moonPhase,\n            // Keep this as an intentional override.\n            starDensity: 0.7,\n          },\n        },\n        dusk: {},\n        midnight: {},\n      },\n    };\n\n    const canonical = buildCanonicalToolUiPresetsForEditedConditions(\n      editedConditionOverrides,\n      repoCheckpointOverrides,\n    );\n    const replaced = replaceEditedConditions(base, canonical);\n\n    expect(replaced.clear?.noon.celestial?.moonPhase).toBeUndefined();\n    expect(replaced.clear?.noon.celestial?.starDensity).toBe(0.7);\n    expect(replaced.rain?.dawn.rain?.glassIntensity).toBe(0.8);\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/playground/weather-tuning/tool-ui-import.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\n\nimport { mapToolUiOverridesToCompositor } from \"@/app/sandbox/weather-tuning/lib/tool-ui-import\";\n\ndescribe(\"mapToolUiOverridesToCompositor\", () => {\n  it(\"maps rain.glassZoom -> rain.zoom\", () => {\n    const result = mapToolUiOverridesToCompositor({\n      rain: { glassZoom: 1.25 },\n    });\n    expect(result.rain).toEqual({ zoom: 1.25 });\n  });\n\n  it(\"maps interactions -> compositor groups\", () => {\n    const result = mapToolUiOverridesToCompositor({\n      interactions: {\n        rainRefractionStrength: 0.42,\n        lightningSceneIllumination: 0.77,\n      },\n    });\n    expect(result.rain).toEqual({ fallingRefraction: 0.42 });\n    expect(result.lightning).toEqual({ sceneIllumination: 0.77 });\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/playground/weather-tuning/workflow-state.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\n\nimport {\n  loadWorkflowState,\n  saveWorkflowState,\n  type WorkflowState,\n} from \"@/app/sandbox/weather-tuning/lib/workflow-state\";\n\nconst STORAGE_KEY = \"weather-tuning-studio-session\";\n\nclass MemoryStorage {\n  private store = new Map<string, string>();\n\n  getItem(key: string): string | null {\n    return this.store.has(key) ? this.store.get(key)! : null;\n  }\n\n  setItem(key: string, value: string): void {\n    this.store.set(key, value);\n  }\n\n  removeItem(key: string): void {\n    this.store.delete(key);\n  }\n\n  clear(): void {\n    this.store.clear();\n  }\n}\n\ndescribe(\"weather-tuning workflow state persistence\", () => {\n  it(\"persists repo checkpoint overrides across reload\", () => {\n    const storage = new MemoryStorage();\n    Object.defineProperty(globalThis, \"window\", {\n      value: { localStorage: storage },\n      configurable: true,\n    });\n\n    const state: WorkflowState = {\n      checkpoints: {\n        clear: {\n          dawn: \"reviewed\",\n          noon: \"pending\",\n          dusk: \"pending\",\n          midnight: \"pending\",\n        },\n      },\n      signedOff: [\"clear\"],\n      repoCheckpointOverrides: {\n        clear: {\n          dawn: { glass: { depth: 9, strength: 95 } },\n          noon: {},\n          dusk: {},\n          midnight: {},\n        },\n      },\n    };\n\n    saveWorkflowState(state);\n\n    const raw = storage.getItem(STORAGE_KEY);\n    expect(raw).not.toBeNull();\n    expect(raw).toContain(\"repoCheckpointOverrides\");\n\n    const loaded = loadWorkflowState();\n    expect(loaded?.repoCheckpointOverrides?.clear?.dawn?.glass?.depth).toBe(9);\n    expect(loaded?.repoCheckpointOverrides?.clear?.dawn?.glass?.strength).toBe(\n      95,\n    );\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/presets/approval-card.ts",
    "content": "import type { SerializableApprovalCard } from \"@/components/tool-ui/approval-card\";\nimport type { PresetWithCodeGen } from \"./types\";\n\nexport type ApprovalCardPresetName =\n  | \"deploy\"\n  | \"destructive\"\n  | \"with-metadata\"\n  | \"receipt-approved\"\n  | \"receipt-denied\";\n\nfunction generateApprovalCardCode(data: SerializableApprovalCard): string {\n  const props: string[] = [];\n\n  props.push(`  id=\"${data.id}\"`);\n  props.push(`  title=\"${data.title}\"`);\n\n  if (data.description) {\n    props.push(`  description=\"${data.description}\"`);\n  }\n\n  if (data.icon) {\n    props.push(`  icon=\"${data.icon}\"`);\n  }\n\n  if (data.metadata && data.metadata.length > 0) {\n    props.push(\n      `  metadata={${JSON.stringify(data.metadata, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n    );\n  }\n\n  if (data.variant && data.variant !== \"default\") {\n    props.push(`  variant=\"${data.variant}\"`);\n  }\n\n  if (data.confirmLabel && data.confirmLabel !== \"Approve\") {\n    props.push(`  confirmLabel=\"${data.confirmLabel}\"`);\n  }\n\n  if (data.cancelLabel && data.cancelLabel !== \"Deny\") {\n    props.push(`  cancelLabel=\"${data.cancelLabel}\"`);\n  }\n\n  if (data.choice) {\n    props.push(`  choice=\"${data.choice}\"`);\n  } else {\n    props.push(`  onConfirm={() => console.log(\"Approved\")}`);\n    props.push(`  onCancel={() => console.log(\"Denied\")}`);\n  }\n\n  return `<ApprovalCard\\n${props.join(\"\\n\")}\\n/>`;\n}\n\nexport const approvalCardPresets: Record<\n  ApprovalCardPresetName,\n  PresetWithCodeGen<SerializableApprovalCard>\n> = {\n  deploy: {\n    description: \"Simple deployment approval\",\n    data: {\n      id: \"approval-card-deploy\",\n      title: \"Deploy to Production\",\n      description: \"This will push the latest changes to all users.\",\n      icon: \"rocket\",\n      confirmLabel: \"Deploy\",\n    } satisfies SerializableApprovalCard,\n    generateExampleCode: generateApprovalCardCode,\n  },\n  destructive: {\n    description: \"Destructive action with warning styling\",\n    data: {\n      id: \"approval-card-destructive\",\n      title: \"Delete Project\",\n      description:\n        \"This action cannot be undone. All files, settings, and history will be permanently removed.\",\n      icon: \"trash-2\",\n      variant: \"destructive\",\n      confirmLabel: \"Delete Project\",\n      cancelLabel: \"Keep Project\",\n    } satisfies SerializableApprovalCard,\n    generateExampleCode: generateApprovalCardCode,\n  },\n  \"with-metadata\": {\n    description: \"Approval with context details\",\n    data: {\n      id: \"approval-card-with-metadata\",\n      title: \"Send Email Campaign\",\n      description: \"Review the details before sending to your subscribers.\",\n      icon: \"mail\",\n      metadata: [\n        { key: \"Recipients\", value: \"12,847 subscribers\" },\n        { key: \"Subject\", value: \"Your Weekly Digest\" },\n        { key: \"Scheduled\", value: \"Immediately\" },\n      ],\n      confirmLabel: \"Send Now\",\n    } satisfies SerializableApprovalCard,\n    generateExampleCode: generateApprovalCardCode,\n  },\n  \"receipt-approved\": {\n    description: \"Receipt state after approval\",\n    data: {\n      id: \"approval-card-receipt-approved\",\n      title: \"Back up database\",\n      choice: \"approved\",\n      confirmLabel: \"Approved\",\n    } satisfies SerializableApprovalCard,\n    generateExampleCode: generateApprovalCardCode,\n  },\n  \"receipt-denied\": {\n    description: \"Receipt state after denial\",\n    data: {\n      id: \"approval-card-receipt-denied\",\n      title: \"Delete all project files\",\n      choice: \"denied\",\n      cancelLabel: \"Denied\",\n    } satisfies SerializableApprovalCard,\n    generateExampleCode: generateApprovalCardCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/presets/audio.ts",
    "content": "import type {\n  SerializableAudio,\n  AudioVariant,\n} from \"@/components/tool-ui/audio\";\nimport type { SerializableAction } from \"@/components/tool-ui/shared\";\nimport type { PresetWithCodeGen } from \"./types\";\n\ninterface AudioData {\n  audio: SerializableAudio;\n  variant?: AudioVariant;\n  localActions?: SerializableAction[];\n}\n\nfunction escape(value: string): string {\n  return value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n}\n\nfunction formatObject(value: Record<string, unknown>): string {\n  return JSON.stringify(value, null, 2).replace(/\\n/g, \"\\n  \");\n}\n\nfunction generateAudioCode(data: AudioData): string {\n  const { audio, variant, localActions } = data;\n  const props: string[] = [];\n\n  props.push(`  id=\"${audio.id}\"`);\n  props.push(`  assetId=\"${audio.assetId}\"`);\n  props.push(`  src=\"${audio.src}\"`);\n\n  if (variant) {\n    props.push(`  variant=\"${variant}\"`);\n  }\n\n  if (audio.title) {\n    props.push(`  title=\"${escape(audio.title)}\"`);\n  }\n\n  if (audio.description) {\n    props.push(`  description=\"${escape(audio.description)}\"`);\n  }\n\n  if (audio.artwork) {\n    props.push(`  artwork=\"${audio.artwork}\"`);\n  }\n\n  if (audio.durationMs) {\n    props.push(`  durationMs={${audio.durationMs}}`);\n  }\n\n  if (audio.fileSizeBytes) {\n    props.push(`  fileSizeBytes={${audio.fileSizeBytes}}`);\n  }\n\n  if (audio.createdAt) {\n    props.push(`  createdAt=\"${audio.createdAt}\"`);\n  }\n\n  if (audio.source) {\n    props.push(\n      `  source={${formatObject(audio.source as Record<string, unknown>)}}`,\n    );\n  }\n\n  const audioCode = `<Audio\\n${props.join(\"\\n\")}\\n/>`;\n  if (!localActions || localActions.length === 0) {\n    return audioCode;\n  }\n\n  return `${audioCode}\n<LocalActions\n  surfaceId=\"${audio.id}\"\n  actions={${JSON.stringify(localActions, null, 2).replace(/\\n/g, \"\\n  \")}}\n  onAction={(actionId) => console.log(\"Local action:\", actionId)}\n/>`;\n}\n\nexport type AudioPresetName = \"full\" | \"compact\" | \"with-actions\";\n\nexport const audioPresets: Record<\n  AudioPresetName,\n  PresetWithCodeGen<AudioData>\n> = {\n  full: {\n    description: \"Portrait card with large artwork\",\n    data: {\n      audio: {\n        id: \"audio-preview-full\",\n        assetId: \"audio-full\",\n        src: \"https://cdn.pixabay.com/audio/2022/03/10/audio_4dedf5bf94.mp3\",\n        title: \"Morning Forest\",\n        description: \"Dawn chorus recorded in Olympic National Park\",\n        artwork:\n          \"https://images.unsplash.com/photo-1448375240586-882707db888b?w=400&auto=format&fit=crop\",\n        durationMs: 42000,\n      },\n    } satisfies AudioData,\n    generateExampleCode: generateAudioCode,\n  },\n  compact: {\n    description: \"Condensed inline player\",\n    data: {\n      variant: \"compact\",\n      audio: {\n        id: \"audio-preview-compact\",\n        assetId: \"audio-compact\",\n        src: \"https://cdn.pixabay.com/audio/2022/03/10/audio_4dedf5bf94.mp3\",\n        title: \"Morning Forest\",\n        description: \"Olympic National Park\",\n        artwork:\n          \"https://images.unsplash.com/photo-1448375240586-882707db888b?w=400&auto=format&fit=crop\",\n        durationMs: 42000,\n      },\n    } satisfies AudioData,\n    generateExampleCode: generateAudioCode,\n  },\n  \"with-actions\": {\n    description: \"Full player with external local actions\",\n    data: {\n      audio: {\n        id: \"audio-preview-actions\",\n        assetId: \"audio-actions\",\n        src: \"https://cdn.pixabay.com/audio/2022/03/10/audio_4dedf5bf94.mp3\",\n        title: \"Morning Forest\",\n        description: \"Dawn chorus recorded in Olympic National Park\",\n        artwork:\n          \"https://images.unsplash.com/photo-1448375240586-882707db888b?w=400&auto=format&fit=crop\",\n        durationMs: 42000,\n      },\n      localActions: [\n        { id: \"download\", label: \"Download\", variant: \"secondary\" },\n        { id: \"share\", label: \"Share\", variant: \"default\" },\n      ],\n    } satisfies AudioData,\n    generateExampleCode: generateAudioCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/presets/chart.ts",
    "content": "import type { SerializableChart } from \"@/components/tool-ui/chart\";\nimport type { PresetWithCodeGen } from \"./types\";\n\ntype ChartData = Omit<SerializableChart, \"id\">;\n\nexport type ChartPresetName = \"revenue\" | \"performance\" | \"minimal\";\n\nfunction generateChartCode(data: ChartData): string {\n  const props: string[] = [];\n\n  props.push(`  id=\"chart-example\"`);\n  props.push(`  type=\"${data.type}\"`);\n\n  if (data.title) {\n    props.push(`  title=\"${data.title}\"`);\n  }\n\n  if (data.description) {\n    props.push(`  description=\"${data.description}\"`);\n  }\n\n  props.push(\n    `  data={${JSON.stringify(data.data, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n  props.push(`  xKey=\"${data.xKey}\"`);\n  props.push(\n    `  series={${JSON.stringify(data.series, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n\n  if (data.colors) {\n    props.push(\n      `  colors={${JSON.stringify(data.colors, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n    );\n  }\n\n  if (data.showLegend) {\n    props.push(`  showLegend`);\n  }\n\n  if (data.showGrid) {\n    props.push(`  showGrid`);\n  }\n\n  return `<Chart\\n${props.join(\"\\n\")}\\n/>`;\n}\n\nexport const chartPresets: Record<\n  ChartPresetName,\n  PresetWithCodeGen<ChartData>\n> = {\n  revenue: {\n    description: \"Bar chart with revenue vs expenses\",\n    data: {\n      type: \"bar\",\n      title: \"Monthly Revenue\",\n      description: \"Revenue vs Expenses (2024)\",\n      data: [\n        { month: \"Jan\", revenue: 4000, expenses: 2400 },\n        { month: \"Feb\", revenue: 3000, expenses: 1398 },\n        { month: \"Mar\", revenue: 5000, expenses: 3200 },\n        { month: \"Apr\", revenue: 2780, expenses: 3908 },\n        { month: \"May\", revenue: 1890, expenses: 4800 },\n        { month: \"Jun\", revenue: 2390, expenses: 3800 },\n      ],\n      xKey: \"month\",\n      series: [\n        { key: \"revenue\", label: \"Revenue\" },\n        { key: \"expenses\", label: \"Expenses\" },\n      ],\n      colors: [\"#14B8A6\", \"#F87171\"],\n      showLegend: true,\n      showGrid: true,\n    } satisfies ChartData,\n    generateExampleCode: generateChartCode,\n  },\n  performance: {\n    description: \"Line chart with system metrics\",\n    data: {\n      type: \"line\",\n      title: \"System Performance\",\n      description: \"CPU and Memory usage over time\",\n      data: [\n        { time: \"00:00\", cpu: 45, memory: 62 },\n        { time: \"04:00\", cpu: 32, memory: 58 },\n        { time: \"08:00\", cpu: 67, memory: 71 },\n        { time: \"12:00\", cpu: 89, memory: 85 },\n        { time: \"16:00\", cpu: 76, memory: 79 },\n        { time: \"20:00\", cpu: 54, memory: 68 },\n      ],\n      xKey: \"time\",\n      series: [\n        { key: \"cpu\", label: \"CPU %\" },\n        { key: \"memory\", label: \"Memory %\" },\n      ],\n      showLegend: true,\n      showGrid: true,\n    } satisfies ChartData,\n    generateExampleCode: generateChartCode,\n  },\n  minimal: {\n    description: \"Compact bar chart without title or legend\",\n    data: {\n      type: \"bar\",\n      data: [\n        { day: \"Mon\", steps: 8420 },\n        { day: \"Tue\", steps: 6200 },\n        { day: \"Wed\", steps: 9150 },\n        { day: \"Thu\", steps: 4800 },\n        { day: \"Fri\", steps: 7300 },\n      ],\n      xKey: \"day\",\n      series: [{ key: \"steps\", label: \"Steps\" }],\n    } satisfies ChartData,\n    generateExampleCode: generateChartCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/presets/citation.ts",
    "content": "import type {\n  SerializableCitation,\n  CitationVariant,\n} from \"@/components/tool-ui/citation\";\nimport type { SerializableAction } from \"@/components/tool-ui/shared\";\nimport type { PresetWithCodeGen } from \"./types\";\n\nfunction favicon(domain: string, size = 32): string {\n  return `https://www.google.com/s2/favicons?domain=${domain}&sz=${size}`;\n}\n\ninterface CitationData {\n  citations: SerializableCitation[];\n  variant?: CitationVariant;\n  maxVisible?: number;\n  localActions?: SerializableAction[];\n}\n\nfunction escape(value: string): string {\n  return value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n}\n\nfunction generateCitationCode(data: CitationData): string {\n  const { citations, variant, maxVisible, localActions } = data;\n\n  // Single citation without list wrapper\n  if (citations.length === 1 && !maxVisible) {\n    const citation = citations[0];\n    const props: string[] = [];\n\n    props.push(`  id=\"${citation.id}\"`);\n\n    if (variant && variant !== \"default\") {\n      props.push(`  variant=\"${variant}\"`);\n    }\n\n    props.push(`  href=\"${citation.href}\"`);\n    props.push(`  title=\"${escape(citation.title)}\"`);\n\n    if (citation.snippet) {\n      props.push(`  snippet=\"${escape(citation.snippet)}\"`);\n    }\n\n    if (citation.domain) {\n      props.push(`  domain=\"${citation.domain}\"`);\n    }\n\n    if (citation.favicon) {\n      props.push(`  favicon=\"${citation.favicon}\"`);\n    }\n\n    if (citation.author) {\n      props.push(`  author=\"${escape(citation.author)}\"`);\n    }\n\n    if (citation.publishedAt) {\n      props.push(`  publishedAt=\"${citation.publishedAt}\"`);\n    }\n\n    if (citation.type) {\n      props.push(`  type=\"${citation.type}\"`);\n    }\n\n    const citationCode = `<Citation\\n${props.join(\"\\n\")}\\n/>`;\n    if (!localActions || localActions.length === 0) {\n      return citationCode;\n    }\n\n    return `${citationCode}\n<LocalActions\n  surfaceId=\"${citation.id}\"\n  actions={${JSON.stringify(localActions, null, 2).replace(/\\n/g, \"\\n  \")}}\n  onAction={(actionId) => console.log(\"Local action:\", actionId)}\n/>`;\n  }\n\n  // Multiple citations with CitationList\n  const listProps: string[] = [];\n  listProps.push(`  id=\"citation-list\"`);\n  listProps.push(`  citations={citations}`);\n\n  if (variant && variant !== \"default\") {\n    listProps.push(`  variant=\"${variant}\"`);\n  }\n\n  if (maxVisible) {\n    listProps.push(`  maxVisible={${maxVisible}}`);\n  }\n\n  return `<CitationList\\n${listProps.join(\"\\n\")}\\n/>`;\n}\n\nexport type CitationPresetName =\n  | \"stacked\"\n  | \"inline\"\n  | \"card\"\n  | \"with-metadata\"\n  | \"with-actions\";\n\nexport const citationPresets: Record<\n  CitationPresetName,\n  PresetWithCodeGen<CitationData>\n> = {\n  stacked: {\n    description: \"Compact stacked favicons\",\n    data: {\n      variant: \"stacked\",\n      citations: [\n        {\n          id: \"citation-stacked-1\",\n          href: \"https://react.dev/reference/react/useState\",\n          title: \"useState – React\",\n          snippet:\n            \"useState is a React Hook that lets you add a state variable.\",\n          domain: \"react.dev\",\n          favicon: favicon(\"react.dev\"),\n          type: \"document\",\n        },\n        {\n          id: \"citation-stacked-2\",\n          href: \"https://developer.mozilla.org/en-US/docs/Web/JavaScript\",\n          title: \"JavaScript - MDN Web Docs\",\n          snippet:\n            \"JavaScript is a lightweight interpreted programming language.\",\n          domain: \"developer.mozilla.org\",\n          favicon: favicon(\"developer.mozilla.org\"),\n          type: \"document\",\n        },\n        {\n          id: \"citation-stacked-3\",\n          href: \"https://www.typescriptlang.org/docs/\",\n          title: \"TypeScript Documentation\",\n          snippet: \"TypeScript is a strongly typed programming language.\",\n          domain: \"typescriptlang.org\",\n          favicon: favicon(\"typescriptlang.org\"),\n          type: \"document\",\n        },\n        {\n          id: \"citation-stacked-4\",\n          href: \"https://nodejs.org/docs/latest/api/\",\n          title: \"Node.js Documentation\",\n          snippet:\n            \"Node.js is a JavaScript runtime built on Chrome's V8 engine.\",\n          domain: \"nodejs.org\",\n          favicon: favicon(\"nodejs.org\"),\n          type: \"api\",\n        },\n        {\n          id: \"citation-stacked-5\",\n          href: \"https://nextjs.org/docs\",\n          title: \"Next.js Documentation\",\n          snippet: \"Next.js is a React framework for production.\",\n          domain: \"nextjs.org\",\n          favicon: favicon(\"nextjs.org\"),\n          type: \"document\",\n        },\n        {\n          id: \"citation-stacked-6\",\n          href: \"https://tailwindcss.com/docs\",\n          title: \"Tailwind CSS Documentation\",\n          snippet: \"A utility-first CSS framework for rapid UI development.\",\n          domain: \"tailwindcss.com\",\n          favicon: favicon(\"tailwindcss.com\"),\n          type: \"document\",\n        },\n      ],\n    } satisfies CitationData,\n    generateExampleCode: generateCitationCode,\n  },\n  inline: {\n    description: \"Inline chips with overflow\",\n    data: {\n      variant: \"inline\",\n      maxVisible: 4,\n      citations: [\n        {\n          id: \"citation-inline-1\",\n          href: \"https://react.dev/reference/react/useState\",\n          title: \"useState – React\",\n          snippet:\n            \"useState is a React Hook that lets you add a state variable.\",\n          domain: \"react.dev\",\n          favicon: favicon(\"react.dev\"),\n          type: \"document\",\n        },\n        {\n          id: \"citation-inline-2\",\n          href: \"https://developer.mozilla.org/en-US/docs/Web/JavaScript\",\n          title: \"JavaScript - MDN Web Docs\",\n          snippet:\n            \"JavaScript is a lightweight interpreted programming language.\",\n          domain: \"developer.mozilla.org\",\n          favicon: favicon(\"developer.mozilla.org\"),\n          type: \"document\",\n        },\n        {\n          id: \"citation-inline-3\",\n          href: \"https://www.typescriptlang.org/docs/\",\n          title: \"TypeScript Documentation\",\n          snippet: \"TypeScript is a strongly typed programming language.\",\n          domain: \"typescriptlang.org\",\n          favicon: favicon(\"typescriptlang.org\"),\n          type: \"document\",\n        },\n        {\n          id: \"citation-inline-4\",\n          href: \"https://nodejs.org/docs/latest/api/\",\n          title: \"Node.js Documentation\",\n          snippet:\n            \"Node.js is a JavaScript runtime built on Chrome's V8 engine.\",\n          domain: \"nodejs.org\",\n          favicon: favicon(\"nodejs.org\"),\n          type: \"api\",\n        },\n        {\n          id: \"citation-inline-5\",\n          href: \"https://nextjs.org/docs\",\n          title: \"Next.js Documentation\",\n          snippet: \"Next.js is a React framework for production.\",\n          domain: \"nextjs.org\",\n          favicon: favicon(\"nextjs.org\"),\n          type: \"document\",\n        },\n        {\n          id: \"citation-inline-6\",\n          href: \"https://tailwindcss.com/docs\",\n          title: \"Tailwind CSS Documentation\",\n          snippet: \"A utility-first CSS framework for rapid UI development.\",\n          domain: \"tailwindcss.com\",\n          favicon: favicon(\"tailwindcss.com\"),\n          type: \"document\",\n        },\n      ],\n    } satisfies CitationData,\n    generateExampleCode: generateCitationCode,\n  },\n  card: {\n    description: \"Full citation card\",\n    data: {\n      citations: [\n        {\n          id: \"citation-card\",\n          href: \"https://react.dev/reference/react/useState\",\n          title: \"useState – React\",\n          snippet:\n            \"useState is a React Hook that lets you add a state variable to your component. Call useState at the top level of your component to declare a state variable.\",\n          domain: \"react.dev\",\n          favicon: favicon(\"react.dev\"),\n          type: \"document\",\n        },\n      ],\n    } satisfies CitationData,\n    generateExampleCode: generateCitationCode,\n  },\n  \"with-metadata\": {\n    description: \"Card with author and date\",\n    data: {\n      citations: [\n        {\n          id: \"citation-metadata\",\n          href: \"https://arxiv.org/abs/2303.08774\",\n          title: \"GPT-4 Technical Report\",\n          snippet:\n            \"We report the development of GPT-4, a large-scale, multimodal model which can accept image and text inputs and produce text outputs.\",\n          domain: \"arxiv.org\",\n          favicon: favicon(\"arxiv.org\"),\n          author: \"OpenAI\",\n          publishedAt: \"2023-03-15T00:00:00Z\",\n          type: \"article\",\n        },\n      ],\n    } satisfies CitationData,\n    generateExampleCode: generateCitationCode,\n  },\n  \"with-actions\": {\n    description: \"Card with external local actions\",\n    data: {\n      citations: [\n        {\n          id: \"citation-actions\",\n          href: \"https://github.com/vercel/next.js\",\n          title: \"vercel/next.js: The React Framework\",\n          snippet:\n            \"Next.js enables you to create full-stack web applications by extending the latest React features, and integrating powerful Rust-based JavaScript tooling.\",\n          domain: \"github.com\",\n          favicon: favicon(\"github.com\"),\n          type: \"code\",\n        },\n      ],\n      localActions: [\n        { id: \"view\", label: \"View source\", variant: \"default\" },\n        { id: \"copy\", label: \"Copy link\", variant: \"secondary\" },\n      ],\n    } satisfies CitationData,\n    generateExampleCode: generateCitationCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/presets/code-block.ts",
    "content": "import type { SerializableCodeBlock } from \"@/components/tool-ui/code-block\";\nimport type { SerializableAction } from \"@/components/tool-ui/shared\";\nimport type { PresetWithCodeGen } from \"./types\";\n\nexport type CodeBlockPresetName =\n  | \"typescript\"\n  | \"python\"\n  | \"json\"\n  | \"bash\"\n  | \"highlighted\"\n  | \"collapsible\"\n  | \"with-actions\";\n\nfunction escape(value: string): string {\n  return value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"').replace(/`/g, \"\\\\`\");\n}\n\ninterface CodeBlockPresetData extends SerializableCodeBlock {\n  localActions?: SerializableAction[];\n}\n\nfunction generateCodeBlockCode(data: CodeBlockPresetData): string {\n  const props: string[] = [];\n\n  props.push(`  id=\"${data.id}\"`);\n  props.push(`  code={\\`${escape(data.code)}\\`}`);\n  props.push(`  language=\"${data.language}\"`);\n  props.push(`  lineNumbers=\"${data.lineNumbers}\"`);\n\n  if (data.filename) {\n    props.push(`  filename=\"${data.filename}\"`);\n  }\n\n  if (data.highlightLines && data.highlightLines.length > 0) {\n    props.push(`  highlightLines={[${data.highlightLines.join(\", \")}]}`);\n  }\n\n  if (data.maxCollapsedLines) {\n    props.push(`  maxCollapsedLines={${data.maxCollapsedLines}}`);\n  }\n\n  const codeBlock = `<CodeBlock.Root\\n${props.join(\"\\n\")}\\n>\\n  <CodeBlock.Header />\\n  <CodeBlock.Content />\\n  <CodeBlock.CollapseToggle />\\n</CodeBlock.Root>`;\n  if (!data.localActions || data.localActions.length === 0) {\n    return codeBlock;\n  }\n\n  return `${codeBlock}\n<LocalActions\n  surfaceId=\"${data.id}\"\n  actions={${JSON.stringify(data.localActions, null, 2).replace(/\\n/g, \"\\n  \")}}\n  onAction={(actionId) => console.log(\"Local action:\", actionId)}\n/>`;\n}\n\nexport const codeBlockPresets: Record<\n  CodeBlockPresetName,\n  PresetWithCodeGen<CodeBlockPresetData>\n> = {\n  typescript: {\n    description: \"TypeScript with filename header\",\n    data: {\n      id: \"code-block-preview-typescript\",\n      code: `import { useState } from \"react\";\n\nexport function Counter() {\n  const [count, setCount] = useState(0);\n\n  return (\n    <button onClick={() => setCount(c => c + 1)}>\n      Count: {count}\n    </button>\n  );\n}`,\n      language: \"typescript\",\n      lineNumbers: \"visible\",\n      filename: \"Counter.tsx\",\n    } satisfies CodeBlockPresetData,\n    generateExampleCode: generateCodeBlockCode,\n  },\n  python: {\n    description: \"Python function with docstring\",\n    data: {\n      id: \"code-block-preview-python\",\n      code: `def fibonacci(n: int) -> list[int]:\n    \"\"\"Generate Fibonacci sequence up to n terms.\"\"\"\n    if n <= 0:\n        return []\n    elif n == 1:\n        return [0]\n\n    sequence = [0, 1]\n    while len(sequence) < n:\n        sequence.append(sequence[-1] + sequence[-2])\n\n    return sequence\n\n# Example usage\nprint(fibonacci(10))`,\n      language: \"python\",\n      lineNumbers: \"visible\",\n      filename: \"fibonacci.py\",\n    } satisfies CodeBlockPresetData,\n    generateExampleCode: generateCodeBlockCode,\n  },\n  json: {\n    description: \"JSON configuration file\",\n    data: {\n      id: \"code-block-preview-json\",\n      code: `{\n  \"name\": \"tool-ui\",\n  \"version\": \"1.0.0\",\n  \"dependencies\": {\n    \"react\": \"^19.0.0\",\n    \"zod\": \"^4.0.0\",\n    \"shiki\": \"^3.0.0\"\n  }\n}`,\n      language: \"json\",\n      lineNumbers: \"visible\",\n      filename: \"package.json\",\n    } satisfies CodeBlockPresetData,\n    generateExampleCode: generateCodeBlockCode,\n  },\n  bash: {\n    description: \"Bash script with comments\",\n    data: {\n      id: \"code-block-preview-bash\",\n      code: `#!/bin/bash\n# Deploy script\n\necho \"Building application...\"\npnpm run build\n\necho \"Running tests...\"\npnpm test\n\necho \"Deploying to production...\"\nrsync -avz ./dist/ user@server:/var/www/app/\n\necho \"Done!\"`,\n      language: \"bash\",\n      lineNumbers: \"visible\",\n      filename: \"deploy.sh\",\n    } satisfies CodeBlockPresetData,\n    generateExampleCode: generateCodeBlockCode,\n  },\n  highlighted: {\n    description: \"Code with highlighted lines (bug indicator)\",\n    data: {\n      id: \"code-block-preview-highlighted\",\n      code: `function processData(items: string[]) {\n  const results = [];\n\n  for (const item of items) {\n    // BUG: This should handle null values\n    results.push(item.toUpperCase());\n  }\n\n  return results;\n}`,\n      language: \"typescript\",\n      lineNumbers: \"visible\",\n      filename: \"processor.ts\",\n      highlightLines: [5, 6],\n    } satisfies CodeBlockPresetData,\n    generateExampleCode: generateCodeBlockCode,\n  },\n  collapsible: {\n    description: \"Long code with collapse/expand\",\n    data: {\n      id: \"code-block-preview-collapsible\",\n      code: `import { z } from \"zod\";\n\nexport const UserSchema = z.object({\n  id: z.string().uuid(),\n  email: z.string().email(),\n  name: z.string().min(1).max(100),\n  role: z.enum([\"admin\", \"member\", \"guest\"]),\n  createdAt: z.coerce.date(),\n  updatedAt: z.coerce.date(),\n  profile: z.object({\n    avatar: z.string().url().optional(),\n    bio: z.string().max(500).optional(),\n    location: z.string().optional(),\n    website: z.string().url().optional(),\n  }).optional(),\n  preferences: z.object({\n    theme: z.enum([\"light\", \"dark\", \"system\"]).default(\"system\"),\n    notifications: z.object({\n      email: z.boolean().default(true),\n      push: z.boolean().default(false),\n      marketing: z.boolean().default(false),\n    }),\n    locale: z.string().default(\"en-US\"),\n    timezone: z.string().default(\"UTC\"),\n  }),\n});\n\nexport type User = z.infer<typeof UserSchema>;\n\nexport const CreateUserSchema = UserSchema.omit({\n  id: true,\n  createdAt: true,\n  updatedAt: true,\n});\n\nexport type CreateUser = z.infer<typeof CreateUserSchema>;`,\n      language: \"typescript\",\n      lineNumbers: \"visible\",\n      filename: \"user-schema.ts\",\n      maxCollapsedLines: 10,\n    } satisfies CodeBlockPresetData,\n    generateExampleCode: generateCodeBlockCode,\n  },\n  \"with-actions\": {\n    description: \"Code with external local actions\",\n    data: {\n      id: \"code-block-preview-with-actions\",\n      code: `# Install dependencies\npnpm install\n\n# Run development server\npnpm dev`,\n      language: \"bash\",\n      lineNumbers: \"visible\",\n      filename: \"setup.sh\",\n      localActions: [\n        { id: \"copy\", label: \"Copy to clipboard\", variant: \"outline\" },\n        { id: \"run\", label: \"Run in terminal\", variant: \"default\" },\n      ],\n    } satisfies CodeBlockPresetData,\n    generateExampleCode: generateCodeBlockCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/presets/code-diff.ts",
    "content": "import type { SerializableCodeDiff } from \"@/components/tool-ui/code-diff\";\nimport type { PresetWithCodeGen } from \"./types\";\n\nexport type CodeDiffPresetName =\n  | \"refactor\"\n  | \"bug-fix\"\n  | \"patch\"\n  | \"split\"\n  | \"collapsed\"\n  | \"minimal\";\n\nfunction escape(value: string): string {\n  return value\n    .replace(/\\\\/g, \"\\\\\\\\\")\n    .replace(/\"/g, '\\\\\"')\n    .replace(/`/g, \"\\\\`\")\n    .replace(/\\$\\{/g, \"\\\\${\");\n}\n\nfunction generateCodeDiffCode(data: SerializableCodeDiff): string {\n  const props: string[] = [];\n\n  props.push(`  id=\"${data.id}\"`);\n\n  if (data.language && data.language !== \"text\") {\n    props.push(`  language=\"${data.language}\"`);\n  }\n\n  if (data.filename) {\n    props.push(`  filename=\"${data.filename}\"`);\n  }\n\n  if (data.maxCollapsedLines) {\n    props.push(`  maxCollapsedLines={${data.maxCollapsedLines}}`);\n  }\n\n  if (data.lineNumbers && data.lineNumbers !== \"visible\") {\n    props.push(`  lineNumbers=\"${data.lineNumbers}\"`);\n  }\n\n  if (data.diffStyle && data.diffStyle !== \"unified\") {\n    props.push(`  diffStyle=\"${data.diffStyle}\"`);\n  }\n\n  if (data.patch) {\n    props.push(`  patch={\\`${escape(data.patch)}\\`}`);\n  } else {\n    if (data.oldCode) {\n      props.push(`  oldCode={\\`${escape(data.oldCode)}\\`}`);\n    }\n    if (data.newCode) {\n      props.push(`  newCode={\\`${escape(data.newCode)}\\`}`);\n    }\n  }\n\n  return `<CodeDiff\\n${props.join(\"\\n\")}\\n/>`;\n}\n\nexport const codeDiffPresets: Record<\n  CodeDiffPresetName,\n  PresetWithCodeGen<SerializableCodeDiff>\n> = {\n  refactor: {\n    description: \"Safer error handling in fetch helper\",\n    data: {\n      id: \"code-diff-preview-refactor\",\n      language: \"typescript\",\n      filename: \"lib/auth.ts\",\n      lineNumbers: \"visible\",\n      diffStyle: \"unified\",\n      oldCode: `export async function fetchUser(id: string) {\n  const res = await db.users.findUnique({ where: { id } });\n  if (!res) throw new Error(\"User not found\");\n  return res;\n}\n`,\n      newCode: `export async function fetchUser(id: string) {\n  const res = await db.users.findUnique({ where: { id } });\n  if (!res) return null;\n  return res;\n}\n`,\n    } satisfies SerializableCodeDiff,\n    generateExampleCode: generateCodeDiffCode,\n  },\n  \"bug-fix\": {\n    description: \"Off-by-one fix in pagination\",\n    data: {\n      id: \"code-diff-preview-bug-fix\",\n      language: \"typescript\",\n      filename: \"hooks/use-pagination.ts\",\n      lineNumbers: \"visible\",\n      diffStyle: \"unified\",\n      oldCode: `const totalPages = Math.floor(items.length / pageSize);\nconst end = page * pageSize - 1;`,\n      newCode: `const totalPages = Math.ceil(items.length / pageSize);\nconst end = page * pageSize;`,\n    } satisfies SerializableCodeDiff,\n    generateExampleCode: generateCodeDiffCode,\n  },\n  collapsed: {\n    description: \"Large refactor with collapsible diff\",\n    data: {\n      id: \"code-diff-preview-collapsed\",\n      language: \"typescript\",\n      filename: \"lib/permissions.ts\",\n      lineNumbers: \"visible\",\n      diffStyle: \"unified\",\n      maxCollapsedLines: 12,\n      oldCode: `export function resolvePermissions(\n  user: User,\n  resource: Resource,\n  context: RequestContext,\n): Permission[] {\n  const base = getBasePermissions(user.role);\n  const overrides = getResourceOverrides(resource.id);\n  const result: Permission[] = [];\n\n  for (const perm of base) {\n    if (overrides.denied.includes(perm)) continue;\n    if (perm === \"write\" && resource.locked) continue;\n    if (perm === \"admin\" && !context.elevated) continue;\n    result.push(perm);\n  }\n\n  for (const perm of overrides.granted) {\n    if (!result.includes(perm)) {\n      result.push(perm);\n    }\n  }\n\n  if (user.isSuperAdmin) {\n    return ALL_PERMISSIONS;\n  }\n\n  return result;\n}`,\n      newCode: `export function resolvePermissions(\n  user: User,\n  resource: Resource,\n  context: RequestContext,\n): Permission[] {\n  if (user.isSuperAdmin) return ALL_PERMISSIONS;\n\n  const base = getBasePermissions(user.role);\n  const overrides = getResourceOverrides(resource.id);\n\n  const filtered = base.filter((perm) => {\n    if (overrides.denied.includes(perm)) return false;\n    if (perm === \"write\" && resource.locked) return false;\n    if (perm === \"admin\" && !context.elevated) return false;\n    return true;\n  });\n\n  const granted = overrides.granted.filter(\n    (perm) => !filtered.includes(perm),\n  );\n\n  return [...filtered, ...granted];\n}`,\n    } satisfies SerializableCodeDiff,\n    generateExampleCode: generateCodeDiffCode,\n  },\n  minimal: {\n    description: \"Clean diff without line numbers\",\n    data: {\n      id: \"code-diff-preview-minimal\",\n      language: \"python\",\n      filename: \"app/config.py\",\n      lineNumbers: \"hidden\",\n      diffStyle: \"unified\",\n      oldCode: `ALLOWED_ORIGINS = [\"https://app.example.com\"]\nRATE_LIMIT = 100\nSESSION_TTL = 3600`,\n      newCode: `ALLOWED_ORIGINS = [\"https://app.example.com\", \"https://staging.example.com\"]\nRATE_LIMIT = 250\nSESSION_TTL = 7200`,\n    } satisfies SerializableCodeDiff,\n    generateExampleCode: generateCodeDiffCode,\n  },\n  patch: {\n    description: \"Unified diff from a pull request\",\n    data: {\n      id: \"code-diff-preview-patch\",\n      language: \"typescript\",\n      filename: \"api/routes.ts\",\n      lineNumbers: \"visible\",\n      diffStyle: \"unified\",\n      patch: `--- a/api/routes.ts\n+++ b/api/routes.ts\n@@ -14,8 +14,12 @@ export function registerRoutes(app: Express) {\n   app.get(\"/api/users\", listUsers);\n   app.get(\"/api/users/:id\", getUser);\n   app.post(\"/api/users\", createUser);\n-  app.put(\"/api/users/:id\", updateUser);\n+  app.patch(\"/api/users/:id\", updateUser);\n   app.delete(\"/api/users/:id\", deleteUser);\n+\n+  // Health check\n+  app.get(\"/healthz\", (_req, res) => {\n+    res.json({ status: \"ok\", uptime: process.uptime() });\n+  });\n }`,\n    } satisfies SerializableCodeDiff,\n    generateExampleCode: generateCodeDiffCode,\n  },\n  split: {\n    description: \"Side-by-side view of a function extraction\",\n    data: {\n      id: \"code-diff-preview-split\",\n      language: \"typescript\",\n      filename: \"utils/format.ts\",\n      lineNumbers: \"visible\",\n      diffStyle: \"split\",\n      oldCode: `export function formatUser(user: User): string {\n  const name = user.firstName + \" \" + user.lastName;\n  const role = user.isAdmin ? \"Admin\" : \"Member\";\n  const since = new Date(user.createdAt)\n    .toLocaleDateString(\"en-US\", {\n      month: \"short\",\n      year: \"numeric\",\n    });\n  return \\`\\${name} (\\${role}) — joined \\${since}\\`;\n}`,\n      newCode: `function formatName(user: User): string {\n  return \\`\\${user.firstName} \\${user.lastName}\\`;\n}\n\nfunction formatRole(user: User): string {\n  return user.isAdmin ? \"Admin\" : \"Member\";\n}\n\nfunction formatJoinDate(date: string): string {\n  return new Date(date).toLocaleDateString(\"en-US\", {\n    month: \"short\",\n    year: \"numeric\",\n  });\n}\n\nexport function formatUser(user: User): string {\n  const name = formatName(user);\n  const role = formatRole(user);\n  const since = formatJoinDate(user.createdAt);\n  return \\`\\${name} (\\${role}) — joined \\${since}\\`;\n}`,\n    } satisfies SerializableCodeDiff,\n    generateExampleCode: generateCodeDiffCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/presets/data-table.ts",
    "content": "import type { Column, RowPrimitive } from \"@/components/tool-ui/data-table\";\nimport type { SerializableAction } from \"@/components/tool-ui/shared\";\nimport type { PresetWithCodeGen } from \"./types\";\n\ntype GenericRow = Record<string, RowPrimitive>;\n\nexport type SortState = { by?: string; direction?: \"asc\" | \"desc\" };\n\nexport interface DataTableData {\n  id: string;\n  columns: Column[];\n  data: GenericRow[];\n  rowIdKey?: string;\n  defaultSort?: SortState;\n  maxHeight?: string;\n  emptyMessage?: string;\n  locale?: string;\n  localActions?: SerializableAction[];\n}\n\nfunction generateDataTableCode(data: DataTableData): string {\n  const props: string[] = [];\n\n  props.push(\n    `  columns={${JSON.stringify(data.columns, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n\n  props.push(\n    `  data={${JSON.stringify(data.data, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n\n  if (data.rowIdKey) {\n    props.push(`  rowIdKey=\"${data.rowIdKey}\"`);\n  }\n\n  if (data.defaultSort) {\n    props.push(\n      `  defaultSort={${JSON.stringify(data.defaultSort, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n    );\n  }\n\n  if (data.emptyMessage && data.emptyMessage !== \"No data available\") {\n    props.push(`  emptyMessage=\"${data.emptyMessage}\"`);\n  }\n\n  if (data.maxHeight) {\n    props.push(`  maxHeight=\"${data.maxHeight}\"`);\n  }\n\n  if (data.locale) {\n    props.push(`  locale=\"${data.locale}\"`);\n  }\n\n  const tableCode = `<DataTable\\n${props.join(\"\\n\")}\\n/>`;\n  if (!data.localActions || data.localActions.length === 0) {\n    return tableCode;\n  }\n\n  return `${tableCode}\n<LocalActions\n  surfaceId=\"${data.id}\"\n  actions={${JSON.stringify(data.localActions, null, 2).replace(/\\n/g, \"\\n  \")}}\n  onAction={(actionId) => console.log(\"Local action:\", actionId)}\n/>`;\n}\n\nconst stockColumns: Column<GenericRow>[] = [\n  { key: \"symbol\", label: \"Symbol\", priority: \"primary\" },\n  { key: \"name\", label: \"Company\", priority: \"primary\" },\n  {\n    key: \"price\",\n    label: \"Price\",\n    align: \"right\",\n    priority: \"primary\",\n    format: { kind: \"currency\", currency: \"USD\", decimals: 2 },\n  },\n  {\n    key: \"change\",\n    label: \"Change\",\n    align: \"right\",\n    priority: \"secondary\",\n    format: { kind: \"delta\", decimals: 2, upIsPositive: true, showSign: true },\n  },\n  {\n    key: \"changePercent\",\n    label: \"Change %\",\n    align: \"right\",\n    priority: \"secondary\",\n    format: { kind: \"percent\", decimals: 2, showSign: true, basis: \"unit\" },\n  },\n  {\n    key: \"volume\",\n    label: \"Volume\",\n    align: \"right\",\n    priority: \"secondary\",\n    format: { kind: \"number\", compact: true },\n  },\n];\n\nconst stockData: GenericRow[] = [\n  {\n    symbol: \"IBM\",\n    name: \"International Business Machines\",\n    price: 170.42,\n    change: 1.12,\n    changePercent: 0.66,\n    volume: 18420000,\n  },\n  {\n    symbol: \"AAPL\",\n    name: \"Apple\",\n    price: 178.25,\n    change: 2.35,\n    changePercent: 1.34,\n    volume: 52430000,\n  },\n  {\n    symbol: \"MSFT\",\n    name: \"Microsoft\",\n    price: 380.0,\n    change: 1.24,\n    changePercent: 0.33,\n    volume: 31250000,\n  },\n  {\n    symbol: \"INTC\",\n    name: \"Intel Corporation\",\n    price: 39.85,\n    change: -0.42,\n    changePercent: -1.04,\n    volume: 29840000,\n  },\n  {\n    symbol: \"ORCL\",\n    name: \"Oracle Corporation\",\n    price: 110.31,\n    change: 0.78,\n    changePercent: 0.71,\n    volume: 14230000,\n  },\n];\n\nconst taskColumns: Column<GenericRow>[] = [\n  { key: \"title\", label: \"Task\", priority: \"primary\" },\n  {\n    key: \"status\",\n    label: \"Status\",\n    priority: \"primary\",\n    format: {\n      kind: \"status\",\n      statusMap: {\n        todo: { tone: \"neutral\", label: \"Todo\" },\n        in_progress: { tone: \"info\", label: \"In Progress\" },\n        done: { tone: \"success\", label: \"Done\" },\n        blocked: { tone: \"danger\", label: \"Blocked\" },\n      },\n    },\n  },\n  {\n    key: \"priority\",\n    label: \"Priority\",\n    priority: \"secondary\",\n    format: {\n      kind: \"status\",\n      statusMap: {\n        low: { tone: \"success\" },\n        medium: { tone: \"warning\" },\n        high: { tone: \"danger\" },\n        critical: { tone: \"danger\", label: \"Critical\" },\n      },\n    },\n  },\n  { key: \"assignee\", label: \"Assignee\", priority: \"secondary\" },\n  {\n    key: \"dueDate\",\n    label: \"Due Date\",\n    priority: \"secondary\",\n    format: { kind: \"date\", dateFormat: \"short\" },\n  },\n  {\n    key: \"completedDate\",\n    label: \"Completed\",\n    priority: \"tertiary\",\n    format: { kind: \"date\", dateFormat: \"long\" },\n  },\n  {\n    key: \"isUrgent\",\n    label: \"Urgent\",\n    priority: \"tertiary\",\n    format: { kind: \"boolean\" },\n  },\n];\n\nconst taskData: GenericRow[] = [\n  {\n    title: \"Transcribe punch cards to magnetic tape\",\n    status: \"in_progress\",\n    priority: \"high\",\n    assignee: \"Grace\",\n    dueDate: \"2025-11-05\",\n    completedDate: null,\n    isUrgent: true,\n  },\n  {\n    title: \"Port FORTRAN IV routines to C\",\n    status: \"todo\",\n    priority: \"critical\",\n    assignee: \"Dennis\",\n    dueDate: \"2025-11-04\",\n    completedDate: null,\n    isUrgent: true,\n  },\n  {\n    title: \"Document UNIX pipeline patterns\",\n    status: \"done\",\n    priority: \"low\",\n    assignee: \"Ken\",\n    dueDate: \"2025-10-28\",\n    completedDate: \"2025-10-27\",\n    isUrgent: false,\n  },\n  {\n    title: \"Design WIMP interface prototype\",\n    status: \"blocked\",\n    priority: \"medium\",\n    assignee: \"Adele\",\n    dueDate: \"2025-11-10\",\n    completedDate: null,\n    isUrgent: false,\n  },\n  {\n    title: \"Optimize RISC instruction scheduling\",\n    status: \"in_progress\",\n    priority: \"medium\",\n    assignee: \"Sophie\",\n    dueDate: \"2025-11-08\",\n    completedDate: null,\n    isUrgent: false,\n  },\n];\n\nconst linksTagsColumns: Column<GenericRow>[] = [\n  { key: \"name\", label: \"Resource\", priority: \"primary\" },\n  {\n    key: \"linkLabel\",\n    label: \"Link\",\n    priority: \"primary\",\n    truncate: true,\n    format: { kind: \"link\", hrefKey: \"url\", external: true },\n  },\n  {\n    key: \"tags\",\n    label: \"Tags\",\n    priority: \"secondary\",\n    format: { kind: \"array\", maxVisible: 2 },\n  },\n];\n\nconst linksTagsData: GenericRow[] = [\n  {\n    name: \"UNIX Philosophy\",\n    linkLabel: \"Read chapter\",\n    url: \"https://homepage.cs.uri.edu/~thenry/resources/unix_art/ch01s06.html\",\n    tags: [\"unix\", \"pipelines\", \"design\"],\n  },\n  {\n    name: \"ARPANET Origins\",\n    linkLabel: \"Internet Society brief\",\n    url: \"https://www.internetsociety.org/internet/history-internet/brief-history-internet/\",\n    tags: [\"arpanet\", \"history\", \"networking\"],\n  },\n  {\n    name: \"Xerox PARC Archive\",\n    linkLabel: \"Open archive\",\n    url: \"https://xeroxparc.archive.org/\",\n    tags: [\"gui\", \"research\", \"innovation\"],\n  },\n  {\n    name: \"ENIAC Collection\",\n    linkLabel: \"Computer History Museum\",\n    url: \"https://www.computerhistory.org/revolution/early-computers/5\",\n    tags: [\"eniac\", \"hardware\", \"history\"],\n  },\n];\n\nconst actionsColumns: Column<GenericRow>[] = [\n  { key: \"id\", label: \"Ticket\", priority: \"primary\" },\n  { key: \"subject\", label: \"Subject\", priority: \"primary\", truncate: true },\n  {\n    key: \"priority\",\n    label: \"Priority\",\n    priority: \"primary\",\n    format: {\n      kind: \"status\",\n      statusMap: {\n        urgent: { tone: \"danger\", label: \"Urgent\" },\n        high: { tone: \"warning\", label: \"High\" },\n        normal: { tone: \"neutral\", label: \"Normal\" },\n      },\n    },\n  },\n  {\n    key: \"status\",\n    label: \"Status\",\n    priority: \"secondary\",\n    format: {\n      kind: \"status\",\n      statusMap: {\n        open: { tone: \"info\", label: \"Open\" },\n        pending: { tone: \"warning\", label: \"Pending\" },\n        escalated: { tone: \"danger\", label: \"Escalated\" },\n      },\n    },\n  },\n  {\n    key: \"waitTime\",\n    label: \"Wait Time\",\n    abbr: \"Wait\",\n    align: \"right\",\n    priority: \"secondary\",\n    format: {\n      kind: \"delta\",\n      decimals: 0,\n      upIsPositive: false,\n      showSign: false,\n    },\n  },\n  {\n    key: \"createdAt\",\n    label: \"Created\",\n    priority: \"tertiary\",\n    format: { kind: \"date\", dateFormat: \"relative\" },\n  },\n];\n\nconst actionsData: GenericRow[] = [\n  {\n    id: \"TKT-4521\",\n    subject: \"Payment failed - urgent customer escalation\",\n    priority: \"urgent\",\n    status: \"escalated\",\n    waitTime: 36,\n    createdAt: \"2025-11-23T08:15:00.000Z\",\n  },\n  {\n    id: \"TKT-4518\",\n    subject: \"Cannot access account after password reset\",\n    priority: \"high\",\n    status: \"open\",\n    waitTime: 12,\n    createdAt: \"2025-11-24T14:30:00.000Z\",\n  },\n  {\n    id: \"TKT-4515\",\n    subject: \"Billing discrepancy on November invoice\",\n    priority: \"high\",\n    status: \"pending\",\n    waitTime: 8,\n    createdAt: \"2025-11-24T18:45:00.000Z\",\n  },\n  {\n    id: \"TKT-4512\",\n    subject: \"Feature request: export to PDF\",\n    priority: \"normal\",\n    status: \"open\",\n    waitTime: 4,\n    createdAt: \"2025-11-25T09:00:00.000Z\",\n  },\n];\n\nexport type DataTablePresetName = \"stocks\" | \"tasks\" | \"links-tags\" | \"actions\";\n\nexport const dataTablePresets: Record<\n  DataTablePresetName,\n  PresetWithCodeGen<DataTableData>\n> = {\n  stocks: {\n    description: \"Market data with currency, delta, and percent formatting\",\n    data: {\n      id: \"data-table-preview-stocks\",\n      columns: stockColumns,\n      data: stockData,\n      rowIdKey: \"symbol\",\n    },\n    generateExampleCode: generateDataTableCode,\n  },\n  tasks: {\n    description: \"Status pills, boolean badges, and multiple date formats\",\n    data: {\n      id: \"data-table-preview-tasks\",\n      columns: taskColumns,\n      data: taskData,\n      rowIdKey: \"title\",\n    },\n    generateExampleCode: generateDataTableCode,\n  },\n  \"links-tags\": {\n    description:\n      \"Compact link and tag formatting without wide resource metadata\",\n    data: {\n      id: \"data-table-preview-links-tags\",\n      columns: linksTagsColumns,\n      data: linksTagsData,\n      rowIdKey: \"name\",\n    },\n    generateExampleCode: generateDataTableCode,\n  },\n  actions: {\n    description:\n      \"Support queue with external local actions and wait indicators\",\n    data: {\n      id: \"data-table-preview-actions\",\n      columns: actionsColumns,\n      data: actionsData,\n      rowIdKey: \"id\",\n      defaultSort: { by: \"waitTime\", direction: \"desc\" },\n      localActions: [\n        {\n          id: \"close\",\n          label: \"Close tickets\",\n          confirmLabel: \"Confirm close\",\n          variant: \"destructive\",\n        },\n        { id: \"assign\", label: \"Assign to me\", variant: \"default\" },\n      ],\n    },\n    generateExampleCode: generateDataTableCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/presets/geo-map.ts",
    "content": "import type { SerializableGeoMap } from \"@/components/tool-ui/geo-map\";\nimport type { PresetWithCodeGen } from \"./types\";\n\ntype GeoMapData = Omit<SerializableGeoMap, \"id\">;\n\nexport type GeoMapPresetName = \"facility\" | \"fleet\" | \"focused\";\n\nfunction generateGeoMapCode(data: GeoMapData): string {\n  const props: string[] = [];\n\n  props.push(`  id=\"geo-map-example\"`);\n\n  if (data.title) {\n    props.push(`  title=\"${data.title}\"`);\n  }\n\n  if (data.description) {\n    props.push(`  description=\"${data.description}\"`);\n  }\n\n  props.push(\n    `  markers={${JSON.stringify(data.markers, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n\n  if (data.routes && data.routes.length > 0) {\n    props.push(\n      `  routes={${JSON.stringify(data.routes, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n    );\n  }\n\n  if (data.clustering) {\n    props.push(\n      `  clustering={${JSON.stringify(data.clustering, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n    );\n  }\n\n  if (data.viewport) {\n    props.push(\n      `  viewport={${JSON.stringify(data.viewport, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n    );\n  }\n\n  if (data.showZoomControl === false) {\n    props.push(`  showZoomControl={false}`);\n  }\n\n  if (data.theme === \"dark\") {\n    props.push(`  theme=\"${data.theme}\"`);\n  }\n\n  return `<GeoMap\\n${props.join(\"\\n\")}\\n/>`;\n}\n\nexport const geoMapPresets: Record<\n  GeoMapPresetName,\n  PresetWithCodeGen<GeoMapData>\n> = {\n  facility: {\n    description: \"Single facility location with details\",\n    data: {\n      title: \"Oakland Service Facility\",\n      description: \"Primary maintenance and dispatch site\",\n      markers: [\n        {\n          id: \"facility-oakland\",\n          lat: 37.8044,\n          lng: -122.2711,\n          label: \"Oakland Facility\",\n          description: \"Open 24/7. Heavy equipment maintenance.\",\n          tooltip: \"always\",\n          icon: {\n            type: \"image\",\n            url: \"https://images.unsplash.com/photo-1565793298595-6a879b1d9492?auto=format&fit=crop&w=80&h=80&q=60\",\n            width: 30,\n            height: 30,\n            borderRadius: 10,\n          },\n        },\n      ],\n      viewport: {\n        mode: \"fit\",\n        maxZoom: 13,\n      },\n    } satisfies GeoMapData,\n    generateExampleCode: generateGeoMapCode,\n  },\n  fleet: {\n    description: \"Clustered fleet with route overlays and custom icons\",\n    data: {\n      title: \"Fleet Positions\",\n      description: \"Last telemetry update: 30s ago\",\n      markers: [\n        {\n          id: \"truck-14\",\n          lat: 34.0522,\n          lng: -118.2437,\n          label: \"Truck 14\",\n          description: \"Delivery in progress\",\n          icon: { type: \"emoji\", value: \"🚚\", size: 24 },\n        },\n        {\n          id: \"truck-22\",\n          lat: 36.1699,\n          lng: -115.1398,\n          label: \"Truck 22\",\n          description: \"Awaiting dispatch\",\n          icon: { type: \"emoji\", value: \"🚛\", size: 24 },\n        },\n        {\n          id: \"truck-31\",\n          lat: 32.7157,\n          lng: -117.1611,\n          label: \"Truck 31\",\n          description: \"Returning to hub\",\n          icon: {\n            type: \"dot\",\n            color: \"#0EA5E9\",\n            borderColor: \"#0369A1\",\n            radius: 8,\n          },\n        },\n        {\n          id: \"truck-42\",\n          lat: 34.041,\n          lng: -118.257,\n          label: \"Truck 42\",\n          description: \"Near downtown stop\",\n          icon: { type: \"emoji\", value: \"📦\", size: 22 },\n        },\n      ],\n      routes: [\n        {\n          id: \"route-west-14\",\n          label: \"Truck 14 Route\",\n          points: [\n            { lat: 33.94, lng: -118.4 },\n            { lat: 34.012, lng: -118.32 },\n            { lat: 34.0522, lng: -118.2437 },\n          ],\n          color: \"#2563EB\",\n          weight: 4,\n          opacity: 0.8,\n        },\n        {\n          id: \"route-west-31\",\n          label: \"Truck 31 Route\",\n          points: [\n            { lat: 32.73, lng: -117.2 },\n            { lat: 32.721, lng: -117.18 },\n            { lat: 32.7157, lng: -117.1611 },\n          ],\n          color: \"#059669\",\n          dashArray: \"8 4\",\n          weight: 3,\n        },\n      ],\n      clustering: {\n        enabled: true,\n        radius: 55,\n        minPoints: 2,\n        maxZoom: 14,\n      },\n      viewport: {\n        mode: \"fit\",\n        padding: 40,\n        maxZoom: 11,\n        target: \"all\",\n      },\n    } satisfies GeoMapData,\n    generateExampleCode: generateGeoMapCode,\n  },\n  focused: {\n    description: \"Fixed viewport with route-focused styling\",\n    data: {\n      title: \"Downtown Sensors\",\n      markers: [\n        {\n          id: \"sensor-a1\",\n          lat: 40.7128,\n          lng: -74.006,\n          label: \"Sensor A1\",\n          icon: {\n            type: \"dot\",\n            color: \"#A855F7\",\n            borderColor: \"#6D28D9\",\n            radius: 7,\n          },\n        },\n        {\n          id: \"sensor-a2\",\n          lat: 40.7185,\n          lng: -74.0021,\n          label: \"Sensor A2\",\n          icon: {\n            type: \"dot\",\n            color: \"#A855F7\",\n            borderColor: \"#6D28D9\",\n            radius: 7,\n          },\n        },\n      ],\n      routes: [\n        {\n          id: \"inspection-loop\",\n          label: \"Inspection Loop\",\n          points: [\n            { lat: 40.7128, lng: -74.006 },\n            { lat: 40.7161, lng: -74.0012 },\n            { lat: 40.7185, lng: -74.0021 },\n          ],\n          color: \"#7C3AED\",\n          weight: 3,\n          opacity: 0.9,\n        },\n      ],\n      viewport: {\n        mode: \"center\",\n        center: { lat: 40.715, lng: -74.0045 },\n        zoom: 13,\n      },\n      showZoomControl: true,\n    } satisfies GeoMapData,\n    generateExampleCode: generateGeoMapCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/presets/image-gallery.ts",
    "content": "import type { SerializableImageGallery } from \"@/components/tool-ui/image-gallery\";\nimport type { PresetWithCodeGen } from \"./types\";\n\nexport type ImageGalleryPresetName =\n  | \"search-results\"\n  | \"portfolio\"\n  | \"product-images\";\n\nfunction generateImageGalleryCode(data: SerializableImageGallery): string {\n  const props: string[] = [];\n\n  props.push(\n    `  images={${JSON.stringify(data.images, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n\n  if (data.title) {\n    props.push(`  title=\"${data.title}\"`);\n  }\n\n  if (data.description) {\n    props.push(`  description=\"${data.description}\"`);\n  }\n\n  props.push(\n    `  onImageClick={(id, image) => {\\n    console.log(\"Clicked:\", id, image);\\n  }}`,\n  );\n\n  return `<ImageGallery\\n${props.join(\"\\n\")}\\n/>`;\n}\n\nexport const imageGalleryPresets: Record<\n  ImageGalleryPresetName,\n  PresetWithCodeGen<SerializableImageGallery>\n> = {\n  \"search-results\": {\n    description: \"Image search results from the web\",\n    data: {\n      id: \"image-gallery-search-results\",\n      title: \"Mountain landscapes\",\n      description: \"Here are some images matching your search\",\n      images: [\n        {\n          id: \"img-1\",\n          src: \"https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&h=600&fit=crop\",\n          alt: \"Dramatic mountain peaks at sunrise with golden light\",\n          width: 800,\n          height: 600,\n          title: \"Alpine Sunrise\",\n          caption: \"Dolomites, Italy\",\n          source: { label: \"Unsplash\", url: \"https://unsplash.com\" },\n        },\n        {\n          id: \"img-2\",\n          src: \"https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?w=800&h=1200&fit=crop\",\n          alt: \"Misty mountain valley with evergreen trees\",\n          width: 800,\n          height: 1200,\n          title: \"Misty Valley\",\n          source: { label: \"Unsplash\", url: \"https://unsplash.com\" },\n        },\n        {\n          id: \"img-3\",\n          src: \"https://images.unsplash.com/photo-1519681393784-d120267933ba?w=800&h=600&fit=crop\",\n          alt: \"Snow-covered mountain peak under starry night sky\",\n          width: 800,\n          height: 600,\n          title: \"Night Summit\",\n          caption: \"Mount Hood, Oregon\",\n        },\n        {\n          id: \"img-4\",\n          src: \"https://images.unsplash.com/photo-1454496522488-7a8e488e8606?w=800&h=600&fit=crop\",\n          alt: \"Reflection of mountains in a crystal clear lake\",\n          width: 800,\n          height: 600,\n        },\n        {\n          id: \"img-5\",\n          src: \"https://images.unsplash.com/photo-1486870591958-9b9d0d1dda99?w=800&h=1000&fit=crop\",\n          alt: \"Hiker standing on mountain ridge at sunset\",\n          width: 800,\n          height: 1000,\n          title: \"Summit View\",\n        },\n      ],\n    } satisfies SerializableImageGallery,\n    generateExampleCode: generateImageGalleryCode,\n  },\n  portfolio: {\n    description: \"Photography portfolio showcase\",\n    data: {\n      id: \"image-gallery-portfolio\",\n      title: \"Architecture Series\",\n      images: [\n        {\n          id: \"arch-1\",\n          src: \"https://images.unsplash.com/photo-1511818966892-d7d671e672a2?w=800&h=1000&fit=crop\",\n          alt: \"Modern glass skyscraper reflecting clouds\",\n          width: 800,\n          height: 1000,\n          title: \"Cloud Reflections\",\n          caption: \"New York City, 2024\",\n        },\n        {\n          id: \"arch-2\",\n          src: \"https://images.unsplash.com/photo-1487958449943-2429e8be8625?w=800&h=600&fit=crop\",\n          alt: \"Geometric patterns of a contemporary building facade\",\n          width: 800,\n          height: 600,\n          title: \"Geometry in Steel\",\n        },\n        {\n          id: \"arch-3\",\n          src: \"https://images.unsplash.com/photo-1448630360428-65456885c650?w=800&h=800&fit=crop\",\n          alt: \"Spiral staircase from above\",\n          width: 800,\n          height: 800,\n          title: \"Spiral\",\n          caption: \"Vatican Museum\",\n        },\n        {\n          id: \"arch-4\",\n          src: \"https://images.unsplash.com/photo-1486325212027-8081e485255e?w=800&h=600&fit=crop\",\n          alt: \"Bridge cables creating abstract patterns\",\n          width: 800,\n          height: 600,\n          title: \"Tension\",\n        },\n      ],\n    } satisfies SerializableImageGallery,\n    generateExampleCode: generateImageGalleryCode,\n  },\n  \"product-images\": {\n    description: \"E-commerce product gallery\",\n    data: {\n      id: \"image-gallery-product\",\n      title: \"Ceramic Mug Collection\",\n      description: \"Handcrafted stoneware, dishwasher safe\",\n      images: [\n        {\n          id: \"prod-1\",\n          src: \"https://images.unsplash.com/photo-1514228742587-6b1558fcca3d?w=600&h=600&fit=crop\",\n          alt: \"Beige ceramic mug front view\",\n          width: 600,\n          height: 600,\n        },\n        {\n          id: \"prod-2\",\n          src: \"https://images.unsplash.com/photo-1572119865084-43c285814d63?w=600&h=600&fit=crop\",\n          alt: \"Ceramic mug being held showing scale\",\n          width: 600,\n          height: 600,\n        },\n        {\n          id: \"prod-3\",\n          src: \"https://images.unsplash.com/photo-1517256064527-09c73fc73e38?w=600&h=600&fit=crop\",\n          alt: \"Mug interior detail showing glaze\",\n          width: 600,\n          height: 600,\n        },\n        {\n          id: \"prod-4\",\n          src: \"https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?w=600&h=600&fit=crop\",\n          alt: \"Mug with coffee on wooden table\",\n          width: 600,\n          height: 600,\n        },\n      ],\n    } satisfies SerializableImageGallery,\n    generateExampleCode: generateImageGalleryCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/presets/image.ts",
    "content": "import type { SerializableImage } from \"@/components/tool-ui/image\";\nimport type { SerializableAction } from \"@/components/tool-ui/shared\";\nimport type { PresetWithCodeGen } from \"./types\";\n\ninterface ImageData {\n  image: SerializableImage;\n  localActions?: SerializableAction[];\n}\n\nfunction escape(value: string): string {\n  return value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n}\n\nfunction formatObject(value: Record<string, unknown>): string {\n  return JSON.stringify(value, null, 2).replace(/\\n/g, \"\\n  \");\n}\n\nfunction generateImageCode(data: ImageData): string {\n  const { image, localActions } = data;\n  const props: string[] = [];\n\n  props.push(`  id=\"${image.id}\"`);\n  props.push(`  assetId=\"${image.assetId}\"`);\n  props.push(`  src=\"${image.src}\"`);\n  props.push(`  alt=\"${escape(image.alt)}\"`);\n\n  if (image.title) {\n    props.push(`  title=\"${escape(image.title)}\"`);\n  }\n\n  if (image.description) {\n    props.push(`  description=\"${escape(image.description)}\"`);\n  }\n\n  if (image.href) {\n    props.push(`  href=\"${image.href}\"`);\n  }\n\n  if (image.domain) {\n    props.push(`  domain=\"${image.domain}\"`);\n  }\n\n  if (image.ratio) {\n    props.push(`  ratio=\"${image.ratio}\"`);\n  }\n\n  if (image.fit) {\n    props.push(`  fit=\"${image.fit}\"`);\n  }\n\n  if (image.fileSizeBytes) {\n    props.push(`  fileSizeBytes={${image.fileSizeBytes}}`);\n  }\n\n  if (image.createdAt) {\n    props.push(`  createdAt=\"${image.createdAt}\"`);\n  }\n\n  if (image.source) {\n    props.push(\n      `  source={${formatObject(image.source as Record<string, unknown>)}}`,\n    );\n  }\n\n  const imageCode = `<Image\\n${props.join(\"\\n\")}\\n/>`;\n  if (!localActions || localActions.length === 0) {\n    return imageCode;\n  }\n\n  return `${imageCode}\n<LocalActions\n  surfaceId=\"${image.id}\"\n  actions={${JSON.stringify(localActions, null, 2).replace(/\\n/g, \"\\n  \")}}\n  onAction={(actionId) => console.log(\"Local action:\", actionId)}\n/>`;\n}\n\nexport type ImagePresetName = \"basic\" | \"with-source\" | \"with-actions\";\n\nexport const imagePresets: Record<\n  ImagePresetName,\n  PresetWithCodeGen<ImageData>\n> = {\n  basic: {\n    description: \"Simple image with title and description\",\n    data: {\n      image: {\n        id: \"image-preview-basic\",\n        assetId: \"image-basic\",\n        src: \"https://images.unsplash.com/photo-1504548840739-580b10ae7715?w=1200&auto=format&fit=crop\",\n        alt: \"Vintage mainframe with blinking lights\",\n        title: \"From mainframes to microchips\",\n        description: \"A snapshot of when rooms were computers.\",\n        ratio: \"4:3\",\n        domain: \"unsplash.com\",\n      },\n    } satisfies ImageData,\n    generateExampleCode: generateImageCode,\n  },\n  \"with-source\": {\n    description: \"Image with source attribution and metadata\",\n    data: {\n      image: {\n        id: \"image-preview-source\",\n        assetId: \"image-source\",\n        src: \"https://images.unsplash.com/photo-1504548840739-580b10ae7715?w=1200&auto=format&fit=crop\",\n        alt: \"Vintage mainframe with blinking lights\",\n        title: \"From mainframes to microchips\",\n        description:\n          \"A snapshot of when rooms were computers — not just what ran inside them.\",\n        ratio: \"4:3\",\n        domain: \"unsplash.com\",\n        createdAt: \"2025-02-10T15:30:00.000Z\",\n        fileSizeBytes: 2457600,\n        source: {\n          label: \"Computing archives\",\n          iconUrl: \"https://api.dicebear.com/7.x/shapes/svg?seed=archives\",\n          url: \"https://assistant-ui.com/tools/alignment\",\n        },\n      },\n    } satisfies ImageData,\n    generateExampleCode: generateImageCode,\n  },\n  \"with-actions\": {\n    description: \"Image with external local actions\",\n    data: {\n      image: {\n        id: \"image-preview-actions\",\n        assetId: \"image-actions\",\n        src: \"https://images.unsplash.com/photo-1518770660439-4636190af475?w=1200&auto=format&fit=crop\",\n        alt: \"Circuit board with processor chip\",\n        title: \"System architecture diagram\",\n        description: \"A detailed overview of the microprocessor layout.\",\n        ratio: \"16:9\",\n        domain: \"unsplash.com\",\n        createdAt: \"2025-03-15T10:00:00.000Z\",\n      },\n      localActions: [\n        { id: \"download\", label: \"Download\", variant: \"secondary\" },\n        { id: \"share\", label: \"Share\", variant: \"default\" },\n      ],\n    } satisfies ImageData,\n    generateExampleCode: generateImageCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/presets/instagram-post.ts",
    "content": "import type { InstagramPostData } from \"@/components/tool-ui/instagram-post\";\nimport type { ActionsProp } from \"@/components/tool-ui/shared\";\nimport type { PresetWithCodeGen } from \"./types\";\n\ninterface InstagramPostPresetData {\n  post: InstagramPostData;\n  localActions?: ActionsProp;\n}\n\nexport type InstagramPostPresetName = \"basic\" | \"carousel\" | \"footer-actions\";\n\nfunction generateInstagramPostCode(data: InstagramPostPresetData): string {\n  const props: string[] = [];\n\n  props.push(\n    `  post={${JSON.stringify(data.post, null, 2).replace(/\\n/g, \"\\n  \")}}`,\n  );\n\n  const postCode = `<InstagramPost\\n${props.join(\"\\n\")}\\n/>`;\n  if (!data.localActions) {\n    return postCode;\n  }\n\n  return `${postCode}\n<LocalActions\n  surfaceId=\"${data.post.id}\"\n  actions={${JSON.stringify(data.localActions, null, 2).replace(/\\n/g, \"\\n  \")}}\n  onAction={(actionId) => console.log(\"Local action:\", actionId)}\n/>`;\n}\n\nexport const instagramPostPresets: Record<\n  InstagramPostPresetName,\n  PresetWithCodeGen<InstagramPostPresetData>\n> = {\n  basic: {\n    description: \"Single image post\",\n    data: {\n      post: {\n        id: \"ig-post-basic\",\n        author: {\n          name: \"Alex Rivera\",\n          handle: \"alexrivera\",\n          avatarUrl:\n            \"https://images.unsplash.com/photo-1695840358933-16dd7baa6dfb?w=200&h=200&fit=crop\",\n          verified: true,\n        },\n        text: \"Golden hour in the city. Sometimes you just have to stop and appreciate the view.\",\n        media: [\n          {\n            type: \"image\",\n            url: \"https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?w=800&h=800&fit=crop\",\n            alt: \"City skyline at golden hour\",\n          },\n        ],\n        stats: { likes: 3842 },\n        createdAt: \"2025-11-05T18:45:00.000Z\",\n      },\n    } satisfies InstagramPostPresetData,\n    generateExampleCode: generateInstagramPostCode,\n  },\n  carousel: {\n    description: \"Multi-image carousel\",\n    data: {\n      post: {\n        id: \"ig-post-carousel\",\n        author: {\n          name: \"Tech Reviews\",\n          handle: \"techreviews\",\n          avatarUrl: \"https://api.dicebear.com/7.x/shapes/svg?seed=techreviews\",\n          verified: true,\n        },\n        text: \"Unboxing the new gadgets! Swipe to see all the goodies we got this week.\",\n        media: [\n          {\n            type: \"image\",\n            url: \"https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=800&h=800&fit=crop\",\n            alt: \"Headphones on yellow background\",\n          },\n          {\n            type: \"image\",\n            url: \"https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=800&h=800&fit=crop\",\n            alt: \"Smart watch\",\n          },\n          {\n            type: \"image\",\n            url: \"https://images.unsplash.com/photo-1572569511254-d8f925fe2cbb?w=800&h=800&fit=crop\",\n            alt: \"Phone accessories\",\n          },\n        ],\n        stats: { likes: 12500 },\n        createdAt: \"2025-11-24T12:00:00.000Z\",\n      },\n    } satisfies InstagramPostPresetData,\n    generateExampleCode: generateInstagramPostCode,\n  },\n  \"footer-actions\": {\n    description: \"Post with external local actions\",\n    data: {\n      post: {\n        id: \"ig-post-footer\",\n        author: {\n          name: \"Travel Blog\",\n          handle: \"wanderlust\",\n          avatarUrl: \"https://api.dicebear.com/7.x/shapes/svg?seed=travel\",\n        },\n        text: \"Paradise found! Tag someone you'd bring here.\",\n        media: [\n          {\n            type: \"image\",\n            url: \"https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=800&h=800&fit=crop\",\n            alt: \"Tropical beach with palm trees\",\n          },\n        ],\n        stats: { likes: 8921 },\n        createdAt: \"2025-11-23T09:30:00.000Z\",\n      },\n      localActions: [\n        { id: \"view-more\", label: \"View More Posts\", variant: \"secondary\" },\n        { id: \"save-location\", label: \"Save Location\", variant: \"default\" },\n      ],\n    } satisfies InstagramPostPresetData,\n    generateExampleCode: generateInstagramPostCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/presets/item-carousel.ts",
    "content": "import type { SerializableItemCarousel } from \"@/components/tool-ui/item-carousel\";\nimport type { PresetWithCodeGen } from \"./types\";\n\nexport type ItemCarouselPresetName =\n  | \"recommendations\"\n  | \"team\"\n  | \"shopping\"\n  | \"courses\"\n  | \"restaurants\"\n  | \"events\";\n\nfunction generateItemCarouselCode(data: SerializableItemCarousel): string {\n  const props: string[] = [];\n\n  props.push(`  id=\"${data.id}\"`);\n\n  const itemsFormatted = JSON.stringify(data.items, null, 4).replace(\n    /\\n/g,\n    \"\\n  \",\n  );\n  props.push(`  items={${itemsFormatted}}`);\n\n  props.push(`  onItemClick={(itemId) => console.log(\"Clicked:\", itemId)}`);\n  props.push(\n    `  onItemAction={(itemId, actionId) => console.log(\"Action:\", itemId, actionId)}`,\n  );\n\n  return `<ItemCarousel\\n${props.join(\"\\n\")}\\n/>`;\n}\n\nexport const itemCarouselPresets: Record<\n  ItemCarouselPresetName,\n  PresetWithCodeGen<SerializableItemCarousel>\n> = {\n  recommendations: {\n    description: \"TV shows to watch next\",\n    data: {\n      id: \"item-carousel-recommendations\",\n      items: [\n        {\n          id: \"rec-1\",\n          name: \"Deadwood\",\n          subtitle: \"HBO · 2004\",\n          color: \"#8b6f47\",\n          actions: [\n            { id: \"info\", label: \"Details\", variant: \"secondary\" },\n            { id: \"watch\", label: \"Watch\" },\n          ],\n        },\n        {\n          id: \"rec-2\",\n          name: \"The Wire\",\n          subtitle: \"HBO · 2002\",\n          color: \"#1e293b\",\n          actions: [\n            { id: \"info\", label: \"Details\", variant: \"secondary\" },\n            { id: \"watch\", label: \"Watch\" },\n          ],\n        },\n        {\n          id: \"rec-3\",\n          name: \"Twin Peaks\",\n          subtitle: \"ABC · 1990\",\n          color: \"#7f1d1d\",\n          actions: [\n            { id: \"info\", label: \"Details\", variant: \"secondary\" },\n            { id: \"watch\", label: \"Watch\" },\n          ],\n        },\n        {\n          id: \"rec-4\",\n          name: \"The Simpsons\",\n          subtitle: \"Fox · 1989\",\n          color: \"#fbbf24\",\n          actions: [{ id: \"add\", label: \"Add to List\" }],\n        },\n        {\n          id: \"rec-5\",\n          name: \"Mad Men\",\n          subtitle: \"AMC · 2007\",\n          color: \"#c2410c\",\n          actions: [\n            { id: \"info\", label: \"Details\", variant: \"secondary\" },\n            { id: \"watch\", label: \"Watch\" },\n          ],\n        },\n        {\n          id: \"rec-6\",\n          name: \"Peep Show\",\n          subtitle: \"Channel 4 · 2003\",\n          color: \"#1e40af\",\n          actions: [\n            { id: \"info\", label: \"Details\", variant: \"secondary\" },\n            { id: \"watch\", label: \"Watch\" },\n          ],\n        },\n        {\n          id: \"rec-7\",\n          name: \"The Sopranos\",\n          subtitle: \"HBO · 1999\",\n          color: \"#991b1b\",\n          actions: [\n            { id: \"info\", label: \"Details\", variant: \"secondary\" },\n            { id: \"watch\", label: \"Watch\" },\n          ],\n        },\n      ],\n    } satisfies SerializableItemCarousel,\n    generateExampleCode: generateItemCarouselCode,\n  },\n  team: {\n    description: \"Team members and contributors\",\n    data: {\n      id: \"item-carousel-team\",\n      items: [\n        {\n          id: \"team-1\",\n          name: \"Peter Petrash\",\n          subtitle: \"Design Engineer\",\n          color: \"#0d9488\",\n          actions: [{ id: \"profile\", label: \"View Profile\" }],\n        },\n        {\n          id: \"team-2\",\n          name: \"Simon Farshid\",\n          subtitle: \"Founder\",\n          color: \"#059669\",\n          actions: [{ id: \"profile\", label: \"View Profile\" }],\n        },\n        {\n          id: \"team-3\",\n          name: \"Bassim\",\n          subtitle: \"Software Engineer\",\n          color: \"#16a34a\",\n          actions: [{ id: \"profile\", label: \"View Profile\" }],\n        },\n        {\n          id: \"team-4\",\n          name: \"Harry Yep\",\n          subtitle: \"Software Engineer\",\n          color: \"#0891b2\",\n          actions: [{ id: \"profile\", label: \"View Profile\" }],\n        },\n        {\n          id: \"team-5\",\n          name: \"Ivan Masyuk\",\n          subtitle: \"Software Engineer\",\n          color: \"#0284c7\",\n          actions: [{ id: \"profile\", label: \"View Profile\" }],\n        },\n        {\n          id: \"team-6\",\n          name: \"Sam Dickson\",\n          subtitle: \"Software Engineer\",\n          color: \"#2563eb\",\n          actions: [{ id: \"profile\", label: \"View Profile\" }],\n        },\n        {\n          id: \"team-7\",\n          name: \"Shobhit Patra\",\n          subtitle: \"Software Engineer\",\n          color: \"#4f46e5\",\n          actions: [{ id: \"profile\", label: \"View Profile\" }],\n        },\n        {\n          id: \"team-8\",\n          name: \"Jasmine Le Roux\",\n          subtitle: \"Software Engineer\",\n          color: \"#7c3aed\",\n          actions: [{ id: \"profile\", label: \"View Profile\" }],\n        },\n      ],\n    } satisfies SerializableItemCarousel,\n    generateExampleCode: generateItemCarouselCode,\n  },\n  shopping: {\n    description: \"Products for comparison shopping\",\n    data: {\n      id: \"item-carousel-shopping\",\n      items: [\n        {\n          id: \"prod-1\",\n          name: \"Sony WH-1000XM5\",\n          subtitle: \"$349.99\",\n          color: \"#d97706\",\n          actions: [\n            { id: \"compare\", label: \"Compare\", variant: \"secondary\" },\n            { id: \"cart\", label: \"Add to Cart\" },\n          ],\n        },\n        {\n          id: \"prod-2\",\n          name: \"Bose QuietComfort Ultra\",\n          subtitle: \"$429.00\",\n          color: \"#ea580c\",\n          actions: [\n            { id: \"compare\", label: \"Compare\", variant: \"secondary\" },\n            { id: \"cart\", label: \"Add to Cart\" },\n          ],\n        },\n        {\n          id: \"prod-3\",\n          name: \"Apple AirPods Max\",\n          subtitle: \"$549.00\",\n          color: \"#dc2626\",\n          actions: [{ id: \"notify\", label: \"Notify Me\" }],\n        },\n        {\n          id: \"prod-4\",\n          name: \"Sennheiser Momentum 4\",\n          subtitle: \"$379.95\",\n          color: \"#e11d48\",\n          actions: [\n            { id: \"compare\", label: \"Compare\", variant: \"secondary\" },\n            { id: \"cart\", label: \"Add to Cart\" },\n          ],\n        },\n        {\n          id: \"prod-5\",\n          name: \"Audio-Technica ATH-M50x\",\n          subtitle: \"$149.00\",\n          color: \"#db2777\",\n          actions: [\n            { id: \"compare\", label: \"Compare\", variant: \"secondary\" },\n            { id: \"cart\", label: \"Add to Cart\" },\n          ],\n        },\n      ],\n    } satisfies SerializableItemCarousel,\n    generateExampleCode: generateItemCarouselCode,\n  },\n  courses: {\n    description: \"Learning content and tutorials\",\n    data: {\n      id: \"item-carousel-courses\",\n      items: [\n        {\n          id: \"course-1\",\n          name: \"React Fundamentals\",\n          subtitle: \"4h · Beginner\",\n          color: \"#0ea5e9\",\n          actions: [\n            { id: \"preview\", label: \"Preview\", variant: \"secondary\" },\n            { id: \"enroll\", label: \"Enroll\" },\n          ],\n        },\n        {\n          id: \"course-2\",\n          name: \"TypeScript Deep Dive\",\n          subtitle: \"6h · Intermediate\",\n          color: \"#3b82f6\",\n          actions: [\n            { id: \"preview\", label: \"Preview\", variant: \"secondary\" },\n            { id: \"enroll\", label: \"Enroll\" },\n          ],\n        },\n        {\n          id: \"course-3\",\n          name: \"System Design\",\n          subtitle: \"8h · Advanced\",\n          color: \"#6366f1\",\n          actions: [{ id: \"waitlist\", label: \"Join Waitlist\" }],\n        },\n        {\n          id: \"course-4\",\n          name: \"GraphQL Essentials\",\n          subtitle: \"3h · Beginner\",\n          color: \"#8b5cf6\",\n          actions: [\n            { id: \"preview\", label: \"Preview\", variant: \"secondary\" },\n            { id: \"enroll\", label: \"Enroll\" },\n          ],\n        },\n        {\n          id: \"course-5\",\n          name: \"Testing Strategies\",\n          subtitle: \"5h · Intermediate\",\n          color: \"#a855f7\",\n          actions: [\n            { id: \"preview\", label: \"Preview\", variant: \"secondary\" },\n            { id: \"enroll\", label: \"Enroll\" },\n          ],\n        },\n      ],\n    } satisfies SerializableItemCarousel,\n    generateExampleCode: generateItemCarouselCode,\n  },\n  restaurants: {\n    description: \"Nearby places to eat\",\n    data: {\n      id: \"item-carousel-restaurants\",\n      items: [\n        {\n          id: \"rest-1\",\n          name: \"Sushi Nakazawa\",\n          subtitle: \"Japanese · 0.3 mi\",\n          color: \"#f43f5e\",\n          actions: [\n            { id: \"menu\", label: \"Menu\", variant: \"secondary\" },\n            { id: \"reserve\", label: \"Reserve\" },\n          ],\n        },\n        {\n          id: \"rest-2\",\n          name: \"Osteria Francescana\",\n          subtitle: \"Italian · 0.5 mi\",\n          color: \"#ec4899\",\n          actions: [\n            { id: \"menu\", label: \"Menu\", variant: \"secondary\" },\n            { id: \"reserve\", label: \"Reserve\" },\n          ],\n        },\n        {\n          id: \"rest-3\",\n          name: \"Eleven Madison Park\",\n          subtitle: \"American · 0.8 mi\",\n          color: \"#d946ef\",\n          actions: [{ id: \"waitlist\", label: \"Join Waitlist\" }],\n        },\n        {\n          id: \"rest-4\",\n          name: \"Le Bernardin\",\n          subtitle: \"French · 1.2 mi\",\n          color: \"#a855f7\",\n          actions: [\n            { id: \"menu\", label: \"Menu\", variant: \"secondary\" },\n            { id: \"reserve\", label: \"Reserve\" },\n          ],\n        },\n      ],\n    } satisfies SerializableItemCarousel,\n    generateExampleCode: generateItemCarouselCode,\n  },\n  events: {\n    description: \"Upcoming activities and gatherings\",\n    data: {\n      id: \"item-carousel-events\",\n      items: [\n        {\n          id: \"event-1\",\n          name: \"React Conf 2025\",\n          subtitle: \"May 15 · San Francisco\",\n          color: \"#14b8a6\",\n          actions: [\n            { id: \"details\", label: \"Details\", variant: \"secondary\" },\n            { id: \"rsvp\", label: \"RSVP\" },\n          ],\n        },\n        {\n          id: \"event-2\",\n          name: \"Design Systems Meetup\",\n          subtitle: \"Jan 8 · New York\",\n          color: \"#06b6d4\",\n          actions: [\n            { id: \"details\", label: \"Details\", variant: \"secondary\" },\n            { id: \"rsvp\", label: \"RSVP\" },\n          ],\n        },\n        {\n          id: \"event-3\",\n          name: \"AI/ML Summit\",\n          subtitle: \"Mar 22 · Austin\",\n          color: \"#0ea5e9\",\n          actions: [{ id: \"notify\", label: \"Get Notified\" }],\n        },\n        {\n          id: \"event-4\",\n          name: \"TypeScript Congress\",\n          subtitle: \"Apr 10 · Online\",\n          color: \"#3b82f6\",\n          actions: [\n            { id: \"details\", label: \"Details\", variant: \"secondary\" },\n            { id: \"rsvp\", label: \"RSVP\" },\n          ],\n        },\n        {\n          id: \"event-5\",\n          name: \"Frontend Happy Hour\",\n          subtitle: \"Dec 20 · Local\",\n          color: \"#6366f1\",\n          actions: [\n            { id: \"details\", label: \"Details\", variant: \"secondary\" },\n            { id: \"rsvp\", label: \"RSVP\" },\n          ],\n        },\n      ],\n    } satisfies SerializableItemCarousel,\n    generateExampleCode: generateItemCarouselCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/presets/link-preview.ts",
    "content": "import type { SerializableLinkPreview } from \"@/components/tool-ui/link-preview\";\nimport type { SerializableAction } from \"@/components/tool-ui/shared\";\nimport type { PresetWithCodeGen } from \"./types\";\n\ninterface LinkPreviewData {\n  linkPreview: SerializableLinkPreview;\n  localActions?: SerializableAction[];\n}\n\nfunction escape(value: string): string {\n  return value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n}\n\nfunction generateLinkPreviewCode(data: LinkPreviewData): string {\n  const { linkPreview, localActions } = data;\n  const props: string[] = [];\n\n  props.push(`  id=\"${linkPreview.id}\"`);\n  props.push(`  href=\"${linkPreview.href}\"`);\n\n  if (linkPreview.title) {\n    props.push(`  title=\"${escape(linkPreview.title)}\"`);\n  }\n\n  if (linkPreview.description) {\n    props.push(`  description=\"${escape(linkPreview.description)}\"`);\n  }\n\n  if (linkPreview.image) {\n    props.push(`  image=\"${linkPreview.image}\"`);\n  }\n\n  if (linkPreview.domain) {\n    props.push(`  domain=\"${linkPreview.domain}\"`);\n  }\n\n  if (linkPreview.favicon) {\n    props.push(`  favicon=\"${linkPreview.favicon}\"`);\n  }\n\n  if (linkPreview.ratio) {\n    props.push(`  ratio=\"${linkPreview.ratio}\"`);\n  }\n\n  if (linkPreview.createdAt) {\n    props.push(`  createdAt=\"${linkPreview.createdAt}\"`);\n  }\n\n  const linkPreviewCode = `<LinkPreview\\n${props.join(\"\\n\")}\\n/>`;\n  if (!localActions || localActions.length === 0) {\n    return linkPreviewCode;\n  }\n\n  return `${linkPreviewCode}\n<LocalActions\n  surfaceId=\"${linkPreview.id}\"\n  actions={${JSON.stringify(localActions, null, 2).replace(/\\n/g, \"\\n  \")}}\n  onAction={(actionId) => console.log(\"Local action:\", actionId)}\n/>`;\n}\n\nexport type LinkPreviewPresetName = \"basic\" | \"with-image\" | \"with-actions\";\n\nexport const linkPreviewPresets: Record<\n  LinkPreviewPresetName,\n  PresetWithCodeGen<LinkPreviewData>\n> = {\n  basic: {\n    description: \"Simple link preview with title and description\",\n    data: {\n      linkPreview: {\n        id: \"link-preview-basic\",\n        href: \"https://react.dev/reference/rsc/server-components\",\n        title: \"React Server Components\",\n        description:\n          \"Server Components are a new type of Component that renders ahead of time.\",\n        domain: \"react.dev\",\n      },\n    } satisfies LinkPreviewData,\n    generateExampleCode: generateLinkPreviewCode,\n  },\n  \"with-image\": {\n    description: \"Link preview with OG image\",\n    data: {\n      linkPreview: {\n        id: \"link-preview-image\",\n        href: \"https://en.wikipedia.org/wiki/History_of_computing_hardware\",\n        title: \"A brief history of computing hardware\",\n        description:\n          \"Mechanical calculators, vacuum tubes, transistors, microprocessors — and what came next.\",\n        image:\n          \"https://images.unsplash.com/photo-1562408590-e32931084e23?auto=format&fit=crop&q=80&w=2046\",\n        domain: \"wikipedia.org\",\n        ratio: \"16:9\",\n      },\n    } satisfies LinkPreviewData,\n    generateExampleCode: generateLinkPreviewCode,\n  },\n  \"with-actions\": {\n    description: \"Link preview with external local actions\",\n    data: {\n      linkPreview: {\n        id: \"link-preview-actions\",\n        href: \"https://developer.mozilla.org/en-US/docs/Web/API\",\n        title: \"Web APIs | MDN\",\n        description:\n          \"When writing code for the Web, there are a large number of Web APIs available.\",\n        image:\n          \"https://images.unsplash.com/photo-1517694712202-14dd9538aa97?auto=format&fit=crop&w=1200\",\n        domain: \"developer.mozilla.org\",\n        favicon: \"https://developer.mozilla.org/favicon.ico\",\n        ratio: \"16:9\",\n      },\n      localActions: [\n        { id: \"open\", label: \"Open\", variant: \"default\" },\n        { id: \"copy\", label: \"Copy link\", variant: \"secondary\" },\n      ],\n    } satisfies LinkPreviewData,\n    generateExampleCode: generateLinkPreviewCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/presets/linkedin-post.ts",
    "content": "import type { LinkedInPostData } from \"@/components/tool-ui/linkedin-post\";\nimport type { ActionsProp } from \"@/components/tool-ui/shared\";\nimport type { PresetWithCodeGen } from \"./types\";\n\ninterface LinkedInPostPresetData {\n  post: LinkedInPostData;\n  localActions?: ActionsProp;\n}\n\nexport type LinkedInPostPresetName =\n  | \"basic\"\n  | \"link\"\n  | \"media\"\n  | \"footer-actions\";\n\nfunction generateLinkedInPostCode(data: LinkedInPostPresetData): string {\n  const props: string[] = [];\n\n  props.push(\n    `  post={${JSON.stringify(data.post, null, 2).replace(/\\n/g, \"\\n  \")}}`,\n  );\n\n  const postCode = `<LinkedInPost\\n${props.join(\"\\n\")}\\n/>`;\n  if (!data.localActions) {\n    return postCode;\n  }\n\n  return `${postCode}\n<LocalActions\n  surfaceId=\"${data.post.id}\"\n  actions={${JSON.stringify(data.localActions, null, 2).replace(/\\n/g, \"\\n  \")}}\n  onAction={(actionId) => console.log(\"Local action:\", actionId)}\n/>`;\n}\n\nexport const linkedInPostPresets: Record<\n  LinkedInPostPresetName,\n  PresetWithCodeGen<LinkedInPostPresetData>\n> = {\n  basic: {\n    description: \"Text-only professional post\",\n    data: {\n      post: {\n        id: \"li-post-basic\",\n        author: {\n          name: \"Dr. Sarah Chen\",\n          avatarUrl:\n            \"https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=200&h=200&fit=crop\",\n          headline: \"VP of Engineering at TechCorp | Building the future of AI\",\n        },\n        text: \"Excited to share that our team just shipped a major update to our ML pipeline. Six months of hard work, countless iterations, and one incredible team.\\n\\nKey learnings:\\n• Start with the problem, not the solution\\n• Iterate fast, fail faster\\n• Celebrate small wins\\n\\nProud of everyone who made this possible!\",\n        stats: { likes: 847 },\n        createdAt: \"2025-11-05T09:15:00.000Z\",\n      },\n    } satisfies LinkedInPostPresetData,\n    generateExampleCode: generateLinkedInPostCode,\n  },\n  link: {\n    description: \"Post with link preview\",\n    data: {\n      post: {\n        id: \"li-post-link\",\n        author: {\n          name: \"Michael Torres\",\n          avatarUrl: \"https://api.dicebear.com/7.x/avataaars/svg?seed=michael\",\n          headline: \"Product Manager | Former Google | Stanford MBA\",\n        },\n        text: \"Great read on the future of product management. The role is evolving faster than ever.\",\n        linkPreview: {\n          url: \"https://hbr.org/product-management\",\n          title: \"The Future of Product Management\",\n          description:\n            \"How AI is reshaping the PM role and what skills matter most in 2025.\",\n          imageUrl:\n            \"https://images.unsplash.com/photo-1552664730-d307ca884978?w=600&h=300&fit=crop\",\n          domain: \"hbr.org\",\n        },\n        stats: { likes: 234 },\n        createdAt: \"2025-11-24T14:30:00.000Z\",\n      },\n    } satisfies LinkedInPostPresetData,\n    generateExampleCode: generateLinkedInPostCode,\n  },\n  media: {\n    description: \"Post with image\",\n    data: {\n      post: {\n        id: \"li-post-media\",\n        author: {\n          name: \"Jennifer Walsh\",\n          avatarUrl: \"https://api.dicebear.com/7.x/avataaars/svg?seed=jennifer\",\n          headline: \"CEO at StartupXYZ | Forbes 30 Under 30\",\n        },\n        text: \"Just wrapped up our company offsite. Nothing beats getting the whole team together in person!\",\n        media: {\n          type: \"image\",\n          url: \"https://images.unsplash.com/photo-1515187029135-18ee286d815b?w=800&h=450&fit=crop\",\n          alt: \"Team gathering at company offsite\",\n        },\n        stats: { likes: 1205 },\n        createdAt: \"2025-11-22T16:00:00.000Z\",\n      },\n    } satisfies LinkedInPostPresetData,\n    generateExampleCode: generateLinkedInPostCode,\n  },\n  \"footer-actions\": {\n    description: \"Post with external local actions\",\n    data: {\n      post: {\n        id: \"li-post-footer\",\n        author: {\n          name: \"TechCorp Careers\",\n          avatarUrl: \"https://api.dicebear.com/7.x/shapes/svg?seed=techcorp\",\n          headline: \"Official TechCorp Recruitment Account\",\n        },\n        text: \"We're hiring! Looking for senior engineers to join our growing team. Remote-friendly, competitive benefits, and exciting projects.\\n\\nRoles open:\\n• Senior Backend Engineer\\n• Staff Frontend Engineer\\n• Engineering Manager\",\n        stats: { likes: 456 },\n        createdAt: \"2025-11-24T10:00:00.000Z\",\n      },\n      localActions: [\n        { id: \"view-jobs\", label: \"View Open Positions\", variant: \"secondary\" },\n        { id: \"apply\", label: \"Apply Now\", variant: \"default\" },\n      ],\n    } satisfies LinkedInPostPresetData,\n    generateExampleCode: generateLinkedInPostCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/presets/message-draft.ts",
    "content": "import type { SerializableMessageDraft } from \"@/components/tool-ui/message-draft\";\nimport type { PresetWithCodeGen } from \"./types\";\n\nexport type MessageDraftPresetName =\n  | \"email\"\n  | \"email-with-cc\"\n  | \"slack-channel\"\n  | \"slack-dm\"\n  | \"sent\";\n\nfunction generateMessageDraftCode(data: SerializableMessageDraft): string {\n  const props: string[] = [];\n\n  props.push(`  id=\"${data.id}\"`);\n  props.push(`  channel=\"${data.channel}\"`);\n\n  if (data.channel === \"email\") {\n    props.push(`  subject=\"${data.subject}\"`);\n    if (data.from) {\n      props.push(`  from=\"${data.from}\"`);\n    }\n    props.push(`  to={${JSON.stringify(data.to)}}`);\n    if (data.cc && data.cc.length > 0) {\n      props.push(`  cc={${JSON.stringify(data.cc)}}`);\n    }\n    if (data.bcc && data.bcc.length > 0) {\n      props.push(`  bcc={${JSON.stringify(data.bcc)}}`);\n    }\n  }\n\n  if (data.channel === \"slack\") {\n    const targetProps = [\n      `type: \"${data.target.type}\"`,\n      `name: \"${data.target.name}\"`,\n    ];\n    if (\n      data.target.type === \"channel\" &&\n      data.target.memberCount !== undefined\n    ) {\n      targetProps.push(`memberCount: ${data.target.memberCount}`);\n    }\n    props.push(`  target={{ ${targetProps.join(\", \")} }}`);\n  }\n\n  props.push(`  body={\\`${data.body.replace(/`/g, \"\\\\`\")}\\`}`);\n\n  if (data.outcome) {\n    props.push(`  outcome=\"${data.outcome}\"`);\n  }\n\n  props.push(`  onSend={() => console.log(\"Message sent\")}`);\n  props.push(`  onCancel={() => console.log(\"Message cancelled\")}`);\n\n  return `<MessageDraft\\n${props.join(\"\\n\")}\\n/>`;\n}\n\nexport const messageDraftPresets: Record<\n  MessageDraftPresetName,\n  PresetWithCodeGen<SerializableMessageDraft>\n> = {\n  email: {\n    description: \"Simple email to a single recipient\",\n    data: {\n      id: \"message-draft-email\",\n      channel: \"email\",\n      subject: \"Updated proposal attached\",\n      from: \"sarah.mitchell@acme.co\",\n      to: [\"marcus.chen@acme.co\"],\n      body: `Hi Marcus,\n\nI've attached the revised proposal with the changes we discussed. The new timeline reflects the Q2 launch date, and I've adjusted the budget breakdown in section 3.\n\nLet me know if you have any questions.\n\nBest,\nSarah`,\n    },\n    generateExampleCode: generateMessageDraftCode,\n  },\n  \"email-with-cc\": {\n    description: \"Email with CC and BCC recipients\",\n    data: {\n      id: \"message-draft-email-cc\",\n      channel: \"email\",\n      subject: \"Re: Q4 Budget Review Meeting\",\n      from: \"dana.kim@raycast.com\",\n      to: [\"finance-team@raycast.com\"],\n      cc: [\"jamie.wright@raycast.com\", \"ops@raycast.com\"],\n      bcc: [\"cfo@raycast.com\"],\n      body: `Hi team,\n\nFollowing up on yesterday's meeting. I've compiled the department requests and attached the consolidated spreadsheet.\n\nKey changes from last quarter:\n- Engineering: +15% for infrastructure scaling\n- Marketing: Flat (reallocating to digital)\n- Support: +8% for new hires\n\nPlease review and flag any concerns by Friday. We'll finalize allocations next week.\n\nThanks,\nDana`,\n    },\n    generateExampleCode: generateMessageDraftCode,\n  },\n  \"slack-channel\": {\n    description: \"Slack message to a channel\",\n    data: {\n      id: \"message-draft-slack-channel\",\n      channel: \"slack\",\n      target: { type: \"channel\", name: \"eng-releases\", memberCount: 47 },\n      body: `Shipped v2.4.1 to production. Changes:\n- Fixed auth token refresh bug\n- Improved search latency by 40%\n- Added keyboard shortcuts for power users\n\nMonitoring dashboards look good. Rollback plan ready if needed.`,\n    },\n    generateExampleCode: generateMessageDraftCode,\n  },\n  \"slack-dm\": {\n    description: \"Direct message to a person\",\n    data: {\n      id: \"message-draft-slack-dm\",\n      channel: \"slack\",\n      target: { type: \"dm\", name: \"Alex Rivera\" },\n      body: `Hey Alex, just wanted to check in on the API integration. The client asked if we're still on track for the Thursday demo. No pressure if things shifted - just want to give them an accurate update.`,\n    },\n    generateExampleCode: generateMessageDraftCode,\n  },\n  sent: {\n    description: \"Sent receipt state\",\n    data: {\n      id: \"message-draft-sent\",\n      channel: \"email\",\n      subject: \"Interview follow-up\",\n      from: \"jordan.hayes@gmail.com\",\n      to: [\"hiring@stripe.com\"],\n      body: `Thank you for taking the time to speak with me today about the Senior Engineer position. I enjoyed learning more about the team's work on payment infrastructure.\n\nI'm excited about the opportunity and look forward to hearing from you.\n\nBest regards,\nJordan`,\n      outcome: \"sent\",\n    },\n    generateExampleCode: generateMessageDraftCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/presets/option-list.ts",
    "content": "import type { SerializableOptionList } from \"@/components/tool-ui/option-list\";\nimport type { PresetWithCodeGen } from \"./types\";\n\nexport type OptionListPresetName =\n  | \"max-selections\"\n  | \"travel\"\n  | \"approval\"\n  | \"receipt\"\n  | \"receipt-multi\"\n  | \"destructive\";\n\nfunction generateOptionListCode(data: SerializableOptionList): string {\n  const props: string[] = [];\n  const hasChoice = data.choice !== undefined && data.choice !== null;\n\n  props.push(\n    `  options={${JSON.stringify(data.options, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n\n  if (data.selectionMode && data.selectionMode !== \"multi\") {\n    props.push(`  selectionMode=\"${data.selectionMode}\"`);\n  }\n\n  if (data.minSelections && data.minSelections !== 1) {\n    props.push(`  minSelections={${data.minSelections}}`);\n  }\n\n  if (data.maxSelections) {\n    props.push(`  maxSelections={${data.maxSelections}}`);\n  }\n\n  if (hasChoice) {\n    const choiceValue =\n      typeof data.choice === \"string\"\n        ? `\"${data.choice}\"`\n        : JSON.stringify(data.choice);\n    props.push(`  choice={${choiceValue}}`);\n  }\n\n  if (data.actions) {\n    props.push(\n      `  actions={${JSON.stringify(data.actions, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n    );\n  }\n\n  if (!hasChoice) {\n    props.push(\n      `  onAction={(actionId, selection) => {\\n    if (actionId === \"confirm\") {\\n      console.log(\"Selection:\", selection);\\n    }\\n  }}`,\n    );\n  }\n\n  return `<OptionList\\n${props.join(\"\\n\")}\\n/>`;\n}\n\nexport const optionListPresets: Record<\n  OptionListPresetName,\n  PresetWithCodeGen<SerializableOptionList>\n> = {\n  \"max-selections\": {\n    description: \"Pick two (you can't have all three)\",\n    data: {\n      id: \"option-list-preview-max-selections\",\n      options: [\n        { id: \"good\", label: \"Good\", description: \"High quality work\" },\n        { id: \"fast\", label: \"Fast\", description: \"Quick turnaround\" },\n        { id: \"cheap\", label: \"Cheap\", description: \"Low cost\" },\n      ],\n      selectionMode: \"multi\",\n      minSelections: 1,\n      maxSelections: 2,\n      actions: [\n        { id: \"cancel\", label: \"Reset\" },\n        { id: \"confirm\", label: \"Confirm\", variant: \"default\" },\n      ],\n    } satisfies SerializableOptionList,\n    generateExampleCode: generateOptionListCode,\n  },\n  travel: {\n    description: \"Single-select with radio styling\",\n    data: {\n      id: \"option-list-preview-travel\",\n      options: [\n        {\n          id: \"walk\",\n          label: \"Walking\",\n          description: \"Sidewalk-friendly route\",\n        },\n        { id: \"drive\", label: \"Driving\", description: \"Fastest ETA\" },\n        {\n          id: \"transit\",\n          label: \"Transit\",\n          description: \"Use subway and buses\",\n        },\n      ],\n      selectionMode: \"single\",\n      actions: [\n        { id: \"cancel\", label: \"Reset\" },\n        { id: \"confirm\", label: \"Continue\", variant: \"default\" },\n      ],\n    } satisfies SerializableOptionList,\n    generateExampleCode: generateOptionListCode,\n  },\n  approval: {\n    description: \"Release checklist (all items required)\",\n    data: {\n      id: \"option-list-preview-approval\",\n      options: [\n        {\n          id: \"code-review\",\n          label: \"Code Review Complete\",\n          description: \"All reviewers have approved\",\n        },\n        {\n          id: \"tests-pass\",\n          label: \"Tests Passing\",\n          description: \"CI pipeline is green\",\n        },\n        {\n          id: \"docs-updated\",\n          label: \"Documentation Updated\",\n          description: \"README and API docs current\",\n        },\n        {\n          id: \"changelog\",\n          label: \"Changelog Entry Added\",\n          description: \"Version bump noted\",\n        },\n      ],\n      selectionMode: \"multi\",\n      minSelections: 4,\n      actions: [\n        { id: \"cancel\", label: \"Cancel\" },\n        {\n          id: \"confirm\",\n          label: \"Approve Release\",\n          confirmLabel: \"Confirm Release\",\n          variant: \"default\",\n        },\n      ],\n    } satisfies SerializableOptionList,\n    generateExampleCode: generateOptionListCode,\n  },\n  receipt: {\n    description: \"Selected travel mode (receipt state)\",\n    data: {\n      id: \"option-list-preview-receipt\",\n      options: [\n        {\n          id: \"walk\",\n          label: \"Walking\",\n          description: \"Sidewalk-friendly route\",\n        },\n        {\n          id: \"drive\",\n          label: \"Driving\",\n          description: \"Fastest ETA for this route\",\n        },\n        {\n          id: \"transit\",\n          label: \"Transit\",\n          description: \"Use subway and buses\",\n        },\n      ],\n      selectionMode: \"single\",\n      choice: \"drive\",\n    } satisfies SerializableOptionList,\n    generateExampleCode: generateOptionListCode,\n  },\n  \"receipt-multi\": {\n    description: \"Selected release checks (receipt state)\",\n    data: {\n      id: \"option-list-preview-receipt-multi\",\n      options: [\n        {\n          id: \"code-review\",\n          label: \"Code Review Complete\",\n          description: \"All reviewers have approved\",\n        },\n        {\n          id: \"tests-pass\",\n          label: \"Tests Passing\",\n          description: \"CI pipeline is green\",\n        },\n        {\n          id: \"docs-updated\",\n          label: \"Documentation Updated\",\n          description: \"README and API docs are current\",\n        },\n        {\n          id: \"changelog\",\n          label: \"Changelog Entry Added\",\n          description: \"Version bump is documented\",\n        },\n      ],\n      selectionMode: \"multi\",\n      choice: [\"code-review\", \"tests-pass\", \"docs-updated\"],\n    } satisfies SerializableOptionList,\n    generateExampleCode: generateOptionListCode,\n  },\n  destructive: {\n    description: \"Delete confirmation with safeguards\",\n    data: {\n      id: \"option-list-preview-destructive\",\n      options: [\n        {\n          id: \"soft-delete\",\n          label: \"Move to Trash\",\n          description: \"Can be restored within 30 days\",\n        },\n        {\n          id: \"hard-delete\",\n          label: \"Delete Permanently\",\n          description: \"Cannot be undone, all data will be lost\",\n        },\n        {\n          id: \"archive\",\n          label: \"Archive Instead\",\n          description: \"Hide from view but keep all data\",\n        },\n      ],\n      selectionMode: \"single\",\n      actions: [\n        { id: \"cancel\", label: \"Cancel\" },\n        {\n          id: \"confirm\",\n          label: \"Delete\",\n          confirmLabel: \"Confirm Delete\",\n          variant: \"destructive\",\n        },\n      ],\n    } satisfies SerializableOptionList,\n    generateExampleCode: generateOptionListCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/presets/order-summary.ts",
    "content": "import type { SerializableOrderSummary } from \"@/components/tool-ui/order-summary\";\nimport type { PresetWithCodeGen } from \"./types\";\n\nexport type OrderSummaryPresetName =\n  | \"default\"\n  | \"with-discount\"\n  | \"free-shipping\"\n  | \"receipt\"\n  | \"international\";\n\nfunction generateOrderSummaryCode(data: SerializableOrderSummary): string {\n  const props: string[] = [];\n\n  props.push(`  id=\"${data.id}\"`);\n\n  if (data.title && data.title !== \"Order Summary\") {\n    props.push(`  title=\"${data.title}\"`);\n  }\n\n  props.push(\n    `  items={${JSON.stringify(data.items, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n\n  props.push(\n    `  pricing={${JSON.stringify(data.pricing, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n\n  if (data.choice) {\n    props.push(\n      `  choice={${JSON.stringify(data.choice, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n    );\n  }\n\n  return `<OrderSummary\\n${props.join(\"\\n\")}\\n/>`;\n}\n\nexport const orderSummaryPresets: Record<\n  OrderSummaryPresetName,\n  PresetWithCodeGen<SerializableOrderSummary>\n> = {\n  default: {\n    description: \"Standard order with multiple items and tax\",\n    data: {\n      id: \"order-summary-default\",\n      items: [\n        {\n          id: \"item-1\",\n          name: \"Premium Coffee Beans\",\n          description: \"Single origin, medium roast\",\n          imageUrl:\n            \"https://images.unsplash.com/photo-1559056199-641a0ac8b55e?w=100&h=100&fit=crop\",\n          quantity: 2,\n          unitPrice: 24.0,\n        },\n        {\n          id: \"item-2\",\n          name: \"Ceramic Pour-Over Set\",\n          description: \"Includes dripper and carafe\",\n          imageUrl:\n            \"https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?w=100&h=100&fit=crop\",\n          quantity: 1,\n          unitPrice: 45.0,\n        },\n      ],\n      pricing: {\n        subtotal: 93.0,\n        tax: 7.44,\n        shipping: 5.99,\n        total: 106.43,\n        currency: \"USD\",\n      },\n    } satisfies SerializableOrderSummary,\n    generateExampleCode: generateOrderSummaryCode,\n  },\n\n  \"with-discount\": {\n    description: \"Order with promotional discount applied\",\n    data: {\n      id: \"order-summary-with-discount\",\n      title: \"Your Cart\",\n      items: [\n        {\n          id: \"item-1\",\n          name: \"Standing Desk Frame\",\n          description: 'Electric height adjustable, 60\" width',\n          imageUrl:\n            \"https://images.unsplash.com/photo-1595515106969-1ce29566ff1c?w=100&h=100&fit=crop\",\n          quantity: 1,\n          unitPrice: 449.0,\n        },\n        {\n          id: \"item-2\",\n          name: \"Bamboo Desktop\",\n          description: '60\" x 30\" sustainable bamboo',\n          imageUrl:\n            \"https://images.unsplash.com/photo-1611269154421-4e27233ac5c7?w=100&h=100&fit=crop\",\n          quantity: 1,\n          unitPrice: 199.0,\n        },\n      ],\n      pricing: {\n        subtotal: 648.0,\n        tax: 51.84,\n        shipping: 0,\n        discount: 64.8,\n        discountLabel: \"SAVE10 (10% off)\",\n        total: 635.04,\n        currency: \"USD\",\n      },\n    } satisfies SerializableOrderSummary,\n    generateExampleCode: generateOrderSummaryCode,\n  },\n\n  \"free-shipping\": {\n    description: \"Order qualifying for free shipping\",\n    data: {\n      id: \"order-summary-free-shipping\",\n      items: [\n        {\n          id: \"item-1\",\n          name: \"Premium Coffee Beans\",\n          description: \"Single origin, medium roast, 1kg bag\",\n          imageUrl:\n            \"https://images.unsplash.com/photo-1559056199-641a0ac8b55e?w=100&h=100&fit=crop\",\n          quantity: 3,\n          unitPrice: 28.5,\n        },\n        {\n          id: \"item-2\",\n          name: \"Ceramic Pour-Over Set\",\n          description: \"Includes dripper and carafe\",\n          imageUrl:\n            \"https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?w=100&h=100&fit=crop\",\n          quantity: 1,\n          unitPrice: 45.0,\n        },\n      ],\n      pricing: {\n        subtotal: 130.5,\n        tax: 10.44,\n        shipping: 0,\n        total: 140.94,\n        currency: \"USD\",\n      },\n    } satisfies SerializableOrderSummary,\n    generateExampleCode: generateOrderSummaryCode,\n  },\n\n  receipt: {\n    description: \"Confirmed order showing receipt state\",\n    data: {\n      id: \"order-summary-receipt\",\n      title: \"Order Confirmed\",\n      items: [\n        {\n          id: \"item-1\",\n          name: \"Running Shoes\",\n          description: \"Lightweight, responsive cushioning\",\n          imageUrl:\n            \"https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=100&h=100&fit=crop\",\n          quantity: 1,\n          unitPrice: 129.0,\n        },\n        {\n          id: \"item-2\",\n          name: \"Performance Socks\",\n          description: \"3-pack, moisture wicking\",\n          quantity: 2,\n          unitPrice: 18.0,\n        },\n      ],\n      pricing: {\n        subtotal: 165.0,\n        tax: 13.2,\n        shipping: 8.95,\n        total: 187.15,\n        currency: \"USD\",\n      },\n      choice: {\n        action: \"confirm\",\n        orderId: \"ORD-2024-8847\",\n        confirmedAt: \"2024-12-15T14:32:00Z\",\n      },\n    } satisfies SerializableOrderSummary,\n    generateExampleCode: generateOrderSummaryCode,\n  },\n\n  international: {\n    description: \"Order in EUR currency with VAT\",\n    data: {\n      id: \"order-summary-international\",\n      items: [\n        {\n          id: \"item-1\",\n          name: \"Mechanical Keyboard\",\n          description: \"65% layout, hot-swappable switches\",\n          imageUrl:\n            \"https://images.unsplash.com/photo-1601445638532-3c6f6c3aa1d6?w=100&h=100&fit=crop\",\n          quantity: 1,\n          unitPrice: 189.0,\n        },\n        {\n          id: \"item-2\",\n          name: \"Desk Mat\",\n          description: \"900x400mm, stitched edges\",\n          quantity: 1,\n          unitPrice: 35.0,\n        },\n      ],\n      pricing: {\n        subtotal: 224.0,\n        tax: 42.56,\n        taxLabel: \"VAT (19%)\",\n        shipping: 12.5,\n        total: 279.06,\n        currency: \"EUR\",\n      },\n    } satisfies SerializableOrderSummary,\n    generateExampleCode: generateOrderSummaryCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/presets/parameter-slider.ts",
    "content": "import type { SerializableParameterSlider } from \"@/components/tool-ui/parameter-slider\";\nimport type { PresetWithCodeGen } from \"./types\";\n\nexport type ParameterSliderPresetName =\n  | \"photo-adjustments\"\n  | \"color-grading\"\n  | \"audio-eq\"\n  | \"single-slider\"\n  | \"video-export\";\n\nfunction formatSliderConfig(\n  slider: SerializableParameterSlider[\"sliders\"][number],\n): string {\n  const parts: string[] = [\n    `id: \"${slider.id}\"`,\n    `label: \"${slider.label}\"`,\n    `min: ${slider.min}`,\n    `max: ${slider.max}`,\n  ];\n\n  if (slider.step !== undefined && slider.step !== 1) {\n    parts.push(`step: ${slider.step}`);\n  }\n\n  parts.push(`value: ${slider.value}`);\n\n  if (slider.unit) {\n    parts.push(`unit: \"${slider.unit}\"`);\n  }\n\n  if (slider.precision !== undefined) {\n    parts.push(`precision: ${slider.precision}`);\n  }\n\n  return `{ ${parts.join(\", \")} }`;\n}\n\nfunction generateParameterSliderCode(\n  data: SerializableParameterSlider,\n): string {\n  const props: string[] = [];\n\n  props.push(`  id=\"${data.id}\"`);\n\n  const slidersStr = data.sliders\n    .map((s) => `    ${formatSliderConfig(s)}`)\n    .join(\",\\n\");\n  props.push(`  sliders={[\\n${slidersStr},\\n  ]}`);\n\n  if (data.actions) {\n    props.push(\n      `  actions={${JSON.stringify(data.actions, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n    );\n  }\n\n  props.push(\n    `  onAction={(actionId, values) => {\\n    console.log(actionId, values);\\n  }}`,\n  );\n\n  return `<ParameterSlider\\n${props.join(\"\\n\")}\\n/>`;\n}\n\nexport const parameterSliderPresets: Record<\n  ParameterSliderPresetName,\n  PresetWithCodeGen<SerializableParameterSlider>\n> = {\n  \"photo-adjustments\": {\n    description: \"Photo editing exposure controls\",\n    data: {\n      id: \"parameter-slider-photo-adjustments\",\n      sliders: [\n        {\n          id: \"exposure\",\n          label: \"Exposure\",\n          min: -3,\n          max: 3,\n          step: 0.1,\n          value: 0.3,\n          unit: \"EV\",\n          precision: 1,\n        },\n        {\n          id: \"contrast\",\n          label: \"Contrast\",\n          min: -100,\n          max: 100,\n          step: 5,\n          value: 15,\n          unit: \"%\",\n        },\n        {\n          id: \"highlights\",\n          label: \"Highlights\",\n          min: -100,\n          max: 100,\n          step: 5,\n          value: -20,\n          unit: \"%\",\n        },\n        {\n          id: \"shadows\",\n          label: \"Shadows\",\n          min: -100,\n          max: 100,\n          step: 5,\n          value: 25,\n          unit: \"%\",\n        },\n      ],\n      actions: [\n        { id: \"reset\", label: \"Reset\", variant: \"ghost\" },\n        { id: \"apply\", label: \"Apply\", variant: \"default\" },\n      ],\n    } satisfies SerializableParameterSlider,\n    generateExampleCode: generateParameterSliderCode,\n  },\n\n  \"color-grading\": {\n    description: \"Color temperature and tint adjustments\",\n    data: {\n      id: \"parameter-slider-color-grading\",\n      sliders: [\n        {\n          id: \"temperature\",\n          label: \"Temperature\",\n          min: 2000,\n          max: 10000,\n          step: 100,\n          value: 5600,\n          unit: \"K\",\n        },\n        { id: \"tint\", label: \"Tint\", min: -100, max: 100, step: 5, value: -8 },\n        {\n          id: \"saturation\",\n          label: \"Saturation\",\n          min: -100,\n          max: 100,\n          step: 5,\n          value: 12,\n          unit: \"%\",\n        },\n      ],\n      actions: [\n        { id: \"reset\", label: \"Reset\", variant: \"ghost\" },\n        { id: \"apply\", label: \"Apply\", variant: \"default\" },\n      ],\n    } satisfies SerializableParameterSlider,\n    generateExampleCode: generateParameterSliderCode,\n  },\n\n  \"audio-eq\": {\n    description: \"Audio equalizer frequency controls\",\n    data: {\n      id: \"parameter-slider-audio-eq\",\n      sliders: [\n        {\n          id: \"bass\",\n          label: \"Bass\",\n          min: -12,\n          max: 12,\n          step: 1,\n          value: 3,\n          unit: \"dB\",\n        },\n        {\n          id: \"mid\",\n          label: \"Mid\",\n          min: -12,\n          max: 12,\n          step: 1,\n          value: -2,\n          unit: \"dB\",\n        },\n        {\n          id: \"treble\",\n          label: \"Treble\",\n          min: -12,\n          max: 12,\n          step: 1,\n          value: 4,\n          unit: \"dB\",\n        },\n      ],\n      actions: [\n        { id: \"reset\", label: \"Flat\", variant: \"ghost\" },\n        { id: \"apply\", label: \"Apply\", variant: \"default\" },\n      ],\n    } satisfies SerializableParameterSlider,\n    generateExampleCode: generateParameterSliderCode,\n  },\n\n  \"single-slider\": {\n    description: \"Single slider for simple adjustments\",\n    data: {\n      id: \"parameter-slider-single\",\n      sliders: [\n        {\n          id: \"blur\",\n          label: \"Background Blur\",\n          min: 0,\n          max: 100,\n          step: 5,\n          value: 35,\n          unit: \"%\",\n        },\n      ],\n      actions: [\n        { id: \"reset\", label: \"Reset\", variant: \"ghost\" },\n        { id: \"apply\", label: \"Apply\", variant: \"default\" },\n      ],\n    } satisfies SerializableParameterSlider,\n    generateExampleCode: generateParameterSliderCode,\n  },\n\n  \"video-export\": {\n    description: \"Video export quality settings\",\n    data: {\n      id: \"parameter-slider-video-export\",\n      sliders: [\n        {\n          id: \"bitrate\",\n          label: \"Bitrate\",\n          min: 1,\n          max: 50,\n          step: 0.5,\n          value: 24,\n          unit: \"Mbps\",\n          precision: 1,\n        },\n        {\n          id: \"keyframe\",\n          label: \"Keyframe Interval\",\n          min: 1,\n          max: 10,\n          step: 1,\n          value: 2,\n          unit: \"sec\",\n        },\n        {\n          id: \"quality\",\n          label: \"CRF Quality\",\n          min: 0,\n          max: 51,\n          step: 1,\n          value: 18,\n        },\n      ],\n      actions: [\n        { id: \"reset\", label: \"Defaults\", variant: \"ghost\" },\n        { id: \"apply\", label: \"Export\", variant: \"default\" },\n      ],\n    } satisfies SerializableParameterSlider,\n    generateExampleCode: generateParameterSliderCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/presets/plan.ts",
    "content": "import type { SerializablePlan } from \"@/components/tool-ui/plan\";\nimport type { PresetWithCodeGen } from \"./types\";\n\nexport type PlanPresetName =\n  | \"simple\"\n  | \"compact\"\n  | \"comprehensive\"\n  | \"mixed_states\"\n  | \"all_complete\";\n\nfunction escape(value: string): string {\n  return value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n}\n\nfunction generatePlanCode(data: SerializablePlan): string {\n  const props: string[] = [];\n\n  props.push(`  id=\"${data.id}\"`);\n  props.push(`  title=\"${escape(data.title)}\"`);\n\n  if (data.description) {\n    props.push(`  description=\"${escape(data.description)}\"`);\n  }\n\n  props.push(\n    `  todos={${JSON.stringify(data.todos, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n\n  if (data.maxVisibleTodos) {\n    props.push(`  maxVisibleTodos={${data.maxVisibleTodos}}`);\n  }\n\n  return `<Plan\\n${props.join(\"\\n\")}\\n/>`;\n}\n\nfunction generatePlanCompactCode(data: SerializablePlan): string {\n  const props: string[] = [];\n\n  props.push(`  id=\"${data.id}\"`);\n  props.push(`  title=\"${escape(data.title)}\"`);\n\n  if (data.description) {\n    props.push(`  description=\"${escape(data.description)}\"`);\n  }\n\n  props.push(\n    `  todos={${JSON.stringify(data.todos, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n\n  if (data.maxVisibleTodos) {\n    props.push(`  maxVisibleTodos={${data.maxVisibleTodos}}`);\n  }\n\n  return `<Plan.Compact\\n${props.join(\"\\n\")}\\n/>`;\n}\n\nexport const planPresets: Record<\n  PlanPresetName,\n  PresetWithCodeGen<SerializablePlan>\n> = {\n  simple: {\n    description: \"A minimal plan with 3 todos\",\n    data: {\n      id: \"plan-simple\",\n      title: \"Quick Setup\",\n      description: \"Get started in 3 easy steps\",\n      todos: [\n        { id: \"1\", label: \"Install dependencies\", status: \"completed\" },\n        { id: \"2\", label: \"Configure environment\", status: \"in_progress\" },\n        { id: \"3\", label: \"Run the app\", status: \"pending\" },\n      ],\n    } satisfies SerializablePlan,\n    generateExampleCode: generatePlanCode,\n  },\n  compact: {\n    description: \"Compact variant with no progress summary bar\",\n    data: {\n      id: \"plan-compact\",\n      title: \"Quick Setup\",\n      description: \"Get started in 3 easy steps\",\n      todos: [\n        { id: \"1\", label: \"Install dependencies\", status: \"completed\" },\n        { id: \"2\", label: \"Configure environment\", status: \"in_progress\" },\n        { id: \"3\", label: \"Run the app\", status: \"pending\" },\n      ],\n    } satisfies SerializablePlan,\n    generateExampleCode: generatePlanCompactCode,\n  },\n  comprehensive: {\n    description: \"Detailed plan with descriptions and progress bar\",\n    data: {\n      id: \"plan-comprehensive\",\n      title: \"Feature Implementation Plan\",\n      description:\n        \"Step-by-step guide for implementing the new authentication system\",\n      todos: [\n        {\n          id: \"1\",\n          label: \"Review existing auth flow\",\n          status: \"completed\",\n          description:\n            \"Analyzed current session-based auth and identified pain points\",\n        },\n        {\n          id: \"2\",\n          label: \"Design new token structure\",\n          status: \"completed\",\n          description:\n            \"Created JWT schema with access/refresh token separation\",\n        },\n        {\n          id: \"3\",\n          label: \"Implement JWT middleware\",\n          status: \"in_progress\",\n          description:\n            \"Adding token validation and refresh logic to API routes\",\n        },\n        { id: \"4\", label: \"Add refresh token logic\", status: \"pending\" },\n        { id: \"5\", label: \"Update user model\", status: \"pending\" },\n        {\n          id: \"6\",\n          label: \"Write integration tests\",\n          status: \"pending\",\n          description: \"Cover auth flows, token expiry, and edge cases\",\n        },\n        { id: \"7\", label: \"Update API documentation\", status: \"pending\" },\n        { id: \"8\", label: \"Deploy to staging\", status: \"pending\" },\n      ],\n    } satisfies SerializablePlan,\n    generateExampleCode: generatePlanCode,\n  },\n  mixed_states: {\n    description: \"All 4 status states with expandable details\",\n    data: {\n      id: \"plan-mixed\",\n      title: \"Migration Progress\",\n      todos: [\n        { id: \"1\", label: \"Backup database\", status: \"completed\" },\n        { id: \"2\", label: \"Run migration scripts\", status: \"completed\" },\n        {\n          id: \"3\",\n          label: \"Verify data integrity\",\n          status: \"in_progress\",\n          description: \"Running checksums on migrated records\",\n        },\n        {\n          id: \"4\",\n          label: \"Update legacy endpoints\",\n          status: \"cancelled\",\n          description: \"Decided to deprecate instead of migrate\",\n        },\n        { id: \"5\", label: \"Switch DNS records\", status: \"pending\" },\n      ],\n    } satisfies SerializablePlan,\n    generateExampleCode: generatePlanCode,\n  },\n  all_complete: {\n    description: \"Completion celebration state\",\n    data: {\n      id: \"plan-all-complete\",\n      title: \"Deployment Complete\",\n      description: \"All steps finished successfully\",\n      todos: [\n        { id: \"1\", label: \"Run pre-flight checks\", status: \"completed\" },\n        { id: \"2\", label: \"Deploy to production\", status: \"completed\" },\n        { id: \"3\", label: \"Verify health endpoints\", status: \"completed\" },\n        { id: \"4\", label: \"Update status page\", status: \"completed\" },\n      ],\n    } satisfies SerializablePlan,\n    generateExampleCode: generatePlanCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/presets/preferences-panel.ts",
    "content": "import type {\n  SerializablePreferencesPanel,\n  SerializablePreferencesPanelReceipt,\n} from \"@/components/tool-ui/preferences-panel\";\nimport type { PresetWithCodeGen } from \"./types\";\n\nexport type PreferencesPanelPresetName =\n  | \"notifications\"\n  | \"privacy\"\n  | \"appearance\"\n  | \"workflow\"\n  | \"receipt\"\n  | \"error\";\n\nfunction generatePreferencesPanelCode(\n  data: SerializablePreferencesPanel | SerializablePreferencesPanelReceipt,\n): string {\n  const props: string[] = [];\n\n  if (data.title) {\n    props.push(`  title=\"${data.title}\"`);\n  }\n\n  props.push(\n    `  sections={${JSON.stringify(data.sections, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n\n  if (\"choice\" in data) {\n    props.push(\n      `  choice={${JSON.stringify(data.choice, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n    );\n\n    if (data.error) {\n      props.push(\n        `  error={${JSON.stringify(data.error, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n      );\n    }\n\n    return `<PreferencesPanel.Receipt\\n${props.join(\"\\n\")}\\n/>`;\n  }\n\n  if (data.actions) {\n    props.push(\n      `  actions={${JSON.stringify(data.actions, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n    );\n  }\n\n  props.push(\n    `  onAction={(actionId, values) => {\\n    console.log(actionId, values);\\n  }}`,\n  );\n\n  return `<PreferencesPanel\\n${props.join(\"\\n\")}\\n/>`;\n}\n\nexport const preferencesPanelPresets: Record<\n  PreferencesPanelPresetName,\n  PresetWithCodeGen<\n    SerializablePreferencesPanel | SerializablePreferencesPanelReceipt\n  >\n> = {\n  notifications: {\n    description: \"Basic notification preferences with switches\",\n    data: {\n      id: \"preferences-panel-preview-notifications\",\n      sections: [\n        {\n          items: [\n            {\n              id: \"email-notifications\",\n              label: \"Email Notifications\",\n              description: \"Receive updates via email\",\n              type: \"switch\",\n              defaultChecked: true,\n            },\n            {\n              id: \"push-notifications\",\n              label: \"Push Notifications\",\n              description: \"Mobile and desktop alerts\",\n              type: \"switch\",\n              defaultChecked: false,\n            },\n            {\n              id: \"weekly-digest\",\n              label: \"Weekly Digest\",\n              description: \"Summary of activity every Monday\",\n              type: \"switch\",\n              defaultChecked: true,\n            },\n          ],\n        },\n      ],\n    } satisfies SerializablePreferencesPanel,\n    generateExampleCode: generatePreferencesPanelCode,\n  },\n  privacy: {\n    description: \"Multi-section settings with headings and mixed controls\",\n    data: {\n      id: \"preferences-panel-preview-privacy\",\n      title: \"Privacy Settings\",\n      sections: [\n        {\n          heading: \"Visibility\",\n          items: [\n            {\n              id: \"profile-visibility\",\n              label: \"Profile Visibility\",\n              description: \"Who can see your profile\",\n              type: \"toggle\",\n              options: [\n                { value: \"public\", label: \"Public\" },\n                { value: \"connections\", label: \"Friends\" },\n                { value: \"private\", label: \"Private\" },\n              ],\n              defaultValue: \"connections\",\n            },\n            {\n              id: \"activity-status\",\n              label: \"Activity Status\",\n              description: \"Show when you're active\",\n              type: \"switch\",\n              defaultChecked: true,\n            },\n          ],\n        },\n        {\n          heading: \"Data\",\n          items: [\n            {\n              id: \"analytics\",\n              label: \"Usage Analytics\",\n              description: \"Help improve our service\",\n              type: \"switch\",\n              defaultChecked: false,\n            },\n          ],\n        },\n      ],\n    } satisfies SerializablePreferencesPanel,\n    generateExampleCode: generatePreferencesPanelCode,\n  },\n  appearance: {\n    description: \"Mixed control types: toggle and select\",\n    data: {\n      id: \"preferences-panel-preview-appearance\",\n      sections: [\n        {\n          items: [\n            {\n              id: \"theme\",\n              label: \"Theme\",\n              description: \"Choose your color scheme\",\n              type: \"toggle\",\n              options: [\n                { value: \"light\", label: \"Light\" },\n                { value: \"dark\", label: \"Dark\" },\n                { value: \"auto\", label: \"Auto\" },\n              ],\n              defaultValue: \"auto\",\n            },\n            {\n              id: \"font-size\",\n              label: \"Font Size\",\n              description: \"Adjust text size\",\n              type: \"select\",\n              selectOptions: [\n                { value: \"xs\", label: \"Extra Small\" },\n                { value: \"sm\", label: \"Small\" },\n                { value: \"md\", label: \"Medium\" },\n                { value: \"lg\", label: \"Large\" },\n                { value: \"xl\", label: \"Extra Large\" },\n              ],\n              defaultSelected: \"md\",\n            },\n            {\n              id: \"compact-mode\",\n              label: \"Compact Mode\",\n              description: \"Reduce spacing for more content\",\n              type: \"switch\",\n              defaultChecked: false,\n            },\n          ],\n        },\n      ],\n    } satisfies SerializablePreferencesPanel,\n    generateExampleCode: generatePreferencesPanelCode,\n  },\n  workflow: {\n    description: \"Workflow automation settings with select dropdown\",\n    data: {\n      id: \"preferences-panel-preview-workflow\",\n      title: \"Automation Settings\",\n      sections: [\n        {\n          items: [\n            {\n              id: \"auto-assign\",\n              label: \"Auto-Assign Tasks\",\n              description: \"Automatically assign based on workload\",\n              type: \"switch\",\n              defaultChecked: true,\n            },\n            {\n              id: \"default-priority\",\n              label: \"Default Priority\",\n              description: \"Priority for new tasks\",\n              type: \"select\",\n              selectOptions: [\n                { value: \"critical\", label: \"Critical\" },\n                { value: \"high\", label: \"High\" },\n                { value: \"medium\", label: \"Medium\" },\n                { value: \"low\", label: \"Low\" },\n                { value: \"backlog\", label: \"Backlog\" },\n              ],\n              defaultSelected: \"medium\",\n            },\n            {\n              id: \"notification-timing\",\n              label: \"Notification Timing\",\n              description: \"When to send reminders\",\n              type: \"toggle\",\n              options: [\n                { value: \"instant\", label: \"Instant\" },\n                { value: \"hourly\", label: \"Hourly\" },\n                { value: \"daily\", label: \"Daily\" },\n              ],\n              defaultValue: \"instant\",\n            },\n          ],\n        },\n      ],\n    } satisfies SerializablePreferencesPanel,\n    generateExampleCode: generatePreferencesPanelCode,\n  },\n  receipt: {\n    description: \"Confirmed preferences in receipt state\",\n    data: {\n      id: \"preferences-panel-preview-receipt\",\n      title: \"Privacy Settings\",\n      sections: [\n        {\n          heading: \"Visibility\",\n          items: [\n            {\n              id: \"profile-visibility\",\n              label: \"Profile Visibility\",\n              description: \"Who can see your profile\",\n              type: \"toggle\",\n              options: [\n                { value: \"public\", label: \"Public\" },\n                { value: \"connections\", label: \"Friends\" },\n                { value: \"private\", label: \"Private\" },\n              ],\n              defaultValue: \"connections\",\n            },\n            {\n              id: \"activity-status\",\n              label: \"Activity Status\",\n              description: \"Show when you're active\",\n              type: \"switch\",\n              defaultChecked: true,\n            },\n          ],\n        },\n        {\n          heading: \"Data\",\n          items: [\n            {\n              id: \"analytics\",\n              label: \"Usage Analytics\",\n              description: \"Help improve our service\",\n              type: \"switch\",\n              defaultChecked: false,\n            },\n          ],\n        },\n      ],\n      choice: {\n        \"profile-visibility\": \"private\",\n        \"activity-status\": false,\n        analytics: false,\n      },\n    } satisfies SerializablePreferencesPanelReceipt,\n    generateExampleCode: generatePreferencesPanelCode,\n  },\n  error: {\n    description: \"Partial save with errors showing failed fields\",\n    data: {\n      id: \"preferences-panel-preview-error\",\n      title: \"Privacy Settings\",\n      sections: [\n        {\n          heading: \"Visibility\",\n          items: [\n            {\n              id: \"profile-visibility\",\n              label: \"Profile Visibility\",\n              description: \"Who can see your profile\",\n              type: \"toggle\",\n              options: [\n                { value: \"public\", label: \"Public\" },\n                { value: \"connections\", label: \"Friends\" },\n                { value: \"private\", label: \"Private\" },\n              ],\n              defaultValue: \"connections\",\n            },\n            {\n              id: \"activity-status\",\n              label: \"Activity Status\",\n              description: \"Show when you're active\",\n              type: \"switch\",\n              defaultChecked: true,\n            },\n          ],\n        },\n        {\n          heading: \"Data\",\n          items: [\n            {\n              id: \"analytics\",\n              label: \"Usage Analytics\",\n              description: \"Help improve our service\",\n              type: \"switch\",\n              defaultChecked: false,\n            },\n          ],\n        },\n      ],\n      choice: {\n        \"profile-visibility\": \"private\",\n        \"activity-status\": false,\n        analytics: false,\n      },\n      error: {\n        analytics: \"Analytics requires accepting Terms of Service\",\n      },\n    } satisfies SerializablePreferencesPanelReceipt,\n    generateExampleCode: generatePreferencesPanelCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/presets/progress-tracker.ts",
    "content": "import type { SerializableProgressTracker } from \"@/components/tool-ui/progress-tracker\";\nimport type { PresetWithCodeGen } from \"./types\";\n\nexport type ProgressTrackerPresetName =\n  | \"in-progress\"\n  | \"completed\"\n  | \"failed\"\n  | \"with-elapsed-time\"\n  | \"receipt\"\n  | \"receipt-failed\";\n\ntype ProgressTrackerPreset = PresetWithCodeGen<SerializableProgressTracker>;\n\nfunction generateProgressTrackerCode(\n  data: SerializableProgressTracker,\n): string {\n  const props: string[] = [];\n\n  props.push(`  id=\"${data.id}\"`);\n  props.push(\n    `  steps={${JSON.stringify(data.steps, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n\n  if (data.elapsedTime !== undefined) {\n    props.push(`  elapsedTime={${data.elapsedTime}}`);\n  }\n\n  if (data.choice) {\n    props.push(\n      `  choice={${JSON.stringify(data.choice, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n    );\n  }\n\n  return `<ProgressTracker\\n${props.join(\"\\n\")}\\n/>`;\n}\n\nexport const progressTrackerPresets = {\n  \"in-progress\": {\n    description: \"Live deployment progress with running steps\",\n    data: {\n      id: \"progress-tracker-in-progress\",\n      steps: [\n        {\n          id: \"build\",\n          label: \"Building\",\n          description: \"Compiling TypeScript and bundling assets\",\n          status: \"completed\",\n        },\n        {\n          id: \"test\",\n          label: \"Running Tests\",\n          description: \"147 tests across 23 suites\",\n          status: \"in-progress\",\n        },\n        {\n          id: \"deploy\",\n          label: \"Deploy to Production\",\n          description: \"Upload to edge nodes\",\n          status: \"pending\",\n        },\n      ],\n      elapsedTime: 43200,\n    } satisfies SerializableProgressTracker,\n    generateExampleCode: generateProgressTrackerCode,\n  },\n  completed: {\n    description: \"All steps successfully completed\",\n    data: {\n      id: \"progress-tracker-completed\",\n      steps: [\n        {\n          id: \"verify\",\n          label: \"Verify Credentials\",\n          description: \"Authentication successful\",\n          status: \"completed\",\n        },\n        {\n          id: \"download\",\n          label: \"Download Files\",\n          description: \"3.2 GB transferred\",\n          status: \"completed\",\n        },\n        {\n          id: \"extract\",\n          label: \"Extract Archive\",\n          description: \"1,247 files extracted\",\n          status: \"completed\",\n        },\n        {\n          id: \"install\",\n          label: \"Install Dependencies\",\n          description: \"npm install completed\",\n          status: \"completed\",\n        },\n      ],\n      elapsedTime: 128500,\n    } satisfies SerializableProgressTracker,\n    generateExampleCode: generateProgressTrackerCode,\n  },\n  failed: {\n    description: \"Operation failed during execution\",\n    data: {\n      id: \"progress-tracker-failed\",\n      steps: [\n        {\n          id: \"connect\",\n          label: \"Connect to Database\",\n          description: \"postgres://prod-db-1.example.com\",\n          status: \"completed\",\n        },\n        {\n          id: \"backup\",\n          label: \"Create Backup\",\n          description: \"Snapshot before migration\",\n          status: \"completed\",\n        },\n        {\n          id: \"migrate\",\n          label: \"Run Migrations\",\n          description: \"Failed: column 'user_id' already exists\",\n          status: \"failed\",\n        },\n        {\n          id: \"verify\",\n          label: \"Verify Schema\",\n          status: \"pending\",\n        },\n      ],\n      elapsedTime: 8300,\n    } satisfies SerializableProgressTracker,\n    generateExampleCode: generateProgressTrackerCode,\n  },\n  \"with-elapsed-time\": {\n    description: \"Long-running import with time tracking\",\n    data: {\n      id: \"progress-tracker-with-elapsed-time\",\n      steps: [\n        {\n          id: \"parse\",\n          label: \"Parse CSV\",\n          description: \"124,856 rows detected\",\n          status: \"completed\",\n        },\n        {\n          id: \"validate\",\n          label: \"Validate Records\",\n          description: \"Checking required fields\",\n          status: \"completed\",\n        },\n        {\n          id: \"transform\",\n          label: \"Transform Data\",\n          description: \"Normalizing formats and values\",\n          status: \"in-progress\",\n        },\n        {\n          id: \"import\",\n          label: \"Import to Database\",\n          description: \"Batch insert in progress\",\n          status: \"pending\",\n        },\n      ],\n      elapsedTime: 247800,\n    } satisfies SerializableProgressTracker,\n    generateExampleCode: generateProgressTrackerCode,\n  },\n  receipt: {\n    description: \"Completed operation shown as receipt\",\n    data: {\n      id: \"progress-tracker-receipt\",\n      steps: [\n        {\n          id: \"build\",\n          label: \"Building\",\n          status: \"completed\",\n        },\n        {\n          id: \"test\",\n          label: \"Testing\",\n          status: \"completed\",\n        },\n        {\n          id: \"deploy\",\n          label: \"Deploying\",\n          status: \"completed\",\n        },\n      ],\n      elapsedTime: 128500,\n      choice: {\n        outcome: \"success\",\n        summary: \"Deployment complete\",\n        at: new Date().toISOString(),\n      },\n    } satisfies SerializableProgressTracker,\n    generateExampleCode: generateProgressTrackerCode,\n  },\n  \"receipt-failed\": {\n    description: \"Failed operation shown as receipt\",\n    data: {\n      id: \"progress-tracker-receipt-failed\",\n      steps: [\n        {\n          id: \"connect\",\n          label: \"Connect to Database\",\n          status: \"completed\",\n        },\n        {\n          id: \"backup\",\n          label: \"Create Backup\",\n          status: \"completed\",\n        },\n        {\n          id: \"migrate\",\n          label: \"Run Migrations\",\n          description: \"Failed: column 'user_id' already exists\",\n          status: \"failed\",\n        },\n        {\n          id: \"verify\",\n          label: \"Verify Schema\",\n          status: \"pending\",\n        },\n      ],\n      elapsedTime: 8300,\n      choice: {\n        outcome: \"failed\",\n        summary: \"Migration failed\",\n        at: new Date().toISOString(),\n      },\n    } satisfies SerializableProgressTracker,\n    generateExampleCode: generateProgressTrackerCode,\n  },\n} satisfies Record<string, ProgressTrackerPreset>;\n"
  },
  {
    "path": "apps/www/lib/presets/question-flow.ts",
    "content": "import type {\n  SerializableQuestionFlow,\n  SerializableProgressiveMode,\n  SerializableUpfrontMode,\n  SerializableReceiptMode,\n} from \"@/components/tool-ui/question-flow\";\nimport type { PresetWithCodeGen } from \"./types\";\n\nexport type QuestionFlowPresetName =\n  | \"progressive\"\n  | \"upfront\"\n  | \"multi-select\"\n  | \"receipt\";\n\nfunction generateProgressiveCode(data: SerializableProgressiveMode): string {\n  const props: string[] = [];\n\n  props.push(`  id=\"${data.id}\"`);\n  props.push(`  step={${data.step}}`);\n  props.push(`  title=\"${data.title}\"`);\n\n  if (data.description) {\n    props.push(`  description=\"${data.description}\"`);\n  }\n\n  props.push(\n    `  options={${JSON.stringify(data.options, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n\n  if (data.selectionMode && data.selectionMode !== \"single\") {\n    props.push(`  selectionMode=\"${data.selectionMode}\"`);\n  }\n\n  props.push(`  onSelect={(ids) => console.log(\"Selected:\", ids)}`);\n  props.push(`  onBack={() => console.log(\"Go back\")}`);\n\n  return `<QuestionFlow\\n${props.join(\"\\n\")}\\n/>`;\n}\n\nfunction generateUpfrontCode(data: SerializableUpfrontMode): string {\n  const props: string[] = [];\n\n  props.push(`  id=\"${data.id}\"`);\n  props.push(\n    `  steps={${JSON.stringify(data.steps, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n  props.push(`  onComplete={(answers) => console.log(\"Complete:\", answers)}`);\n\n  return `<QuestionFlow\\n${props.join(\"\\n\")}\\n/>`;\n}\n\nfunction generateReceiptCode(data: SerializableReceiptMode): string {\n  const props: string[] = [];\n\n  props.push(`  id=\"${data.id}\"`);\n  props.push(\n    `  choice={${JSON.stringify(data.choice, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n\n  return `<QuestionFlow\\n${props.join(\"\\n\")}\\n/>`;\n}\n\nfunction generateQuestionFlowCode(data: SerializableQuestionFlow): string {\n  if (\"choice\" in data && data.choice) {\n    return generateReceiptCode(data as SerializableReceiptMode);\n  }\n  if (\"steps\" in data && data.steps) {\n    return generateUpfrontCode(data as SerializableUpfrontMode);\n  }\n  return generateProgressiveCode(data as SerializableProgressiveMode);\n}\n\nexport const questionFlowPresets: Record<\n  QuestionFlowPresetName,\n  PresetWithCodeGen<SerializableQuestionFlow>\n> = {\n  progressive: {\n    description: \"AI-controlled step with database selection\",\n    data: {\n      id: \"question-flow-database\",\n      step: 2,\n      title: \"Select database type\",\n      description: \"Where should we store your data?\",\n      options: [\n        {\n          id: \"postgres\",\n          label: \"PostgreSQL\",\n          description: \"Open source with strong SQL support\",\n        },\n        {\n          id: \"mysql\",\n          label: \"MySQL\",\n          description: \"Widely supported, good for web applications\",\n        },\n        {\n          id: \"sqlite\",\n          label: \"SQLite\",\n          description: \"Embedded, no setup required\",\n        },\n      ],\n    } satisfies SerializableProgressiveMode,\n    generateExampleCode: generateQuestionFlowCode,\n  },\n  upfront: {\n    description: \"Complete wizard with all steps defined upfront\",\n    data: {\n      id: \"question-flow-project-setup\",\n      steps: [\n        {\n          id: \"language\",\n          title: \"Select a programming language\",\n          description:\n            \"This determines which frameworks and tools are available.\",\n          options: [\n            { id: \"python\", label: \"Python\" },\n            { id: \"typescript\", label: \"TypeScript\" },\n            { id: \"go\", label: \"Go\" },\n          ],\n        },\n        {\n          id: \"framework\",\n          title: \"Choose a framework\",\n          description: \"Pick the framework you're most comfortable with.\",\n          options: [\n            { id: \"fastapi\", label: \"FastAPI\" },\n            { id: \"django\", label: \"Django\" },\n            { id: \"flask\", label: \"Flask\" },\n          ],\n        },\n        {\n          id: \"database\",\n          title: \"Select your database\",\n          description: \"Your data will be stored and queried from here.\",\n          options: [\n            { id: \"postgres\", label: \"PostgreSQL\" },\n            { id: \"mysql\", label: \"MySQL\" },\n            { id: \"mongodb\", label: \"MongoDB\" },\n          ],\n        },\n      ],\n    } satisfies SerializableUpfrontMode,\n    generateExampleCode: generateQuestionFlowCode,\n  },\n  \"multi-select\": {\n    description: \"Step with multiple selections allowed\",\n    data: {\n      id: \"question-flow-features\",\n      step: 3,\n      title: \"Select features to include\",\n      description: \"Choose all the features you want in your project.\",\n      selectionMode: \"multi\",\n      options: [\n        {\n          id: \"auth\",\n          label: \"Authentication\",\n          description: \"User login and registration\",\n        },\n        {\n          id: \"api\",\n          label: \"REST API\",\n          description: \"API endpoints for external access\",\n        },\n        {\n          id: \"admin\",\n          label: \"Admin Panel\",\n          description: \"Dashboard for managing content\",\n        },\n        {\n          id: \"analytics\",\n          label: \"Analytics\",\n          description: \"Track user behavior and metrics\",\n        },\n      ],\n    } satisfies SerializableProgressiveMode,\n    generateExampleCode: generateQuestionFlowCode,\n  },\n  receipt: {\n    description: \"Completed wizard showing configuration summary\",\n    data: {\n      id: \"question-flow-receipt\",\n      choice: {\n        title: \"Project configured\",\n        summary: [\n          { label: \"Language\", value: \"Python\" },\n          { label: \"Framework\", value: \"FastAPI\" },\n          { label: \"Database\", value: \"PostgreSQL\" },\n          { label: \"Features\", value: \"Auth, API, Admin\" },\n        ],\n      },\n    } satisfies SerializableReceiptMode,\n    generateExampleCode: generateQuestionFlowCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/presets/stats-display.ts",
    "content": "import type { SerializableStatsDisplay } from \"@/components/tool-ui/stats-display\";\nimport type { PresetWithCodeGen } from \"./types\";\n\nexport type StatsDisplayPresetName =\n  | \"business-metrics\"\n  | \"fitness-summary\"\n  | \"portfolio\"\n  | \"mixed-formats\"\n  | \"single-stat\";\n\ntype StatsDisplayPreset = PresetWithCodeGen<SerializableStatsDisplay>;\n\nfunction generateStatsDisplayCode(data: SerializableStatsDisplay): string {\n  const props: string[] = [];\n\n  props.push(`  id=\"${data.id}\"`);\n\n  if (data.title) {\n    props.push(`  title=\"${data.title}\"`);\n  }\n\n  if (data.description) {\n    props.push(`  description=\"${data.description}\"`);\n  }\n\n  props.push(\n    `  stats={${JSON.stringify(data.stats, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n\n  return `<StatsDisplay\\n${props.join(\"\\n\")}\\n/>`;\n}\n\nexport const statsDisplayPresets: Record<\n  StatsDisplayPresetName,\n  StatsDisplayPreset\n> = {\n  \"business-metrics\": {\n    description: \"Full-featured dashboard with sparklines and diff indicators\",\n    data: {\n      id: \"stats-display-business-metrics\",\n      title: \"Q4 Performance\",\n      description: \"October through December 2024\",\n      stats: [\n        {\n          key: \"revenue\",\n          label: \"Revenue\",\n          value: 847300,\n          format: { kind: \"currency\", currency: \"USD\", decimals: 0 },\n          sparkline: {\n            data: [\n              72000, 68000, 74000, 81000, 78000, 85000, 89000, 91000, 86000,\n              94000, 97000, 102000,\n            ],\n            color: \"var(--chart-1)\",\n          },\n          diff: { value: 12.4, decimals: 1 },\n        },\n        {\n          key: \"active-users\",\n          label: \"Active Users\",\n          value: 24890,\n          format: { kind: \"number\", compact: true },\n          sparkline: {\n            data: [\n              18200, 19100, 19800, 20400, 21200, 21900, 22600, 23100, 23800,\n              24200, 24500, 24890,\n            ],\n            color: \"var(--chart-3)\",\n          },\n          diff: { value: 8.2, decimals: 1 },\n        },\n        {\n          key: \"churn\",\n          label: \"Churn Rate\",\n          value: 2.1,\n          format: { kind: \"percent\", decimals: 1, basis: \"unit\" },\n          sparkline: {\n            data: [3.2, 3.0, 2.8, 2.9, 2.7, 2.5, 2.4, 2.3, 2.2, 2.1, 2.1, 2.1],\n            color: \"var(--chart-4)\",\n          },\n          diff: { value: -0.8, decimals: 1, upIsPositive: false },\n        },\n        {\n          key: \"nps\",\n          label: \"NPS Score\",\n          value: 72,\n          format: { kind: \"number\" },\n          sparkline: {\n            data: [58, 61, 64, 62, 65, 68, 66, 69, 70, 71, 71, 72],\n            color: \"var(--chart-5)\",\n          },\n          diff: { value: 5.0, decimals: 0 },\n        },\n      ],\n    },\n    generateExampleCode: generateStatsDisplayCode,\n  },\n  \"fitness-summary\": {\n    description: \"Personal health metrics with sparklines\",\n    data: {\n      id: \"stats-display-fitness-summary\",\n      title: \"Today's Activity\",\n      stats: [\n        {\n          key: \"steps\",\n          label: \"Steps\",\n          value: 8432,\n          format: { kind: \"number\" },\n          sparkline: {\n            data: [1200, 2400, 3100, 4800, 5200, 6100, 7400, 8432],\n            color: \"var(--chart-3)\",\n          },\n          diff: { value: 12.4, decimals: 0 },\n        },\n        {\n          key: \"calories\",\n          label: \"Calories\",\n          value: 524,\n          format: { kind: \"number\" },\n          sparkline: {\n            data: [80, 145, 210, 285, 340, 410, 465, 524],\n            color: \"var(--chart-5)\",\n          },\n        },\n        {\n          key: \"active-minutes\",\n          label: \"Active Minutes\",\n          value: 47,\n          format: { kind: \"number\" },\n          sparkline: {\n            data: [8, 15, 22, 28, 32, 38, 43, 47],\n            color: \"var(--chart-4)\",\n          },\n        },\n        {\n          key: \"heart-rate\",\n          label: \"Avg Heart Rate\",\n          value: 72,\n          format: { kind: \"number\" },\n          sparkline: {\n            data: [68, 71, 74, 82, 78, 71, 69, 72],\n            color: \"var(--chart-2)\",\n          },\n        },\n      ],\n    },\n    generateExampleCode: generateStatsDisplayCode,\n  },\n  portfolio: {\n    description: \"Investment performance with currency and percent formatting\",\n    data: {\n      id: \"stats-display-portfolio\",\n      title: \"Portfolio Summary\",\n      description: \"As of market close\",\n      stats: [\n        {\n          key: \"total-value\",\n          label: \"Total Value\",\n          value: 284750,\n          format: { kind: \"currency\", currency: \"USD\", decimals: 0 },\n          sparkline: {\n            data: [\n              268000, 271000, 265000, 274000, 278000, 282000, 279000, 284750,\n            ],\n            color: \"var(--chart-1)\",\n          },\n        },\n        {\n          key: \"day-change\",\n          label: \"Day Change\",\n          value: 1847.5,\n          format: { kind: \"currency\", currency: \"USD\" },\n          diff: { value: 0.65, decimals: 2 },\n        },\n        {\n          key: \"total-return\",\n          label: \"Total Return\",\n          value: 0.182,\n          format: { kind: \"percent\", decimals: 1, basis: \"fraction\" },\n          diff: { value: 18.2, decimals: 1 },\n        },\n        {\n          key: \"dividend-yield\",\n          label: \"Dividend Yield\",\n          value: 2.4,\n          format: { kind: \"percent\", decimals: 1, basis: \"unit\" },\n        },\n      ],\n    },\n    generateExampleCode: generateStatsDisplayCode,\n  },\n  \"mixed-formats\": {\n    description: \"Different format types in a single display\",\n    data: {\n      id: \"stats-display-mixed-formats\",\n      title: \"Subscription Overview\",\n      stats: [\n        {\n          key: \"mrr\",\n          label: \"MRR\",\n          value: 42850,\n          format: { kind: \"currency\", currency: \"EUR\", decimals: 0 },\n          sparkline: {\n            data: [38000, 39200, 40100, 41000, 41800, 42850],\n            color: \"var(--chart-1)\",\n          },\n          diff: { value: 6.8, decimals: 1 },\n        },\n        {\n          key: \"subscribers\",\n          label: \"Active Subscribers\",\n          value: 1247,\n          format: { kind: \"number\" },\n          diff: { value: 3.2, decimals: 1 },\n        },\n        {\n          key: \"conversion\",\n          label: \"Trial Conversion\",\n          value: 0.234,\n          format: { kind: \"percent\", decimals: 1, basis: \"fraction\" },\n        },\n        {\n          key: \"plan\",\n          label: \"Most Popular Plan\",\n          value: \"Professional\",\n          format: { kind: \"text\" },\n        },\n      ],\n    },\n    generateExampleCode: generateStatsDisplayCode,\n  },\n  \"single-stat\": {\n    description: \"Single prominent metric with sparkline\",\n    data: {\n      id: \"stats-display-single-stat\",\n      stats: [\n        {\n          key: \"active-users\",\n          label: \"Active Users\",\n          value: 1847,\n          format: { kind: \"number\" },\n          sparkline: {\n            data: [1420, 1380, 1510, 1620, 1580, 1690, 1720, 1780, 1810, 1847],\n            color: \"var(--chart-1)\",\n          },\n          diff: { value: 12.3, decimals: 1 },\n        },\n      ],\n    },\n    generateExampleCode: generateStatsDisplayCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/presets/terminal.ts",
    "content": "import type { SerializableTerminal } from \"@/components/tool-ui/terminal\";\nimport type { SerializableAction } from \"@/components/tool-ui/shared\";\nimport type { PresetWithCodeGen } from \"./types\";\n\nexport type TerminalPresetName =\n  | \"success\"\n  | \"error\"\n  | \"build\"\n  | \"ansiColors\"\n  | \"collapsible\"\n  | \"noOutput\"\n  | \"with-actions\";\n\nfunction escape(value: string): string {\n  return value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"').replace(/`/g, \"\\\\`\");\n}\n\ninterface TerminalPresetData extends SerializableTerminal {\n  localActions?: SerializableAction[];\n}\n\nfunction generateTerminalCode(data: TerminalPresetData): string {\n  const props: string[] = [];\n\n  props.push(`  command=\"${escape(data.command)}\"`);\n\n  if (data.stdout) {\n    props.push(`  stdout={\\`${escape(data.stdout)}\\`}`);\n  }\n\n  if (data.stderr) {\n    props.push(`  stderr={\\`${escape(data.stderr)}\\`}`);\n  }\n\n  props.push(`  exitCode={${data.exitCode}}`);\n\n  if (data.durationMs !== undefined) {\n    props.push(`  durationMs={${data.durationMs}}`);\n  }\n\n  if (data.cwd) {\n    props.push(`  cwd=\"${data.cwd}\"`);\n  }\n\n  if (data.truncated) {\n    props.push(`  truncated={${data.truncated}}`);\n  }\n\n  if (data.maxCollapsedLines) {\n    props.push(`  maxCollapsedLines={${data.maxCollapsedLines}}`);\n  }\n\n  const terminal = `<Terminal\\n${props.join(\"\\n\")}\\n/>`;\n  if (!data.localActions || data.localActions.length === 0) {\n    return terminal;\n  }\n\n  return `${terminal}\n<LocalActions\n  surfaceId=\"${data.id}\"\n  actions={${JSON.stringify(data.localActions, null, 2).replace(/\\n/g, \"\\n  \")}}\n  onAction={(actionId) => console.log(\"Local action:\", actionId)}\n/>`;\n}\n\nexport const terminalPresets: Record<\n  TerminalPresetName,\n  PresetWithCodeGen<TerminalPresetData>\n> = {\n  success: {\n    description: \"Successful test run with duration\",\n    data: {\n      id: \"terminal-preview-success\",\n      command: \"pnpm test\",\n      stdout: `\\x1b[32m✓\\x1b[0m src/utils.test.ts \\x1b[90m(5 tests)\\x1b[0m \\x1b[33m23ms\\x1b[0m\n\\x1b[32m✓\\x1b[0m src/api.test.ts \\x1b[90m(12 tests)\\x1b[0m \\x1b[33m156ms\\x1b[0m\n\\x1b[32m✓\\x1b[0m src/components.test.ts \\x1b[90m(8 tests)\\x1b[0m \\x1b[33m89ms\\x1b[0m\n\n\\x1b[1mTest Files\\x1b[0m  \\x1b[32m3 passed\\x1b[0m (3)\n\\x1b[1m     Tests\\x1b[0m  \\x1b[32m25 passed\\x1b[0m (25)\n\\x1b[1m  Start at\\x1b[0m  10:23:45\n\\x1b[1m  Duration\\x1b[0m  312ms`,\n      exitCode: 0,\n      durationMs: 312,\n      cwd: \"~/project\",\n    } satisfies TerminalPresetData,\n    generateExampleCode: generateTerminalCode,\n  },\n  error: {\n    description: \"Failed build with TypeScript error\",\n    data: {\n      id: \"terminal-preview-error\",\n      command: \"pnpm build\",\n      stdout: `Building application...\nCompiling TypeScript...`,\n      stderr: `error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.\n\n  src/utils.ts:23:15\n    23   calculate(input);\n                  ~~~~~\n\nFound 1 error in src/utils.ts:23`,\n      exitCode: 1,\n      durationMs: 1523,\n      cwd: \"~/project\",\n    } satisfies TerminalPresetData,\n    generateExampleCode: generateTerminalCode,\n  },\n  build: {\n    description: \"Docker build output\",\n    data: {\n      id: \"terminal-preview-build\",\n      command: \"docker build -t myapp:latest .\",\n      stdout: `[+] Building 45.2s (12/12) FINISHED\n => [internal] load build definition from Dockerfile\n => [internal] load .dockerignore\n => [internal] load metadata for node:20-alpine\n => [1/7] FROM node:20-alpine@sha256:abc123...\n => [2/7] WORKDIR /app\n => [3/7] COPY package*.json ./\n => [4/7] RUN npm ci --only=production\n => [5/7] COPY . .\n => [6/7] RUN npm run build\n => [7/7] EXPOSE 3000\n => exporting to image\n => => naming to docker.io/library/myapp:latest\n\nSuccessfully built image myapp:latest`,\n      exitCode: 0,\n      durationMs: 45200,\n    } satisfies TerminalPresetData,\n    generateExampleCode: generateTerminalCode,\n  },\n  ansiColors: {\n    description: \"ANSI colored lint output\",\n    data: {\n      id: \"terminal-preview-ansi\",\n      command: \"npm run lint\",\n      stdout: `\\x1b[32m✔\\x1b[0m No ESLint warnings or errors\n\\x1b[36minfo\\x1b[0m Checking formatting...\n\\x1b[33m⚠\\x1b[0m 2 files need formatting\n  \\x1b[90msrc/utils.ts\\x1b[0m\n  \\x1b[90msrc/api.ts\\x1b[0m\n\\x1b[32m✔\\x1b[0m TypeScript compilation successful\n\\x1b[1m\\x1b[32mAll checks passed!\\x1b[0m`,\n      exitCode: 0,\n      durationMs: 2341,\n    } satisfies TerminalPresetData,\n    generateExampleCode: generateTerminalCode,\n  },\n  collapsible: {\n    description: \"Long output with collapse\",\n    data: {\n      id: \"terminal-preview-collapsible\",\n      command: \"pnpm install\",\n      stdout: `Packages: +847\n++++++++++++++++++++++++++++++++++++++++++++++++++\n\nProgress: resolved 892, reused 891, downloaded 1, added 847, done\n\ndependencies:\n+ @radix-ui/react-dialog 1.1.4\n+ @radix-ui/react-dropdown-menu 2.1.4\n+ @radix-ui/react-popover 1.1.4\n+ @radix-ui/react-select 2.1.4\n+ @radix-ui/react-tabs 1.1.2\n+ @radix-ui/react-tooltip 1.1.6\n+ class-variance-authority 0.7.1\n+ clsx 2.1.1\n+ lucide-react 0.468.0\n+ next 15.1.3\n+ react 19.0.0\n+ react-dom 19.0.0\n+ tailwind-merge 2.6.0\n+ tailwindcss-animate 1.0.7\n+ zod 3.24.1\n\ndevDependencies:\n+ @types/node 22.10.5\n+ @types/react 19.0.2\n+ eslint 9.17.0\n+ prettier 3.4.2\n+ typescript 5.7.2\n\nDone in 4.8s`,\n      exitCode: 0,\n      durationMs: 4800,\n      maxCollapsedLines: 8,\n    } satisfies TerminalPresetData,\n    generateExampleCode: generateTerminalCode,\n  },\n  noOutput: {\n    description: \"Command with no output\",\n    data: {\n      id: \"terminal-preview-no-output\",\n      command: \"touch newfile.txt\",\n      exitCode: 0,\n      durationMs: 12,\n      cwd: \"~/project\",\n    } satisfies TerminalPresetData,\n    generateExampleCode: generateTerminalCode,\n  },\n  \"with-actions\": {\n    description: \"Failed command with external local actions\",\n    data: {\n      id: \"terminal-preview-with-actions\",\n      command: \"npm run deploy\",\n      stdout: \"Deploying to production...\",\n      stderr: `Error: Connection timeout\nUnable to reach deployment server after 30s`,\n      exitCode: 1,\n      durationMs: 30125,\n      cwd: \"~/project\",\n      localActions: [\n        { id: \"retry\", label: \"Retry\", variant: \"default\" },\n        { id: \"debug\", label: \"View logs\", variant: \"outline\" },\n      ],\n    } satisfies TerminalPresetData,\n    generateExampleCode: generateTerminalCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/presets/types.ts",
    "content": "export interface Preset<T> {\n  description: string;\n  data: T;\n}\n\nexport type PresetRecord<TName extends string, TData> = Record<\n  TName,\n  Preset<TData>\n>;\n\nexport interface PresetWithCodeGen<T> extends Preset<T> {\n  generateExampleCode: (data: T) => string;\n}\n\nexport type PresetRecordWithCodeGen<TName extends string, TData> = Record<\n  TName,\n  PresetWithCodeGen<TData>\n>;\n"
  },
  {
    "path": "apps/www/lib/presets/video.ts",
    "content": "import type { SerializableVideo } from \"@/components/tool-ui/video\";\nimport type { SerializableAction } from \"@/components/tool-ui/shared\";\nimport type { PresetWithCodeGen } from \"./types\";\n\ninterface VideoData {\n  video: SerializableVideo;\n  localActions?: SerializableAction[];\n}\n\nfunction escape(value: string): string {\n  return value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n}\n\nfunction formatObject(value: Record<string, unknown>): string {\n  return JSON.stringify(value, null, 2).replace(/\\n/g, \"\\n  \");\n}\n\nfunction generateVideoCode(data: VideoData): string {\n  const { video, localActions } = data;\n  const props: string[] = [];\n\n  props.push(`  id=\"${video.id}\"`);\n  props.push(`  assetId=\"${video.assetId}\"`);\n  props.push(`  src=\"${video.src}\"`);\n\n  if (video.poster) {\n    props.push(`  poster=\"${video.poster}\"`);\n  }\n\n  if (video.title) {\n    props.push(`  title=\"${escape(video.title)}\"`);\n  }\n\n  if (video.description) {\n    props.push(`  description=\"${escape(video.description)}\"`);\n  }\n\n  if (video.ratio) {\n    props.push(`  ratio=\"${video.ratio}\"`);\n  }\n\n  if (video.fit) {\n    props.push(`  fit=\"${video.fit}\"`);\n  }\n\n  if (video.durationMs) {\n    props.push(`  durationMs={${video.durationMs}}`);\n  }\n\n  if (video.createdAt) {\n    props.push(`  createdAt=\"${video.createdAt}\"`);\n  }\n\n  if (video.source) {\n    props.push(\n      `  source={${formatObject(video.source as Record<string, unknown>)}}`,\n    );\n  }\n\n  const videoCode = `<Video\\n${props.join(\"\\n\")}\\n/>`;\n  if (!localActions || localActions.length === 0) {\n    return videoCode;\n  }\n\n  return `${videoCode}\n<LocalActions\n  surfaceId=\"${video.id}\"\n  actions={${JSON.stringify(localActions, null, 2).replace(/\\n/g, \"\\n  \")}}\n  onAction={(actionId) => console.log(\"Local action:\", actionId)}\n/>`;\n}\n\nexport type VideoPresetName = \"basic\" | \"with-poster\" | \"with-actions\";\n\nexport const videoPresets: Record<\n  VideoPresetName,\n  PresetWithCodeGen<VideoData>\n> = {\n  basic: {\n    description: \"Simple video player\",\n    data: {\n      video: {\n        id: \"video-preview-basic\",\n        assetId: \"video-basic\",\n        src: \"https://archive.org/download/NatureStockVideo/IMG_9500_.mp4\",\n        title: \"Forest canopy\",\n        ratio: \"16:9\",\n      },\n    } satisfies VideoData,\n    generateExampleCode: generateVideoCode,\n  },\n  \"with-poster\": {\n    description: \"Video with poster thumbnail and metadata\",\n    data: {\n      video: {\n        id: \"video-preview-poster\",\n        assetId: \"video-poster\",\n        src: \"https://archive.org/download/NatureStockVideo/IMG_9500_.mp4\",\n        poster:\n          \"https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=900&auto=format&fit=crop\",\n        title: \"Forest Canopy\",\n        description: \"Sunlight filtering through the trees.\",\n        ratio: \"16:9\",\n        fit: \"cover\",\n        durationMs: 8000,\n        createdAt: \"2025-01-15T08:00:00.000Z\",\n        source: {\n          label: \"Archive.org\",\n          iconUrl: \"https://api.dicebear.com/7.x/shapes/svg?seed=archive\",\n        },\n      },\n    } satisfies VideoData,\n    generateExampleCode: generateVideoCode,\n  },\n  \"with-actions\": {\n    description: \"Video with external local actions\",\n    data: {\n      video: {\n        id: \"video-preview-actions\",\n        assetId: \"video-actions\",\n        src: \"https://archive.org/download/NatureStockVideo/IMG_9500_.mp4\",\n        poster:\n          \"https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=900&auto=format&fit=crop\",\n        title: \"Forest Canopy\",\n        ratio: \"16:9\",\n        durationMs: 8000,\n      },\n      localActions: [\n        { id: \"share\", label: \"Share\", variant: \"default\" },\n        { id: \"download\", label: \"Download\", variant: \"secondary\" },\n      ],\n    } satisfies VideoData,\n    generateExampleCode: generateVideoCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/presets/weather-widget.ts",
    "content": "import type { WeatherWidgetPayload } from \"@/components/tool-ui/weather-widget/runtime\";\nimport type { PresetWithCodeGen } from \"./types\";\n\nexport type WeatherWidgetPresetName =\n  | \"thunderstorm\"\n  | \"cold-snap\"\n  | \"rainy-week\"\n  | \"cloudy-sunset\"\n  | \"sunny-forecast\";\n\nfunction generateWeatherWidgetCode(data: WeatherWidgetPayload): string {\n  const props: string[] = [];\n\n  props.push('  version=\"3.1\"');\n  props.push(`  id=\"${data.id}\"`);\n  props.push(\n    `  location={${JSON.stringify(data.location, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n  props.push(\n    `  units={${JSON.stringify(data.units, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n  props.push(\n    `  current={${JSON.stringify(data.current, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n  props.push(\n    `  forecast={${JSON.stringify(data.forecast, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n  props.push(\n    `  time={${JSON.stringify(data.time, null, 4).replace(/\\n/g, \"\\n  \")}}`,\n  );\n\n  if (data.updatedAt) {\n    props.push(`  updatedAt=\"${data.updatedAt}\"`);\n  }\n\n  return `<WeatherWidget\\n${props.join(\"\\n\")}\\n/>`;\n}\n\nexport const weatherWidgetPresets: Record<\n  WeatherWidgetPresetName,\n  PresetWithCodeGen<WeatherWidgetPayload>\n> = {\n  thunderstorm: {\n    description: \"Kansas City having a night\",\n    data: {\n      version: \"3.1\",\n      id: \"weather-widget-thunderstorm\",\n      location: { name: \"Kansas City, MO\" },\n      units: { temperature: \"fahrenheit\" },\n      current: {\n        temperature: 72,\n        tempMin: 65,\n        tempMax: 78,\n        conditionCode: \"thunderstorm\",\n      },\n      forecast: [\n        { label: \"Tue\", tempMin: 62, tempMax: 75, conditionCode: \"heavy-rain\" },\n        { label: \"Wed\", tempMin: 58, tempMax: 70, conditionCode: \"rain\" },\n        { label: \"Thu\", tempMin: 55, tempMax: 68, conditionCode: \"cloudy\" },\n        {\n          label: \"Fri\",\n          tempMin: 52,\n          tempMax: 72,\n          conditionCode: \"partly-cloudy\",\n        },\n        { label: \"Sat\", tempMin: 58, tempMax: 76, conditionCode: \"clear\" },\n      ],\n      time: { localTimeOfDay: 22.5 / 24 },\n      updatedAt: \"2026-01-28T22:30:00Z\",\n    },\n    generateExampleCode: generateWeatherWidgetCode,\n  },\n  \"cold-snap\": {\n    description: \"Minneapolis doing Minneapolis things\",\n    data: {\n      version: \"3.1\",\n      id: \"weather-widget-cold-snap\",\n      location: { name: \"Minneapolis, MN\" },\n      units: { temperature: \"fahrenheit\" },\n      current: {\n        temperature: 18,\n        tempMin: 8,\n        tempMax: 22,\n        conditionCode: \"snow\",\n      },\n      forecast: [\n        { label: \"Tue\", tempMin: 5, tempMax: 19, conditionCode: \"snow\" },\n        { label: \"Wed\", tempMin: -2, tempMax: 12, conditionCode: \"snow\" },\n        { label: \"Thu\", tempMin: -8, tempMax: 6, conditionCode: \"cloudy\" },\n        {\n          label: \"Fri\",\n          tempMin: -5,\n          tempMax: 10,\n          conditionCode: \"partly-cloudy\",\n        },\n        { label: \"Sat\", tempMin: 2, tempMax: 18, conditionCode: \"clear\" },\n      ],\n      time: { localTimeOfDay: 21 / 24 },\n      updatedAt: \"2026-01-28T21:00:00Z\",\n    },\n    generateExampleCode: generateWeatherWidgetCode,\n  },\n  \"rainy-week\": {\n    description: \"Seattle, so... rain\",\n    data: {\n      version: \"3.1\",\n      id: \"weather-widget-rainy-week\",\n      location: { name: \"Seattle, WA\" },\n      units: { temperature: \"fahrenheit\" },\n      current: {\n        temperature: 52,\n        tempMin: 48,\n        tempMax: 55,\n        conditionCode: \"rain\",\n      },\n      forecast: [\n        { label: \"Mon\", tempMin: 46, tempMax: 54, conditionCode: \"drizzle\" },\n        { label: \"Tue\", tempMin: 47, tempMax: 53, conditionCode: \"rain\" },\n        { label: \"Wed\", tempMin: 45, tempMax: 52, conditionCode: \"heavy-rain\" },\n        { label: \"Thu\", tempMin: 44, tempMax: 51, conditionCode: \"rain\" },\n        { label: \"Fri\", tempMin: 46, tempMax: 55, conditionCode: \"cloudy\" },\n      ],\n      time: { localTimeOfDay: 16 / 24 },\n      updatedAt: \"2026-01-28T16:00:00Z\",\n    },\n    generateExampleCode: generateWeatherWidgetCode,\n  },\n  \"cloudy-sunset\": {\n    description: \"That golden hour cloud thing\",\n    data: {\n      version: \"3.1\",\n      id: \"weather-widget-cloudy-sunset\",\n      location: { name: \"Santa Fe, NM\" },\n      units: { temperature: \"fahrenheit\" },\n      current: {\n        temperature: 48,\n        tempMin: 32,\n        tempMax: 52,\n        conditionCode: \"overcast\",\n      },\n      forecast: [\n        { label: \"Tue\", tempMin: 28, tempMax: 49, conditionCode: \"cloudy\" },\n        {\n          label: \"Wed\",\n          tempMin: 30,\n          tempMax: 54,\n          conditionCode: \"partly-cloudy\",\n        },\n        { label: \"Thu\", tempMin: 33, tempMax: 58, conditionCode: \"clear\" },\n        {\n          label: \"Fri\",\n          tempMin: 35,\n          tempMax: 56,\n          conditionCode: \"partly-cloudy\",\n        },\n        { label: \"Sat\", tempMin: 31, tempMax: 51, conditionCode: \"cloudy\" },\n      ],\n      time: { localTimeOfDay: 17.75 / 24 },\n      updatedAt: \"2026-01-28T17:45:00Z\",\n    },\n    generateExampleCode: generateWeatherWidgetCode,\n  },\n  \"sunny-forecast\": {\n    description: \"San Diego being San Diego\",\n    data: {\n      version: \"3.1\",\n      id: \"weather-widget-sunny-forecast\",\n      location: { name: \"San Diego, CA\" },\n      units: { temperature: \"fahrenheit\" },\n      current: {\n        temperature: 76,\n        tempMin: 68,\n        tempMax: 79,\n        conditionCode: \"clear\",\n      },\n      forecast: [\n        { label: \"Tue\", tempMin: 65, tempMax: 78, conditionCode: \"clear\" },\n        { label: \"Wed\", tempMin: 66, tempMax: 81, conditionCode: \"clear\" },\n        {\n          label: \"Thu\",\n          tempMin: 67,\n          tempMax: 82,\n          conditionCode: \"partly-cloudy\",\n        },\n        { label: \"Fri\", tempMin: 68, tempMax: 80, conditionCode: \"clear\" },\n        {\n          label: \"Sat\",\n          tempMin: 64,\n          tempMax: 77,\n          conditionCode: \"partly-cloudy\",\n        },\n      ],\n      time: { localTimeOfDay: 14.5 / 24 },\n      updatedAt: \"2026-01-28T14:30:00Z\",\n    },\n    generateExampleCode: generateWeatherWidgetCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/presets/x-post.ts",
    "content": "import type { XPostData } from \"@/components/tool-ui/x-post\";\nimport type { ActionsProp } from \"@/components/tool-ui/shared\";\nimport type { PresetWithCodeGen } from \"./types\";\n\ninterface XPostPresetData {\n  post: XPostData;\n  localActions?: ActionsProp;\n}\n\nexport type XPostPresetName =\n  | \"basic\"\n  | \"quoted\"\n  | \"media\"\n  | \"link\"\n  | \"footer-actions\";\n\nfunction generateXPostCode(data: XPostPresetData): string {\n  const props: string[] = [];\n\n  props.push(\n    `  post={${JSON.stringify(data.post, null, 2).replace(/\\n/g, \"\\n  \")}}`,\n  );\n\n  const postCode = `<XPost\\n${props.join(\"\\n\")}\\n/>`;\n  if (!data.localActions) {\n    return postCode;\n  }\n\n  return `${postCode}\n<LocalActions\n  surfaceId=\"${data.post.id}\"\n  actions={${JSON.stringify(data.localActions, null, 2).replace(/\\n/g, \"\\n  \")}}\n  onAction={(actionId) => console.log(\"Local action:\", actionId)}\n/>`;\n}\n\nexport const xPostPresets: Record<\n  XPostPresetName,\n  PresetWithCodeGen<XPostPresetData>\n> = {\n  basic: {\n    description: \"Simple text post with stats\",\n    data: {\n      post: {\n        id: \"x-post-basic\",\n        author: {\n          name: \"Athia Zohra\",\n          handle: \"athiazohra\",\n          avatarUrl:\n            \"https://images.unsplash.com/photo-1753288695169-e51f5a3ff24f?w=200&h=200&fit=crop\",\n          verified: true,\n        },\n        text: \"Wild to think: in the 1940s we literally rewired programs by hand. Today, we ship apps worldwide with a single command.\",\n        stats: { likes: 5 },\n        createdAt: \"2025-11-05T14:01:00.000Z\",\n      },\n    } satisfies XPostPresetData,\n    generateExampleCode: generateXPostCode,\n  },\n  quoted: {\n    description: \"Post with quoted tweet\",\n    data: {\n      post: {\n        id: \"x-post-quoted\",\n        author: {\n          name: \"Athia Zohra\",\n          handle: \"athiazohra\",\n          avatarUrl:\n            \"https://images.unsplash.com/photo-1753288695169-e51f5a3ff24f?w=200&h=200&fit=crop\",\n          verified: true,\n        },\n        text: \"This is a great thread on the history of computing.\",\n        quotedPost: {\n          id: \"x-post-inner\",\n          author: {\n            name: \"CHM Archives\",\n            handle: \"computerhistory\",\n            avatarUrl: \"https://api.dicebear.com/7.x/avataaars/svg?seed=CHM\",\n            verified: true,\n          },\n          text: \"A look back at the transition from vacuum tubes to transistors to microprocessors — every leap shrank the hardware and expanded possibility.\",\n          createdAt: \"2025-11-05T13:00:00.000Z\",\n        },\n        stats: { likes: 5 },\n        createdAt: \"2025-11-05T14:01:00.000Z\",\n      },\n    } satisfies XPostPresetData,\n    generateExampleCode: generateXPostCode,\n  },\n  media: {\n    description: \"Post with image attachment\",\n    data: {\n      post: {\n        id: \"x-post-media\",\n        author: {\n          name: \"Tech Daily\",\n          handle: \"techdaily\",\n          avatarUrl: \"https://api.dicebear.com/7.x/shapes/svg?seed=tech\",\n          verified: true,\n        },\n        text: \"The new MacBook Pro is here. First impressions thread:\",\n        media: {\n          type: \"image\",\n          url: \"https://images.unsplash.com/photo-1517336714731-489689fd1ca8?w=800&h=450&fit=crop\",\n          alt: \"MacBook Pro on desk\",\n          aspectRatio: \"16:9\",\n        },\n        stats: { likes: 892 },\n        createdAt: \"2025-11-24T10:30:00.000Z\",\n      },\n    } satisfies XPostPresetData,\n    generateExampleCode: generateXPostCode,\n  },\n  link: {\n    description: \"Post with link preview card\",\n    data: {\n      post: {\n        id: \"x-post-link\",\n        author: {\n          name: \"Dev Weekly\",\n          handle: \"devweekly\",\n          avatarUrl: \"https://api.dicebear.com/7.x/shapes/svg?seed=devweekly\",\n        },\n        text: \"Great article on modern CSS techniques:\",\n        linkPreview: {\n          url: \"https://web.dev/articles/css-nesting\",\n          title: \"CSS Nesting\",\n          description:\n            \"Learn how to use native CSS nesting in your stylesheets.\",\n          imageUrl:\n            \"https://images.unsplash.com/photo-1507721999472-8ed4421c4af2?w=600&h=300&fit=crop\",\n          domain: \"web.dev\",\n        },\n        stats: { likes: 156 },\n        createdAt: \"2025-11-23T16:45:00.000Z\",\n      },\n    } satisfies XPostPresetData,\n    generateExampleCode: generateXPostCode,\n  },\n  \"footer-actions\": {\n    description: \"Post with external local actions\",\n    data: {\n      post: {\n        id: \"x-post-footer\",\n        author: {\n          name: \"DevOps Weekly\",\n          handle: \"devopsweekly\",\n          avatarUrl: \"https://api.dicebear.com/7.x/shapes/svg?seed=devops\",\n          verified: true,\n        },\n        text: \"Announcing our new CI/CD pipeline templates! Check them out and let us know what you think.\",\n        stats: { likes: 128 },\n        createdAt: \"2025-11-24T16:30:00.000Z\",\n      },\n      localActions: [\n        { id: \"report\", label: \"Report\", variant: \"destructive\" },\n        { id: \"view-templates\", label: \"View Templates\", variant: \"default\" },\n      ],\n    } satisfies XPostPresetData,\n    generateExampleCode: generateXPostCode,\n  },\n};\n"
  },
  {
    "path": "apps/www/lib/registry/tool-ui-registry.ts",
    "content": "import { existsSync, statSync, promises as fs } from \"fs\";\nimport path from \"path\";\nimport ts from \"typescript\";\n\ntype RegistryItemType =\n  | \"registry:block\"\n  | \"registry:component\"\n  | \"registry:lib\"\n  | \"registry:hook\"\n  | \"registry:ui\"\n  | \"registry:page\"\n  | \"registry:file\"\n  | \"registry:style\"\n  | \"registry:theme\"\n  | \"registry:item\";\n\nexport interface RegistryFile {\n  path: string;\n  type: RegistryItemType;\n  target?: string;\n  content?: string;\n}\n\nexport interface RegistryItem {\n  $schema: string;\n  name: string;\n  type: RegistryItemType;\n  title: string;\n  description: string;\n  dependencies?: string[];\n  registryDependencies?: string[];\n  files: RegistryFile[];\n}\n\nexport interface RegistryIndex {\n  $schema: string;\n  name: string;\n  homepage: string;\n  items: Array<Omit<RegistryItem, \"$schema\"> & { files?: RegistryFile[] }>;\n}\n\ninterface ToolUiRegistryDefinition {\n  name: string;\n  title: string;\n  description: string;\n  sourceDir: string;\n  entrypointFilePaths?: string[];\n}\n\nexport interface ToolUiRegistryArtifacts {\n  index: RegistryIndex;\n  items: RegistryItem[];\n}\n\nconst REGISTRY_SCHEMA = \"https://ui.shadcn.com/schema/registry.json\";\nconst REGISTRY_ITEM_SCHEMA = \"https://ui.shadcn.com/schema/registry-item.json\";\n\nconst IMPORT_META_URL_RE =\n  /new\\s+URL\\(\\s*[\"']([^\"']+)[\"']\\s*,\\s*import\\.meta\\.url\\s*\\)/g;\nconst TOOL_UI_COMPONENTS_DIR = \"components/tool-ui\";\nconst IGNORED_REGISTRY_FILE_NAMES = new Set([\".DS_Store\", \"Thumbs.db\"]);\nconst IMPORT_GRAPH_SOURCE_EXTENSIONS = new Set([\n  \".ts\",\n  \".tsx\",\n  \".js\",\n  \".jsx\",\n  \".mjs\",\n  \".cjs\",\n  \".mts\",\n  \".cts\",\n]);\nconst DEPENDENCY_VERSION_OVERRIDES: Record<string, string> = {\n  recharts: \"2.15.4\",\n};\n\nconst COMPONENT_DESCRIPTION_OVERRIDES: Partial<Record<string, string>> = {\n  \"approval-card\": \"Binary confirmation for agent actions.\",\n  audio: \"Audio playback with artwork and metadata.\",\n  chart: \"Visualize data with interactive charts.\",\n  citation: \"Display source references with attribution.\",\n  \"code-block\": \"Display syntax-highlighted code snippets.\",\n  \"data-table\": \"Sortable, responsive data tables for tool call results.\",\n  \"geo-map\": \"Display geolocated entities and fleet positions.\",\n  image: \"Display images with metadata and attribution.\",\n  \"image-gallery\": \"Grid layout for browsing image collections.\",\n  \"instagram-post\": \"Render Instagram post previews.\",\n  \"item-carousel\": \"Horizontal carousel for browsing collections.\",\n  \"link-preview\": \"Rich link previews with OG data.\",\n  \"linkedin-post\": \"Render LinkedIn post previews.\",\n  \"message-draft\": \"Review and confirm drafted messages before sending.\",\n  \"option-list\": \"Single or multi-select choices with confirmation actions.\",\n  \"order-summary\": \"Itemized purchase confirmation with pricing.\",\n  \"parameter-slider\": \"Numeric parameter adjustment controls.\",\n  plan: \"Display step-by-step task workflows in AI interfaces.\",\n  \"preferences-panel\": \"Compact settings panel for user preferences.\",\n  \"progress-tracker\":\n    \"Show real-time status feedback for multi-step operations in AI interfaces.\",\n  \"question-flow\": \"Multi-step guided questions with branching.\",\n  \"stats-display\": \"Display key metrics in compact cards.\",\n  terminal: \"Show command-line output and logs.\",\n  video: \"Video playback with controls and poster.\",\n  \"weather-widget\": \"Display weather conditions and forecasts.\",\n  \"x-post\": \"Render X (Twitter) post previews.\",\n};\nconst COMPONENT_ENTRYPOINT_OVERRIDES: Partial<Record<string, string[]>> = {\n  \"weather-widget\": [\"runtime.ts\"],\n};\n\nfunction inferRegistryFileType(filePath: string): RegistryItemType {\n  if (filePath.endsWith(\".tsx\")) return \"registry:component\";\n  if (filePath.endsWith(\".ts\")) return \"registry:lib\";\n  if (filePath.endsWith(\".css\")) return \"registry:style\";\n  return \"registry:file\";\n}\n\nasync function listFilesRecursively(dirPath: string): Promise<string[]> {\n  const entries = await fs.readdir(dirPath, { withFileTypes: true });\n  const filePaths: string[] = [];\n\n  for (const entry of entries) {\n    if (IGNORED_REGISTRY_FILE_NAMES.has(entry.name)) {\n      continue;\n    }\n\n    const entryPath = path.join(dirPath, entry.name);\n    if (entry.isDirectory()) {\n      filePaths.push(...(await listFilesRecursively(entryPath)));\n      continue;\n    }\n    if (entry.isFile()) {\n      filePaths.push(entryPath);\n    }\n  }\n\n  return filePaths.sort((a, b) => a.localeCompare(b));\n}\n\nfunction toPosixPath(filePath: string): string {\n  return filePath.split(path.sep).join(path.posix.sep);\n}\n\nfunction shouldTraverseImportGraph(filePath: string): boolean {\n  return IMPORT_GRAPH_SOURCE_EXTENSIONS.has(path.extname(filePath));\n}\n\nfunction resolveLocalImport(\n  projectRoot: string,\n  fromAbsolutePath: string,\n  specifier: string,\n): string | null {\n  if (!specifier.startsWith(\".\") && !specifier.startsWith(\"@/\")) {\n    return null;\n  }\n\n  const basePath = specifier.startsWith(\"@/\")\n    ? path.join(projectRoot, specifier.slice(2))\n    : path.resolve(path.dirname(fromAbsolutePath), specifier);\n  const candidates = [\n    `${basePath}.ts`,\n    `${basePath}.tsx`,\n    path.join(basePath, \"index.ts\"),\n    path.join(basePath, \"index.tsx\"),\n    basePath,\n  ];\n\n  for (const candidate of candidates) {\n    if (!existsSync(candidate)) {\n      continue;\n    }\n\n    if (path.extname(candidate) || statSync(candidate).isFile()) {\n      return candidate;\n    }\n\n    const indexTs = path.join(candidate, \"index.ts\");\n    const indexTsx = path.join(candidate, \"index.tsx\");\n    if (existsSync(indexTs)) return indexTs;\n    if (existsSync(indexTsx)) return indexTsx;\n  }\n\n  return null;\n}\n\nfunction extractImportSpecifiers(\n  content: string,\n  options?: { includeTypeOnly?: boolean },\n): string[] {\n  const includeTypeOnly = options?.includeTypeOnly ?? false;\n  const imports = new Set<string>();\n  const sourceFile = ts.createSourceFile(\n    \"registry-import-scan.tsx\",\n    content,\n    ts.ScriptTarget.Latest,\n    true,\n    ts.ScriptKind.TSX,\n  );\n\n  const isTypeOnlyImport = (clause: ts.ImportClause | undefined): boolean => {\n    if (!clause) return false;\n    if (clause.isTypeOnly) return true;\n    if (clause.name) return false;\n    if (!clause.namedBindings) return false;\n    if (ts.isNamespaceImport(clause.namedBindings)) return false;\n    if (clause.namedBindings.elements.length === 0) return false;\n    return clause.namedBindings.elements.every((element) => element.isTypeOnly);\n  };\n\n  const visit = (node: ts.Node) => {\n    if (\n      ts.isImportDeclaration(node) &&\n      ts.isStringLiteral(node.moduleSpecifier) &&\n      (includeTypeOnly || !isTypeOnlyImport(node.importClause))\n    ) {\n      imports.add(node.moduleSpecifier.text);\n    }\n\n    if (\n      ts.isExportDeclaration(node) &&\n      node.moduleSpecifier &&\n      ts.isStringLiteral(node.moduleSpecifier) &&\n      (includeTypeOnly || !node.isTypeOnly)\n    ) {\n      imports.add(node.moduleSpecifier.text);\n    }\n\n    if (\n      ts.isCallExpression(node) &&\n      node.expression.kind === ts.SyntaxKind.ImportKeyword &&\n      node.arguments.length === 1 &&\n      ts.isStringLiteral(node.arguments[0])\n    ) {\n      imports.add(node.arguments[0].text);\n    }\n\n    ts.forEachChild(node, visit);\n  };\n\n  ts.forEachChild(sourceFile, visit);\n\n  return Array.from(imports);\n}\n\nfunction extractImportMetaUrlSpecifiers(content: string): string[] {\n  const imports = new Set<string>();\n  IMPORT_META_URL_RE.lastIndex = 0;\n  let match: RegExpExecArray | null;\n\n  while ((match = IMPORT_META_URL_RE.exec(content)) !== null) {\n    const specifier = match[1];\n    imports.add(specifier);\n  }\n\n  return Array.from(imports);\n}\n\nfunction extractLocalImportSpecifiers(content: string): string[] {\n  const imports = new Set<string>();\n\n  for (const specifier of extractImportSpecifiers(content, {\n    includeTypeOnly: true,\n  })) {\n    if (specifier.startsWith(\".\") || specifier.startsWith(\"@/\")) {\n      imports.add(specifier);\n    }\n  }\n\n  for (const specifier of extractImportMetaUrlSpecifiers(content)) {\n    if (specifier.startsWith(\".\") || specifier.startsWith(\"@/\")) {\n      imports.add(specifier);\n    }\n  }\n\n  return Array.from(imports);\n}\n\nfunction toTitleCase(name: string): string {\n  return name\n    .split(\"-\")\n    .filter(Boolean)\n    .map((segment) => segment[0].toUpperCase() + segment.slice(1))\n    .join(\" \");\n}\n\nfunction toPackageName(specifier: string): string | null {\n  if (\n    specifier.startsWith(\".\") ||\n    specifier.startsWith(\"/\") ||\n    specifier.startsWith(\"@/\") ||\n    specifier.startsWith(\"node:\")\n  ) {\n    return null;\n  }\n\n  if (specifier.startsWith(\"@\")) {\n    const [scope, pkg] = specifier.split(\"/\");\n    return scope && pkg ? `${scope}/${pkg}` : null;\n  }\n\n  const [pkg] = specifier.split(\"/\");\n  return pkg || null;\n}\n\nfunction toRegistryDependency(specifier: string): string | null {\n  if (!specifier.startsWith(\"@/components/ui/\")) return null;\n  const value = specifier.replace(\"@/components/ui/\", \"\").split(\"/\")[0];\n  return value || null;\n}\n\nfunction toRegistryDependencyFromResolvedPath(\n  relativePath: string,\n): string | null {\n  if (!relativePath.startsWith(\"components/ui/\")) return null;\n  const value = relativePath\n    .replace(\"components/ui/\", \"\")\n    .split(\"/\")[0]\n    .replace(/\\.[^.]+$/, \"\");\n  return value || null;\n}\n\nfunction withPinnedVersion(pkg: string): string {\n  const pinnedVersion = DEPENDENCY_VERSION_OVERRIDES[pkg];\n  return pinnedVersion ? `${pkg}@${pinnedVersion}` : pkg;\n}\n\nasync function discoverToolUiRegistryDefinitions(\n  projectRoot: string,\n): Promise<ToolUiRegistryDefinition[]> {\n  const absoluteDir = path.join(projectRoot, TOOL_UI_COMPONENTS_DIR);\n  const entries = await fs.readdir(absoluteDir, { withFileTypes: true });\n\n  return entries\n    .filter((entry) => entry.isDirectory())\n    .map((entry) => entry.name)\n    .filter((name) => name !== \"shared\")\n    .sort((a, b) => a.localeCompare(b))\n    .map((name) => {\n      const title = toTitleCase(name);\n      const description =\n        COMPONENT_DESCRIPTION_OVERRIDES[name] ??\n        `${title} component for AI interfaces.`;\n      return {\n        name,\n        title,\n        description,\n        sourceDir: `${TOOL_UI_COMPONENTS_DIR}/${name}`,\n        entrypointFilePaths: COMPONENT_ENTRYPOINT_OVERRIDES[name]?.map(\n          (entrypoint) => `${TOOL_UI_COMPONENTS_DIR}/${name}/${entrypoint}`,\n        ),\n      };\n    });\n}\n\nfunction shouldIncludeResolvedPath(\n  relativePath: string,\n  sourceDir: string,\n): boolean {\n  if (relativePath.startsWith(`${sourceDir}/`)) return true;\n  return relativePath.startsWith(\"components/tool-ui/shared/\");\n}\n\nasync function collectItemFilePaths(\n  projectRoot: string,\n  definition: ToolUiRegistryDefinition,\n): Promise<string[]> {\n  const absoluteSourceDir = path.join(projectRoot, definition.sourceDir);\n  const componentFilePaths =\n    definition.entrypointFilePaths && definition.entrypointFilePaths.length > 0\n      ? definition.entrypointFilePaths.map((entrypointPath) =>\n          path.join(projectRoot, entrypointPath),\n        )\n      : await listFilesRecursively(absoluteSourceDir);\n\n  const included = new Set(componentFilePaths);\n  const queue = [...componentFilePaths];\n  const seen = new Set<string>();\n\n  while (queue.length > 0) {\n    const absolutePath = queue.pop() as string;\n    if (seen.has(absolutePath)) continue;\n    seen.add(absolutePath);\n    if (!shouldTraverseImportGraph(absolutePath)) continue;\n\n    const content = await fs.readFile(absolutePath, \"utf8\");\n    const importSpecifiers = extractLocalImportSpecifiers(content);\n\n    for (const specifier of importSpecifiers) {\n      const resolvedPath = resolveLocalImport(\n        projectRoot,\n        absolutePath,\n        specifier,\n      );\n      if (!resolvedPath) continue;\n\n      const relativePath = toPosixPath(\n        path.relative(projectRoot, resolvedPath),\n      );\n      if (!shouldIncludeResolvedPath(relativePath, definition.sourceDir))\n        continue;\n\n      if (!included.has(resolvedPath)) {\n        included.add(resolvedPath);\n        queue.push(resolvedPath);\n      }\n    }\n  }\n\n  return Array.from(included)\n    .map((absolutePath) =>\n      toPosixPath(path.relative(projectRoot, absolutePath)),\n    )\n    .sort((a, b) => a.localeCompare(b));\n}\n\nasync function buildRegistryItem(\n  projectRoot: string,\n  definition: ToolUiRegistryDefinition,\n): Promise<RegistryItem> {\n  const relativeFilePaths = await collectItemFilePaths(projectRoot, definition);\n  const dependencySet = new Set<string>();\n  const registryDependencySet = new Set<string>();\n\n  const files = await Promise.all(\n    relativeFilePaths.map(async (relativePath): Promise<RegistryFile> => {\n      const absolutePath = path.join(projectRoot, relativePath);\n      const content = await fs.readFile(absolutePath, \"utf8\");\n\n      const importSpecifiers = extractImportSpecifiers(content);\n      for (const specifier of importSpecifiers) {\n        const pkg = toPackageName(specifier);\n        if (pkg && pkg !== \"react\" && pkg !== \"react-dom\") {\n          dependencySet.add(withPinnedVersion(pkg));\n        }\n\n        let registryDependency = toRegistryDependency(specifier);\n        if (\n          !registryDependency &&\n          (specifier.startsWith(\".\") || specifier.startsWith(\"@/\"))\n        ) {\n          const resolvedPath = resolveLocalImport(\n            projectRoot,\n            absolutePath,\n            specifier,\n          );\n          if (resolvedPath) {\n            const resolvedRelativePath = toPosixPath(\n              path.relative(projectRoot, resolvedPath),\n            );\n            registryDependency =\n              toRegistryDependencyFromResolvedPath(resolvedRelativePath);\n          }\n        }\n        if (registryDependency) {\n          registryDependencySet.add(registryDependency);\n        }\n      }\n\n      return {\n        path: relativePath,\n        type: inferRegistryFileType(relativePath),\n        target: relativePath,\n        content,\n      };\n    }),\n  );\n\n  return {\n    $schema: REGISTRY_ITEM_SCHEMA,\n    name: definition.name,\n    type: \"registry:block\",\n    title: definition.title,\n    description: definition.description,\n    dependencies:\n      dependencySet.size > 0\n        ? Array.from(dependencySet).sort((a, b) => a.localeCompare(b))\n        : undefined,\n    registryDependencies:\n      registryDependencySet.size > 0\n        ? Array.from(registryDependencySet).sort((a, b) => a.localeCompare(b))\n        : undefined,\n    files,\n  };\n}\n\nfunction toIndexItem(item: RegistryItem): RegistryIndex[\"items\"][number] {\n  return {\n    name: item.name,\n    type: item.type,\n    title: item.title,\n    description: item.description,\n    dependencies: item.dependencies,\n    registryDependencies: item.registryDependencies,\n    files: item.files.map(({ path: filePath, type, target }) => ({\n      path: filePath,\n      type,\n      ...(target ? { target } : {}),\n    })),\n  };\n}\n\nexport async function buildToolUiRegistryArtifacts(\n  projectRoot: string,\n): Promise<ToolUiRegistryArtifacts> {\n  const definitions = await discoverToolUiRegistryDefinitions(projectRoot);\n  const items = await Promise.all(\n    definitions.map((definition) => buildRegistryItem(projectRoot, definition)),\n  );\n\n  const index: RegistryIndex = {\n    $schema: REGISTRY_SCHEMA,\n    name: \"tool-ui\",\n    homepage: \"https://tool-ui.com\",\n    items: items.map(toIndexItem),\n  };\n\n  return {\n    index,\n    items,\n  };\n}\n\nexport async function writeToolUiRegistryArtifacts(\n  projectRoot: string,\n): Promise<ToolUiRegistryArtifacts> {\n  const artifacts = await buildToolUiRegistryArtifacts(projectRoot);\n  const outputDir = path.join(projectRoot, \"public\", \"r\");\n  await fs.mkdir(outputDir, { recursive: true });\n\n  const expectedFiles = new Set([\n    \"registry.json\",\n    ...artifacts.items.map((item) => `${item.name}.json`),\n  ]);\n  const existingFiles = await fs.readdir(outputDir);\n  await Promise.all(\n    existingFiles\n      .filter((fileName) => fileName.endsWith(\".json\"))\n      .filter((fileName) => !expectedFiles.has(fileName))\n      .map((fileName) => fs.unlink(path.join(outputDir, fileName))),\n  );\n\n  await fs.writeFile(\n    path.join(outputDir, \"registry.json\"),\n    `${JSON.stringify(artifacts.index, null, 2)}\\n`,\n    \"utf8\",\n  );\n\n  await Promise.all(\n    artifacts.items.map((item) =>\n      fs.writeFile(\n        path.join(outputDir, `${item.name}.json`),\n        `${JSON.stringify(item, null, 2)}\\n`,\n        \"utf8\",\n      ),\n    ),\n  );\n\n  return artifacts;\n}\n"
  },
  {
    "path": "apps/www/lib/staging/configs/parameter-slider.tsx",
    "content": "\"use client\";\n\nimport { Fragment, type RefObject } from \"react\";\nimport type { StagingConfig, DebugLevel } from \"../types\";\nimport {\n  RoundedRectOverlay,\n  ThumbIndicator,\n} from \"@/app/staging/_components/rounded-rect-overlay\";\n\ninterface SliderElementRects {\n  label: DOMRect | null;\n  value: DOMRect | null;\n  thumb: DOMRect | null;\n  track: DOMRect | null;\n}\n\nfunction getSliderElements(\n  containerRef: RefObject<HTMLElement | null>,\n): SliderElementRects[] {\n  const container = containerRef.current;\n  if (!container) return [];\n\n  // Find the main parameter-slider article - it might be the container itself or a child\n  const slider =\n    container.getAttribute(\"data-slot\") === \"parameter-slider\"\n      ? container\n      : container.querySelector('[data-slot=\"parameter-slider\"]');\n\n  if (!slider) return [];\n\n  // Find all Radix slider root elements - they have the group/slider class and role=group\n  // Radix adds data-orientation to the Root element\n  const sliderRoots = slider.querySelectorAll(\"[data-orientation]\");\n\n  if (sliderRoots.length === 0) return [];\n\n  return Array.from(sliderRoots).map((root) => {\n    // Find the thumb (has role=\"slider\")\n    const thumb = root.querySelector('[role=\"slider\"]');\n\n    // The label/value overlay is the last div child of root (after Track and Thumb)\n    // It contains 2 span children for label and value\n    const divChildren = Array.from(root.children).filter(\n      (child) => child.tagName === \"DIV\",\n    );\n    const overlayDiv = divChildren.find((div) => {\n      const spans = div.querySelectorAll(\":scope > span\");\n      return spans.length === 2;\n    });\n\n    let labelSpan: Element | null = null;\n    let valueSpan: Element | null = null;\n\n    if (overlayDiv) {\n      const spans = overlayDiv.querySelectorAll(\":scope > span\");\n      labelSpan = spans[0] ?? null;\n      valueSpan = spans[1] ?? null;\n    }\n\n    return {\n      label: labelSpan?.getBoundingClientRect() ?? null,\n      value: valueSpan?.getBoundingClientRect() ?? null,\n      thumb: thumb?.getBoundingClientRect() ?? null,\n      track: root.getBoundingClientRect(),\n    };\n  });\n}\n\nfunction ParameterSliderDebugOverlay({\n  level,\n  componentRef,\n}: {\n  level: DebugLevel;\n  componentRef: RefObject<HTMLElement | null>;\n}) {\n  const container = componentRef.current;\n  if (!container) {\n    return (\n      <div className=\"absolute top-2 left-2 rounded bg-yellow-500/80 px-2 py-1 text-xs text-black\">\n        Debug: No container ref\n      </div>\n    );\n  }\n\n  const containerRect = container.getBoundingClientRect();\n  const sliderElements = getSliderElements(componentRef);\n\n  if (sliderElements.length === 0) {\n    return (\n      <div className=\"absolute top-2 left-2 rounded bg-red-500/80 px-2 py-1 text-xs text-white\">\n        Debug: No slider elements found in container\n      </div>\n    );\n  }\n\n  return (\n    <>\n      {sliderElements.map((row, index) => (\n        <Fragment key={index}>\n          {row.label && (\n            <RoundedRectOverlay\n              rect={row.label}\n              containerRect={containerRect}\n              padding={4}\n              paddingOuter={0}\n              color=\"blue\"\n              showMargin={level === \"margins\" || level === \"full\"}\n              marginSize={12}\n              marginSizeOuter={4}\n              outerEdgeRadiusFactor={0.3}\n              isLeftAligned={true}\n            />\n          )}\n\n          {row.value && (\n            <RoundedRectOverlay\n              rect={row.value}\n              containerRect={containerRect}\n              padding={4}\n              paddingOuter={0}\n              color=\"green\"\n              showMargin={level === \"margins\" || level === \"full\"}\n              marginSize={12}\n              marginSizeOuter={4}\n              outerEdgeRadiusFactor={0.3}\n              isLeftAligned={false}\n            />\n          )}\n\n          {level === \"full\" && row.thumb && (\n            <ThumbIndicator rect={row.thumb} containerRect={containerRect} />\n          )}\n        </Fragment>\n      ))}\n    </>\n  );\n}\n\nexport const parameterSliderStagingConfig: StagingConfig = {\n  supportedDebugLevels: [\"off\", \"boundaries\", \"margins\", \"full\"],\n  renderDebugOverlay: ({ level, componentRef }) => (\n    <ParameterSliderDebugOverlay level={level} componentRef={componentRef} />\n  ),\n};\n"
  },
  {
    "path": "apps/www/lib/staging/configs/plan.tsx",
    "content": "\"use client\";\n\nimport { useState, useEffect } from \"react\";\nimport type { PlanTodo } from \"@/components/tool-ui/plan/schema\";\nimport { Plan } from \"@/components/tool-ui/plan\";\nimport { Button } from \"@/components/ui/button\";\nimport type { StagingConfig } from \"../types\";\n\nconst SAMPLE_TODOS: PlanTodo[] = [\n  {\n    id: \"1\",\n    label: \"Design component API\",\n    description: \"Define props interface and behavior\",\n    status: \"completed\",\n  },\n  {\n    id: \"2\",\n    label: \"Implement core functionality\",\n    description: \"Build main component logic with TypeScript\",\n    status: \"in_progress\",\n  },\n  {\n    id: \"3\",\n    label: \"Add unit tests\",\n    description: \"Write comprehensive test coverage\",\n    status: \"pending\",\n  },\n  {\n    id: \"4\",\n    label: \"Update documentation\",\n    description: \"Create usage examples and API docs\",\n    status: \"pending\",\n  },\n];\n\nfunction PlanTuningPanel() {\n  const [todos, setTodos] = useState<PlanTodo[]>(SAMPLE_TODOS);\n  const [isAnimating, setIsAnimating] = useState(false);\n  const [currentStep, setCurrentStep] = useState(1);\n\n  // Auto-play animation\n  useEffect(() => {\n    if (!isAnimating || currentStep >= todos.length) {\n      if (currentStep >= todos.length) {\n        setIsAnimating(false);\n      }\n      return;\n    }\n\n    const timer = setTimeout(() => {\n      setTodos((prev) => {\n        const updated = [...prev];\n\n        if (currentStep > 0) {\n          updated[currentStep - 1] = {\n            ...updated[currentStep - 1],\n            status: \"completed\",\n          };\n        }\n\n        updated[currentStep] = {\n          ...updated[currentStep],\n          status: \"in_progress\",\n        };\n\n        return updated;\n      });\n\n      setCurrentStep((prev) => prev + 1);\n    }, 1500);\n\n    return () => clearTimeout(timer);\n  }, [isAnimating, currentStep, todos.length]);\n\n  const handlePlay = () => {\n    setIsAnimating(true);\n    if (currentStep >= todos.length) {\n      setCurrentStep(1);\n      setTodos(SAMPLE_TODOS);\n    }\n  };\n\n  const handleReset = () => {\n    setIsAnimating(false);\n    setCurrentStep(1);\n    setTodos(SAMPLE_TODOS);\n  };\n\n  const handleCompleteAll = () => {\n    setIsAnimating(false);\n    setTodos((prev) =>\n      prev.map((todo) => ({ ...todo, status: \"completed\" as const })),\n    );\n  };\n\n  const handleAddTodo = () => {\n    const newTodo: PlanTodo = {\n      id: `${todos.length + 1}`,\n      label: `New task ${todos.length + 1}`,\n      description: \"Dynamically added todo item\",\n      status: \"pending\",\n    };\n    setTodos((prev) => [...prev, newTodo]);\n  };\n\n  const handleRemoveTodo = () => {\n    if (todos.length > 1) {\n      setTodos((prev) => prev.slice(0, -1));\n    }\n  };\n\n  const handleSetStatus = (todoId: string, status: PlanTodo[\"status\"]) => {\n    setIsAnimating(false);\n    setTodos((prev) =>\n      prev.map((todo) => (todo.id === todoId ? { ...todo, status } : todo)),\n    );\n  };\n\n  const completedCount = todos.filter((t) => t.status === \"completed\").length;\n  const progress = Math.round((completedCount / todos.length) * 100);\n\n  return (\n    <div className=\"flex w-full max-w-6xl flex-col gap-8\">\n      <Plan\n        id=\"staging-plan\"\n        title=\"Animation Testing\"\n        description=\"Interactive controls for testing all animation states\"\n        todos={todos}\n      />\n\n      <div className=\"space-y-6 border-t pt-6\">\n        <div>\n          <h3 className=\"mb-3 text-sm font-semibold\">\n            Animation Controls\n            <span className=\"ml-2 text-xs font-normal text-muted-foreground\">\n              (Progress: {completedCount}/{todos.length} = {progress}%)\n            </span>\n          </h3>\n          <div className=\"flex flex-wrap gap-2\">\n            <Button onClick={handlePlay} disabled={isAnimating} size=\"sm\">\n              {currentStep >= todos.length ? \"Replay\" : \"Play\"}\n            </Button>\n            <Button onClick={handleReset} variant=\"outline\" size=\"sm\">\n              Reset\n            </Button>\n            <Button onClick={handleCompleteAll} variant=\"outline\" size=\"sm\">\n              Complete All\n            </Button>\n            <Button onClick={handleAddTodo} variant=\"outline\" size=\"sm\">\n              Add Todo\n            </Button>\n            <Button\n              onClick={handleRemoveTodo}\n              variant=\"outline\"\n              size=\"sm\"\n              disabled={todos.length <= 1}\n            >\n              Remove Todo\n            </Button>\n          </div>\n        </div>\n\n        <div className=\"space-y-4\">\n          <h3 className=\"text-sm font-semibold\">Individual Todo Controls</h3>\n          <div className=\"grid gap-3\">\n            {todos.map((todo) => (\n              <div\n                key={todo.id}\n                className=\"flex items-center justify-between rounded-lg border p-3\"\n              >\n                <div className=\"flex-1\">\n                  <div className=\"font-medium\">{todo.label}</div>\n                  {todo.description && (\n                    <div className=\"text-xs text-muted-foreground\">\n                      {todo.description}\n                    </div>\n                  )}\n                </div>\n                <div className=\"flex gap-1\">\n                  <Button\n                    onClick={() => handleSetStatus(todo.id, \"pending\")}\n                    variant={todo.status === \"pending\" ? \"default\" : \"outline\"}\n                    size=\"sm\"\n                  >\n                    Pending\n                  </Button>\n                  <Button\n                    onClick={() => handleSetStatus(todo.id, \"in_progress\")}\n                    variant={\n                      todo.status === \"in_progress\" ? \"default\" : \"outline\"\n                    }\n                    size=\"sm\"\n                  >\n                    In Progress\n                  </Button>\n                  <Button\n                    onClick={() => handleSetStatus(todo.id, \"completed\")}\n                    variant={\n                      todo.status === \"completed\" ? \"default\" : \"outline\"\n                    }\n                    size=\"sm\"\n                  >\n                    Completed\n                  </Button>\n                  <Button\n                    onClick={() => handleSetStatus(todo.id, \"cancelled\")}\n                    variant={\n                      todo.status === \"cancelled\" ? \"destructive\" : \"outline\"\n                    }\n                    size=\"sm\"\n                  >\n                    Cancelled\n                  </Button>\n                </div>\n              </div>\n            ))}\n          </div>\n        </div>\n\n        <div className=\"rounded-lg bg-muted p-4\">\n          <h4 className=\"mb-2 text-sm font-semibold\">Animation Features</h4>\n          <ul className=\"space-y-1 text-sm text-muted-foreground\">\n            <li>• Spring bounce on completion/cancellation icons</li>\n            <li>• Stroke-drawing animation for checkmarks and X icons</li>\n            <li>• Fast 0.7s spinner for in-progress state</li>\n            <li>• Shimmer effect on in-progress labels</li>\n            <li>• Staggered entrance for new todos (50ms between items)</li>\n            <li>• Progress bar celebration: shimmer + glow + pulse at 100%</li>\n            <li>• Bouncy chevron rotation with spring physics</li>\n            <li>• Staggered content reveal in accordion (100-150ms delays)</li>\n            <li>• All animations respect prefers-reduced-motion setting</li>\n          </ul>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport const planStagingConfig: StagingConfig = {\n  supportedDebugLevels: [\"off\"],\n  renderTuningPanel: () => <PlanTuningPanel />,\n};\n"
  },
  {
    "path": "apps/www/lib/staging/configs/progress-tracker.tsx",
    "content": "\"use client\";\n\nimport { useState, useEffect } from \"react\";\nimport type { StagingConfig } from \"../types\";\nimport { ProgressTracker } from \"@/components/tool-ui/progress-tracker\";\nimport type { ProgressStep } from \"@/components/tool-ui/progress-tracker/schema\";\nimport { Button } from \"@/components/ui/button\";\n\nconst SAMPLE_STEPS: ProgressStep[] = [\n  {\n    id: \"build\",\n    label: \"Building\",\n    description: \"Compiling TypeScript and bundling assets\",\n    status: \"completed\",\n  },\n  {\n    id: \"test\",\n    label: \"Running Tests\",\n    description: \"84 tests across 12 suites\",\n    status: \"in-progress\",\n  },\n  {\n    id: \"deploy\",\n    label: \"Deploy to Production\",\n    description: \"Upload to edge nodes\",\n    status: \"pending\",\n  },\n];\n\nfunction ProgressTrackerTuningPanel() {\n  const [steps, setSteps] = useState<ProgressStep[]>(SAMPLE_STEPS);\n  const [elapsedTime, setElapsedTime] = useState(38400);\n  const [isAnimating, setIsAnimating] = useState(false);\n  const [currentStepIndex, setCurrentStepIndex] = useState(1);\n\n  useEffect(() => {\n    if (!isAnimating) return;\n\n    if (currentStepIndex >= steps.length) {\n      setIsAnimating(false);\n      return;\n    }\n\n    const timeoutId = window.setTimeout(() => {\n      setSteps((prev) =>\n        prev.map((step, index) => ({\n          ...step,\n          status:\n            index < currentStepIndex\n              ? \"completed\"\n              : index === currentStepIndex\n                ? \"in-progress\"\n                : \"pending\",\n        })),\n      );\n\n      setElapsedTime((prev) => prev + 12000);\n      setCurrentStepIndex((prev) => prev + 1);\n    }, 1500);\n\n    return () => window.clearTimeout(timeoutId);\n  }, [isAnimating, currentStepIndex, steps.length]);\n\n  const startAnimation = () => {\n    setCurrentStepIndex(0);\n    setElapsedTime(38400);\n    setSteps(\n      SAMPLE_STEPS.map((step, index) => ({\n        ...step,\n        status: index === 0 ? \"in-progress\" : \"pending\",\n      })),\n    );\n    setIsAnimating(true);\n  };\n\n  const resetToDefault = () => {\n    setIsAnimating(false);\n    setCurrentStepIndex(1);\n    setElapsedTime(38400);\n    setSteps(SAMPLE_STEPS);\n  };\n\n  const setAllCompleted = () => {\n    setIsAnimating(false);\n    setSteps((prev) => prev.map((step) => ({ ...step, status: \"completed\" })));\n  };\n\n  const setStepStatus = (stepId: string, status: ProgressStep[\"status\"]) => {\n    setIsAnimating(false);\n    setSteps((prev) =>\n      prev.map((step) => (step.id === stepId ? { ...step, status } : step)),\n    );\n  };\n\n  return (\n    <div className=\"flex w-full max-w-6xl flex-col gap-8\">\n      <div className=\"flex w-full justify-center\">\n        <ProgressTracker\n          id=\"staging-progress-tracker\"\n          steps={steps}\n          elapsedTime={elapsedTime}\n        />\n      </div>\n\n      <div className=\"border-t pt-6\">\n        <div className=\"mb-4 flex items-center justify-between\">\n          <h3 className=\"text-lg font-semibold\">Animation Controls</h3>\n          <div className=\"flex gap-2\">\n            <Button onClick={startAnimation} disabled={isAnimating} size=\"sm\">\n              {isAnimating ? \"Animating...\" : \"Play Animation\"}\n            </Button>\n            <Button onClick={resetToDefault} variant=\"outline\" size=\"sm\">\n              Reset\n            </Button>\n            <Button onClick={setAllCompleted} variant=\"outline\" size=\"sm\">\n              Complete All\n            </Button>\n          </div>\n        </div>\n\n        <div className=\"grid gap-4 sm:grid-cols-3\">\n          {steps.map((step) => (\n            <div key={step.id} className=\"rounded-lg border p-4\">\n              <div className=\"mb-3 text-sm font-medium\">{step.label}</div>\n              <div className=\"flex flex-col gap-2\">\n                <Button\n                  onClick={() => setStepStatus(step.id, \"pending\")}\n                  variant={step.status === \"pending\" ? \"default\" : \"outline\"}\n                  size=\"sm\"\n                  className=\"w-full\"\n                >\n                  Pending\n                </Button>\n                <Button\n                  onClick={() => setStepStatus(step.id, \"in-progress\")}\n                  variant={\n                    step.status === \"in-progress\" ? \"default\" : \"outline\"\n                  }\n                  size=\"sm\"\n                  className=\"w-full\"\n                >\n                  In Progress\n                </Button>\n                <Button\n                  onClick={() => setStepStatus(step.id, \"completed\")}\n                  variant={step.status === \"completed\" ? \"default\" : \"outline\"}\n                  size=\"sm\"\n                  className=\"w-full\"\n                >\n                  Completed\n                </Button>\n                <Button\n                  onClick={() => setStepStatus(step.id, \"failed\")}\n                  variant={step.status === \"failed\" ? \"destructive\" : \"outline\"}\n                  size=\"sm\"\n                  className=\"w-full\"\n                >\n                  Failed\n                </Button>\n              </div>\n            </div>\n          ))}\n        </div>\n\n        <div className=\"mt-6 rounded-lg bg-muted p-4\">\n          <h4 className=\"mb-2 text-sm font-medium\">Animation Features</h4>\n          <ul className=\"text-muted-foreground space-y-1 text-sm\">\n            <li>\n              • Spring bounce animation on completion with cubic-bezier timing\n            </li>\n            <li>• Checkmark draw-in effect with stroke animation</li>\n            <li>• Shimmer effect on active step background</li>\n            <li>• 300ms smooth transitions between states</li>\n            <li>• Real-time elapsed time updates</li>\n          </ul>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport const progressTrackerStagingConfig: StagingConfig = {\n  supportedDebugLevels: [\"off\"],\n  renderTuningPanel: () => <ProgressTrackerTuningPanel />,\n};\n"
  },
  {
    "path": "apps/www/lib/staging/configs/stats-display.tsx",
    "content": "\"use client\";\n\nimport {\n  useState,\n  useId,\n  useCallback,\n  useEffect,\n  type CSSProperties,\n} from \"react\";\nimport { useControls, button, Leva } from \"leva\";\nimport type { StagingConfig } from \"../types\";\nimport {\n  cn,\n  Card,\n  CardHeader,\n  CardTitle,\n  CardDescription,\n  CardContent,\n} from \"@/components/tool-ui/stats-display/_adapter\";\nimport type {\n  StatsDisplayProps,\n  StatItem,\n  StatFormat,\n  StatDiff,\n} from \"@/components/tool-ui/stats-display/schema\";\n\ninterface SparklineAnimationParams {\n  slideDuration: number;\n  slideDistance: number;\n  fadeDuration: number;\n  fillFadeDuration: number;\n  fillOpacity: number;\n  baseStrokeOpacity: number;\n  glintDuration: number;\n  glintDelay: number;\n  glintDashSize: number;\n  glintGapSize: number;\n  glintStrokeWidth: number;\n  glintPeakOpacity: number;\n  slowGlintDelay: number;\n  slowGlintDashSize: number;\n  slowGlintGapSize: number;\n  slowGlintStrokeWidth: number;\n  slowGlintPeakOpacity: number;\n}\n\ninterface StatCardAnimationParams {\n  staggerOffset: number;\n  labelDelay: number;\n  labelDuration: number;\n  labelSlide: number;\n  valueDuration: number;\n  valueDelay: number;\n  valueSlide: number;\n}\n\ninterface TunableSparklineProps {\n  data: number[];\n  color?: string;\n  width?: number;\n  height?: number;\n  className?: string;\n  style?: CSSProperties;\n  showFill?: boolean;\n  animation: SparklineAnimationParams;\n  baseDelay?: number;\n}\n\nfunction TunableSparkline({\n  data,\n  color = \"currentColor\",\n  width = 64,\n  height = 24,\n  className,\n  style,\n  showFill = false,\n  animation,\n  baseDelay = 0,\n}: TunableSparklineProps) {\n  const gradientId = useId();\n\n  if (data.length < 2) {\n    return null;\n  }\n\n  const minVal = Math.min(...data);\n  const maxVal = Math.max(...data);\n  const range = maxVal - minVal || 1;\n\n  const padding = 0;\n  const usableWidth = width;\n  const usableHeight = height;\n\n  const linePoints = data.map((value, index) => {\n    const x = padding + (index / (data.length - 1)) * usableWidth;\n    const y =\n      padding + usableHeight - ((value - minVal) / range) * usableHeight;\n    return { x, y };\n  });\n\n  const linePointsString = linePoints.map((p) => `${p.x},${p.y}`).join(\" \");\n\n  const areaPointsString = [\n    `${padding},${height}`,\n    ...linePoints.map((p) => `${p.x},${p.y}`),\n    `${width - padding},${height}`,\n  ].join(\" \");\n\n  const fillDelay = `${baseDelay}ms`;\n  const slowGlintDelay = `${baseDelay + animation.slowGlintDelay}ms`;\n  const glintDelay = `${baseDelay + animation.glintDelay}ms`;\n\n  return (\n    <svg\n      viewBox={`0 0 ${width} ${height}`}\n      aria-hidden=\"true\"\n      className={cn(\"h-full w-full shrink-0\", className)}\n      style={\n        {\n          ...style,\n          \"--slide-duration\": `${animation.slideDuration}ms`,\n          \"--slide-distance\": `${animation.slideDistance}px`,\n        } as CSSProperties\n      }\n      preserveAspectRatio=\"none\"\n    >\n      {showFill && (\n        <>\n          <defs>\n            <linearGradient id={gradientId} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n              <stop\n                offset=\"0%\"\n                stopColor={color}\n                stopOpacity={animation.fillOpacity}\n              />\n              <stop offset=\"100%\" stopColor={color} stopOpacity={0} />\n            </linearGradient>\n          </defs>\n          <polygon\n            points={areaPointsString}\n            fill={`url(#${gradientId})`}\n            className=\"animate-in fade-in fill-mode-both\"\n            style={{\n              animationDelay: fillDelay,\n              animationDuration: `${animation.fillFadeDuration}ms`,\n              animationTimingFunction: \"cubic-bezier(0.16, 1, 0.3, 1)\",\n            }}\n          />\n        </>\n      )}\n      <polyline\n        points={linePointsString}\n        fill=\"none\"\n        stroke={color}\n        strokeWidth={1}\n        strokeOpacity={animation.baseStrokeOpacity}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        vectorEffect=\"non-scaling-stroke\"\n      />\n      <polyline\n        points={linePointsString}\n        fill=\"none\"\n        stroke={color}\n        strokeWidth={animation.slowGlintStrokeWidth}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        vectorEffect=\"non-scaling-stroke\"\n        pathLength={1}\n        strokeDasharray={`${animation.slowGlintDashSize} ${animation.slowGlintGapSize}`}\n        strokeDashoffset={1}\n        strokeOpacity={0}\n        style={{\n          animation: `glint-slow-tunable ${animation.glintDuration}s ease-out ${slowGlintDelay} forwards`,\n        }}\n      />\n      <polyline\n        points={linePointsString}\n        fill=\"none\"\n        stroke={color}\n        strokeWidth={animation.glintStrokeWidth}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        vectorEffect=\"non-scaling-stroke\"\n        pathLength={1}\n        strokeDasharray={`${animation.glintDashSize} ${animation.glintGapSize}`}\n        strokeDashoffset={1}\n        strokeOpacity={0}\n        style={{\n          animation: `glint-tunable ${animation.glintDuration}s ease-out ${glintDelay} forwards`,\n        }}\n      />\n      <style>{`\n        @keyframes glint-tunable {\n          0% { stroke-dashoffset: 1; stroke-opacity: 0; }\n          20% { stroke-opacity: ${animation.glintPeakOpacity}; }\n          100% { stroke-dashoffset: -1; stroke-opacity: 0; }\n        }\n        @keyframes glint-slow-tunable {\n          0% { stroke-dashoffset: 1; stroke-opacity: 0; }\n          20% { stroke-opacity: ${animation.slowGlintPeakOpacity}; }\n          100% { stroke-dashoffset: -1; stroke-opacity: 0; }\n        }\n      `}</style>\n    </svg>\n  );\n}\n\ninterface FormattedValueProps {\n  value: string | number;\n  format?: StatFormat;\n  locale?: string;\n}\n\nfunction FormattedValue({ value, format, locale }: FormattedValueProps) {\n  if (typeof value === \"string\" || !format) {\n    return <span className=\"font-light tabular-nums\">{String(value)}</span>;\n  }\n\n  switch (format.kind) {\n    case \"number\": {\n      const decimals = format.decimals ?? 0;\n      if (format.compact) {\n        const parts = new Intl.NumberFormat(locale, {\n          minimumFractionDigits: decimals,\n          maximumFractionDigits: decimals,\n          notation: \"compact\",\n        }).formatToParts(value);\n        const fullNumber = new Intl.NumberFormat(locale).format(value);\n        return (\n          <span className=\"font-light tabular-nums\" aria-label={fullNumber}>\n            {parts.map((part, i) =>\n              part.type === \"compact\" ? (\n                <span\n                  key={i}\n                  className=\"ml-0.5 text-[0.65em] opacity-80\"\n                  aria-hidden=\"true\"\n                >\n                  {part.value}\n                </span>\n              ) : (\n                <span key={i}>{part.value}</span>\n              ),\n            )}\n          </span>\n        );\n      }\n      const formatted = new Intl.NumberFormat(locale, {\n        minimumFractionDigits: decimals,\n        maximumFractionDigits: decimals,\n      }).format(value);\n      return <span className=\"font-light tabular-nums\">{formatted}</span>;\n    }\n    case \"currency\": {\n      const currency = format.currency;\n      const decimals = format.decimals ?? 2;\n      const formatted = new Intl.NumberFormat(locale, {\n        style: \"currency\",\n        currency,\n        minimumFractionDigits: decimals,\n        maximumFractionDigits: decimals,\n      }).format(value);\n      const spokenValue = new Intl.NumberFormat(locale, {\n        style: \"currency\",\n        currency,\n        currencyDisplay: \"name\",\n        minimumFractionDigits: decimals,\n        maximumFractionDigits: decimals,\n      }).format(value);\n      return (\n        <span className=\"font-light tabular-nums\" aria-label={spokenValue}>\n          {formatted}\n        </span>\n      );\n    }\n    case \"percent\": {\n      const decimals = format.decimals ?? 2;\n      const basis = format.basis ?? \"fraction\";\n      const numeric = basis === \"fraction\" ? value * 100 : value;\n      const formatted = numeric.toFixed(decimals);\n      return (\n        <span\n          className=\"font-light tabular-nums\"\n          aria-label={`${formatted} percent`}\n        >\n          {formatted}\n          <span className=\"ml-0.5 text-[0.65em] opacity-80\" aria-hidden=\"true\">\n            %\n          </span>\n        </span>\n      );\n    }\n    case \"text\":\n    default:\n      return <span className=\"font-light tabular-nums\">{String(value)}</span>;\n  }\n}\n\ninterface DeltaValueProps {\n  diff: StatDiff;\n}\n\nfunction DeltaValue({ diff }: DeltaValueProps) {\n  const { value, decimals = 1, upIsPositive = true, label } = diff;\n\n  const isPositive = value > 0;\n  const isNegative = value < 0;\n\n  const isGood = upIsPositive ? isPositive : isNegative;\n  const isBad = upIsPositive ? isNegative : isPositive;\n\n  const colorClass = isGood\n    ? \"text-green-600 dark:text-green-500\"\n    : isBad\n      ? \"text-red-600 dark:text-red-500\"\n      : \"text-muted-foreground\";\n\n  const bgClass = isGood\n    ? \"bg-green-500/10 dark:bg-green-500/15\"\n    : isBad\n      ? \"bg-red-500/10 dark:bg-red-500/15\"\n      : \"bg-muted\";\n\n  const formatted = Math.abs(value).toFixed(decimals);\n  const sign = isNegative ? \"−\" : \"+\";\n  const display = `${sign}${formatted}%`;\n\n  return (\n    <span\n      className={cn(\n        \"inline-flex items-center gap-1 rounded-full px-1.5 py-0.5 text-xs font-semibold tabular-nums\",\n        colorClass,\n        bgClass,\n      )}\n    >\n      {!upIsPositive && (\n        <span className=\"text-[0.9em]\">{isGood ? \"↓\" : \"↑\"}</span>\n      )}\n      {display}\n      {label && (\n        <span className=\"text-muted-foreground font-normal\">{label}</span>\n      )}\n    </span>\n  );\n}\n\ninterface TunableStatCardProps {\n  stat: StatItem;\n  locale?: string;\n  isSingle?: boolean;\n  diagonalIndex?: number;\n  sparklineAnimation: SparklineAnimationParams;\n  cardAnimation: StatCardAnimationParams;\n}\n\nfunction TunableStatCard({\n  stat,\n  locale,\n  isSingle = false,\n  diagonalIndex = 0,\n  sparklineAnimation,\n  cardAnimation,\n}: TunableStatCardProps) {\n  const sparklineColor = stat.sparkline?.color ?? \"var(--muted-foreground)\";\n  const hasSparkline = Boolean(stat.sparkline);\n  const baseDelay = diagonalIndex * cardAnimation.staggerOffset;\n\n  return (\n    <div\n      className={cn(\n        \"relative flex min-h-28 flex-col gap-1 px-6\",\n        isSingle ? \"justify-center\" : \"justify-end\",\n      )}\n    >\n      {hasSparkline && (\n        <TunableSparkline\n          data={stat.sparkline!.data}\n          color={sparklineColor}\n          showFill\n          animation={sparklineAnimation}\n          baseDelay={baseDelay}\n          className=\"pointer-events-none absolute inset-x-0 top-2 bottom-2\"\n          style={{\n            opacity: 0,\n            transform: `translateY(${sparklineAnimation.slideDistance}px)`,\n            animation: `slide-in-tunable ${sparklineAnimation.slideDuration}ms cubic-bezier(0.16, 1, 0.3, 1) ${baseDelay}ms forwards, fade-in-tunable ${sparklineAnimation.fadeDuration}ms cubic-bezier(0.16, 1, 0.3, 1) ${baseDelay}ms forwards`,\n          }}\n        />\n      )}\n      <span\n        className=\"text-muted-foreground relative text-xs font-normal tracking-wider uppercase opacity-0\"\n        style={{\n          animation: `slide-in-label ${cardAnimation.labelDuration}ms cubic-bezier(0.16, 1, 0.3, 1) ${baseDelay + cardAnimation.labelDelay}ms forwards, fade-in-tunable ${cardAnimation.labelDuration}ms cubic-bezier(0.16, 1, 0.3, 1) ${baseDelay + cardAnimation.labelDelay}ms forwards`,\n        }}\n      >\n        {stat.label}\n      </span>\n      <div\n        className=\"relative flex items-baseline gap-2 pb-2 opacity-0\"\n        style={{\n          animation: `slide-in-value ${cardAnimation.valueDuration}ms cubic-bezier(0.16, 1, 0.3, 1) ${baseDelay + cardAnimation.valueDelay}ms forwards, fade-in-tunable ${cardAnimation.valueDuration}ms cubic-bezier(0.16, 1, 0.3, 1) ${baseDelay + cardAnimation.valueDelay}ms forwards`,\n        }}\n      >\n        <span\n          className={cn(\n            \"font-light tracking-normal\",\n            isSingle ? \"text-5xl\" : \"text-3xl\",\n          )}\n        >\n          <FormattedValue\n            value={stat.value}\n            format={stat.format}\n            locale={locale}\n          />\n        </span>\n        {stat.diff && <DeltaValue diff={stat.diff} />}\n      </div>\n      <style>{`\n        @keyframes slide-in-tunable {\n          from { transform: translateY(${sparklineAnimation.slideDistance}px); }\n          to { transform: translateY(0); }\n        }\n        @keyframes slide-in-label {\n          from { transform: translateY(${cardAnimation.labelSlide}px); opacity: 0; }\n          to { transform: translateY(0); opacity: 0.9; }\n        }\n        @keyframes slide-in-value {\n          from { transform: translateY(${cardAnimation.valueSlide}px); opacity: 0; }\n          to { transform: translateY(0); opacity: 1; }\n        }\n        @keyframes fade-in-tunable {\n          from { opacity: 0; }\n          to { opacity: 1; }\n        }\n      `}</style>\n    </div>\n  );\n}\n\ninterface TunableStatsDisplayProps extends StatsDisplayProps {\n  sparklineAnimation: SparklineAnimationParams;\n  cardAnimation: StatCardAnimationParams;\n}\n\nfunction TunableStatsDisplay({\n  id,\n  title,\n  description,\n  stats,\n  className,\n  locale: localeProp,\n  sparklineAnimation,\n  cardAnimation,\n}: TunableStatsDisplayProps) {\n  const locale =\n    localeProp ??\n    (typeof navigator !== \"undefined\" ? navigator.language : undefined);\n  const hasHeader = Boolean(title || description);\n  const isSingle = stats.length === 1;\n\n  // Calculate diagonal index for ripple effect (row + col)\n  // Assumes 2 columns when container is wide enough\n  const columns = stats.length <= 2 ? stats.length : 2;\n  const getDiagonalIndex = (index: number) => {\n    const row = Math.floor(index / columns);\n    const col = index % columns;\n    return row + col;\n  };\n\n  return (\n    <article\n      data-slot=\"stats-display\"\n      data-tool-ui-id={id}\n      className={cn(\n        \"w-full max-w-xl min-w-80\",\n        isSingle && \"max-w-sm\",\n        className,\n      )}\n    >\n      <Card className={cn(\"overflow-clip !pt-2 !pb-0\", hasHeader && \"!gap-0\")}>\n        {hasHeader && (\n          <CardHeader className=\"border-border border-b !pt-3 !pb-4\">\n            {title && <CardTitle className=\"text-pretty\">{title}</CardTitle>}\n            {description && (\n              <CardDescription className=\"text-pretty\">\n                {description}\n              </CardDescription>\n            )}\n          </CardHeader>\n        )}\n        <CardContent className=\"@container overflow-hidden p-0\">\n          <div\n            className=\"grid @[440px]:-mt-px @[440px]:-ml-px\"\n            style={{\n              gridTemplateColumns: \"repeat(auto-fit, minmax(220px, 1fr))\",\n            }}\n          >\n            {stats.map((stat, index) => (\n              <div\n                key={stat.key}\n                className={cn(\n                  \"@[440px]:border-border overflow-clip py-3 first:pt-0 @[440px]:border-t @[440px]:border-l @[440px]:py-3 @[440px]:first:pt-3\",\n                  index > 0 && \"border-border border-t\",\n                )}\n              >\n                <TunableStatCard\n                  stat={stat}\n                  locale={locale}\n                  isSingle={isSingle}\n                  diagonalIndex={getDiagonalIndex(index)}\n                  sparklineAnimation={sparklineAnimation}\n                  cardAnimation={cardAnimation}\n                />\n              </div>\n            ))}\n          </div>\n        </CardContent>\n      </Card>\n    </article>\n  );\n}\n\ninterface TuningPanelProps {\n  data: StatsDisplayProps;\n}\n\nfunction TuningPanel({ data }: TuningPanelProps) {\n  const [key, setKey] = useState(0);\n\n  const loopControls = useControls(\"Playback\", {\n    autoLoop: { value: false, label: \"Auto Loop\" },\n    loopInterval: {\n      value: 3000,\n      min: 1000,\n      max: 10000,\n      step: 500,\n      label: \"Interval (ms)\",\n    },\n  });\n\n  const replay = useCallback(() => setKey((k) => k + 1), []);\n\n  useEffect(() => {\n    if (!loopControls.autoLoop) return;\n\n    replay();\n    const intervalId = setInterval(replay, loopControls.loopInterval);\n    return () => clearInterval(intervalId);\n  }, [loopControls.autoLoop, loopControls.loopInterval, replay]);\n\n  const sparklineAnimation = useControls(\"Sparkline\", {\n    slideDuration: {\n      value: 1000,\n      min: 100,\n      max: 3000,\n      step: 50,\n      label: \"Slide Duration (ms)\",\n    },\n    slideDistance: {\n      value: 48,\n      min: 0,\n      max: 100,\n      step: 4,\n      label: \"Slide Distance (px)\",\n    },\n    fadeDuration: {\n      value: 1500,\n      min: 100,\n      max: 3000,\n      step: 50,\n      label: \"Fade Duration (ms)\",\n    },\n    fillFadeDuration: {\n      value: 1000,\n      min: 100,\n      max: 3000,\n      step: 50,\n      label: \"Fill Fade Duration\",\n    },\n    fillOpacity: {\n      value: 0.09,\n      min: 0,\n      max: 0.5,\n      step: 0.01,\n      label: \"Fill Opacity\",\n    },\n    baseStrokeOpacity: {\n      value: 0.15,\n      min: 0,\n      max: 1,\n      step: 0.05,\n      label: \"Base Stroke Opacity\",\n    },\n  });\n\n  const glintAnimation = useControls(\"Glint Effect\", {\n    glintDuration: {\n      value: 0.8,\n      min: 0.1,\n      max: 3,\n      step: 0.1,\n      label: \"Duration (s)\",\n    },\n    glintDelay: { value: 0, min: 0, max: 2000, step: 50, label: \"Delay (ms)\" },\n    glintDashSize: {\n      value: 0.24,\n      min: 0.05,\n      max: 0.8,\n      step: 0.02,\n      label: \"Dash (0-1)\",\n    },\n    glintGapSize: {\n      value: 0.76,\n      min: 0.2,\n      max: 1.5,\n      step: 0.02,\n      label: \"Gap (0-1)\",\n    },\n    glintStrokeWidth: {\n      value: 0.75,\n      min: 0.5,\n      max: 6,\n      step: 0.25,\n      label: \"Stroke Width\",\n    },\n    glintPeakOpacity: {\n      value: 0.9,\n      min: 0,\n      max: 1,\n      step: 0.05,\n      label: \"Peak Opacity\",\n    },\n  });\n\n  const slowGlintAnimation = useControls(\"Slow Glint\", {\n    slowGlintDelay: {\n      value: 0,\n      min: 0,\n      max: 2000,\n      step: 50,\n      label: \"Delay (ms)\",\n    },\n    slowGlintDashSize: {\n      value: 0.36,\n      min: 0.1,\n      max: 0.8,\n      step: 0.02,\n      label: \"Dash (0-1)\",\n    },\n    slowGlintGapSize: {\n      value: 0.64,\n      min: 0.2,\n      max: 1.5,\n      step: 0.02,\n      label: \"Gap (0-1)\",\n    },\n    slowGlintStrokeWidth: {\n      value: 0.75,\n      min: 0.5,\n      max: 6,\n      step: 0.25,\n      label: \"Stroke Width\",\n    },\n    slowGlintPeakOpacity: {\n      value: 0.2,\n      min: 0,\n      max: 1,\n      step: 0.05,\n      label: \"Peak Opacity\",\n    },\n  });\n\n  const cardAnimation = useControls(\"Stat Cards\", {\n    staggerOffset: {\n      value: 175,\n      min: 0,\n      max: 500,\n      step: 25,\n      label: \"Stagger (ms)\",\n    },\n    labelDelay: {\n      value: 75,\n      min: 0,\n      max: 500,\n      step: 25,\n      label: \"Label Delay (ms)\",\n    },\n    labelDuration: {\n      value: 500,\n      min: 100,\n      max: 2000,\n      step: 50,\n      label: \"Label Duration\",\n    },\n    labelSlide: {\n      value: 4,\n      min: 0,\n      max: 32,\n      step: 2,\n      label: \"Label Slide (px)\",\n    },\n    valueDelay: {\n      value: 150,\n      min: 0,\n      max: 500,\n      step: 25,\n      label: \"Value Delay (ms)\",\n    },\n    valueDuration: {\n      value: 500,\n      min: 100,\n      max: 2000,\n      step: 50,\n      label: \"Value Duration\",\n    },\n    valueSlide: {\n      value: 8,\n      min: 0,\n      max: 32,\n      step: 2,\n      label: \"Value Slide (px)\",\n    },\n  });\n\n  const copyConfig = useCallback(() => {\n    const config = {\n      sparkline: {\n        slideDuration: sparklineAnimation.slideDuration,\n        slideDistance: sparklineAnimation.slideDistance,\n        fadeDuration: sparklineAnimation.fadeDuration,\n        fillFadeDuration: sparklineAnimation.fillFadeDuration,\n        fillOpacity: sparklineAnimation.fillOpacity,\n        baseStrokeOpacity: sparklineAnimation.baseStrokeOpacity,\n      },\n      glint: {\n        duration: glintAnimation.glintDuration,\n        delay: glintAnimation.glintDelay,\n        dashSize: glintAnimation.glintDashSize,\n        gapSize: glintAnimation.glintGapSize,\n        strokeWidth: glintAnimation.glintStrokeWidth,\n        peakOpacity: glintAnimation.glintPeakOpacity,\n      },\n      slowGlint: {\n        delay: slowGlintAnimation.slowGlintDelay,\n        dashSize: slowGlintAnimation.slowGlintDashSize,\n        gapSize: slowGlintAnimation.slowGlintGapSize,\n        strokeWidth: slowGlintAnimation.slowGlintStrokeWidth,\n        peakOpacity: slowGlintAnimation.slowGlintPeakOpacity,\n      },\n      cards: {\n        staggerOffset: cardAnimation.staggerOffset,\n        labelDelay: cardAnimation.labelDelay,\n        labelDuration: cardAnimation.labelDuration,\n        labelSlide: cardAnimation.labelSlide,\n        valueDelay: cardAnimation.valueDelay,\n        valueDuration: cardAnimation.valueDuration,\n        valueSlide: cardAnimation.valueSlide,\n      },\n    };\n\n    const configText = JSON.stringify(config, null, 2);\n    navigator.clipboard.writeText(configText).then(() => {\n      console.log(\"Config copied to clipboard!\");\n    });\n  }, [sparklineAnimation, glintAnimation, slowGlintAnimation, cardAnimation]);\n\n  useControls({\n    \"Replay Animation\": button(replay),\n    \"Copy Config\": button(copyConfig),\n  });\n\n  const mergedSparklineAnimation: SparklineAnimationParams = {\n    slideDuration: sparklineAnimation.slideDuration,\n    slideDistance: sparklineAnimation.slideDistance,\n    fadeDuration: sparklineAnimation.fadeDuration,\n    fillFadeDuration: sparklineAnimation.fillFadeDuration,\n    fillOpacity: sparklineAnimation.fillOpacity,\n    baseStrokeOpacity: sparklineAnimation.baseStrokeOpacity,\n    glintDuration: glintAnimation.glintDuration,\n    glintDelay: glintAnimation.glintDelay,\n    glintDashSize: glintAnimation.glintDashSize,\n    glintGapSize: glintAnimation.glintGapSize,\n    glintStrokeWidth: glintAnimation.glintStrokeWidth,\n    glintPeakOpacity: glintAnimation.glintPeakOpacity,\n    slowGlintDelay: slowGlintAnimation.slowGlintDelay,\n    slowGlintDashSize: slowGlintAnimation.slowGlintDashSize,\n    slowGlintGapSize: slowGlintAnimation.slowGlintGapSize,\n    slowGlintStrokeWidth: slowGlintAnimation.slowGlintStrokeWidth,\n    slowGlintPeakOpacity: slowGlintAnimation.slowGlintPeakOpacity,\n  };\n\n  return (\n    <div className=\"flex gap-8\">\n      <div className=\"flex-1\">\n        <TunableStatsDisplay\n          key={key}\n          {...data}\n          sparklineAnimation={mergedSparklineAnimation}\n          cardAnimation={cardAnimation}\n        />\n      </div>\n      <Leva flat hideCopyButton titleBar={{ title: \"Animation Tuning\" }} />\n    </div>\n  );\n}\n\nexport const statsDisplayStagingConfig: StagingConfig = {\n  supportedDebugLevels: [\"off\"],\n  renderDebugOverlay: () => null,\n  renderTuningPanel: ({ data }) => (\n    <TuningPanel data={data as unknown as StatsDisplayProps} />\n  ),\n};\n"
  },
  {
    "path": "apps/www/lib/staging/staging-config.ts",
    "content": "import type { ComponentId, StagingConfig } from \"./types\";\nimport { parameterSliderStagingConfig } from \"./configs/parameter-slider\";\nimport { statsDisplayStagingConfig } from \"./configs/stats-display\";\nimport { progressTrackerStagingConfig } from \"./configs/progress-tracker\";\nimport { planStagingConfig } from \"./configs/plan\";\n\nconst stagingConfigs: Partial<Record<ComponentId, StagingConfig>> = {\n  \"parameter-slider\": parameterSliderStagingConfig,\n  \"stats-display\": statsDisplayStagingConfig,\n  \"progress-tracker\": progressTrackerStagingConfig,\n  plan: planStagingConfig,\n};\n\nexport function getStagingConfig(\n  componentId: ComponentId,\n): StagingConfig | null {\n  return stagingConfigs[componentId] ?? null;\n}\n"
  },
  {
    "path": "apps/www/lib/staging/types.ts",
    "content": "import type { RefObject, ReactNode } from \"react\";\nimport type { ComponentId } from \"@/lib/docs/preview-config\";\n\nexport type DebugLevel = \"off\" | \"boundaries\" | \"margins\" | \"full\";\n\nexport interface StagingConfig {\n  supportedDebugLevels: DebugLevel[];\n  renderDebugOverlay?: (props: {\n    level: DebugLevel;\n    componentRef: RefObject<HTMLElement | null>;\n  }) => ReactNode;\n  renderTuningPanel?: (props: { data: Record<string, unknown> }) => ReactNode;\n}\n\nexport type { ComponentId };\n"
  },
  {
    "path": "apps/www/lib/system/tool-builder-message.ts",
    "content": "export const SYSTEM_MESSAGE = `You are an AI assistant that creates tool UI widgets for assistant-ui.\n\n# Design Philosophy\n\nTool UIs are **compact, purposeful widgets** that enhance conversation context. They complement chat rather than replace it.\n\n**Core Principle:** Widgets should be simple, focused pieces of UI that present essential information in a glanceable format.\n\n# Workflow\n\n**DO NOT explore directories or ask questions. Build immediately.**\n\n**Important:** Your current working directory is \\`/template\\`. All file paths are relative to this directory. When referencing files, use paths like \\`components/demo-tool-ui.tsx\\` (not absolute paths).\n\nWhen creating a tool UI widget:\n1. **FIRST**, update \\`lib/demo-tool-props.tsx\\` with realistic placeholder data for \\`args\\` and \\`result\\` to demonstrate the widget with a nice-looking example\n2. **THEN**, modify \\`components/demo-tool-ui.tsx\\` to build the UI\n3. **ONLY modify** the \\`render\\` function body in \\`components/demo-tool-ui.tsx\\`\n4. Add imports at the top if needed for additional Shadcn components\n5. Keep the \\`makeAssistantToolUI\\` wrapper structure unchanged\n6. You can update the \\`args\\` and \\`result\\` types to match the tool you're building\n\nThe widget will automatically display in the preview pane.\n\n# Size & Content Constraints\n\n**Visual Boundaries:**\n- Card width: 360px (sm) to 560px (lg) maximum\n- Widgets must fit comfortably in a chat interface\n\n**Text Constraints:**\n- Titles: ≤40 characters\n- Text lines: ≤100 characters\n- Show only essential data - exclude non-essential metadata unless explicitly requested\n- When requests are ambiguous, prefer the smallest possible summary\n\n**Complexity Budget:**\n- Widgets are glanceable UI pieces, not full applications\n- Focus on clarity and simplicity over feature completeness\n- Present information hierarchically (most important first)\n\n# Demo Tool Props File\n\nThere is a file at \\`lib/demo-tool-props.tsx\\` with the following initial content:\n\n\\`\\`\\`tsx\n// update this file to show a good placeholder for the demo tool props\n\nexport const args = {};\nexport const result = {};\n\\`\\`\\`\n\n**You should update this file FIRST** when creating a tool UI widget. Populate \\`args\\` and \\`result\\` with realistic placeholder data that demonstrates the tool's purpose. This allows you and the user to see a nice-looking demo immediately in the preview pane.\n\n# The \\`demo-tool-ui.tsx\\` file\n\nThis is the main file where you will build your tool UI widget.\nThis is its initial contents:\n\n\\`\\`\\`tsx\n\"use client\"\n\nimport { makeAssistantToolUI } from \"@assistant-ui/react\"\nimport { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from \"@/components/ui/card\"\nimport { Button } from \"@/components/ui/button\"\n// Add more imports here as needed\n\nconst DemoToolUI = makeAssistantToolUI<\n  Record<string, any>, // args type\n  {} // result type\n>({\n  toolName: \"demo_tool\",\n  render: function DemoToolUI({ args, result }) {\n    // ONLY modify code inside this render function\n    return (\n      <div className=\"flex justify-center items-center min-h-[60vh]\">\n        <Card className=\"w-[380px]\">\n          {/* Your widget UI here */}\n        </Card>\n      </div>\n    )\n  },\n});\n\nexport default DemoToolUI;\n\\`\\`\\`\n\n**You SHOULD update:**\n- The variable name (\\`DemoToolUI\\`) to match the tool you're building (e.g., \\`WeatherToolUI\\`)\n- The \\`toolName\\` property to match the actual tool name (e.g., \\`\"get_weather\"\\`)\n- The export statement to match the new variable name\n\n**DO NOT** modify:\n- The \\`makeAssistantToolUI\\` wrapper function itself\n- Type parameters (leave as \\`Record<string, any>\\` and \\`{}\\`)\n- The structure of code outside the render function\n\n# Available Components\n\n**Full Shadcn/UI library available** - import any component from \\`@/components/ui/\\`:\n\nCommon components: \\`Avatar\\`, \\`Badge\\`, \\`Button\\`, \\`Card\\`, \\`Checkbox\\`, \\`Dialog\\`, \\`DropdownMenu\\`, \\`Input\\`, \\`Label\\`, \\`Select\\`, \\`Separator\\`, \\`Skeleton\\`, \\`Switch\\`, \\`Table\\`, \\`Tabs\\`, \\`Textarea\\`, \\`Tooltip\\`\n\nIcons: Import from \\`lucide-react\\`\n\nStyling: Use Tailwind CSS with semantic classes (\\`bg-card\\`, \\`text-foreground\\`, \\`border-border\\`) for theme support\n\n# Data Handling\n\n**Render Function Parameters:**\n- \\`args\\`: Tool input arguments (Record<string, any>)\n- \\`result\\`: Tool execution result ({})\n\n**Best Practices:**\n- Include only essential data fields\n- Handle edge cases (missing data, empty states)\n- Use optional chaining for safety: \\`args?.field\\`\n- Show loading states with \\`<Skeleton />\\` when appropriate\n- Support both light and dark themes\n\n# Interaction Model\n\nWidgets use **server-driven interactivity** via assistant-ui:\n- User actions trigger tool calls or chat responses\n- Server responds with updated widgets or messages\n- Use built-in component capabilities (Button onClick, Input onChange)\n- Form submissions should communicate back to the assistant\n\nExample interactive pattern:\n\\`\\`\\`tsx\n<Button\n  onClick={() => {\n    // This will trigger a new assistant message\n  }}\n>\n  Action\n</Button>\n\\`\\`\\`\n\n# Examples\n\n**Good Widget (Compact, Focused):**\n\\`\\`\\`tsx\nrender: WidgetUI({ args }) {\n  return (\n    <div className=\"flex justify-center items-center min-h-[60vh]\">\n      <Card className=\"w-[400px]\">\n        <CardHeader>\n          <CardTitle>Weather for {args.city}</CardTitle>\n        </CardHeader>\n        <CardContent>\n          <div className=\"text-4xl font-bold\">{args.temp}°C</div>\n          <p className=\"text-muted-foreground\">{args.condition}</p>\n        </CardContent>\n      </Card>\n    </div>\n  )\n}\n\\`\\`\\`\n\n**Avoid (Too Complex):**\n- Full dashboards with multiple sections\n- Extensive forms with 10+ fields\n- Complex data tables spanning entire viewport\n- Features that would be better as full applications\n\n**Remember:** You're building a chat widget, not a full application. Keep it simple, purposeful, and glanceable.\n\n## Example X Post Card + Propose Tweet Tool UI\n\nHint: You may be asked to simply reproduce this exact example for a demo.\n\n\"use client\";\n\nimport { makeAssistantToolUI } from \"@assistant-ui/react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { MessageCircle, Repeat2, Heart, BarChart3 } from \"lucide-react\";\nimport { useState, useEffect } from \"react\";\n\nconst socialIcons = [MessageCircle, Repeat2, Heart, BarChart3];\n\nconst ProposeTweetToolUI = makeAssistantToolUI<\n  {\n    body: string;\n  },\n  {\n    status: \"approved\" | \"rejected\";\n    approvedTweet?: string;\n  }\n>({\n  toolName: \"proposeTweet\",\n  render: function ProposeTweetToolUI({ args, result, addResult }) {\n    const [editedText, setEditedText] = useState(args?.body || \"\");\n    const [isEditing, setIsEditing] = useState(false);\n    const [hasEdited, setHasEdited] = useState(false);\n    const isApproved = result?.status === \"approved\";\n\n    useEffect(() => {\n      if (!hasEdited && args?.body) {\n        setEditedText(args.body);\n      }\n    }, [args?.body, hasEdited]);\n\n    const handleSave = () => {\n      setIsEditing(false);\n      setHasEdited(true);\n    };\n\n    const handleCancel = () => {\n      setEditedText(args?.body || \"\");\n      setIsEditing(false);\n    };\n\n    const handleApprove = () => {\n      addResult({ status: \"approved\", approvedTweet: editedText });\n    };\n\n    const handleReject = () => {\n      addResult({ status: \"rejected\" });\n    };\n\n    return (\n      <div className=\"mx-2 mb-4 space-y-3\">\n        <div className=\"text-base\">\n          Here&apos;s a draft post for X. Review and approve it before posting.\n        </div>\n\n        <div className=\"max-w-[600px]\">\n          <article\n            className={\\`rounded-xl border bg-card p-3 $\\{\n              isApproved ? \"border-green-500\" : \"border-border\"\n            }\\`}\n          >\n            <div className=\"flex gap-3\">\n              <img\n                alt=\"User\"\n                className=\"h-10 w-10 shrink-0 rounded-full\"\n                src=\"https://api.dicebear.com/7.x/initials/svg?seed=User&backgroundColor=1e293b\"\n              />\n\n              <div className=\"min-w-0 flex-1\">\n                <div className=\"flex items-center gap-1\">\n                  <span className=\"font-semibold\">User</span>\n                  <span className=\"text-muted-foreground\">@user</span>\n                </div>\n\n                {isEditing ? (\n                  <Textarea\n                    value={editedText}\n                    onChange={(e) => setEditedText(e.target.value)}\n                    className=\"mt-2 min-h-[80px]\"\n                    placeholder=\"What's happening?\"\n                    maxLength={280}\n                  />\n                ) : (\n                  <>\n                    <div className=\"mt-1 whitespace-pre-wrap\">{editedText}</div>\n                    <div className=\"mt-3 -ml-2 flex gap-8\">\n                      {socialIcons.map((Icon, i) => (\n                        <Button\n                          key={i}\n                          variant=\"ghost\"\n                          size=\"sm\"\n                          className=\"h-auto gap-1.5 px-1 py-1\"\n                        >\n                          <Icon className=\"h-4 w-4\" />\n                          <span className=\"text-sm\">0</span>\n                        </Button>\n                      ))}\n                    </div>\n                  </>\n                )}\n              </div>\n            </div>\n          </article>\n        </div>\n\n        {!isApproved && (\n          <div className=\"flex gap-2\">\n            {isEditing ? (\n              <>\n                <Button\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  className=\"rounded-full\"\n                  onClick={handleCancel}\n                >\n                  Cancel\n                </Button>\n                <Button size=\"sm\" className=\"rounded-full\" onClick={handleSave}>\n                  Save Changes\n                </Button>\n              </>\n            ) : (\n              <>\n                <Button\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  className=\"rounded-full\"\n                  onClick={handleReject}\n                >\n                  Reject\n                </Button>\n                <Button\n                  variant=\"outline\"\n                  size=\"sm\"\n                  className=\"rounded-full\"\n                  onClick={() => setIsEditing(true)}\n                >\n                  Edit Post\n                </Button>\n                <Button\n                  size=\"sm\"\n                  className=\"rounded-full\"\n                  onClick={handleApprove}\n                >\n                  Accept and Post\n                </Button>\n              </>\n            )}\n          </div>\n        )}\n      </div>\n    );\n  },\n});\n\nexport default ProposeTweetToolUI;\n`;\n"
  },
  {
    "path": "apps/www/lib/tests/app/leaflet-css-import.test.ts",
    "content": "import { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { describe, expect, test } from \"vitest\";\n\ndescribe(\"root layout stylesheet imports\", () => {\n  test(\"imports Leaflet CSS directly so map tile/marker layout rules are always present\", () => {\n    const layoutPath = path.join(process.cwd(), \"app/layout.tsx\");\n    const source = readFileSync(layoutPath, \"utf8\");\n\n    expect(source).toContain('import \"leaflet/dist/leaflet.css\";');\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/tests/docs/header-preview-tabs-hydration.test.ts",
    "content": "// @vitest-environment jsdom\n\nimport { act } from \"@testing-library/react\";\nimport { createElement } from \"react\";\nimport { hydrateRoot } from \"react-dom/client\";\nimport { renderToString } from \"react-dom/server\";\nimport { afterEach, describe, expect, test, vi } from \"vitest\";\n\nvi.mock(\"@/lib/analytics\", () => ({\n  analytics: {\n    component: {\n      tabSwitched: vi.fn(),\n    },\n  },\n}));\n\nvi.mock(\"@/lib/docs/gallery-usage-code\", () => ({\n  getImportLine: () => 'import { GeoMap } from \"@/components/tool-ui/geo-map\";',\n}));\n\nvi.mock(\"fumadocs-ui/components/dynamic-codeblock\", () => ({\n  DynamicCodeBlock: ({ code }: { code: string }) =>\n    createElement(\"pre\", null, code),\n}));\n\nvi.mock(\"@/lib/docs/preview-config\", async () => {\n  const React = await import(\"react\");\n\n  return {\n    getPreviewConfig: () => ({\n      presets: {\n        default: {\n          data: {},\n          generateExampleCode: () => '<GeoMap id=\"demo\" markers={markers} />',\n        },\n      },\n      defaultPreset: \"default\",\n      // Intentionally non-deterministic to model preview content that differs across renders.\n      renderComponent: () =>\n        React.createElement(\n          \"div\",\n          { \"data-random\": Math.random().toString() },\n          \"Preview\",\n        ),\n    }),\n  };\n});\n\nimport { HeaderPreviewTabs } from \"@/app/docs/_components/header-preview-tabs\";\n\ndescribe(\"HeaderPreviewTabs hydration\", () => {\n  afterEach(() => {\n    document.body.innerHTML = \"\";\n  });\n\n  test(\"does not SSR preview tab markup before hydration\", async () => {\n    const props = { componentId: \"geo-map\" as never };\n\n    const html = renderToString(createElement(HeaderPreviewTabs, props));\n    expect(html).toBe(\"\");\n\n    const container = document.createElement(\"div\");\n    container.innerHTML = html;\n    document.body.append(container);\n\n    let root: ReturnType<typeof hydrateRoot> | null = null;\n    await act(async () => {\n      root = hydrateRoot(container, createElement(HeaderPreviewTabs, props));\n      await Promise.resolve();\n      await Promise.resolve();\n    });\n    expect(root).not.toBeNull();\n    await act(async () => {\n      root?.unmount();\n    });\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/tests/docs/tracked-dynamic-codeblock.test.ts",
    "content": "// @vitest-environment jsdom\n\nimport { fireEvent, render, screen } from \"@testing-library/react\";\nimport { createElement, type ReactNode } from \"react\";\nimport { beforeEach, describe, expect, test, vi } from \"vitest\";\n\nconst { blockCopiedSpy, installSnippetCopiedSpy } = vi.hoisted(() => ({\n  blockCopiedSpy: vi.fn(),\n  installSnippetCopiedSpy: vi.fn(),\n}));\n\nvi.mock(\"@/lib/analytics\", () => ({\n  analytics: {\n    code: {\n      blockCopied: blockCopiedSpy,\n    },\n    docs: {\n      installSnippetCopied: installSnippetCopiedSpy,\n    },\n  },\n}));\n\nvi.mock(\"fumadocs-ui/components/dynamic-codeblock\", () => {\n  const MockCopyButton = ({\n    children,\n    containerRef: _containerRef,\n    ...props\n  }: {\n    children?: ReactNode;\n    containerRef?: unknown;\n  } & Record<string, unknown>) =>\n    createElement(\n      \"button\",\n      {\n        type: \"button\",\n        \"aria-label\": \"Copy Text\",\n        ...props,\n      },\n      children ?? \"Copy\",\n    );\n\n  return {\n    DynamicCodeBlock: ({\n      codeblock,\n    }: {\n      codeblock?: {\n        Actions?: (props: {\n          className?: string;\n          children?: ReactNode;\n        }) => ReactNode;\n      };\n    }) => {\n      const defaultCopyButton = createElement(MockCopyButton, {\n        containerRef: { current: null },\n      });\n\n      if (codeblock?.Actions) {\n        return createElement(\n          \"div\",\n          null,\n          codeblock.Actions({\n            className: \"actions\",\n            children: defaultCopyButton,\n          }),\n        );\n      }\n\n      return createElement(\"div\", null, defaultCopyButton);\n    },\n  };\n});\n\nimport { TrackedDynamicCodeBlock } from \"@/app/docs/_components/tracked-dynamic-codeblock\";\n\ndescribe(\"TrackedDynamicCodeBlock\", () => {\n  beforeEach(() => {\n    blockCopiedSpy.mockClear();\n    installSnippetCopiedSpy.mockClear();\n  });\n\n  test(\"replaces generic copy button labels with contextual code labels\", () => {\n    render(\n      createElement(TrackedDynamicCodeBlock, {\n        lang: \"tsx\",\n        code: 'import { GeoMap } from \"@/components/tool-ui/geo-map\";\\n\\n<GeoMap id=\"fleet\" markers={markers} />',\n      }),\n    );\n\n    const button = screen.getByRole(\"button\");\n    const label = button.getAttribute(\"aria-label\");\n\n    expect(label).not.toBeNull();\n    expect(label).not.toBe(\"Copy Text\");\n    expect(label).toContain(\"Copy TSX snippet\");\n    expect(label).toContain(\"import { GeoMap }\");\n  });\n\n  test(\"uses install-command specific labels and analytics source\", () => {\n    render(\n      createElement(TrackedDynamicCodeBlock, {\n        lang: \"bash\",\n        code: \"npx shadcn@latest add @tool-ui/geo-map\",\n      }),\n    );\n\n    const button = screen.getByRole(\"button\");\n    expect(button.getAttribute(\"aria-label\")).toBe(\n      \"Copy registry install command\",\n    );\n\n    fireEvent.click(button);\n\n    expect(blockCopiedSpy).toHaveBeenCalledWith(\"bash\", \"docs_installation\");\n    expect(installSnippetCopiedSpy).toHaveBeenCalledWith(\n      \"registry\",\n      \"docs_code_block\",\n    );\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/tests/registry/tool-ui-registry.test.ts",
    "content": "import path from \"path\";\nimport { fileURLToPath } from \"url\";\nimport { describe, expect, it } from \"vitest\";\nimport { promises as fs } from \"fs\";\nimport { buildToolUiRegistryArtifacts } from \"@/lib/registry/tool-ui-registry\";\n\nfunction getProjectRoot(): string {\n  const currentFile = fileURLToPath(import.meta.url);\n  return path.resolve(path.dirname(currentFile), \"../../..\");\n}\n\ndescribe(\"Tool UI registry artifacts\", () => {\n  async function listExpectedComponents(): Promise<string[]> {\n    const projectRoot = getProjectRoot();\n    const componentRoot = path.join(projectRoot, \"components\", \"tool-ui\");\n    const entries = await fs.readdir(componentRoot, { withFileTypes: true });\n\n    return entries\n      .filter((entry) => entry.isDirectory())\n      .map((entry) => entry.name)\n      .filter((name) => name !== \"shared\")\n      .sort((a, b) => a.localeCompare(b));\n  }\n\n  it(\"builds a flat index and per-item content payloads\", async () => {\n    const artifacts = await buildToolUiRegistryArtifacts(getProjectRoot());\n    const itemNames = artifacts.items.map((item) => item.name).sort();\n    const expectedComponents = await listExpectedComponents();\n\n    expect(itemNames).toEqual(expectedComponents);\n\n    expect(artifacts.index.items).toHaveLength(artifacts.items.length);\n\n    for (const indexItem of artifacts.index.items) {\n      for (const file of indexItem.files ?? []) {\n        expect(Object.prototype.hasOwnProperty.call(file, \"content\")).toBe(\n          false,\n        );\n      }\n    }\n\n    const dataTableItem = artifacts.items.find(\n      (item) => item.name === \"data-table\",\n    );\n    expect(dataTableItem).toBeDefined();\n    expect(\n      dataTableItem?.files.some((file) =>\n        file.path.endsWith(\"data-table/data-table.tsx\"),\n      ),\n    ).toBe(true);\n    expect(\n      dataTableItem?.files.some(\n        (file) => file.path === \"components/tool-ui/shared/action-buttons.tsx\",\n      ),\n    ).toBe(false);\n    expect(\n      dataTableItem?.files.some((file) => file.path === \"lib/ui/cn.ts\"),\n    ).toBe(false);\n    const dataTableAdapter = dataTableItem?.files.find(\n      (file) => file.path === \"components/tool-ui/data-table/_adapter.tsx\",\n    );\n    expect(dataTableAdapter?.content).toContain(\n      'export { cn } from \"@/lib/utils\";',\n    );\n    expect(dataTableItem?.dependencies?.includes(\"clsx\")).toBe(false);\n    expect(dataTableItem?.dependencies?.includes(\"tailwind-merge\")).toBe(false);\n    expect(dataTableItem?.dependencies?.includes(\"zod\")).toBe(true);\n    expect(dataTableItem?.registryDependencies).toEqual([\n      \"accordion\",\n      \"badge\",\n      \"button\",\n      \"dropdown-menu\",\n      \"table\",\n      \"tooltip\",\n    ]);\n  });\n\n  it(\"includes accordion/collapsible registry dependencies for motion primitives\", async () => {\n    const artifacts = await buildToolUiRegistryArtifacts(getProjectRoot());\n\n    const expectations = new Map<string, string[]>([\n      [\"plan\", [\"accordion\", \"collapsible\"]],\n      [\"data-table\", [\"accordion\"]],\n      [\"code-block\", [\"collapsible\"]],\n      [\"terminal\", [\"collapsible\"]],\n    ]);\n\n    for (const [componentName, requiredDependencies] of expectations) {\n      const item = artifacts.items.find(\n        (candidate) => candidate.name === componentName,\n      );\n      expect(item, `missing registry item: ${componentName}`).toBeDefined();\n\n      for (const dependency of requiredDependencies) {\n        expect(item?.registryDependencies ?? []).toContain(dependency);\n      }\n    }\n  });\n\n  it(\"pins chart dependency to a recharts v2 release compatible with shadcn charts\", async () => {\n    const artifacts = await buildToolUiRegistryArtifacts(getProjectRoot());\n    const chartItem = artifacts.items.find((item) => item.name === \"chart\");\n\n    expect(chartItem, \"missing registry item: chart\").toBeDefined();\n    expect(chartItem?.dependencies ?? []).toContain(\"recharts@2.15.4\");\n    expect(chartItem?.dependencies ?? []).not.toContain(\"recharts\");\n  });\n\n  it(\"keeps progress-tracker registry dependencies minimal\", async () => {\n    const artifacts = await buildToolUiRegistryArtifacts(getProjectRoot());\n    const progressTrackerItem = artifacts.items.find(\n      (item) => item.name === \"progress-tracker\",\n    );\n\n    expect(\n      progressTrackerItem,\n      \"missing registry item: progress-tracker\",\n    ).toBeDefined();\n    expect(progressTrackerItem?.registryDependencies ?? []).not.toContain(\n      \"button\",\n    );\n  });\n\n  it(\"includes local type-only import files required by shipped schemas\", async () => {\n    const artifacts = await buildToolUiRegistryArtifacts(getProjectRoot());\n\n    const componentsRequiringEmbeddedActions = [\n      \"option-list\",\n      \"parameter-slider\",\n      \"preferences-panel\",\n    ] as const;\n\n    for (const componentName of componentsRequiringEmbeddedActions) {\n      const item = artifacts.items.find(\n        (candidate) => candidate.name === componentName,\n      );\n      expect(item, `missing registry item: ${componentName}`).toBeDefined();\n\n      const itemPaths = new Set(item?.files.map((file) => file.path) ?? []);\n      expect(\n        itemPaths.has(\"components/tool-ui/shared/embedded-actions.ts\"),\n      ).toBe(true);\n    }\n  });\n\n  it(\"ships weather-widget as runtime-entry closure without authoring-only effects files\", async () => {\n    const artifacts = await buildToolUiRegistryArtifacts(getProjectRoot());\n    const weatherWidgetItem = artifacts.items.find(\n      (item) => item.name === \"weather-widget\",\n    );\n\n    expect(\n      weatherWidgetItem,\n      \"missing registry item: weather-widget\",\n    ).toBeDefined();\n\n    const weatherPaths = new Set(\n      weatherWidgetItem?.files.map((file) => file.path) ?? [],\n    );\n    expect(weatherPaths.size).toBe(5);\n\n    expect(\n      weatherPaths.has(\"components/tool-ui/weather-widget/runtime.ts\"),\n    ).toBe(true);\n    expect(\n      weatherPaths.has(\"components/tool-ui/weather-widget/schema-runtime.ts\"),\n    ).toBe(true);\n    expect(\n      weatherPaths.has(\n        \"components/tool-ui/weather-widget/generated/weather-runtime-core.generated.ts\",\n      ),\n    ).toBe(true);\n    expect(\n      weatherPaths.has(\n        \"components/tool-ui/weather-widget/weather-widget-container.tsx\",\n      ),\n    ).toBe(true);\n    expect(\n      weatherPaths.has(\n        \"components/tool-ui/weather-widget/weather-data-overlay.tsx\",\n      ),\n    ).toBe(true);\n\n    expect(\n      weatherPaths.has(\"components/tool-ui/weather-widget/effects/index.ts\"),\n    ).toBe(false);\n    expect(\n      weatherPaths.has(\n        \"components/tool-ui/weather-widget/effects/tuned-presets.ts\",\n      ),\n    ).toBe(false);\n    expect(\n      weatherPaths.has(\n        \"components/tool-ui/weather-widget/effects/weather-effect-shaders.ts\",\n      ),\n    ).toBe(false);\n    expect(\n      weatherPaths.has(\n        \"components/tool-ui/weather-widget/effects/use-glass-region.ts\",\n      ),\n    ).toBe(false);\n    expect(\n      weatherPaths.has(\n        \"components/tool-ui/weather-widget/effects/glass-panel-svg.tsx\",\n      ),\n    ).toBe(false);\n    expect(\n      weatherPaths.has(\n        \"components/tool-ui/weather-widget/effects/custom-effect-props.ts\",\n      ),\n    ).toBe(false);\n    expect(\n      weatherPaths.has(\n        \"components/tool-ui/weather-widget/effects/effect-compositor-custom-props.ts\",\n      ),\n    ).toBe(false);\n    expect(\n      weatherPaths.has(\n        \"components/tool-ui/weather-widget/effects/checkpoint-overrides.ts\",\n      ),\n    ).toBe(false);\n    expect(\n      weatherPaths.has(\n        \"components/tool-ui/weather-widget/effects/parameter-mapper.ts\",\n      ),\n    ).toBe(false);\n    expect(\n      weatherPaths.has(\n        \"components/tool-ui/weather-widget/effects/weather-effect-render-passes.ts\",\n      ),\n    ).toBe(false);\n    expect(\n      weatherPaths.has(\n        \"components/tool-ui/weather-widget/effects/use-weather-effects-renderer.ts\",\n      ),\n    ).toBe(false);\n    expect(\n      weatherPaths.has(\n        \"components/tool-ui/weather-widget/weather-widget.generated.js\",\n      ),\n    ).toBe(false);\n    expect(\n      weatherPaths.has(\n        \"lib/weather-authoring/weather-widget/weather-widget-container.tsx\",\n      ),\n    ).toBe(false);\n    expect(\n      weatherPaths.has(\n        \"lib/weather-authoring/weather-widget/effects/parameter-mapper.ts\",\n      ),\n    ).toBe(false);\n    expect(\n      weatherPaths.has(\"components/tool-ui/weather-widget/schema.ts\"),\n    ).toBe(false);\n    expect(weatherPaths.has(\"components/tool-ui/shared/contract.ts\")).toBe(\n      false,\n    );\n    expect(weatherPaths.has(\"components/tool-ui/shared/parse.ts\")).toBe(false);\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/tests/setup/console-guard.ts",
    "content": "import { afterEach, beforeEach, vi } from \"vitest\";\n\nconst ALLOWED_PATTERNS: RegExp[] = [];\n\nlet consoleErrorSpy: ReturnType<typeof vi.spyOn>;\nlet consoleWarnSpy: ReturnType<typeof vi.spyOn>;\n\nfunction toMessage(args: unknown[]): string {\n  return args\n    .map((arg) => {\n      if (arg instanceof Error) {\n        return `${arg.name}: ${arg.message}`;\n      }\n      if (typeof arg === \"string\") {\n        return arg;\n      }\n      try {\n        return JSON.stringify(arg);\n      } catch {\n        return String(arg);\n      }\n    })\n    .join(\" \");\n}\n\nfunction isAllowedMessage(message: string): boolean {\n  return ALLOWED_PATTERNS.some((pattern) => pattern.test(message));\n}\n\nbeforeEach(() => {\n  consoleErrorSpy = vi.spyOn(console, \"error\").mockImplementation(() => {});\n  consoleWarnSpy = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n});\n\nafterEach(() => {\n  const errorMessages = consoleErrorSpy.mock.calls.map((call: unknown[]) =>\n    toMessage(call),\n  );\n  const warnMessages = consoleWarnSpy.mock.calls.map((call: unknown[]) =>\n    toMessage(call),\n  );\n  const unexpectedMessages = [...errorMessages, ...warnMessages].filter(\n    (message) => !isAllowedMessage(message),\n  );\n\n  consoleErrorSpy.mockRestore();\n  consoleWarnSpy.mockRestore();\n\n  if (unexpectedMessages.length > 0) {\n    throw new Error(\n      [\n        \"Unexpected console warnings/errors detected during test:\",\n        ...unexpectedMessages.map((message) => `- ${message}`),\n      ].join(\"\\n\"),\n    );\n  }\n});\n"
  },
  {
    "path": "apps/www/lib/tests/tool-ui/docs/changelog-inference-parser.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\nimport {\n  ensureCriticalMigrationCoverage,\n  parseInferredReleaseNotes,\n} from \"@/lib/changelog/inference\";\n\ndescribe(\"changelog inference parser\", () => {\n  test(\"parses fenced JSON output\", () => {\n    const output = [\n      \"Here is the inferred changelog payload:\",\n      \"```json\",\n      '{ \"breakingChanges\": [\"A\"], \"changes\": [\"B\"], \"migrationPrompt\": \"C\" }',\n      \"```\",\n    ].join(\"\\n\");\n\n    const parsed = parseInferredReleaseNotes(output);\n    expect(parsed.breakingChanges).toEqual([\"A\"]);\n    expect(parsed.changes).toEqual([\"B\"]);\n    expect(parsed.migrationPrompt).toBe(\"C\");\n  });\n\n  test(\"preserves migration prompt even without breaking changes\", () => {\n    const output = JSON.stringify({\n      breakingChanges: [],\n      changes: [\"B\"],\n      migrationPrompt: \"Re-install components via npx shadcn@latest add\",\n    });\n\n    const parsed = parseInferredReleaseNotes(output);\n    expect(parsed.breakingChanges).toEqual([]);\n    expect(parsed.changes).toEqual([\"B\"]);\n    expect(parsed.migrationPrompt).toBe(\n      \"Re-install components via npx shadcn@latest add\",\n    );\n  });\n\n  test(\"throws when JSON payload is missing required fields\", () => {\n    const output = JSON.stringify({\n      changes: [],\n    });\n\n    expect(() => parseInferredReleaseNotes(output)).toThrow(\n      \"Invalid inferred changelog payload\",\n    );\n  });\n\n  test(\"normalizes literal newline escapes in migration prompts\", () => {\n    const output = JSON.stringify({\n      breakingChanges: [\"A\"],\n      changes: [\"B\"],\n      migrationPrompt: \"Line 1\\\\nLine 2\\\\n\\\\nLine 3\",\n    });\n\n    const parsed = parseInferredReleaseNotes(output);\n    expect(parsed.migrationPrompt).toBe(\"Line 1\\nLine 2\\n\\nLine 3\");\n  });\n\n  test(\"drops component-specific schema lines when a global boundary line exists\", () => {\n    const output = JSON.stringify({\n      breakingChanges: [\n        \"DataTable schema helpers moved to a dedicated /schema entrypoint.\",\n        \"Tool UI component entrypoints now enforce a /schema boundary across all components.\",\n      ],\n      changes: [\n        \"Split DataTable schema helpers into /schema entrypoint and updated contracts.\",\n        \"Migrated schema helper exports to /schema entrypoints across Tool UI components.\",\n      ],\n      migrationPrompt:\n        \"Migrate codebase to adopt explicit /schema entrypoints for Tool UI and DataTable schemas.\",\n    });\n\n    const parsed = parseInferredReleaseNotes(output);\n    expect(parsed.breakingChanges).toEqual([\n      \"Tool UI component entrypoints now enforce a /schema boundary across all components.\",\n    ]);\n    expect(parsed.changes).toEqual([\n      \"Migrated schema helper exports to /schema entrypoints across Tool UI components.\",\n    ]);\n    expect(parsed.migrationPrompt).toBe(\n      \"Migrate codebase to adopt explicit /schema entrypoints for Tool UI schemas.\",\n    );\n  });\n\n  test(\"detects global schema scope from varied phrasing\", () => {\n    const output = JSON.stringify({\n      breakingChanges: [\n        \"Tool UI: enforced /schema boundaries on component entrypoints repo-wide.\",\n      ],\n      changes: [\n        \"Tool UI: enforced /schema boundaries on component entrypoints repo-wide.\",\n        \"DataTable: moved schema helpers to dedicated /schema entrypoint.\",\n      ],\n      migrationPrompt:\n        \"Migrate codebase to adopt explicit /schema entrypoints for Tool UI and DataTable schemas.\",\n    });\n\n    const parsed = parseInferredReleaseNotes(output);\n    expect(parsed.breakingChanges).toEqual([\n      \"Tool UI: enforced /schema boundaries on component entrypoints repo-wide.\",\n    ]);\n    expect(parsed.changes).toEqual([\n      \"Tool UI: enforced /schema boundaries on component entrypoints repo-wide.\",\n    ]);\n    expect(parsed.migrationPrompt).toBe(\n      \"Migrate codebase to adopt explicit /schema entrypoints for Tool UI schemas.\",\n    );\n  });\n\n  test(\"adds action-model migration coverage when action-model signals are present\", () => {\n    const covered = ensureCriticalMigrationCoverage(\n      {\n        breakingChanges: [\n          \"Tool UI component entrypoints now enforce /schema boundaries.\",\n        ],\n        changes: [\"Tool UI: repo-wide enforcement of /schema entrypoints.\"],\n        migrationPrompt:\n          \"Migrate codebase to adopt explicit /schema entrypoints for Tool UI schemas.\",\n      },\n      {\n        changedFiles: [\n          \"components/tool-ui/shared/local-actions.tsx\",\n          \"components/tool-ui/shared/decision-actions.tsx\",\n        ],\n        commitSummary: \"- abc1234 feat: action model cutover\",\n      },\n    );\n\n    expect(covered.breakingChanges.join(\"\\n\")).toContain(\"LocalActions\");\n    expect(covered.changes.join(\"\\n\")).toContain(\"DecisionActions\");\n    expect(covered.migrationPrompt).toContain(\n      \"Migrate action handling to `ToolUI.LocalActions` / `ToolUI.DecisionActions` for consequential workflows.\",\n    );\n    expect(covered.migrationPrompt).not.toContain(\"outlier action configs\");\n  });\n\n  test(\"adds action-model change coverage without forcing breaking migration when signals are non-breaking\", () => {\n    const covered = ensureCriticalMigrationCoverage(\n      {\n        breakingChanges: [],\n        changes: [\"Small docs polish.\"],\n        migrationPrompt: null,\n      },\n      {\n        changedFiles: [\"components/tool-ui/shared/local-actions.tsx\"],\n        commitSummary: \"- abc1234 refactor: polish LocalActions labels\",\n      },\n    );\n\n    expect(covered.changes.join(\"\\n\")).toContain(\"LocalActions\");\n    expect(covered.breakingChanges).toEqual([]);\n    expect(covered.migrationPrompt).toBeNull();\n  });\n\n  test(\"does not add action-model migration coverage without action-model signals\", () => {\n    const source = {\n      breakingChanges: [\n        \"Tool UI component entrypoints now enforce /schema boundaries.\",\n      ],\n      changes: [\"Tool UI: repo-wide enforcement of /schema entrypoints.\"],\n      migrationPrompt:\n        \"Migrate codebase to adopt explicit /schema entrypoints for Tool UI schemas.\",\n    };\n\n    const covered = ensureCriticalMigrationCoverage(source, {\n      changedFiles: [\"components/tool-ui/data-table/schema.ts\"],\n      commitSummary: \"- def5678 feat: schema boundary hardening\",\n    });\n\n    expect(covered).toEqual(source);\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/tests/tool-ui/docs/changelog-inference-prompt.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\nimport { buildInferencePrompt } from \"@/lib/changelog/inference\";\n\ndescribe(\"changelog inference prompt\", () => {\n  test(\"includes cross-component guidance to avoid seed-component bias\", () => {\n    const prompt = buildInferencePrompt({\n      releaseDate: \"2026-02-12\",\n      changedFiles: [\"components/tool-ui/data-table/schema.ts\"],\n      commitSummary: \"- abc1234 refactor: migrate schema entrypoints\",\n      changelogTemplateContext: \"## 2026-02-11\\n\\n### Changes\\n\\n- Example\",\n    });\n\n    expect(prompt).toContain(\n      \"Avoid over-indexing on a seed component (for example DataTable) when scope is cross-component.\",\n    );\n    expect(prompt).toContain(\n      \"Include markdown links to relevant docs routes when confidence is high (for example [Actions](/docs/actions) for action-model changes).\",\n    );\n    expect(prompt).toContain(\n      \"Cross-component schema migration (global wording)\",\n    );\n    expect(prompt).toContain(\n      'Bad breakingChanges example: [\"DataTable moved to /schema entrypoint.\"]',\n    );\n  });\n\n  test(\"includes component-local example and dynamic evidence sections\", () => {\n    const prompt = buildInferencePrompt({\n      releaseDate: \"2026-02-12\",\n      changedFiles: [\n        \"components/tool-ui/option-list/schema.ts\",\n        \"components/tool-ui/option-list/option-list.tsx\",\n      ],\n      commitSummary:\n        \"- def5678 fix(option-list): tighten selection constraints\",\n      changelogTemplateContext: \"## 2026-02-11\\n\\n### Changes\\n\\n- Example\",\n    });\n\n    expect(prompt).toContain(\"Component-local fix (specific wording)\");\n    expect(prompt).toContain(\"Changed files:\");\n    expect(prompt).toContain(\"Commit evidence:\");\n    expect(prompt).toContain(\"- components/tool-ui/option-list/schema.ts\");\n    expect(prompt).toContain(\n      \"- def5678 fix(option-list): tighten selection constraints\",\n    );\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/tests/tool-ui/docs/docs-search-shortcut.test.ts",
    "content": "import { describe, expect, test, vi } from \"vitest\";\nimport { triggerSearchFromShortcut } from \"@/app/docs/_components/docs-search-shortcut\";\n\nconst createShortcutEvent = (props?: Partial<KeyboardEvent>) =>\n  ({\n    defaultPrevented: false,\n    metaKey: true,\n    ctrlKey: false,\n    key: \"k\",\n    preventDefault: vi.fn(),\n    ...props,\n  }) as unknown as KeyboardEvent;\n\ndescribe(\"docs search keyboard shortcut\", () => {\n  test(\"opens at most once for a single keydown event object\", () => {\n    const onOpen = vi.fn();\n    const event = createShortcutEvent();\n\n    triggerSearchFromShortcut(event, onOpen);\n    triggerSearchFromShortcut(event, onOpen);\n\n    expect(onOpen).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/tests/tool-ui/docs/gallery-layout.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\n\nimport {\n  GALLERY_DESKTOP_GRID_CLASS,\n  GALLERY_LAYOUT_CLASS,\n  GALLERY_STANDARD_PREVIEW_WRAPPER_CLASS,\n} from \"@/lib/docs/gallery-layout\";\n\ndescribe(\"gallery page layout\", () => {\n  test(\"uses deterministic two-stack layout at lg instead of CSS columns\", () => {\n    expect(GALLERY_LAYOUT_CLASS).toContain(\"w-full\");\n    expect(GALLERY_LAYOUT_CLASS).not.toContain(\"columns-\");\n    expect(GALLERY_DESKTOP_GRID_CLASS).toContain(\"lg:grid-cols-2\");\n    expect(GALLERY_DESKTOP_GRID_CLASS).not.toContain(\"columns-\");\n  });\n\n  test(\"centers standardized preview wrappers for narrow components\", () => {\n    expect(GALLERY_STANDARD_PREVIEW_WRAPPER_CLASS).toContain(\"max-w-[680px]\");\n    expect(GALLERY_STANDARD_PREVIEW_WRAPPER_CLASS).toContain(\"flex\");\n    expect(GALLERY_STANDARD_PREVIEW_WRAPPER_CLASS).toContain(\"justify-center\");\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/tests/tool-ui/docs/install-snippet-analytics.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\nimport {\n  detectInstallSnippetType,\n  getDocsCodeCopySource,\n} from \"@/lib/docs/install-snippet-analytics\";\n\ndescribe(\"install snippet analytics helpers\", () => {\n  test(\"detects skills install commands\", () => {\n    const snippet =\n      \"npx skills add https://github.com/assistant-ui/tool-ui --skill tool-ui\";\n    expect(detectInstallSnippetType(snippet)).toBe(\"skills\");\n  });\n\n  test(\"detects tool-agent install commands\", () => {\n    const snippet = 'npx tool-agent \"integrate the plan component\"';\n    expect(detectInstallSnippetType(snippet)).toBe(\"tool_agent\");\n  });\n\n  test(\"detects shadcn registry install commands\", () => {\n    const snippet = \"npx shadcn@latest add @tool-ui/plan\";\n    expect(detectInstallSnippetType(snippet)).toBe(\"registry\");\n  });\n\n  test(\"detects package-manager install commands\", () => {\n    const snippet = \"pnpm add @assistant-ui/react\";\n    expect(detectInstallSnippetType(snippet)).toBe(\"package_manager\");\n  });\n\n  test(\"returns null for non-install snippets\", () => {\n    const snippet = \"const x = 1;\";\n    expect(detectInstallSnippetType(snippet)).toBeNull();\n  });\n\n  test(\"maps install snippets to install copy source\", () => {\n    expect(getDocsCodeCopySource(\"skills\")).toBe(\"docs_installation\");\n    expect(getDocsCodeCopySource(\"tool_agent\")).toBe(\"docs_installation\");\n    expect(getDocsCodeCopySource(null)).toBe(\"docs_code_block\");\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/tests/tool-ui/geo-map/geo-map-render.test.ts",
    "content": "// @vitest-environment jsdom\n\nimport { act, render, waitFor } from \"@testing-library/react\";\nimport { createElement, type ComponentProps, type ReactNode } from \"react\";\nimport { afterEach, beforeEach, describe, expect, test, vi } from \"vitest\";\n\nimport { GeoMap } from \"@/components/tool-ui/geo-map\";\n\ntype MockMapEvents = {\n  moveend?: () => void;\n  zoomend?: () => void;\n};\n\nlet mockMapEvents: MockMapEvents = {};\nlet setViewCallCount = 0;\nlet useUnstableMapReference = false;\nconst popupPropsSpy = vi.fn();\nconst tooltipPropsSpy = vi.fn();\nconst tileLayerPropsSpy = vi.fn();\nconst markerPropsSpy = vi.fn();\nconst polylinePropsSpy = vi.fn();\nconst mapContainerPropsSpy = vi.fn();\nconst mapContainerAttributeSpy = vi.fn();\n\nconst mockMap = {\n  closePopup: vi.fn(),\n  getContainer: vi.fn(() => ({\n    setAttribute: mapContainerAttributeSpy,\n  })),\n  getBounds: vi.fn(() => ({\n    getWest: () => -122.5,\n    getSouth: () => 37.7,\n    getEast: () => -122.3,\n    getNorth: () => 37.9,\n  })),\n  getZoom: vi.fn(() => 12),\n  setView: vi.fn(() => {\n    setViewCallCount += 1;\n    if (setViewCallCount > 5) {\n      throw new Error(\"viewport-loop-detected\");\n    }\n    mockMapEvents.moveend?.();\n  }),\n  fitBounds: vi.fn(() => {\n    mockMapEvents.moveend?.();\n  }),\n  flyTo: vi.fn(),\n};\n\nfunction DivWrapper({\n  children,\n  ...props\n}: ComponentProps<\"div\"> & { children?: ReactNode }) {\n  return createElement(\"div\", props, children);\n}\n\nfunction LeafletWrapper({\n  children,\n}: ComponentProps<\"div\"> & { children?: ReactNode }) {\n  return createElement(\"div\", null, children);\n}\n\nfunction PopupWrapper({\n  children,\n  ...props\n}: ComponentProps<\"div\"> & { children?: ReactNode }) {\n  popupPropsSpy(props);\n  return createElement(\"div\", { className: props.className }, children);\n}\n\nfunction TooltipWrapper({\n  children,\n  ...props\n}: ComponentProps<\"div\"> & { children?: ReactNode }) {\n  tooltipPropsSpy(props);\n  return createElement(\"div\", { className: props.className }, children);\n}\n\nvi.mock(\"@/lib/utils\", () => ({\n  cn: (...classes: Array<string | undefined | false | null>) =>\n    classes.filter(Boolean).join(\" \"),\n}));\n\nvi.mock(\"@/components/ui/card\", () => ({\n  Card: DivWrapper,\n  CardContent: DivWrapper,\n  CardDescription: DivWrapper,\n  CardHeader: DivWrapper,\n  CardTitle: DivWrapper,\n}));\n\nvi.mock(\"leaflet\", () => ({\n  divIcon: vi.fn(() => ({})),\n  latLngBounds: vi.fn(() => ({})),\n}));\n\nvi.mock(\"react-leaflet\", () => ({\n  CircleMarker: LeafletWrapper,\n  MapContainer: ({\n    children,\n    ...props\n  }: ComponentProps<\"div\"> & { children?: ReactNode }) => {\n    mapContainerPropsSpy(props);\n    return createElement(\n      \"div\",\n      {\n        className: props.className,\n        role: props.role,\n        \"aria-label\": props[\"aria-label\"],\n      },\n      children,\n    );\n  },\n  Marker: ({\n    children,\n    ...props\n  }: ComponentProps<\"div\"> & { children?: ReactNode }) => {\n    markerPropsSpy(props);\n    return createElement(\"div\", null, children);\n  },\n  Polyline: ({\n    children,\n    ...props\n  }: ComponentProps<\"div\"> & { children?: ReactNode }) => {\n    polylinePropsSpy(props);\n    return createElement(\"div\", null, children);\n  },\n  Popup: PopupWrapper,\n  TileLayer: (props: unknown) => {\n    tileLayerPropsSpy(props);\n    return null;\n  },\n  Tooltip: TooltipWrapper,\n  ZoomControl: () => null,\n  useMap: () => (useUnstableMapReference ? { ...mockMap } : mockMap),\n  useMapEvents: (events: MockMapEvents) => {\n    mockMapEvents = events;\n    return mockMap;\n  },\n}));\n\ndescribe(\"GeoMap render behavior\", () => {\n  beforeEach(() => {\n    mockMapEvents = {};\n    setViewCallCount = 0;\n    useUnstableMapReference = false;\n    mockMap.setView.mockClear();\n    mockMap.fitBounds.mockClear();\n    mockMap.flyTo.mockClear();\n    mockMap.closePopup.mockClear();\n    mockMap.getContainer.mockClear();\n    mockMap.getBounds.mockClear();\n    mockMap.getZoom.mockClear();\n    popupPropsSpy.mockClear();\n    tooltipPropsSpy.mockClear();\n    tileLayerPropsSpy.mockClear();\n    markerPropsSpy.mockClear();\n    polylinePropsSpy.mockClear();\n    mapContainerPropsSpy.mockClear();\n    mapContainerAttributeSpy.mockClear();\n\n    vi.stubGlobal(\n      \"MutationObserver\",\n      class {\n        observe() {}\n        disconnect() {}\n      },\n    );\n\n    vi.stubGlobal(\n      \"matchMedia\",\n      vi.fn(() => ({\n        matches: false,\n        media: \"\",\n        onchange: null,\n        addListener: vi.fn(),\n        removeListener: vi.fn(),\n        addEventListener: vi.fn(),\n        removeEventListener: vi.fn(),\n        dispatchEvent: vi.fn(),\n      })),\n    );\n  });\n\n  afterEach(() => {\n    vi.unstubAllGlobals();\n  });\n\n  test(\"renders a simple loading shell until the map engine is ready\", async () => {\n    render(\n      createElement(GeoMap, {\n        id: \"geo-map-loading-shell\",\n        markers: [{ id: \"truck-31\", lat: 32.7157, lng: -117.1611 }],\n      }),\n    );\n\n    expect(\n      document.querySelector('[data-slot=\"geo-map-loading\"]'),\n    ).not.toBeNull();\n\n    await waitFor(() => {\n      expect(tileLayerPropsSpy).toHaveBeenCalled();\n    });\n    await waitFor(() => {\n      expect(\n        document.querySelector('[data-slot=\"geo-map-loading\"]'),\n      ).toBeNull();\n    });\n  });\n\n  test(\"applies the geo-map popup class for deterministic shell styling\", async () => {\n    render(\n      createElement(GeoMap, {\n        id: \"geo-map-popup-class\",\n        markers: [\n          {\n            id: \"truck-31\",\n            lat: 32.7157,\n            lng: -117.1611,\n            label: \"Truck 31\",\n            description: \"Returning to hub\",\n          },\n        ],\n      }),\n    );\n\n    await waitFor(() => {\n      expect(popupPropsSpy).toHaveBeenCalled();\n    });\n    const popupProps = popupPropsSpy.mock.calls[0]?.[0];\n    expect(popupProps.className).toContain(\"geo-map-popup\");\n  });\n\n  test(\"keeps popup keyboard dismissible and exposes a close button\", async () => {\n    render(\n      createElement(GeoMap, {\n        id: \"geo-map-popup-a11y\",\n        markers: [\n          {\n            id: \"truck-31\",\n            lat: 32.7157,\n            lng: -117.1611,\n            label: \"Truck 31\",\n            description: \"Returning to hub\",\n          },\n        ],\n      }),\n    );\n\n    await waitFor(() => {\n      expect(popupPropsSpy).toHaveBeenCalled();\n    });\n\n    const popupProps = popupPropsSpy.mock.calls[0]?.[0] as\n      | { closeButton?: boolean; closeOnEscapeKey?: boolean }\n      | undefined;\n    expect(popupProps?.closeButton).toBe(true);\n    expect(popupProps?.closeOnEscapeKey).toBe(true);\n  });\n\n  test(\"announces map region with an accessible label\", async () => {\n    render(\n      createElement(GeoMap, {\n        id: \"geo-map-map-aria\",\n        title: \"Fleet Positions\",\n        description: \"Last telemetry update: 30s ago\",\n        markers: [{ id: \"truck-31\", lat: 32.7157, lng: -117.1611 }],\n      }),\n    );\n\n    await waitFor(() => {\n      expect(mapContainerPropsSpy).toHaveBeenCalled();\n    });\n\n    await waitFor(() => {\n      expect(mockMap.getContainer).toHaveBeenCalled();\n    });\n    expect(mapContainerAttributeSpy).toHaveBeenCalledWith(\"role\", \"region\");\n    expect(mapContainerAttributeSpy).toHaveBeenCalledWith(\n      \"aria-label\",\n      \"Fleet Positions. Last telemetry update: 30s ago\",\n    );\n  });\n\n  test(\"adds descriptive marker title and alt text with label and description\", async () => {\n    render(\n      createElement(GeoMap, {\n        id: \"geo-map-marker-aria\",\n        markers: [\n          {\n            id: \"truck-31\",\n            lat: 32.7157,\n            lng: -117.1611,\n            label: \"Truck 31\",\n            description: \"Returning to hub\",\n            icon: { type: \"emoji\", value: \"🚚\" },\n          },\n        ],\n      }),\n    );\n\n    await waitFor(() => {\n      expect(markerPropsSpy).toHaveBeenCalled();\n    });\n\n    const markerProps = markerPropsSpy.mock.calls[0]?.[0] as\n      | { title?: string; alt?: string }\n      | undefined;\n    expect(markerProps?.title).toBe(\"Truck 31. Returning to hub\");\n    expect(markerProps?.alt).toBe(\"Truck 31. Returning to hub\");\n  });\n\n  test(\"closes popups when escape is pressed\", async () => {\n    render(\n      createElement(GeoMap, {\n        id: \"geo-map-popup-escape-close\",\n        markers: [{ id: \"truck-31\", lat: 32.7157, lng: -117.1611 }],\n      }),\n    );\n\n    await waitFor(() => {\n      expect(mockMap.getContainer).toHaveBeenCalled();\n    });\n\n    act(() => {\n      document.dispatchEvent(\n        new KeyboardEvent(\"keydown\", {\n          key: \"Escape\",\n        }),\n      );\n    });\n\n    expect(mockMap.closePopup).toHaveBeenCalledTimes(1);\n  });\n\n  test(\"calls route click callback from polyline interactions\", async () => {\n    const onRouteClick = vi.fn();\n\n    render(\n      createElement(GeoMap, {\n        id: \"geo-map-route-click\",\n        markers: [{ id: \"truck-31\", lat: 32.7157, lng: -117.1611 }],\n        routes: [\n          {\n            id: \"route-1\",\n            points: [\n              { lat: 32.7157, lng: -117.1611 },\n              { lat: 32.7357, lng: -117.1411 },\n            ],\n          },\n        ],\n        onRouteClick,\n      }),\n    );\n\n    await waitFor(() => {\n      expect(polylinePropsSpy).toHaveBeenCalled();\n    });\n\n    const routeProps = polylinePropsSpy.mock.calls[0]?.[0] as\n      | { eventHandlers?: { click?: () => void } }\n      | undefined;\n\n    act(() => {\n      routeProps?.eventHandlers?.click?.();\n    });\n\n    expect(onRouteClick).toHaveBeenCalledTimes(1);\n  });\n\n  test(\"expands cluster markers by flying to cluster zoom\", async () => {\n    render(\n      createElement(GeoMap, {\n        id: \"geo-map-cluster-expand\",\n        markers: [\n          { id: \"cluster-a\", lat: 37.7749, lng: -122.4194 },\n          { id: \"cluster-b\", lat: 37.775, lng: -122.4195 },\n        ],\n        clustering: { enabled: true, minPoints: 2 },\n      }),\n    );\n\n    await waitFor(() => {\n      expect(markerPropsSpy).toHaveBeenCalled();\n    });\n\n    const clusterMarkerProps = markerPropsSpy.mock.calls\n      .map(\n        (call) =>\n          call[0] as { title?: string; eventHandlers?: { click?: () => void } },\n      )\n      .find(\n        (props) =>\n          typeof props.title === \"string\" &&\n          props.title.startsWith(\"Cluster containing\"),\n      );\n\n    expect(clusterMarkerProps).toBeDefined();\n    act(() => {\n      clusterMarkerProps?.eventHandlers?.click?.();\n    });\n\n    expect(mockMap.flyTo).toHaveBeenCalledTimes(1);\n  });\n\n  test(\"accepts css variable overrides for popup and tooltip shell theming\", () => {\n    render(\n      createElement(GeoMap, {\n        id: \"geo-map-token-overrides\",\n        markers: [{ id: \"truck-31\", lat: 32.7157, lng: -117.1611 }],\n        style: {\n          \"--geo-map-popup-bg\": \"var(--card)\",\n          \"--geo-map-popup-fg\": \"var(--card-foreground)\",\n          \"--geo-map-tooltip-bg\": \"var(--accent)\",\n        },\n      }),\n    );\n\n    const root = document.querySelector(\n      '[data-tool-ui-id=\"geo-map-token-overrides\"]',\n    ) as HTMLElement | null;\n\n    expect(root).not.toBeNull();\n    expect(root?.style.getPropertyValue(\"--geo-map-popup-bg\")).toBe(\n      \"var(--card)\",\n    );\n    expect(root?.style.getPropertyValue(\"--geo-map-popup-fg\")).toBe(\n      \"var(--card-foreground)\",\n    );\n    expect(root?.style.getPropertyValue(\"--geo-map-tooltip-bg\")).toBe(\n      \"var(--accent)\",\n    );\n  });\n\n  test(\"does not inject runtime style tags for shell styles\", () => {\n    render(\n      createElement(GeoMap, {\n        id: \"geo-map-zoom-controls-style\",\n        markers: [{ id: \"truck-31\", lat: 32.7157, lng: -117.1611 }],\n      }),\n    );\n\n    const root = document.querySelector(\n      '[data-tool-ui-id=\"geo-map-zoom-controls-style\"]',\n    ) as HTMLElement | null;\n    expect(root?.querySelector(\"style\")).toBeNull();\n    expect(root?.getAttribute(\"data-slot\")).toBe(\"geo-map\");\n  });\n\n  test(\"hides tooltip while popup is open\", async () => {\n    render(\n      createElement(GeoMap, {\n        id: \"geo-map-tooltip-popup-interaction\",\n        markers: [\n          {\n            id: \"truck-31\",\n            lat: 32.7157,\n            lng: -117.1611,\n            label: \"Truck 31\",\n            description: \"Returning to hub\",\n            tooltip: \"always\",\n          },\n        ],\n      }),\n    );\n\n    await waitFor(() => {\n      expect(popupPropsSpy).toHaveBeenCalled();\n      expect(tooltipPropsSpy).toHaveBeenCalled();\n    });\n\n    const popupProps = popupPropsSpy.mock.calls[0]?.[0] as\n      | {\n          eventHandlers?: {\n            add?: () => void;\n            remove?: () => void;\n          };\n        }\n      | undefined;\n\n    expect(document.querySelector(\".geo-map-tooltip\")).not.toBeNull();\n    expect(popupProps?.eventHandlers?.add).toBeTypeOf(\"function\");\n    expect(popupProps?.eventHandlers?.remove).toBeTypeOf(\"function\");\n\n    act(() => {\n      popupProps?.eventHandlers?.add?.();\n    });\n\n    await waitFor(() => {\n      expect(document.querySelector(\".geo-map-tooltip\")).toBeNull();\n    });\n\n    act(() => {\n      popupProps?.eventHandlers?.remove?.();\n    });\n\n    await waitFor(() => {\n      expect(document.querySelector(\".geo-map-tooltip\")).not.toBeNull();\n    });\n  });\n\n  test(\"uses light tiles by default when no theme is provided\", async () => {\n    render(\n      createElement(GeoMap, {\n        id: \"geo-map-default-theme\",\n        markers: [{ id: \"truck-31\", lat: 32.7157, lng: -117.1611 }],\n      }),\n    );\n\n    await waitFor(() => {\n      expect(tileLayerPropsSpy).toHaveBeenCalled();\n    });\n\n    const firstTileLayerProps = tileLayerPropsSpy.mock.calls[0]?.[0] as\n      | { url?: string }\n      | undefined;\n    expect(firstTileLayerProps?.url).toContain(\"light_all\");\n  });\n\n  test(\"uses dark tiles when theme is explicitly dark\", async () => {\n    render(\n      createElement(GeoMap, {\n        id: \"geo-map-dark-theme\",\n        markers: [{ id: \"truck-31\", lat: 32.7157, lng: -117.1611 }],\n        theme: \"dark\",\n      }),\n    );\n\n    await waitFor(() => {\n      expect(tileLayerPropsSpy).toHaveBeenCalled();\n    });\n\n    const firstTileLayerProps = tileLayerPropsSpy.mock.calls[0]?.[0] as\n      | { url?: string }\n      | undefined;\n    expect(firstTileLayerProps?.url).toContain(\"dark_all\");\n  });\n\n  test(\"inherits dark tiles from document theme when theme prop is omitted\", async () => {\n    document.documentElement.setAttribute(\"data-theme\", \"dark\");\n\n    render(\n      createElement(GeoMap, {\n        id: \"geo-map-document-theme-dark\",\n        markers: [{ id: \"truck-31\", lat: 32.7157, lng: -117.1611 }],\n      }),\n    );\n\n    await waitFor(() => {\n      const lastProps = tileLayerPropsSpy.mock.calls.at(-1)?.[0] as\n        | { url?: string }\n        | undefined;\n      expect(lastProps?.url).toContain(\"dark_all\");\n    });\n\n    document.documentElement.removeAttribute(\"data-theme\");\n  });\n\n  test(\"sets a dark map canvas fallback background token in dark theme\", () => {\n    render(\n      createElement(GeoMap, {\n        id: \"geo-map-dark-canvas-fallback\",\n        markers: [{ id: \"truck-31\", lat: 32.7157, lng: -117.1611 }],\n        theme: \"dark\",\n      }),\n    );\n\n    const root = document.querySelector(\n      '[data-tool-ui-id=\"geo-map-dark-canvas-fallback\"]',\n    ) as HTMLElement | null;\n    expect(root).not.toBeNull();\n    expect(root?.style.getPropertyValue(\"--geo-map-canvas-bg\")).toBe(\n      \"var(--background)\",\n    );\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/tests/tool-ui/geo-map/schema.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\n\nimport {\n  parseSerializableGeoMap,\n  safeParseSerializableGeoMap,\n} from \"@/components/tool-ui/geo-map/schema\";\n\ndescribe(\"GeoMap schema\", () => {\n  test(\"parses a valid single-marker payload\", () => {\n    const parsed = parseSerializableGeoMap({\n      id: \"geo-map-store\",\n      title: \"Store Location\",\n      markers: [{ id: \"store-1\", lat: 37.7749, lng: -122.4194 }],\n    });\n\n    expect(parsed.id).toBe(\"geo-map-store\");\n    expect(parsed.markers).toHaveLength(1);\n  });\n\n  test(\"supports multi-marker payloads\", () => {\n    const parsed = parseSerializableGeoMap({\n      id: \"geo-map-fleet\",\n      markers: [\n        { id: \"truck-1\", lat: 34.0522, lng: -118.2437 },\n        { id: \"truck-2\", lat: 36.1699, lng: -115.1398 },\n      ],\n    });\n\n    expect(parsed.markers).toHaveLength(2);\n  });\n\n  test(\"rejects marker coordinates outside supported ranges\", () => {\n    expect(\n      safeParseSerializableGeoMap({\n        id: \"geo-map-invalid-lat\",\n        markers: [{ lat: 90.1, lng: 10 }],\n      }),\n    ).toBeNull();\n\n    expect(\n      safeParseSerializableGeoMap({\n        id: \"geo-map-invalid-lng\",\n        markers: [{ lat: 10, lng: 180.1 }],\n      }),\n    ).toBeNull();\n  });\n\n  test(\"rejects duplicate marker ids\", () => {\n    expect(\n      safeParseSerializableGeoMap({\n        id: \"geo-map-duplicate-markers\",\n        markers: [\n          { id: \"asset-1\", lat: 10, lng: 20 },\n          { id: \"asset-1\", lat: 30, lng: 40 },\n        ],\n      }),\n    ).toBeNull();\n  });\n\n  test(\"supports fit and center viewport modes\", () => {\n    const fit = safeParseSerializableGeoMap({\n      id: \"geo-map-fit\",\n      markers: [{ lat: 37.7749, lng: -122.4194 }],\n      viewport: { mode: \"fit\", padding: 48, maxZoom: 12 },\n    });\n    expect(fit).not.toBeNull();\n\n    const center = safeParseSerializableGeoMap({\n      id: \"geo-map-center\",\n      markers: [{ lat: 37.7749, lng: -122.4194 }],\n      viewport: {\n        mode: \"center\",\n        center: { lat: 37.7749, lng: -122.4194 },\n        zoom: 10,\n      },\n    });\n    expect(center).not.toBeNull();\n  });\n\n  test(\"rejects center viewport when zoom is missing\", () => {\n    expect(\n      safeParseSerializableGeoMap({\n        id: \"geo-map-center-missing-zoom\",\n        markers: [{ lat: 37.7749, lng: -122.4194 }],\n        viewport: { mode: \"center\", center: { lat: 37.7749, lng: -122.4194 } },\n      }),\n    ).toBeNull();\n  });\n\n  test(\"accepts valid clustering config\", () => {\n    const parsed = safeParseSerializableGeoMap({\n      id: \"geo-map-clustered\",\n      markers: [\n        { id: \"a1\", lat: 37.7749, lng: -122.4194 },\n        { id: \"a2\", lat: 37.7751, lng: -122.4192 },\n      ],\n      clustering: {\n        enabled: true,\n        radius: 50,\n        maxZoom: 14,\n        minPoints: 3,\n      },\n    });\n\n    expect(parsed).not.toBeNull();\n  });\n\n  test(\"accepts explicit light/dark themes and rejects auto theme\", () => {\n    const light = safeParseSerializableGeoMap({\n      id: \"geo-map-theme-light\",\n      markers: [{ lat: 37.7749, lng: -122.4194 }],\n      theme: \"light\",\n    });\n    expect(light).not.toBeNull();\n\n    const dark = safeParseSerializableGeoMap({\n      id: \"geo-map-theme-dark\",\n      markers: [{ lat: 37.7749, lng: -122.4194 }],\n      theme: \"dark\",\n    });\n    expect(dark).not.toBeNull();\n\n    const auto = safeParseSerializableGeoMap({\n      id: \"geo-map-theme-auto\",\n      markers: [{ lat: 37.7749, lng: -122.4194 }],\n      theme: \"auto\",\n    });\n    expect(auto).toBeNull();\n  });\n\n  test(\"rejects invalid clustering ranges\", () => {\n    expect(\n      safeParseSerializableGeoMap({\n        id: \"geo-map-bad-cluster-radius\",\n        markers: [{ lat: 37.7749, lng: -122.4194 }],\n        clustering: { radius: 10 },\n      }),\n    ).toBeNull();\n\n    expect(\n      safeParseSerializableGeoMap({\n        id: \"geo-map-bad-cluster-min-points\",\n        markers: [{ lat: 37.7749, lng: -122.4194 }],\n        clustering: { minPoints: 1 },\n      }),\n    ).toBeNull();\n  });\n\n  test(\"accepts valid route payload and fit target\", () => {\n    const parsed = parseSerializableGeoMap({\n      id: \"geo-map-routes\",\n      markers: [{ id: \"depot\", lat: 37.7749, lng: -122.4194 }],\n      routes: [\n        {\n          id: \"route-1\",\n          points: [\n            { lat: 37.7749, lng: -122.4194 },\n            { lat: 37.7849, lng: -122.4094 },\n          ],\n          color: \"#2563EB\",\n          weight: 4,\n          opacity: 0.75,\n          dashArray: \"6 4\",\n        },\n      ],\n      viewport: { mode: \"fit\", target: \"all\" },\n    });\n\n    expect(parsed.routes).toHaveLength(1);\n  });\n\n  test(\"rejects route with less than two points\", () => {\n    expect(\n      safeParseSerializableGeoMap({\n        id: \"geo-map-invalid-route\",\n        markers: [{ lat: 37.7749, lng: -122.4194 }],\n        routes: [\n          {\n            id: \"route-1\",\n            points: [{ lat: 37.7749, lng: -122.4194 }],\n          },\n        ],\n      }),\n    ).toBeNull();\n  });\n\n  test(\"rejects duplicate route ids\", () => {\n    expect(\n      safeParseSerializableGeoMap({\n        id: \"geo-map-duplicate-routes\",\n        markers: [{ lat: 37.7749, lng: -122.4194 }],\n        routes: [\n          {\n            id: \"route-1\",\n            points: [\n              { lat: 37.7749, lng: -122.4194 },\n              { lat: 37.7849, lng: -122.4094 },\n            ],\n          },\n          {\n            id: \"route-1\",\n            points: [\n              { lat: 37.7649, lng: -122.4294 },\n              { lat: 37.7549, lng: -122.4394 },\n            ],\n          },\n        ],\n      }),\n    ).toBeNull();\n  });\n\n  test(\"accepts supported marker icon variants\", () => {\n    const parsed = safeParseSerializableGeoMap({\n      id: \"geo-map-icons\",\n      markers: [\n        {\n          id: \"dot\",\n          lat: 37.7749,\n          lng: -122.4194,\n          icon: {\n            type: \"dot\",\n            color: \"#3B82F6\",\n            borderColor: \"#1D4ED8\",\n            radius: 8,\n          },\n        },\n        {\n          id: \"emoji\",\n          lat: 37.7849,\n          lng: -122.4094,\n          icon: { type: \"emoji\", value: \"🚚\", size: 24, bgColor: \"#0F172A\" },\n        },\n        {\n          id: \"image\",\n          lat: 37.7649,\n          lng: -122.4294,\n          icon: {\n            type: \"image\",\n            url: \"https://cdn.example.com/truck.png\",\n            width: 28,\n            height: 28,\n            borderRadius: 14,\n          },\n        },\n      ],\n    });\n\n    expect(parsed).not.toBeNull();\n  });\n\n  test(\"rejects invalid image icon protocol\", () => {\n    expect(\n      safeParseSerializableGeoMap({\n        id: \"geo-map-invalid-image-icon\",\n        markers: [\n          {\n            id: \"image\",\n            lat: 37.7749,\n            lng: -122.4194,\n            icon: {\n              type: \"image\",\n              url: \"data:image/png;base64,AAA\",\n            },\n          },\n        ],\n      }),\n    ).toBeNull();\n  });\n\n  test(\"preserves backward compatibility for marker-only payloads\", () => {\n    const parsed = parseSerializableGeoMap({\n      id: \"geo-map-back-compat\",\n      markers: [{ id: \"legacy-1\", lat: 40.7128, lng: -74.006 }],\n    });\n\n    expect(parsed.markers).toHaveLength(1);\n    expect(parsed.routes).toBeUndefined();\n    expect(parsed.clustering).toBeUndefined();\n  });\n\n  test(\"ignores unknown presentational props from payloads\", () => {\n    const parsed = parseSerializableGeoMap({\n      id: \"geo-map-unknown-presentational\",\n      markers: [{ id: \"legacy-1\", lat: 40.7128, lng: -74.006 }],\n      mapClassName: \"should-be-stripped\",\n      overlayClassName: \"should-be-stripped\",\n      popupContentClassName: \"should-be-stripped\",\n    });\n\n    expect(parsed).not.toHaveProperty(\"mapClassName\");\n    expect(parsed).not.toHaveProperty(\"overlayClassName\");\n    expect(parsed).not.toHaveProperty(\"popupContentClassName\");\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/tests/tool-ui/geo-map/spatial.test.ts",
    "content": "import { describe, expect, test, vi } from \"vitest\";\n\nvi.mock(\"@/components/tool-ui/geo-map/_adapter\", () => ({\n  CircleMarker: () => null,\n  MapContainer: () => null,\n  Marker: () => null,\n  Polyline: () => null,\n  TileLayer: () => null,\n  ZoomControl: () => null,\n  useMap: () => ({}),\n  useMapEvents: () => ({}),\n}));\n\nvi.mock(\"@/components/tool-ui/geo-map/geo-map-icons\", () => ({\n  createClusterIcon: () => ({}),\n  resolveMarkerIcon: () => null,\n}));\n\nvi.mock(\"@/components/tool-ui/geo-map/geo-map-overlays\", () => ({\n  GeoMapOverlays: () => null,\n}));\n\nimport {\n  collectFitPoints,\n  getClustersForDatelineAwareBbox,\n  resolveFitPointsWithFallback,\n  splitDatelineBbox,\n  toSafeExpansionZoom,\n  type GeoMapClusterFeature,\n} from \"@/components/tool-ui/geo-map/geo-map-engine\";\n\nconst markers = [\n  { id: \"m1\", lat: 37.7749, lng: -122.4194 },\n  { id: \"m2\", lat: 34.0522, lng: -118.2437 },\n];\n\nconst routes = [\n  {\n    id: \"r1\",\n    points: [\n      { lat: 40.7128, lng: -74.006 },\n      { lat: 40.7306, lng: -73.9352 },\n    ],\n  },\n];\n\ndescribe(\"geo-map spatial helpers\", () => {\n  test(\"collectFitPoints returns points based on target\", () => {\n    expect(collectFitPoints(markers, routes, \"markers\")).toEqual([\n      [37.7749, -122.4194],\n      [34.0522, -118.2437],\n    ]);\n\n    expect(collectFitPoints(markers, routes, \"routes\")).toEqual([\n      [40.7128, -74.006],\n      [40.7306, -73.9352],\n    ]);\n\n    expect(collectFitPoints(markers, routes, \"all\")).toEqual([\n      [37.7749, -122.4194],\n      [34.0522, -118.2437],\n      [40.7128, -74.006],\n      [40.7306, -73.9352],\n    ]);\n  });\n\n  test(\"resolveFitPointsWithFallback falls back to markers when target has no points\", () => {\n    const points = resolveFitPointsWithFallback(markers, [], \"routes\");\n\n    expect(points).toEqual([\n      [37.7749, -122.4194],\n      [34.0522, -118.2437],\n    ]);\n  });\n\n  test(\"splitDatelineBbox returns one box when not crossing antimeridian\", () => {\n    expect(splitDatelineBbox([-130, 20, -100, 50])).toEqual([\n      [-130, 20, -100, 50],\n    ]);\n  });\n\n  test(\"splitDatelineBbox splits into two boxes when crossing antimeridian\", () => {\n    expect(splitDatelineBbox([170, -20, -170, 20])).toEqual([\n      [170, -20, 180, 20],\n      [-180, -20, -170, 20],\n    ]);\n  });\n\n  test(\"getClustersForDatelineAwareBbox dedupes cluster and point features\", () => {\n    const featureA = {\n      type: \"Feature\",\n      geometry: { type: \"Point\", coordinates: [1, 2] },\n      properties: { cluster: true, cluster_id: 42, point_count: 4 },\n      id: 42,\n    } satisfies GeoMapClusterFeature;\n\n    const featureB = {\n      type: \"Feature\",\n      geometry: { type: \"Point\", coordinates: [10, 20] },\n      properties: { cluster: false, markerId: \"m1\" },\n      id: \"m1\",\n    } satisfies GeoMapClusterFeature;\n\n    const result = getClustersForDatelineAwareBbox(\n      [170, -20, -170, 20],\n      5,\n      () => [featureA, featureB],\n    );\n\n    expect(result).toHaveLength(2);\n    expect(result).toContainEqual(featureA);\n    expect(result).toContainEqual(featureB);\n  });\n\n  test(\"toSafeExpansionZoom clamps and normalizes zoom values\", () => {\n    expect(toSafeExpansionZoom(NaN)).toBe(2);\n    expect(toSafeExpansionZoom(0)).toBe(1);\n    expect(toSafeExpansionZoom(30)).toBe(22);\n    expect(toSafeExpansionZoom(10.9)).toBe(11);\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/tests/tool-ui/scripts/install-git-hooks.test.ts",
    "content": "import { describe, expect, test, vi } from \"vitest\";\nimport { configureGitHooks } from \"@/scripts/install-git-hooks\";\n\ndescribe(\"install git hooks\", () => {\n  test(\"skips configuration when not inside a git work tree\", () => {\n    const runCommand =\n      vi.fn<(command: string, args: string[]) => number | null>();\n    runCommand.mockReturnValueOnce(1);\n\n    const status = configureGitHooks(runCommand);\n\n    expect(status).toBe(\"skipped\");\n    expect(runCommand).toHaveBeenCalledTimes(1);\n    expect(runCommand).toHaveBeenCalledWith(\"git\", [\n      \"rev-parse\",\n      \"--is-inside-work-tree\",\n    ]);\n  });\n\n  test(\"configures the repository hooks path to .githooks\", () => {\n    const runCommand =\n      vi.fn<(command: string, args: string[]) => number | null>();\n    runCommand.mockReturnValueOnce(0).mockReturnValueOnce(0);\n\n    const status = configureGitHooks(runCommand);\n\n    expect(status).toBe(\"configured\");\n    expect(runCommand).toHaveBeenNthCalledWith(1, \"git\", [\n      \"rev-parse\",\n      \"--is-inside-work-tree\",\n    ]);\n    expect(runCommand).toHaveBeenNthCalledWith(2, \"git\", [\n      \"config\",\n      \"--local\",\n      \"core.hooksPath\",\n      \".githooks\",\n    ]);\n  });\n\n  test(\"returns failed when git config cannot set hooksPath\", () => {\n    const runCommand =\n      vi.fn<(command: string, args: string[]) => number | null>();\n    runCommand.mockReturnValueOnce(0).mockReturnValueOnce(1);\n\n    const status = configureGitHooks(runCommand);\n\n    expect(status).toBe(\"failed\");\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/tests/tool-ui/shared/safe-navigation.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\n\nimport { resolveSafeNavigationHref } from \"@/components/tool-ui/shared/media/safe-navigation\";\n\ndescribe(\"resolveSafeNavigationHref\", () => {\n  test(\"returns undefined for unsafe URLs\", () => {\n    expect(resolveSafeNavigationHref(\"javascript:alert(1)\")).toBeUndefined();\n    expect(resolveSafeNavigationHref(\"data:text/html,hello\")).toBeUndefined();\n  });\n\n  test(\"returns first safe candidate from fallbacks\", () => {\n    expect(\n      resolveSafeNavigationHref(\n        undefined,\n        \"javascript:alert(1)\",\n        \"https://assistant-ui.com/docs\",\n      ),\n    ).toBe(\"https://assistant-ui.com/docs\");\n  });\n\n  test(\"keeps safe http(s) URLs\", () => {\n    expect(resolveSafeNavigationHref(\"https://assistant-ui.com\")).toBe(\n      \"https://assistant-ui.com/\",\n    );\n    expect(resolveSafeNavigationHref(\"http://example.com/path\")).toBe(\n      \"http://example.com/path\",\n    );\n  });\n\n  test(\"keeps safe relative URLs\", () => {\n    expect(resolveSafeNavigationHref(\"/docs\")).toBe(\"/docs\");\n    expect(resolveSafeNavigationHref(\"./reference\")).toBe(\"./reference\");\n    expect(resolveSafeNavigationHref(\"../guides\")).toBe(\"../guides\");\n  });\n\n  test(\"rejects protocol-relative URLs\", () => {\n    expect(resolveSafeNavigationHref(\"//evil.example.com\")).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/tests/tool-ui/social/link-sanitization.test.ts",
    "content": "import { createElement } from \"react\";\nimport { renderToStaticMarkup } from \"react-dom/server\";\nimport { describe, expect, test } from \"vitest\";\n\nimport { XPost } from \"@/components/tool-ui/x-post\";\nimport { LinkedInPost } from \"@/components/tool-ui/linkedin-post\";\nimport { ImageGallery } from \"@/components/tool-ui/image-gallery\";\nimport { LinkValue } from \"@/components/tool-ui/data-table/formatters\";\n\ndescribe(\"tool-ui social/media link sanitization\", () => {\n  test(\"does not render unsafe X post link-preview href\", () => {\n    const html = renderToStaticMarkup(\n      createElement(XPost, {\n        post: {\n          id: \"x-1\",\n          author: {\n            name: \"Ada Lovelace\",\n            handle: \"ada\",\n            avatarUrl: \"https://example.com/avatar.png\",\n          },\n          linkPreview: {\n            url: \"javascript:alert(1)\",\n            title: \"unsafe\",\n          },\n        },\n      }),\n    );\n\n    expect(html).not.toContain('href=\"javascript:alert(1)\"');\n  });\n\n  test(\"does not render unsafe LinkedIn link-preview href\", () => {\n    const html = renderToStaticMarkup(\n      createElement(LinkedInPost, {\n        post: {\n          id: \"li-1\",\n          author: {\n            name: \"Grace Hopper\",\n            avatarUrl: \"https://example.com/avatar.png\",\n          },\n          linkPreview: {\n            url: \"javascript:alert(1)\",\n            title: \"unsafe\",\n          },\n        },\n      }),\n    );\n\n    expect(html).not.toContain('href=\"javascript:alert(1)\"');\n  });\n\n  test(\"does not render unsafe ImageGallery source href\", () => {\n    const html = renderToStaticMarkup(\n      createElement(ImageGallery, {\n        id: \"gallery-1\",\n        images: [\n          {\n            id: \"img-1\",\n            src: \"https://example.com/image.jpg\",\n            alt: \"Image\",\n            width: 1200,\n            height: 800,\n            source: {\n              label: \"Unsafe source\",\n              url: \"javascript:alert(1)\",\n            },\n          },\n        ],\n      }),\n    );\n\n    expect(html).not.toContain('href=\"javascript:alert(1)\"');\n  });\n\n  test(\"does not render unsafe DataTable link href\", () => {\n    const html = renderToStaticMarkup(\n      createElement(LinkValue, {\n        value: \"Click me\",\n        row: { target: \"javascript:alert(1)\" },\n        options: { kind: \"link\", hrefKey: \"target\", external: true },\n      }),\n    );\n\n    expect(html).not.toContain('href=\"javascript:alert(1)\"');\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/tests/tool-ui/video/media-events.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\n\nimport {\n  getMuteMediaEvent,\n  normalizeVideoDataForCallback,\n} from \"@/components/tool-ui/video/video-helpers\";\n\ndescribe(\"video mute media event helper\", () => {\n  test(\"returns null when mute state does not change\", () => {\n    expect(getMuteMediaEvent(true, true)).toBeNull();\n    expect(getMuteMediaEvent(false, false)).toBeNull();\n  });\n\n  test(\"returns mute/unmute only when mute state toggles\", () => {\n    expect(getMuteMediaEvent(false, true)).toBe(\"mute\");\n    expect(getMuteMediaEvent(true, false)).toBe(\"unmute\");\n  });\n\n  test(\"normalizes callback payload to rendered defaults\", () => {\n    const normalized = normalizeVideoDataForCallback(\n      {\n        id: \"video-helper-payload\",\n        assetId: \"asset-helper-payload\",\n        src: \"https://archive.org/download/NatureStockVideo/IMG_9500_.mp4\",\n        source: {\n          label: \"Archive.org\",\n          url: \"https://archive.org\",\n        },\n      },\n      {\n        ratio: \"16:9\",\n        fit: \"cover\",\n        locale: \"en-US\",\n        sanitizedHref: undefined,\n        sanitizedSourceUrl: \"https://archive.org/\",\n      },\n    );\n\n    expect(normalized.ratio).toBe(\"16:9\");\n    expect(normalized.fit).toBe(\"cover\");\n    expect(normalized.locale).toBe(\"en-US\");\n    expect(normalized.source?.url).toBe(\"https://archive.org/\");\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/tests/tool-ui/weather-widget/canvas-resolver-parity.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\n\nimport {\n  TIME_CHECKPOINTS,\n  TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES,\n  resolveWeatherEffectsCanvasProps,\n  type TimeCheckpoint,\n} from \"@/lib/weather-authoring/weather-widget/effects\";\nimport type { WeatherConditionCode } from \"@/lib/weather-authoring/weather-widget/schema\";\nimport {\n  getRawBaseParamsForCondition,\n  mergeWithOverrides,\n} from \"@/app/sandbox/weather-compositor/presets\";\nimport { mapCompositorParamsToCanvasProps } from \"@/app/sandbox/weather-tuning/lib/map-to-canvas-props\";\nimport { resolveCompositorParamsAtTime } from \"@/app/sandbox/weather-tuning/lib/resolve-params\";\nimport { createStudioTimestamp } from \"@/app/sandbox/weather-tuning/lib/studio-timestamp\";\nimport { mapToolUiPresetsToCompositor } from \"@/app/sandbox/weather-tuning/lib/tool-ui-import\";\n\nconst REFERENCE_DATE = new Date(\"2026-01-01T00:00:00Z\");\nconst REPO_CHECKPOINT_OVERRIDES = mapToolUiPresetsToCompositor(\n  TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES,\n);\n\nfunction resolveStudioCanvasProps(\n  condition: WeatherConditionCode,\n  timeOfDay: number,\n) {\n  const timestamp = createStudioTimestamp(timeOfDay, REFERENCE_DATE);\n  const rawBaseAtTime = getRawBaseParamsForCondition(condition, timestamp);\n  rawBaseAtTime.celestial.timeOfDay = timeOfDay;\n\n  const getRawBaseForCheckpoint = (checkpoint: TimeCheckpoint) => {\n    const checkpointTime = TIME_CHECKPOINTS[checkpoint];\n    const checkpointTimestamp = createStudioTimestamp(\n      checkpointTime,\n      REFERENCE_DATE,\n    );\n    const base = getRawBaseParamsForCondition(condition, checkpointTimestamp);\n    base.celestial.timeOfDay = checkpointTime;\n    return base;\n  };\n\n  const resolved = resolveCompositorParamsAtTime({\n    timeOfDay,\n    rawBaseAtTime,\n    getRawBaseForCheckpoint,\n    repoCheckpointOverrides: REPO_CHECKPOINT_OVERRIDES[condition],\n    getRepoBaseForCheckpoint: (checkpoint) =>\n      mergeWithOverrides(\n        getRawBaseForCheckpoint(checkpoint),\n        REPO_CHECKPOINT_OVERRIDES[condition]?.[checkpoint],\n      ),\n    userCheckpointOverrides: undefined,\n  });\n\n  return mapCompositorParamsToCanvasProps(resolved);\n}\n\ndescribe(\"shared weather canvas resolver parity\", () => {\n  test.each([\n    { condition: \"clear\" as const, timeOfDay: 0.37 },\n    { condition: \"cloudy\" as const, timeOfDay: 0.62 },\n    { condition: \"rain\" as const, timeOfDay: 0.88 },\n  ])(\n    \"matches studio preview for $condition at $timeOfDay (interpolated mode)\",\n    ({ condition, timeOfDay }) => {\n      const timestamp = createStudioTimestamp(timeOfDay, REFERENCE_DATE);\n      const shared = resolveWeatherEffectsCanvasProps({\n        conditionCode: condition,\n        timestamp,\n        timeOfDay,\n        tunedPresets: TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES,\n        checkpointMode: \"interpolated\",\n      });\n\n      const studio = resolveStudioCanvasProps(condition, timeOfDay);\n\n      expect(shared).toEqual(studio);\n    },\n  );\n\n  test(\"checkpoint mode changes output between keyframes\", () => {\n    const condition: WeatherConditionCode = \"cloudy\";\n    const timeOfDay = 0.37;\n    const timestamp = createStudioTimestamp(timeOfDay, REFERENCE_DATE);\n\n    const nearest = resolveWeatherEffectsCanvasProps({\n      conditionCode: condition,\n      timestamp,\n      tunedPresets: TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES,\n      checkpointMode: \"nearest\",\n    });\n\n    const interpolated = resolveWeatherEffectsCanvasProps({\n      conditionCode: condition,\n      timestamp,\n      tunedPresets: TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES,\n      checkpointMode: \"interpolated\",\n    });\n\n    expect(interpolated).not.toEqual(nearest);\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/tests/tool-ui/weather-widget/glass-style-resolver.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\n\nimport { resolveGlassBackdropFilterStyles } from \"@/lib/weather-authoring/weather-widget/effects\";\n\ndescribe(\"resolveGlassBackdropFilterStyles\", () => {\n  it(\"returns the SVG glass styles when backdrop-filter is present\", () => {\n    const glassStyles = {\n      backdropFilter: \"url(data:image/svg+xml,...) blur(10px)\",\n      WebkitBackdropFilter: \"url(data:image/svg+xml,...) blur(10px)\",\n    } as const;\n\n    expect(\n      resolveGlassBackdropFilterStyles({ glassStyles, blurAmount: 30 }),\n    ).toBe(glassStyles);\n  });\n\n  it(\"falls back to a simple blur when SVG glass isn't active yet\", () => {\n    const result = resolveGlassBackdropFilterStyles({\n      glassStyles: {},\n      blurAmount: 24,\n    });\n\n    expect(result).toEqual({\n      backdropFilter: \"blur(24px)\",\n      WebkitBackdropFilter: \"blur(24px)\",\n    });\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/tests/tool-ui/weather-widget/parameter-mapper.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\n\nimport {\n  configToRainProps,\n  configToSnowProps,\n  getMoonPhase,\n  getSceneBrightness,\n  timeOfDayToSunAltitude,\n  mapWeatherToEffects,\n} from \"@/lib/weather-authoring/weather-widget/effects/parameter-mapper\";\n\ndescribe(\"weather-widget parameter-mapper\", () => {\n  test(\"haze floors against scaled cloud darkness\", () => {\n    const config = mapWeatherToEffects({\n      conditionCode: \"thunderstorm\",\n      visibility: 10,\n      timestamp: \"2025-01-01T12:00:00Z\",\n    });\n\n    // thunderstorm preset cloud.darkness = 0.7 → haze floor = 0.7 * 0.3 = 0.21\n    expect(config.atmosphere.haze).toBeCloseTo(0.21, 6);\n  });\n\n  test(\"getSceneBrightness is clamped to [0, 1]\", () => {\n    const middayClear = getSceneBrightness(\"2025-01-01T12:00:00Z\", \"clear\");\n    const midnightStorm = getSceneBrightness(\n      \"2025-01-01T00:00:00Z\",\n      \"thunderstorm\",\n    );\n\n    for (const value of [middayClear, midnightStorm]) {\n      expect(value).toBeGreaterThanOrEqual(0);\n      expect(value).toBeLessThanOrEqual(1);\n    }\n  });\n\n  test(\"rain angle mapping converts degrees to radians-ish\", () => {\n    const config = mapWeatherToEffects({\n      conditionCode: \"thunderstorm\",\n      timestamp: \"2025-01-01T12:00:00Z\",\n    });\n\n    const rain = configToRainProps(config);\n    expect(rain).not.toBeNull();\n    expect(rain?.fallingAngle).toBeCloseTo(15 * 0.02, 6);\n  });\n\n  test(\"hail condition includes snow layer so mixed precip is visible\", () => {\n    const config = mapWeatherToEffects({\n      conditionCode: \"hail\",\n      timestamp: \"2025-01-01T12:00:00Z\",\n    });\n\n    const snow = configToSnowProps(config);\n    expect(snow).not.toBeNull();\n    expect(snow?.intensity ?? 0).toBeGreaterThan(0);\n  });\n\n  test(\"explicit timeOfDay drives atmosphere day/night state even when timestamp differs\", () => {\n    const config = mapWeatherToEffects({\n      conditionCode: \"clear\",\n      // Noon timestamp conflicts with explicit midnight timeOfDay.\n      timestamp: \"2025-01-01T12:00:00Z\",\n      timeOfDay: 0,\n    });\n\n    expect(config.celestial?.timeOfDay).toBe(0);\n    expect(config.atmosphere.sunAltitude).toBeCloseTo(\n      timeOfDayToSunAltitude(0),\n      6,\n    );\n    expect(config.atmosphere.starVisibility).toBeGreaterThan(0);\n  });\n\n  test(\"getMoonPhase stays in [0, 1] for pre-2000 timestamps\", () => {\n    const phase = getMoonPhase(\"1999-01-01T12:00:00Z\");\n\n    expect(phase).toBeGreaterThanOrEqual(0);\n    expect(phase).toBeLessThanOrEqual(1);\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/tests/tool-ui/weather-widget/production-runtime-harness.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\n\nimport { createProductionHarnessRuntimeInput } from \"@/app/sandbox/weather-widget-production/runtime-input\";\nimport { resolveWeatherEffectsCanvasRuntimeProps as resolveBaseCanvasProps } from \"@/lib/weather-authoring/weather-widget/effects/canvas-resolver-runtime\";\nimport { TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES } from \"@/lib/weather-authoring/weather-widget/effects/generated/tuned-presets.generated\";\nimport { resolveWeatherEffectsCanvasRuntimeProps as resolveRuntimeDefaults } from \"@/lib/weather-authoring/weather-widget/effects/weather-effects-props\";\n\ndescribe(\"weather-widget production runtime harness\", () => {\n  test(\"snaps diagnostics input to the same checkpoint used by production\", () => {\n    const input = createProductionHarnessRuntimeInput({\n      conditionCode: \"overcast\",\n      windSpeed: 3.3,\n      precipitationLevel: \"none\",\n      visibility: 10000,\n      timeOfDay: 0.37,\n      timestamp: \"2026-02-18T08:53:00.000Z\",\n    });\n\n    expect(input.timeOfDay).toBe(0.25);\n  });\n\n  test(\"snaps tie-boundary diagnostics input to midnight checkpoint\", () => {\n    const dawnBoundaryInput = createProductionHarnessRuntimeInput({\n      conditionCode: \"overcast\",\n      windSpeed: 3.3,\n      precipitationLevel: \"none\",\n      visibility: 10000,\n      timeOfDay: 0.125,\n      timestamp: \"2026-02-18T08:53:00.000Z\",\n    });\n\n    const duskBoundaryInput = createProductionHarnessRuntimeInput({\n      conditionCode: \"overcast\",\n      windSpeed: 3.3,\n      precipitationLevel: \"none\",\n      visibility: 10000,\n      timeOfDay: 0.875,\n      timestamp: \"2026-02-18T20:53:00.000Z\",\n    });\n\n    expect(dawnBoundaryInput.timeOfDay).toBe(0);\n    expect(duskBoundaryInput.timeOfDay).toBe(0);\n  });\n\n  test(\"overcast noon uses stronger tuned post effects than untuned base\", () => {\n    const input = createProductionHarnessRuntimeInput({\n      conditionCode: \"overcast\" as const,\n      windSpeed: 3.3,\n      precipitationLevel: \"none\" as const,\n      visibility: 10000,\n      timeOfDay: 0.5,\n      timestamp: \"2026-02-18T12:00:00.000Z\",\n    });\n\n    const untuned = resolveRuntimeDefaults(resolveBaseCanvasProps(input));\n    const tuned = resolveRuntimeDefaults(\n      resolveBaseCanvasProps({\n        ...input,\n        tunedPresets: TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES,\n      }),\n    );\n\n    expect(tuned.post.godRayIntensity).toBeGreaterThan(\n      untuned.post.godRayIntensity,\n    );\n    expect(tuned.post.godRayIntensity).toBeGreaterThan(0);\n    expect(tuned.cloud.ambientDarkness).toBeLessThan(\n      untuned.cloud.ambientDarkness,\n    );\n  });\n\n  test(\"overcast tuned god rays are checkpoint-shaped and restrained\", () => {\n    const buildTuned = (timeOfDay: number) =>\n      resolveRuntimeDefaults(\n        resolveBaseCanvasProps({\n          ...createProductionHarnessRuntimeInput({\n            conditionCode: \"overcast\",\n            windSpeed: 3.3,\n            precipitationLevel: \"none\",\n            visibility: 10000,\n            timeOfDay,\n            timestamp: \"2026-02-18T12:00:00.000Z\",\n          }),\n          tunedPresets: TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES,\n        }),\n      );\n\n    const dawn = buildTuned(0.25).post.godRayIntensity;\n    const noon = buildTuned(0.5).post.godRayIntensity;\n    const dusk = buildTuned(0.75).post.godRayIntensity;\n    const midnight = buildTuned(0).post.godRayIntensity;\n\n    expect(dawn).toBeGreaterThan(noon);\n    expect(dusk).toBeGreaterThan(noon);\n    expect(midnight).toBeLessThanOrEqual(noon);\n    expect(dawn).toBeLessThanOrEqual(0.4);\n    expect(noon).toBeLessThanOrEqual(0.2);\n    expect(dusk).toBeLessThanOrEqual(0.4);\n    expect(midnight).toBeLessThanOrEqual(0.05);\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/tests/tool-ui/weather-widget/weather-data-overlay-observer.test.ts",
    "content": "// @vitest-environment jsdom\n\nimport { render, waitFor } from \"@testing-library/react\";\nimport { createElement } from \"react\";\nimport { afterEach, beforeEach, describe, expect, test, vi } from \"vitest\";\n\nimport { observeCardDimensions } from \"@/lib/weather-authoring/weather-widget/weather-data-overlay\";\nimport { WeatherDataOverlay } from \"@/lib/weather-authoring/weather-widget/weather-data-overlay\";\n\nfunction installCssSupportsStub() {\n  vi.stubGlobal(\"CSS\", {\n    supports: vi.fn(() => true),\n  } as unknown as typeof CSS);\n}\n\ndescribe(\"weather-data-overlay resize observer guard\", () => {\n  beforeEach(() => {\n    installCssSupportsStub();\n  });\n\n  afterEach(() => {\n    vi.unstubAllGlobals();\n  });\n\n  test(\"returns a safe no-op cleanup when ResizeObserver is unavailable\", () => {\n    vi.stubGlobal(\"ResizeObserver\", undefined);\n\n    const cleanup = observeCardDimensions({} as HTMLDivElement, vi.fn());\n\n    expect(typeof cleanup).toBe(\"function\");\n    expect(() => cleanup()).not.toThrow();\n  });\n\n  test(\"observes and disconnects when ResizeObserver exists\", () => {\n    const observe = vi.fn();\n    const disconnect = vi.fn();\n\n    class MockResizeObserver {\n      constructor(_callback: ResizeObserverCallback) {}\n      observe(target: Element) {\n        observe(target);\n      }\n      disconnect() {\n        disconnect();\n      }\n    }\n\n    vi.stubGlobal(\n      \"ResizeObserver\",\n      MockResizeObserver as unknown as typeof ResizeObserver,\n    );\n\n    const element = {} as HTMLDivElement;\n    const cleanup = observeCardDimensions(element, vi.fn());\n\n    expect(observe).toHaveBeenCalledWith(element);\n    cleanup();\n    expect(disconnect).toHaveBeenCalledTimes(1);\n  });\n\n  test(\"attaches observer when forecast strip mounts after initial render\", async () => {\n    const observe = vi.fn();\n    const disconnect = vi.fn();\n\n    class MockResizeObserver {\n      constructor(_callback: ResizeObserverCallback) {}\n      observe(target: Element) {\n        observe(target);\n      }\n      disconnect() {\n        disconnect();\n      }\n    }\n\n    vi.stubGlobal(\n      \"ResizeObserver\",\n      MockResizeObserver as unknown as typeof ResizeObserver,\n    );\n\n    const baseProps = {\n      location: \"San Francisco, CA\",\n      conditionCode: \"clear\" as const,\n      temperature: 72,\n      tempHigh: 78,\n      tempLow: 65,\n      reducedMotion: true,\n    };\n\n    const { rerender } = render(\n      createElement(WeatherDataOverlay, { ...baseProps, forecast: [] }),\n    );\n\n    expect(observe).toHaveBeenCalledTimes(0);\n\n    rerender(\n      createElement(WeatherDataOverlay, {\n        ...baseProps,\n        forecast: [\n          { label: \"Now\", conditionCode: \"clear\", tempMin: 65, tempMax: 78 },\n        ],\n      }),\n    );\n\n    await waitFor(() => {\n      expect(observe).toHaveBeenCalledTimes(1);\n    });\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/tests/tool-ui/weather-widget/weather-runtime-codegen.test.ts",
    "content": "// @vitest-environment node\n\nimport { describe, expect, test } from \"vitest\";\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\n\nimport { TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES } from \"@/lib/weather-authoring/weather-widget/effects/generated/tuned-presets.generated\";\nimport * as generatedShaders from \"@/lib/weather-authoring/weather-widget/effects/generated/weather-effect-shaders.generated\";\nimport { TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES as bundledRuntimeTunedOverrides } from \"@/components/tool-ui/weather-widget/generated/weather-runtime-core.generated\";\nimport {\n  canonicalizeWeatherPresetData,\n  getStaleWeatherRuntimeArtifacts,\n  loadWeatherAuthoringPreset,\n  loadWeatherAuthoringShaders,\n  minifyWeatherShaderSource,\n} from \"@/lib/weather-codegen/compile-weather-runtime\";\n\nconst PROJECT_ROOT = process.cwd();\nconst WEATHER_RUNTIME_BUNDLE_PATH =\n  \"components/tool-ui/weather-widget/generated/weather-runtime-core.generated.ts\";\nconst WEATHER_RUNTIME_MAX_BYTES = 120_000;\nconst WEATHER_RUNTIME_MAX_LINES = 120;\nconst WEATHER_REGISTRY_ENTRY_PATH = \"public/r/weather-widget.json\";\nconst WEATHER_REGISTRY_MAX_BYTES = 170_000;\nconst WEATHER_REGISTRY_RUNTIME_FILES = [\n  \"components/tool-ui/weather-widget/generated/weather-runtime-core.generated.ts\",\n  \"components/tool-ui/weather-widget/runtime.ts\",\n  \"components/tool-ui/weather-widget/schema-runtime.ts\",\n  \"components/tool-ui/weather-widget/weather-data-overlay.tsx\",\n  \"components/tool-ui/weather-widget/weather-widget-container.tsx\",\n] as const;\nconst SHADER_EXPORT_NAMES = [\n  \"FULLSCREEN_VERTEX\",\n  \"CELESTIAL_FRAGMENT\",\n  \"CLOUD_FRAGMENT\",\n  \"RAIN_FRAGMENT\",\n  \"LIGHTNING_FRAGMENT\",\n  \"SNOW_FRAGMENT\",\n  \"COMPOSITE_FRAGMENT\",\n] as const;\n\ndescribe(\"weather runtime codegen\", () => {\n  test(\"bundled runtime is emitted with ts-nocheck for strict consumer tsconfigs\", () => {\n    const bundledRuntimePath = path.join(\n      PROJECT_ROOT,\n      WEATHER_RUNTIME_BUNDLE_PATH,\n    );\n    const bundledRuntime = readFileSync(bundledRuntimePath, \"utf8\");\n\n    expect(bundledRuntime.startsWith(\"// @ts-nocheck\\n\")).toBe(true);\n  });\n\n  test(\"runtime entrypoint uses type-only export syntax\", () => {\n    const runtimeEntrypoint = readFileSync(\n      path.join(PROJECT_ROOT, \"components/tool-ui/weather-widget/runtime.ts\"),\n      \"utf8\",\n    );\n\n    expect(runtimeEntrypoint).toContain(\"export type {\");\n  });\n\n  test(\"overlay does not use unsupported aria-label on span elements\", () => {\n    const overlaySource = readFileSync(\n      path.join(\n        PROJECT_ROOT,\n        \"components/tool-ui/weather-widget/weather-data-overlay.tsx\",\n      ),\n      \"utf8\",\n    );\n\n    expect(overlaySource).not.toMatch(/<span[^>]*aria-label=/);\n  });\n\n  test(\"bundled runtime does not reference removed component asset paths\", () => {\n    const bundledRuntimePath = path.join(\n      PROJECT_ROOT,\n      WEATHER_RUNTIME_BUNDLE_PATH,\n    );\n    const bundledRuntime = readFileSync(bundledRuntimePath, \"utf8\");\n\n    expect(bundledRuntime).not.toContain(\"../assets/moon-texture.jpg\");\n  });\n\n  test(\"shipped weather runtime files do not import private authoring modules\", () => {\n    const registryEntryPath = path.join(\n      PROJECT_ROOT,\n      \"public/r/weather-widget.json\",\n    );\n    const registryEntryRaw = readFileSync(registryEntryPath, \"utf8\");\n    const registryEntry = JSON.parse(registryEntryRaw) as {\n      files: Array<{ path: string }>;\n    };\n\n    for (const file of registryEntry.files) {\n      const absolutePath = path.join(PROJECT_ROOT, file.path);\n      const source = readFileSync(absolutePath, \"utf8\");\n      expect(source).not.toMatch(/\\bfrom\\s+[\"']@\\/lib\\/weather-authoring\\//);\n      expect(source).not.toMatch(\n        /\\bimport\\s*\\(\\s*[\"']@\\/lib\\/weather-authoring\\//,\n      );\n    }\n  });\n\n  test(\"generated artifacts are not stale\", async () => {\n    await expect(\n      getStaleWeatherRuntimeArtifacts(PROJECT_ROOT),\n    ).resolves.toEqual([]);\n  });\n\n  test(\"bundled runtime stays under size budget\", () => {\n    const bundledRuntimePath = path.join(\n      PROJECT_ROOT,\n      WEATHER_RUNTIME_BUNDLE_PATH,\n    );\n    const bundledRuntime = readFileSync(bundledRuntimePath, \"utf8\");\n    const lineCount = bundledRuntime.split(\"\\n\").length;\n    const byteCount = Buffer.byteLength(bundledRuntime, \"utf8\");\n\n    expect(lineCount).toBeLessThanOrEqual(WEATHER_RUNTIME_MAX_LINES);\n    expect(byteCount).toBeLessThanOrEqual(WEATHER_RUNTIME_MAX_BYTES);\n  });\n\n  test(\"weather registry payload stays under size budget\", () => {\n    const registryEntryPath = path.join(\n      PROJECT_ROOT,\n      WEATHER_REGISTRY_ENTRY_PATH,\n    );\n    const registryEntry = readFileSync(registryEntryPath, \"utf8\");\n    const byteCount = Buffer.byteLength(registryEntry, \"utf8\");\n\n    expect(byteCount).toBeLessThanOrEqual(WEATHER_REGISTRY_MAX_BYTES);\n  });\n\n  test(\"bundled runtime uses the generated tuned preset overrides\", () => {\n    expect(bundledRuntimeTunedOverrides).toEqual(\n      TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES,\n    );\n  });\n\n  test(\"weather registry payload embeds the current runtime files\", () => {\n    const registryEntryPath = path.join(\n      PROJECT_ROOT,\n      WEATHER_REGISTRY_ENTRY_PATH,\n    );\n    const registryEntry = JSON.parse(\n      readFileSync(registryEntryPath, \"utf8\"),\n    ) as {\n      files: Array<{ path: string; content?: string }>;\n    };\n    const fileContentsByPath = new Map(\n      registryEntry.files.map((file) => [file.path, file.content] as const),\n    );\n\n    expect(fileContentsByPath.size).toBe(WEATHER_REGISTRY_RUNTIME_FILES.length);\n\n    for (const relativePath of WEATHER_REGISTRY_RUNTIME_FILES) {\n      const embedded = fileContentsByPath.get(relativePath);\n      expect(typeof embedded).toBe(\"string\");\n\n      const currentFileContents = readFileSync(\n        path.join(PROJECT_ROOT, relativePath),\n        \"utf8\",\n      );\n\n      expect(embedded).toBe(currentFileContents);\n    }\n  });\n\n  test(\"generated presets match canonicalized authoring source\", () => {\n    const authoringPreset = loadWeatherAuthoringPreset(PROJECT_ROOT);\n\n    expect(TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES).toEqual(\n      canonicalizeWeatherPresetData(authoringPreset),\n    );\n  });\n\n  test(\"generated shader module matches minified authoring sources\", () => {\n    const authoringShaders = loadWeatherAuthoringShaders(PROJECT_ROOT);\n    const shaderExports = generatedShaders as Record<string, string>;\n\n    for (const exportName of SHADER_EXPORT_NAMES) {\n      expect(shaderExports[exportName]).toBe(\n        minifyWeatherShaderSource(authoringShaders[exportName]),\n      );\n    }\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/tests/tool-ui/weather-widget/weather-widget-layout.test.ts",
    "content": "import { createElement } from \"react\";\nimport { renderToStaticMarkup } from \"react-dom/server\";\nimport { describe, expect, test } from \"vitest\";\n\nimport { WeatherWidget } from \"@/lib/weather-authoring/weather-widget\";\n\nfunction getClassForDataSlot(html: string, dataSlot: string): string {\n  const tagMatch = html.match(\n    new RegExp(`<[^>]*data-slot=\"${dataSlot}\"[^>]*>`),\n  );\n  if (!tagMatch) {\n    throw new Error(`Could not find class for data-slot=\"${dataSlot}\"`);\n  }\n  const classMatch = tagMatch[0].match(/class=\"([^\"]+)\"/);\n  if (!classMatch) {\n    throw new Error(`Could not find class for data-slot=\"${dataSlot}\"`);\n  }\n  return classMatch[1];\n}\n\ndescribe(\"weather-widget layout containment\", () => {\n  test(\"keeps size containment off the outer wrapper to avoid collapsing height\", () => {\n    const html = renderToStaticMarkup(\n      createElement(WeatherWidget, {\n        version: \"3.1\",\n        id: \"weather-1\",\n        location: { name: \"San Francisco, CA\" },\n        units: { temperature: \"fahrenheit\" },\n        current: {\n          temperature: 72,\n          tempMin: 65,\n          tempMax: 78,\n          conditionCode: \"snow\",\n        },\n        forecast: [\n          { label: \"Now\", tempMin: 65, tempMax: 78, conditionCode: \"snow\" },\n          { label: \"Tue\", tempMin: 64, tempMax: 77, conditionCode: \"snow\" },\n          { label: \"Wed\", tempMin: 62, tempMax: 75, conditionCode: \"snow\" },\n          { label: \"Thu\", tempMin: 60, tempMax: 73, conditionCode: \"snow\" },\n          { label: \"Fri\", tempMin: 63, tempMax: 76, conditionCode: \"snow\" },\n        ],\n        time: { timeBucket: 4 },\n      }),\n    );\n\n    const wrapperClass = getClassForDataSlot(html, \"weather-widget\");\n    const cardClass = getClassForDataSlot(html, \"card\");\n\n    expect(wrapperClass).toContain(\"isolate\");\n    expect(wrapperClass).not.toContain(\"[container-type:size]\");\n    expect(cardClass).toContain(\"@container/weather\");\n    expect(cardClass).toContain(\"[container-type:size]\");\n    expect(html).not.toContain(\" jsx=\");\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/tests/tool-ui/weather-widget/webgl-budget-guard.test.ts",
    "content": "import { beforeEach, describe, expect, test } from \"vitest\";\n\nimport {\n  __resetWeatherWebglCanvasBudgetForTests,\n  getAllocatedWeatherWebglCanvasCount,\n  releaseWeatherWebglBudgetSlotOnInitFailure,\n  setMaxConcurrentWeatherWebglCanvases,\n  tryAcquireWeatherWebglCanvasBudgetSlot,\n} from \"@/lib/weather-authoring/weather-widget/effects/weather-effects-canvas\";\n\ndescribe(\"weather-effects-canvas WebGL budget guard\", () => {\n  beforeEach(() => {\n    __resetWeatherWebglCanvasBudgetForTests();\n    setMaxConcurrentWeatherWebglCanvases(1);\n  });\n\n  test(\"releases an acquired slot when init fails after budget acquisition\", () => {\n    const canvas = {} as HTMLCanvasElement;\n\n    expect(tryAcquireWeatherWebglCanvasBudgetSlot(canvas)).toBe(true);\n    expect(getAllocatedWeatherWebglCanvasCount()).toBe(1);\n\n    const nextSlotState = releaseWeatherWebglBudgetSlotOnInitFailure(\n      canvas,\n      true,\n    );\n\n    expect(nextSlotState).toBeNull();\n    expect(getAllocatedWeatherWebglCanvasCount()).toBe(0);\n  });\n});\n"
  },
  {
    "path": "apps/www/lib/ui/cn.ts",
    "content": "export { cn } from \"@/lib/utils\";\n"
  },
  {
    "path": "apps/www/lib/utils.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/presets/tuned-presets.json",
    "content": "{\n  \"clear\": {\n    \"dawn\": {\n      \"celestial\": {\n        \"starDensity\": 0.36,\n        \"celestialX\": 0.69,\n        \"sunGlowIntensity\": 3.47,\n        \"sunGlowSize\": 0.49,\n        \"sunRayCount\": 4,\n        \"sunRayShimmer\": 5,\n        \"sunRayShimmerSpeed\": 5,\n        \"skyBrightness\": 1\n      },\n      \"glass\": {\n        \"blur\": 4.5,\n        \"brightness\": 0.95\n      }\n    },\n    \"noon\": {\n      \"celestial\": {\n        \"moonPhase\": 0.2421,\n        \"starDensity\": 0.14,\n        \"celestialY\": 0.71,\n        \"sunGlowIntensity\": 1.72,\n        \"sunGlowSize\": 0.69,\n        \"sunRayCount\": 4,\n        \"sunRayIntensity\": 0.19,\n        \"sunRayShimmer\": 5,\n        \"sunRayShimmerSpeed\": 5,\n        \"skyBrightness\": 0.8,\n        \"skySaturation\": 1.9\n      },\n      \"glass\": {\n        \"brightness\": 1\n      }\n    },\n    \"dusk\": {\n      \"celestial\": {\n        \"starDensity\": 0.36,\n        \"celestialX\": 0.69,\n        \"sunGlowIntensity\": 3.84,\n        \"sunGlowSize\": 0.43,\n        \"sunRayCount\": 4,\n        \"sunRayLength\": 1.37,\n        \"sunRayIntensity\": 0.24,\n        \"skyBrightness\": 1\n      },\n      \"glass\": {\n        \"brightness\": 0.9\n      }\n    },\n    \"midnight\": {\n      \"celestial\": {\n        \"moonPhase\": 0.51,\n        \"starDensity\": 1.72,\n        \"celestialY\": 0.71,\n        \"sunGlowIntensity\": 1.59,\n        \"moonGlowIntensity\": 3.23,\n        \"skyBrightness\": 0.44,\n        \"skySaturation\": 2,\n        \"skyContrast\": 1\n      }\n    }\n  },\n  \"partly-cloudy\": {\n    \"dawn\": {\n      \"celestial\": {\n        \"moonPhase\": 0.5,\n        \"starDensity\": 1.72,\n        \"celestialY\": 0.71,\n        \"sunGlowIntensity\": 3.89,\n        \"sunGlowSize\": 0.38,\n        \"sunRayCount\": 4,\n        \"sunRayIntensity\": 0.17,\n        \"moonGlowIntensity\": 3.55,\n        \"moonGlowSize\": 0.97\n      },\n      \"cloud\": {\n        \"coverage\": 0.37,\n        \"density\": 1.24,\n        \"softness\": 0.28,\n        \"windSpeed\": 0.01,\n        \"lightIntensity\": 1.42,\n        \"ambientDarkness\": 0.42\n      },\n      \"glass\": {\n        \"blur\": 3,\n        \"brightness\": 0.95\n      }\n    },\n    \"noon\": {\n      \"celestial\": {\n        \"moonPhase\": 0.5,\n        \"starDensity\": 1.72,\n        \"celestialY\": 0.71,\n        \"moonGlowIntensity\": 3.55,\n        \"moonGlowSize\": 0.97\n      },\n      \"cloud\": {\n        \"cloudScale\": 1.86,\n        \"coverage\": 0.48,\n        \"density\": 1.71,\n        \"softness\": 0.41,\n        \"windSpeed\": 0.01,\n        \"lightIntensity\": 0.67,\n        \"ambientDarkness\": 0.42,\n        \"backlightIntensity\": 0,\n        \"numLayers\": 2\n      },\n      \"glass\": {\n        \"brightness\": 1\n      }\n    },\n    \"dusk\": {\n      \"celestial\": {\n        \"moonPhase\": 0.5,\n        \"starDensity\": 1.72,\n        \"celestialY\": 0.71,\n        \"sunGlowIntensity\": 3.89,\n        \"sunGlowSize\": 0.38,\n        \"sunRayCount\": 4,\n        \"sunRayIntensity\": 0.17,\n        \"moonGlowIntensity\": 3.55,\n        \"moonGlowSize\": 0.97\n      },\n      \"cloud\": {\n        \"cloudScale\": 1.68,\n        \"coverage\": 0.37,\n        \"density\": 1.24,\n        \"softness\": 0.26,\n        \"windSpeed\": 0.01,\n        \"lightIntensity\": 1.42,\n        \"ambientDarkness\": 0.42\n      },\n      \"glass\": {\n        \"brightness\": 0.9\n      }\n    },\n    \"midnight\": {\n      \"celestial\": {\n        \"moonPhase\": 0.5,\n        \"starDensity\": 1.72,\n        \"celestialY\": 0.71,\n        \"moonGlowIntensity\": 3.55,\n        \"moonGlowSize\": 0.97\n      },\n      \"cloud\": {\n        \"cloudScale\": 1.11,\n        \"coverage\": 0.37,\n        \"density\": 1.24,\n        \"softness\": 0.21,\n        \"windSpeed\": 0.01,\n        \"turbulence\": 0.18,\n        \"lightIntensity\": 1.42,\n        \"ambientDarkness\": 0.42,\n        \"backlightIntensity\": 1.23,\n        \"numLayers\": 5\n      }\n    }\n  },\n  \"cloudy\": {\n    \"dawn\": {\n      \"celestial\": {\n        \"sunGlowIntensity\": 2.4,\n        \"celestialY\": 0.74,\n        \"celestialX\": 0.69,\n        \"skyBrightness\": 1,\n        \"starDensity\": 0.36,\n        \"sunRayShimmer\": 5,\n        \"sunRayShimmerSpeed\": 5,\n        \"skySaturation\": 0.88,\n        \"skyContrast\": 1.1,\n        \"sunGlowSize\": 0.38\n      },\n      \"cloud\": {\n        \"coverage\": 0.81,\n        \"density\": 1.1,\n        \"softness\": 0.42,\n        \"windSpeed\": 0.04,\n        \"lightIntensity\": 0.67,\n        \"ambientDarkness\": 1,\n        \"backlightIntensity\": 1.07,\n        \"numLayers\": 1,\n        \"cloudScale\": 1.73\n      },\n      \"glass\": {\n        \"blur\": 3,\n        \"brightness\": 0.95\n      },\n      \"post\": {\n        \"bloomIntensity\": 0.99,\n        \"bloomThreshold\": 0.78,\n        \"bloomKnee\": 0.58,\n        \"bloomRadius\": 5.5,\n        \"bloomTapScale\": 1.39,\n        \"exposureIntensity\": 1.14,\n        \"exposureRecovery\": 1.95,\n        \"godRayIntensity\": 1.08,\n        \"godRayDecay\": 0.857,\n        \"godRayDensity\": 0.79,\n        \"godRayWeight\": 0.42,\n        \"godRaySamples\": 121\n      }\n    },\n    \"noon\": {\n      \"celestial\": {\n        \"moonPhase\": 0.2421,\n        \"sunGlowIntensity\": 1.35,\n        \"sunGlowSize\": 0.61,\n        \"celestialY\": 0.71,\n        \"celestialX\": 0.74,\n        \"starDensity\": 0.14,\n        \"sunRayShimmer\": 5,\n        \"sunRayShimmerSpeed\": 5,\n        \"skyBrightness\": 0.84,\n        \"skySaturation\": 1.44,\n        \"sunRayCount\": 4,\n        \"sunRayIntensity\": 0.22,\n        \"skyContrast\": 1.31\n      },\n      \"cloud\": {\n        \"density\": 1.06,\n        \"windSpeed\": 0.04,\n        \"lightIntensity\": 0.44,\n        \"backlightIntensity\": 0.45,\n        \"numLayers\": 1,\n        \"cloudScale\": 1.73,\n        \"coverage\": 0.81,\n        \"softness\": 0.42,\n        \"ambientDarkness\": 0\n      },\n      \"glass\": {\n        \"brightness\": 1\n      },\n      \"post\": {\n        \"bloomIntensity\": 0.42,\n        \"bloomThreshold\": 0.76,\n        \"bloomKnee\": 0.58,\n        \"bloomRadius\": 5.5,\n        \"bloomTapScale\": 1.39,\n        \"exposureIntensity\": 1.14,\n        \"exposureRecovery\": 1.95,\n        \"godRayIntensity\": 1.33,\n        \"godRayDecay\": 0.884,\n        \"godRayDensity\": 0.79,\n        \"godRayWeight\": 0.42,\n        \"godRaySamples\": 121\n      }\n    },\n    \"dusk\": {\n      \"celestial\": {\n        \"sunGlowIntensity\": 3.21,\n        \"sunGlowSize\": 0.47,\n        \"celestialY\": 0.74,\n        \"celestialX\": 0.69,\n        \"starDensity\": 0.11,\n        \"sunRayShimmer\": 5,\n        \"sunRayShimmerSpeed\": 5,\n        \"skySaturation\": 1.38,\n        \"skyContrast\": 0.99\n      },\n      \"cloud\": {\n        \"density\": 0.9,\n        \"windSpeed\": 0.04,\n        \"numLayers\": 1,\n        \"cloudScale\": 1.73,\n        \"coverage\": 0.81,\n        \"softness\": 0.42,\n        \"lightIntensity\": 0.78,\n        \"ambientDarkness\": 0.54,\n        \"backlightIntensity\": 0.78\n      },\n      \"glass\": {\n        \"brightness\": 0.9\n      },\n      \"post\": {\n        \"bloomIntensity\": 1.11,\n        \"bloomThreshold\": 0.78,\n        \"bloomKnee\": 0.58,\n        \"bloomRadius\": 5.5,\n        \"bloomTapScale\": 1.39,\n        \"exposureIntensity\": 1.14,\n        \"exposureRecovery\": 1.95,\n        \"godRayIntensity\": 1.08,\n        \"godRayDecay\": 0.857,\n        \"godRayDensity\": 0.79,\n        \"godRayWeight\": 0.42,\n        \"godRaySamples\": 121\n      }\n    },\n    \"midnight\": {\n      \"celestial\": {\n        \"celestialY\": 0.71,\n        \"moonGlowIntensity\": 3.55,\n        \"moonGlowSize\": 0.97,\n        \"celestialX\": 0.74,\n        \"starDensity\": 1.72,\n        \"sunRayShimmer\": 5,\n        \"sunRayShimmerSpeed\": 5\n      },\n      \"cloud\": {\n        \"density\": 1.02,\n        \"windSpeed\": 0.04,\n        \"numLayers\": 1,\n        \"cloudScale\": 1.73,\n        \"coverage\": 0.81,\n        \"softness\": 0.42\n      },\n      \"post\": {\n        \"bloomIntensity\": 1.11,\n        \"bloomThreshold\": 0.78,\n        \"bloomKnee\": 0.58,\n        \"bloomRadius\": 5.5,\n        \"bloomTapScale\": 1.39,\n        \"exposureIntensity\": 1.14,\n        \"exposureRecovery\": 1.95,\n        \"godRayIntensity\": 1.08,\n        \"godRayDecay\": 0.857,\n        \"godRayDensity\": 0.79,\n        \"godRayWeight\": 0.42,\n        \"godRaySamples\": 121\n      }\n    }\n  },\n  \"overcast\": {\n    \"dawn\": {\n      \"celestial\": {\n        \"starDensity\": 0.11,\n        \"celestialY\": 0.71,\n        \"sunGlowIntensity\": 5.1,\n        \"sunGlowSize\": 0.27,\n        \"sunRayCount\": 6,\n        \"sunRayLength\": 0.6,\n        \"sunRayIntensity\": 0.05,\n        \"moonGlowIntensity\": 1.91,\n        \"moonGlowSize\": 0.32,\n        \"skyBrightness\": 1.36,\n        \"skySaturation\": 1.6,\n        \"skyContrast\": 0.73,\n        \"moonPhase\": 0.5\n      },\n      \"cloud\": {\n        \"cloudScale\": 1.9,\n        \"density\": 0.3,\n        \"softness\": 0.77,\n        \"windSpeed\": 0.04,\n        \"turbulence\": 0.66,\n        \"lightIntensity\": 0.75,\n        \"ambientDarkness\": 0,\n        \"backlightIntensity\": 1.41,\n        \"numLayers\": 8\n      },\n      \"glass\": {\n        \"brightness\": 0.95\n      },\n      \"post\": {\n        \"haze\": 0.15,\n        \"hazeHorizon\": 1,\n        \"hazeContrast\": 0,\n        \"bloomThreshold\": 0.81,\n        \"godRayIntensity\": 0.32,\n        \"godRayWeight\": 0.3\n      }\n    },\n    \"noon\": {\n      \"celestial\": {\n        \"starDensity\": 0.11,\n        \"celestialY\": 0.71,\n        \"sunGlowIntensity\": 5.2,\n        \"sunGlowSize\": 0.38,\n        \"sunRayCount\": 6,\n        \"sunRayLength\": 1.59,\n        \"sunRayIntensity\": 0.05,\n        \"moonGlowIntensity\": 1.91,\n        \"moonGlowSize\": 0.32,\n        \"skyBrightness\": 1.16,\n        \"skySaturation\": 0.88,\n        \"skyContrast\": 0.53,\n        \"moonPhase\": 0.5\n      },\n      \"cloud\": {\n        \"cloudScale\": 1.9,\n        \"coverage\": 0.95,\n        \"density\": 0.3,\n        \"softness\": 0.77,\n        \"windSpeed\": 0.04,\n        \"turbulence\": 0.66,\n        \"lightIntensity\": 0.08,\n        \"ambientDarkness\": 0,\n        \"backlightIntensity\": 0.9,\n        \"numLayers\": 8\n      },\n      \"glass\": {\n        \"brightness\": 1\n      },\n      \"post\": {\n        \"haze\": 0.15,\n        \"hazeHorizon\": 1,\n        \"hazeContrast\": 0,\n        \"bloomThreshold\": 0.81,\n        \"godRayIntensity\": 0.12,\n        \"godRayWeight\": 0.2\n      }\n    },\n    \"dusk\": {\n      \"celestial\": {\n        \"starDensity\": 0.11,\n        \"celestialY\": 0.71,\n        \"sunGlowIntensity\": 4.15,\n        \"sunGlowSize\": 0.28,\n        \"sunRayCount\": 6,\n        \"sunRayLength\": 0.6,\n        \"sunRayIntensity\": 0.05,\n        \"moonGlowIntensity\": 1.91,\n        \"moonGlowSize\": 0.32,\n        \"skyBrightness\": 1.12,\n        \"skySaturation\": 2,\n        \"skyContrast\": 0.83,\n        \"moonPhase\": 0.5\n      },\n      \"cloud\": {\n        \"cloudScale\": 1.9,\n        \"density\": 0.3,\n        \"softness\": 0.77,\n        \"windSpeed\": 0.04,\n        \"turbulence\": 0.66,\n        \"lightIntensity\": 0.78,\n        \"ambientDarkness\": 0,\n        \"backlightIntensity\": 1.41,\n        \"numLayers\": 8\n      },\n      \"glass\": {\n        \"brightness\": 0.9\n      },\n      \"post\": {\n        \"haze\": 0.15,\n        \"hazeHorizon\": 1,\n        \"hazeContrast\": 0,\n        \"bloomThreshold\": 0.81,\n        \"godRayIntensity\": 0.32,\n        \"godRayWeight\": 0.3\n      }\n    },\n    \"midnight\": {\n      \"celestial\": {\n        \"starDensity\": 0.11,\n        \"celestialY\": 0.71,\n        \"sunGlowIntensity\": 2.42,\n        \"sunGlowSize\": 0.38,\n        \"sunRayCount\": 6,\n        \"sunRayLength\": 1.59,\n        \"sunRayIntensity\": 0.05,\n        \"moonGlowIntensity\": 2.75,\n        \"moonGlowSize\": 1.54,\n        \"skyBrightness\": 1.58,\n        \"skySaturation\": 1.15,\n        \"skyContrast\": 0.84,\n        \"moonPhase\": 0.5\n      },\n      \"cloud\": {\n        \"cloudScale\": 1.9,\n        \"coverage\": 0.9,\n        \"density\": 1.16,\n        \"softness\": 0.87,\n        \"windSpeed\": 0.04,\n        \"turbulence\": 0.66,\n        \"lightIntensity\": 0,\n        \"ambientDarkness\": 0,\n        \"backlightIntensity\": 1.41,\n        \"numLayers\": 8\n      },\n      \"post\": {\n        \"haze\": 0.35,\n        \"hazeHorizon\": 0.96,\n        \"hazeContrast\": 0,\n        \"bloomThreshold\": 0.81,\n        \"godRayIntensity\": 0.02,\n        \"godRayWeight\": 0.15\n      }\n    }\n  },\n  \"fog\": {\n    \"dawn\": {\n      \"celestial\": {\n        \"starDensity\": 0.11,\n        \"celestialY\": 0.71,\n        \"sunGlowIntensity\": 2.42,\n        \"sunGlowSize\": 0.38,\n        \"sunRayLength\": 1.59,\n        \"sunRayIntensity\": 0.05,\n        \"skyBrightness\": 1.05,\n        \"skySaturation\": 1.25,\n        \"skyContrast\": 0.41\n      },\n      \"cloud\": {\n        \"cloudScale\": 0.64,\n        \"density\": 1.5,\n        \"softness\": 0.77,\n        \"windSpeed\": 0.02,\n        \"turbulence\": 0.67,\n        \"ambientDarkness\": 1,\n        \"backlightIntensity\": 0.54,\n        \"numLayers\": 6\n      },\n      \"glass\": {\n        \"brightness\": 0.95\n      }\n    },\n    \"noon\": {\n      \"celestial\": {\n        \"starDensity\": 0.11,\n        \"celestialY\": 0.71,\n        \"sunGlowIntensity\": 0.92,\n        \"sunGlowSize\": 0.84,\n        \"sunRayCount\": 0,\n        \"sunRayLength\": 0,\n        \"sunRayIntensity\": 0,\n        \"skyBrightness\": 0.76,\n        \"skySaturation\": 0.6,\n        \"skyContrast\": 0.47\n      },\n      \"cloud\": {\n        \"cloudScale\": 0.83,\n        \"density\": 1.5,\n        \"softness\": 0.84,\n        \"windSpeed\": 0.03,\n        \"turbulence\": 0.86,\n        \"ambientDarkness\": 1,\n        \"backlightIntensity\": 0.72,\n        \"numLayers\": 6\n      },\n      \"glass\": {\n        \"brightness\": 1\n      }\n    },\n    \"dusk\": {\n      \"celestial\": {\n        \"starDensity\": 0.11,\n        \"celestialY\": 0.71,\n        \"sunGlowIntensity\": 2.42,\n        \"sunGlowSize\": 0.38,\n        \"sunRayLength\": 1.59,\n        \"sunRayIntensity\": 0.05,\n        \"skyBrightness\": 1.02,\n        \"skySaturation\": 0.88,\n        \"skyContrast\": 0.62\n      },\n      \"cloud\": {\n        \"cloudScale\": 0.93,\n        \"softness\": 0.5,\n        \"windSpeed\": 0.03,\n        \"turbulence\": 1,\n        \"ambientDarkness\": 0,\n        \"backlightIntensity\": 0.54,\n        \"numLayers\": 6\n      },\n      \"glass\": {\n        \"brightness\": 0.9\n      }\n    },\n    \"midnight\": {\n      \"celestial\": {\n        \"moonPhase\": 0.5,\n        \"starDensity\": 0.11,\n        \"celestialY\": 0.71,\n        \"sunGlowIntensity\": 2.42,\n        \"sunGlowSize\": 0.38,\n        \"sunRayLength\": 1.59,\n        \"sunRayIntensity\": 0.05,\n        \"moonGlowIntensity\": 2.74,\n        \"moonGlowSize\": 1.43,\n        \"skyBrightness\": 1.02,\n        \"skySaturation\": 0.88,\n        \"skyContrast\": 0.62\n      },\n      \"cloud\": {\n        \"cloudScale\": 0.91,\n        \"density\": 1.27,\n        \"softness\": 0.63,\n        \"windSpeed\": 0.03,\n        \"turbulence\": 0.86,\n        \"lightIntensity\": 2,\n        \"ambientDarkness\": 0,\n        \"backlightIntensity\": 1.4,\n        \"numLayers\": 6\n      }\n    }\n  },\n  \"drizzle\": {\n    \"dawn\": {\n      \"celestial\": {\n        \"starDensity\": 1.72,\n        \"celestialX\": 0.69,\n        \"celestialY\": 0.74,\n        \"moonGlowIntensity\": 3.55,\n        \"moonGlowSize\": 0.97,\n        \"skyBrightness\": 0.96,\n        \"skySaturation\": 0.94,\n        \"skyContrast\": 0.84\n      },\n      \"cloud\": {\n        \"cloudScale\": 1.21,\n        \"coverage\": 0.61,\n        \"density\": 0.98,\n        \"softness\": 0.17,\n        \"windSpeed\": 0.02,\n        \"turbulence\": 0.16,\n        \"lightIntensity\": 0,\n        \"ambientDarkness\": 0.7,\n        \"backlightIntensity\": 0.82,\n        \"numLayers\": 1\n      },\n      \"rain\": {\n        \"glassIntensity\": 0.29,\n        \"glassZoom\": 0.65,\n        \"fallingIntensity\": 0.13,\n        \"fallingSpeed\": 2.24,\n        \"fallingAngle\": 0.02,\n        \"fallingStreakLength\": 0.32\n      },\n      \"glass\": {\n        \"brightness\": 0.95\n      }\n    },\n    \"noon\": {\n      \"celestial\": {\n        \"starDensity\": 1.72,\n        \"celestialX\": 0.74,\n        \"celestialY\": 0.71,\n        \"sunGlowIntensity\": 2.73,\n        \"sunRayLength\": 1.45,\n        \"moonGlowIntensity\": 3.55,\n        \"moonGlowSize\": 0.97,\n        \"skyBrightness\": 0.77,\n        \"skySaturation\": 0.85,\n        \"skyContrast\": 0.84\n      },\n      \"cloud\": {\n        \"cloudScale\": 1.21,\n        \"coverage\": 0.61,\n        \"density\": 0.98,\n        \"softness\": 0.17,\n        \"windSpeed\": 0.02,\n        \"turbulence\": 0.16,\n        \"lightIntensity\": 0,\n        \"ambientDarkness\": 0.7,\n        \"backlightIntensity\": 0.82,\n        \"numLayers\": 1\n      },\n      \"rain\": {\n        \"glassIntensity\": 0.29,\n        \"glassZoom\": 0.65,\n        \"fallingIntensity\": 0.13,\n        \"fallingSpeed\": 2.24,\n        \"fallingAngle\": 0.02,\n        \"fallingStreakLength\": 0.32\n      },\n      \"glass\": {\n        \"brightness\": 1\n      }\n    },\n    \"dusk\": {\n      \"celestial\": {\n        \"starDensity\": 1.72,\n        \"celestialX\": 0.69,\n        \"celestialY\": 0.74,\n        \"sunGlowIntensity\": 2.73,\n        \"sunRayLength\": 1.45,\n        \"moonGlowIntensity\": 3.55,\n        \"moonGlowSize\": 0.97,\n        \"skyBrightness\": 0.77,\n        \"skySaturation\": 0.85,\n        \"skyContrast\": 0.84\n      },\n      \"cloud\": {\n        \"cloudScale\": 1.21,\n        \"coverage\": 0.61,\n        \"density\": 0.98,\n        \"softness\": 0.17,\n        \"windSpeed\": 0.02,\n        \"turbulence\": 0.16,\n        \"lightIntensity\": 0,\n        \"ambientDarkness\": 0.7,\n        \"backlightIntensity\": 0.82,\n        \"numLayers\": 1\n      },\n      \"rain\": {\n        \"glassIntensity\": 0.29,\n        \"glassZoom\": 0.65,\n        \"fallingIntensity\": 0.13,\n        \"fallingSpeed\": 2.24,\n        \"fallingAngle\": 0.02,\n        \"fallingStreakLength\": 0.32\n      },\n      \"glass\": {\n        \"brightness\": 0.9\n      }\n    },\n    \"midnight\": {\n      \"celestial\": {\n        \"starDensity\": 1.72,\n        \"celestialX\": 0.74,\n        \"celestialY\": 0.71,\n        \"sunGlowIntensity\": 2.73,\n        \"sunRayLength\": 1.45,\n        \"moonGlowIntensity\": 3.55,\n        \"moonGlowSize\": 0.97,\n        \"skyBrightness\": 0.77,\n        \"skySaturation\": 0.85,\n        \"skyContrast\": 0.84\n      },\n      \"cloud\": {\n        \"cloudScale\": 1.21,\n        \"coverage\": 0.61,\n        \"density\": 0.98,\n        \"softness\": 0.17,\n        \"windSpeed\": 0.02,\n        \"turbulence\": 0.16,\n        \"lightIntensity\": 0,\n        \"ambientDarkness\": 0.7,\n        \"backlightIntensity\": 0.82,\n        \"numLayers\": 1\n      },\n      \"rain\": {\n        \"glassIntensity\": 0.2,\n        \"glassZoom\": 0.5,\n        \"fallingIntensity\": 0.13,\n        \"fallingSpeed\": 2.24,\n        \"fallingAngle\": 0.02,\n        \"fallingStreakLength\": 0.32\n      }\n    }\n  },\n  \"rain\": {\n    \"dawn\": {\n      \"celestial\": {\n        \"starDensity\": 1.72,\n        \"celestialX\": 0.69,\n        \"celestialY\": 0.74,\n        \"moonGlowIntensity\": 3.55,\n        \"moonGlowSize\": 0.97,\n        \"skyBrightness\": 0.96,\n        \"skySaturation\": 0.94,\n        \"skyContrast\": 0.84\n      },\n      \"cloud\": {\n        \"cloudScale\": 1.13,\n        \"coverage\": 0.72,\n        \"density\": 0.94,\n        \"softness\": 0.17,\n        \"windSpeed\": 0.05,\n        \"turbulence\": 0.34,\n        \"lightIntensity\": 0,\n        \"ambientDarkness\": 0.7,\n        \"backlightIntensity\": 0.82,\n        \"numLayers\": 1\n      },\n      \"rain\": {\n        \"glassIntensity\": 0.38,\n        \"glassZoom\": 0.5,\n        \"fallingIntensity\": 0.84,\n        \"fallingSpeed\": 3,\n        \"fallingAngle\": 0,\n        \"fallingStreakLength\": 0.54,\n        \"fallingLayers\": 4\n      },\n      \"glass\": {\n        \"brightness\": 0.95\n      },\n      \"interactions\": {\n        \"rainRefractionStrength\": 0.76\n      }\n    },\n    \"noon\": {\n      \"celestial\": {\n        \"moonPhase\": 0.3776,\n        \"starDensity\": 1.72,\n        \"celestialX\": 0.74,\n        \"celestialY\": 0.71,\n        \"sunSize\": 0.14,\n        \"moonSize\": 0.17,\n        \"sunGlowIntensity\": 3.05,\n        \"sunGlowSize\": 0.22,\n        \"sunRayCount\": 0,\n        \"sunRayLength\": 0,\n        \"sunRayIntensity\": 0,\n        \"moonGlowIntensity\": 3.55,\n        \"moonGlowSize\": 0.97,\n        \"skyBrightness\": 1.12,\n        \"skySaturation\": 0.59,\n        \"skyContrast\": 0.49\n      },\n      \"cloud\": {\n        \"cloudScale\": 1.13,\n        \"coverage\": 0.79,\n        \"density\": 1.08,\n        \"softness\": 0.23,\n        \"windSpeed\": 0.05,\n        \"windAngle\": 0,\n        \"turbulence\": 0.34,\n        \"lightIntensity\": 0,\n        \"ambientDarkness\": 1,\n        \"backlightIntensity\": 0.59,\n        \"numLayers\": 1\n      },\n      \"rain\": {\n        \"glassIntensity\": 0.38,\n        \"glassZoom\": 0.5,\n        \"fallingIntensity\": 0.84,\n        \"fallingSpeed\": 3,\n        \"fallingAngle\": 0,\n        \"fallingLayers\": 5,\n        \"fallingStreakLength\": 0.54\n      },\n      \"lightning\": {\n        \"enabled\": true\n      },\n      \"glass\": {\n        \"brightness\": 1\n      },\n      \"interactions\": {\n        \"rainRefractionStrength\": 0.76\n      }\n    },\n    \"dusk\": {\n      \"celestial\": {\n        \"moonPhase\": 0.4115,\n        \"starDensity\": 1.72,\n        \"celestialX\": 0.69,\n        \"celestialY\": 0.74,\n        \"sunSize\": 0.14,\n        \"moonSize\": 0.17,\n        \"sunGlowIntensity\": 3.05,\n        \"sunGlowSize\": 0.3,\n        \"sunRayCount\": 6,\n        \"sunRayLength\": 3,\n        \"sunRayIntensity\": 0.1,\n        \"moonGlowIntensity\": 3.55,\n        \"moonGlowSize\": 0.97,\n        \"skyBrightness\": 0.96,\n        \"skySaturation\": 0.94,\n        \"skyContrast\": 0.84\n      },\n      \"cloud\": {\n        \"cloudScale\": 1.13,\n        \"coverage\": 0.72,\n        \"density\": 0.94,\n        \"softness\": 0.17,\n        \"windSpeed\": 0.05,\n        \"windAngle\": 0,\n        \"turbulence\": 0.34,\n        \"lightIntensity\": 0,\n        \"ambientDarkness\": 1,\n        \"backlightIntensity\": 0.82,\n        \"numLayers\": 1\n      },\n      \"rain\": {\n        \"glassIntensity\": 0.38,\n        \"glassZoom\": 0.5,\n        \"fallingIntensity\": 0.84,\n        \"fallingSpeed\": 3,\n        \"fallingAngle\": 0,\n        \"fallingLayers\": 5,\n        \"fallingStreakLength\": 0.54\n      },\n      \"lightning\": {\n        \"enabled\": true\n      },\n      \"glass\": {\n        \"brightness\": 0.9\n      },\n      \"interactions\": {\n        \"rainRefractionStrength\": 0.76\n      }\n    },\n    \"midnight\": {\n      \"celestial\": {\n        \"moonPhase\": 0.3776,\n        \"starDensity\": 1.72,\n        \"celestialX\": 0.74,\n        \"celestialY\": 0.71,\n        \"sunSize\": 0.14,\n        \"moonSize\": 0.17,\n        \"sunGlowIntensity\": 3.05,\n        \"sunGlowSize\": 0.3,\n        \"sunRayCount\": 6,\n        \"sunRayLength\": 3,\n        \"sunRayIntensity\": 0.1,\n        \"moonGlowIntensity\": 3.55,\n        \"moonGlowSize\": 0.97,\n        \"skyBrightness\": 1.11,\n        \"skySaturation\": 0.78,\n        \"skyContrast\": 1\n      },\n      \"cloud\": {\n        \"cloudScale\": 1.13,\n        \"coverage\": 0.72,\n        \"density\": 1,\n        \"softness\": 0.23,\n        \"windSpeed\": 0.05,\n        \"windAngle\": 0,\n        \"turbulence\": 0.34,\n        \"lightIntensity\": 0,\n        \"ambientDarkness\": 1,\n        \"backlightIntensity\": 0.77,\n        \"numLayers\": 1\n      },\n      \"rain\": {\n        \"glassIntensity\": 0.38,\n        \"glassZoom\": 0.5,\n        \"fallingIntensity\": 0.84,\n        \"fallingSpeed\": 3,\n        \"fallingAngle\": 0,\n        \"fallingLayers\": 5,\n        \"fallingStreakLength\": 0.54\n      },\n      \"interactions\": {\n        \"rainRefractionStrength\": 0.76\n      }\n    }\n  },\n  \"heavy-rain\": {\n    \"dawn\": {\n      \"celestial\": {\n        \"moonPhase\": 0.3776,\n        \"starDensity\": 1.72,\n        \"celestialX\": 0.69,\n        \"celestialY\": 0.74,\n        \"moonGlowIntensity\": 3.55,\n        \"moonGlowSize\": 0.97,\n        \"skyBrightness\": 0.87,\n        \"skySaturation\": 0.56,\n        \"sunGlowIntensity\": 3.75,\n        \"sunGlowSize\": 0.43\n      },\n      \"cloud\": {\n        \"cloudScale\": 1.52,\n        \"coverage\": 0.53,\n        \"density\": 1.17,\n        \"softness\": 0.25,\n        \"windSpeed\": 0.05,\n        \"turbulence\": 0.45,\n        \"lightIntensity\": 0.06,\n        \"ambientDarkness\": 1,\n        \"backlightIntensity\": 1.35,\n        \"numLayers\": 1\n      },\n      \"rain\": {\n        \"glassZoom\": 0.58,\n        \"fallingSpeed\": 3,\n        \"fallingAngle\": 0.3,\n        \"fallingStreakLength\": 2,\n        \"fallingLayers\": 6,\n        \"glassIntensity\": 0.98\n      },\n      \"glass\": {\n        \"brightness\": 0.95\n      },\n      \"interactions\": {\n        \"rainRefractionStrength\": 1\n      },\n      \"post\": {\n        \"godRayIntensity\": 1.11,\n        \"godRayDecay\": 0.838,\n        \"godRayDensity\": 0.56,\n        \"godRayWeight\": 0.66,\n        \"haze\": 0.14,\n        \"hazeHorizon\": 1,\n        \"bloomIntensity\": 0.36,\n        \"godRaySamples\": 35\n      }\n    },\n    \"noon\": {\n      \"celestial\": {\n        \"moonPhase\": 0.3776,\n        \"starDensity\": 1.68,\n        \"celestialY\": 0.71,\n        \"sunGlowIntensity\": 3.7,\n        \"sunGlowSize\": 0.22,\n        \"sunRayCount\": 0,\n        \"sunRayLength\": 0,\n        \"sunRayIntensity\": 0,\n        \"moonGlowIntensity\": 3.55,\n        \"moonGlowSize\": 0.97,\n        \"skyBrightness\": 0.81,\n        \"skySaturation\": 0.36,\n        \"skyContrast\": 0.54\n      },\n      \"cloud\": {\n        \"cloudScale\": 1.8,\n        \"coverage\": 0.58,\n        \"density\": 0.87,\n        \"softness\": 0.2,\n        \"windSpeed\": 0.05,\n        \"turbulence\": 0.24,\n        \"lightIntensity\": 0,\n        \"backlightIntensity\": 0.59,\n        \"numLayers\": 1,\n        \"ambientDarkness\": 1\n      },\n      \"rain\": {\n        \"glassIntensity\": 0.98,\n        \"glassZoom\": 0.58,\n        \"fallingAngle\": 0.3,\n        \"fallingSpeed\": 3\n      },\n      \"glass\": {\n        \"brightness\": 1\n      },\n      \"interactions\": {\n        \"rainRefractionStrength\": 1\n      },\n      \"post\": {\n        \"godRayIntensity\": 1.24,\n        \"godRayWeight\": 0.6,\n        \"godRaySamples\": 0\n      }\n    },\n    \"dusk\": {\n      \"celestial\": {\n        \"moonPhase\": 0.4115,\n        \"starDensity\": 1.72,\n        \"celestialX\": 0.69,\n        \"celestialY\": 0.74,\n        \"sunSize\": 0.14,\n        \"moonSize\": 0.17,\n        \"sunGlowIntensity\": 4.15,\n        \"sunGlowSize\": 0.35,\n        \"sunRayCount\": 6,\n        \"sunRayLength\": 3,\n        \"sunRayIntensity\": 0.1,\n        \"moonGlowIntensity\": 3.55,\n        \"moonGlowSize\": 0.97,\n        \"skyBrightness\": 0.96,\n        \"skySaturation\": 0.72,\n        \"skyContrast\": 0.99\n      },\n      \"cloud\": {\n        \"cloudScale\": 2.02,\n        \"coverage\": 0.72,\n        \"density\": 0.94,\n        \"softness\": 0.17,\n        \"windSpeed\": 0.05,\n        \"windAngle\": 0,\n        \"turbulence\": 0.51,\n        \"lightIntensity\": 0,\n        \"ambientDarkness\": 1,\n        \"backlightIntensity\": 1.8,\n        \"numLayers\": 1\n      },\n      \"rain\": {\n        \"glassIntensity\": 0.98,\n        \"glassZoom\": 0.58,\n        \"fallingIntensity\": 1,\n        \"fallingSpeed\": 3,\n        \"fallingAngle\": 0.3,\n        \"fallingStreakLength\": 2,\n        \"fallingLayers\": 6\n      },\n      \"glass\": {\n        \"brightness\": 0.9\n      },\n      \"interactions\": {\n        \"rainRefractionStrength\": 1\n      },\n      \"post\": {\n        \"godRayWeight\": 0.55,\n        \"haze\": 0.21,\n        \"hazeHorizon\": 1,\n        \"bloomIntensity\": 0.28\n      }\n    },\n    \"midnight\": {\n      \"celestial\": {\n        \"moonPhase\": 0.3776,\n        \"starDensity\": 1.72,\n        \"celestialX\": 0.74,\n        \"celestialY\": 0.71,\n        \"sunSize\": 0.14,\n        \"moonSize\": 0.17,\n        \"sunGlowIntensity\": 3.05,\n        \"sunGlowSize\": 0.3,\n        \"sunRayCount\": 6,\n        \"sunRayLength\": 3,\n        \"sunRayIntensity\": 0.1,\n        \"moonGlowIntensity\": 3.55,\n        \"moonGlowSize\": 0.97,\n        \"skyBrightness\": 1.11,\n        \"skySaturation\": 0.78,\n        \"skyContrast\": 1\n      },\n      \"cloud\": {\n        \"cloudScale\": 2.16,\n        \"coverage\": 0.72,\n        \"density\": 1.5,\n        \"softness\": 0.37,\n        \"windSpeed\": 0.05,\n        \"windAngle\": 0,\n        \"turbulence\": 0.55,\n        \"lightIntensity\": 0,\n        \"ambientDarkness\": 1,\n        \"backlightIntensity\": 0.81,\n        \"numLayers\": 1\n      },\n      \"rain\": {\n        \"glassIntensity\": 0.98,\n        \"glassZoom\": 0.58,\n        \"fallingIntensity\": 1,\n        \"fallingSpeed\": 3,\n        \"fallingAngle\": 0.3,\n        \"fallingStreakLength\": 2,\n        \"fallingLayers\": 6\n      },\n      \"glass\": {\n        \"blur\": 1,\n        \"brightness\": 1.15\n      },\n      \"interactions\": {\n        \"rainRefractionStrength\": 1\n      }\n    }\n  },\n  \"thunderstorm\": {\n    \"dawn\": {\n      \"celestial\": {\n        \"starDensity\": 1.72,\n        \"celestialX\": 0.69,\n        \"celestialY\": 0.74,\n        \"moonGlowIntensity\": 0,\n        \"moonGlowSize\": 0,\n        \"skyBrightness\": 0.78,\n        \"skySaturation\": 0.94,\n        \"skyContrast\": 1.03,\n        \"sunRayCount\": 5,\n        \"sunRayLength\": 1.7,\n        \"sunRayIntensity\": 0.13,\n        \"sunRayShimmer\": 5,\n        \"sunRayShimmerSpeed\": 5,\n        \"sunGlowIntensity\": 5.1\n      },\n      \"cloud\": {\n        \"cloudScale\": 1.56,\n        \"coverage\": 0.71,\n        \"density\": 0.97,\n        \"softness\": 0.28,\n        \"windSpeed\": 0.04,\n        \"lightIntensity\": 0,\n        \"backlightIntensity\": 0.55,\n        \"numLayers\": 1,\n        \"turbulence\": 0.36\n      },\n      \"rain\": {\n        \"glassZoom\": 0.51,\n        \"fallingSpeed\": 3,\n        \"fallingStreakLength\": 1.46,\n        \"fallingLayers\": 12,\n        \"fallingAngle\": 0.02,\n        \"glassIntensity\": 1.71\n      },\n      \"lightning\": {\n        \"enabled\": true,\n        \"autoInterval\": 8,\n        \"branchDensity\": 0.83,\n        \"flashIntensity\": 1.57\n      },\n      \"glass\": {\n        \"brightness\": 0.9,\n        \"blur\": 1,\n        \"strength\": 45,\n        \"chromaticAberration\": 2\n      },\n      \"interactions\": {\n        \"lightningSceneIllumination\": 0.83,\n        \"rainRefractionStrength\": 2\n      },\n      \"post\": {\n        \"haze\": 0.07,\n        \"hazeHorizon\": 1,\n        \"hazeContrast\": 0,\n        \"bloomIntensity\": 0.39,\n        \"exposureIntensity\": 0.95,\n        \"exposureRecovery\": 4.5,\n        \"godRayIntensity\": 0.26,\n        \"godRayDecay\": 0.894,\n        \"godRayDensity\": 0.91,\n        \"godRayWeight\": 0.36,\n        \"godRaySamples\": 39\n      }\n    },\n    \"noon\": {\n      \"celestial\": {\n        \"starDensity\": 1.72,\n        \"celestialY\": 0.71,\n        \"sunGlowIntensity\": 2.7,\n        \"sunGlowSize\": 0.51,\n        \"sunRayCount\": 0,\n        \"sunRayLength\": 0,\n        \"sunRayIntensity\": 0,\n        \"moonGlowIntensity\": 0,\n        \"moonGlowSize\": 0,\n        \"skyBrightness\": 0.74,\n        \"skySaturation\": 0.55,\n        \"skyContrast\": 1.07,\n        \"sunRayShimmer\": 5,\n        \"sunRayShimmerSpeed\": 5\n      },\n      \"cloud\": {\n        \"cloudScale\": 1.56,\n        \"coverage\": 0.71,\n        \"density\": 0.97,\n        \"softness\": 0.28,\n        \"windSpeed\": 0.04,\n        \"turbulence\": 0.36,\n        \"lightIntensity\": 0,\n        \"ambientDarkness\": 1,\n        \"backlightIntensity\": 0.55,\n        \"numLayers\": 1\n      },\n      \"rain\": {\n        \"glassZoom\": 0.51,\n        \"fallingSpeed\": 3,\n        \"fallingStreakLength\": 1.46,\n        \"fallingLayers\": 12,\n        \"fallingAngle\": 0.02,\n        \"glassIntensity\": 1.32\n      },\n      \"lightning\": {\n        \"enabled\": true,\n        \"flashIntensity\": 1.57\n      },\n      \"glass\": {\n        \"brightness\": 0.9,\n        \"blur\": 1,\n        \"strength\": 45,\n        \"chromaticAberration\": 2\n      },\n      \"interactions\": {\n        \"rainRefractionStrength\": 2,\n        \"lightningSceneIllumination\": 0.83\n      },\n      \"post\": {\n        \"haze\": 0.07,\n        \"hazeHorizon\": 1,\n        \"hazeContrast\": 0,\n        \"bloomIntensity\": 0.44,\n        \"exposureIntensity\": 1.1,\n        \"exposureRecovery\": 4.5\n      }\n    },\n    \"dusk\": {\n      \"celestial\": {\n        \"starDensity\": 1.72,\n        \"celestialX\": 0.69,\n        \"celestialY\": 0.74,\n        \"moonGlowIntensity\": 0,\n        \"moonGlowSize\": 0,\n        \"skyBrightness\": 0.91,\n        \"skySaturation\": 1.19,\n        \"skyContrast\": 1.03,\n        \"sunRayShimmer\": 5,\n        \"sunRayShimmerSpeed\": 5,\n        \"sunGlowIntensity\": 5.05\n      },\n      \"cloud\": {\n        \"cloudScale\": 1.56,\n        \"coverage\": 0.71,\n        \"density\": 0.97,\n        \"softness\": 0.28,\n        \"windSpeed\": 0.04,\n        \"lightIntensity\": 0,\n        \"backlightIntensity\": 0.55,\n        \"numLayers\": 1,\n        \"turbulence\": 0.36,\n        \"ambientDarkness\": 1\n      },\n      \"rain\": {\n        \"glassZoom\": 0.51,\n        \"fallingSpeed\": 3,\n        \"fallingStreakLength\": 1.46,\n        \"fallingLayers\": 12,\n        \"fallingAngle\": 0.02,\n        \"glassIntensity\": 1.32\n      },\n      \"lightning\": {\n        \"enabled\": true,\n        \"branchDensity\": 0.83,\n        \"flashIntensity\": 1.57\n      },\n      \"glass\": {\n        \"brightness\": 0.9,\n        \"blur\": 1,\n        \"strength\": 45,\n        \"chromaticAberration\": 2\n      },\n      \"interactions\": {\n        \"lightningSceneIllumination\": 0.83,\n        \"rainRefractionStrength\": 2\n      },\n      \"post\": {\n        \"haze\": 0.07,\n        \"hazeHorizon\": 1,\n        \"hazeContrast\": 0,\n        \"bloomIntensity\": 0.44,\n        \"exposureIntensity\": 1.1,\n        \"exposureRecovery\": 4.5\n      }\n    },\n    \"midnight\": {\n      \"celestial\": {\n        \"moonPhase\": 0.5,\n        \"starDensity\": 1.72,\n        \"celestialY\": 0.71,\n        \"moonGlowIntensity\": 4.55,\n        \"moonGlowSize\": 1.25,\n        \"skyBrightness\": 0.78,\n        \"skySaturation\": 0.74,\n        \"skyContrast\": 1.03,\n        \"sunRayShimmer\": 5,\n        \"sunRayShimmerSpeed\": 5\n      },\n      \"cloud\": {\n        \"cloudScale\": 1.56,\n        \"coverage\": 0.71,\n        \"density\": 0.97,\n        \"softness\": 0.28,\n        \"windSpeed\": 0.04,\n        \"turbulence\": 0.36,\n        \"lightIntensity\": 0,\n        \"ambientDarkness\": 0.88,\n        \"backlightIntensity\": 0.55,\n        \"numLayers\": 1\n      },\n      \"rain\": {\n        \"glassIntensity\": 1.32,\n        \"glassZoom\": 0.51,\n        \"fallingSpeed\": 3,\n        \"fallingStreakLength\": 1.46,\n        \"fallingLayers\": 12,\n        \"fallingAngle\": 0.02\n      },\n      \"lightning\": {\n        \"enabled\": true,\n        \"autoInterval\": 8.5,\n        \"branchDensity\": 0.89,\n        \"flashIntensity\": 1.57\n      },\n      \"glass\": {\n        \"blur\": 1,\n        \"strength\": 45,\n        \"chromaticAberration\": 2,\n        \"brightness\": 0.9\n      },\n      \"interactions\": {\n        \"lightningSceneIllumination\": 0.83,\n        \"rainRefractionStrength\": 2\n      },\n      \"post\": {\n        \"haze\": 0.07,\n        \"hazeHorizon\": 1,\n        \"hazeContrast\": 0,\n        \"bloomIntensity\": 0.44,\n        \"exposureIntensity\": 1.1,\n        \"exposureRecovery\": 4.5\n      }\n    }\n  },\n  \"snow\": {\n    \"dawn\": {\n      \"celestial\": {\n        \"starDensity\": 1.72,\n        \"celestialY\": 0.71,\n        \"moonGlowIntensity\": 3.55,\n        \"moonGlowSize\": 0.97,\n        \"skyBrightness\": 0.6,\n        \"skySaturation\": 1.5,\n        \"skyContrast\": 0\n      },\n      \"cloud\": {\n        \"density\": 1.19,\n        \"windSpeed\": 0.04,\n        \"turbulence\": 0.3,\n        \"lightIntensity\": 1.73,\n        \"ambientDarkness\": 0.94,\n        \"backlightIntensity\": 1.14\n      },\n      \"snow\": {\n        \"intensity\": 0.53,\n        \"layers\": 8,\n        \"fallSpeed\": 1.5,\n        \"windSpeed\": 2,\n        \"windAngle\": 1.95,\n        \"turbulence\": 0.59,\n        \"drift\": 1,\n        \"flutter\": 1,\n        \"windShear\": 0.8,\n        \"flakeSize\": 1.05,\n        \"sizeVariation\": 0.63,\n        \"opacity\": 0.4,\n        \"glowAmount\": 0.44,\n        \"sparkle\": 0.97\n      },\n      \"glass\": {\n        \"depth\": 8,\n        \"strength\": 10,\n        \"chromaticAberration\": 2,\n        \"brightness\": 0.95\n      },\n      \"post\": {\n        \"haze\": 0.03,\n        \"hazeHorizon\": 1,\n        \"hazeDesaturation\": 0,\n        \"hazeContrast\": 0.05,\n        \"godRayIntensity\": 0.4\n      }\n    },\n    \"noon\": {\n      \"celestial\": {\n        \"starDensity\": 1.72,\n        \"celestialY\": 0.71,\n        \"sunGlowIntensity\": 3.45,\n        \"sunGlowSize\": 0.29,\n        \"sunRayCount\": 0,\n        \"moonGlowIntensity\": 3.55,\n        \"moonGlowSize\": 0.97,\n        \"skyBrightness\": 0.35,\n        \"skySaturation\": 0.73,\n        \"skyContrast\": 0.69\n      },\n      \"cloud\": {\n        \"density\": 1.19,\n        \"windSpeed\": 0.04,\n        \"turbulence\": 0.3,\n        \"lightIntensity\": 0.57,\n        \"ambientDarkness\": 1,\n        \"backlightIntensity\": 1.01\n      },\n      \"snow\": {\n        \"intensity\": 0.53,\n        \"layers\": 8,\n        \"fallSpeed\": 1.5,\n        \"windSpeed\": 2,\n        \"windAngle\": 1.95,\n        \"turbulence\": 0.59,\n        \"drift\": 1,\n        \"flutter\": 1,\n        \"windShear\": 0.8,\n        \"flakeSize\": 1.28,\n        \"sizeVariation\": 0.63,\n        \"opacity\": 0.34,\n        \"glowAmount\": 0.47,\n        \"sparkle\": 1\n      },\n      \"glass\": {\n        \"depth\": 8,\n        \"strength\": 10,\n        \"chromaticAberration\": 2,\n        \"blur\": 1,\n        \"brightness\": 1.15,\n        \"saturation\": 1.4\n      },\n      \"post\": {\n        \"haze\": 0.03,\n        \"hazeHorizon\": 1,\n        \"hazeDesaturation\": 0,\n        \"hazeContrast\": 0.05,\n        \"godRayIntensity\": 0.4\n      }\n    },\n    \"dusk\": {\n      \"celestial\": {\n        \"starDensity\": 1.72,\n        \"celestialY\": 0.71,\n        \"sunGlowIntensity\": 4.9,\n        \"moonGlowIntensity\": 3.55,\n        \"moonGlowSize\": 0.97,\n        \"skyBrightness\": 1.01,\n        \"skySaturation\": 2,\n        \"skyContrast\": 0.86\n      },\n      \"cloud\": {\n        \"cloudScale\": 1.82,\n        \"coverage\": 0.62,\n        \"density\": 1.19,\n        \"softness\": 0.31,\n        \"windSpeed\": 0.04,\n        \"turbulence\": 0.3,\n        \"lightIntensity\": 0.55,\n        \"ambientDarkness\": 0.9,\n        \"backlightIntensity\": 1.53\n      },\n      \"snow\": {\n        \"intensity\": 0.96,\n        \"layers\": 8,\n        \"fallSpeed\": 1.5,\n        \"windSpeed\": 2,\n        \"windAngle\": 1.95,\n        \"turbulence\": 0.59,\n        \"drift\": 1,\n        \"flutter\": 1,\n        \"windShear\": 0.8,\n        \"flakeSize\": 1.05,\n        \"sizeVariation\": 0.63,\n        \"opacity\": 0.34,\n        \"glowAmount\": 0.88,\n        \"sparkle\": 0.97\n      },\n      \"glass\": {\n        \"depth\": 8,\n        \"strength\": 10,\n        \"chromaticAberration\": 2,\n        \"blur\": 2\n      },\n      \"post\": {\n        \"haze\": 0.03,\n        \"hazeHorizon\": 1,\n        \"hazeDesaturation\": 0,\n        \"hazeContrast\": 0.05,\n        \"godRayIntensity\": 0.4\n      }\n    },\n    \"midnight\": {\n      \"celestial\": {\n        \"moonPhase\": 0.5,\n        \"starDensity\": 1.72,\n        \"celestialY\": 0.71,\n        \"moonGlowIntensity\": 3.55,\n        \"moonGlowSize\": 0.97,\n        \"skyBrightness\": 1.07,\n        \"skySaturation\": 1.57,\n        \"skyContrast\": 0.59\n      },\n      \"cloud\": {\n        \"cloudScale\": 1.15,\n        \"coverage\": 0.5,\n        \"density\": 1.19,\n        \"softness\": 0.14,\n        \"windSpeed\": 0.04,\n        \"windAngle\": -0.08,\n        \"turbulence\": 0.3,\n        \"lightIntensity\": 1.29,\n        \"ambientDarkness\": 0.04\n      },\n      \"snow\": {\n        \"intensity\": 0.53,\n        \"layers\": 8,\n        \"fallSpeed\": 1.5,\n        \"windSpeed\": 1.44,\n        \"windAngle\": 1.95,\n        \"turbulence\": 0.59,\n        \"drift\": 1,\n        \"flutter\": 1,\n        \"windShear\": 0.8,\n        \"flakeSize\": 1.05,\n        \"sizeVariation\": 0.63,\n        \"opacity\": 0.34,\n        \"glowAmount\": 0.88,\n        \"sparkle\": 0.97\n      },\n      \"glass\": {\n        \"depth\": 8,\n        \"strength\": 10,\n        \"chromaticAberration\": 2,\n        \"blur\": 1\n      },\n      \"post\": {\n        \"haze\": 0.03,\n        \"hazeHorizon\": 1,\n        \"hazeDesaturation\": 0,\n        \"hazeContrast\": 0.05,\n        \"godRayIntensity\": 0.4\n      }\n    }\n  },\n  \"sleet\": {\n    \"dawn\": {\n      \"celestial\": {\n        \"starDensity\": 0.36,\n        \"celestialX\": 0.69,\n        \"celestialY\": 0.74,\n        \"sunGlowIntensity\": 5,\n        \"skyBrightness\": 0.82\n      },\n      \"cloud\": {\n        \"windSpeed\": 0.03,\n        \"turbulence\": 0.56,\n        \"ambientDarkness\": 1,\n        \"backlightIntensity\": 1.27\n      },\n      \"rain\": {\n        \"glassIntensity\": 0.41,\n        \"glassZoom\": 0.85,\n        \"fallingIntensity\": 0.22,\n        \"fallingSpeed\": 2.07,\n        \"fallingAngle\": 0.31,\n        \"fallingStreakLength\": 2\n      },\n      \"snow\": {\n        \"intensity\": 0.34,\n        \"layers\": 8,\n        \"fallSpeed\": 8,\n        \"windSpeed\": 1.8,\n        \"drift\": 1,\n        \"flakeSize\": 0.85,\n        \"windAngle\": 1.76,\n        \"turbulence\": 0.92,\n        \"flutter\": 0.98,\n        \"windShear\": 1,\n        \"sizeVariation\": 1,\n        \"opacity\": 0.78,\n        \"glowAmount\": 0.09,\n        \"sparkle\": 0.48\n      },\n      \"glass\": {\n        \"brightness\": 0.95\n      }\n    },\n    \"noon\": {\n      \"celestial\": {\n        \"starDensity\": 0.36,\n        \"celestialX\": 0.69,\n        \"celestialY\": 0.74,\n        \"sunGlowIntensity\": 5\n      },\n      \"cloud\": {\n        \"windSpeed\": 0.03,\n        \"turbulence\": 0.56\n      },\n      \"rain\": {\n        \"glassIntensity\": 0.41,\n        \"glassZoom\": 0.85,\n        \"fallingIntensity\": 0.22,\n        \"fallingSpeed\": 2.07,\n        \"fallingAngle\": 0.31,\n        \"fallingStreakLength\": 2\n      },\n      \"lightning\": {\n        \"enabled\": true\n      },\n      \"snow\": {\n        \"intensity\": 0.34,\n        \"layers\": 8,\n        \"fallSpeed\": 8,\n        \"windSpeed\": 1.8,\n        \"drift\": 1,\n        \"flakeSize\": 0.85,\n        \"flutter\": 0.98,\n        \"sizeVariation\": 1,\n        \"opacity\": 0.78,\n        \"glowAmount\": 0.09,\n        \"sparkle\": 0.48,\n        \"windAngle\": 1.76,\n        \"turbulence\": 1,\n        \"windShear\": 1\n      },\n      \"glass\": {\n        \"brightness\": 1\n      }\n    },\n    \"dusk\": {\n      \"celestial\": {\n        \"starDensity\": 0.36,\n        \"celestialX\": 0.69,\n        \"celestialY\": 0.74,\n        \"sunGlowIntensity\": 5\n      },\n      \"cloud\": {\n        \"windSpeed\": 0.03,\n        \"turbulence\": 0.56\n      },\n      \"rain\": {\n        \"glassIntensity\": 0.41,\n        \"glassZoom\": 0.85,\n        \"fallingIntensity\": 0.22,\n        \"fallingSpeed\": 2.07,\n        \"fallingAngle\": 0.31,\n        \"fallingStreakLength\": 2\n      },\n      \"lightning\": {\n        \"enabled\": true\n      },\n      \"snow\": {\n        \"intensity\": 0.34,\n        \"layers\": 8,\n        \"fallSpeed\": 8,\n        \"windSpeed\": 1.8,\n        \"drift\": 1,\n        \"flakeSize\": 0.85,\n        \"flutter\": 0.98,\n        \"sizeVariation\": 1,\n        \"opacity\": 0.78,\n        \"glowAmount\": 0.09,\n        \"sparkle\": 0.48,\n        \"windAngle\": 1.76,\n        \"windShear\": 1\n      },\n      \"glass\": {\n        \"brightness\": 0.9\n      }\n    },\n    \"midnight\": {\n      \"celestial\": {\n        \"starDensity\": 0.36,\n        \"celestialX\": 0.69,\n        \"celestialY\": 0.74,\n        \"sunGlowIntensity\": 5\n      },\n      \"cloud\": {\n        \"windSpeed\": 0.03,\n        \"turbulence\": 0.56\n      },\n      \"rain\": {\n        \"glassIntensity\": 0.41,\n        \"glassZoom\": 0.85,\n        \"fallingIntensity\": 0.22,\n        \"fallingSpeed\": 2.07,\n        \"fallingAngle\": 0.31,\n        \"fallingStreakLength\": 2\n      },\n      \"lightning\": {\n        \"enabled\": true\n      },\n      \"snow\": {\n        \"intensity\": 0.34,\n        \"layers\": 8,\n        \"fallSpeed\": 8,\n        \"drift\": 1,\n        \"flakeSize\": 0.85,\n        \"windSpeed\": 1.8,\n        \"flutter\": 0.98,\n        \"sizeVariation\": 1,\n        \"opacity\": 0.78,\n        \"glowAmount\": 0.09,\n        \"sparkle\": 0.48,\n        \"windAngle\": 1.76,\n        \"windShear\": 1\n      }\n    }\n  },\n  \"hail\": {\n    \"dawn\": {\n      \"celestial\": {\n        \"sunGlowIntensity\": 5,\n        \"celestialY\": 0.74,\n        \"celestialX\": 0.69,\n        \"skyBrightness\": 0.42,\n        \"starDensity\": 0.36,\n        \"sunGlowSize\": 0.36,\n        \"skySaturation\": 0.74,\n        \"sunRayShimmer\": 5,\n        \"sunRayShimmerSpeed\": 5\n      },\n      \"cloud\": {\n        \"density\": 0.83,\n        \"windSpeed\": 0.14,\n        \"windAngle\": -3.14,\n        \"turbulence\": 0.55,\n        \"lightIntensity\": 0.55,\n        \"ambientDarkness\": 0.75,\n        \"backlightIntensity\": 0.64,\n        \"numLayers\": 1\n      },\n      \"rain\": {\n        \"glassIntensity\": 1,\n        \"glassZoom\": 0.69,\n        \"fallingIntensity\": 0.42,\n        \"fallingSpeed\": 3,\n        \"fallingStreakLength\": 0.56,\n        \"fallingLayers\": 1\n      },\n      \"snow\": {\n        \"intensity\": 0.81,\n        \"layers\": 8,\n        \"fallSpeed\": 8,\n        \"windAngle\": 2.23,\n        \"turbulence\": 0.91,\n        \"drift\": 0.63,\n        \"flutter\": 0.79,\n        \"windShear\": 0.78,\n        \"flakeSize\": 0.77,\n        \"sizeVariation\": 0.48,\n        \"opacity\": 1,\n        \"glowAmount\": 0.29,\n        \"sparkle\": 0.44,\n        \"windSpeed\": 0.99\n      },\n      \"glass\": {\n        \"brightness\": 0.95\n      },\n      \"interactions\": {\n        \"rainRefractionStrength\": 0.96\n      },\n      \"post\": {\n        \"haze\": 0.11,\n        \"hazeHorizon\": 1,\n        \"bloomIntensity\": 0.13,\n        \"bloomThreshold\": 0.64,\n        \"godRayIntensity\": 0.56\n      }\n    },\n    \"noon\": {\n      \"celestial\": {\n        \"moonPhase\": 0.2421,\n        \"sunGlowIntensity\": 2.5,\n        \"sunGlowSize\": 0.54,\n        \"celestialY\": 0.71,\n        \"celestialX\": 0.74,\n        \"starDensity\": 0.14,\n        \"skyBrightness\": 0.56,\n        \"skySaturation\": 0.62,\n        \"skyContrast\": 1.03,\n        \"sunRayCount\": 3,\n        \"sunRayShimmer\": 5,\n        \"sunRayShimmerSpeed\": 5\n      },\n      \"cloud\": {\n        \"coverage\": 0.74,\n        \"density\": 0.95,\n        \"softness\": 0.36,\n        \"windSpeed\": 0.14,\n        \"windAngle\": -3.14,\n        \"turbulence\": 0.55,\n        \"lightIntensity\": 0.6,\n        \"ambientDarkness\": 1,\n        \"backlightIntensity\": 0.21,\n        \"numLayers\": 1\n      },\n      \"rain\": {\n        \"glassIntensity\": 0.72,\n        \"glassZoom\": 0.69,\n        \"fallingIntensity\": 0.42,\n        \"fallingSpeed\": 3,\n        \"fallingStreakLength\": 0.56,\n        \"fallingLayers\": 1\n      },\n      \"snow\": {\n        \"intensity\": 0.8,\n        \"layers\": 8,\n        \"fallSpeed\": 8,\n        \"windAngle\": 2.23,\n        \"turbulence\": 0.91,\n        \"drift\": 0.63,\n        \"flutter\": 0.79,\n        \"windShear\": 0.78,\n        \"flakeSize\": 0.77,\n        \"sizeVariation\": 0.77,\n        \"opacity\": 1,\n        \"glowAmount\": 0.71,\n        \"sparkle\": 0.1,\n        \"windSpeed\": 0.89\n      },\n      \"glass\": {\n        \"brightness\": 1\n      },\n      \"interactions\": {\n        \"rainRefractionStrength\": 0.96\n      },\n      \"post\": {\n        \"haze\": 0.23,\n        \"hazeHorizon\": 1,\n        \"bloomIntensity\": 0.13,\n        \"bloomThreshold\": 0.64,\n        \"godRayIntensity\": 0.56\n      }\n    },\n    \"dusk\": {\n      \"celestial\": {\n        \"sunGlowIntensity\": 3.21,\n        \"sunGlowSize\": 0.51,\n        \"celestialY\": 0.74,\n        \"celestialX\": 0.69,\n        \"starDensity\": 0.11,\n        \"skyBrightness\": 0.47,\n        \"skySaturation\": 1.42,\n        \"skyContrast\": 1.39,\n        \"sunRayShimmer\": 5,\n        \"sunRayShimmerSpeed\": 5\n      },\n      \"cloud\": {\n        \"density\": 0.83,\n        \"windSpeed\": 0.14,\n        \"windAngle\": -3.14,\n        \"turbulence\": 0.55,\n        \"lightIntensity\": 0.79,\n        \"ambientDarkness\": 0.75,\n        \"backlightIntensity\": 0.64,\n        \"numLayers\": 1\n      },\n      \"rain\": {\n        \"glassIntensity\": 0.72,\n        \"glassZoom\": 0.69,\n        \"fallingIntensity\": 0.42,\n        \"fallingSpeed\": 3,\n        \"fallingStreakLength\": 0.56,\n        \"fallingLayers\": 1\n      },\n      \"snow\": {\n        \"intensity\": 0.47,\n        \"layers\": 8,\n        \"fallSpeed\": 8,\n        \"windAngle\": 2.23,\n        \"turbulence\": 0.91,\n        \"drift\": 0.63,\n        \"flutter\": 0.79,\n        \"windShear\": 0.78,\n        \"flakeSize\": 0.77,\n        \"sizeVariation\": 0.77,\n        \"opacity\": 1,\n        \"glowAmount\": 0.71,\n        \"sparkle\": 0.1,\n        \"windSpeed\": 1\n      },\n      \"glass\": {\n        \"brightness\": 0.9\n      },\n      \"interactions\": {\n        \"rainRefractionStrength\": 0.96\n      },\n      \"post\": {\n        \"haze\": 0.15,\n        \"hazeHorizon\": 1,\n        \"bloomIntensity\": 0.13,\n        \"bloomThreshold\": 0.64,\n        \"godRayIntensity\": 0.56\n      }\n    },\n    \"midnight\": {\n      \"celestial\": {\n        \"celestialY\": 0.71,\n        \"moonGlowIntensity\": 3.55,\n        \"moonGlowSize\": 0.97,\n        \"celestialX\": 0.74,\n        \"starDensity\": 1.72,\n        \"skyBrightness\": 0.42,\n        \"skySaturation\": 0.74,\n        \"sunRayShimmer\": 5,\n        \"sunRayShimmerSpeed\": 5\n      },\n      \"cloud\": {\n        \"density\": 0.83,\n        \"windSpeed\": 0.14,\n        \"windAngle\": -3.14,\n        \"turbulence\": 0.55,\n        \"lightIntensity\": 1.21,\n        \"ambientDarkness\": 0.75,\n        \"backlightIntensity\": 0.64,\n        \"numLayers\": 1\n      },\n      \"rain\": {\n        \"glassIntensity\": 0.72,\n        \"glassZoom\": 0.69,\n        \"fallingIntensity\": 0.42,\n        \"fallingSpeed\": 3,\n        \"fallingStreakLength\": 0.56,\n        \"fallingLayers\": 1\n      },\n      \"snow\": {\n        \"intensity\": 0.47,\n        \"layers\": 8,\n        \"fallSpeed\": 8,\n        \"windAngle\": 2.23,\n        \"turbulence\": 0.87,\n        \"drift\": 0.63,\n        \"flutter\": 0.79,\n        \"windShear\": 0.78,\n        \"flakeSize\": 0.77,\n        \"sizeVariation\": 0.85,\n        \"opacity\": 1,\n        \"glowAmount\": 0.71,\n        \"sparkle\": 0.1,\n        \"windSpeed\": 1.11\n      },\n      \"interactions\": {\n        \"rainRefractionStrength\": 0.96\n      },\n      \"post\": {\n        \"haze\": 0.14,\n        \"hazeHorizon\": 1,\n        \"bloomIntensity\": 0.13,\n        \"bloomThreshold\": 0.64,\n        \"godRayIntensity\": 0.56\n      }\n    }\n  },\n  \"windy\": {\n    \"dawn\": {\n      \"celestial\": {\n        \"sunGlowIntensity\": 5,\n        \"celestialY\": 0.74,\n        \"celestialX\": 0.69,\n        \"skyBrightness\": 1,\n        \"starDensity\": 0.36\n      },\n      \"cloud\": {\n        \"cloudScale\": 0.85,\n        \"coverage\": 0.4,\n        \"windSpeed\": 0.17\n      },\n      \"glass\": {\n        \"brightness\": 0.95\n      }\n    },\n    \"noon\": {\n      \"celestial\": {\n        \"moonPhase\": 0.2421,\n        \"sunGlowIntensity\": 1.59,\n        \"sunGlowSize\": 0.55,\n        \"celestialY\": 0.71,\n        \"celestialX\": 0.74,\n        \"starDensity\": 0.14\n      },\n      \"cloud\": {\n        \"cloudScale\": 0.85,\n        \"coverage\": 0.4,\n        \"windSpeed\": 0.17\n      },\n      \"glass\": {\n        \"brightness\": 1\n      }\n    },\n    \"dusk\": {\n      \"celestial\": {\n        \"sunGlowIntensity\": 3.21,\n        \"sunGlowSize\": 0.51,\n        \"celestialY\": 0.74,\n        \"celestialX\": 0.69,\n        \"starDensity\": 0.11\n      },\n      \"cloud\": {\n        \"cloudScale\": 0.85,\n        \"coverage\": 0.4,\n        \"windSpeed\": 0.17\n      },\n      \"glass\": {\n        \"brightness\": 0.9\n      }\n    },\n    \"midnight\": {\n      \"celestial\": {\n        \"celestialY\": 0.71,\n        \"moonGlowIntensity\": 3.55,\n        \"moonGlowSize\": 0.97,\n        \"celestialX\": 0.74,\n        \"starDensity\": 1.72\n      },\n      \"cloud\": {\n        \"cloudScale\": 0.85,\n        \"coverage\": 0.4,\n        \"windSpeed\": 0.17\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/runtime/glass-panel-svg.tsx",
    "content": "// glass-panel-svg.tsx\n//\n// Creates a \"frosted glass\" refraction effect using SVG displacement maps\n// applied via CSS backdrop-filter. The effect bends what's behind the glass\n// panel, simulating how thick glass distorts light at its edges.\n//\n// How it works:\n// 1. Generate an SVG with color gradients encoding X/Y displacement (R=X, G=Y)\n// 2. Embed that SVG as a data URI in an feDisplacementMap filter\n// 3. Apply the filter via backdrop-filter CSS property\n//\n// Why SVG instead of WebGL: backdrop-filter composes naturally with the DOM,\n// handles transparency correctly, and doesn't require canvas management.\n// Tradeoff: regenerates the SVG when dimensions change (memoized to minimize).\n//\n// Browser support: Chrome, Safari, Edge. Firefox has partial support.\n// Degrades gracefully to no effect on unsupported browsers.\n//\n// Usage:\n//   <GlassPanel depth={10} strength={40}>content</GlassPanel>\n//   or\n//   const styles = useGlassStyles({ width, height, depth: 10 })\n//   <div style={styles}>content</div>\n\n\"use client\";\n\nimport {\n  useRef,\n  useState,\n  useEffect,\n  useCallback,\n  useMemo,\n  type ReactNode,\n  type CSSProperties,\n  type RefObject,\n} from \"react\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\ninterface Dimensions {\n  width: number;\n  height: number;\n}\n\ninterface GlassEffectOptions {\n  /** How far the refraction extends inward from edges (px) */\n  depth: number;\n  /** Border radius for the inner \"flat\" area (px) */\n  radius: number;\n  /** Intensity of the displacement effect */\n  strength: number;\n  /** Color fringing at edges—simulates light dispersion through glass */\n  chromaticAberration: number;\n  /** Gaussian blur applied before and after displacement */\n  blur: number;\n  /** Multiplier for backdrop brightness */\n  brightness: number;\n  /** Multiplier for backdrop color saturation */\n  saturation: number;\n}\n\n// =============================================================================\n// Constants\n// =============================================================================\n\nconst DEFAULT_GLASS_OPTIONS: GlassEffectOptions = {\n  depth: 12,\n  radius: 12,\n  strength: 40,\n  chromaticAberration: 8,\n  blur: 2,\n  brightness: 1.05,\n  saturation: 1.2,\n} as const;\n\n// =============================================================================\n// SVG Filter Generators\n// =============================================================================\n\ninterface DisplacementMapParams {\n  width: number;\n  height: number;\n  radius: number;\n  depth: number;\n}\n\n/**\n * Generates an SVG displacement map as a data URI.\n *\n * The displacement map encodes how much to shift each pixel:\n * - Red channel (0-255) → X displacement (-128 to +127 after normalization)\n * - Green channel (0-255) → Y displacement\n * - Gray (#808080) = no displacement (the \"neutral\" value)\n *\n * Structure:\n * - Outer area: gradients that push pixels outward (glass edge refraction)\n * - Inner rounded rect: neutral gray (flat, undistorted center)\n * - Blur on the inner rect creates a smooth transition\n */\nfunction buildDisplacementMapSvg({\n  width,\n  height,\n  radius,\n  depth,\n}: DisplacementMapParams): string {\n  // Scale gradient positions relative to element size to keep edge effect proportional\n  const radiusYPct = Math.ceil((radius / height) * 15);\n  const radiusXPct = Math.ceil((radius / width) * 15);\n  const innerWidth = Math.max(0, width - 2 * depth);\n  const innerHeight = Math.max(0, height - 2 * depth);\n\n  const svg = `<svg height=\"${height}\" width=\"${width}\" viewBox=\"0 0 ${width} ${height}\" xmlns=\"http://www.w3.org/2000/svg\">\n    <style>.mix { mix-blend-mode: screen; }</style>\n    <defs>\n      <linearGradient id=\"Y\" x1=\"0\" x2=\"0\" y1=\"${radiusYPct}%\" y2=\"${100 - radiusYPct}%\">\n        <stop offset=\"0%\" stop-color=\"#0F0\" />\n        <stop offset=\"100%\" stop-color=\"#000\" />\n      </linearGradient>\n      <linearGradient id=\"X\" x1=\"${radiusXPct}%\" x2=\"${100 - radiusXPct}%\" y1=\"0\" y2=\"0\">\n        <stop offset=\"0%\" stop-color=\"#F00\" />\n        <stop offset=\"100%\" stop-color=\"#000\" />\n      </linearGradient>\n    </defs>\n    <rect x=\"0\" y=\"0\" height=\"${height}\" width=\"${width}\" fill=\"#808080\" />\n    <g filter=\"blur(2px)\">\n      <rect x=\"0\" y=\"0\" height=\"${height}\" width=\"${width}\" fill=\"#000080\" />\n      <rect x=\"0\" y=\"0\" height=\"${height}\" width=\"${width}\" fill=\"url(#Y)\" class=\"mix\" />\n      <rect x=\"0\" y=\"0\" height=\"${height}\" width=\"${width}\" fill=\"url(#X)\" class=\"mix\" />\n      <rect x=\"${depth}\" y=\"${depth}\" height=\"${innerHeight}\" width=\"${innerWidth}\" fill=\"#808080\" rx=\"${radius}\" ry=\"${radius}\" filter=\"blur(${depth}px)\" />\n    </g>\n  </svg>`;\n\n  return \"data:image/svg+xml;utf8,\" + encodeURIComponent(svg);\n}\n\ninterface DisplacementFilterParams extends DisplacementMapParams {\n  strength: number;\n  chromaticAberration: number;\n}\n\n/**\n * Generates an SVG filter with feDisplacementMap as a data URI.\n *\n * When chromaticAberration > 0, displaces R/G/B channels by different amounts\n * to simulate how glass disperses light into its component colors (like a prism).\n * Red shifts most, green middle, blue least—creating color fringing at edges.\n */\nfunction buildDisplacementFilterUrl({\n  width,\n  height,\n  radius,\n  depth,\n  strength,\n  chromaticAberration,\n}: DisplacementFilterParams): string {\n  const mapUrl = buildDisplacementMapSvg({ width, height, radius, depth });\n\n  const feImage = `<feImage x=\"0\" y=\"0\" height=\"${height}\" width=\"${width}\" href=\"${mapUrl}\" result=\"displacementMap\" />`;\n\n  let filterContent: string;\n\n  if (chromaticAberration === 0) {\n    filterContent = `\n      ${feImage}\n      <feDisplacementMap in=\"SourceGraphic\" in2=\"displacementMap\" scale=\"${strength}\" xChannelSelector=\"R\" yChannelSelector=\"G\" />\n    `;\n  } else {\n    // Displace each color channel by different amounts to create fringing\n    const redScale = strength + chromaticAberration * 2;\n    const greenScale = strength + chromaticAberration;\n    const blueScale = strength;\n\n    // Each channel: displace → isolate with color matrix → blend back together\n    filterContent = `\n      ${feImage}\n      <feDisplacementMap in=\"SourceGraphic\" in2=\"displacementMap\" scale=\"${redScale}\" xChannelSelector=\"R\" yChannelSelector=\"G\" />\n      <feColorMatrix type=\"matrix\" values=\"1 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1 0\" result=\"displacedR\" />\n      <feDisplacementMap in=\"SourceGraphic\" in2=\"displacementMap\" scale=\"${greenScale}\" xChannelSelector=\"R\" yChannelSelector=\"G\" />\n      <feColorMatrix type=\"matrix\" values=\"0 0 0 0 0  0 1 0 0 0  0 0 0 0 0  0 0 0 1 0\" result=\"displacedG\" />\n      <feDisplacementMap in=\"SourceGraphic\" in2=\"displacementMap\" scale=\"${blueScale}\" xChannelSelector=\"R\" yChannelSelector=\"G\" />\n      <feColorMatrix type=\"matrix\" values=\"0 0 0 0 0  0 0 0 0 0  0 0 1 0 0  0 0 0 1 0\" result=\"displacedB\" />\n      <feBlend in=\"displacedR\" in2=\"displacedG\" mode=\"screen\"/>\n      <feBlend in2=\"displacedB\" mode=\"screen\"/>\n    `;\n  }\n\n  const svg = `<svg height=\"${height}\" width=\"${width}\" viewBox=\"0 0 ${width} ${height}\" xmlns=\"http://www.w3.org/2000/svg\">\n    <defs>\n      <filter id=\"displace\" color-interpolation-filters=\"sRGB\">${filterContent}</filter>\n    </defs>\n  </svg>`;\n\n  return \"data:image/svg+xml;utf8,\" + encodeURIComponent(svg) + \"#displace\";\n}\n\n// =============================================================================\n// Style Builders\n// =============================================================================\n\ninterface BackdropFilterParams {\n  filterUrl: string;\n  blur: number;\n  brightness: number;\n  saturation: number;\n}\n\n/**\n * Builds the backdrop-filter CSS value.\n *\n * Filter order matters:\n * 1. Pre-blur (half strength) - softens source before displacement\n * 2. SVG displacement filter - the actual refraction\n * 3. Post-blur (full strength) - smooths displacement artifacts\n * 4. Brightness/saturation - final color adjustment\n */\nfunction buildBackdropFilterValue({\n  filterUrl,\n  blur,\n  brightness,\n  saturation,\n}: BackdropFilterParams): string {\n  return `blur(${blur / 2}px) url('${filterUrl}') blur(${blur}px) brightness(${brightness}) saturate(${saturation})`;\n}\n\n// =============================================================================\n// Hooks\n// =============================================================================\n\n/**\n * Tracks element dimensions via ResizeObserver.\n *\n * Dimensions are rounded to integers because the SVG filter is regenerated\n * on size change, and sub-pixel changes would cause unnecessary rebuilds.\n */\nfunction useElementDimensions(\n  ref: RefObject<HTMLElement | null>,\n): Dimensions | null {\n  const [dimensions, setDimensions] = useState<Dimensions | null>(null);\n\n  const updateDimensions = useCallback(() => {\n    if (ref.current) {\n      const rect = ref.current.getBoundingClientRect();\n      setDimensions({\n        width: Math.round(rect.width),\n        height: Math.round(rect.height),\n      });\n    }\n  }, [ref]);\n\n  useEffect(() => {\n    updateDimensions();\n\n    const element = ref.current;\n    if (!element) return;\n\n    const observer = new ResizeObserver(updateDimensions);\n    observer.observe(element);\n\n    return () => observer.disconnect();\n  }, [updateDimensions, ref]);\n\n  return dimensions;\n}\n\n/**\n * Detects browser support for backdrop-filter.\n *\n * Returns true during SSR (optimistic) to avoid layout shift.\n * The effect degrades gracefully—unsupported browsers just see\n * no glass distortion, which is fine.\n */\nfunction useSupportsBackdropFilter(): boolean {\n  const [supported, setSupported] = useState(true);\n\n  useEffect(() => {\n    const hasSupport =\n      CSS.supports(\"backdrop-filter\", \"blur(1px)\") ||\n      CSS.supports(\"-webkit-backdrop-filter\", \"blur(1px)\");\n    setSupported(hasSupport);\n  }, []);\n\n  return supported;\n}\n\n// =============================================================================\n// Public API\n// =============================================================================\n\nexport interface GlassPanelProps {\n  children?: ReactNode;\n  className?: string;\n  depth?: number;\n  radius?: number;\n  strength?: number;\n  chromaticAberration?: number;\n  blur?: number;\n  /** When true, renders the displacement map as background for debugging */\n  debug?: boolean;\n}\n\n/**\n * A container that applies SVG glass refraction effect via backdrop-filter.\n */\nexport function GlassPanel({\n  children,\n  className,\n  depth = DEFAULT_GLASS_OPTIONS.depth,\n  radius = DEFAULT_GLASS_OPTIONS.radius,\n  strength = DEFAULT_GLASS_OPTIONS.strength,\n  chromaticAberration = DEFAULT_GLASS_OPTIONS.chromaticAberration,\n  blur = DEFAULT_GLASS_OPTIONS.blur,\n  debug = false,\n}: GlassPanelProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const dimensions = useElementDimensions(ref);\n\n  const style = useMemo((): CSSProperties => {\n    const base: CSSProperties = { borderRadius: radius };\n\n    if (!dimensions || dimensions.width <= 0 || dimensions.height <= 0) {\n      return base;\n    }\n\n    if (debug) {\n      const mapUrl = buildDisplacementMapSvg({\n        width: dimensions.width,\n        height: dimensions.height,\n        radius,\n        depth,\n      });\n      return {\n        ...base,\n        background: `url(\"${mapUrl}\")`,\n        backgroundSize: \"cover\",\n      };\n    }\n\n    const filterUrl = buildDisplacementFilterUrl({\n      width: dimensions.width,\n      height: dimensions.height,\n      radius,\n      depth,\n      strength,\n      chromaticAberration,\n    });\n\n    const backdropFilter = buildBackdropFilterValue({\n      filterUrl,\n      blur,\n      brightness: DEFAULT_GLASS_OPTIONS.brightness,\n      saturation: DEFAULT_GLASS_OPTIONS.saturation,\n    });\n\n    return {\n      ...base,\n      backdropFilter,\n      WebkitBackdropFilter: backdropFilter,\n    };\n  }, [dimensions, radius, depth, strength, chromaticAberration, blur, debug]);\n\n  return (\n    <div ref={ref} className={className} style={style}>\n      {children}\n    </div>\n  );\n}\n\n/**\n * Global CSS classes for glass panel styling (background, shadows).\n */\nexport function GlassPanelCSS() {\n  return (\n    <style>{`\n      .glass-panel {\n        background: rgba(255, 255, 255, 0.15);\n        box-shadow:\n          inset 0 0 0 1px rgba(255, 255, 255, 0.2),\n          inset 0 1px 0 rgba(255, 255, 255, 0.4),\n          0 4px 16px rgba(0, 0, 0, 0.1);\n      }\n      .glass-panel-dark {\n        background: rgba(0, 0, 0, 0.2);\n        box-shadow:\n          inset 0 0 0 1px rgba(255, 255, 255, 0.1),\n          inset 0 1px 0 rgba(255, 255, 255, 0.15),\n          0 4px 16px rgba(0, 0, 0, 0.2);\n      }\n    `}</style>\n  );\n}\n\nexport interface GlassPanelUnderlayProps {\n  children: ReactNode;\n  className?: string;\n  depth?: number;\n  radius?: number;\n  strength?: number;\n  chromaticAberration?: number;\n  blur?: number;\n  disabled?: boolean;\n}\n\n/**\n * Applies SVG glass refraction effect to a container.\n * On incompatible browsers or when disabled, renders without the effect.\n */\nexport function GlassPanelUnderlay({\n  children,\n  className,\n  depth = DEFAULT_GLASS_OPTIONS.depth,\n  radius = DEFAULT_GLASS_OPTIONS.radius,\n  strength = DEFAULT_GLASS_OPTIONS.strength,\n  chromaticAberration = DEFAULT_GLASS_OPTIONS.chromaticAberration,\n  blur = DEFAULT_GLASS_OPTIONS.blur,\n  disabled = false,\n}: GlassPanelUnderlayProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const dimensions = useElementDimensions(ref);\n  const supported = useSupportsBackdropFilter();\n\n  const style = useMemo((): CSSProperties => {\n    const base: CSSProperties = { borderRadius: radius };\n\n    const canApply =\n      !disabled &&\n      supported &&\n      dimensions &&\n      dimensions.width > 0 &&\n      dimensions.height > 0;\n\n    if (!canApply) {\n      return base;\n    }\n\n    const filterUrl = buildDisplacementFilterUrl({\n      width: dimensions.width,\n      height: dimensions.height,\n      radius,\n      depth,\n      strength,\n      chromaticAberration,\n    });\n\n    const backdropFilter = buildBackdropFilterValue({\n      filterUrl,\n      blur,\n      brightness: DEFAULT_GLASS_OPTIONS.brightness,\n      saturation: DEFAULT_GLASS_OPTIONS.saturation,\n    });\n\n    return {\n      ...base,\n      backdropFilter,\n      WebkitBackdropFilter: backdropFilter,\n    };\n  }, [\n    dimensions,\n    radius,\n    depth,\n    strength,\n    chromaticAberration,\n    blur,\n    disabled,\n    supported,\n  ]);\n\n  return (\n    <div ref={ref} className={className} style={style}>\n      {children}\n    </div>\n  );\n}\n\nexport interface UseGlassStylesOptions {\n  width: number;\n  height: number;\n  depth?: number;\n  radius?: number;\n  strength?: number;\n  chromaticAberration?: number;\n  blur?: number;\n  brightness?: number;\n  saturation?: number;\n  enabled?: boolean;\n}\n\n/**\n * Hook that generates glass effect styles to apply directly to an element.\n * Use when you need to apply the glass effect to an existing element\n * rather than wrapping it with GlassPanel or GlassPanelUnderlay.\n */\nexport function useGlassStyles({\n  width,\n  height,\n  depth = DEFAULT_GLASS_OPTIONS.depth,\n  radius = DEFAULT_GLASS_OPTIONS.radius,\n  strength = DEFAULT_GLASS_OPTIONS.strength,\n  chromaticAberration = DEFAULT_GLASS_OPTIONS.chromaticAberration,\n  blur = DEFAULT_GLASS_OPTIONS.blur,\n  brightness = DEFAULT_GLASS_OPTIONS.brightness,\n  saturation = DEFAULT_GLASS_OPTIONS.saturation,\n  enabled = true,\n}: UseGlassStylesOptions): CSSProperties {\n  const supported = useSupportsBackdropFilter();\n\n  return useMemo(() => {\n    if (!enabled || !supported || width <= 0 || height <= 0) {\n      return {};\n    }\n\n    const filterUrl = buildDisplacementFilterUrl({\n      width,\n      height,\n      radius,\n      depth,\n      strength,\n      chromaticAberration,\n    });\n\n    const backdropFilter = buildBackdropFilterValue({\n      filterUrl,\n      blur,\n      brightness,\n      saturation,\n    });\n\n    return {\n      backdropFilter,\n      WebkitBackdropFilter: backdropFilter,\n    };\n  }, [\n    width,\n    height,\n    depth,\n    radius,\n    strength,\n    chromaticAberration,\n    blur,\n    brightness,\n    saturation,\n    enabled,\n    supported,\n  ]);\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/shaders/celestial.frag.glsl",
    "content": "#version 300 es\nprecision highp float;\n\nin vec2 v_uv;\nout vec4 fragColor;\n\nuniform float u_time;\nuniform vec2 u_resolution;\nuniform float u_timeOfDay;\nuniform float u_moonPhase;\nuniform float u_starDensity;\nuniform vec2 u_celestialPos;\nuniform float u_sunSize;\nuniform float u_moonSize;\nuniform float u_sunGlowIntensity;\nuniform float u_sunGlowSize;\nuniform float u_sunRayCount;\nuniform float u_sunRayLength;\nuniform float u_sunRayIntensity;\nuniform float u_sunRayShimmer;\nuniform float u_sunRayShimmerSpeed;\nuniform float u_moonGlowIntensity;\nuniform float u_moonGlowSize;\nuniform float u_skyBrightness;\nuniform float u_skySaturation;\nuniform float u_skyContrast;\nuniform sampler2D u_moonTexture;\nuniform bool u_hasMoonTexture;\n\n#define PI 3.14159265359\n#define GODRAY_MAX_SAMPLES 32\n#define TAU 6.28318530718\n\nfloat hash(vec2 p) {\n  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nvec2 hash2(vec2 p) {\n  p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));\n  return fract(sin(p) * 43758.5453);\n}\n\nfloat noise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  f = f * f * (3.0 - 2.0 * f);\n  float a = hash(i);\n  float b = hash(i + vec2(1.0, 0.0));\n  float c = hash(i + vec2(0.0, 1.0));\n  float d = hash(i + vec2(1.0, 1.0));\n  return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);\n}\n\nfloat fbm(vec2 p, int octaves) {\n  float value = 0.0;\n  float amplitude = 0.5;\n  float frequency = 1.0;\n  for (int i = 0; i < 6; i++) {\n    if (i >= octaves) break;\n    value += amplitude * noise(p * frequency);\n    frequency *= 2.0;\n    amplitude *= 0.5;\n  }\n  return value;\n}\n\n// Calculate sun Y position based on time of day\n// Sun rises 0.18-0.32, visible during day, sets 0.68-0.82\n// Note: UV y=0 is bottom, y=1 is top, so below horizon means y < 0\nfloat getSunY(float timeOfDay, float baseY) {\n  float belowHorizon = -0.25;\n  float riseProgress = smoothstep(0.18, 0.32, timeOfDay);\n  float setProgress = smoothstep(0.68, 0.82, timeOfDay);\n  float visible = riseProgress * (1.0 - setProgress);\n  return mix(belowHorizon, baseY, visible);\n}\n\n// Calculate moon Y position based on time of day\n// Moon sets 0.12-0.26 (overlaps slightly with sun rise)\n// Moon rises 0.74-0.88 (overlaps slightly with sun set)\n// During overlap both are near horizon so both faded = subtle handoff\nfloat getMoonY(float timeOfDay, float baseY) {\n  float belowHorizon = -0.25;\n  float risingEvening = smoothstep(0.74, 0.88, timeOfDay);\n  float settingMorning = 1.0 - smoothstep(0.12, 0.26, timeOfDay);\n  float visible = max(risingEvening, settingMorning);\n  return mix(belowHorizon, baseY, visible);\n}\n\n// Fade opacity near horizon for smooth edge (bottom of screen)\n// Extended range so bodies visible earlier in their rise\nfloat getHorizonFade(float y) {\n  return smoothstep(-0.2, 0.0, y);\n}\n\nvec3 getSkyColor(vec2 uv, float timeOfDay) {\n  vec3 dayTop = vec3(0.4, 0.6, 0.9);\n  vec3 dayHorizon = vec3(0.7, 0.8, 0.95);\n  vec3 sunsetTop = vec3(0.2, 0.2, 0.4);\n  vec3 sunsetHorizon = vec3(0.9, 0.5, 0.2);\n  vec3 nightTop = vec3(0.02, 0.02, 0.05);\n  vec3 nightHorizon = vec3(0.05, 0.05, 0.1);\n\n  float dayAmount = smoothstep(0.25, 0.4, timeOfDay) * smoothstep(0.75, 0.6, timeOfDay);\n  float sunsetAmount = max(\n    smoothstep(0.2, 0.3, timeOfDay) * smoothstep(0.4, 0.3, timeOfDay),\n    smoothstep(0.6, 0.7, timeOfDay) * smoothstep(0.8, 0.7, timeOfDay)\n  );\n  float nightAmount = max(0.0, 1.0 - dayAmount - sunsetAmount);\n\n  float gradientFactor = pow(1.0 - uv.y, 1.0);\n\n  vec3 topColor = dayTop * dayAmount + sunsetTop * sunsetAmount + nightTop * nightAmount;\n  vec3 horizonColor = dayHorizon * dayAmount + sunsetHorizon * sunsetAmount + nightHorizon * nightAmount;\n\n  vec3 avgColor = (topColor + horizonColor) * 0.5;\n  topColor = mix(avgColor, topColor, u_skyContrast);\n  horizonColor = mix(avgColor, horizonColor, u_skyContrast);\n\n  vec3 color = mix(topColor, horizonColor, gradientFactor);\n\n  color *= u_skyBrightness;\n\n  float gray = dot(color, vec3(0.299, 0.587, 0.114));\n  if (u_skySaturation <= 1.0) {\n    color = mix(vec3(gray), color, u_skySaturation);\n  } else {\n    float boost = u_skySaturation - 1.0;\n    color = color + (color - vec3(gray)) * boost;\n  }\n\n  return clamp(color, 0.0, 1.0);\n}\n\nfloat drawStars(vec2 uv, float density, float time) {\n  float stars = 0.0;\n  for (int layer = 0; layer < 3; layer++) {\n    float layerScale = 100.0 + float(layer) * 50.0;\n    vec2 gridUV = uv * layerScale;\n    vec2 gridID = floor(gridUV);\n    vec2 gridFract = fract(gridUV);\n    vec2 starPos = hash2(gridID + float(layer) * 100.0);\n    float dist = length(gridFract - starPos);\n    float starPresent = step(1.0 - density * 0.3, hash(gridID * (float(layer) + 1.0)));\n    float starSize = 0.02 + hash(gridID.yx) * 0.03;\n    float twinkle = sin(time * (2.0 + hash(gridID) * 3.0) + hash(gridID.yx) * TAU) * 0.3 + 0.7;\n    float star = smoothstep(starSize, 0.0, dist) * starPresent * twinkle;\n    star *= 1.0 - float(layer) * 0.3;\n    stars += star;\n  }\n  return stars;\n}\n\nvec3 drawSun(vec2 uv, vec2 sunPos, float size) {\n  vec2 aspect = vec2(u_resolution.x / u_resolution.y, 1.0);\n  vec2 diff = (uv - sunPos) * aspect;\n  float dist = length(diff);\n  float angle = atan(diff.y, diff.x);\n\n  float disc = 1.0 - smoothstep(size * 0.9, size, dist);\n\n  vec3 sunCore = vec3(1.0, 1.0, 0.95);\n  vec3 sunEdge = vec3(1.0, 0.9, 0.4);\n  float edgeFactor = clamp(dist / size, 0.0, 1.0);\n  vec3 sunColor = mix(sunCore, sunEdge, edgeFactor);\n\n  float limbDarkening = 1.0 - pow(clamp(dist / size, 0.0, 1.0), 2.0) * 0.3;\n  sunColor *= limbDarkening;\n\n  float glowSize = max(0.1, u_sunGlowSize);\n  float scaledDist = dist / glowSize;\n  float glow1 = exp(-scaledDist * 8.0) * 0.5;\n  float glow2 = exp(-scaledDist * 3.0) * 0.3;\n  float glow3 = exp(-scaledDist * 1.5) * 0.15;\n\n  vec3 glowColor = vec3(1.0, 0.8, 0.4);\n  float glowTotal = (glow1 + glow2 + glow3) * u_sunGlowIntensity;\n\n  vec3 result = sunColor * disc * 2.0;\n  result += glowColor * glowTotal;\n\n  // ---------------------------------------------------------------------------\n  // Prismatic flare + rays\n  // ---------------------------------------------------------------------------\n  // Keep these effects subtle and mostly white — we want \"eye optics\" more than\n  // sci-fi neon. The rainbow shows up as a gentle chromatic fringe on very\n  // bright highlights.\n\n  // A thin, slightly prismatic halo ring around the sun.\n  float ringCenter = size * 1.15;\n  float ringWidth = max(size * 0.35, 0.001);\n  float ringMask = smoothstep(size * 0.85, size * 1.05, dist);\n  ringMask *= 1.0 - smoothstep(size * 5.0, size * 9.0, dist);\n\n  // Chromatic dispersion grows slightly with distance from the disc.\n  float chromaShift = size * (0.012 + u_sunRayIntensity * 0.06);\n  chromaShift *= smoothstep(size * 0.9, size * 2.4, dist);\n\n  float ringR = exp(-pow((dist - chromaShift - ringCenter) / ringWidth, 2.0));\n  float ringG = exp(-pow((dist - ringCenter) / ringWidth, 2.0));\n  float ringB = exp(-pow((dist + chromaShift - ringCenter) / ringWidth, 2.0));\n\n  // Desaturated spectrum-ish tint (mostly white).\n  float ringT = clamp((dist - size) / (size * 2.2), 0.0, 1.0);\n  vec3 ringSpectral = 0.55 + 0.45 * cos(TAU * (ringT + vec3(0.0, 0.33, 0.67)));\n  ringSpectral = clamp(ringSpectral, 0.0, 1.0);\n  vec3 ringColor = mix(vec3(1.0), ringSpectral, 0.45);\n\n  float ringIntensity = (ringR + ringG + ringB) / 3.0;\n  ringIntensity *= ringMask * u_sunGlowIntensity * 0.025;\n  result += ringColor * ringIntensity;\n\n  // Sun rays (diffraction spikes) with gentle shimmer/breath.\n  if (u_sunRayCount > 0.0 && u_sunRayIntensity > 0.0) {\n    // Rays are only visible close to the disc; bail early for perf.\n    if (dist < size * 3.6) {\n      float motion = clamp(u_sunRayShimmer, 0.0, 5.0);\n      float t = u_time * max(0.0, u_sunRayShimmerSpeed);\n\n      float rayPhase = angle * u_sunRayCount;\n      float rayIndex = floor(rayPhase / PI + 0.5);\n      float raySeed = hash(vec2(rayIndex, 19.17));\n\n      // Major rays + faint minor spikes (iris/eyelash diffraction).\n      float major = pow(abs(cos(rayPhase)), 10.0);\n      float minor = pow(abs(cos(rayPhase * 2.0 + raySeed * 2.3)), 22.0) * 0.18;\n      float rayShape = max(major, minor);\n\n      // Per-ray breathing (very slow) + along-ray shimmer (slightly faster).\n      float breathe =\n        1.0 +\n        (noise(vec2(t * 0.05, raySeed * 7.0)) - 0.5) * (0.08 * motion);\n      float shimmer =\n        1.0 +\n        (noise(vec2(dist * 12.0 - t * 0.25, raySeed * 23.0)) - 0.5) *\n          (0.12 * motion);\n      float micro =\n        1.0 +\n        (noise(vec2(t * 0.6, rayPhase * 0.8)) - 0.5) * (0.06 * motion);\n\n      float rayNoise =\n        0.72 +\n        0.28 * noise(vec2(rayPhase * 0.35, t * 0.12 + raySeed * 10.0));\n      float rayPattern = rayShape * rayNoise;\n\n      float rayStart = smoothstep(size * 0.75, size * 1.25, dist);\n      float rayEnd = smoothstep(size * (3.0 * breathe), size * 1.5, dist);\n\n      float rayLengthVar = 0.75 + raySeed * 0.55;\n      float maxRayDist = max(0.001, u_sunRayLength * 0.15);\n      float rayFalloff = exp(\n        -dist * dist / (maxRayDist * maxRayDist * rayLengthVar * breathe)\n      );\n\n      float rays = rayPattern * rayFalloff * rayStart * rayEnd * u_sunRayIntensity;\n      rays *= shimmer * micro;\n\n      // Chromatic fringe: compute a slightly different falloff per channel.\n      float prismMask = smoothstep(size * 1.05, size * 2.6, dist);\n      float rayChroma = size * (0.01 + u_sunRayIntensity * 0.05) * prismMask;\n\n      float distR = max(0.0, dist - rayChroma);\n      float distB = dist + rayChroma;\n\n      float falloffR = exp(\n        -distR * distR / (maxRayDist * maxRayDist * rayLengthVar * breathe)\n      );\n      float falloffG = rayFalloff;\n      float falloffB = exp(\n        -distB * distB / (maxRayDist * maxRayDist * rayLengthVar * breathe)\n      );\n\n      vec3 rayRGB = vec3(falloffR, falloffG, falloffB) * rayPattern * rayStart * rayEnd;\n      float rayAvg = (rayRGB.r + rayRGB.g + rayRGB.b) / 3.0;\n      vec3 rayChromaColor = rayRGB / max(rayAvg, 1e-4);\n\n      // Add a very subtle spectrum tint so the fringe reads as \"rainbow-like\",\n      // without turning into a colorful fantasy effect.\n      float rayT = clamp((dist - size) / (size * 2.6), 0.0, 1.0);\n      vec3 raySpectral = 0.55 + 0.45 * cos(TAU * (rayT + vec3(0.0, 0.33, 0.67)));\n      raySpectral = clamp(raySpectral, 0.0, 1.0);\n      raySpectral = mix(vec3(1.0), raySpectral, 0.28);\n\n      vec3 rayWarm = vec3(1.0, 0.92, 0.7);\n      float prismMix = clamp(0.08 + u_sunRayIntensity * 0.6, 0.0, 0.45) * prismMask;\n      vec3 rayColor = mix(rayWarm, rayChromaColor, prismMix);\n      rayColor = mix(rayColor, raySpectral, prismMix * 0.65);\n\n      result += rayColor * rays;\n    }\n  }\n\n  return result;\n}\n\nvec3 getSphereNormal(vec2 discUV) {\n  float r2 = dot(discUV, discUV);\n  if (r2 > 1.0) return vec3(0.0);\n  float z = sqrt(1.0 - r2);\n  return normalize(vec3(discUV.x, discUV.y, z));\n}\n\nvec2 sphereToEquirectangular(vec3 normal) {\n  float longitude = atan(normal.x, normal.z);\n  float u = longitude / TAU + 0.5;\n  float latitude = asin(clamp(normal.y, -1.0, 1.0));\n  float v = latitude / PI + 0.5;\n  return vec2(u, v);\n}\n\nvec3 getMoonSurfaceColor(vec3 normal, vec2 discUV) {\n  if (u_hasMoonTexture) {\n    vec2 texUV = sphereToEquirectangular(normal);\n    return texture(u_moonTexture, texUV).rgb;\n  }\n  float brightness = 0.7 + fbm(discUV * 5.0, 3) * 0.3;\n  return vec3(brightness * 0.85, brightness * 0.83, brightness * 0.8);\n}\n\nvec4 drawMoon(vec2 uv, vec2 moonPos, float size, float phase) {\n  vec2 aspect = vec2(u_resolution.x / u_resolution.y, 1.0);\n  vec2 diff = (uv - moonPos) * aspect;\n  float dist = length(diff);\n\n  vec2 discUV = diff / size;\n  float discDist = length(discUV);\n  float disc = 1.0 - smoothstep(0.95, 1.0, discDist);\n\n  float glowSize = max(0.1, u_moonGlowSize);\n  float glowIntensity = u_moonGlowIntensity;\n\n  if (disc < 0.001) {\n    float scaledDist = dist / glowSize;\n    float glow1 = exp(-scaledDist * 6.0) * 0.15;\n    float glow2 = exp(-scaledDist * 2.0) * 0.06;\n    vec3 glowColor = vec3(0.8, 0.85, 0.95);\n    float phaseAngle = phase * TAU;\n    vec3 sunDir = vec3(sin(phaseAngle), 0.0, -cos(phaseAngle));\n    float glowPhase = max(0.2, dot(normalize(vec3(discUV, 0.5)), sunDir) * 0.5 + 0.5);\n    return vec4(glowColor * (glow1 + glow2) * glowPhase * glowIntensity, 0.0);\n  }\n\n  vec3 normal = getSphereNormal(discUV);\n  float phaseAngle = phase * TAU;\n  vec3 sunDir = vec3(sin(phaseAngle), 0.0, -cos(phaseAngle));\n  float NdotL = dot(normal, sunDir);\n  float terminator = smoothstep(-0.02, 0.08, NdotL);\n\n  vec3 baseColor = getMoonSurfaceColor(normal, discUV);\n  vec3 ambient = baseColor * 0.03;\n  vec3 lit = baseColor * terminator;\n  vec3 moonSurface = ambient + lit;\n\n  float limbDarkening = 1.0 - pow(discDist, 3.0) * 0.15;\n  moonSurface *= limbDarkening;\n\n  float rimLight = pow(1.0 - abs(NdotL), 4.0) * terminator * 0.1;\n  moonSurface += vec3(1.0, 0.98, 0.95) * rimLight;\n\n  float scaledDist = dist / glowSize;\n  float glow1 = exp(-scaledDist * 6.0) * 0.12;\n  float glow2 = exp(-scaledDist * 2.0) * 0.06;\n  vec3 glowColor = vec3(0.8, 0.85, 0.95);\n  float litAmount = max(0.1, terminator);\n  vec3 glow = glowColor * (glow1 + glow2) * litAmount * glowIntensity;\n\n  return vec4(moonSurface * disc + glow, disc);\n}\n\nvoid main() {\n  vec2 uv = v_uv;\n\n  vec3 color = getSkyColor(uv, u_timeOfDay);\n\n  // Calculate separate Y positions for sun and moon\n  float sunY = getSunY(u_timeOfDay, u_celestialPos.y);\n  float moonY = getMoonY(u_timeOfDay, u_celestialPos.y);\n  vec2 sunPos = vec2(u_celestialPos.x, sunY);\n  vec2 moonPos = vec2(u_celestialPos.x, moonY);\n\n  // Stars visible when moon is up (night time)\n  float moonFade = getHorizonFade(moonY);\n  if (moonFade > 0.01) {\n    float stars = drawStars(uv, u_starDensity, u_time);\n    color += vec3(stars) * moonFade;\n  }\n\n  // Draw sun with horizon fade\n  float sunFade = getHorizonFade(sunY);\n  if (sunFade > 0.01) {\n    vec3 sun = drawSun(uv, sunPos, u_sunSize);\n    color += sun * sunFade;\n  }\n\n  // Draw moon with horizon fade\n  if (moonFade > 0.01) {\n    vec4 moon = drawMoon(uv, moonPos, u_moonSize, u_moonPhase);\n    float alpha = moon.a * moonFade;\n    color = mix(color, moon.rgb, alpha) + moon.rgb * (1.0 - moon.a) * moonFade;\n  }\n\n  // Alpha is reserved for a cloud-occlusion mask (used by post-processing like\n  // crepuscular rays). The celestial pass contains no cloud coverage, so it\n  // writes 0.\n  fragColor = vec4(color, 0.0);\n}\n\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/shaders/cloud.frag.glsl",
    "content": "#version 300 es\nprecision highp float;\n\nin vec2 v_uv;\nout vec4 fragColor;\n\nuniform float u_time;\nuniform vec2 u_resolution;\nuniform sampler2D u_sceneTexture;\nuniform float u_timeOfDay;\nuniform float u_coverage;\nuniform float u_density;\nuniform float u_softness;\nuniform float u_windSpeed;\nuniform float u_windAngle;\nuniform float u_turbulence;\nuniform float u_lightIntensity;\nuniform float u_ambientDarkness;\nuniform int u_numLayers;\nuniform float u_cloudScale;\nuniform vec2 u_celestialPos;\nuniform float u_celestialSize;\nuniform float u_celestialBrightness;\nuniform float u_backlightIntensity;\n\n#define PI 3.14159265359\n\nfloat hash(vec2 p) {\n  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nfloat noise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  f = f * f * (3.0 - 2.0 * f);\n  float a = hash(i);\n  float b = hash(i + vec2(1.0, 0.0));\n  float c = hash(i + vec2(0.0, 1.0));\n  float d = hash(i + vec2(1.0, 1.0));\n  return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);\n}\n\nfloat fbm(vec2 p, int octaves) {\n  float value = 0.0;\n  float amplitude = 0.5;\n  for (int i = 0; i < 8; i++) {\n    if (i >= octaves) break;\n    value += amplitude * noise(p);\n    p *= 2.0;\n    amplitude *= 0.5;\n  }\n  return value;\n}\n\nfloat cloudLayer(vec2 uv, float time, float layerSeed, float speed, float turbAmount) {\n  vec2 wind = vec2(cos(u_windAngle), sin(u_windAngle)) * speed * time;\n\n  // Each layer gets unique offset, scale, and rotation based on seed\n  float layerScale = (1.8 + hash(vec2(layerSeed, 0.0)) * 1.2) * u_cloudScale;\n  float layerRotation = hash(vec2(layerSeed, 1.0)) * 0.5 - 0.25;\n  vec2 layerOffset = vec2(\n    hash(vec2(layerSeed, 2.0)) * 100.0,\n    hash(vec2(layerSeed, 3.0)) * 100.0\n  );\n\n  // Apply rotation\n  float c = cos(layerRotation);\n  float s = sin(layerRotation);\n  vec2 rotatedUV = vec2(uv.x * c - uv.y * s, uv.x * s + uv.y * c);\n\n  vec2 p = rotatedUV * layerScale + wind + layerOffset;\n\n  // Turbulence with layer-specific offset\n  float turbSeed = layerSeed * 50.0;\n  vec2 turbOffset = vec2(\n    fbm(p * 0.5 + time * 0.1 + turbSeed, 4),\n    fbm(p * 0.5 + turbSeed + 100.0 + time * 0.1, 4)\n  ) * turbAmount;\n\n  float n = fbm(p + turbOffset, 6);\n  return n;\n}\n\nvec3 cloudLighting(float density, float heightInCloud, float sunAlt, float warmth, float nightFactor, vec2 uv) {\n  float daylight = smoothstep(-0.12, 0.1, sunAlt);\n\n  vec3 dayLitColor = vec3(1.0, 0.98, 0.96);\n  vec3 sunsetLitColor = vec3(1.0, 0.7, 0.45);\n  vec3 nightLitColor = vec3(0.12, 0.14, 0.2);\n  vec3 litColor = mix(dayLitColor, sunsetLitColor, warmth);\n  litColor = mix(litColor, nightLitColor, nightFactor);\n\n  vec3 dayShadowColor = vec3(0.45, 0.5, 0.6);\n  vec3 sunsetShadowColor = vec3(0.35, 0.25, 0.3);\n  vec3 nightShadowColor = vec3(0.03, 0.04, 0.07);\n  vec3 shadowColor = mix(dayShadowColor, sunsetShadowColor, warmth);\n  shadowColor = mix(shadowColor, nightShadowColor, nightFactor);\n  shadowColor *= (1.0 - u_ambientDarkness * 0.3);\n\n  float topLight = heightInCloud * max(0.0, sunAlt);\n  float sideLight = (1.0 - abs(heightInCloud - 0.5) * 2.0) * (1.0 - sunAlt * 0.5);\n  float bottomLight = (1.0 - heightInCloud) * warmth * 0.5;\n  float ambientLight = mix(0.03, 0.2, daylight);\n\n  float lightAmount = (topLight * 0.5 + sideLight * 0.3 + bottomLight) * daylight + ambientLight;\n  lightAmount = clamp(lightAmount * u_lightIntensity, 0.0, 1.0);\n\n  vec3 cloudColor = mix(shadowColor, litColor, lightAmount);\n\n  float rimLight = pow(density, 0.5) * (1.0 - density) * 4.0;\n  vec3 rimColor = mix(vec3(1.0, 1.0, 0.95), vec3(1.0, 0.8, 0.5), warmth);\n  rimColor = mix(rimColor, vec3(0.15, 0.18, 0.25), nightFactor);\n  float rimStrength = mix(0.1, 0.3, daylight);\n  cloudColor += rimColor * rimLight * rimStrength * u_lightIntensity;\n\n  float hotSpot = pow(max(0.0, lightAmount - 0.6) * 2.5, 2.0) * warmth * daylight;\n  cloudColor += vec3(1.0, 0.5, 0.2) * hotSpot * 0.4;\n\n  // Celestial body illumination - clouds near sun/moon get backlit\n  float aspect = u_resolution.x / u_resolution.y;\n  vec2 celestialUV = u_celestialPos;\n  vec2 diff = (uv - celestialUV) * vec2(aspect, 1.0);\n  float distToCelestial = length(diff);\n\n  // Light transmission - thin clouds scatter light, thick clouds block it\n  float transmission = pow(1.0 - density, 2.0); // quadratic falloff - dense clouds block more\n\n  // Backlight glow - extends beyond the celestial body, but blocked by dense clouds\n  float glowRadius = u_celestialSize * 6.0;\n  float proximityGlow = exp(-distToCelestial * distToCelestial / (glowRadius * glowRadius));\n  float backlight = proximityGlow * transmission * u_celestialBrightness;\n\n  // Silver lining - bright edges where thin cloud meets thick cloud near celestial\n  // Peaks at medium density (the transition zone), requires proximity to celestial\n  float edgeDist = u_celestialSize * 3.0;\n  float nearCelestial = smoothstep(edgeDist * 2.5, edgeDist * 0.3, distToCelestial);\n  float edgeFactor = density * (1.0 - density) * 4.0; // peaks at 0.5 density\n  float silverLining = nearCelestial * edgeFactor * u_celestialBrightness;\n\n  // Color based on day/night, scaled by backlight intensity control\n  vec3 backlightColor = mix(vec3(1.0, 0.9, 0.7), vec3(0.7, 0.75, 0.9), nightFactor);\n  cloudColor += backlightColor * (backlight * 0.5 + silverLining * 0.8) * u_backlightIntensity;\n\n  return cloudColor;\n}\n\nvoid main() {\n  vec2 uv = v_uv;\n  vec4 scene = texture(u_sceneTexture, uv);\n\n  float sunAlt = u_timeOfDay < 0.5 ? u_timeOfDay * 2.0 : 2.0 - u_timeOfDay * 2.0;\n  sunAlt = sunAlt * 2.0 - 1.0;\n\n  float warmth = 1.0 - smoothstep(0.0, 0.4, sunAlt);\n  warmth = warmth * warmth;\n  float nightFactor = 1.0 - smoothstep(-0.12, 0.02, sunAlt);\n  float daylight = smoothstep(-0.12, 0.1, sunAlt);\n\n  vec3 color = scene.rgb;\n  float accumulatedAlpha = 0.0;\n\n  for (int i = u_numLayers - 1; i >= 0; i--) {\n    float layerIdx = float(i);\n    float layerDepth = layerIdx / max(1.0, float(u_numLayers) - 1.0);\n\n    float layerSeed = layerIdx * 7.31 + 13.0;\n    float layerSpeed = u_windSpeed * (0.6 + hash(vec2(layerSeed, 10.0)) * 0.8);\n    float layerTurb = u_turbulence * (0.7 + hash(vec2(layerSeed, 11.0)) * 0.6);\n\n    float cloud = cloudLayer(uv, u_time, layerSeed, layerSpeed, layerTurb);\n\n    float threshold = 1.0 - u_coverage;\n    cloud = smoothstep(threshold, threshold + u_softness, cloud);\n\n    float heightInCloud = uv.y * 0.6 + cloud * 0.4;\n    vec3 cloudColor = cloudLighting(cloud, heightInCloud, sunAlt, warmth, nightFactor, uv);\n\n    vec3 hazeColor = mix(vec3(0.05, 0.06, 0.1), vec3(0.6, 0.7, 0.85), daylight);\n    float hazeAmount = layerDepth * layerDepth * 0.5;\n    cloudColor = mix(cloudColor, hazeColor, hazeAmount);\n\n    float contrastReduction = 1.0 - layerDepth * 0.3;\n    cloudColor = mix(vec3(0.5), cloudColor, contrastReduction);\n\n    float alpha = cloud * u_density * (0.6 + (1.0 - layerDepth) * 0.4);\n    color = mix(color, cloudColor, alpha * (1.0 - accumulatedAlpha));\n    accumulatedAlpha = accumulatedAlpha + alpha * (1.0 - accumulatedAlpha);\n  }\n\n  // Preserve cloud opacity as alpha so later passes can keep the mask intact\n  // (for god rays / atmospheric post-processing).\n  fragColor = vec4(color, accumulatedAlpha);\n}\n\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/shaders/composite.frag.glsl",
    "content": "#version 300 es\nprecision highp float;\n\nin vec2 v_uv;\nout vec4 fragColor;\n\nuniform sampler2D u_sceneTexture;\nuniform float u_time;\nuniform vec2 u_resolution;\nuniform float u_timeOfDay;\nuniform vec2 u_sunPos;\nuniform float u_sunVisible;\nuniform float u_lastFlashTime;\nuniform float u_strikeSeed;\nuniform float u_lightningSceneIllumination;\n\nuniform bool u_postEnabled;\n\n// Aerial perspective / haze\nuniform float u_haze;\nuniform float u_hazeHorizon;\nuniform float u_hazeDesaturation;\nuniform float u_hazeContrast;\n\n// Bloom / glare\nuniform float u_bloomIntensity;\nuniform float u_bloomThreshold;\nuniform float u_bloomKnee;\nuniform float u_bloomRadius;\nuniform float u_bloomTapScale;\n\n// Exposure response (lightning)\nuniform float u_exposureIntensity;\nuniform float u_exposureDesaturation;\nuniform float u_exposureRecovery;\n\n// Crepuscular rays\nuniform float u_godRayIntensity;\nuniform float u_godRayDecay;\nuniform float u_godRayDensity;\nuniform float u_godRayWeight;\nuniform int u_godRaySamples;\n\n#define PI 3.14159265359\n#define GODRAY_MAX_SAMPLES 32\n\nfloat saturate(float x) {\n  return clamp(x, 0.0, 1.0);\n}\n\nfloat luminance(vec3 c) {\n  return dot(c, vec3(0.299, 0.587, 0.114));\n}\n\n// -----------------------------------------------------------------------------\n// Lightning flash envelope (mirrors lightning pass timing)\n// -----------------------------------------------------------------------------\nfloat easeOutSine(float t) { return sin(t * PI * 0.5); }\nfloat easeInSine(float t) { return 1.0 - cos(t * PI * 0.5); }\nfloat easeInOutSine(float t) { return -(cos(PI * t) - 1.0) * 0.5; }\nfloat easeOutQuad(float t) { return 1.0 - (1.0 - t) * (1.0 - t); }\nfloat easeOutCubic(float t) { float inv = 1.0 - t; return 1.0 - inv * inv * inv; }\n\nfloat hash11(float p) {\n  p = fract(p * 0.1031);\n  p *= p + 33.33;\n  p *= p + p;\n  return fract(p);\n}\n\nfloat flashEnvelope(float timeSinceStrike, float duration) {\n  if (timeSinceStrike < 0.0 || timeSinceStrike > duration) return 0.0;\n  float t = timeSinceStrike / duration;\n  float attackT = clamp(t / 0.03, 0.0, 1.0);\n  float attack = easeOutCubic(attackT);\n  float sustainT = clamp((t - 0.05) / 0.65, 0.0, 1.0);\n  float sustain = 1.0 - easeInOutSine(sustainT);\n  float decay = exp(-t * 2.0);\n  decay = mix(decay, easeOutSine(1.0 - t), 0.3);\n  float endT = clamp((t - 0.75) / 0.25, 0.0, 1.0);\n  float endFade = 1.0 - easeInSine(endT);\n  return attack * max(sustain, decay * 0.4) * endFade;\n}\n\nfloat restrikeEnvelope(float timeSinceStrike, float duration, float seed) {\n  float env = flashEnvelope(timeSinceStrike, duration * 0.7);\n  if (hash11(seed * 7.7) > 0.7) {\n    float restrike1 = flashEnvelope(timeSinceStrike - duration * 0.5, duration * 0.3);\n    env = max(env, restrike1 * 0.6);\n  }\n  if (hash11(seed * 11.3) > 0.85) {\n    float restrike2 = flashEnvelope(timeSinceStrike - duration * 0.75, duration * 0.2);\n    env = max(env, restrike2 * 0.4);\n  }\n  return env;\n}\n\n// -----------------------------------------------------------------------------\n// Post-process building blocks\n// -----------------------------------------------------------------------------\nfloat getSunAltitudeFromTimeOfDay(float timeOfDay) {\n  // 0..1 timeOfDay -> -1..1 altitude (mirrors cloud/celestial assumptions)\n  float sunAlt = timeOfDay < 0.5 ? timeOfDay * 2.0 : 2.0 - timeOfDay * 2.0;\n  return sunAlt * 2.0 - 1.0;\n}\n\nvec3 applyHaze(vec3 color, vec2 uv) {\n  float haze = saturate(u_haze);\n  if (haze <= 0.0001) return color;\n\n  float horizon = pow(1.0 - uv.y, 1.8);\n  float hazeWeight = haze * mix(1.0, horizon, saturate(u_hazeHorizon));\n\n  // Slight horizon lift (mix towards a sky-tinted haze color).\n  float sunAlt = getSunAltitudeFromTimeOfDay(u_timeOfDay);\n  float daylight = smoothstep(-0.12, 0.1, sunAlt);\n  vec3 hazeDay = vec3(0.60, 0.70, 0.85);\n  vec3 hazeNight = vec3(0.06, 0.07, 0.10);\n  vec3 hazeColor = mix(hazeNight, hazeDay, daylight);\n  color = mix(color, hazeColor, hazeWeight * 0.55);\n\n  // Contrast compression (towards mid gray).\n  float contrast = saturate(1.0 - hazeWeight * saturate(u_hazeContrast));\n  color = mix(vec3(0.5), color, contrast);\n\n  // Slight desaturation.\n  float gray = luminance(color);\n  float sat = saturate(1.0 - hazeWeight * saturate(u_hazeDesaturation));\n  color = mix(vec3(gray), color, sat);\n\n  return color;\n}\n\nvec3 bloomTap(vec2 uv) {\n  vec3 c = texture(u_sceneTexture, clamp(uv, 0.0, 1.0)).rgb;\n  float l = luminance(c);\n  float knee = max(0.0001, u_bloomKnee);\n  float m = smoothstep(u_bloomThreshold, u_bloomThreshold + knee, l);\n  return c * m;\n}\n\nvec3 computeBloom(vec2 uv) {\n  float intensity = u_bloomIntensity;\n  if (intensity <= 0.0001) return vec3(0.0);\n\n  vec2 texel = 1.0 / max(u_resolution, vec2(1.0));\n  // Interpret bloomRadius as a scene-relative scalar, not literal pixels.\n  // This keeps the control meaningful across widget sizes and DPR.\n  float radiusPx = max(0.0, u_bloomRadius) * (u_resolution.y * 0.02);\n  radiusPx *= max(0.25, u_bloomTapScale);\n  vec2 d = texel * radiusPx;\n\n  // 9-tap blur (center + 4 cardinal + 4 diagonal). Weights sum to 1.\n  vec3 sum = vec3(0.0);\n  sum += bloomTap(uv) * 0.20;\n  sum += bloomTap(uv + vec2( d.x, 0.0)) * 0.12;\n  sum += bloomTap(uv + vec2(-d.x, 0.0)) * 0.12;\n  sum += bloomTap(uv + vec2(0.0,  d.y)) * 0.12;\n  sum += bloomTap(uv + vec2(0.0, -d.y)) * 0.12;\n  sum += bloomTap(uv + vec2( d.x,  d.y)) * 0.08;\n  sum += bloomTap(uv + vec2(-d.x,  d.y)) * 0.08;\n  sum += bloomTap(uv + vec2( d.x, -d.y)) * 0.08;\n  sum += bloomTap(uv + vec2(-d.x, -d.y)) * 0.08;\n\n  return sum * intensity;\n}\n\nvec3 applyExposureResponse(vec3 color, float flashStrength) {\n  if (flashStrength <= 0.0001) return color;\n\n  // Treat flashStrength as already “intensity-scaled” (so the UI sliders are\n  // predictable). Use a smooth, LDR-friendly curve that still brightens values\n  // below 1.0 (Reinhard can cancel out gains for sub-1 values).\n  float t = saturate(flashStrength);\n  float gain = 1.0 + flashStrength * 2.2;\n  vec3 lifted = color * gain;\n  vec3 tonemapped = 1.0 - exp(-lifted);\n\n  vec3 outColor = mix(color, tonemapped, t);\n\n  // Subtle desaturation at peak flash.\n  float gray = luminance(outColor);\n  float desat = saturate(u_exposureDesaturation) * t;\n  outColor = mix(outColor, vec3(gray), desat);\n\n  // A tiny “white crush” at peak helps it read as sensor saturation.\n  outColor = mix(outColor, vec3(1.0), t * 0.06);\n\n  return outColor;\n}\n\nvec3 computeGodRays(vec2 uv) {\n  if (u_godRayIntensity <= 0.0001) return vec3(0.0);\n  if (u_godRaySamples <= 0) return vec3(0.0);\n  if (u_sunVisible <= 0.001) return vec3(0.0);\n\n  float sunAlt = getSunAltitudeFromTimeOfDay(u_timeOfDay);\n  float daylight = smoothstep(-0.12, 0.1, sunAlt);\n  // Crepuscular rays are most believable when the sun is low. This gating also\n  // prevents noon blowout even if the user cranks u_godRayIntensity.\n  float lowSun = 1.0 - smoothstep(0.25, 0.75, max(0.0, sunAlt));\n\n  // Clamp sun position to sampling domain; if it's far off-screen, rays look odd.\n  vec2 sunUV = clamp(u_sunPos, vec2(-0.25), vec2(1.25));\n  vec2 delta = (uv - sunUV) * (u_godRayDensity / float(u_godRaySamples));\n\n  vec2 coord = uv;\n  float illuminationDecay = 1.0;\n  float accum = 0.0;\n\n  // Hard cap for WebGL loop constraints.\n  for (int i = 0; i < GODRAY_MAX_SAMPLES; i++) {\n    if (i >= u_godRaySamples) break;\n    coord -= delta;\n    vec4 s = texture(u_sceneTexture, clamp(coord, 0.0, 1.0));\n\n    // Alpha encodes cloud opacity (0 = clear, 1 = fully occluded).\n    float transmittance = 1.0 - saturate(s.a);\n    float sampleLum = luminance(s.rgb);\n\n    // Require some brightness along the path so rays read like “sunlight leaking”.\n    // Use a high threshold so we don’t integrate the entire bright sky, which\n    // can quickly wash the scene to white at midday.\n    float brightMask = saturate((sampleLum - 0.85) / 0.15);\n    brightMask *= brightMask;\n    float raySample = transmittance * brightMask;\n\n    accum += raySample * illuminationDecay * u_godRayWeight;\n    illuminationDecay *= u_godRayDecay;\n  }\n\n  // Color: warm daylight shafts.\n  vec3 rayColor = mix(vec3(0.7, 0.72, 0.8), vec3(1.0, 0.92, 0.75), daylight);\n\n  float intensity = u_godRayIntensity * saturate(u_sunVisible) * daylight * lowSun;\n  return rayColor * accum * intensity;\n}\n\nvoid main() {\n  vec4 scene = texture(u_sceneTexture, v_uv);\n  vec3 color = scene.rgb;\n\n  if (!u_postEnabled) {\n    fragColor = vec4(color, 1.0);\n    return;\n  }\n\n  // Rays first so they can be bloomed/overexposed later.\n  color += computeGodRays(v_uv);\n\n  // Bloom/glare (forward scatter).\n  color += computeBloom(v_uv);\n\n  // Lightning-driven exposure response (global camera feel).\n  float flashStrength = 0.0;\n  if (u_exposureIntensity > 0.0001) {\n    float timeSinceStrike = u_time - u_lastFlashTime;\n    float durationSec = 0.8;\n\n    float f = restrikeEnvelope(timeSinceStrike, durationSec, u_strikeSeed);\n    float afterimageDuration = durationSec * 1.5;\n    // Guard against 0 recovery which would “stick” the afterimage.\n    float afterT = clamp((timeSinceStrike * max(0.05, u_exposureRecovery)) / afterimageDuration, 0.0, 1.0);\n    float afterimage = timeSinceStrike < 0.0 ? 0.0 : (1.0 - easeInSine(afterT));\n\n    // Legacy “scene illumination” (kept separate from exposure so tuning the\n    // camera response doesn’t get unintentionally suppressed).\n    float sceneFlash = f * max(0.0, u_lightningSceneIllumination);\n    color += vec3(0.3, 0.32, 0.4) * sceneFlash;\n\n    // Exposure response: controlled directly by u_exposureIntensity.\n    // Keep the tail subtle; flash does the heavy lifting.\n    flashStrength = f * u_exposureIntensity;\n    flashStrength = max(flashStrength, afterimage * u_exposureIntensity * 0.12);\n    color = applyExposureResponse(color, flashStrength);\n  }\n\n  // Aerial perspective / haze as the final “air” grade.\n  color = applyHaze(color, v_uv);\n\n  fragColor = vec4(clamp(color, 0.0, 1.0), 1.0);\n}\n\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/shaders/fullscreen.vert.glsl",
    "content": "#version 300 es\nin vec4 a_position;\nout vec2 v_uv;\n\nvoid main() {\n  gl_Position = a_position;\n  v_uv = a_position.xy * 0.5 + 0.5;\n}\n\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/shaders/lightning.frag.glsl",
    "content": "#version 300 es\nprecision highp float;\n\nin vec2 v_uv;\nout vec4 fragColor;\n\nuniform float u_time;\nuniform vec2 u_resolution;\nuniform sampler2D u_sceneTexture;\nuniform bool u_enabled;\nuniform float u_flashIntensity;\nuniform float u_branchDensity;\nuniform float u_sceneIllumination;\nuniform float u_lastFlashTime;\nuniform float u_strikeSeed;\n\n#define MAX_SEGMENTS 32\n#define MAX_BRANCHES 16\n#define PI 3.14159265359\n\nfloat easeOutSine(float t) { return sin(t * PI * 0.5); }\nfloat easeInSine(float t) { return 1.0 - cos(t * PI * 0.5); }\nfloat easeInOutSine(float t) { return -(cos(PI * t) - 1.0) * 0.5; }\nfloat easeOutQuad(float t) { return 1.0 - (1.0 - t) * (1.0 - t); }\nfloat easeOutCubic(float t) { float inv = 1.0 - t; return 1.0 - inv * inv * inv; }\n\nfloat hash11(float p) {\n  p = fract(p * 0.1031);\n  p *= p + 33.33;\n  p *= p + p;\n  return fract(p);\n}\n\nfloat hash12(vec2 p) {\n  vec3 p3 = fract(vec3(p.xyx) * 0.1031);\n  p3 += dot(p3, p3.yzx + 33.33);\n  return fract((p3.x + p3.y) * p3.z);\n}\n\nvec2 hash22(vec2 p) {\n  vec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973));\n  p3 += dot(p3, p3.yzx + 33.33);\n  return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfloat noise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  f = f * f * (3.0 - 2.0 * f);\n  float a = hash12(i);\n  float b = hash12(i + vec2(1.0, 0.0));\n  float c = hash12(i + vec2(0.0, 1.0));\n  float d = hash12(i + vec2(1.0, 1.0));\n  return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);\n}\n\nfloat distToSegment(vec2 p, vec2 a, vec2 b) {\n  vec2 pa = p - a;\n  vec2 ba = b - a;\n  float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);\n  return length(pa - ba * h);\n}\n\nvec2 displacedPoint(vec2 start, vec2 end, float t, float seed, float displacementAmt) {\n  vec2 basePoint = mix(start, end, t);\n  vec2 dir = end - start;\n  vec2 perp = normalize(vec2(-dir.y, dir.x));\n  float envelope = sin(t * PI);\n  float n1 = noise(vec2(t * 8.0, seed * 100.0)) * 2.0 - 1.0;\n  float n2 = noise(vec2(t * 16.0, seed * 100.0 + 50.0)) * 2.0 - 1.0;\n  float n3 = noise(vec2(t * 32.0, seed * 100.0 + 100.0)) * 2.0 - 1.0;\n  float displacement = (n1 * 0.6 + n2 * 0.3 + n3 * 0.1) * envelope * displacementAmt;\n  float targetBias = 1.0 - t * 0.3;\n  displacement *= targetBias;\n  return basePoint + perp * displacement * length(dir);\n}\n\nfloat mainBoltDistance(vec2 uv, vec2 start, vec2 end, float seed, float displacementAmt) {\n  float minDist = 999.0;\n  vec2 prevPoint = start;\n  for (int i = 1; i <= MAX_SEGMENTS; i++) {\n    float t = float(i) / float(MAX_SEGMENTS);\n    vec2 currPoint = displacedPoint(start, end, t, seed, displacementAmt);\n    float d = distToSegment(uv, prevPoint, currPoint);\n    minDist = min(minDist, d);\n    prevPoint = currPoint;\n  }\n  return minDist;\n}\n\nfloat branchDistance(vec2 uv, vec2 branchStart, vec2 branchDir, float branchLen, float seed, float displacementAmt) {\n  vec2 branchEnd = branchStart + branchDir * branchLen;\n  float minDist = 999.0;\n  vec2 prevPoint = branchStart;\n  for (int i = 1; i <= 12; i++) {\n    float t = float(i) / 12.0;\n    vec2 currPoint = displacedPoint(branchStart, branchEnd, t, seed, displacementAmt * 0.7);\n    float d = distToSegment(uv, prevPoint, currPoint);\n    minDist = min(minDist, d);\n    prevPoint = currPoint;\n  }\n  return minDist;\n}\n\nvec2 branchesDistance(vec2 uv, vec2 start, vec2 end, float seed, float displacementAmt, float density) {\n  float minDist = 999.0;\n  float brightness = 0.0;\n  vec2 mainDir = normalize(end - start);\n  float mainLen = length(end - start);\n\n  for (int i = 0; i < MAX_BRANCHES; i++) {\n    float idx = float(i);\n    float branchT = 0.15 + hash11(seed + idx * 7.31) * 0.7;\n    float branchProb = (1.0 - branchT) * density;\n    if (hash11(seed + idx * 3.17) > branchProb) continue;\n\n    vec2 branchStart = displacedPoint(start, end, branchT, seed, displacementAmt);\n    float angleOffset = (hash11(seed + idx * 11.13) * 2.0 - 1.0) * 0.6;\n    float side = hash11(seed + idx * 5.71) > 0.5 ? 1.0 : -1.0;\n    float angle = atan(mainDir.y, mainDir.x) + side * (0.3 + abs(angleOffset) * 0.5);\n    vec2 branchDir = vec2(cos(angle), sin(angle));\n    float branchLen = mainLen * (0.15 + hash11(seed + idx * 13.37) * 0.25);\n\n    float d = branchDistance(uv, branchStart, branchDir, branchLen, seed + idx * 100.0, displacementAmt);\n    if (d < minDist) {\n      minDist = d;\n      brightness = 0.5 - branchT * 0.2;\n    }\n\n    if (density > 0.3 && hash11(seed + idx * 17.19) < density * 0.5) {\n      float subT = 0.3 + hash11(seed + idx * 19.23) * 0.4;\n      vec2 subStart = branchStart + branchDir * branchLen * subT;\n      float subAngle = angle + (hash11(seed + idx * 23.29) * 2.0 - 1.0) * 0.5;\n      vec2 subDir = vec2(cos(subAngle), sin(subAngle));\n      float subLen = branchLen * 0.4;\n      float subD = branchDistance(uv, subStart, subDir, subLen, seed + idx * 200.0, displacementAmt * 0.5);\n      if (subD < minDist) {\n        minDist = subD;\n        brightness = 0.25;\n      }\n    }\n  }\n  return vec2(minDist, brightness);\n}\n\nvec3 lightningGlow(float dist, float brightness, float intensity, float thickness) {\n  float scaledDist = dist / max(thickness, 0.1);\n  float core = smoothstep(0.003, 0.0, scaledDist) * brightness;\n  float innerGlow = exp(-scaledDist * 150.0) * brightness;\n  float outerGlow = exp(-dist * dist * 3000.0) * brightness * thickness;\n\n  vec3 coreColor = vec3(1.0, 1.0, 1.0);\n  vec3 innerColor = vec3(0.7, 0.8, 1.0);\n  vec3 outerColor = vec3(0.5, 0.5, 0.9);\n\n  vec3 color = coreColor * core * 2.0;\n  color += innerColor * innerGlow * 0.8;\n  color += outerColor * outerGlow * 0.5;\n  return color * intensity;\n}\n\nfloat flashEnvelope(float timeSinceStrike, float duration) {\n  if (timeSinceStrike < 0.0 || timeSinceStrike > duration) return 0.0;\n  float t = timeSinceStrike / duration;\n  float attackT = clamp(t / 0.03, 0.0, 1.0);\n  float attack = easeOutCubic(attackT);\n  float sustainT = clamp((t - 0.05) / 0.65, 0.0, 1.0);\n  float sustain = 1.0 - easeInOutSine(sustainT);\n  float decay = exp(-t * 2.0);\n  decay = mix(decay, easeOutSine(1.0 - t), 0.3);\n  float endT = clamp((t - 0.75) / 0.25, 0.0, 1.0);\n  float endFade = 1.0 - easeInSine(endT);\n  return attack * max(sustain, decay * 0.4) * endFade;\n}\n\nfloat restrikeEnvelope(float timeSinceStrike, float duration, float seed) {\n  float env = flashEnvelope(timeSinceStrike, duration * 0.7);\n  if (hash11(seed * 7.7) > 0.7) {\n    float restrike1 = flashEnvelope(timeSinceStrike - duration * 0.5, duration * 0.3);\n    env = max(env, restrike1 * 0.6);\n  }\n  if (hash11(seed * 11.3) > 0.85) {\n    float restrike2 = flashEnvelope(timeSinceStrike - duration * 0.75, duration * 0.2);\n    env = max(env, restrike2 * 0.4);\n  }\n  return env;\n}\n\nvoid main() {\n  vec4 scene = texture(u_sceneTexture, v_uv);\n\n  if (!u_enabled) {\n    fragColor = scene;\n    return;\n  }\n\n  vec2 uv = v_uv;\n  float aspect = u_resolution.x / u_resolution.y;\n  uv.x *= aspect;\n\n  float timeSinceStrike = u_time - u_lastFlashTime;\n  float durationSec = 0.8;\n\n  float flash = restrikeEnvelope(timeSinceStrike, durationSec, u_strikeSeed);\n  float afterimageDuration = durationSec * 1.5;\n  float afterimageT = clamp(timeSinceStrike / afterimageDuration, 0.0, 1.0);\n  float afterimage = timeSinceStrike < 0.0 ? 0.0 : (1.0 - easeInSine(afterimageT));\n\n  vec3 color = scene.rgb;\n\n  if (flash > 0.01 || afterimage > 0.01) {\n    vec2 strikeHash = hash22(vec2(u_strikeSeed * 123.456, u_strikeSeed * 789.012));\n    vec2 boltStart = vec2((0.3 + strikeHash.x * 0.4) * aspect, 1.05);\n    vec2 boltEnd = vec2(boltStart.x + (strikeHash.x - 0.5) * 0.4, -0.05);\n\n    float straightDist = distToSegment(uv, boltStart, boltEnd);\n\n    // Always apply the broad source glow (cheap). If this is inside the early-out\n    // region it will get hard-clipped and look like a visible “container” around\n    // the bolt.\n    float sourceGlow = exp(-length(uv - boltStart) * 3.0);\n    color += vec3(0.4, 0.45, 0.6) * sourceGlow * afterimage * 0.3;\n\n    // Cheap early-out: most pixels are far from the bolt path.\n    // This avoids running the expensive segment/branch distance loops when the\n    // contribution would be effectively zero.\n    float distLimit = 0.18 + u_branchDensity * 0.25 + u_flashIntensity * 0.05;\n    float feather = 0.08;\n    float region = 1.0 - smoothstep(distLimit - feather, distLimit, straightDist);\n    if (region <= 0.0005) {\n      fragColor = vec4(color, scene.a);\n      return;\n    }\n\n    float displacementAmt = 0.15;\n    float mainDist = mainBoltDistance(uv, boltStart, boltEnd, u_strikeSeed, displacementAmt);\n    vec2 branchResult = branchesDistance(uv, boltStart, boltEnd, u_strikeSeed, displacementAmt, u_branchDensity);\n    float branchDist = branchResult.x;\n    float branchBrightness = branchResult.y;\n\n    float mainThickness = mix(0.2, 1.0, easeOutSine(sqrt(max(flash, 0.0))));\n    vec3 afterglowColor = vec3(0.5, 0.45, 0.7);\n\n    vec3 mainCore = lightningGlow(mainDist, easeOutQuad(max(flash, 0.0)), u_flashIntensity, mainThickness);\n    float mainAfterglowDist = mainDist * 0.6;\n    float mainAfterglowStrength = exp(-mainAfterglowDist * 50.0) * afterimage * 0.5;\n    vec3 mainAfterglow = afterglowColor * mainAfterglowStrength;\n\n    float branchThickness = mix(0.15, 1.0, easeOutSine(max(flash, 0.0)));\n    vec3 branchCore = lightningGlow(branchDist, branchBrightness * easeOutQuad(max(flash, 0.0)), u_flashIntensity, branchThickness);\n    float branchAfterglowDist = branchDist * 0.7;\n    float branchAfterglowStrength = exp(-branchAfterglowDist * 80.0) * branchBrightness * afterimage * 0.4;\n    vec3 branchAfterglow = afterglowColor * branchAfterglowStrength;\n\n    color += (mainCore + branchCore) * max(flash, 0.0) * region;\n    color += (mainAfterglow + branchAfterglow) * afterimage * region;\n  }\n\n  fragColor = vec4(color, scene.a);\n}\n\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/shaders/rain.frag.glsl",
    "content": "#version 300 es\nprecision highp float;\n\nin vec2 v_uv;\nout vec4 fragColor;\n\nuniform float u_time;\nuniform vec2 u_resolution;\nuniform sampler2D u_sceneTexture;\nuniform float u_glassIntensity;\nuniform float u_glassZoom;\nuniform float u_fallingIntensity;\nuniform float u_fallingSpeed;\nuniform float u_fallingAngle;\nuniform float u_fallingStreakLength;\nuniform int u_fallingLayers;\nuniform float u_refractionStrength;\n\n#define S(a, b, t) smoothstep(a, b, t)\n\nvec3 N13(float p) {\n  vec3 p3 = fract(vec3(p) * vec3(0.1031, 0.11369, 0.13787));\n  p3 += dot(p3, p3.yzx + 19.19);\n  return fract(vec3((p3.x + p3.y) * p3.z, (p3.x + p3.z) * p3.y, (p3.y + p3.z) * p3.x));\n}\n\nfloat N(float t) {\n  return fract(sin(t * 12345.564) * 7658.76);\n}\n\nfloat Saw(float b, float t) {\n  return S(0.0, b, t) * S(1.0, b, t);\n}\n\nvec2 DropLayer(vec2 uv, float t) {\n  vec2 UV = uv;\n  uv.y += t * 0.75;\n  vec2 aspect = vec2(6.0, 1.0);\n  vec2 grid = aspect * 2.0;\n  vec2 id = floor(uv * grid);\n  float colShift = N(id.x);\n  uv.y += colShift;\n  id = floor(uv * grid);\n  vec3 n = N13(id.x * 35.2 + id.y * 2376.1);\n  vec2 st = fract(uv * grid) - vec2(0.5, 0.0);\n  float x = n.x - 0.5;\n  float y = UV.y * 20.0;\n  float wiggle = sin(y + sin(y));\n  x += wiggle * (0.5 - abs(x)) * (n.z - 0.5);\n  x *= 0.7;\n  float ti = fract(t + n.z);\n  y = (Saw(0.85, ti) - 0.5) * 0.9 + 0.5;\n  vec2 p = vec2(x, y);\n  float d = length((st - p) * aspect.yx);\n  float mainDrop = S(0.4, 0.0, d);\n  float r = sqrt(S(1.0, y, st.y));\n  float cd = abs(st.x - x);\n  float trail = S(0.23 * r, 0.15 * r * r, cd);\n  float trailFront = S(-0.02, 0.02, st.y - y);\n  trail *= trailFront * r * r;\n  float y2 = fract(UV.y * 10.0) + (st.y - 0.5);\n  float dd = length(st - vec2(x, y2));\n  float droplets = S(0.3, 0.0, dd);\n  float m = mainDrop + droplets * r * trailFront;\n  return vec2(m, trail);\n}\n\nfloat StaticDrops(vec2 uv, float t) {\n  uv *= 40.0;\n  vec2 id = floor(uv);\n  uv = fract(uv) - 0.5;\n  vec3 n = N13(id.x * 107.45 + id.y * 3543.654);\n  vec2 p = (n.xy - 0.5) * 0.7;\n  float d = length(uv - p);\n  float fade = Saw(0.025, fract(t + n.z));\n  float c = S(0.3, 0.0, d) * fract(n.z * 10.0) * fade;\n  return c;\n}\n\nvec2 Drops(vec2 uv, float t, float l0, float l1, float l2) {\n  float s = StaticDrops(uv, t) * l0;\n  vec2 m1 = DropLayer(uv, t) * l1;\n  vec2 m2 = DropLayer(uv * 1.85, t) * l2;\n  float c = s + m1.x + m2.x;\n  c = S(0.3, 1.0, c);\n  return vec2(c, max(m1.y * l0, m2.y * l1));\n}\n\nfloat hash12(vec2 p) {\n  vec3 p3 = fract(vec3(p.xyx) * 0.1031);\n  p3 += dot(p3, p3.yzx + 33.33);\n  return fract((p3.x + p3.y) * p3.z);\n}\n\nvec2 FallingRainLayer(vec2 uv, float t, float speed, float angle, float streakLen, float scale, float density) {\n  vec2 offset = vec2(0.0);\n  vec2 p = uv;\n  p.x += p.y * angle;\n  p *= scale;\n  p.y += t * speed;\n  vec2 id = floor(p);\n  vec2 gv = fract(p) - 0.5;\n\n  for (int y = -1; y <= 1; y++) {\n    for (int x = -1; x <= 1; x++) {\n      vec2 offs = vec2(float(x), float(y));\n      vec2 cellId = id + offs;\n      float n1 = hash12(cellId);\n      if (n1 > density) continue;\n      vec2 n2 = vec2(hash12(cellId * 17.23), hash12(cellId * 31.17));\n      vec2 dropPos = offs + n2 - 0.5;\n      vec2 localUV = gv - dropPos;\n      float streakW = 0.025 + n1 * 0.02;\n      float streakH = streakLen * (0.4 + hash12(cellId * 7.13) * 0.6);\n      float t_pos = (localUV.y + streakH) / (2.0 * streakH);\n      t_pos = clamp(t_pos, 0.0, 1.0);\n      if (abs(localUV.y) > streakH * 1.2) continue;\n      float taper = mix(1.3, 0.4, t_pos * t_pos);\n      float width = streakW * taper;\n      float core = S(width, width * 0.2, abs(localUV.x));\n      float vertFade = S(0.0, 0.1, t_pos) * S(1.0, 0.85, t_pos);\n      float streak = core * vertFade;\n      if (streak > 0.001) {\n        offset.x += localUV.x * streak * 0.5;\n        offset.y += (n1 - 0.5) * streak * 0.1;\n      }\n    }\n  }\n  return offset;\n}\n\nvec2 FallingRain(vec2 uv, float t) {\n  vec2 totalOffset = vec2(0.0);\n  if (u_fallingIntensity < 0.01) return totalOffset;\n\n  float speed = u_fallingSpeed * 5.0;\n  float streakLen = u_fallingStreakLength * 0.3;\n\n  for (int i = 0; i < 6; i++) {\n    if (i >= u_fallingLayers) break;\n    float layerIdx = float(i);\n    float depth = layerIdx / float(max(u_fallingLayers - 1, 1));\n    float layerScale = mix(6.0, 30.0, depth);\n    float layerSpeed = speed * mix(2.0, 0.5, depth);\n    float layerDensity = u_fallingIntensity * mix(0.8, 0.3, depth);\n    float layerStrength = mix(1.0, 0.15, depth);\n    float layerStreakLen = streakLen * mix(1.5, 0.4, depth);\n    float layerAngle = u_fallingAngle * mix(1.0, 0.6, depth);\n    vec2 layerOffset = vec2(sin(layerIdx * 73.156) * 3.0, cos(layerIdx * 37.842) * 3.0);\n    vec2 layer = FallingRainLayer(uv + layerOffset, t + layerIdx * 0.13, layerSpeed, layerAngle, layerStreakLen, layerScale, layerDensity);\n    totalOffset += layer * layerStrength;\n  }\n  return totalOffset * 0.4;\n}\n\nvoid main() {\n  vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / u_resolution.y;\n  vec2 UV = v_uv;\n\n  uv *= u_glassZoom;\n  float t = u_time * 0.2;\n\n  float rainAmount = u_glassIntensity;\n  float staticDrops = S(-0.5, 1.0, rainAmount) * 2.0;\n  float layer1 = S(0.25, 0.75, rainAmount);\n  float layer2 = S(0.0, 0.5, rainAmount);\n\n  vec2 c = Drops(uv, t, staticDrops, layer1, layer2);\n\n  vec2 e = vec2(0.001, 0.0);\n  float cx = Drops(uv + e, t, staticDrops, layer1, layer2).x;\n  float cy = Drops(uv + e.yx, t, staticDrops, layer1, layer2).x;\n  vec2 glassNormal = vec2(cx - c.x, cy - c.x);\n\n  vec2 fallingRainOffset = FallingRain(uv, u_time);\n\n  vec2 totalRefraction = (glassNormal + fallingRainOffset) * u_refractionStrength;\n\n  vec2 refractedUV = UV + totalRefraction;\n  refractedUV = clamp(refractedUV, 0.0, 1.0);\n\n  vec4 scene = texture(u_sceneTexture, refractedUV);\n  vec3 color = scene.rgb;\n\n  // Subtle specular on rain\n  float rainMagnitude = length(fallingRainOffset);\n  if (rainMagnitude > 0.001) {\n    float brightness = dot(scene.rgb, vec3(0.299, 0.587, 0.114));\n    float specular = rainMagnitude * 15.0 * (0.1 + brightness * 0.9);\n    color += vec3(0.8, 0.85, 0.95) * specular * 0.3;\n  }\n\n  color += vec3(0.1, 0.12, 0.15) * c.x * 0.5;\n\n  fragColor = vec4(color, scene.a);\n}\n\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/shaders/snow.frag.glsl",
    "content": "#version 300 es\nprecision highp float;\n\nin vec2 v_uv;\nout vec4 fragColor;\n\nuniform float u_time;\nuniform vec2 u_resolution;\nuniform sampler2D u_sceneTexture;\nuniform float u_intensity;\nuniform int u_layers;\nuniform float u_fallSpeed;\nuniform float u_windSpeed;\nuniform float u_windAngle;\nuniform float u_turbulence;\nuniform float u_drift;\nuniform float u_flutter;\nuniform float u_windShear;\nuniform float u_flakeSize;\nuniform float u_sizeVariation;\nuniform float u_opacity;\nuniform float u_glowAmount;\nuniform float u_sparkle;\n\n#define PI 3.14159265359\n#define MAX_LAYERS 6\n\nfloat hash12(vec2 p) {\n  vec3 p3 = fract(vec3(p.xyx) * 0.1031);\n  p3 += dot(p3, p3.yzx + 33.33);\n  return fract((p3.x + p3.y) * p3.z);\n}\n\nvec2 hash22(vec2 p) {\n  vec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973));\n  p3 += dot(p3, p3.yzx + 33.33);\n  return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfloat noise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  f = f * f * (3.0 - 2.0 * f);\n  float a = hash12(i);\n  float b = hash12(i + vec2(1.0, 0.0));\n  float c = hash12(i + vec2(0.0, 1.0));\n  float d = hash12(i + vec2(1.0, 1.0));\n  return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);\n}\n\nvec2 rotate2D(vec2 p, float angle) {\n  float c = cos(angle);\n  float s = sin(angle);\n  return vec2(p.x * c - p.y * s, p.x * s + p.y * c);\n}\n\nfloat snowflakeShape(vec2 uv, float size, float seed, float rotation) {\n  vec2 rotatedUV = rotate2D(uv, rotation);\n  float dist = length(rotatedUV);\n  float circle = smoothstep(size, size * 0.3, dist);\n  float angle = atan(rotatedUV.y, rotatedUV.x);\n  float hexPattern = 0.5 + 0.5 * cos(angle * 6.0);\n  hexPattern = pow(hexPattern, 2.0);\n  float crystalAmount = smoothstep(0.02, 0.05, size) * 0.3;\n  float shape = mix(circle, circle * (0.7 + hexPattern * 0.3), crystalAmount);\n  float glow = exp(-dist * dist / (size * size * 3.0)) * u_glowAmount;\n  return shape + glow * 0.4;\n}\n\nvec2 getWind(float layerDepth) {\n  // Important: keep wind independent of uv to avoid warping the entire field.\n  // Per-flake turbulence/drift is applied later (to flakePos) so the result\n  // reads like particles, not like a screen-space displacement/refraction map.\n  vec2 baseWind = vec2(cos(u_windAngle), 0.0) * u_windSpeed;\n  float windResponse = mix(0.3, 1.0, 1.0 - layerDepth);\n\n  // Model \"wind shear\" as stronger motion in foreground layers rather than a\n  // screen-space gradient (which can make the whole effect look bent).\n  float shearResponse = 1.0 + u_windShear * (1.0 - layerDepth) * 0.35;\n\n  return baseWind * windResponse * shearResponse;\n}\n\nfloat sparkle(vec2 cellId, float time, float seed) {\n  float sparklePhase = hash12(cellId + vec2(seed * 100.0, 0.0)) * 100.0;\n  float sparkleFreq = 2.0 + hash12(cellId + vec2(0.0, seed * 100.0)) * 3.0;\n  float sparkleWave = sin(time * sparkleFreq + sparklePhase);\n  float sparkleIntensity = pow(max(0.0, sparkleWave), 16.0);\n  float sparkleProbability = hash12(cellId + vec2(floor(time * 0.5), 0.0));\n  sparkleIntensity *= step(0.85, sparkleProbability);\n  return sparkleIntensity * u_sparkle;\n}\n\nvec3 snowLayer(vec2 uv, float time, float layerIndex, float totalLayers) {\n  float depth = layerIndex / max(1.0, totalLayers - 1.0);\n  float layerScale = mix(8.0, 40.0, depth);\n  float layerSpeed = u_fallSpeed * mix(1.2, 0.4, depth);\n  float layerDensity = u_intensity * mix(1.0, 0.5, depth);\n  float layerFlakeSize = u_flakeSize * mix(1.5, 0.3, depth);\n  float layerOpacity = u_opacity * mix(1.0, 0.4, depth);\n\n  vec2 layerOffset = vec2(\n    sin(layerIndex * 73.156) * 10.0,\n    cos(layerIndex * 37.842) * 10.0\n  );\n\n  vec2 p = (uv + layerOffset) * layerScale;\n  p.y += time * layerSpeed * 2.0;\n\n  vec2 baseWind = getWind(depth);\n  p.x += time * baseWind.x * 0.3;\n\n  vec2 id = floor(p);\n  vec2 gv = fract(p) - 0.5;\n\n  float snow = 0.0;\n  float sparkleAccum = 0.0;\n\n  for (int y = -1; y <= 1; y++) {\n    for (int x = -1; x <= 1; x++) {\n      vec2 offs = vec2(float(x), float(y));\n      vec2 cellId = id + offs;\n\n      float h1 = hash12(cellId);\n      vec2 h2 = hash22(cellId);\n      float h3 = hash12(cellId + vec2(127.0, 311.0));\n      float h4 = hash12(cellId + vec2(271.0, 183.0));\n\n      if (h1 > layerDensity) continue;\n\n      float sizeVar = 1.0 + (h3 - 0.5) * u_sizeVariation;\n      float size = layerFlakeSize * sizeVar * 0.04;\n\n      vec2 flakePos = h2 * 0.8 - 0.4;\n\n      float flutterPhase = h3 * PI * 2.0;\n      float flutterAmp = u_flutter * 0.15 * (1.0 - depth);\n      flakePos.x += sin(time * 3.0 + flutterPhase) * flutterAmp;\n      flakePos.y += cos(time * 2.5 + flutterPhase * 1.3) * flutterAmp * 0.5;\n\n      // Per-flake drift (bounded) — avoid bending the whole field by not applying\n      // sinusoidal offsets to p (the grid coordinate system).\n      float driftPhase = h4 * PI * 2.0 + layerIndex * 1.7;\n      flakePos.x += sin(time * 0.55 + driftPhase) * u_drift * 0.18;\n\n      // Per-flake turbulence (bounded) — adds gusty motion without warping UVs.\n      float turbFreq = 0.6 + u_turbulence * 1.4;\n      vec2 turb = vec2(\n        noise(cellId * 0.17 + time * turbFreq),\n        noise(cellId.yx * 0.17 + time * turbFreq + 17.0)\n      ) - 0.5;\n      flakePos += turb * (u_turbulence * 0.22) * (1.0 - depth);\n\n      vec2 localUV = gv - offs - flakePos;\n\n      float rotationSpeed = (1.5 - sizeVar * 0.5) * (0.5 + h4 * 1.0);\n      float rotationPhase = h4 * PI * 2.0;\n      float rotation = time * rotationSpeed + rotationPhase;\n\n      float flake = snowflakeShape(localUV, size, h1, rotation);\n      float flakeSparkle = sparkle(cellId, time, h1) * flake;\n      sparkleAccum += flakeSparkle;\n\n      snow += flake * layerOpacity;\n    }\n  }\n\n  return vec3(snow, sparkleAccum, depth);\n}\n\nvoid main() {\n  vec4 scene = texture(u_sceneTexture, v_uv);\n  vec2 uv = v_uv;\n  float aspect = u_resolution.x / u_resolution.y;\n  uv.x *= aspect;\n\n  float snow = 0.0;\n  float totalSparkle = 0.0;\n\n  for (int i = u_layers - 1; i >= 0; i--) {\n    vec3 layerResult = snowLayer(uv, u_time, float(i), float(u_layers));\n    snow += layerResult.x;\n    totalSparkle += layerResult.y;\n  }\n\n  snow = clamp(snow, 0.0, 1.0);\n  totalSparkle = clamp(totalSparkle, 0.0, 1.0);\n\n  vec3 snowColor = vec3(0.75, 0.78, 0.85);\n  vec3 sparkleColor = vec3(0.9, 0.92, 1.0);\n\n  vec3 color = scene.rgb + snowColor * snow + sparkleColor * totalSparkle;\n\n  fragColor = vec4(color, scene.a);\n}\n\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/shared/contract.ts",
    "content": "export { defineToolUiContract } from \"@/components/tool-ui/shared/contract\";\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/README.md",
    "content": "# Weather Widget\n\nImplementation for the \"weather-widget\" Tool UI surface.\n\n## Files\n\n- preferred runtime entrypoint: components/tool-ui/weather-widget/runtime.ts\n- authoring exports: lib/weather-authoring/weather-widget/index.tsx\n- authoring schema + parse helpers: lib/weather-authoring/weather-widget/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/weather-widget/content.mdx\n- Preset payload: lib/presets/weather-widget.ts\n- authoring presets: lib/weather-authoring/presets/tuned-presets.json\n- authoring shaders: lib/weather-authoring/shaders/\\*.glsl\n- authoring runtime modules: lib/weather-authoring/runtime/\\*.tsx\n- generated runtime artifacts:\n  - lib/weather-authoring/weather-widget/effects/generated/tuned-presets.generated.ts\n  - lib/weather-authoring/weather-widget/effects/generated/weather-effect-shaders.generated.ts\n  - lib/weather-authoring/weather-widget/effects/generated/glass-panel-svg.generated.tsx\n  - lib/weather-authoring/weather-widget/weather-data-overlay.generated.ts\n  - components/tool-ui/weather-widget/generated/weather-runtime-core.generated.ts\n\n## Build workflow\n\n- compile generated weather runtime artifacts: `pnpm weather:compile`\n- watch authoring sources and regenerate on change: `pnpm weather:watch`\n- fail if generated artifacts are stale: `pnpm weather:check`\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/_adapter.tsx",
    "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn   → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Card → shadcn/ui Card\n */\n\nexport { cn } from \"@/lib/utils\";\nexport { Card } from \"@/components/ui/card\";\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/canvas-resolver-base.ts",
    "content": "import { getTimeOfDay, mapWeatherToEffects } from \"./parameter-mapper\";\nimport type { WeatherEffectParams } from \"./types\";\nimport type { WeatherEffectsCanvasProps } from \"./weather-effects-types\";\nimport type {\n  WeatherCompositorParams,\n  WeatherStudioCompositorParams,\n} from \"./weather-compositor-types\";\n\nexport type { WeatherStudioCompositorParams } from \"./weather-compositor-types\";\n\nexport function createStudioTimestamp(\n  timeOfDay: number,\n  referenceTimestamp?: string,\n): string {\n  const reference = referenceTimestamp\n    ? new Date(referenceTimestamp)\n    : new Date();\n  const date = new Date(reference);\n  date.setUTCFullYear(2000, 0, 1);\n\n  const hours = Math.floor(timeOfDay * 24);\n  const minutes = Math.floor((timeOfDay * 24 - hours) * 60);\n\n  date.setUTCHours(hours, minutes, 0, 0);\n  return date.toISOString();\n}\n\nfunction buildCompositorBaseFromWeather(\n  params: WeatherEffectParams & { timeOfDay?: number },\n): WeatherCompositorParams {\n  const effectConfig = mapWeatherToEffects(params);\n  const timeOfDay =\n    params.timeOfDay ??\n    effectConfig.celestial?.timeOfDay ??\n    getTimeOfDay(params.timestamp);\n\n  const hasCloud = effectConfig.cloud !== undefined;\n  const hasRain = effectConfig.rain !== undefined;\n  const hasLightning = effectConfig.lightning !== undefined;\n  const hasSnow = effectConfig.snow !== undefined;\n\n  const lightningIntervalMin = effectConfig.lightning?.intervalMin ?? 4;\n  const lightningIntervalMax = effectConfig.lightning?.intervalMax ?? 12;\n\n  return {\n    layers: {\n      celestial: true,\n      clouds: hasCloud,\n      rain: hasRain,\n      lightning: hasLightning,\n      snow: hasSnow,\n    },\n    celestial: {\n      timeOfDay,\n      moonPhase: effectConfig.celestial?.moonPhase ?? 0.5,\n      starDensity: effectConfig.celestial?.starDensity ?? 0.5,\n      celestialX: effectConfig.celestial?.celestialX ?? 0.5,\n      celestialY: effectConfig.celestial?.celestialY ?? 0.72,\n      sunSize: effectConfig.celestial?.sunSize ?? 0.06,\n      moonSize: effectConfig.celestial?.moonSize ?? 0.05,\n      sunGlowIntensity: effectConfig.celestial?.sunGlowIntensity ?? 1.0,\n      sunGlowSize: effectConfig.celestial?.sunGlowSize ?? 0.3,\n      sunRayCount: effectConfig.celestial?.sunRayCount ?? 12,\n      sunRayLength: effectConfig.celestial?.sunRayLength ?? 0.5,\n      sunRayIntensity: effectConfig.celestial?.sunRayIntensity ?? 0.4,\n      sunRayShimmer: 1.0,\n      sunRayShimmerSpeed: 1.0,\n      moonGlowIntensity: effectConfig.celestial?.moonGlowIntensity ?? 1.0,\n      moonGlowSize: effectConfig.celestial?.moonGlowSize ?? 0.2,\n      skyBrightness: 1.0,\n      skySaturation: 1.0,\n      skyContrast: 1.0,\n    },\n    cloud: {\n      cloudScale: 1.5,\n      coverage: effectConfig.cloud?.coverage ?? 0.5,\n      density: 0.7,\n      softness: 0.3,\n      windSpeed: effectConfig.cloud?.speed ?? 0.5,\n      windAngle: 0,\n      turbulence: effectConfig.cloud?.turbulence ?? 0.5,\n      lightIntensity: 1.0,\n      ambientDarkness: effectConfig.cloud?.darkness ?? 0.3,\n      backlightIntensity: 0.5,\n      numLayers: 3,\n    },\n    rain: {\n      glassIntensity: hasRain ? (effectConfig.rain?.intensity ?? 0.5) * 0.7 : 0,\n      zoom: 1.0,\n      fallingIntensity: hasRain ? (effectConfig.rain?.intensity ?? 0.6) : 0,\n      fallingSpeed: 1.0,\n      fallingAngle: hasRain ? (effectConfig.rain?.angle ?? 5) * 0.02 : 0.1,\n      fallingStreakLength: 0.8,\n      fallingLayers: 3,\n      fallingRefraction: 0.3,\n    },\n    lightning: {\n      autoMode: hasLightning\n        ? (effectConfig.lightning?.autoTrigger ?? true)\n        : false,\n      autoInterval: hasLightning\n        ? (lightningIntervalMin + lightningIntervalMax) / 2\n        : 8,\n      glowIntensity: 0.8,\n      branchDensity: 0.6,\n      sceneIllumination: 0.6,\n    },\n    snow: {\n      intensity: hasSnow ? (effectConfig.snow?.intensity ?? 0.7) : 0,\n      layers: 4,\n      fallSpeed: 0.5,\n      windSpeed: hasSnow ? (effectConfig.snow?.windDrift ?? 0.3) : 0.3,\n      windAngle: 0,\n      turbulence: 0.3,\n      drift: hasSnow ? (effectConfig.snow?.windDrift ?? 0.3) : 0.3,\n      flutter: 0.5,\n      windShear: 0.2,\n      flakeSize: 1.0,\n      sizeVariation: 0.5,\n      opacity: 0.8,\n      glowAmount: 0.3,\n      sparkle: 0.2,\n    },\n    post: {\n      enabled: true,\n      haze: 0,\n      hazeHorizon: 0.5,\n      hazeDesaturation: 0.3,\n      hazeContrast: 0.2,\n      bloomIntensity: 0,\n      bloomThreshold: 0.8,\n      bloomKnee: 0.5,\n      bloomRadius: 8,\n      bloomTapScale: 1,\n      exposureIntensity: 0,\n      exposureDesaturation: 0.3,\n      exposureRecovery: 2,\n      godRayIntensity: 0,\n      godRayDecay: 0.96,\n      godRayDensity: 0.5,\n      godRayWeight: 0.3,\n      godRaySamples: 60,\n    },\n  };\n}\n\nexport function mapWeatherCompositorParamsToCanvasProps(\n  params: WeatherStudioCompositorParams,\n): WeatherEffectsCanvasProps {\n  return {\n    layers: params.layers,\n    celestial: {\n      timeOfDay: params.celestial.timeOfDay,\n      moonPhase: params.celestial.moonPhase,\n      starDensity: params.celestial.starDensity,\n      celestialX: params.celestial.celestialX,\n      celestialY: params.celestial.celestialY,\n      sunSize: params.celestial.sunSize,\n      moonSize: params.celestial.moonSize,\n      sunGlowIntensity: params.celestial.sunGlowIntensity,\n      sunGlowSize: params.celestial.sunGlowSize,\n      sunRayCount: params.celestial.sunRayCount,\n      sunRayLength: params.celestial.sunRayLength,\n      sunRayIntensity: params.celestial.sunRayIntensity,\n      sunRayShimmer: params.celestial.sunRayShimmer,\n      sunRayShimmerSpeed: params.celestial.sunRayShimmerSpeed,\n      moonGlowIntensity: params.celestial.moonGlowIntensity,\n      moonGlowSize: params.celestial.moonGlowSize,\n      skyBrightness: params.celestial.skyBrightness,\n      skySaturation: params.celestial.skySaturation,\n      skyContrast: params.celestial.skyContrast,\n    },\n    cloud: {\n      coverage: params.cloud.coverage,\n      density: params.cloud.density,\n      softness: params.cloud.softness,\n      cloudScale: params.cloud.cloudScale,\n      windSpeed: params.cloud.windSpeed,\n      windAngle: params.cloud.windAngle,\n      turbulence: params.cloud.turbulence,\n      lightIntensity: params.cloud.lightIntensity,\n      ambientDarkness: params.cloud.ambientDarkness,\n      backlightIntensity: params.cloud.backlightIntensity,\n      numLayers: params.cloud.numLayers,\n    },\n    rain: {\n      glassIntensity: params.rain.glassIntensity,\n      glassZoom: params.rain.zoom,\n      fallingIntensity: params.rain.fallingIntensity,\n      fallingSpeed: params.rain.fallingSpeed,\n      fallingAngle: params.rain.fallingAngle,\n      fallingStreakLength: params.rain.fallingStreakLength,\n      fallingLayers: params.rain.fallingLayers,\n    },\n    lightning: {\n      enabled: params.layers.lightning,\n      autoMode: params.lightning.autoMode,\n      autoInterval: params.lightning.autoInterval,\n      flashIntensity: params.lightning.glowIntensity,\n      branchDensity: params.lightning.branchDensity,\n    },\n    snow: {\n      intensity: params.snow.intensity,\n      layers: params.snow.layers,\n      fallSpeed: params.snow.fallSpeed,\n      windSpeed: params.snow.windSpeed,\n      windAngle: params.snow.windAngle,\n      turbulence: params.snow.turbulence,\n      drift: params.snow.drift,\n      flutter: params.snow.flutter,\n      windShear: params.snow.windShear,\n      flakeSize: params.snow.flakeSize,\n      sizeVariation: params.snow.sizeVariation,\n      opacity: params.snow.opacity,\n      glowAmount: params.snow.glowAmount,\n      sparkle: params.snow.sparkle,\n    },\n    interactions: {\n      rainRefractionStrength: params.rain.fallingRefraction,\n      lightningSceneIllumination: params.lightning.sceneIllumination,\n    },\n    post: params.post,\n  };\n}\n\nexport function buildCanvasBaseFromWeather(\n  params: WeatherEffectParams,\n): WeatherEffectsCanvasProps {\n  return mapWeatherCompositorParamsToCanvasProps(\n    buildCompositorBaseFromWeather(params),\n  );\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/canvas-resolver-runtime.ts",
    "content": "import type { WeatherEffectParams } from \"./types\";\nimport { buildCanvasBaseFromWeather } from \"./canvas-resolver-base\";\nimport {\n  applyWeatherEffectsOverrides,\n  getNearestCheckpoint,\n  type WeatherEffectsTunedPresets,\n} from \"./tuning\";\nimport type { WeatherEffectsCanvasProps } from \"./weather-effects-types\";\n\nexport interface ResolveRuntimeWeatherEffectsCanvasPropsInput extends WeatherEffectParams {\n  tunedPresets?: WeatherEffectsTunedPresets;\n  /**\n   * Optional explicit time-of-day (0-1) used for nearest checkpoint selection.\n   * When omitted, it is derived from `timestamp`.\n   */\n  timeOfDay?: number;\n}\n\nexport function resolveWeatherEffectsCanvasRuntimeProps(\n  input: ResolveRuntimeWeatherEffectsCanvasPropsInput,\n): WeatherEffectsCanvasProps {\n  const base = buildCanvasBaseFromWeather(input);\n  const explicitTimeOfDay = input.timeOfDay;\n\n  if (typeof explicitTimeOfDay === \"number\" && base.celestial) {\n    base.celestial.timeOfDay = explicitTimeOfDay;\n  }\n\n  const conditionCheckpoints = input.tunedPresets?.[input.conditionCode];\n  const timeOfDay = explicitTimeOfDay ?? base.celestial?.timeOfDay;\n  if (!conditionCheckpoints || timeOfDay === undefined) {\n    return base;\n  }\n\n  const checkpoint = getNearestCheckpoint(timeOfDay);\n  return applyWeatherEffectsOverrides(base, conditionCheckpoints[checkpoint]);\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/canvas-resolver.ts",
    "content": "import type { WeatherEffectParams } from \"./types\";\nimport {\n  buildCanvasBaseFromWeather,\n  createStudioTimestamp,\n} from \"./canvas-resolver-base\";\nimport { resolveInterpolatedOverridesForTime } from \"./checkpoint-overrides\";\nimport {\n  applyWeatherEffectsOverrides,\n  getNearestCheckpoint,\n  TIME_CHECKPOINTS,\n  type TimeCheckpoint,\n  type WeatherEffectsTunedPresets,\n} from \"./tuning\";\nimport type {\n  CloudParams,\n  InteractionParams,\n  LightningParams,\n  PostProcessParams,\n  RainParams,\n  WeatherEffectsCanvasProps,\n} from \"./weather-effects-types\";\n\nexport { mapWeatherCompositorParamsToCanvasProps } from \"./canvas-resolver-base\";\nexport type { WeatherStudioCompositorParams } from \"./canvas-resolver-base\";\nexport { resolveConditionCheckpointOverridesForTime } from \"./checkpoint-overrides\";\n\nexport type WeatherEffectsCheckpointMode = \"nearest\" | \"interpolated\";\n\nexport interface ResolveWeatherEffectsCanvasPropsInput extends WeatherEffectParams {\n  tunedPresets?: WeatherEffectsTunedPresets;\n  checkpointMode?: WeatherEffectsCheckpointMode;\n  /**\n   * Optional explicit time-of-day (0-1) used for checkpoint blending.\n   * When omitted, it is derived from `timestamp`.\n   */\n  timeOfDay?: number;\n}\n\nfunction resolveBaseForCheckpoint(\n  params: WeatherEffectParams,\n  checkpoint: TimeCheckpoint,\n): WeatherEffectsCanvasProps {\n  const time = TIME_CHECKPOINTS[checkpoint];\n  const timestamp = createStudioTimestamp(time, params.timestamp);\n  return buildCanvasBaseFromWeather({\n    ...params,\n    timestamp,\n  });\n}\n\nexport function resolveWeatherEffectsCanvasProps(\n  input: ResolveWeatherEffectsCanvasPropsInput,\n): WeatherEffectsCanvasProps {\n  const checkpointMode = input.checkpointMode ?? \"nearest\";\n  const base = buildCanvasBaseFromWeather(input);\n  const explicitTimeOfDay = input.timeOfDay;\n  if (typeof explicitTimeOfDay === \"number\" && base.celestial) {\n    base.celestial.timeOfDay = explicitTimeOfDay;\n  }\n\n  const conditionCheckpoints = input.tunedPresets?.[input.conditionCode];\n  const timeOfDay = explicitTimeOfDay ?? base.celestial?.timeOfDay;\n  if (!conditionCheckpoints || timeOfDay === undefined) {\n    return base;\n  }\n\n  if (checkpointMode === \"interpolated\") {\n    const interpolated = resolveInterpolatedOverridesForTime({\n      checkpointOverrides: conditionCheckpoints,\n      timeOfDay,\n      getBaseForCheckpoint: (checkpoint) =>\n        resolveBaseForCheckpoint(input, checkpoint),\n    });\n\n    return interpolated\n      ? applyWeatherEffectsOverrides(base, interpolated)\n      : base;\n  }\n\n  const checkpoint = getNearestCheckpoint(timeOfDay);\n  return applyWeatherEffectsOverrides(base, conditionCheckpoints[checkpoint]);\n}\n\nexport type {\n  CloudParams,\n  InteractionParams,\n  LightningParams,\n  PostProcessParams,\n  RainParams,\n};\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/checkpoint-overrides.ts",
    "content": "import type { WeatherConditionCode } from \"../schema\";\nimport {\n  getNearestCheckpoint,\n  TIME_CHECKPOINTS,\n  TIME_CHECKPOINT_ORDER,\n  type TimeCheckpoint,\n  type WeatherEffectsCheckpointOverrides,\n  type WeatherEffectsOverrides,\n  type WeatherEffectsTunedPresets,\n} from \"./tuning\";\nimport type { WeatherEffectsCanvasProps } from \"./weather-effects-types\";\n\ninterface SurroundingCheckpoints {\n  before: TimeCheckpoint;\n  after: TimeCheckpoint;\n  t: number;\n}\n\nfunction getSurroundingCheckpoints(timeOfDay: number): SurroundingCheckpoints {\n  const checkpointTimes = TIME_CHECKPOINT_ORDER.map((checkpoint) => ({\n    checkpoint,\n    value: TIME_CHECKPOINTS[checkpoint],\n  })).sort((a, b) => a.value - b.value);\n\n  const normalized = ((timeOfDay % 1) + 1) % 1;\n\n  for (let i = 0; i < checkpointTimes.length; i++) {\n    const current = checkpointTimes[i];\n    const next = checkpointTimes[(i + 1) % checkpointTimes.length];\n\n    const start = current.value;\n    let end = next.value;\n\n    if (end < start) {\n      end += 1;\n    }\n\n    let query = normalized;\n    if (query < start && end > 1) {\n      query += 1;\n    }\n\n    if (query >= start && query < end) {\n      const range = end - start;\n      const t = range > 0 ? (query - start) / range : 0;\n      return {\n        before: current.checkpoint,\n        after: next.checkpoint,\n        t,\n      };\n    }\n  }\n\n  return {\n    before: \"midnight\",\n    after: \"dawn\",\n    t: 0,\n  };\n}\n\nfunction lerp(a: number, b: number, t: number): number {\n  return a + (b - a) * t;\n}\n\nfunction interpolatePartialObject<T extends object>(\n  before: Partial<T> | undefined,\n  after: Partial<T> | undefined,\n  baseBefore: T | undefined,\n  baseAfter: T | undefined,\n  t: number,\n): Partial<T> | undefined {\n  if (!before && !after) return undefined;\n\n  const result: Partial<T> = {};\n  const keys = new Set([\n    ...(before ? Object.keys(before) : []),\n    ...(after ? Object.keys(after) : []),\n  ]) as Set<keyof T>;\n\n  for (const key of keys) {\n    const beforeValue = before?.[key];\n    const afterValue = after?.[key];\n\n    if (beforeValue === undefined && afterValue === undefined) {\n      continue;\n    }\n\n    let from = beforeValue;\n    let to = afterValue;\n\n    if (from === undefined && baseBefore) {\n      from = baseBefore[key];\n    }\n    if (to === undefined && baseAfter) {\n      to = baseAfter[key];\n    }\n\n    if (from === undefined || to === undefined) {\n      result[key] = (from ?? to) as T[keyof T];\n      continue;\n    }\n\n    if (typeof from === \"number\" && typeof to === \"number\") {\n      result[key] = lerp(from, to, t) as T[keyof T];\n      continue;\n    }\n\n    if (typeof from === \"boolean\") {\n      result[key] = (t < 0.5 ? from : to) as T[keyof T];\n      continue;\n    }\n\n    result[key] = (t < 0.5 ? from : to) as T[keyof T];\n  }\n\n  return Object.keys(result).length > 0 ? result : undefined;\n}\n\nfunction interpolateWeatherEffectsOverrides(\n  before: WeatherEffectsOverrides | undefined,\n  after: WeatherEffectsOverrides | undefined,\n  baseBefore: WeatherEffectsCanvasProps | undefined,\n  baseAfter: WeatherEffectsCanvasProps | undefined,\n  t: number,\n): WeatherEffectsOverrides | undefined {\n  if (!before && !after) return undefined;\n\n  const result: WeatherEffectsOverrides = {};\n\n  const layers = interpolatePartialObject(\n    before?.layers,\n    after?.layers,\n    baseBefore?.layers,\n    baseAfter?.layers,\n    t,\n  );\n  if (layers) result.layers = layers;\n\n  const celestial = interpolatePartialObject(\n    before?.celestial,\n    after?.celestial,\n    baseBefore?.celestial,\n    baseAfter?.celestial,\n    t,\n  );\n  if (celestial) result.celestial = celestial;\n\n  const cloud = interpolatePartialObject(\n    before?.cloud,\n    after?.cloud,\n    baseBefore?.cloud,\n    baseAfter?.cloud,\n    t,\n  );\n  if (cloud) result.cloud = cloud;\n\n  const rain = interpolatePartialObject(\n    before?.rain,\n    after?.rain,\n    baseBefore?.rain,\n    baseAfter?.rain,\n    t,\n  );\n  if (rain) result.rain = rain;\n\n  const lightning = interpolatePartialObject(\n    before?.lightning,\n    after?.lightning,\n    baseBefore?.lightning,\n    baseAfter?.lightning,\n    t,\n  );\n  if (lightning) result.lightning = lightning;\n\n  const snow = interpolatePartialObject(\n    before?.snow,\n    after?.snow,\n    baseBefore?.snow,\n    baseAfter?.snow,\n    t,\n  );\n  if (snow) result.snow = snow;\n\n  const interactions = interpolatePartialObject(\n    before?.interactions,\n    after?.interactions,\n    baseBefore?.interactions,\n    baseAfter?.interactions,\n    t,\n  );\n  if (interactions) result.interactions = interactions;\n\n  const post = interpolatePartialObject(\n    before?.post,\n    after?.post,\n    baseBefore?.post,\n    baseAfter?.post,\n    t,\n  );\n  if (post) result.post = post;\n\n  return Object.keys(result).length > 0 ? result : undefined;\n}\n\nexport function resolveInterpolatedOverridesForTime(opts: {\n  checkpointOverrides: WeatherEffectsCheckpointOverrides;\n  timeOfDay: number;\n  getBaseForCheckpoint: (\n    checkpoint: TimeCheckpoint,\n  ) => WeatherEffectsCanvasProps;\n}): WeatherEffectsOverrides | undefined {\n  const { before, after, t } = getSurroundingCheckpoints(opts.timeOfDay);\n\n  return interpolateWeatherEffectsOverrides(\n    opts.checkpointOverrides[before],\n    opts.checkpointOverrides[after],\n    opts.getBaseForCheckpoint(before),\n    opts.getBaseForCheckpoint(after),\n    t,\n  );\n}\n\nexport function resolveConditionCheckpointOverridesForTime(opts: {\n  conditionCode: WeatherConditionCode;\n  timeOfDay: number;\n  tunedPresets: WeatherEffectsTunedPresets;\n  checkpointMode?: \"nearest\" | \"interpolated\";\n}): WeatherEffectsOverrides | undefined {\n  const checkpointMode = opts.checkpointMode ?? \"nearest\";\n  const conditionCheckpoints = opts.tunedPresets[opts.conditionCode];\n  if (!conditionCheckpoints) return undefined;\n\n  if (checkpointMode === \"interpolated\") {\n    const { before, after, t } = getSurroundingCheckpoints(opts.timeOfDay);\n    return interpolateWeatherEffectsOverrides(\n      conditionCheckpoints[before],\n      conditionCheckpoints[after],\n      undefined,\n      undefined,\n      t,\n    );\n  }\n\n  return conditionCheckpoints[getNearestCheckpoint(opts.timeOfDay)];\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/custom-effect-props.ts",
    "content": "export type WeatherEffectLayer =\n  | \"celestial\"\n  | \"clouds\"\n  | \"rain\"\n  | \"lightning\"\n  | \"snow\";\n\n/**\n * Custom effect layer props for direct control.\n * When provided, these override the auto-calculated values from weather mapping.\n */\nexport interface CustomEffectProps {\n  enabledLayers?: WeatherEffectLayer[];\n  celestial?: {\n    timeOfDay: number;\n    moonPhase: number;\n    starDensity: number;\n    celestialX: number;\n    celestialY: number;\n    sunSize: number;\n    moonSize: number;\n    sunGlowIntensity: number;\n    sunGlowSize: number;\n    sunRayCount: number;\n    sunRayLength: number;\n    sunRayIntensity: number;\n    /**\n     * Scales subtle, noise-driven ray motion (shimmer + slow \"breathing\").\n     * 0 disables motion; 1 is the default subtlety; >1 increases visibility.\n     */\n    sunRayShimmer?: number;\n    /**\n     * Global speed multiplier for the ray shimmer/breath noise inputs.\n     * 1 is the default speed; >1 speeds up motion.\n     */\n    sunRayShimmerSpeed?: number;\n    moonGlowIntensity: number;\n    moonGlowSize: number;\n  };\n  cloud?: {\n    cloudScale?: number;\n    coverage: number;\n    density?: number;\n    softness?: number;\n    windSpeed: number;\n    windAngle?: number;\n    turbulence: number;\n    sunAltitude: number;\n    sunAzimuth?: number;\n    lightIntensity?: number;\n    ambientDarkness: number;\n    numLayers?: number;\n    layerSpread?: number;\n    starDensity: number;\n    starSize?: number;\n    starTwinkleSpeed?: number;\n    starTwinkleAmount?: number;\n    horizonLine?: number;\n  };\n  rain?: {\n    glassIntensity: number;\n    zoom?: number;\n    fallingIntensity: number;\n    fallingSpeed?: number;\n    fallingAngle: number;\n    fallingStreakLength?: number;\n    fallingLayers?: number;\n    fallingRefraction?: number;\n    fallingWaviness?: number;\n    fallingThicknessVar?: number;\n  };\n  lightning?: {\n    branchDensity?: number;\n    displacement?: number;\n    glowIntensity?: number;\n    flashDuration?: number;\n    sceneIllumination?: number;\n    afterglowPersistence?: number;\n    autoMode: boolean;\n    autoInterval: number;\n  };\n  snow?: {\n    intensity: number;\n    layers?: number;\n    fallSpeed?: number;\n    windSpeed: number;\n    windAngle?: number;\n    turbulence?: number;\n    drift: number;\n    flutter?: number;\n    windShear?: number;\n    flakeSize?: number;\n    sizeVariation?: number;\n    opacity?: number;\n    glowAmount?: number;\n    sparkle?: number;\n    visibility?: number;\n  };\n  glass?: {\n    enabled?: boolean;\n    depth?: number;\n    strength?: number;\n    chromaticAberration?: number;\n    blur?: number;\n    brightness?: number;\n    saturation?: number;\n  };\n  post?: {\n    enabled?: boolean;\n    haze?: number;\n    hazeHorizon?: number;\n    hazeDesaturation?: number;\n    hazeContrast?: number;\n    bloomIntensity?: number;\n    bloomThreshold?: number;\n    bloomKnee?: number;\n    bloomRadius?: number;\n    bloomTapScale?: number;\n    exposureIntensity?: number;\n    exposureDesaturation?: number;\n    exposureRecovery?: number;\n    godRayIntensity?: number;\n    godRayDecay?: number;\n    godRayDensity?: number;\n    godRayWeight?: number;\n    godRaySamples?: number;\n  };\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/effect-compositor-custom-props.ts",
    "content": "import type {\n  LayerToggles,\n  WeatherEffectsCanvasProps,\n} from \"./weather-effects-types\";\nimport type {\n  CustomEffectProps,\n  WeatherEffectLayer,\n} from \"./custom-effect-props\";\n\nfunction sunAltitudeToLightIntensity(sunAltitude: number): number {\n  const light =\n    sunAltitude < 0\n      ? 0.05 + (1 + sunAltitude) * 0.1\n      : 0.15 + sunAltitude * 0.85;\n  return Math.max(0, Math.min(1, light));\n}\n\nfunction isLayerEnabled(\n  enabledLayers: CustomEffectProps[\"enabledLayers\"],\n  layer: WeatherEffectLayer,\n  hasConfig: boolean,\n): boolean {\n  if (!hasConfig) return false;\n  if (!enabledLayers) return true;\n  return enabledLayers.includes(layer);\n}\n\nexport function mapCustomEffectPropsToCanvasProps(\n  custom: CustomEffectProps,\n): WeatherEffectsCanvasProps | null {\n  const hasCelestial = isLayerEnabled(\n    custom.enabledLayers,\n    \"celestial\",\n    custom.celestial !== undefined,\n  );\n  const hasCloud = isLayerEnabled(\n    custom.enabledLayers,\n    \"clouds\",\n    custom.cloud !== undefined,\n  );\n  const hasRain = isLayerEnabled(\n    custom.enabledLayers,\n    \"rain\",\n    custom.rain !== undefined,\n  );\n  const hasLightning = isLayerEnabled(\n    custom.enabledLayers,\n    \"lightning\",\n    custom.lightning !== undefined,\n  );\n  const hasSnow = isLayerEnabled(\n    custom.enabledLayers,\n    \"snow\",\n    custom.snow !== undefined,\n  );\n  const hasPost = custom.post !== undefined;\n\n  if (\n    !hasCelestial &&\n    !hasCloud &&\n    !hasRain &&\n    !hasLightning &&\n    !hasSnow &&\n    !hasPost\n  ) {\n    return null;\n  }\n\n  const layers: Partial<LayerToggles> = {\n    celestial: hasCelestial,\n    clouds: hasCloud,\n    rain: hasRain,\n    lightning: hasLightning,\n    snow: hasSnow,\n  };\n\n  const interactions: Partial<\n    NonNullable<WeatherEffectsCanvasProps[\"interactions\"]>\n  > = {};\n  if (custom.rain?.fallingRefraction !== undefined) {\n    interactions.rainRefractionStrength = custom.rain.fallingRefraction;\n  }\n  if (custom.lightning?.sceneIllumination !== undefined) {\n    interactions.lightningSceneIllumination =\n      custom.lightning.sceneIllumination;\n  }\n\n  return {\n    layers,\n    celestial: hasCelestial ? custom.celestial : undefined,\n    cloud:\n      hasCloud && custom.cloud\n        ? {\n            coverage: custom.cloud.coverage,\n            density: custom.cloud.density,\n            softness: custom.cloud.softness,\n            cloudScale: custom.cloud.cloudScale,\n            windSpeed: custom.cloud.windSpeed,\n            windAngle: custom.cloud.windAngle,\n            turbulence: custom.cloud.turbulence,\n            lightIntensity:\n              custom.cloud.lightIntensity ??\n              sunAltitudeToLightIntensity(custom.cloud.sunAltitude),\n            ambientDarkness: custom.cloud.ambientDarkness,\n            numLayers: custom.cloud.numLayers,\n          }\n        : undefined,\n    rain:\n      hasRain && custom.rain\n        ? {\n            glassIntensity: custom.rain.glassIntensity,\n            glassZoom: custom.rain.zoom,\n            fallingIntensity: custom.rain.fallingIntensity,\n            fallingSpeed: custom.rain.fallingSpeed,\n            fallingAngle: custom.rain.fallingAngle,\n            fallingStreakLength: custom.rain.fallingStreakLength,\n            fallingLayers: custom.rain.fallingLayers,\n          }\n        : undefined,\n    lightning:\n      hasLightning && custom.lightning\n        ? {\n            enabled: true,\n            autoMode: custom.lightning.autoMode,\n            autoInterval: custom.lightning.autoInterval,\n            flashIntensity: custom.lightning.glowIntensity,\n            branchDensity: custom.lightning.branchDensity,\n          }\n        : undefined,\n    snow:\n      hasSnow && custom.snow\n        ? {\n            intensity: custom.snow.intensity,\n            layers: custom.snow.layers,\n            fallSpeed: custom.snow.fallSpeed,\n            windSpeed: custom.snow.windSpeed,\n            drift: custom.snow.drift,\n            flakeSize: custom.snow.flakeSize,\n          }\n        : undefined,\n    interactions:\n      Object.keys(interactions).length > 0 ? interactions : undefined,\n    post: custom.post,\n  };\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/effect-compositor-quality.ts",
    "content": "import type { EffectSettings } from \"./types\";\n\nexport type ResolvedEffectQuality = \"low\" | \"medium\" | \"high\";\n\nfunction resolveAutoQuality(): ResolvedEffectQuality {\n  if (typeof window === \"undefined\") return \"high\";\n\n  const dpr = window.devicePixelRatio || 1;\n  const px = window.innerWidth * window.innerHeight * dpr * dpr;\n\n  const cores =\n    typeof navigator !== \"undefined\"\n      ? navigator.hardwareConcurrency\n      : undefined;\n  const mem =\n    typeof navigator !== \"undefined\"\n      ? (navigator as Navigator & { deviceMemory?: number }).deviceMemory\n      : undefined;\n\n  const isSmallScreen = window.innerWidth < 768;\n\n  if (\n    (typeof mem === \"number\" && mem <= 4) ||\n    (typeof cores === \"number\" && cores <= 4)\n  ) {\n    return isSmallScreen ? \"low\" : \"medium\";\n  }\n\n  if (px > 2_500_000) return isSmallScreen ? \"low\" : \"medium\";\n\n  return isSmallScreen ? \"medium\" : \"high\";\n}\n\nexport function resolveEffectQuality(\n  quality: EffectSettings[\"quality\"],\n): ResolvedEffectQuality {\n  if (quality === \"low\" || quality === \"medium\" || quality === \"high\") {\n    return quality;\n  }\n  return resolveAutoQuality();\n}\n\nexport function resolveEffectCanvasDpr(\n  quality: ResolvedEffectQuality,\n): number | undefined {\n  if (typeof window === \"undefined\") return undefined;\n\n  const base = window.devicePixelRatio || 1;\n  const cap = quality === \"low\" ? 1.0 : quality === \"medium\" ? 1.5 : 2.0;\n\n  return Math.max(1, Math.min(base, cap));\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/effect-compositor-runtime.tsx",
    "content": "\"use client\";\n\nimport { useMemo, useState, useEffect } from \"react\";\nimport type { WeatherConditionCode } from \"../schema\";\nimport type { EffectSettings } from \"./types\";\nimport { WeatherEffectsCanvas } from \"./weather-effects-canvas\";\nimport type { WeatherEffectsCanvasProps } from \"./weather-effects-types\";\nimport { TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES } from \"./generated/tuned-presets.generated\";\nimport { type WeatherEffectsTunedPresets } from \"./tuning\";\nimport { resolveWeatherEffectsCanvasRuntimeProps } from \"./canvas-resolver-runtime\";\nimport {\n  resolveEffectCanvasDpr,\n  resolveEffectQuality,\n} from \"./effect-compositor-quality\";\n\nconst DEFAULT_TUNED_PRESETS: WeatherEffectsTunedPresets =\n  TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES;\n\ninterface EffectCompositorRuntimeProps {\n  conditionCode: WeatherConditionCode;\n  windSpeed?: number;\n  precipitationLevel?: \"none\" | \"light\" | \"moderate\" | \"heavy\";\n  visibility?: number;\n  timestamp?: string;\n  timeOfDay?: number;\n  settings?: EffectSettings;\n  className?: string;\n}\n\nexport function EffectCompositorRuntime({\n  conditionCode,\n  windSpeed,\n  precipitationLevel,\n  visibility,\n  timestamp,\n  timeOfDay,\n  settings,\n  className,\n}: EffectCompositorRuntimeProps) {\n  const [isMounted, setIsMounted] = useState(false);\n  const enabled = settings?.enabled !== false;\n  const reducedMotion = settings?.reducedMotion ?? false;\n  const resolvedQuality = useMemo(\n    () => resolveEffectQuality(settings?.quality ?? \"auto\"),\n    [settings?.quality],\n  );\n\n  useEffect(() => {\n    setIsMounted(true);\n  }, []);\n\n  const dpr = useMemo(() => {\n    return resolveEffectCanvasDpr(resolvedQuality);\n  }, [resolvedQuality]);\n\n  const canvasProps = useMemo<WeatherEffectsCanvasProps | null>(() => {\n    if (!enabled || reducedMotion) return null;\n\n    return resolveWeatherEffectsCanvasRuntimeProps({\n      conditionCode,\n      windSpeed,\n      precipitationLevel,\n      visibility,\n      timestamp,\n      timeOfDay,\n      tunedPresets: DEFAULT_TUNED_PRESETS,\n    });\n  }, [\n    enabled,\n    reducedMotion,\n    conditionCode,\n    windSpeed,\n    precipitationLevel,\n    visibility,\n    timestamp,\n    timeOfDay,\n  ]);\n\n  if (!isMounted || !enabled || reducedMotion || !canvasProps) {\n    return null;\n  }\n\n  return (\n    <div\n      className={className}\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        overflow: \"hidden\",\n        pointerEvents: \"none\",\n        borderRadius: \"inherit\",\n      }}\n      aria-hidden=\"true\"\n    >\n      <WeatherEffectsCanvas\n        className=\"absolute inset-0\"\n        dpr={dpr}\n        {...canvasProps}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/effect-compositor.tsx",
    "content": "\"use client\";\n\nimport { useMemo, useState, useEffect } from \"react\";\nimport type { WeatherConditionCode } from \"../schema\";\nimport type { EffectSettings } from \"./types\";\nimport type { CustomEffectProps } from \"./custom-effect-props\";\nimport { WeatherEffectsCanvas } from \"./weather-effects-canvas\";\nimport type { WeatherEffectsCanvasProps } from \"./weather-effects-types\";\nimport { TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES } from \"./generated/tuned-presets.generated\";\nimport { type WeatherEffectsTunedPresets } from \"./tuning\";\nimport {\n  resolveWeatherEffectsCanvasProps,\n  type WeatherEffectsCheckpointMode,\n} from \"./canvas-resolver\";\nimport { mapCustomEffectPropsToCanvasProps } from \"./effect-compositor-custom-props\";\nimport {\n  resolveEffectCanvasDpr,\n  resolveEffectQuality,\n} from \"./effect-compositor-quality\";\n\nconst DEFAULT_CHECKPOINT_MODE: WeatherEffectsCheckpointMode = \"nearest\";\nconst DEFAULT_TUNED_PRESETS: WeatherEffectsTunedPresets =\n  TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES;\n\ninterface EffectCompositorProps {\n  conditionCode: WeatherConditionCode;\n  windSpeed?: number;\n  precipitationLevel?: \"none\" | \"light\" | \"moderate\" | \"heavy\";\n  visibility?: number;\n  timestamp?: string;\n  timeOfDay?: number;\n  settings?: EffectSettings;\n  className?: string;\n  /**\n   * Custom effect props for direct control over all effect parameters.\n   * When provided, these override the auto-calculated values.\n   */\n  customProps?: CustomEffectProps;\n}\n\nexport function EffectCompositor({\n  conditionCode,\n  windSpeed,\n  precipitationLevel,\n  visibility,\n  timestamp,\n  timeOfDay,\n  settings,\n  className,\n  customProps,\n}: EffectCompositorProps) {\n  const [isMounted, setIsMounted] = useState(false);\n  const enabled = settings?.enabled !== false;\n  const reducedMotion = settings?.reducedMotion ?? false;\n  const hasCustomProps = customProps !== undefined;\n  const resolvedQuality = useMemo(\n    () => resolveEffectQuality(settings?.quality ?? \"auto\"),\n    [settings?.quality],\n  );\n\n  useEffect(() => {\n    setIsMounted(true);\n  }, []);\n\n  const dpr = useMemo(() => {\n    return resolveEffectCanvasDpr(resolvedQuality);\n  }, [resolvedQuality]);\n\n  const canvasProps = useMemo<WeatherEffectsCanvasProps | null>(() => {\n    if (!enabled || reducedMotion) return null;\n\n    if (hasCustomProps && customProps) {\n      return mapCustomEffectPropsToCanvasProps(customProps);\n    }\n\n    return resolveWeatherEffectsCanvasProps({\n      conditionCode,\n      windSpeed,\n      precipitationLevel,\n      visibility,\n      timestamp,\n      timeOfDay,\n      tunedPresets: DEFAULT_TUNED_PRESETS,\n      checkpointMode: DEFAULT_CHECKPOINT_MODE,\n    });\n  }, [\n    enabled,\n    reducedMotion,\n    hasCustomProps,\n    customProps,\n    conditionCode,\n    windSpeed,\n    precipitationLevel,\n    visibility,\n    timestamp,\n    timeOfDay,\n  ]);\n\n  if (!isMounted || !enabled || reducedMotion || !canvasProps) {\n    return null;\n  }\n\n  return (\n    <div\n      className={className}\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        overflow: \"hidden\",\n        pointerEvents: \"none\",\n        borderRadius: \"inherit\",\n      }}\n      aria-hidden=\"true\"\n    >\n      <WeatherEffectsCanvas\n        className=\"absolute inset-0\"\n        dpr={dpr}\n        {...canvasProps}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/generated/glass-panel-svg.generated.tsx",
    "content": "// AUTO-GENERATED by `pnpm weather:compile`.\n// Source: lib/weather-authoring/runtime/glass-panel-svg.tsx\n// DO NOT EDIT MANUALLY.\n\n\"use client\";\nimport { useRef, useState, useEffect, useCallback, useMemo, type ReactNode, type CSSProperties, type RefObject, } from \"react\";\ninterface Dimensions {\n    width: number;\n    height: number;\n}\ninterface GlassEffectOptions {\n    depth: number;\n    radius: number;\n    strength: number;\n    chromaticAberration: number;\n    blur: number;\n    brightness: number;\n    saturation: number;\n}\nconst DEFAULT_GLASS_OPTIONS: GlassEffectOptions = {\n    depth: 12,\n    radius: 12,\n    strength: 40,\n    chromaticAberration: 8,\n    blur: 2,\n    brightness: 1.05,\n    saturation: 1.2,\n} as const;\ninterface DisplacementMapParams {\n    width: number;\n    height: number;\n    radius: number;\n    depth: number;\n}\nfunction buildDisplacementMapSvg({ width, height, radius, depth, }: DisplacementMapParams): string {\n    const radiusYPct = Math.ceil((radius / height) * 15);\n    const radiusXPct = Math.ceil((radius / width) * 15);\n    const innerWidth = Math.max(0, width - 2 * depth);\n    const innerHeight = Math.max(0, height - 2 * depth);\n    const svg = `<svg height=\"${height}\" width=\"${width}\" viewBox=\"0 0 ${width} ${height}\" xmlns=\"http://www.w3.org/2000/svg\">\n    <style>.mix { mix-blend-mode: screen; }</style>\n    <defs>\n      <linearGradient id=\"Y\" x1=\"0\" x2=\"0\" y1=\"${radiusYPct}%\" y2=\"${100 - radiusYPct}%\">\n        <stop offset=\"0%\" stop-color=\"#0F0\" />\n        <stop offset=\"100%\" stop-color=\"#000\" />\n      </linearGradient>\n      <linearGradient id=\"X\" x1=\"${radiusXPct}%\" x2=\"${100 - radiusXPct}%\" y1=\"0\" y2=\"0\">\n        <stop offset=\"0%\" stop-color=\"#F00\" />\n        <stop offset=\"100%\" stop-color=\"#000\" />\n      </linearGradient>\n    </defs>\n    <rect x=\"0\" y=\"0\" height=\"${height}\" width=\"${width}\" fill=\"#808080\" />\n    <g filter=\"blur(2px)\">\n      <rect x=\"0\" y=\"0\" height=\"${height}\" width=\"${width}\" fill=\"#000080\" />\n      <rect x=\"0\" y=\"0\" height=\"${height}\" width=\"${width}\" fill=\"url(#Y)\" class=\"mix\" />\n      <rect x=\"0\" y=\"0\" height=\"${height}\" width=\"${width}\" fill=\"url(#X)\" class=\"mix\" />\n      <rect x=\"${depth}\" y=\"${depth}\" height=\"${innerHeight}\" width=\"${innerWidth}\" fill=\"#808080\" rx=\"${radius}\" ry=\"${radius}\" filter=\"blur(${depth}px)\" />\n    </g>\n  </svg>`;\n    return \"data:image/svg+xml;utf8,\" + encodeURIComponent(svg);\n}\ninterface DisplacementFilterParams extends DisplacementMapParams {\n    strength: number;\n    chromaticAberration: number;\n}\nfunction buildDisplacementFilterUrl({ width, height, radius, depth, strength, chromaticAberration, }: DisplacementFilterParams): string {\n    const mapUrl = buildDisplacementMapSvg({ width, height, radius, depth });\n    const feImage = `<feImage x=\"0\" y=\"0\" height=\"${height}\" width=\"${width}\" href=\"${mapUrl}\" result=\"displacementMap\" />`;\n    let filterContent: string;\n    if (chromaticAberration === 0) {\n        filterContent = `\n      ${feImage}\n      <feDisplacementMap in=\"SourceGraphic\" in2=\"displacementMap\" scale=\"${strength}\" xChannelSelector=\"R\" yChannelSelector=\"G\" />\n    `;\n    }\n    else {\n        const redScale = strength + chromaticAberration * 2;\n        const greenScale = strength + chromaticAberration;\n        const blueScale = strength;\n        filterContent = `\n      ${feImage}\n      <feDisplacementMap in=\"SourceGraphic\" in2=\"displacementMap\" scale=\"${redScale}\" xChannelSelector=\"R\" yChannelSelector=\"G\" />\n      <feColorMatrix type=\"matrix\" values=\"1 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1 0\" result=\"displacedR\" />\n      <feDisplacementMap in=\"SourceGraphic\" in2=\"displacementMap\" scale=\"${greenScale}\" xChannelSelector=\"R\" yChannelSelector=\"G\" />\n      <feColorMatrix type=\"matrix\" values=\"0 0 0 0 0  0 1 0 0 0  0 0 0 0 0  0 0 0 1 0\" result=\"displacedG\" />\n      <feDisplacementMap in=\"SourceGraphic\" in2=\"displacementMap\" scale=\"${blueScale}\" xChannelSelector=\"R\" yChannelSelector=\"G\" />\n      <feColorMatrix type=\"matrix\" values=\"0 0 0 0 0  0 0 0 0 0  0 0 1 0 0  0 0 0 1 0\" result=\"displacedB\" />\n      <feBlend in=\"displacedR\" in2=\"displacedG\" mode=\"screen\"/>\n      <feBlend in2=\"displacedB\" mode=\"screen\"/>\n    `;\n    }\n    const svg = `<svg height=\"${height}\" width=\"${width}\" viewBox=\"0 0 ${width} ${height}\" xmlns=\"http://www.w3.org/2000/svg\">\n    <defs>\n      <filter id=\"displace\" color-interpolation-filters=\"sRGB\">${filterContent}</filter>\n    </defs>\n  </svg>`;\n    return \"data:image/svg+xml;utf8,\" + encodeURIComponent(svg) + \"#displace\";\n}\ninterface BackdropFilterParams {\n    filterUrl: string;\n    blur: number;\n    brightness: number;\n    saturation: number;\n}\nfunction buildBackdropFilterValue({ filterUrl, blur, brightness, saturation, }: BackdropFilterParams): string {\n    return `blur(${blur / 2}px) url('${filterUrl}') blur(${blur}px) brightness(${brightness}) saturate(${saturation})`;\n}\nfunction useElementDimensions(ref: RefObject<HTMLElement | null>): Dimensions | null {\n    const [dimensions, setDimensions] = useState<Dimensions | null>(null);\n    const updateDimensions = useCallback(() => {\n        if (ref.current) {\n            const rect = ref.current.getBoundingClientRect();\n            setDimensions({\n                width: Math.round(rect.width),\n                height: Math.round(rect.height),\n            });\n        }\n    }, [ref]);\n    useEffect(() => {\n        updateDimensions();\n        const element = ref.current;\n        if (!element)\n            return;\n        const observer = new ResizeObserver(updateDimensions);\n        observer.observe(element);\n        return () => observer.disconnect();\n    }, [updateDimensions, ref]);\n    return dimensions;\n}\nfunction useSupportsBackdropFilter(): boolean {\n    const [supported, setSupported] = useState(true);\n    useEffect(() => {\n        const hasSupport = CSS.supports(\"backdrop-filter\", \"blur(1px)\") ||\n            CSS.supports(\"-webkit-backdrop-filter\", \"blur(1px)\");\n        setSupported(hasSupport);\n    }, []);\n    return supported;\n}\nexport interface GlassPanelProps {\n    children?: ReactNode;\n    className?: string;\n    depth?: number;\n    radius?: number;\n    strength?: number;\n    chromaticAberration?: number;\n    blur?: number;\n    debug?: boolean;\n}\nexport function GlassPanel({ children, className, depth = DEFAULT_GLASS_OPTIONS.depth, radius = DEFAULT_GLASS_OPTIONS.radius, strength = DEFAULT_GLASS_OPTIONS.strength, chromaticAberration = DEFAULT_GLASS_OPTIONS.chromaticAberration, blur = DEFAULT_GLASS_OPTIONS.blur, debug = false, }: GlassPanelProps) {\n    const ref = useRef<HTMLDivElement>(null);\n    const dimensions = useElementDimensions(ref);\n    const style = useMemo((): CSSProperties => {\n        const base: CSSProperties = { borderRadius: radius };\n        if (!dimensions || dimensions.width <= 0 || dimensions.height <= 0) {\n            return base;\n        }\n        if (debug) {\n            const mapUrl = buildDisplacementMapSvg({\n                width: dimensions.width,\n                height: dimensions.height,\n                radius,\n                depth,\n            });\n            return {\n                ...base,\n                background: `url(\"${mapUrl}\")`,\n                backgroundSize: \"cover\",\n            };\n        }\n        const filterUrl = buildDisplacementFilterUrl({\n            width: dimensions.width,\n            height: dimensions.height,\n            radius,\n            depth,\n            strength,\n            chromaticAberration,\n        });\n        const backdropFilter = buildBackdropFilterValue({\n            filterUrl,\n            blur,\n            brightness: DEFAULT_GLASS_OPTIONS.brightness,\n            saturation: DEFAULT_GLASS_OPTIONS.saturation,\n        });\n        return {\n            ...base,\n            backdropFilter,\n            WebkitBackdropFilter: backdropFilter,\n        };\n    }, [dimensions, radius, depth, strength, chromaticAberration, blur, debug]);\n    return (<div ref={ref} className={className} style={style}>\n      {children}\n    </div>);\n}\nexport function GlassPanelCSS() {\n    return (<style>{`\n      .glass-panel {\n        background: rgba(255, 255, 255, 0.15);\n        box-shadow:\n          inset 0 0 0 1px rgba(255, 255, 255, 0.2),\n          inset 0 1px 0 rgba(255, 255, 255, 0.4),\n          0 4px 16px rgba(0, 0, 0, 0.1);\n      }\n      .glass-panel-dark {\n        background: rgba(0, 0, 0, 0.2);\n        box-shadow:\n          inset 0 0 0 1px rgba(255, 255, 255, 0.1),\n          inset 0 1px 0 rgba(255, 255, 255, 0.15),\n          0 4px 16px rgba(0, 0, 0, 0.2);\n      }\n    `}</style>);\n}\nexport interface GlassPanelUnderlayProps {\n    children: ReactNode;\n    className?: string;\n    depth?: number;\n    radius?: number;\n    strength?: number;\n    chromaticAberration?: number;\n    blur?: number;\n    disabled?: boolean;\n}\nexport function GlassPanelUnderlay({ children, className, depth = DEFAULT_GLASS_OPTIONS.depth, radius = DEFAULT_GLASS_OPTIONS.radius, strength = DEFAULT_GLASS_OPTIONS.strength, chromaticAberration = DEFAULT_GLASS_OPTIONS.chromaticAberration, blur = DEFAULT_GLASS_OPTIONS.blur, disabled = false, }: GlassPanelUnderlayProps) {\n    const ref = useRef<HTMLDivElement>(null);\n    const dimensions = useElementDimensions(ref);\n    const supported = useSupportsBackdropFilter();\n    const style = useMemo((): CSSProperties => {\n        const base: CSSProperties = { borderRadius: radius };\n        const canApply = !disabled &&\n            supported &&\n            dimensions &&\n            dimensions.width > 0 &&\n            dimensions.height > 0;\n        if (!canApply) {\n            return base;\n        }\n        const filterUrl = buildDisplacementFilterUrl({\n            width: dimensions.width,\n            height: dimensions.height,\n            radius,\n            depth,\n            strength,\n            chromaticAberration,\n        });\n        const backdropFilter = buildBackdropFilterValue({\n            filterUrl,\n            blur,\n            brightness: DEFAULT_GLASS_OPTIONS.brightness,\n            saturation: DEFAULT_GLASS_OPTIONS.saturation,\n        });\n        return {\n            ...base,\n            backdropFilter,\n            WebkitBackdropFilter: backdropFilter,\n        };\n    }, [\n        dimensions,\n        radius,\n        depth,\n        strength,\n        chromaticAberration,\n        blur,\n        disabled,\n        supported,\n    ]);\n    return (<div ref={ref} className={className} style={style}>\n      {children}\n    </div>);\n}\nexport interface UseGlassStylesOptions {\n    width: number;\n    height: number;\n    depth?: number;\n    radius?: number;\n    strength?: number;\n    chromaticAberration?: number;\n    blur?: number;\n    brightness?: number;\n    saturation?: number;\n    enabled?: boolean;\n}\nexport function useGlassStyles({ width, height, depth = DEFAULT_GLASS_OPTIONS.depth, radius = DEFAULT_GLASS_OPTIONS.radius, strength = DEFAULT_GLASS_OPTIONS.strength, chromaticAberration = DEFAULT_GLASS_OPTIONS.chromaticAberration, blur = DEFAULT_GLASS_OPTIONS.blur, brightness = DEFAULT_GLASS_OPTIONS.brightness, saturation = DEFAULT_GLASS_OPTIONS.saturation, enabled = true, }: UseGlassStylesOptions): CSSProperties {\n    const supported = useSupportsBackdropFilter();\n    return useMemo(() => {\n        if (!enabled || !supported || width <= 0 || height <= 0) {\n            return {};\n        }\n        const filterUrl = buildDisplacementFilterUrl({\n            width,\n            height,\n            radius,\n            depth,\n            strength,\n            chromaticAberration,\n        });\n        const backdropFilter = buildBackdropFilterValue({\n            filterUrl,\n            blur,\n            brightness,\n            saturation,\n        });\n        return {\n            backdropFilter,\n            WebkitBackdropFilter: backdropFilter,\n        };\n    }, [\n        width,\n        height,\n        depth,\n        radius,\n        strength,\n        chromaticAberration,\n        blur,\n        brightness,\n        saturation,\n        enabled,\n        supported,\n    ]);\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/generated/tuned-presets.generated.ts",
    "content": "// AUTO-GENERATED by `pnpm weather:compile`.\n// Source: lib/weather-authoring/presets/tuned-presets.json\n// DO NOT EDIT MANUALLY.\n\nimport type { WeatherConditionCode } from \"../../schema-runtime\";\nimport type { WeatherEffectsCheckpointOverrides } from \"../tuning\";\n\nexport const TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES: Partial<\n  Record<WeatherConditionCode, WeatherEffectsCheckpointOverrides>\n> = {\"clear\":{\"dawn\":{\"celestial\":{\"celestialX\":0.69,\"skyBrightness\":1,\"starDensity\":0.36,\"sunGlowIntensity\":3.47,\"sunGlowSize\":0.49,\"sunRayCount\":4,\"sunRayShimmer\":5,\"sunRayShimmerSpeed\":5},\"glass\":{\"blur\":4.5,\"brightness\":0.95}},\"dusk\":{\"celestial\":{\"celestialX\":0.69,\"skyBrightness\":1,\"starDensity\":0.36,\"sunGlowIntensity\":3.84,\"sunGlowSize\":0.43,\"sunRayCount\":4,\"sunRayIntensity\":0.24,\"sunRayLength\":1.37},\"glass\":{\"brightness\":0.9}},\"midnight\":{\"celestial\":{\"celestialY\":0.71,\"moonGlowIntensity\":3.23,\"moonPhase\":0.51,\"skyBrightness\":0.44,\"skyContrast\":1,\"skySaturation\":2,\"starDensity\":1.72,\"sunGlowIntensity\":1.59}},\"noon\":{\"celestial\":{\"celestialY\":0.71,\"moonPhase\":0.2421,\"skyBrightness\":0.8,\"skySaturation\":1.9,\"starDensity\":0.14,\"sunGlowIntensity\":1.72,\"sunGlowSize\":0.69,\"sunRayCount\":4,\"sunRayIntensity\":0.19,\"sunRayShimmer\":5,\"sunRayShimmerSpeed\":5},\"glass\":{\"brightness\":1}}},\"cloudy\":{\"dawn\":{\"celestial\":{\"celestialX\":0.69,\"celestialY\":0.74,\"skyBrightness\":1,\"skyContrast\":1.1,\"skySaturation\":0.88,\"starDensity\":0.36,\"sunGlowIntensity\":2.4,\"sunGlowSize\":0.38,\"sunRayShimmer\":5,\"sunRayShimmerSpeed\":5},\"cloud\":{\"ambientDarkness\":1,\"backlightIntensity\":1.07,\"cloudScale\":1.73,\"coverage\":0.81,\"density\":1.1,\"lightIntensity\":0.67,\"numLayers\":1,\"softness\":0.42,\"windSpeed\":0.04},\"glass\":{\"blur\":3,\"brightness\":0.95},\"post\":{\"bloomIntensity\":0.99,\"bloomKnee\":0.58,\"bloomRadius\":5.5,\"bloomTapScale\":1.39,\"bloomThreshold\":0.78,\"exposureIntensity\":1.14,\"exposureRecovery\":1.95,\"godRayDecay\":0.857,\"godRayDensity\":0.79,\"godRayIntensity\":1.08,\"godRaySamples\":121,\"godRayWeight\":0.42}},\"dusk\":{\"celestial\":{\"celestialX\":0.69,\"celestialY\":0.74,\"skyContrast\":0.99,\"skySaturation\":1.38,\"starDensity\":0.11,\"sunGlowIntensity\":3.21,\"sunGlowSize\":0.47,\"sunRayShimmer\":5,\"sunRayShimmerSpeed\":5},\"cloud\":{\"ambientDarkness\":0.54,\"backlightIntensity\":0.78,\"cloudScale\":1.73,\"coverage\":0.81,\"density\":0.9,\"lightIntensity\":0.78,\"numLayers\":1,\"softness\":0.42,\"windSpeed\":0.04},\"glass\":{\"brightness\":0.9},\"post\":{\"bloomIntensity\":1.11,\"bloomKnee\":0.58,\"bloomRadius\":5.5,\"bloomTapScale\":1.39,\"bloomThreshold\":0.78,\"exposureIntensity\":1.14,\"exposureRecovery\":1.95,\"godRayDecay\":0.857,\"godRayDensity\":0.79,\"godRayIntensity\":1.08,\"godRaySamples\":121,\"godRayWeight\":0.42}},\"midnight\":{\"celestial\":{\"celestialX\":0.74,\"celestialY\":0.71,\"moonGlowIntensity\":3.55,\"moonGlowSize\":0.97,\"starDensity\":1.72,\"sunRayShimmer\":5,\"sunRayShimmerSpeed\":5},\"cloud\":{\"cloudScale\":1.73,\"coverage\":0.81,\"density\":1.02,\"numLayers\":1,\"softness\":0.42,\"windSpeed\":0.04},\"post\":{\"bloomIntensity\":1.11,\"bloomKnee\":0.58,\"bloomRadius\":5.5,\"bloomTapScale\":1.39,\"bloomThreshold\":0.78,\"exposureIntensity\":1.14,\"exposureRecovery\":1.95,\"godRayDecay\":0.857,\"godRayDensity\":0.79,\"godRayIntensity\":1.08,\"godRaySamples\":121,\"godRayWeight\":0.42}},\"noon\":{\"celestial\":{\"celestialX\":0.74,\"celestialY\":0.71,\"moonPhase\":0.2421,\"skyBrightness\":0.84,\"skyContrast\":1.31,\"skySaturation\":1.44,\"starDensity\":0.14,\"sunGlowIntensity\":1.35,\"sunGlowSize\":0.61,\"sunRayCount\":4,\"sunRayIntensity\":0.22,\"sunRayShimmer\":5,\"sunRayShimmerSpeed\":5},\"cloud\":{\"ambientDarkness\":0,\"backlightIntensity\":0.45,\"cloudScale\":1.73,\"coverage\":0.81,\"density\":1.06,\"lightIntensity\":0.44,\"numLayers\":1,\"softness\":0.42,\"windSpeed\":0.04},\"glass\":{\"brightness\":1},\"post\":{\"bloomIntensity\":0.42,\"bloomKnee\":0.58,\"bloomRadius\":5.5,\"bloomTapScale\":1.39,\"bloomThreshold\":0.76,\"exposureIntensity\":1.14,\"exposureRecovery\":1.95,\"godRayDecay\":0.884,\"godRayDensity\":0.79,\"godRayIntensity\":1.33,\"godRaySamples\":121,\"godRayWeight\":0.42}}},\"drizzle\":{\"dawn\":{\"celestial\":{\"celestialX\":0.69,\"celestialY\":0.74,\"moonGlowIntensity\":3.55,\"moonGlowSize\":0.97,\"skyBrightness\":0.96,\"skyContrast\":0.84,\"skySaturation\":0.94,\"starDensity\":1.72},\"cloud\":{\"ambientDarkness\":0.7,\"backlightIntensity\":0.82,\"cloudScale\":1.21,\"coverage\":0.61,\"density\":0.98,\"lightIntensity\":0,\"numLayers\":1,\"softness\":0.17,\"turbulence\":0.16,\"windSpeed\":0.02},\"glass\":{\"brightness\":0.95},\"rain\":{\"fallingAngle\":0.02,\"fallingIntensity\":0.13,\"fallingSpeed\":2.24,\"fallingStreakLength\":0.32,\"glassIntensity\":0.29,\"glassZoom\":0.65}},\"dusk\":{\"celestial\":{\"celestialX\":0.69,\"celestialY\":0.74,\"moonGlowIntensity\":3.55,\"moonGlowSize\":0.97,\"skyBrightness\":0.77,\"skyContrast\":0.84,\"skySaturation\":0.85,\"starDensity\":1.72,\"sunGlowIntensity\":2.73,\"sunRayLength\":1.45},\"cloud\":{\"ambientDarkness\":0.7,\"backlightIntensity\":0.82,\"cloudScale\":1.21,\"coverage\":0.61,\"density\":0.98,\"lightIntensity\":0,\"numLayers\":1,\"softness\":0.17,\"turbulence\":0.16,\"windSpeed\":0.02},\"glass\":{\"brightness\":0.9},\"rain\":{\"fallingAngle\":0.02,\"fallingIntensity\":0.13,\"fallingSpeed\":2.24,\"fallingStreakLength\":0.32,\"glassIntensity\":0.29,\"glassZoom\":0.65}},\"midnight\":{\"celestial\":{\"celestialX\":0.74,\"celestialY\":0.71,\"moonGlowIntensity\":3.55,\"moonGlowSize\":0.97,\"skyBrightness\":0.77,\"skyContrast\":0.84,\"skySaturation\":0.85,\"starDensity\":1.72,\"sunGlowIntensity\":2.73,\"sunRayLength\":1.45},\"cloud\":{\"ambientDarkness\":0.7,\"backlightIntensity\":0.82,\"cloudScale\":1.21,\"coverage\":0.61,\"density\":0.98,\"lightIntensity\":0,\"numLayers\":1,\"softness\":0.17,\"turbulence\":0.16,\"windSpeed\":0.02},\"rain\":{\"fallingAngle\":0.02,\"fallingIntensity\":0.13,\"fallingSpeed\":2.24,\"fallingStreakLength\":0.32,\"glassIntensity\":0.2,\"glassZoom\":0.5}},\"noon\":{\"celestial\":{\"celestialX\":0.74,\"celestialY\":0.71,\"moonGlowIntensity\":3.55,\"moonGlowSize\":0.97,\"skyBrightness\":0.77,\"skyContrast\":0.84,\"skySaturation\":0.85,\"starDensity\":1.72,\"sunGlowIntensity\":2.73,\"sunRayLength\":1.45},\"cloud\":{\"ambientDarkness\":0.7,\"backlightIntensity\":0.82,\"cloudScale\":1.21,\"coverage\":0.61,\"density\":0.98,\"lightIntensity\":0,\"numLayers\":1,\"softness\":0.17,\"turbulence\":0.16,\"windSpeed\":0.02},\"glass\":{\"brightness\":1},\"rain\":{\"fallingAngle\":0.02,\"fallingIntensity\":0.13,\"fallingSpeed\":2.24,\"fallingStreakLength\":0.32,\"glassIntensity\":0.29,\"glassZoom\":0.65}}},\"fog\":{\"dawn\":{\"celestial\":{\"celestialY\":0.71,\"skyBrightness\":1.05,\"skyContrast\":0.41,\"skySaturation\":1.25,\"starDensity\":0.11,\"sunGlowIntensity\":2.42,\"sunGlowSize\":0.38,\"sunRayIntensity\":0.05,\"sunRayLength\":1.59},\"cloud\":{\"ambientDarkness\":1,\"backlightIntensity\":0.54,\"cloudScale\":0.64,\"density\":1.5,\"numLayers\":6,\"softness\":0.77,\"turbulence\":0.67,\"windSpeed\":0.02},\"glass\":{\"brightness\":0.95}},\"dusk\":{\"celestial\":{\"celestialY\":0.71,\"skyBrightness\":1.02,\"skyContrast\":0.62,\"skySaturation\":0.88,\"starDensity\":0.11,\"sunGlowIntensity\":2.42,\"sunGlowSize\":0.38,\"sunRayIntensity\":0.05,\"sunRayLength\":1.59},\"cloud\":{\"ambientDarkness\":0,\"backlightIntensity\":0.54,\"cloudScale\":0.93,\"numLayers\":6,\"softness\":0.5,\"turbulence\":1,\"windSpeed\":0.03},\"glass\":{\"brightness\":0.9}},\"midnight\":{\"celestial\":{\"celestialY\":0.71,\"moonGlowIntensity\":2.74,\"moonGlowSize\":1.43,\"moonPhase\":0.5,\"skyBrightness\":1.02,\"skyContrast\":0.62,\"skySaturation\":0.88,\"starDensity\":0.11,\"sunGlowIntensity\":2.42,\"sunGlowSize\":0.38,\"sunRayIntensity\":0.05,\"sunRayLength\":1.59},\"cloud\":{\"ambientDarkness\":0,\"backlightIntensity\":1.4,\"cloudScale\":0.91,\"density\":1.27,\"lightIntensity\":2,\"numLayers\":6,\"softness\":0.63,\"turbulence\":0.86,\"windSpeed\":0.03}},\"noon\":{\"celestial\":{\"celestialY\":0.71,\"skyBrightness\":0.76,\"skyContrast\":0.47,\"skySaturation\":0.6,\"starDensity\":0.11,\"sunGlowIntensity\":0.92,\"sunGlowSize\":0.84,\"sunRayCount\":0,\"sunRayIntensity\":0,\"sunRayLength\":0},\"cloud\":{\"ambientDarkness\":1,\"backlightIntensity\":0.72,\"cloudScale\":0.83,\"density\":1.5,\"numLayers\":6,\"softness\":0.84,\"turbulence\":0.86,\"windSpeed\":0.03},\"glass\":{\"brightness\":1}}},\"hail\":{\"dawn\":{\"celestial\":{\"celestialX\":0.69,\"celestialY\":0.74,\"skyBrightness\":0.42,\"skySaturation\":0.74,\"starDensity\":0.36,\"sunGlowIntensity\":5,\"sunGlowSize\":0.36,\"sunRayShimmer\":5,\"sunRayShimmerSpeed\":5},\"cloud\":{\"ambientDarkness\":0.75,\"backlightIntensity\":0.64,\"density\":0.83,\"lightIntensity\":0.55,\"numLayers\":1,\"turbulence\":0.55,\"windAngle\":-3.14,\"windSpeed\":0.14},\"glass\":{\"brightness\":0.95},\"interactions\":{\"rainRefractionStrength\":0.96},\"post\":{\"bloomIntensity\":0.13,\"bloomThreshold\":0.64,\"godRayIntensity\":0.56,\"haze\":0.11,\"hazeHorizon\":1},\"rain\":{\"fallingIntensity\":0.42,\"fallingLayers\":1,\"fallingSpeed\":3,\"fallingStreakLength\":0.56,\"glassIntensity\":1,\"glassZoom\":0.69},\"snow\":{\"drift\":0.63,\"fallSpeed\":8,\"flakeSize\":0.77,\"flutter\":0.79,\"glowAmount\":0.29,\"intensity\":0.81,\"layers\":8,\"opacity\":1,\"sizeVariation\":0.48,\"sparkle\":0.44,\"turbulence\":0.91,\"windAngle\":2.23,\"windShear\":0.78,\"windSpeed\":0.99}},\"dusk\":{\"celestial\":{\"celestialX\":0.69,\"celestialY\":0.74,\"skyBrightness\":0.47,\"skyContrast\":1.39,\"skySaturation\":1.42,\"starDensity\":0.11,\"sunGlowIntensity\":3.21,\"sunGlowSize\":0.51,\"sunRayShimmer\":5,\"sunRayShimmerSpeed\":5},\"cloud\":{\"ambientDarkness\":0.75,\"backlightIntensity\":0.64,\"density\":0.83,\"lightIntensity\":0.79,\"numLayers\":1,\"turbulence\":0.55,\"windAngle\":-3.14,\"windSpeed\":0.14},\"glass\":{\"brightness\":0.9},\"interactions\":{\"rainRefractionStrength\":0.96},\"post\":{\"bloomIntensity\":0.13,\"bloomThreshold\":0.64,\"godRayIntensity\":0.56,\"haze\":0.15,\"hazeHorizon\":1},\"rain\":{\"fallingIntensity\":0.42,\"fallingLayers\":1,\"fallingSpeed\":3,\"fallingStreakLength\":0.56,\"glassIntensity\":0.72,\"glassZoom\":0.69},\"snow\":{\"drift\":0.63,\"fallSpeed\":8,\"flakeSize\":0.77,\"flutter\":0.79,\"glowAmount\":0.71,\"intensity\":0.47,\"layers\":8,\"opacity\":1,\"sizeVariation\":0.77,\"sparkle\":0.1,\"turbulence\":0.91,\"windAngle\":2.23,\"windShear\":0.78,\"windSpeed\":1}},\"midnight\":{\"celestial\":{\"celestialX\":0.74,\"celestialY\":0.71,\"moonGlowIntensity\":3.55,\"moonGlowSize\":0.97,\"skyBrightness\":0.42,\"skySaturation\":0.74,\"starDensity\":1.72,\"sunRayShimmer\":5,\"sunRayShimmerSpeed\":5},\"cloud\":{\"ambientDarkness\":0.75,\"backlightIntensity\":0.64,\"density\":0.83,\"lightIntensity\":1.21,\"numLayers\":1,\"turbulence\":0.55,\"windAngle\":-3.14,\"windSpeed\":0.14},\"interactions\":{\"rainRefractionStrength\":0.96},\"post\":{\"bloomIntensity\":0.13,\"bloomThreshold\":0.64,\"godRayIntensity\":0.56,\"haze\":0.14,\"hazeHorizon\":1},\"rain\":{\"fallingIntensity\":0.42,\"fallingLayers\":1,\"fallingSpeed\":3,\"fallingStreakLength\":0.56,\"glassIntensity\":0.72,\"glassZoom\":0.69},\"snow\":{\"drift\":0.63,\"fallSpeed\":8,\"flakeSize\":0.77,\"flutter\":0.79,\"glowAmount\":0.71,\"intensity\":0.47,\"layers\":8,\"opacity\":1,\"sizeVariation\":0.85,\"sparkle\":0.1,\"turbulence\":0.87,\"windAngle\":2.23,\"windShear\":0.78,\"windSpeed\":1.11}},\"noon\":{\"celestial\":{\"celestialX\":0.74,\"celestialY\":0.71,\"moonPhase\":0.2421,\"skyBrightness\":0.56,\"skyContrast\":1.03,\"skySaturation\":0.62,\"starDensity\":0.14,\"sunGlowIntensity\":2.5,\"sunGlowSize\":0.54,\"sunRayCount\":3,\"sunRayShimmer\":5,\"sunRayShimmerSpeed\":5},\"cloud\":{\"ambientDarkness\":1,\"backlightIntensity\":0.21,\"coverage\":0.74,\"density\":0.95,\"lightIntensity\":0.6,\"numLayers\":1,\"softness\":0.36,\"turbulence\":0.55,\"windAngle\":-3.14,\"windSpeed\":0.14},\"glass\":{\"brightness\":1},\"interactions\":{\"rainRefractionStrength\":0.96},\"post\":{\"bloomIntensity\":0.13,\"bloomThreshold\":0.64,\"godRayIntensity\":0.56,\"haze\":0.23,\"hazeHorizon\":1},\"rain\":{\"fallingIntensity\":0.42,\"fallingLayers\":1,\"fallingSpeed\":3,\"fallingStreakLength\":0.56,\"glassIntensity\":0.72,\"glassZoom\":0.69},\"snow\":{\"drift\":0.63,\"fallSpeed\":8,\"flakeSize\":0.77,\"flutter\":0.79,\"glowAmount\":0.71,\"intensity\":0.8,\"layers\":8,\"opacity\":1,\"sizeVariation\":0.77,\"sparkle\":0.1,\"turbulence\":0.91,\"windAngle\":2.23,\"windShear\":0.78,\"windSpeed\":0.89}}},\"heavy-rain\":{\"dawn\":{\"celestial\":{\"celestialX\":0.69,\"celestialY\":0.74,\"moonGlowIntensity\":3.55,\"moonGlowSize\":0.97,\"moonPhase\":0.3776,\"skyBrightness\":0.87,\"skySaturation\":0.56,\"starDensity\":1.72,\"sunGlowIntensity\":3.75,\"sunGlowSize\":0.43},\"cloud\":{\"ambientDarkness\":1,\"backlightIntensity\":1.35,\"cloudScale\":1.52,\"coverage\":0.53,\"density\":1.17,\"lightIntensity\":0.06,\"numLayers\":1,\"softness\":0.25,\"turbulence\":0.45,\"windSpeed\":0.05},\"glass\":{\"brightness\":0.95},\"interactions\":{\"rainRefractionStrength\":1},\"post\":{\"bloomIntensity\":0.36,\"godRayDecay\":0.838,\"godRayDensity\":0.56,\"godRayIntensity\":1.11,\"godRaySamples\":35,\"godRayWeight\":0.66,\"haze\":0.14,\"hazeHorizon\":1},\"rain\":{\"fallingAngle\":0.3,\"fallingLayers\":6,\"fallingSpeed\":3,\"fallingStreakLength\":2,\"glassIntensity\":0.98,\"glassZoom\":0.58}},\"dusk\":{\"celestial\":{\"celestialX\":0.69,\"celestialY\":0.74,\"moonGlowIntensity\":3.55,\"moonGlowSize\":0.97,\"moonPhase\":0.4115,\"moonSize\":0.17,\"skyBrightness\":0.96,\"skyContrast\":0.99,\"skySaturation\":0.72,\"starDensity\":1.72,\"sunGlowIntensity\":4.15,\"sunGlowSize\":0.35,\"sunRayCount\":6,\"sunRayIntensity\":0.1,\"sunRayLength\":3,\"sunSize\":0.14},\"cloud\":{\"ambientDarkness\":1,\"backlightIntensity\":1.8,\"cloudScale\":2.02,\"coverage\":0.72,\"density\":0.94,\"lightIntensity\":0,\"numLayers\":1,\"softness\":0.17,\"turbulence\":0.51,\"windAngle\":0,\"windSpeed\":0.05},\"glass\":{\"brightness\":0.9},\"interactions\":{\"rainRefractionStrength\":1},\"post\":{\"bloomIntensity\":0.28,\"godRayWeight\":0.55,\"haze\":0.21,\"hazeHorizon\":1},\"rain\":{\"fallingAngle\":0.3,\"fallingIntensity\":1,\"fallingLayers\":6,\"fallingSpeed\":3,\"fallingStreakLength\":2,\"glassIntensity\":0.98,\"glassZoom\":0.58}},\"midnight\":{\"celestial\":{\"celestialX\":0.74,\"celestialY\":0.71,\"moonGlowIntensity\":3.55,\"moonGlowSize\":0.97,\"moonPhase\":0.3776,\"moonSize\":0.17,\"skyBrightness\":1.11,\"skyContrast\":1,\"skySaturation\":0.78,\"starDensity\":1.72,\"sunGlowIntensity\":3.05,\"sunGlowSize\":0.3,\"sunRayCount\":6,\"sunRayIntensity\":0.1,\"sunRayLength\":3,\"sunSize\":0.14},\"cloud\":{\"ambientDarkness\":1,\"backlightIntensity\":0.81,\"cloudScale\":2.16,\"coverage\":0.72,\"density\":1.5,\"lightIntensity\":0,\"numLayers\":1,\"softness\":0.37,\"turbulence\":0.55,\"windAngle\":0,\"windSpeed\":0.05},\"glass\":{\"blur\":1,\"brightness\":1.15},\"interactions\":{\"rainRefractionStrength\":1},\"rain\":{\"fallingAngle\":0.3,\"fallingIntensity\":1,\"fallingLayers\":6,\"fallingSpeed\":3,\"fallingStreakLength\":2,\"glassIntensity\":0.98,\"glassZoom\":0.58}},\"noon\":{\"celestial\":{\"celestialY\":0.71,\"moonGlowIntensity\":3.55,\"moonGlowSize\":0.97,\"moonPhase\":0.3776,\"skyBrightness\":0.81,\"skyContrast\":0.54,\"skySaturation\":0.36,\"starDensity\":1.68,\"sunGlowIntensity\":3.7,\"sunGlowSize\":0.22,\"sunRayCount\":0,\"sunRayIntensity\":0,\"sunRayLength\":0},\"cloud\":{\"ambientDarkness\":1,\"backlightIntensity\":0.59,\"cloudScale\":1.8,\"coverage\":0.58,\"density\":0.87,\"lightIntensity\":0,\"numLayers\":1,\"softness\":0.2,\"turbulence\":0.24,\"windSpeed\":0.05},\"glass\":{\"brightness\":1},\"interactions\":{\"rainRefractionStrength\":1},\"post\":{\"godRayIntensity\":1.24,\"godRaySamples\":0,\"godRayWeight\":0.6},\"rain\":{\"fallingAngle\":0.3,\"fallingSpeed\":3,\"glassIntensity\":0.98,\"glassZoom\":0.58}}},\"overcast\":{\"dawn\":{\"celestial\":{\"celestialY\":0.71,\"moonGlowIntensity\":1.91,\"moonGlowSize\":0.32,\"moonPhase\":0.5,\"skyBrightness\":1.36,\"skyContrast\":0.73,\"skySaturation\":1.6,\"starDensity\":0.11,\"sunGlowIntensity\":5.1,\"sunGlowSize\":0.27,\"sunRayCount\":6,\"sunRayIntensity\":0.05,\"sunRayLength\":0.6},\"cloud\":{\"ambientDarkness\":0,\"backlightIntensity\":1.41,\"cloudScale\":1.9,\"density\":0.3,\"lightIntensity\":0.75,\"numLayers\":8,\"softness\":0.77,\"turbulence\":0.66,\"windSpeed\":0.04},\"glass\":{\"brightness\":0.95},\"post\":{\"bloomThreshold\":0.81,\"godRayIntensity\":0.32,\"godRayWeight\":0.3,\"haze\":0.15,\"hazeContrast\":0,\"hazeHorizon\":1}},\"dusk\":{\"celestial\":{\"celestialY\":0.71,\"moonGlowIntensity\":1.91,\"moonGlowSize\":0.32,\"moonPhase\":0.5,\"skyBrightness\":1.12,\"skyContrast\":0.83,\"skySaturation\":2,\"starDensity\":0.11,\"sunGlowIntensity\":4.15,\"sunGlowSize\":0.28,\"sunRayCount\":6,\"sunRayIntensity\":0.05,\"sunRayLength\":0.6},\"cloud\":{\"ambientDarkness\":0,\"backlightIntensity\":1.41,\"cloudScale\":1.9,\"density\":0.3,\"lightIntensity\":0.78,\"numLayers\":8,\"softness\":0.77,\"turbulence\":0.66,\"windSpeed\":0.04},\"glass\":{\"brightness\":0.9},\"post\":{\"bloomThreshold\":0.81,\"godRayIntensity\":0.32,\"godRayWeight\":0.3,\"haze\":0.15,\"hazeContrast\":0,\"hazeHorizon\":1}},\"midnight\":{\"celestial\":{\"celestialY\":0.71,\"moonGlowIntensity\":2.75,\"moonGlowSize\":1.54,\"moonPhase\":0.5,\"skyBrightness\":1.58,\"skyContrast\":0.84,\"skySaturation\":1.15,\"starDensity\":0.11,\"sunGlowIntensity\":2.42,\"sunGlowSize\":0.38,\"sunRayCount\":6,\"sunRayIntensity\":0.05,\"sunRayLength\":1.59},\"cloud\":{\"ambientDarkness\":0,\"backlightIntensity\":1.41,\"cloudScale\":1.9,\"coverage\":0.9,\"density\":1.16,\"lightIntensity\":0,\"numLayers\":8,\"softness\":0.87,\"turbulence\":0.66,\"windSpeed\":0.04},\"post\":{\"bloomThreshold\":0.81,\"godRayIntensity\":0.02,\"godRayWeight\":0.15,\"haze\":0.35,\"hazeContrast\":0,\"hazeHorizon\":0.96}},\"noon\":{\"celestial\":{\"celestialY\":0.71,\"moonGlowIntensity\":1.91,\"moonGlowSize\":0.32,\"moonPhase\":0.5,\"skyBrightness\":1.16,\"skyContrast\":0.53,\"skySaturation\":0.88,\"starDensity\":0.11,\"sunGlowIntensity\":5.2,\"sunGlowSize\":0.38,\"sunRayCount\":6,\"sunRayIntensity\":0.05,\"sunRayLength\":1.59},\"cloud\":{\"ambientDarkness\":0,\"backlightIntensity\":0.9,\"cloudScale\":1.9,\"coverage\":0.95,\"density\":0.3,\"lightIntensity\":0.08,\"numLayers\":8,\"softness\":0.77,\"turbulence\":0.66,\"windSpeed\":0.04},\"glass\":{\"brightness\":1},\"post\":{\"bloomThreshold\":0.81,\"godRayIntensity\":0.12,\"godRayWeight\":0.2,\"haze\":0.15,\"hazeContrast\":0,\"hazeHorizon\":1}}},\"partly-cloudy\":{\"dawn\":{\"celestial\":{\"celestialY\":0.71,\"moonGlowIntensity\":3.55,\"moonGlowSize\":0.97,\"moonPhase\":0.5,\"starDensity\":1.72,\"sunGlowIntensity\":3.89,\"sunGlowSize\":0.38,\"sunRayCount\":4,\"sunRayIntensity\":0.17},\"cloud\":{\"ambientDarkness\":0.42,\"coverage\":0.37,\"density\":1.24,\"lightIntensity\":1.42,\"softness\":0.28,\"windSpeed\":0.01},\"glass\":{\"blur\":3,\"brightness\":0.95}},\"dusk\":{\"celestial\":{\"celestialY\":0.71,\"moonGlowIntensity\":3.55,\"moonGlowSize\":0.97,\"moonPhase\":0.5,\"starDensity\":1.72,\"sunGlowIntensity\":3.89,\"sunGlowSize\":0.38,\"sunRayCount\":4,\"sunRayIntensity\":0.17},\"cloud\":{\"ambientDarkness\":0.42,\"cloudScale\":1.68,\"coverage\":0.37,\"density\":1.24,\"lightIntensity\":1.42,\"softness\":0.26,\"windSpeed\":0.01},\"glass\":{\"brightness\":0.9}},\"midnight\":{\"celestial\":{\"celestialY\":0.71,\"moonGlowIntensity\":3.55,\"moonGlowSize\":0.97,\"moonPhase\":0.5,\"starDensity\":1.72},\"cloud\":{\"ambientDarkness\":0.42,\"backlightIntensity\":1.23,\"cloudScale\":1.11,\"coverage\":0.37,\"density\":1.24,\"lightIntensity\":1.42,\"numLayers\":5,\"softness\":0.21,\"turbulence\":0.18,\"windSpeed\":0.01}},\"noon\":{\"celestial\":{\"celestialY\":0.71,\"moonGlowIntensity\":3.55,\"moonGlowSize\":0.97,\"moonPhase\":0.5,\"starDensity\":1.72},\"cloud\":{\"ambientDarkness\":0.42,\"backlightIntensity\":0,\"cloudScale\":1.86,\"coverage\":0.48,\"density\":1.71,\"lightIntensity\":0.67,\"numLayers\":2,\"softness\":0.41,\"windSpeed\":0.01},\"glass\":{\"brightness\":1}}},\"rain\":{\"dawn\":{\"celestial\":{\"celestialX\":0.69,\"celestialY\":0.74,\"moonGlowIntensity\":3.55,\"moonGlowSize\":0.97,\"skyBrightness\":0.96,\"skyContrast\":0.84,\"skySaturation\":0.94,\"starDensity\":1.72},\"cloud\":{\"ambientDarkness\":0.7,\"backlightIntensity\":0.82,\"cloudScale\":1.13,\"coverage\":0.72,\"density\":0.94,\"lightIntensity\":0,\"numLayers\":1,\"softness\":0.17,\"turbulence\":0.34,\"windSpeed\":0.05},\"glass\":{\"brightness\":0.95},\"interactions\":{\"rainRefractionStrength\":0.76},\"rain\":{\"fallingAngle\":0,\"fallingIntensity\":0.84,\"fallingLayers\":4,\"fallingSpeed\":3,\"fallingStreakLength\":0.54,\"glassIntensity\":0.38,\"glassZoom\":0.5}},\"dusk\":{\"celestial\":{\"celestialX\":0.69,\"celestialY\":0.74,\"moonGlowIntensity\":3.55,\"moonGlowSize\":0.97,\"moonPhase\":0.4115,\"moonSize\":0.17,\"skyBrightness\":0.96,\"skyContrast\":0.84,\"skySaturation\":0.94,\"starDensity\":1.72,\"sunGlowIntensity\":3.05,\"sunGlowSize\":0.3,\"sunRayCount\":6,\"sunRayIntensity\":0.1,\"sunRayLength\":3,\"sunSize\":0.14},\"cloud\":{\"ambientDarkness\":1,\"backlightIntensity\":0.82,\"cloudScale\":1.13,\"coverage\":0.72,\"density\":0.94,\"lightIntensity\":0,\"numLayers\":1,\"softness\":0.17,\"turbulence\":0.34,\"windAngle\":0,\"windSpeed\":0.05},\"glass\":{\"brightness\":0.9},\"interactions\":{\"rainRefractionStrength\":0.76},\"lightning\":{\"enabled\":true},\"rain\":{\"fallingAngle\":0,\"fallingIntensity\":0.84,\"fallingLayers\":5,\"fallingSpeed\":3,\"fallingStreakLength\":0.54,\"glassIntensity\":0.38,\"glassZoom\":0.5}},\"midnight\":{\"celestial\":{\"celestialX\":0.74,\"celestialY\":0.71,\"moonGlowIntensity\":3.55,\"moonGlowSize\":0.97,\"moonPhase\":0.3776,\"moonSize\":0.17,\"skyBrightness\":1.11,\"skyContrast\":1,\"skySaturation\":0.78,\"starDensity\":1.72,\"sunGlowIntensity\":3.05,\"sunGlowSize\":0.3,\"sunRayCount\":6,\"sunRayIntensity\":0.1,\"sunRayLength\":3,\"sunSize\":0.14},\"cloud\":{\"ambientDarkness\":1,\"backlightIntensity\":0.77,\"cloudScale\":1.13,\"coverage\":0.72,\"density\":1,\"lightIntensity\":0,\"numLayers\":1,\"softness\":0.23,\"turbulence\":0.34,\"windAngle\":0,\"windSpeed\":0.05},\"interactions\":{\"rainRefractionStrength\":0.76},\"rain\":{\"fallingAngle\":0,\"fallingIntensity\":0.84,\"fallingLayers\":5,\"fallingSpeed\":3,\"fallingStreakLength\":0.54,\"glassIntensity\":0.38,\"glassZoom\":0.5}},\"noon\":{\"celestial\":{\"celestialX\":0.74,\"celestialY\":0.71,\"moonGlowIntensity\":3.55,\"moonGlowSize\":0.97,\"moonPhase\":0.3776,\"moonSize\":0.17,\"skyBrightness\":1.12,\"skyContrast\":0.49,\"skySaturation\":0.59,\"starDensity\":1.72,\"sunGlowIntensity\":3.05,\"sunGlowSize\":0.22,\"sunRayCount\":0,\"sunRayIntensity\":0,\"sunRayLength\":0,\"sunSize\":0.14},\"cloud\":{\"ambientDarkness\":1,\"backlightIntensity\":0.59,\"cloudScale\":1.13,\"coverage\":0.79,\"density\":1.08,\"lightIntensity\":0,\"numLayers\":1,\"softness\":0.23,\"turbulence\":0.34,\"windAngle\":0,\"windSpeed\":0.05},\"glass\":{\"brightness\":1},\"interactions\":{\"rainRefractionStrength\":0.76},\"lightning\":{\"enabled\":true},\"rain\":{\"fallingAngle\":0,\"fallingIntensity\":0.84,\"fallingLayers\":5,\"fallingSpeed\":3,\"fallingStreakLength\":0.54,\"glassIntensity\":0.38,\"glassZoom\":0.5}}},\"sleet\":{\"dawn\":{\"celestial\":{\"celestialX\":0.69,\"celestialY\":0.74,\"skyBrightness\":0.82,\"starDensity\":0.36,\"sunGlowIntensity\":5},\"cloud\":{\"ambientDarkness\":1,\"backlightIntensity\":1.27,\"turbulence\":0.56,\"windSpeed\":0.03},\"glass\":{\"brightness\":0.95},\"rain\":{\"fallingAngle\":0.31,\"fallingIntensity\":0.22,\"fallingSpeed\":2.07,\"fallingStreakLength\":2,\"glassIntensity\":0.41,\"glassZoom\":0.85},\"snow\":{\"drift\":1,\"fallSpeed\":8,\"flakeSize\":0.85,\"flutter\":0.98,\"glowAmount\":0.09,\"intensity\":0.34,\"layers\":8,\"opacity\":0.78,\"sizeVariation\":1,\"sparkle\":0.48,\"turbulence\":0.92,\"windAngle\":1.76,\"windShear\":1,\"windSpeed\":1.8}},\"dusk\":{\"celestial\":{\"celestialX\":0.69,\"celestialY\":0.74,\"starDensity\":0.36,\"sunGlowIntensity\":5},\"cloud\":{\"turbulence\":0.56,\"windSpeed\":0.03},\"glass\":{\"brightness\":0.9},\"lightning\":{\"enabled\":true},\"rain\":{\"fallingAngle\":0.31,\"fallingIntensity\":0.22,\"fallingSpeed\":2.07,\"fallingStreakLength\":2,\"glassIntensity\":0.41,\"glassZoom\":0.85},\"snow\":{\"drift\":1,\"fallSpeed\":8,\"flakeSize\":0.85,\"flutter\":0.98,\"glowAmount\":0.09,\"intensity\":0.34,\"layers\":8,\"opacity\":0.78,\"sizeVariation\":1,\"sparkle\":0.48,\"windAngle\":1.76,\"windShear\":1,\"windSpeed\":1.8}},\"midnight\":{\"celestial\":{\"celestialX\":0.69,\"celestialY\":0.74,\"starDensity\":0.36,\"sunGlowIntensity\":5},\"cloud\":{\"turbulence\":0.56,\"windSpeed\":0.03},\"lightning\":{\"enabled\":true},\"rain\":{\"fallingAngle\":0.31,\"fallingIntensity\":0.22,\"fallingSpeed\":2.07,\"fallingStreakLength\":2,\"glassIntensity\":0.41,\"glassZoom\":0.85},\"snow\":{\"drift\":1,\"fallSpeed\":8,\"flakeSize\":0.85,\"flutter\":0.98,\"glowAmount\":0.09,\"intensity\":0.34,\"layers\":8,\"opacity\":0.78,\"sizeVariation\":1,\"sparkle\":0.48,\"windAngle\":1.76,\"windShear\":1,\"windSpeed\":1.8}},\"noon\":{\"celestial\":{\"celestialX\":0.69,\"celestialY\":0.74,\"starDensity\":0.36,\"sunGlowIntensity\":5},\"cloud\":{\"turbulence\":0.56,\"windSpeed\":0.03},\"glass\":{\"brightness\":1},\"lightning\":{\"enabled\":true},\"rain\":{\"fallingAngle\":0.31,\"fallingIntensity\":0.22,\"fallingSpeed\":2.07,\"fallingStreakLength\":2,\"glassIntensity\":0.41,\"glassZoom\":0.85},\"snow\":{\"drift\":1,\"fallSpeed\":8,\"flakeSize\":0.85,\"flutter\":0.98,\"glowAmount\":0.09,\"intensity\":0.34,\"layers\":8,\"opacity\":0.78,\"sizeVariation\":1,\"sparkle\":0.48,\"turbulence\":1,\"windAngle\":1.76,\"windShear\":1,\"windSpeed\":1.8}}},\"snow\":{\"dawn\":{\"celestial\":{\"celestialY\":0.71,\"moonGlowIntensity\":3.55,\"moonGlowSize\":0.97,\"skyBrightness\":0.6,\"skyContrast\":0,\"skySaturation\":1.5,\"starDensity\":1.72},\"cloud\":{\"ambientDarkness\":0.94,\"backlightIntensity\":1.14,\"density\":1.19,\"lightIntensity\":1.73,\"turbulence\":0.3,\"windSpeed\":0.04},\"glass\":{\"brightness\":0.95,\"chromaticAberration\":2,\"depth\":8,\"strength\":10},\"post\":{\"godRayIntensity\":0.4,\"haze\":0.03,\"hazeContrast\":0.05,\"hazeDesaturation\":0,\"hazeHorizon\":1},\"snow\":{\"drift\":1,\"fallSpeed\":1.5,\"flakeSize\":1.05,\"flutter\":1,\"glowAmount\":0.44,\"intensity\":0.53,\"layers\":8,\"opacity\":0.4,\"sizeVariation\":0.63,\"sparkle\":0.97,\"turbulence\":0.59,\"windAngle\":1.95,\"windShear\":0.8,\"windSpeed\":2}},\"dusk\":{\"celestial\":{\"celestialY\":0.71,\"moonGlowIntensity\":3.55,\"moonGlowSize\":0.97,\"skyBrightness\":1.01,\"skyContrast\":0.86,\"skySaturation\":2,\"starDensity\":1.72,\"sunGlowIntensity\":4.9},\"cloud\":{\"ambientDarkness\":0.9,\"backlightIntensity\":1.53,\"cloudScale\":1.82,\"coverage\":0.62,\"density\":1.19,\"lightIntensity\":0.55,\"softness\":0.31,\"turbulence\":0.3,\"windSpeed\":0.04},\"glass\":{\"blur\":2,\"chromaticAberration\":2,\"depth\":8,\"strength\":10},\"post\":{\"godRayIntensity\":0.4,\"haze\":0.03,\"hazeContrast\":0.05,\"hazeDesaturation\":0,\"hazeHorizon\":1},\"snow\":{\"drift\":1,\"fallSpeed\":1.5,\"flakeSize\":1.05,\"flutter\":1,\"glowAmount\":0.88,\"intensity\":0.96,\"layers\":8,\"opacity\":0.34,\"sizeVariation\":0.63,\"sparkle\":0.97,\"turbulence\":0.59,\"windAngle\":1.95,\"windShear\":0.8,\"windSpeed\":2}},\"midnight\":{\"celestial\":{\"celestialY\":0.71,\"moonGlowIntensity\":3.55,\"moonGlowSize\":0.97,\"moonPhase\":0.5,\"skyBrightness\":1.07,\"skyContrast\":0.59,\"skySaturation\":1.57,\"starDensity\":1.72},\"cloud\":{\"ambientDarkness\":0.04,\"cloudScale\":1.15,\"coverage\":0.5,\"density\":1.19,\"lightIntensity\":1.29,\"softness\":0.14,\"turbulence\":0.3,\"windAngle\":-0.08,\"windSpeed\":0.04},\"glass\":{\"blur\":1,\"chromaticAberration\":2,\"depth\":8,\"strength\":10},\"post\":{\"godRayIntensity\":0.4,\"haze\":0.03,\"hazeContrast\":0.05,\"hazeDesaturation\":0,\"hazeHorizon\":1},\"snow\":{\"drift\":1,\"fallSpeed\":1.5,\"flakeSize\":1.05,\"flutter\":1,\"glowAmount\":0.88,\"intensity\":0.53,\"layers\":8,\"opacity\":0.34,\"sizeVariation\":0.63,\"sparkle\":0.97,\"turbulence\":0.59,\"windAngle\":1.95,\"windShear\":0.8,\"windSpeed\":1.44}},\"noon\":{\"celestial\":{\"celestialY\":0.71,\"moonGlowIntensity\":3.55,\"moonGlowSize\":0.97,\"skyBrightness\":0.35,\"skyContrast\":0.69,\"skySaturation\":0.73,\"starDensity\":1.72,\"sunGlowIntensity\":3.45,\"sunGlowSize\":0.29,\"sunRayCount\":0},\"cloud\":{\"ambientDarkness\":1,\"backlightIntensity\":1.01,\"density\":1.19,\"lightIntensity\":0.57,\"turbulence\":0.3,\"windSpeed\":0.04},\"glass\":{\"blur\":1,\"brightness\":1.15,\"chromaticAberration\":2,\"depth\":8,\"saturation\":1.4,\"strength\":10},\"post\":{\"godRayIntensity\":0.4,\"haze\":0.03,\"hazeContrast\":0.05,\"hazeDesaturation\":0,\"hazeHorizon\":1},\"snow\":{\"drift\":1,\"fallSpeed\":1.5,\"flakeSize\":1.28,\"flutter\":1,\"glowAmount\":0.47,\"intensity\":0.53,\"layers\":8,\"opacity\":0.34,\"sizeVariation\":0.63,\"sparkle\":1,\"turbulence\":0.59,\"windAngle\":1.95,\"windShear\":0.8,\"windSpeed\":2}}},\"thunderstorm\":{\"dawn\":{\"celestial\":{\"celestialX\":0.69,\"celestialY\":0.74,\"moonGlowIntensity\":0,\"moonGlowSize\":0,\"skyBrightness\":0.78,\"skyContrast\":1.03,\"skySaturation\":0.94,\"starDensity\":1.72,\"sunGlowIntensity\":5.1,\"sunRayCount\":5,\"sunRayIntensity\":0.13,\"sunRayLength\":1.7,\"sunRayShimmer\":5,\"sunRayShimmerSpeed\":5},\"cloud\":{\"backlightIntensity\":0.55,\"cloudScale\":1.56,\"coverage\":0.71,\"density\":0.97,\"lightIntensity\":0,\"numLayers\":1,\"softness\":0.28,\"turbulence\":0.36,\"windSpeed\":0.04},\"glass\":{\"blur\":1,\"brightness\":0.9,\"chromaticAberration\":2,\"strength\":45},\"interactions\":{\"lightningSceneIllumination\":0.83,\"rainRefractionStrength\":2},\"lightning\":{\"autoInterval\":8,\"branchDensity\":0.83,\"enabled\":true,\"flashIntensity\":1.57},\"post\":{\"bloomIntensity\":0.39,\"exposureIntensity\":0.95,\"exposureRecovery\":4.5,\"godRayDecay\":0.894,\"godRayDensity\":0.91,\"godRayIntensity\":0.26,\"godRaySamples\":39,\"godRayWeight\":0.36,\"haze\":0.07,\"hazeContrast\":0,\"hazeHorizon\":1},\"rain\":{\"fallingAngle\":0.02,\"fallingLayers\":12,\"fallingSpeed\":3,\"fallingStreakLength\":1.46,\"glassIntensity\":1.71,\"glassZoom\":0.51}},\"dusk\":{\"celestial\":{\"celestialX\":0.69,\"celestialY\":0.74,\"moonGlowIntensity\":0,\"moonGlowSize\":0,\"skyBrightness\":0.91,\"skyContrast\":1.03,\"skySaturation\":1.19,\"starDensity\":1.72,\"sunGlowIntensity\":5.05,\"sunRayShimmer\":5,\"sunRayShimmerSpeed\":5},\"cloud\":{\"ambientDarkness\":1,\"backlightIntensity\":0.55,\"cloudScale\":1.56,\"coverage\":0.71,\"density\":0.97,\"lightIntensity\":0,\"numLayers\":1,\"softness\":0.28,\"turbulence\":0.36,\"windSpeed\":0.04},\"glass\":{\"blur\":1,\"brightness\":0.9,\"chromaticAberration\":2,\"strength\":45},\"interactions\":{\"lightningSceneIllumination\":0.83,\"rainRefractionStrength\":2},\"lightning\":{\"branchDensity\":0.83,\"enabled\":true,\"flashIntensity\":1.57},\"post\":{\"bloomIntensity\":0.44,\"exposureIntensity\":1.1,\"exposureRecovery\":4.5,\"haze\":0.07,\"hazeContrast\":0,\"hazeHorizon\":1},\"rain\":{\"fallingAngle\":0.02,\"fallingLayers\":12,\"fallingSpeed\":3,\"fallingStreakLength\":1.46,\"glassIntensity\":1.32,\"glassZoom\":0.51}},\"midnight\":{\"celestial\":{\"celestialY\":0.71,\"moonGlowIntensity\":4.55,\"moonGlowSize\":1.25,\"moonPhase\":0.5,\"skyBrightness\":0.78,\"skyContrast\":1.03,\"skySaturation\":0.74,\"starDensity\":1.72,\"sunRayShimmer\":5,\"sunRayShimmerSpeed\":5},\"cloud\":{\"ambientDarkness\":0.88,\"backlightIntensity\":0.55,\"cloudScale\":1.56,\"coverage\":0.71,\"density\":0.97,\"lightIntensity\":0,\"numLayers\":1,\"softness\":0.28,\"turbulence\":0.36,\"windSpeed\":0.04},\"glass\":{\"blur\":1,\"brightness\":0.9,\"chromaticAberration\":2,\"strength\":45},\"interactions\":{\"lightningSceneIllumination\":0.83,\"rainRefractionStrength\":2},\"lightning\":{\"autoInterval\":8.5,\"branchDensity\":0.89,\"enabled\":true,\"flashIntensity\":1.57},\"post\":{\"bloomIntensity\":0.44,\"exposureIntensity\":1.1,\"exposureRecovery\":4.5,\"haze\":0.07,\"hazeContrast\":0,\"hazeHorizon\":1},\"rain\":{\"fallingAngle\":0.02,\"fallingLayers\":12,\"fallingSpeed\":3,\"fallingStreakLength\":1.46,\"glassIntensity\":1.32,\"glassZoom\":0.51}},\"noon\":{\"celestial\":{\"celestialY\":0.71,\"moonGlowIntensity\":0,\"moonGlowSize\":0,\"skyBrightness\":0.74,\"skyContrast\":1.07,\"skySaturation\":0.55,\"starDensity\":1.72,\"sunGlowIntensity\":2.7,\"sunGlowSize\":0.51,\"sunRayCount\":0,\"sunRayIntensity\":0,\"sunRayLength\":0,\"sunRayShimmer\":5,\"sunRayShimmerSpeed\":5},\"cloud\":{\"ambientDarkness\":1,\"backlightIntensity\":0.55,\"cloudScale\":1.56,\"coverage\":0.71,\"density\":0.97,\"lightIntensity\":0,\"numLayers\":1,\"softness\":0.28,\"turbulence\":0.36,\"windSpeed\":0.04},\"glass\":{\"blur\":1,\"brightness\":0.9,\"chromaticAberration\":2,\"strength\":45},\"interactions\":{\"lightningSceneIllumination\":0.83,\"rainRefractionStrength\":2},\"lightning\":{\"enabled\":true,\"flashIntensity\":1.57},\"post\":{\"bloomIntensity\":0.44,\"exposureIntensity\":1.1,\"exposureRecovery\":4.5,\"haze\":0.07,\"hazeContrast\":0,\"hazeHorizon\":1},\"rain\":{\"fallingAngle\":0.02,\"fallingLayers\":12,\"fallingSpeed\":3,\"fallingStreakLength\":1.46,\"glassIntensity\":1.32,\"glassZoom\":0.51}}},\"windy\":{\"dawn\":{\"celestial\":{\"celestialX\":0.69,\"celestialY\":0.74,\"skyBrightness\":1,\"starDensity\":0.36,\"sunGlowIntensity\":5},\"cloud\":{\"cloudScale\":0.85,\"coverage\":0.4,\"windSpeed\":0.17},\"glass\":{\"brightness\":0.95}},\"dusk\":{\"celestial\":{\"celestialX\":0.69,\"celestialY\":0.74,\"starDensity\":0.11,\"sunGlowIntensity\":3.21,\"sunGlowSize\":0.51},\"cloud\":{\"cloudScale\":0.85,\"coverage\":0.4,\"windSpeed\":0.17},\"glass\":{\"brightness\":0.9}},\"midnight\":{\"celestial\":{\"celestialX\":0.74,\"celestialY\":0.71,\"moonGlowIntensity\":3.55,\"moonGlowSize\":0.97,\"starDensity\":1.72},\"cloud\":{\"cloudScale\":0.85,\"coverage\":0.4,\"windSpeed\":0.17}},\"noon\":{\"celestial\":{\"celestialX\":0.74,\"celestialY\":0.71,\"moonPhase\":0.2421,\"starDensity\":0.14,\"sunGlowIntensity\":1.59,\"sunGlowSize\":0.55},\"cloud\":{\"cloudScale\":0.85,\"coverage\":0.4,\"windSpeed\":0.17},\"glass\":{\"brightness\":1}}}};\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/generated/weather-effect-shaders.generated.ts",
    "content": "// AUTO-GENERATED by `pnpm weather:compile`.\n// Source: lib/weather-authoring/shaders/*.glsl\n// DO NOT EDIT MANUALLY.\n\nexport const FULLSCREEN_VERTEX = \"#version 300 es\\nin vec4 a_position;out vec2 v_uv;void main(){gl_Position=a_position;v_uv=a_position.xy*0.5+0.5;}\";\n\nexport const CELESTIAL_FRAGMENT = \"#version 300 es\\nprecision highp float;in vec2 v_uv;out vec4 fragColor;uniform float u_time;uniform vec2 u_resolution;uniform float u_timeOfDay;uniform float u_moonPhase;uniform float u_starDensity;uniform vec2 u_celestialPos;uniform float u_sunSize;uniform float u_moonSize;uniform float u_sunGlowIntensity;uniform float u_sunGlowSize;uniform float u_sunRayCount;uniform float u_sunRayLength;uniform float u_sunRayIntensity;uniform float u_sunRayShimmer;uniform float u_sunRayShimmerSpeed;uniform float u_moonGlowIntensity;uniform float u_moonGlowSize;uniform float u_skyBrightness;uniform float u_skySaturation;uniform float u_skyContrast;uniform sampler2D u_moonTexture;uniform bool u_hasMoonTexture;\\n#define PI 3.14159265359\\n#define GODRAY_MAX_SAMPLES 32\\n#define TAU 6.28318530718\\nfloat hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453123);}vec2 hash2(vec2 p){p=vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3)));return fract(sin(p)*43758.5453);}float noise(vec2 p){vec2 i=floor(p);vec2 f=fract(p);f=f*f*(3.0-2.0*f);float a=hash(i);float b=hash(i+vec2(1.0,0.0));float c=hash(i+vec2(0.0,1.0));float d=hash(i+vec2(1.0,1.0));return mix(mix(a,b,f.x),mix(c,d,f.x),f.y);}float fbm(vec2 p,int octaves){float value=0.0;float amplitude=0.5;float frequency=1.0;for(int i=0;i<6;i++){if(i>=octaves)break;value+=amplitude*noise(p*frequency);frequency*=2.0;amplitude*=0.5;}return value;}float getSunY(float timeOfDay,float baseY){float belowHorizon=-0.25;float riseProgress=smoothstep(0.18,0.32,timeOfDay);float setProgress=smoothstep(0.68,0.82,timeOfDay);float visible=riseProgress*(1.0-setProgress);return mix(belowHorizon,baseY,visible);}float getMoonY(float timeOfDay,float baseY){float belowHorizon=-0.25;float risingEvening=smoothstep(0.74,0.88,timeOfDay);float settingMorning=1.0-smoothstep(0.12,0.26,timeOfDay);float visible=max(risingEvening,settingMorning);return mix(belowHorizon,baseY,visible);}float getHorizonFade(float y){return smoothstep(-0.2,0.0,y);}vec3 getSkyColor(vec2 uv,float timeOfDay){vec3 dayTop=vec3(0.4,0.6,0.9);vec3 dayHorizon=vec3(0.7,0.8,0.95);vec3 sunsetTop=vec3(0.2,0.2,0.4);vec3 sunsetHorizon=vec3(0.9,0.5,0.2);vec3 nightTop=vec3(0.02,0.02,0.05);vec3 nightHorizon=vec3(0.05,0.05,0.1);float dayAmount=smoothstep(0.25,0.4,timeOfDay)*smoothstep(0.75,0.6,timeOfDay);float sunsetAmount=max(smoothstep(0.2,0.3,timeOfDay)*smoothstep(0.4,0.3,timeOfDay),smoothstep(0.6,0.7,timeOfDay)*smoothstep(0.8,0.7,timeOfDay));float nightAmount=max(0.0,1.0-dayAmount-sunsetAmount);float gradientFactor=pow(1.0-uv.y,1.0);vec3 topColor=dayTop*dayAmount+sunsetTop*sunsetAmount+nightTop*nightAmount;vec3 horizonColor=dayHorizon*dayAmount+sunsetHorizon*sunsetAmount+nightHorizon*nightAmount;vec3 avgColor=(topColor+horizonColor)*0.5;topColor=mix(avgColor,topColor,u_skyContrast);horizonColor=mix(avgColor,horizonColor,u_skyContrast);vec3 color=mix(topColor,horizonColor,gradientFactor);color*=u_skyBrightness;float gray=dot(color,vec3(0.299,0.587,0.114));if(u_skySaturation<=1.0){color=mix(vec3(gray),color,u_skySaturation);}else{float boost=u_skySaturation-1.0;color=color+(color-vec3(gray))*boost;}return clamp(color,0.0,1.0);}float drawStars(vec2 uv,float density,float time){float stars=0.0;for(int layer=0;layer<3;layer++){float layerScale=100.0+float(layer)*50.0;vec2 gridUV=uv*layerScale;vec2 gridID=floor(gridUV);vec2 gridFract=fract(gridUV);vec2 starPos=hash2(gridID+float(layer)*100.0);float dist=length(gridFract-starPos);float starPresent=step(1.0-density*0.3,hash(gridID*(float(layer)+1.0)));float starSize=0.02+hash(gridID.yx)*0.03;float twinkle=sin(time*(2.0+hash(gridID)*3.0)+hash(gridID.yx)*TAU)*0.3+0.7;float star=smoothstep(starSize,0.0,dist)*starPresent*twinkle;star*=1.0-float(layer)*0.3;stars+=star;}return stars;}vec3 drawSun(vec2 uv,vec2 sunPos,float size){vec2 aspect=vec2(u_resolution.x/u_resolution.y,1.0);vec2 diff=(uv-sunPos)*aspect;float dist=length(diff);float angle=atan(diff.y,diff.x);float disc=1.0-smoothstep(size*0.9,size,dist);vec3 sunCore=vec3(1.0,1.0,0.95);vec3 sunEdge=vec3(1.0,0.9,0.4);float edgeFactor=clamp(dist/size,0.0,1.0);vec3 sunColor=mix(sunCore,sunEdge,edgeFactor);float limbDarkening=1.0-pow(clamp(dist/size,0.0,1.0),2.0)*0.3;sunColor*=limbDarkening;float glowSize=max(0.1,u_sunGlowSize);float scaledDist=dist/glowSize;float glow1=exp(-scaledDist*8.0)*0.5;float glow2=exp(-scaledDist*3.0)*0.3;float glow3=exp(-scaledDist*1.5)*0.15;vec3 glowColor=vec3(1.0,0.8,0.4);float glowTotal=(glow1+glow2+glow3)*u_sunGlowIntensity;vec3 result=sunColor*disc*2.0;result+=glowColor*glowTotal;float ringCenter=size*1.15;float ringWidth=max(size*0.35,0.001);float ringMask=smoothstep(size*0.85,size*1.05,dist);ringMask*=1.0-smoothstep(size*5.0,size*9.0,dist);float chromaShift=size*(0.012+u_sunRayIntensity*0.06);chromaShift*=smoothstep(size*0.9,size*2.4,dist);float ringR=exp(-pow((dist-chromaShift-ringCenter)/ringWidth,2.0));float ringG=exp(-pow((dist-ringCenter)/ringWidth,2.0));float ringB=exp(-pow((dist+chromaShift-ringCenter)/ringWidth,2.0));float ringT=clamp((dist-size)/(size*2.2),0.0,1.0);vec3 ringSpectral=0.55+0.45*cos(TAU*(ringT+vec3(0.0,0.33,0.67)));ringSpectral=clamp(ringSpectral,0.0,1.0);vec3 ringColor=mix(vec3(1.0),ringSpectral,0.45);float ringIntensity=(ringR+ringG+ringB)/3.0;ringIntensity*=ringMask*u_sunGlowIntensity*0.025;result+=ringColor*ringIntensity;if(u_sunRayCount>0.0&&u_sunRayIntensity>0.0){if(dist<size*3.6){float motion=clamp(u_sunRayShimmer,0.0,5.0);float t=u_time*max(0.0,u_sunRayShimmerSpeed);float rayPhase=angle*u_sunRayCount;float rayIndex=floor(rayPhase/PI+0.5);float raySeed=hash(vec2(rayIndex,19.17));float major=pow(abs(cos(rayPhase)),10.0);float minor=pow(abs(cos(rayPhase*2.0+raySeed*2.3)),22.0)*0.18;float rayShape=max(major,minor);float breathe=1.0+(noise(vec2(t*0.05,raySeed*7.0))-0.5)*(0.08*motion);float shimmer=1.0+(noise(vec2(dist*12.0-t*0.25,raySeed*23.0))-0.5)*(0.12*motion);float micro=1.0+(noise(vec2(t*0.6,rayPhase*0.8))-0.5)*(0.06*motion);float rayNoise=0.72+0.28*noise(vec2(rayPhase*0.35,t*0.12+raySeed*10.0));float rayPattern=rayShape*rayNoise;float rayStart=smoothstep(size*0.75,size*1.25,dist);float rayEnd=smoothstep(size*(3.0*breathe),size*1.5,dist);float rayLengthVar=0.75+raySeed*0.55;float maxRayDist=max(0.001,u_sunRayLength*0.15);float rayFalloff=exp(-dist*dist/(maxRayDist*maxRayDist*rayLengthVar*breathe));float rays=rayPattern*rayFalloff*rayStart*rayEnd*u_sunRayIntensity;rays*=shimmer*micro;float prismMask=smoothstep(size*1.05,size*2.6,dist);float rayChroma=size*(0.01+u_sunRayIntensity*0.05)*prismMask;float distR=max(0.0,dist-rayChroma);float distB=dist+rayChroma;float falloffR=exp(-distR*distR/(maxRayDist*maxRayDist*rayLengthVar*breathe));float falloffG=rayFalloff;float falloffB=exp(-distB*distB/(maxRayDist*maxRayDist*rayLengthVar*breathe));vec3 rayRGB=vec3(falloffR,falloffG,falloffB)*rayPattern*rayStart*rayEnd;float rayAvg=(rayRGB.r+rayRGB.g+rayRGB.b)/3.0;vec3 rayChromaColor=rayRGB/max(rayAvg,1e-4);float rayT=clamp((dist-size)/(size*2.6),0.0,1.0);vec3 raySpectral=0.55+0.45*cos(TAU*(rayT+vec3(0.0,0.33,0.67)));raySpectral=clamp(raySpectral,0.0,1.0);raySpectral=mix(vec3(1.0),raySpectral,0.28);vec3 rayWarm=vec3(1.0,0.92,0.7);float prismMix=clamp(0.08+u_sunRayIntensity*0.6,0.0,0.45)*prismMask;vec3 rayColor=mix(rayWarm,rayChromaColor,prismMix);rayColor=mix(rayColor,raySpectral,prismMix*0.65);result+=rayColor*rays;}}return result;}vec3 getSphereNormal(vec2 discUV){float r2=dot(discUV,discUV);if(r2>1.0)return vec3(0.0);float z=sqrt(1.0-r2);return normalize(vec3(discUV.x,discUV.y,z));}vec2 sphereToEquirectangular(vec3 normal){float longitude=atan(normal.x,normal.z);float u=longitude/TAU+0.5;float latitude=asin(clamp(normal.y,-1.0,1.0));float v=latitude/PI+0.5;return vec2(u,v);}vec3 getMoonSurfaceColor(vec3 normal,vec2 discUV){if(u_hasMoonTexture){vec2 texUV=sphereToEquirectangular(normal);return texture(u_moonTexture,texUV).rgb;}float brightness=0.7+fbm(discUV*5.0,3)*0.3;return vec3(brightness*0.85,brightness*0.83,brightness*0.8);}vec4 drawMoon(vec2 uv,vec2 moonPos,float size,float phase){vec2 aspect=vec2(u_resolution.x/u_resolution.y,1.0);vec2 diff=(uv-moonPos)*aspect;float dist=length(diff);vec2 discUV=diff/size;float discDist=length(discUV);float disc=1.0-smoothstep(0.95,1.0,discDist);float glowSize=max(0.1,u_moonGlowSize);float glowIntensity=u_moonGlowIntensity;if(disc<0.001){float scaledDist=dist/glowSize;float glow1=exp(-scaledDist*6.0)*0.15;float glow2=exp(-scaledDist*2.0)*0.06;vec3 glowColor=vec3(0.8,0.85,0.95);float phaseAngle=phase*TAU;vec3 sunDir=vec3(sin(phaseAngle),0.0,-cos(phaseAngle));float glowPhase=max(0.2,dot(normalize(vec3(discUV,0.5)),sunDir)*0.5+0.5);return vec4(glowColor*(glow1+glow2)*glowPhase*glowIntensity,0.0);}vec3 normal=getSphereNormal(discUV);float phaseAngle=phase*TAU;vec3 sunDir=vec3(sin(phaseAngle),0.0,-cos(phaseAngle));float NdotL=dot(normal,sunDir);float terminator=smoothstep(-0.02,0.08,NdotL);vec3 baseColor=getMoonSurfaceColor(normal,discUV);vec3 ambient=baseColor*0.03;vec3 lit=baseColor*terminator;vec3 moonSurface=ambient+lit;float limbDarkening=1.0-pow(discDist,3.0)*0.15;moonSurface*=limbDarkening;float rimLight=pow(1.0-abs(NdotL),4.0)*terminator*0.1;moonSurface+=vec3(1.0,0.98,0.95)*rimLight;float scaledDist=dist/glowSize;float glow1=exp(-scaledDist*6.0)*0.12;float glow2=exp(-scaledDist*2.0)*0.06;vec3 glowColor=vec3(0.8,0.85,0.95);float litAmount=max(0.1,terminator);vec3 glow=glowColor*(glow1+glow2)*litAmount*glowIntensity;return vec4(moonSurface*disc+glow,disc);}void main(){vec2 uv=v_uv;vec3 color=getSkyColor(uv,u_timeOfDay);float sunY=getSunY(u_timeOfDay,u_celestialPos.y);float moonY=getMoonY(u_timeOfDay,u_celestialPos.y);vec2 sunPos=vec2(u_celestialPos.x,sunY);vec2 moonPos=vec2(u_celestialPos.x,moonY);float moonFade=getHorizonFade(moonY);if(moonFade>0.01){float stars=drawStars(uv,u_starDensity,u_time);color+=vec3(stars)*moonFade;}float sunFade=getHorizonFade(sunY);if(sunFade>0.01){vec3 sun=drawSun(uv,sunPos,u_sunSize);color+=sun*sunFade;}if(moonFade>0.01){vec4 moon=drawMoon(uv,moonPos,u_moonSize,u_moonPhase);float alpha=moon.a*moonFade;color=mix(color,moon.rgb,alpha)+moon.rgb*(1.0-moon.a)*moonFade;}fragColor=vec4(color,0.0);}\";\n\nexport const CLOUD_FRAGMENT = \"#version 300 es\\nprecision highp float;in vec2 v_uv;out vec4 fragColor;uniform float u_time;uniform vec2 u_resolution;uniform sampler2D u_sceneTexture;uniform float u_timeOfDay;uniform float u_coverage;uniform float u_density;uniform float u_softness;uniform float u_windSpeed;uniform float u_windAngle;uniform float u_turbulence;uniform float u_lightIntensity;uniform float u_ambientDarkness;uniform int u_numLayers;uniform float u_cloudScale;uniform vec2 u_celestialPos;uniform float u_celestialSize;uniform float u_celestialBrightness;uniform float u_backlightIntensity;\\n#define PI 3.14159265359\\nfloat hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453123);}float noise(vec2 p){vec2 i=floor(p);vec2 f=fract(p);f=f*f*(3.0-2.0*f);float a=hash(i);float b=hash(i+vec2(1.0,0.0));float c=hash(i+vec2(0.0,1.0));float d=hash(i+vec2(1.0,1.0));return mix(mix(a,b,f.x),mix(c,d,f.x),f.y);}float fbm(vec2 p,int octaves){float value=0.0;float amplitude=0.5;for(int i=0;i<8;i++){if(i>=octaves)break;value+=amplitude*noise(p);p*=2.0;amplitude*=0.5;}return value;}float cloudLayer(vec2 uv,float time,float layerSeed,float speed,float turbAmount){vec2 wind=vec2(cos(u_windAngle),sin(u_windAngle))*speed*time;float layerScale=(1.8+hash(vec2(layerSeed,0.0))*1.2)*u_cloudScale;float layerRotation=hash(vec2(layerSeed,1.0))*0.5-0.25;vec2 layerOffset=vec2(hash(vec2(layerSeed,2.0))*100.0,hash(vec2(layerSeed,3.0))*100.0);float c=cos(layerRotation);float s=sin(layerRotation);vec2 rotatedUV=vec2(uv.x*c-uv.y*s,uv.x*s+uv.y*c);vec2 p=rotatedUV*layerScale+wind+layerOffset;float turbSeed=layerSeed*50.0;vec2 turbOffset=vec2(fbm(p*0.5+time*0.1+turbSeed,4),fbm(p*0.5+turbSeed+100.0+time*0.1,4))*turbAmount;float n=fbm(p+turbOffset,6);return n;}vec3 cloudLighting(float density,float heightInCloud,float sunAlt,float warmth,float nightFactor,vec2 uv){float daylight=smoothstep(-0.12,0.1,sunAlt);vec3 dayLitColor=vec3(1.0,0.98,0.96);vec3 sunsetLitColor=vec3(1.0,0.7,0.45);vec3 nightLitColor=vec3(0.12,0.14,0.2);vec3 litColor=mix(dayLitColor,sunsetLitColor,warmth);litColor=mix(litColor,nightLitColor,nightFactor);vec3 dayShadowColor=vec3(0.45,0.5,0.6);vec3 sunsetShadowColor=vec3(0.35,0.25,0.3);vec3 nightShadowColor=vec3(0.03,0.04,0.07);vec3 shadowColor=mix(dayShadowColor,sunsetShadowColor,warmth);shadowColor=mix(shadowColor,nightShadowColor,nightFactor);shadowColor*=(1.0-u_ambientDarkness*0.3);float topLight=heightInCloud*max(0.0,sunAlt);float sideLight=(1.0-abs(heightInCloud-0.5)*2.0)*(1.0-sunAlt*0.5);float bottomLight=(1.0-heightInCloud)*warmth*0.5;float ambientLight=mix(0.03,0.2,daylight);float lightAmount=(topLight*0.5+sideLight*0.3+bottomLight)*daylight+ambientLight;lightAmount=clamp(lightAmount*u_lightIntensity,0.0,1.0);vec3 cloudColor=mix(shadowColor,litColor,lightAmount);float rimLight=pow(density,0.5)*(1.0-density)*4.0;vec3 rimColor=mix(vec3(1.0,1.0,0.95),vec3(1.0,0.8,0.5),warmth);rimColor=mix(rimColor,vec3(0.15,0.18,0.25),nightFactor);float rimStrength=mix(0.1,0.3,daylight);cloudColor+=rimColor*rimLight*rimStrength*u_lightIntensity;float hotSpot=pow(max(0.0,lightAmount-0.6)*2.5,2.0)*warmth*daylight;cloudColor+=vec3(1.0,0.5,0.2)*hotSpot*0.4;float aspect=u_resolution.x/u_resolution.y;vec2 celestialUV=u_celestialPos;vec2 diff=(uv-celestialUV)*vec2(aspect,1.0);float distToCelestial=length(diff);float transmission=pow(1.0-density,2.0);float glowRadius=u_celestialSize*6.0;float proximityGlow=exp(-distToCelestial*distToCelestial/(glowRadius*glowRadius));float backlight=proximityGlow*transmission*u_celestialBrightness;float edgeDist=u_celestialSize*3.0;float nearCelestial=smoothstep(edgeDist*2.5,edgeDist*0.3,distToCelestial);float edgeFactor=density*(1.0-density)*4.0;float silverLining=nearCelestial*edgeFactor*u_celestialBrightness;vec3 backlightColor=mix(vec3(1.0,0.9,0.7),vec3(0.7,0.75,0.9),nightFactor);cloudColor+=backlightColor*(backlight*0.5+silverLining*0.8)*u_backlightIntensity;return cloudColor;}void main(){vec2 uv=v_uv;vec4 scene=texture(u_sceneTexture,uv);float sunAlt=u_timeOfDay<0.5?u_timeOfDay*2.0:2.0-u_timeOfDay*2.0;sunAlt=sunAlt*2.0-1.0;float warmth=1.0-smoothstep(0.0,0.4,sunAlt);warmth=warmth*warmth;float nightFactor=1.0-smoothstep(-0.12,0.02,sunAlt);float daylight=smoothstep(-0.12,0.1,sunAlt);vec3 color=scene.rgb;float accumulatedAlpha=0.0;for(int i=u_numLayers-1;i>=0;i--){float layerIdx=float(i);float layerDepth=layerIdx/max(1.0,float(u_numLayers)-1.0);float layerSeed=layerIdx*7.31+13.0;float layerSpeed=u_windSpeed*(0.6+hash(vec2(layerSeed,10.0))*0.8);float layerTurb=u_turbulence*(0.7+hash(vec2(layerSeed,11.0))*0.6);float cloud=cloudLayer(uv,u_time,layerSeed,layerSpeed,layerTurb);float threshold=1.0-u_coverage;cloud=smoothstep(threshold,threshold+u_softness,cloud);float heightInCloud=uv.y*0.6+cloud*0.4;vec3 cloudColor=cloudLighting(cloud,heightInCloud,sunAlt,warmth,nightFactor,uv);vec3 hazeColor=mix(vec3(0.05,0.06,0.1),vec3(0.6,0.7,0.85),daylight);float hazeAmount=layerDepth*layerDepth*0.5;cloudColor=mix(cloudColor,hazeColor,hazeAmount);float contrastReduction=1.0-layerDepth*0.3;cloudColor=mix(vec3(0.5),cloudColor,contrastReduction);float alpha=cloud*u_density*(0.6+(1.0-layerDepth)*0.4);color=mix(color,cloudColor,alpha*(1.0-accumulatedAlpha));accumulatedAlpha=accumulatedAlpha+alpha*(1.0-accumulatedAlpha);}fragColor=vec4(color,accumulatedAlpha);}\";\n\nexport const RAIN_FRAGMENT = \"#version 300 es\\nprecision highp float;in vec2 v_uv;out vec4 fragColor;uniform float u_time;uniform vec2 u_resolution;uniform sampler2D u_sceneTexture;uniform float u_glassIntensity;uniform float u_glassZoom;uniform float u_fallingIntensity;uniform float u_fallingSpeed;uniform float u_fallingAngle;uniform float u_fallingStreakLength;uniform int u_fallingLayers;uniform float u_refractionStrength;\\n#define S(a, b, t) smoothstep(a, b, t)\\nvec3 N13(float p){vec3 p3=fract(vec3(p)*vec3(0.1031,0.11369,0.13787));p3+=dot(p3,p3.yzx+19.19);return fract(vec3((p3.x+p3.y)*p3.z,(p3.x+p3.z)*p3.y,(p3.y+p3.z)*p3.x));}float N(float t){return fract(sin(t*12345.564)*7658.76);}float Saw(float b,float t){return S(0.0,b,t)*S(1.0,b,t);}vec2 DropLayer(vec2 uv,float t){vec2 UV=uv;uv.y+=t*0.75;vec2 aspect=vec2(6.0,1.0);vec2 grid=aspect*2.0;vec2 id=floor(uv*grid);float colShift=N(id.x);uv.y+=colShift;id=floor(uv*grid);vec3 n=N13(id.x*35.2+id.y*2376.1);vec2 st=fract(uv*grid)-vec2(0.5,0.0);float x=n.x-0.5;float y=UV.y*20.0;float wiggle=sin(y+sin(y));x+=wiggle*(0.5-abs(x))*(n.z-0.5);x*=0.7;float ti=fract(t+n.z);y=(Saw(0.85,ti)-0.5)*0.9+0.5;vec2 p=vec2(x,y);float d=length((st-p)*aspect.yx);float mainDrop=S(0.4,0.0,d);float r=sqrt(S(1.0,y,st.y));float cd=abs(st.x-x);float trail=S(0.23*r,0.15*r*r,cd);float trailFront=S(-0.02,0.02,st.y-y);trail*=trailFront*r*r;float y2=fract(UV.y*10.0)+(st.y-0.5);float dd=length(st-vec2(x,y2));float droplets=S(0.3,0.0,dd);float m=mainDrop+droplets*r*trailFront;return vec2(m,trail);}float StaticDrops(vec2 uv,float t){uv*=40.0;vec2 id=floor(uv);uv=fract(uv)-0.5;vec3 n=N13(id.x*107.45+id.y*3543.654);vec2 p=(n.xy-0.5)*0.7;float d=length(uv-p);float fade=Saw(0.025,fract(t+n.z));float c=S(0.3,0.0,d)*fract(n.z*10.0)*fade;return c;}vec2 Drops(vec2 uv,float t,float l0,float l1,float l2){float s=StaticDrops(uv,t)*l0;vec2 m1=DropLayer(uv,t)*l1;vec2 m2=DropLayer(uv*1.85,t)*l2;float c=s+m1.x+m2.x;c=S(0.3,1.0,c);return vec2(c,max(m1.y*l0,m2.y*l1));}float hash12(vec2 p){vec3 p3=fract(vec3(p.xyx)*0.1031);p3+=dot(p3,p3.yzx+33.33);return fract((p3.x+p3.y)*p3.z);}vec2 FallingRainLayer(vec2 uv,float t,float speed,float angle,float streakLen,float scale,float density){vec2 offset=vec2(0.0);vec2 p=uv;p.x+=p.y*angle;p*=scale;p.y+=t*speed;vec2 id=floor(p);vec2 gv=fract(p)-0.5;for(int y=-1;y<=1;y++){for(int x=-1;x<=1;x++){vec2 offs=vec2(float(x),float(y));vec2 cellId=id+offs;float n1=hash12(cellId);if(n1>density)continue;vec2 n2=vec2(hash12(cellId*17.23),hash12(cellId*31.17));vec2 dropPos=offs+n2-0.5;vec2 localUV=gv-dropPos;float streakW=0.025+n1*0.02;float streakH=streakLen*(0.4+hash12(cellId*7.13)*0.6);float t_pos=(localUV.y+streakH)/(2.0*streakH);t_pos=clamp(t_pos,0.0,1.0);if(abs(localUV.y)>streakH*1.2)continue;float taper=mix(1.3,0.4,t_pos*t_pos);float width=streakW*taper;float core=S(width,width*0.2,abs(localUV.x));float vertFade=S(0.0,0.1,t_pos)*S(1.0,0.85,t_pos);float streak=core*vertFade;if(streak>0.001){offset.x+=localUV.x*streak*0.5;offset.y+=(n1-0.5)*streak*0.1;}}}return offset;}vec2 FallingRain(vec2 uv,float t){vec2 totalOffset=vec2(0.0);if(u_fallingIntensity<0.01)return totalOffset;float speed=u_fallingSpeed*5.0;float streakLen=u_fallingStreakLength*0.3;for(int i=0;i<6;i++){if(i>=u_fallingLayers)break;float layerIdx=float(i);float depth=layerIdx/float(max(u_fallingLayers-1,1));float layerScale=mix(6.0,30.0,depth);float layerSpeed=speed*mix(2.0,0.5,depth);float layerDensity=u_fallingIntensity*mix(0.8,0.3,depth);float layerStrength=mix(1.0,0.15,depth);float layerStreakLen=streakLen*mix(1.5,0.4,depth);float layerAngle=u_fallingAngle*mix(1.0,0.6,depth);vec2 layerOffset=vec2(sin(layerIdx*73.156)*3.0,cos(layerIdx*37.842)*3.0);vec2 layer=FallingRainLayer(uv+layerOffset,t+layerIdx*0.13,layerSpeed,layerAngle,layerStreakLen,layerScale,layerDensity);totalOffset+=layer*layerStrength;}return totalOffset*0.4;}void main(){vec2 uv=(gl_FragCoord.xy-0.5*u_resolution.xy)/u_resolution.y;vec2 UV=v_uv;uv*=u_glassZoom;float t=u_time*0.2;float rainAmount=u_glassIntensity;float staticDrops=S(-0.5,1.0,rainAmount)*2.0;float layer1=S(0.25,0.75,rainAmount);float layer2=S(0.0,0.5,rainAmount);vec2 c=Drops(uv,t,staticDrops,layer1,layer2);vec2 e=vec2(0.001,0.0);float cx=Drops(uv+e,t,staticDrops,layer1,layer2).x;float cy=Drops(uv+e.yx,t,staticDrops,layer1,layer2).x;vec2 glassNormal=vec2(cx-c.x,cy-c.x);vec2 fallingRainOffset=FallingRain(uv,u_time);vec2 totalRefraction=(glassNormal+fallingRainOffset)*u_refractionStrength;vec2 refractedUV=UV+totalRefraction;refractedUV=clamp(refractedUV,0.0,1.0);vec4 scene=texture(u_sceneTexture,refractedUV);vec3 color=scene.rgb;float rainMagnitude=length(fallingRainOffset);if(rainMagnitude>0.001){float brightness=dot(scene.rgb,vec3(0.299,0.587,0.114));float specular=rainMagnitude*15.0*(0.1+brightness*0.9);color+=vec3(0.8,0.85,0.95)*specular*0.3;}color+=vec3(0.1,0.12,0.15)*c.x*0.5;fragColor=vec4(color,scene.a);}\";\n\nexport const LIGHTNING_FRAGMENT = \"#version 300 es\\nprecision highp float;in vec2 v_uv;out vec4 fragColor;uniform float u_time;uniform vec2 u_resolution;uniform sampler2D u_sceneTexture;uniform bool u_enabled;uniform float u_flashIntensity;uniform float u_branchDensity;uniform float u_sceneIllumination;uniform float u_lastFlashTime;uniform float u_strikeSeed;\\n#define MAX_SEGMENTS 32\\n#define MAX_BRANCHES 16\\n#define PI 3.14159265359\\nfloat easeOutSine(float t){return sin(t*PI*0.5);}float easeInSine(float t){return 1.0-cos(t*PI*0.5);}float easeInOutSine(float t){return-(cos(PI*t)-1.0)*0.5;}float easeOutQuad(float t){return 1.0-(1.0-t)*(1.0-t);}float easeOutCubic(float t){float inv=1.0-t;return 1.0-inv*inv*inv;}float hash11(float p){p=fract(p*0.1031);p*=p+33.33;p*=p+p;return fract(p);}float hash12(vec2 p){vec3 p3=fract(vec3(p.xyx)*0.1031);p3+=dot(p3,p3.yzx+33.33);return fract((p3.x+p3.y)*p3.z);}vec2 hash22(vec2 p){vec3 p3=fract(vec3(p.xyx)*vec3(0.1031,0.1030,0.0973));p3+=dot(p3,p3.yzx+33.33);return fract((p3.xx+p3.yz)*p3.zy);}float noise(vec2 p){vec2 i=floor(p);vec2 f=fract(p);f=f*f*(3.0-2.0*f);float a=hash12(i);float b=hash12(i+vec2(1.0,0.0));float c=hash12(i+vec2(0.0,1.0));float d=hash12(i+vec2(1.0,1.0));return mix(mix(a,b,f.x),mix(c,d,f.x),f.y);}float distToSegment(vec2 p,vec2 a,vec2 b){vec2 pa=p-a;vec2 ba=b-a;float h=clamp(dot(pa,ba)/dot(ba,ba),0.0,1.0);return length(pa-ba*h);}vec2 displacedPoint(vec2 start,vec2 end,float t,float seed,float displacementAmt){vec2 basePoint=mix(start,end,t);vec2 dir=end-start;vec2 perp=normalize(vec2(-dir.y,dir.x));float envelope=sin(t*PI);float n1=noise(vec2(t*8.0,seed*100.0))*2.0-1.0;float n2=noise(vec2(t*16.0,seed*100.0+50.0))*2.0-1.0;float n3=noise(vec2(t*32.0,seed*100.0+100.0))*2.0-1.0;float displacement=(n1*0.6+n2*0.3+n3*0.1)*envelope*displacementAmt;float targetBias=1.0-t*0.3;displacement*=targetBias;return basePoint+perp*displacement*length(dir);}float mainBoltDistance(vec2 uv,vec2 start,vec2 end,float seed,float displacementAmt){float minDist=999.0;vec2 prevPoint=start;for(int i=1;i<=MAX_SEGMENTS;i++){float t=float(i)/float(MAX_SEGMENTS);vec2 currPoint=displacedPoint(start,end,t,seed,displacementAmt);float d=distToSegment(uv,prevPoint,currPoint);minDist=min(minDist,d);prevPoint=currPoint;}return minDist;}float branchDistance(vec2 uv,vec2 branchStart,vec2 branchDir,float branchLen,float seed,float displacementAmt){vec2 branchEnd=branchStart+branchDir*branchLen;float minDist=999.0;vec2 prevPoint=branchStart;for(int i=1;i<=12;i++){float t=float(i)/12.0;vec2 currPoint=displacedPoint(branchStart,branchEnd,t,seed,displacementAmt*0.7);float d=distToSegment(uv,prevPoint,currPoint);minDist=min(minDist,d);prevPoint=currPoint;}return minDist;}vec2 branchesDistance(vec2 uv,vec2 start,vec2 end,float seed,float displacementAmt,float density){float minDist=999.0;float brightness=0.0;vec2 mainDir=normalize(end-start);float mainLen=length(end-start);for(int i=0;i<MAX_BRANCHES;i++){float idx=float(i);float branchT=0.15+hash11(seed+idx*7.31)*0.7;float branchProb=(1.0-branchT)*density;if(hash11(seed+idx*3.17)>branchProb)continue;vec2 branchStart=displacedPoint(start,end,branchT,seed,displacementAmt);float angleOffset=(hash11(seed+idx*11.13)*2.0-1.0)*0.6;float side=hash11(seed+idx*5.71)>0.5?1.0:-1.0;float angle=atan(mainDir.y,mainDir.x)+side*(0.3+abs(angleOffset)*0.5);vec2 branchDir=vec2(cos(angle),sin(angle));float branchLen=mainLen*(0.15+hash11(seed+idx*13.37)*0.25);float d=branchDistance(uv,branchStart,branchDir,branchLen,seed+idx*100.0,displacementAmt);if(d<minDist){minDist=d;brightness=0.5-branchT*0.2;}if(density>0.3&&hash11(seed+idx*17.19)<density*0.5){float subT=0.3+hash11(seed+idx*19.23)*0.4;vec2 subStart=branchStart+branchDir*branchLen*subT;float subAngle=angle+(hash11(seed+idx*23.29)*2.0-1.0)*0.5;vec2 subDir=vec2(cos(subAngle),sin(subAngle));float subLen=branchLen*0.4;float subD=branchDistance(uv,subStart,subDir,subLen,seed+idx*200.0,displacementAmt*0.5);if(subD<minDist){minDist=subD;brightness=0.25;}}}return vec2(minDist,brightness);}vec3 lightningGlow(float dist,float brightness,float intensity,float thickness){float scaledDist=dist/max(thickness,0.1);float core=smoothstep(0.003,0.0,scaledDist)*brightness;float innerGlow=exp(-scaledDist*150.0)*brightness;float outerGlow=exp(-dist*dist*3000.0)*brightness*thickness;vec3 coreColor=vec3(1.0,1.0,1.0);vec3 innerColor=vec3(0.7,0.8,1.0);vec3 outerColor=vec3(0.5,0.5,0.9);vec3 color=coreColor*core*2.0;color+=innerColor*innerGlow*0.8;color+=outerColor*outerGlow*0.5;return color*intensity;}float flashEnvelope(float timeSinceStrike,float duration){if(timeSinceStrike<0.0||timeSinceStrike>duration)return 0.0;float t=timeSinceStrike/duration;float attackT=clamp(t/0.03,0.0,1.0);float attack=easeOutCubic(attackT);float sustainT=clamp((t-0.05)/0.65,0.0,1.0);float sustain=1.0-easeInOutSine(sustainT);float decay=exp(-t*2.0);decay=mix(decay,easeOutSine(1.0-t),0.3);float endT=clamp((t-0.75)/0.25,0.0,1.0);float endFade=1.0-easeInSine(endT);return attack*max(sustain,decay*0.4)*endFade;}float restrikeEnvelope(float timeSinceStrike,float duration,float seed){float env=flashEnvelope(timeSinceStrike,duration*0.7);if(hash11(seed*7.7)>0.7){float restrike1=flashEnvelope(timeSinceStrike-duration*0.5,duration*0.3);env=max(env,restrike1*0.6);}if(hash11(seed*11.3)>0.85){float restrike2=flashEnvelope(timeSinceStrike-duration*0.75,duration*0.2);env=max(env,restrike2*0.4);}return env;}void main(){vec4 scene=texture(u_sceneTexture,v_uv);if(!u_enabled){fragColor=scene;return;}vec2 uv=v_uv;float aspect=u_resolution.x/u_resolution.y;uv.x*=aspect;float timeSinceStrike=u_time-u_lastFlashTime;float durationSec=0.8;float flash=restrikeEnvelope(timeSinceStrike,durationSec,u_strikeSeed);float afterimageDuration=durationSec*1.5;float afterimageT=clamp(timeSinceStrike/afterimageDuration,0.0,1.0);float afterimage=timeSinceStrike<0.0?0.0:(1.0-easeInSine(afterimageT));vec3 color=scene.rgb;if(flash>0.01||afterimage>0.01){vec2 strikeHash=hash22(vec2(u_strikeSeed*123.456,u_strikeSeed*789.012));vec2 boltStart=vec2((0.3+strikeHash.x*0.4)*aspect,1.05);vec2 boltEnd=vec2(boltStart.x+(strikeHash.x-0.5)*0.4,-0.05);float straightDist=distToSegment(uv,boltStart,boltEnd);float sourceGlow=exp(-length(uv-boltStart)*3.0);color+=vec3(0.4,0.45,0.6)*sourceGlow*afterimage*0.3;float distLimit=0.18+u_branchDensity*0.25+u_flashIntensity*0.05;float feather=0.08;float region=1.0-smoothstep(distLimit-feather,distLimit,straightDist);if(region<=0.0005){fragColor=vec4(color,scene.a);return;}float displacementAmt=0.15;float mainDist=mainBoltDistance(uv,boltStart,boltEnd,u_strikeSeed,displacementAmt);vec2 branchResult=branchesDistance(uv,boltStart,boltEnd,u_strikeSeed,displacementAmt,u_branchDensity);float branchDist=branchResult.x;float branchBrightness=branchResult.y;float mainThickness=mix(0.2,1.0,easeOutSine(sqrt(max(flash,0.0))));vec3 afterglowColor=vec3(0.5,0.45,0.7);vec3 mainCore=lightningGlow(mainDist,easeOutQuad(max(flash,0.0)),u_flashIntensity,mainThickness);float mainAfterglowDist=mainDist*0.6;float mainAfterglowStrength=exp(-mainAfterglowDist*50.0)*afterimage*0.5;vec3 mainAfterglow=afterglowColor*mainAfterglowStrength;float branchThickness=mix(0.15,1.0,easeOutSine(max(flash,0.0)));vec3 branchCore=lightningGlow(branchDist,branchBrightness*easeOutQuad(max(flash,0.0)),u_flashIntensity,branchThickness);float branchAfterglowDist=branchDist*0.7;float branchAfterglowStrength=exp(-branchAfterglowDist*80.0)*branchBrightness*afterimage*0.4;vec3 branchAfterglow=afterglowColor*branchAfterglowStrength;color+=(mainCore+branchCore)*max(flash,0.0)*region;color+=(mainAfterglow+branchAfterglow)*afterimage*region;}fragColor=vec4(color,scene.a);}\";\n\nexport const SNOW_FRAGMENT = \"#version 300 es\\nprecision highp float;in vec2 v_uv;out vec4 fragColor;uniform float u_time;uniform vec2 u_resolution;uniform sampler2D u_sceneTexture;uniform float u_intensity;uniform int u_layers;uniform float u_fallSpeed;uniform float u_windSpeed;uniform float u_windAngle;uniform float u_turbulence;uniform float u_drift;uniform float u_flutter;uniform float u_windShear;uniform float u_flakeSize;uniform float u_sizeVariation;uniform float u_opacity;uniform float u_glowAmount;uniform float u_sparkle;\\n#define PI 3.14159265359\\n#define MAX_LAYERS 6\\nfloat hash12(vec2 p){vec3 p3=fract(vec3(p.xyx)*0.1031);p3+=dot(p3,p3.yzx+33.33);return fract((p3.x+p3.y)*p3.z);}vec2 hash22(vec2 p){vec3 p3=fract(vec3(p.xyx)*vec3(0.1031,0.1030,0.0973));p3+=dot(p3,p3.yzx+33.33);return fract((p3.xx+p3.yz)*p3.zy);}float noise(vec2 p){vec2 i=floor(p);vec2 f=fract(p);f=f*f*(3.0-2.0*f);float a=hash12(i);float b=hash12(i+vec2(1.0,0.0));float c=hash12(i+vec2(0.0,1.0));float d=hash12(i+vec2(1.0,1.0));return mix(mix(a,b,f.x),mix(c,d,f.x),f.y);}vec2 rotate2D(vec2 p,float angle){float c=cos(angle);float s=sin(angle);return vec2(p.x*c-p.y*s,p.x*s+p.y*c);}float snowflakeShape(vec2 uv,float size,float seed,float rotation){vec2 rotatedUV=rotate2D(uv,rotation);float dist=length(rotatedUV);float circle=smoothstep(size,size*0.3,dist);float angle=atan(rotatedUV.y,rotatedUV.x);float hexPattern=0.5+0.5*cos(angle*6.0);hexPattern=pow(hexPattern,2.0);float crystalAmount=smoothstep(0.02,0.05,size)*0.3;float shape=mix(circle,circle*(0.7+hexPattern*0.3),crystalAmount);float glow=exp(-dist*dist/(size*size*3.0))*u_glowAmount;return shape+glow*0.4;}vec2 getWind(float layerDepth){vec2 baseWind=vec2(cos(u_windAngle),0.0)*u_windSpeed;float windResponse=mix(0.3,1.0,1.0-layerDepth);float shearResponse=1.0+u_windShear*(1.0-layerDepth)*0.35;return baseWind*windResponse*shearResponse;}float sparkle(vec2 cellId,float time,float seed){float sparklePhase=hash12(cellId+vec2(seed*100.0,0.0))*100.0;float sparkleFreq=2.0+hash12(cellId+vec2(0.0,seed*100.0))*3.0;float sparkleWave=sin(time*sparkleFreq+sparklePhase);float sparkleIntensity=pow(max(0.0,sparkleWave),16.0);float sparkleProbability=hash12(cellId+vec2(floor(time*0.5),0.0));sparkleIntensity*=step(0.85,sparkleProbability);return sparkleIntensity*u_sparkle;}vec3 snowLayer(vec2 uv,float time,float layerIndex,float totalLayers){float depth=layerIndex/max(1.0,totalLayers-1.0);float layerScale=mix(8.0,40.0,depth);float layerSpeed=u_fallSpeed*mix(1.2,0.4,depth);float layerDensity=u_intensity*mix(1.0,0.5,depth);float layerFlakeSize=u_flakeSize*mix(1.5,0.3,depth);float layerOpacity=u_opacity*mix(1.0,0.4,depth);vec2 layerOffset=vec2(sin(layerIndex*73.156)*10.0,cos(layerIndex*37.842)*10.0);vec2 p=(uv+layerOffset)*layerScale;p.y+=time*layerSpeed*2.0;vec2 baseWind=getWind(depth);p.x+=time*baseWind.x*0.3;vec2 id=floor(p);vec2 gv=fract(p)-0.5;float snow=0.0;float sparkleAccum=0.0;for(int y=-1;y<=1;y++){for(int x=-1;x<=1;x++){vec2 offs=vec2(float(x),float(y));vec2 cellId=id+offs;float h1=hash12(cellId);vec2 h2=hash22(cellId);float h3=hash12(cellId+vec2(127.0,311.0));float h4=hash12(cellId+vec2(271.0,183.0));if(h1>layerDensity)continue;float sizeVar=1.0+(h3-0.5)*u_sizeVariation;float size=layerFlakeSize*sizeVar*0.04;vec2 flakePos=h2*0.8-0.4;float flutterPhase=h3*PI*2.0;float flutterAmp=u_flutter*0.15*(1.0-depth);flakePos.x+=sin(time*3.0+flutterPhase)*flutterAmp;flakePos.y+=cos(time*2.5+flutterPhase*1.3)*flutterAmp*0.5;float driftPhase=h4*PI*2.0+layerIndex*1.7;flakePos.x+=sin(time*0.55+driftPhase)*u_drift*0.18;float turbFreq=0.6+u_turbulence*1.4;vec2 turb=vec2(noise(cellId*0.17+time*turbFreq),noise(cellId.yx*0.17+time*turbFreq+17.0))-0.5;flakePos+=turb*(u_turbulence*0.22)*(1.0-depth);vec2 localUV=gv-offs-flakePos;float rotationSpeed=(1.5-sizeVar*0.5)*(0.5+h4*1.0);float rotationPhase=h4*PI*2.0;float rotation=time*rotationSpeed+rotationPhase;float flake=snowflakeShape(localUV,size,h1,rotation);float flakeSparkle=sparkle(cellId,time,h1)*flake;sparkleAccum+=flakeSparkle;snow+=flake*layerOpacity;}}return vec3(snow,sparkleAccum,depth);}void main(){vec4 scene=texture(u_sceneTexture,v_uv);vec2 uv=v_uv;float aspect=u_resolution.x/u_resolution.y;uv.x*=aspect;float snow=0.0;float totalSparkle=0.0;for(int i=u_layers-1;i>=0;i--){vec3 layerResult=snowLayer(uv,u_time,float(i),float(u_layers));snow+=layerResult.x;totalSparkle+=layerResult.y;}snow=clamp(snow,0.0,1.0);totalSparkle=clamp(totalSparkle,0.0,1.0);vec3 snowColor=vec3(0.75,0.78,0.85);vec3 sparkleColor=vec3(0.9,0.92,1.0);vec3 color=scene.rgb+snowColor*snow+sparkleColor*totalSparkle;fragColor=vec4(color,scene.a);}\";\n\nexport const COMPOSITE_FRAGMENT = \"#version 300 es\\nprecision highp float;in vec2 v_uv;out vec4 fragColor;uniform sampler2D u_sceneTexture;uniform float u_time;uniform vec2 u_resolution;uniform float u_timeOfDay;uniform vec2 u_sunPos;uniform float u_sunVisible;uniform float u_lastFlashTime;uniform float u_strikeSeed;uniform float u_lightningSceneIllumination;uniform bool u_postEnabled;uniform float u_haze;uniform float u_hazeHorizon;uniform float u_hazeDesaturation;uniform float u_hazeContrast;uniform float u_bloomIntensity;uniform float u_bloomThreshold;uniform float u_bloomKnee;uniform float u_bloomRadius;uniform float u_bloomTapScale;uniform float u_exposureIntensity;uniform float u_exposureDesaturation;uniform float u_exposureRecovery;uniform float u_godRayIntensity;uniform float u_godRayDecay;uniform float u_godRayDensity;uniform float u_godRayWeight;uniform int u_godRaySamples;\\n#define PI 3.14159265359\\n#define GODRAY_MAX_SAMPLES 32\\nfloat saturate(float x){return clamp(x,0.0,1.0);}float luminance(vec3 c){return dot(c,vec3(0.299,0.587,0.114));}float easeOutSine(float t){return sin(t*PI*0.5);}float easeInSine(float t){return 1.0-cos(t*PI*0.5);}float easeInOutSine(float t){return-(cos(PI*t)-1.0)*0.5;}float easeOutQuad(float t){return 1.0-(1.0-t)*(1.0-t);}float easeOutCubic(float t){float inv=1.0-t;return 1.0-inv*inv*inv;}float hash11(float p){p=fract(p*0.1031);p*=p+33.33;p*=p+p;return fract(p);}float flashEnvelope(float timeSinceStrike,float duration){if(timeSinceStrike<0.0||timeSinceStrike>duration)return 0.0;float t=timeSinceStrike/duration;float attackT=clamp(t/0.03,0.0,1.0);float attack=easeOutCubic(attackT);float sustainT=clamp((t-0.05)/0.65,0.0,1.0);float sustain=1.0-easeInOutSine(sustainT);float decay=exp(-t*2.0);decay=mix(decay,easeOutSine(1.0-t),0.3);float endT=clamp((t-0.75)/0.25,0.0,1.0);float endFade=1.0-easeInSine(endT);return attack*max(sustain,decay*0.4)*endFade;}float restrikeEnvelope(float timeSinceStrike,float duration,float seed){float env=flashEnvelope(timeSinceStrike,duration*0.7);if(hash11(seed*7.7)>0.7){float restrike1=flashEnvelope(timeSinceStrike-duration*0.5,duration*0.3);env=max(env,restrike1*0.6);}if(hash11(seed*11.3)>0.85){float restrike2=flashEnvelope(timeSinceStrike-duration*0.75,duration*0.2);env=max(env,restrike2*0.4);}return env;}float getSunAltitudeFromTimeOfDay(float timeOfDay){float sunAlt=timeOfDay<0.5?timeOfDay*2.0:2.0-timeOfDay*2.0;return sunAlt*2.0-1.0;}vec3 applyHaze(vec3 color,vec2 uv){float haze=saturate(u_haze);if(haze<=0.0001)return color;float horizon=pow(1.0-uv.y,1.8);float hazeWeight=haze*mix(1.0,horizon,saturate(u_hazeHorizon));float sunAlt=getSunAltitudeFromTimeOfDay(u_timeOfDay);float daylight=smoothstep(-0.12,0.1,sunAlt);vec3 hazeDay=vec3(0.60,0.70,0.85);vec3 hazeNight=vec3(0.06,0.07,0.10);vec3 hazeColor=mix(hazeNight,hazeDay,daylight);color=mix(color,hazeColor,hazeWeight*0.55);float contrast=saturate(1.0-hazeWeight*saturate(u_hazeContrast));color=mix(vec3(0.5),color,contrast);float gray=luminance(color);float sat=saturate(1.0-hazeWeight*saturate(u_hazeDesaturation));color=mix(vec3(gray),color,sat);return color;}vec3 bloomTap(vec2 uv){vec3 c=texture(u_sceneTexture,clamp(uv,0.0,1.0)).rgb;float l=luminance(c);float knee=max(0.0001,u_bloomKnee);float m=smoothstep(u_bloomThreshold,u_bloomThreshold+knee,l);return c*m;}vec3 computeBloom(vec2 uv){float intensity=u_bloomIntensity;if(intensity<=0.0001)return vec3(0.0);vec2 texel=1.0/max(u_resolution,vec2(1.0));float radiusPx=max(0.0,u_bloomRadius)*(u_resolution.y*0.02);radiusPx*=max(0.25,u_bloomTapScale);vec2 d=texel*radiusPx;vec3 sum=vec3(0.0);sum+=bloomTap(uv)*0.20;sum+=bloomTap(uv+vec2(d.x,0.0))*0.12;sum+=bloomTap(uv+vec2(-d.x,0.0))*0.12;sum+=bloomTap(uv+vec2(0.0,d.y))*0.12;sum+=bloomTap(uv+vec2(0.0,-d.y))*0.12;sum+=bloomTap(uv+vec2(d.x,d.y))*0.08;sum+=bloomTap(uv+vec2(-d.x,d.y))*0.08;sum+=bloomTap(uv+vec2(d.x,-d.y))*0.08;sum+=bloomTap(uv+vec2(-d.x,-d.y))*0.08;return sum*intensity;}vec3 applyExposureResponse(vec3 color,float flashStrength){if(flashStrength<=0.0001)return color;float t=saturate(flashStrength);float gain=1.0+flashStrength*2.2;vec3 lifted=color*gain;vec3 tonemapped=1.0-exp(-lifted);vec3 outColor=mix(color,tonemapped,t);float gray=luminance(outColor);float desat=saturate(u_exposureDesaturation)*t;outColor=mix(outColor,vec3(gray),desat);outColor=mix(outColor,vec3(1.0),t*0.06);return outColor;}vec3 computeGodRays(vec2 uv){if(u_godRayIntensity<=0.0001)return vec3(0.0);if(u_godRaySamples<=0)return vec3(0.0);if(u_sunVisible<=0.001)return vec3(0.0);float sunAlt=getSunAltitudeFromTimeOfDay(u_timeOfDay);float daylight=smoothstep(-0.12,0.1,sunAlt);float lowSun=1.0-smoothstep(0.25,0.75,max(0.0,sunAlt));vec2 sunUV=clamp(u_sunPos,vec2(-0.25),vec2(1.25));vec2 delta=(uv-sunUV)*(u_godRayDensity/float(u_godRaySamples));vec2 coord=uv;float illuminationDecay=1.0;float accum=0.0;for(int i=0;i<GODRAY_MAX_SAMPLES;i++){if(i>=u_godRaySamples)break;coord-=delta;vec4 s=texture(u_sceneTexture,clamp(coord,0.0,1.0));float transmittance=1.0-saturate(s.a);float sampleLum=luminance(s.rgb);float brightMask=saturate((sampleLum-0.85)/0.15);brightMask*=brightMask;float raySample=transmittance*brightMask;accum+=raySample*illuminationDecay*u_godRayWeight;illuminationDecay*=u_godRayDecay;}vec3 rayColor=mix(vec3(0.7,0.72,0.8),vec3(1.0,0.92,0.75),daylight);float intensity=u_godRayIntensity*saturate(u_sunVisible)*daylight*lowSun;return rayColor*accum*intensity;}void main(){vec4 scene=texture(u_sceneTexture,v_uv);vec3 color=scene.rgb;if(!u_postEnabled){fragColor=vec4(color,1.0);return;}color+=computeGodRays(v_uv);color+=computeBloom(v_uv);float flashStrength=0.0;if(u_exposureIntensity>0.0001){float timeSinceStrike=u_time-u_lastFlashTime;float durationSec=0.8;float f=restrikeEnvelope(timeSinceStrike,durationSec,u_strikeSeed);float afterimageDuration=durationSec*1.5;float afterT=clamp((timeSinceStrike*max(0.05,u_exposureRecovery))/afterimageDuration,0.0,1.0);float afterimage=timeSinceStrike<0.0?0.0:(1.0-easeInSine(afterT));float sceneFlash=f*max(0.0,u_lightningSceneIllumination);color+=vec3(0.3,0.32,0.4)*sceneFlash;flashStrength=f*u_exposureIntensity;flashStrength=max(flashStrength,afterimage*u_exposureIntensity*0.12);color=applyExposureResponse(color,flashStrength);}color=applyHaze(color,v_uv);fragColor=vec4(clamp(color,0.0,1.0),1.0);}\";\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/glass-panel-svg.tsx",
    "content": "// Runtime entrypoint for generated glass SVG helpers.\n// Authoring source: lib/weather-authoring/runtime/glass-panel-svg.tsx\n\nexport {\n  GlassPanel,\n  GlassPanelCSS,\n  GlassPanelUnderlay,\n  useGlassStyles,\n} from \"./generated/glass-panel-svg.generated\";\nexport type {\n  GlassPanelProps,\n  GlassPanelUnderlayProps,\n  UseGlassStylesOptions,\n} from \"./generated/glass-panel-svg.generated\";\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/glass-style-resolver.ts",
    "content": "import type { CSSProperties } from \"react\";\n\n/**\n * `useGlassStyles()` returns an empty object until the element has non-zero\n * dimensions (and the browser supports `backdrop-filter`). In those cases we\n * still want a readable frosted panel, so fall back to a simple blur.\n */\nexport function resolveGlassBackdropFilterStyles({\n  glassStyles,\n  blurAmount,\n}: {\n  glassStyles: CSSProperties;\n  blurAmount: number;\n}): CSSProperties {\n  const svgGlassActive = Boolean((glassStyles as CSSProperties).backdropFilter);\n  if (svgGlassActive) return glassStyles;\n\n  const blur = `blur(${blurAmount}px)`;\n  return {\n    backdropFilter: blur,\n    WebkitBackdropFilter: blur,\n  };\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/index.ts",
    "content": "export { EffectCompositor } from \"./effect-compositor\";\nexport type {\n  CustomEffectProps,\n  WeatherEffectLayer,\n} from \"./custom-effect-props\";\nexport {\n  mapWeatherToEffects,\n  getSunAltitude,\n  isNightTime,\n  getTimeOfDay,\n  getMoonPhase,\n  getSceneBrightness,\n  getSceneBrightnessFromTimeOfDay,\n  timeOfDayToSunAltitude,\n  getWeatherTheme,\n} from \"./parameter-mapper\";\nexport type { WeatherTheme } from \"./parameter-mapper\";\nexport type {\n  EffectSettings,\n  EffectQuality,\n  EffectLayerConfig,\n  WeatherEffectParams,\n  CelestialConfig,\n} from \"./types\";\n\nexport { WeatherEffectsCanvas } from \"./weather-effects-canvas\";\nexport type {\n  WeatherEffectsCanvasProps,\n  CelestialParams,\n  CloudParams,\n  RainParams,\n  LightningParams,\n  SnowParams,\n  InteractionParams,\n  LayerToggles,\n} from \"./weather-effects-types\";\n\nexport * from \"./tuning\";\nexport { TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES } from \"./generated/tuned-presets.generated\";\n\nexport {\n  GlassPanel,\n  GlassPanelCSS,\n  GlassPanelUnderlay,\n  useGlassStyles,\n} from \"./glass-panel-svg\";\nexport { resolveGlassBackdropFilterStyles } from \"./glass-style-resolver\";\nexport {\n  mapWeatherCompositorParamsToCanvasProps,\n  resolveConditionCheckpointOverridesForTime,\n  resolveWeatherEffectsCanvasProps,\n} from \"./canvas-resolver\";\nexport type {\n  WeatherEffectsCheckpointMode,\n  WeatherStudioCompositorParams,\n} from \"./canvas-resolver\";\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/parameter-mapper.generated.js",
    "content": "// AUTO-GENERATED by `pnpm weather:compile`.\n// Source: lib/weather-authoring/weather-widget/effects/parameter-mapper.ts\n// DO NOT EDIT MANUALLY.\n\nexport function getTimeOfDay(timestamp) {\n  if (!timestamp) {\n    return 0.5;\n  }\n  const date = new Date(timestamp);\n  const hours = date.getUTCHours();\n  const minutes = date.getUTCMinutes();\n  return (hours + minutes / 60) / 24;\n}\nexport function getMoonPhase(timestamp) {\n  if (!timestamp) {\n    return 0.5;\n  }\n  const date = new Date(timestamp);\n  date.setUTCHours(0, 0, 0, 0);\n  const knownNewMoon = new Date(\"2000-01-06T00:00:00Z\");\n  const daysSinceNewMoon =\n    (date.getTime() - knownNewMoon.getTime()) / (1000 * 60 * 60 * 24);\n  const synodicMonth = 29.530588853;\n  const phaseDays =\n    ((daysSinceNewMoon % synodicMonth) + synodicMonth) % synodicMonth;\n  return phaseDays / synodicMonth;\n}\nexport function getSunAltitude(timestamp) {\n  if (!timestamp) {\n    return 0.5;\n  }\n  const date = new Date(timestamp);\n  const hours = date.getUTCHours() + date.getUTCMinutes() / 60;\n  if (hours < 6) {\n    return -1 + hours / 6;\n  } else if (hours < 12) {\n    return (hours - 6) / 6;\n  } else if (hours < 18) {\n    return 1 - (hours - 12) / 6;\n  } else {\n    return -(hours - 18) / 6;\n  }\n}\nexport function isNightTime(sunAltitude) {\n  return sunAltitude < 0;\n}\nconst CONDITION_BRIGHTNESS = {\n  clear: 1.0,\n  \"partly-cloudy\": 0.9,\n  cloudy: 0.8,\n  overcast: 0.65,\n  fog: 0.7,\n  drizzle: 0.7,\n  rain: 0.6,\n  \"heavy-rain\": 0.45,\n  thunderstorm: 0.3,\n  snow: 0.8,\n  sleet: 0.65,\n  hail: 0.5,\n  windy: 0.9,\n};\nexport function getSceneBrightness(timestamp, conditionCode = \"clear\") {\n  const sunAltitude = getSunAltitude(timestamp);\n  let solarBrightness;\n  if (sunAltitude < 0) {\n    solarBrightness = 0.05 + (1 + sunAltitude) * 0.1;\n  } else {\n    solarBrightness = 0.15 + sunAltitude * 0.85;\n  }\n  const conditionModifier = CONDITION_BRIGHTNESS[conditionCode];\n  const brightness = solarBrightness * conditionModifier;\n  return Math.max(0, Math.min(1, brightness));\n}\nexport function getWeatherTheme(brightness, currentTheme) {\n  const DARK_THRESHOLD = 0.35;\n  const LIGHT_THRESHOLD = 0.45;\n  if (brightness < DARK_THRESHOLD) {\n    return \"dark\";\n  }\n  if (brightness > LIGHT_THRESHOLD) {\n    return \"light\";\n  }\n  return currentTheme ?? \"dark\";\n}\nexport function timeOfDayToSunAltitude(timeOfDay) {\n  const hours = timeOfDay * 24;\n  if (hours < 6) {\n    return -1 + hours / 6;\n  } else if (hours < 12) {\n    return (hours - 6) / 6;\n  } else if (hours < 18) {\n    return 1 - (hours - 12) / 6;\n  } else {\n    return -(hours - 18) / 6;\n  }\n}\nexport function getSceneBrightnessFromTimeOfDay(\n  timeOfDay,\n  conditionCode = \"clear\",\n) {\n  const sunAltitude = timeOfDayToSunAltitude(timeOfDay);\n  let solarBrightness;\n  if (sunAltitude < 0) {\n    solarBrightness = 0.05 + (1 + sunAltitude) * 0.1;\n  } else {\n    solarBrightness = 0.15 + sunAltitude * 0.85;\n  }\n  const conditionModifier = CONDITION_BRIGHTNESS[conditionCode];\n  const brightness = solarBrightness * conditionModifier;\n  return Math.max(0, Math.min(1, brightness));\n}\nfunction mapWindSpeed(mph = 0) {\n  if (mph <= 10) return (mph / 10) * 0.3;\n  if (mph <= 25) return 0.3 + ((mph - 10) / 15) * 0.4;\n  return 0.7 + Math.min((mph - 25) / 25, 0.3);\n}\nfunction mapPrecipitation(level) {\n  switch (level) {\n    case \"light\":\n      return 0.3;\n    case \"moderate\":\n      return 0.6;\n    case \"heavy\":\n      return 1.0;\n    default:\n      return 0;\n  }\n}\nfunction mapVisibility(miles = 10) {\n  if (miles >= 10) return 0;\n  if (miles >= 5) return ((10 - miles) / 5) * 0.3;\n  return 0.3 + ((5 - miles) / 5) * 0.7;\n}\nfunction clamp01(value) {\n  return Math.max(0, Math.min(1, value));\n}\nfunction smoothstep(edge0, edge1, x) {\n  const t = clamp01((x - edge0) / (edge1 - edge0));\n  return t * t * (3 - 2 * t);\n}\nconst UNIFIED_CELESTIAL = {\n  x: 0.74,\n  y: 0.78,\n  sunSize: 0.14,\n  moonSize: 0.17,\n  starDensity: 2.0,\n  sunGlowIntensity: 3.05,\n  sunGlowSize: 0.3,\n  sunRayCount: 6,\n  sunRayLength: 3.0,\n  sunRayIntensity: 0.1,\n  moonGlowIntensity: 3.45,\n  moonGlowSize: 0.94,\n};\nconst CELESTIAL_PRESETS = {\n  clear: UNIFIED_CELESTIAL,\n  \"partly-cloudy\": UNIFIED_CELESTIAL,\n  cloudy: UNIFIED_CELESTIAL,\n  overcast: UNIFIED_CELESTIAL,\n  fog: UNIFIED_CELESTIAL,\n  drizzle: UNIFIED_CELESTIAL,\n  rain: UNIFIED_CELESTIAL,\n  \"heavy-rain\": UNIFIED_CELESTIAL,\n  thunderstorm: UNIFIED_CELESTIAL,\n  snow: UNIFIED_CELESTIAL,\n  sleet: UNIFIED_CELESTIAL,\n  hail: UNIFIED_CELESTIAL,\n  windy: UNIFIED_CELESTIAL,\n};\nconst CONDITION_PRESETS = {\n  clear: {\n    cloud: { coverage: 0.1, speed: 0.3, darkness: 0, turbulence: 0.2 },\n  },\n  \"partly-cloudy\": {\n    cloud: { coverage: 0.4, speed: 0.4, darkness: 0.1, turbulence: 0.3 },\n  },\n  cloudy: {\n    cloud: { coverage: 0.7, speed: 0.4, darkness: 0.2, turbulence: 0.3 },\n  },\n  overcast: {\n    cloud: { coverage: 0.95, speed: 0.3, darkness: 0.35, turbulence: 0.25 },\n  },\n  fog: {\n    cloud: { coverage: 0.6, speed: 0.15, darkness: 0.15, turbulence: 0.1 },\n  },\n  drizzle: {\n    cloud: { coverage: 0.75, speed: 0.35, darkness: 0.3, turbulence: 0.3 },\n    rain: { intensity: 0.25, glassDrops: true, fallingRain: true, angle: 3 },\n  },\n  rain: {\n    cloud: { coverage: 0.85, speed: 0.5, darkness: 0.4, turbulence: 0.4 },\n    rain: { intensity: 0.6, glassDrops: true, fallingRain: true, angle: 5 },\n  },\n  \"heavy-rain\": {\n    cloud: { coverage: 0.95, speed: 0.6, darkness: 0.55, turbulence: 0.5 },\n    rain: { intensity: 1.0, glassDrops: true, fallingRain: true, angle: 8 },\n  },\n  thunderstorm: {\n    cloud: { coverage: 1.0, speed: 0.7, darkness: 0.7, turbulence: 0.6 },\n    rain: { intensity: 1.0, glassDrops: true, fallingRain: true, angle: 15 },\n    lightning: {\n      enabled: true,\n      autoTrigger: true,\n      intervalMin: 4,\n      intervalMax: 12,\n    },\n  },\n  snow: {\n    cloud: { coverage: 0.7, speed: 0.25, darkness: 0.2, turbulence: 0.2 },\n    snow: { intensity: 0.7, windDrift: 0.3 },\n  },\n  sleet: {\n    cloud: { coverage: 0.8, speed: 0.4, darkness: 0.35, turbulence: 0.35 },\n    rain: { intensity: 0.5, glassDrops: true, fallingRain: true, angle: 10 },\n    snow: { intensity: 0.3, windDrift: 0.4 },\n  },\n  hail: {\n    cloud: { coverage: 0.9, speed: 0.6, darkness: 0.5, turbulence: 0.5 },\n    rain: { intensity: 0.7, glassDrops: true, fallingRain: true, angle: 5 },\n    snow: { intensity: 0.3, windDrift: 0.4 },\n  },\n  windy: {\n    cloud: { coverage: 0.5, speed: 1.0, darkness: 0.1, turbulence: 0.6 },\n  },\n};\nexport function mapWeatherToEffects(params) {\n  const {\n    conditionCode,\n    windSpeed,\n    precipitationLevel,\n    visibility,\n    timestamp,\n    timeOfDay: explicitTimeOfDay,\n  } = params;\n  const preset = CONDITION_PRESETS[conditionCode];\n  const resolvedTimeOfDay = explicitTimeOfDay ?? getTimeOfDay(timestamp);\n  const sunAltitude = timeOfDayToSunAltitude(resolvedTimeOfDay);\n  const windIntensity = mapWindSpeed(windSpeed);\n  const precipIntensity = mapPrecipitation(precipitationLevel);\n  const hazeAmount = mapVisibility(visibility);\n  const isNight = isNightTime(sunAltitude);\n  const atmosphere = {\n    sunAltitude,\n    haze: Math.max(hazeAmount, (preset.cloud?.darkness ?? 0) * 0.3),\n    starVisibility:\n      isNight && !preset.cloud\n        ? 1.0\n        : isNight\n          ? 1.0 - (preset.cloud?.coverage ?? 0)\n          : 0,\n  };\n  const config = { atmosphere };\n  if (preset.cloud) {\n    config.cloud = {\n      ...preset.cloud,\n      speed: preset.cloud.speed * (1 + windIntensity * 0.5),\n      turbulence: preset.cloud.turbulence * (1 + windIntensity * 0.3),\n    };\n  }\n  if (preset.rain) {\n    const rainIntensity =\n      precipIntensity > 0 ? precipIntensity : preset.rain.intensity;\n    config.rain = {\n      ...preset.rain,\n      intensity: rainIntensity,\n      angle: preset.rain.angle + windIntensity * 10,\n    };\n  }\n  if (preset.lightning) {\n    config.lightning = { ...preset.lightning };\n  }\n  if (preset.snow) {\n    config.snow = {\n      ...preset.snow,\n      windDrift: preset.snow.windDrift + windIntensity * 0.3,\n    };\n  }\n  const moonPhase = getMoonPhase(timestamp);\n  const celestialPreset = CELESTIAL_PRESETS[conditionCode];\n  config.celestial = {\n    timeOfDay: resolvedTimeOfDay,\n    moonPhase,\n    starDensity: isNight ? celestialPreset.starDensity : 0,\n    celestialX: celestialPreset.x,\n    celestialY: celestialPreset.y,\n    sunSize: celestialPreset.sunSize,\n    moonSize: celestialPreset.moonSize,\n    sunGlowIntensity: celestialPreset.sunGlowIntensity,\n    sunGlowSize: celestialPreset.sunGlowSize,\n    sunRayCount: celestialPreset.sunRayCount,\n    sunRayLength: celestialPreset.sunRayLength,\n    sunRayIntensity: celestialPreset.sunRayIntensity,\n    moonGlowIntensity: celestialPreset.moonGlowIntensity,\n    moonGlowSize: celestialPreset.moonGlowSize,\n  };\n  const haze = clamp01(atmosphere.haze);\n  const cloudCoverage = config.cloud?.coverage ?? 0;\n  const hasClouds = cloudCoverage > 0.001;\n  const bloomConditionBoost =\n    conditionCode === \"fog\"\n      ? 0.18\n      : conditionCode === \"thunderstorm\"\n        ? 0.12\n        : conditionCode === \"heavy-rain\"\n          ? 0.1\n          : conditionCode === \"overcast\"\n            ? 0.08\n            : conditionCode === \"cloudy\" || conditionCode === \"partly-cloudy\"\n              ? 0.06\n              : 0.04;\n  const bloomIntensity = clamp01(0.04 + bloomConditionBoost + haze * 0.22);\n  const bloomRadius = 1.1 + haze * 1.2;\n  const exposureIntensity = config.lightning?.enabled ? 0.85 : 0.0;\n  const dayFactor = smoothstep(-0.05, 0.08, sunAltitude);\n  const sunLowFactor = 1.0 - smoothstep(0.18, 0.7, Math.max(0, sunAltitude));\n  const coverageFactor = smoothstep(0.25, 0.85, cloudCoverage);\n  const notFullyOvercast = 1.0 - smoothstep(0.97, 1.0, cloudCoverage);\n  const particleFactor = 0.35 + haze * 0.65;\n  const godRayIntensity = hasClouds\n    ? clamp01(\n        dayFactor *\n          sunLowFactor *\n          coverageFactor *\n          notFullyOvercast *\n          particleFactor *\n          0.6,\n      )\n    : 0.0;\n  const post = {\n    enabled: true,\n    haze,\n    bloomIntensity,\n    bloomRadius,\n    exposureIntensity,\n    godRayIntensity,\n  };\n  config.post = post;\n  return config;\n}\nexport function configToCloudProps(config) {\n  if (!config.cloud) return null;\n  return {\n    coverage: config.cloud.coverage,\n    windSpeed: config.cloud.speed,\n    turbulence: config.cloud.turbulence,\n    sunAltitude: config.atmosphere.sunAltitude,\n    ambientDarkness: config.cloud.darkness,\n    starDensity: config.atmosphere.starVisibility * 0.5,\n  };\n}\nexport function configToRainProps(config) {\n  if (!config.rain) return null;\n  return {\n    glassIntensity: config.rain.glassDrops ? config.rain.intensity * 0.7 : 0,\n    fallingIntensity: config.rain.fallingRain ? config.rain.intensity : 0,\n    fallingAngle: config.rain.angle * 0.02,\n  };\n}\nexport function configToLightningProps(config) {\n  if (!config.lightning) return null;\n  return {\n    autoMode: config.lightning.autoTrigger,\n    autoInterval:\n      (config.lightning.intervalMin + config.lightning.intervalMax) / 2,\n  };\n}\nexport function configToSnowProps(config) {\n  if (!config.snow) return null;\n  return {\n    intensity: config.snow.intensity,\n    windSpeed: config.snow.windDrift,\n    drift: config.snow.windDrift,\n  };\n}\nexport function configToCelestialProps(config) {\n  if (!config.celestial) return null;\n  return {\n    timeOfDay: config.celestial.timeOfDay,\n    moonPhase: config.celestial.moonPhase,\n    starDensity: config.celestial.starDensity,\n    celestialX: config.celestial.celestialX,\n    celestialY: config.celestial.celestialY,\n    sunSize: config.celestial.sunSize,\n    moonSize: config.celestial.moonSize,\n    sunGlowIntensity: config.celestial.sunGlowIntensity,\n    sunGlowSize: config.celestial.sunGlowSize,\n    sunRayCount: config.celestial.sunRayCount,\n    sunRayLength: config.celestial.sunRayLength,\n    sunRayIntensity: config.celestial.sunRayIntensity,\n    moonGlowIntensity: config.celestial.moonGlowIntensity,\n    moonGlowSize: config.celestial.moonGlowSize,\n  };\n}\nexport function configToPostProps(config) {\n  return config.post ?? null;\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/parameter-mapper.ts",
    "content": "import type { WeatherConditionCode } from \"../schema\";\nimport type {\n  EffectLayerConfig,\n  WeatherEffectParams,\n  AtmosphereConfig,\n  PostProcessConfig,\n} from \"./types\";\n\n/**\n * Calculate time of day from timestamp (0-1 scale).\n * 0 = midnight, 0.25 = 6am, 0.5 = noon, 0.75 = 6pm\n */\nexport function getTimeOfDay(timestamp?: string): number {\n  if (!timestamp) {\n    return 0.5; // Default to noon\n  }\n\n  const date = new Date(timestamp);\n  const hours = date.getUTCHours();\n  const minutes = date.getUTCMinutes();\n\n  return (hours + minutes / 60) / 24;\n}\n\n/**\n * Calculate approximate moon phase from date (0-1 scale).\n * 0 = new moon, 0.5 = full moon\n * Uses a simplified 29.5 day synodic month calculation.\n */\nexport function getMoonPhase(timestamp?: string): number {\n  if (!timestamp) {\n    return 0.5; // Default to full moon\n  }\n\n  const date = new Date(timestamp);\n  // Normalize to midnight UTC so phase only changes day-to-day, not hour-to-hour\n  date.setUTCHours(0, 0, 0, 0);\n  // Known new moon reference: January 6, 2000\n  const knownNewMoon = new Date(\"2000-01-06T00:00:00Z\");\n  const daysSinceNewMoon =\n    (date.getTime() - knownNewMoon.getTime()) / (1000 * 60 * 60 * 24);\n  const synodicMonth = 29.530588853;\n\n  const phaseDays =\n    ((daysSinceNewMoon % synodicMonth) + synodicMonth) % synodicMonth;\n  return phaseDays / synodicMonth;\n}\n\n/**\n * Calculate sun altitude from timestamp using a generic day cycle.\n * Returns -1 (midnight) to 1 (noon).\n */\nexport function getSunAltitude(timestamp?: string): number {\n  if (!timestamp) {\n    return 0.5; // Default to mid-morning\n  }\n\n  const date = new Date(timestamp);\n  const hours = date.getUTCHours() + date.getUTCMinutes() / 60;\n\n  // Generic sun cycle: rises at 6am, peaks at noon, sets at 6pm\n  if (hours < 6) {\n    // Before 6am: deep night to dawn (-1 to 0)\n    return -1 + hours / 6;\n  } else if (hours < 12) {\n    // 6am to noon: rising (0 to 1)\n    return (hours - 6) / 6;\n  } else if (hours < 18) {\n    // Noon to 6pm: falling (1 to 0)\n    return 1 - (hours - 12) / 6;\n  } else {\n    // After 6pm: dusk to night (0 to -1)\n    return -(hours - 18) / 6;\n  }\n}\n\n/**\n * Determine if it's night based on sun altitude.\n */\nexport function isNightTime(sunAltitude: number): boolean {\n  return sunAltitude < 0;\n}\n\n/**\n * Condition-based brightness modifiers.\n * 1.0 = no attenuation (clear), lower = darker scene.\n * Derived from CONDITION_PRESETS cloud.darkness values.\n */\nconst CONDITION_BRIGHTNESS: Record<WeatherConditionCode, number> = {\n  clear: 1.0,\n  \"partly-cloudy\": 0.9,\n  cloudy: 0.8,\n  overcast: 0.65,\n  fog: 0.7,\n  drizzle: 0.7,\n  rain: 0.6,\n  \"heavy-rain\": 0.45,\n  thunderstorm: 0.3,\n  snow: 0.8,\n  sleet: 0.65,\n  hail: 0.5,\n  windy: 0.9,\n};\n\n/**\n * Calculate expected scene brightness from timestamp and condition.\n * Returns 0-1 where 0 = very dark (use dark theme), 1 = very bright (use light theme).\n *\n * This is a predictive calculation - it estimates brightness from weather data\n * rather than sampling the actual rendered canvas.\n */\nexport function getSceneBrightness(\n  timestamp?: string,\n  conditionCode: WeatherConditionCode = \"clear\",\n): number {\n  const sunAltitude = getSunAltitude(timestamp);\n\n  // Solar contribution: convert -1..1 to 0..1, with a curve\n  // Night (sunAltitude < 0) contributes very little light\n  // Dawn/dusk (0-0.3) is dim, midday (0.7-1.0) is bright\n  let solarBrightness: number;\n  if (sunAltitude < 0) {\n    // Night: 0.05 to 0.15 based on how deep into night\n    solarBrightness = 0.05 + (1 + sunAltitude) * 0.1;\n  } else {\n    // Day: 0.15 to 1.0\n    solarBrightness = 0.15 + sunAltitude * 0.85;\n  }\n\n  // Apply condition modifier\n  const conditionModifier = CONDITION_BRIGHTNESS[conditionCode];\n\n  // Combine: solar * condition, but condition can't make night bright\n  const brightness = solarBrightness * conditionModifier;\n\n  // Clamp to 0-1\n  return Math.max(0, Math.min(1, brightness));\n}\n\n/**\n * Determine UI theme based on scene brightness.\n * Includes hysteresis to prevent rapid toggling at threshold.\n */\nexport type WeatherTheme = \"light\" | \"dark\";\n\nexport function getWeatherTheme(\n  brightness: number,\n  currentTheme?: WeatherTheme,\n): WeatherTheme {\n  // Hysteresis thresholds\n  const DARK_THRESHOLD = 0.35;\n  const LIGHT_THRESHOLD = 0.45;\n\n  if (brightness < DARK_THRESHOLD) {\n    return \"dark\";\n  }\n  if (brightness > LIGHT_THRESHOLD) {\n    return \"light\";\n  }\n\n  // In the hysteresis zone (0.35-0.45): keep current theme\n  return currentTheme ?? \"dark\";\n}\n\n/**\n * Convert timeOfDay (0-1) to sun altitude (-1 to 1).\n * 0 = midnight (-1), 0.25 = 6am (0), 0.5 = noon (1), 0.75 = 6pm (0)\n */\nexport function timeOfDayToSunAltitude(timeOfDay: number): number {\n  // Map 0-1 to 0-24 hours\n  const hours = timeOfDay * 24;\n\n  if (hours < 6) {\n    // Before 6am: deep night to dawn (-1 to 0)\n    return -1 + hours / 6;\n  } else if (hours < 12) {\n    // 6am to noon: rising (0 to 1)\n    return (hours - 6) / 6;\n  } else if (hours < 18) {\n    // Noon to 6pm: falling (1 to 0)\n    return 1 - (hours - 12) / 6;\n  } else {\n    // After 6pm: dusk to night (0 to -1)\n    return -(hours - 18) / 6;\n  }\n}\n\n/**\n * Calculate scene brightness from timeOfDay (0-1) and condition.\n * Returns 0-1 where 0 = very dark (use dark theme), 1 = very bright (use light theme).\n */\nexport function getSceneBrightnessFromTimeOfDay(\n  timeOfDay: number,\n  conditionCode: WeatherConditionCode = \"clear\",\n): number {\n  const sunAltitude = timeOfDayToSunAltitude(timeOfDay);\n\n  let solarBrightness: number;\n  if (sunAltitude < 0) {\n    solarBrightness = 0.05 + (1 + sunAltitude) * 0.1;\n  } else {\n    solarBrightness = 0.15 + sunAltitude * 0.85;\n  }\n\n  const conditionModifier = CONDITION_BRIGHTNESS[conditionCode];\n  const brightness = solarBrightness * conditionModifier;\n\n  return Math.max(0, Math.min(1, brightness));\n}\n\n/**\n * Map wind speed (mph) to effect intensity.\n * 0-10 mph: subtle, 10-25 mph: moderate, 25+ mph: dramatic\n */\nfunction mapWindSpeed(mph: number = 0): number {\n  if (mph <= 10) return (mph / 10) * 0.3;\n  if (mph <= 25) return 0.3 + ((mph - 10) / 15) * 0.4;\n  return 0.7 + Math.min((mph - 25) / 25, 0.3);\n}\n\n/**\n * Map precipitation level to intensity (0-1).\n */\nfunction mapPrecipitation(\n  level?: \"none\" | \"light\" | \"moderate\" | \"heavy\",\n): number {\n  switch (level) {\n    case \"light\":\n      return 0.3;\n    case \"moderate\":\n      return 0.6;\n    case \"heavy\":\n      return 1.0;\n    default:\n      return 0;\n  }\n}\n\n/**\n * Map visibility (miles) to haze amount (0-1).\n * 10+ miles: clear (0), 5-10: light haze, <5: heavy haze\n */\nfunction mapVisibility(miles: number = 10): number {\n  if (miles >= 10) return 0;\n  if (miles >= 5) return ((10 - miles) / 5) * 0.3;\n  return 0.3 + ((5 - miles) / 5) * 0.7;\n}\n\nfunction clamp01(value: number): number {\n  return Math.max(0, Math.min(1, value));\n}\n\nfunction smoothstep(edge0: number, edge1: number, x: number): number {\n  const t = clamp01((x - edge0) / (edge1 - edge0));\n  return t * t * (3 - 2 * t);\n}\n\n/**\n * Celestial position, size, and atmospheric effect presets per condition.\n * Based on Arnheim compositional principles and atmospheric optics:\n *\n * Position (x, y):\n * - Upper-right quadrant (0.6-0.75 x) balances left-heavy UI chrome\n * - Higher y = open sky feeling; lower y = oppressive/dramatic\n * - Thunderstorm sits lowest (0.50), clear/windy highest (0.76-0.78)\n *\n * Size:\n * - Atmospheric scattering makes sun LARGER in fog/haze (0.28 in fog)\n * - Clear/windy = crisp, tight sun (0.09-0.10)\n *\n * Rays:\n * - Only visible in clear conditions (clear, partly-cloudy, windy, snow)\n * - Ray count 0 = disabled for rain/overcast/thunderstorm\n * - Wind clears air = maximum ray intensity (0.7)\n *\n * Glow:\n * - Fog = maximum spread (0.7), ethereal effect\n * - Clear/windy = tight, focused (0.22-0.25)\n * - Rain washes out glow (0.25-0.35 intensity)\n */\ninterface CelestialPreset {\n  x: number;\n  y: number;\n  sunSize: number;\n  moonSize: number;\n  starDensity: number;\n  sunGlowIntensity: number;\n  sunGlowSize: number;\n  sunRayCount: number;\n  sunRayLength: number;\n  sunRayIntensity: number;\n  moonGlowIntensity: number;\n  moonGlowSize: number;\n}\n\n// Unified celestial settings across all conditions\nconst UNIFIED_CELESTIAL: CelestialPreset = {\n  x: 0.74,\n  y: 0.78,\n  sunSize: 0.14,\n  moonSize: 0.17,\n  starDensity: 2.0,\n  sunGlowIntensity: 3.05,\n  sunGlowSize: 0.3,\n  sunRayCount: 6,\n  sunRayLength: 3.0,\n  sunRayIntensity: 0.1,\n  moonGlowIntensity: 3.45,\n  moonGlowSize: 0.94,\n};\n\nconst CELESTIAL_PRESETS: Record<WeatherConditionCode, CelestialPreset> = {\n  clear: UNIFIED_CELESTIAL,\n  \"partly-cloudy\": UNIFIED_CELESTIAL,\n  cloudy: UNIFIED_CELESTIAL,\n  overcast: UNIFIED_CELESTIAL,\n  fog: UNIFIED_CELESTIAL,\n  drizzle: UNIFIED_CELESTIAL,\n  rain: UNIFIED_CELESTIAL,\n  \"heavy-rain\": UNIFIED_CELESTIAL,\n  thunderstorm: UNIFIED_CELESTIAL,\n  snow: UNIFIED_CELESTIAL,\n  sleet: UNIFIED_CELESTIAL,\n  hail: UNIFIED_CELESTIAL,\n  windy: UNIFIED_CELESTIAL,\n};\n\n/**\n * Base condition presets. These define the default effect configuration\n * for each weather condition before parameter modifiers are applied.\n */\nconst CONDITION_PRESETS: Record<\n  WeatherConditionCode,\n  Omit<EffectLayerConfig, \"atmosphere\" | \"celestial\">\n> = {\n  clear: {\n    cloud: { coverage: 0.1, speed: 0.3, darkness: 0, turbulence: 0.2 },\n  },\n\n  \"partly-cloudy\": {\n    cloud: { coverage: 0.4, speed: 0.4, darkness: 0.1, turbulence: 0.3 },\n  },\n\n  cloudy: {\n    cloud: { coverage: 0.7, speed: 0.4, darkness: 0.2, turbulence: 0.3 },\n  },\n\n  overcast: {\n    cloud: { coverage: 0.95, speed: 0.3, darkness: 0.35, turbulence: 0.25 },\n  },\n\n  fog: {\n    cloud: { coverage: 0.6, speed: 0.15, darkness: 0.15, turbulence: 0.1 },\n  },\n\n  drizzle: {\n    cloud: { coverage: 0.75, speed: 0.35, darkness: 0.3, turbulence: 0.3 },\n    rain: { intensity: 0.25, glassDrops: true, fallingRain: true, angle: 3 },\n  },\n\n  rain: {\n    cloud: { coverage: 0.85, speed: 0.5, darkness: 0.4, turbulence: 0.4 },\n    rain: { intensity: 0.6, glassDrops: true, fallingRain: true, angle: 5 },\n  },\n\n  \"heavy-rain\": {\n    cloud: { coverage: 0.95, speed: 0.6, darkness: 0.55, turbulence: 0.5 },\n    rain: { intensity: 1.0, glassDrops: true, fallingRain: true, angle: 8 },\n  },\n\n  thunderstorm: {\n    cloud: { coverage: 1.0, speed: 0.7, darkness: 0.7, turbulence: 0.6 },\n    rain: { intensity: 1.0, glassDrops: true, fallingRain: true, angle: 15 },\n    lightning: {\n      enabled: true,\n      autoTrigger: true,\n      intervalMin: 4,\n      intervalMax: 12,\n    },\n  },\n\n  snow: {\n    cloud: { coverage: 0.7, speed: 0.25, darkness: 0.2, turbulence: 0.2 },\n    snow: { intensity: 0.7, windDrift: 0.3 },\n  },\n\n  sleet: {\n    cloud: { coverage: 0.8, speed: 0.4, darkness: 0.35, turbulence: 0.35 },\n    rain: { intensity: 0.5, glassDrops: true, fallingRain: true, angle: 10 },\n    snow: { intensity: 0.3, windDrift: 0.4 },\n  },\n\n  hail: {\n    cloud: { coverage: 0.9, speed: 0.6, darkness: 0.5, turbulence: 0.5 },\n    rain: { intensity: 0.7, glassDrops: true, fallingRain: true, angle: 5 },\n    snow: { intensity: 0.3, windDrift: 0.4 },\n  },\n\n  windy: {\n    cloud: { coverage: 0.5, speed: 1.0, darkness: 0.1, turbulence: 0.6 },\n  },\n};\n\n/**\n * Maps weather parameters to effect layer configuration.\n * This is the core translation layer between schema and shaders.\n */\nexport function mapWeatherToEffects(\n  params: WeatherEffectParams,\n): EffectLayerConfig {\n  const {\n    conditionCode,\n    windSpeed,\n    precipitationLevel,\n    visibility,\n    timestamp,\n    timeOfDay: explicitTimeOfDay,\n  } = params;\n\n  // Get base preset for this condition\n  const preset = CONDITION_PRESETS[conditionCode];\n\n  // Calculate derived values.\n  // When explicit timeOfDay is provided, it is the canonical scene-time input.\n  const resolvedTimeOfDay = explicitTimeOfDay ?? getTimeOfDay(timestamp);\n  const sunAltitude = timeOfDayToSunAltitude(resolvedTimeOfDay);\n  const windIntensity = mapWindSpeed(windSpeed);\n  const precipIntensity = mapPrecipitation(precipitationLevel);\n  const hazeAmount = mapVisibility(visibility);\n  const isNight = isNightTime(sunAltitude);\n\n  // Build atmosphere config\n  const atmosphere: AtmosphereConfig = {\n    sunAltitude,\n    haze: Math.max(hazeAmount, (preset.cloud?.darkness ?? 0) * 0.3),\n    starVisibility:\n      isNight && !preset.cloud\n        ? 1.0\n        : isNight\n          ? 1.0 - (preset.cloud?.coverage ?? 0)\n          : 0,\n  };\n\n  // Build effect config, applying modifiers to preset values\n  const config: EffectLayerConfig = { atmosphere };\n\n  // Cloud layer with wind modifiers\n  if (preset.cloud) {\n    config.cloud = {\n      ...preset.cloud,\n      speed: preset.cloud.speed * (1 + windIntensity * 0.5),\n      turbulence: preset.cloud.turbulence * (1 + windIntensity * 0.3),\n    };\n  }\n\n  // Rain layer with precipitation and wind modifiers\n  if (preset.rain) {\n    const rainIntensity =\n      precipIntensity > 0 ? precipIntensity : preset.rain.intensity;\n    config.rain = {\n      ...preset.rain,\n      intensity: rainIntensity,\n      angle: preset.rain.angle + windIntensity * 10,\n    };\n  }\n\n  // Lightning layer (no modifiers, uses preset directly)\n  if (preset.lightning) {\n    config.lightning = { ...preset.lightning };\n  }\n\n  // Snow layer with wind modifiers\n  if (preset.snow) {\n    config.snow = {\n      ...preset.snow,\n      windDrift: preset.snow.windDrift + windIntensity * 0.3,\n    };\n  }\n\n  // Celestial layer - always present, calculated from timestamp + condition presets\n  const moonPhase = getMoonPhase(timestamp);\n  const celestialPreset = CELESTIAL_PRESETS[conditionCode];\n  config.celestial = {\n    timeOfDay: resolvedTimeOfDay,\n    moonPhase,\n    starDensity: isNight ? celestialPreset.starDensity : 0,\n    celestialX: celestialPreset.x,\n    celestialY: celestialPreset.y,\n    sunSize: celestialPreset.sunSize,\n    moonSize: celestialPreset.moonSize,\n    sunGlowIntensity: celestialPreset.sunGlowIntensity,\n    sunGlowSize: celestialPreset.sunGlowSize,\n    sunRayCount: celestialPreset.sunRayCount,\n    sunRayLength: celestialPreset.sunRayLength,\n    sunRayIntensity: celestialPreset.sunRayIntensity,\n    moonGlowIntensity: celestialPreset.moonGlowIntensity,\n    moonGlowSize: celestialPreset.moonGlowSize,\n  };\n\n  // ---------------------------------------------------------------------------\n  // Post-processing (air + camera response)\n  // ---------------------------------------------------------------------------\n  const haze = clamp01(atmosphere.haze);\n  const cloudCoverage = config.cloud?.coverage ?? 0;\n  const hasClouds = cloudCoverage > 0.001;\n\n  // Bloom should read as forward scatter: stronger in hazier air and for\n  // dramatic conditions, but still subtle by default.\n  const bloomConditionBoost =\n    conditionCode === \"fog\"\n      ? 0.18\n      : conditionCode === \"thunderstorm\"\n        ? 0.12\n        : conditionCode === \"heavy-rain\"\n          ? 0.1\n          : conditionCode === \"overcast\"\n            ? 0.08\n            : conditionCode === \"cloudy\" || conditionCode === \"partly-cloudy\"\n              ? 0.06\n              : 0.04;\n\n  const bloomIntensity = clamp01(0.04 + bloomConditionBoost + haze * 0.22);\n  const bloomRadius = 1.1 + haze * 1.2;\n\n  // Exposure response only matters when lightning can trigger.\n  const exposureIntensity = config.lightning?.enabled ? 0.85 : 0.0;\n\n  // Crepuscular rays need: sun above horizon, haze (particles), and cloud\n  // structure (occlusion + gaps).\n  const dayFactor = smoothstep(-0.05, 0.08, sunAltitude);\n  const sunLowFactor = 1.0 - smoothstep(0.18, 0.7, Math.max(0, sunAltitude));\n  const coverageFactor = smoothstep(0.25, 0.85, cloudCoverage);\n  const notFullyOvercast = 1.0 - smoothstep(0.97, 1.0, cloudCoverage);\n  const particleFactor = 0.35 + haze * 0.65;\n\n  const godRayIntensity = hasClouds\n    ? clamp01(\n        dayFactor *\n          sunLowFactor *\n          coverageFactor *\n          notFullyOvercast *\n          particleFactor *\n          0.6,\n      )\n    : 0.0;\n\n  const post: PostProcessConfig = {\n    enabled: true,\n    haze,\n    bloomIntensity,\n    bloomRadius,\n    exposureIntensity,\n    godRayIntensity,\n  };\n  config.post = post;\n\n  return config;\n}\n\n/**\n * Convert EffectLayerConfig to individual canvas prop objects.\n * This provides the final uniform values for each effect shader.\n */\nexport function configToCloudProps(config: EffectLayerConfig) {\n  if (!config.cloud) return null;\n\n  return {\n    coverage: config.cloud.coverage,\n    windSpeed: config.cloud.speed,\n    turbulence: config.cloud.turbulence,\n    sunAltitude: config.atmosphere.sunAltitude,\n    ambientDarkness: config.cloud.darkness,\n    starDensity: config.atmosphere.starVisibility * 0.5,\n  };\n}\n\nexport function configToRainProps(config: EffectLayerConfig) {\n  if (!config.rain) return null;\n\n  return {\n    glassIntensity: config.rain.glassDrops ? config.rain.intensity * 0.7 : 0,\n    fallingIntensity: config.rain.fallingRain ? config.rain.intensity : 0,\n    fallingAngle: config.rain.angle * 0.02, // Convert degrees to radians-ish\n  };\n}\n\nexport function configToLightningProps(config: EffectLayerConfig) {\n  if (!config.lightning) return null;\n\n  return {\n    autoMode: config.lightning.autoTrigger,\n    autoInterval:\n      (config.lightning.intervalMin + config.lightning.intervalMax) / 2,\n  };\n}\n\nexport function configToSnowProps(config: EffectLayerConfig) {\n  if (!config.snow) return null;\n\n  return {\n    intensity: config.snow.intensity,\n    windSpeed: config.snow.windDrift,\n    drift: config.snow.windDrift,\n  };\n}\n\nexport function configToCelestialProps(config: EffectLayerConfig) {\n  if (!config.celestial) return null;\n\n  return {\n    timeOfDay: config.celestial.timeOfDay,\n    moonPhase: config.celestial.moonPhase,\n    starDensity: config.celestial.starDensity,\n    celestialX: config.celestial.celestialX,\n    celestialY: config.celestial.celestialY,\n    sunSize: config.celestial.sunSize,\n    moonSize: config.celestial.moonSize,\n    sunGlowIntensity: config.celestial.sunGlowIntensity,\n    sunGlowSize: config.celestial.sunGlowSize,\n    sunRayCount: config.celestial.sunRayCount,\n    sunRayLength: config.celestial.sunRayLength,\n    sunRayIntensity: config.celestial.sunRayIntensity,\n    moonGlowIntensity: config.celestial.moonGlowIntensity,\n    moonGlowSize: config.celestial.moonGlowSize,\n  };\n}\n\nexport function configToPostProps(config: EffectLayerConfig) {\n  return config.post ?? null;\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/tuned-presets.ts",
    "content": "// Runtime entrypoint for generated weather tuning presets.\n// Authoring source: lib/weather-authoring/presets/tuned-presets.json\n\nexport { TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES } from \"./generated/tuned-presets.generated\";\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/tuning.ts",
    "content": "import type { WeatherConditionCode } from \"../schema\";\nimport type {\n  WeatherEffectsCanvasProps,\n  LayerToggles,\n  CelestialParams,\n  CloudParams,\n  RainParams,\n  LightningParams,\n  SnowParams,\n  GlassParams,\n  InteractionParams,\n  PostProcessParams,\n} from \"./weather-effects-types\";\n\nexport type TimeCheckpoint = \"dawn\" | \"noon\" | \"dusk\" | \"midnight\";\n\nexport const TIME_CHECKPOINTS: Record<TimeCheckpoint, number> = {\n  dawn: 0.25,\n  noon: 0.5,\n  dusk: 0.75,\n  midnight: 0.0,\n};\n\nexport const TIME_CHECKPOINT_ORDER: TimeCheckpoint[] = [\n  \"dawn\",\n  \"noon\",\n  \"dusk\",\n  \"midnight\",\n];\n\nexport interface WeatherEffectsOverrides {\n  layers?: Partial<LayerToggles>;\n  celestial?: Partial<CelestialParams>;\n  cloud?: Partial<CloudParams>;\n  rain?: Partial<RainParams>;\n  lightning?: Partial<LightningParams>;\n  snow?: Partial<SnowParams>;\n  glass?: Partial<GlassParams>;\n  interactions?: Partial<InteractionParams>;\n  post?: Partial<PostProcessParams>;\n}\n\nexport interface WeatherEffectsCheckpointOverrides {\n  dawn: WeatherEffectsOverrides;\n  noon: WeatherEffectsOverrides;\n  dusk: WeatherEffectsOverrides;\n  midnight: WeatherEffectsOverrides;\n}\n\nexport type WeatherEffectsTunedPresets = Partial<\n  Record<WeatherConditionCode, WeatherEffectsCheckpointOverrides>\n>;\n\nexport function getNearestCheckpoint(timeOfDay: number): TimeCheckpoint {\n  const normalized = ((timeOfDay % 1) + 1) % 1;\n\n  let nearest: TimeCheckpoint = \"noon\";\n  let minDist = Infinity;\n\n  for (const checkpoint of TIME_CHECKPOINT_ORDER) {\n    const value = TIME_CHECKPOINTS[checkpoint];\n    let dist = Math.abs(normalized - value);\n    if (dist > 0.5) dist = 1 - dist;\n    const isTie = Math.abs(dist - minDist) <= Number.EPSILON;\n    if (dist < minDist || (isTie && checkpoint === \"midnight\")) {\n      minDist = dist;\n      nearest = checkpoint;\n    }\n  }\n\n  return nearest;\n}\n\nfunction mergeGroup<T extends object>(\n  base: Partial<T> | undefined,\n  override: Partial<T> | undefined,\n): Partial<T> | undefined {\n  if (!base && !override) return undefined;\n  return { ...base, ...override };\n}\n\n/**\n * Merge tuned overrides into computed canvas props.\n *\n * This is intentionally a shallow merge per parameter group.\n * Missing override fields fall back to the base props (and then canvas defaults).\n */\nexport function applyWeatherEffectsOverrides(\n  base: WeatherEffectsCanvasProps,\n  overrides: WeatherEffectsOverrides,\n): WeatherEffectsCanvasProps {\n  return {\n    ...base,\n    layers: mergeGroup(base.layers, overrides.layers),\n    celestial: mergeGroup(base.celestial, overrides.celestial),\n    cloud: mergeGroup(base.cloud, overrides.cloud),\n    rain: mergeGroup(base.rain, overrides.rain),\n    lightning: mergeGroup(base.lightning, overrides.lightning),\n    snow: mergeGroup(base.snow, overrides.snow),\n    glass: mergeGroup(base.glass, overrides.glass),\n    interactions: mergeGroup(base.interactions, overrides.interactions),\n    post: mergeGroup(base.post, overrides.post),\n  };\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/types.ts",
    "content": "import type { WeatherConditionCode } from \"../schema\";\n\nexport type EffectQuality = \"low\" | \"medium\" | \"high\" | \"auto\";\n\nexport interface EffectSettings {\n  enabled?: boolean;\n  quality?: EffectQuality;\n  reducedMotion?: boolean;\n}\n\nexport interface CloudLayerConfig {\n  coverage: number;\n  speed: number;\n  darkness: number;\n  turbulence: number;\n}\n\nexport interface RainLayerConfig {\n  intensity: number;\n  glassDrops: boolean;\n  fallingRain: boolean;\n  angle: number;\n}\n\nexport interface LightningLayerConfig {\n  enabled: boolean;\n  autoTrigger: boolean;\n  intervalMin: number;\n  intervalMax: number;\n}\n\nexport interface SnowLayerConfig {\n  intensity: number;\n  windDrift: number;\n}\n\nexport interface AtmosphereConfig {\n  sunAltitude: number;\n  haze: number;\n  starVisibility: number;\n}\n\nexport interface PostProcessConfig {\n  enabled?: boolean;\n\n  haze?: number;\n  hazeHorizon?: number;\n  hazeDesaturation?: number;\n  hazeContrast?: number;\n\n  bloomIntensity?: number;\n  bloomThreshold?: number;\n  bloomKnee?: number;\n  bloomRadius?: number;\n  bloomTapScale?: number;\n\n  exposureIntensity?: number;\n  exposureDesaturation?: number;\n  exposureRecovery?: number;\n\n  godRayIntensity?: number;\n  godRayDecay?: number;\n  godRayDensity?: number;\n  godRayWeight?: number;\n  godRaySamples?: number;\n}\n\nexport interface CelestialConfig {\n  timeOfDay: number;\n  moonPhase: number;\n  starDensity: number;\n  celestialX: number;\n  celestialY: number;\n  sunSize: number;\n  moonSize: number;\n  sunGlowIntensity: number;\n  sunGlowSize: number;\n  sunRayCount: number;\n  sunRayLength: number;\n  sunRayIntensity: number;\n  moonGlowIntensity: number;\n  moonGlowSize: number;\n}\n\nexport interface EffectLayerConfig {\n  cloud?: CloudLayerConfig;\n  rain?: RainLayerConfig;\n  lightning?: LightningLayerConfig;\n  snow?: SnowLayerConfig;\n  celestial?: CelestialConfig;\n  atmosphere: AtmosphereConfig;\n  post?: PostProcessConfig;\n}\n\nexport interface WeatherEffectParams {\n  conditionCode: WeatherConditionCode;\n  windSpeed?: number;\n  precipitationLevel?: \"none\" | \"light\" | \"moderate\" | \"heavy\";\n  visibility?: number;\n  timestamp?: string;\n  timeOfDay?: number;\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/use-glass-region.ts",
    "content": "\"use client\";\n\nimport {\n  useState,\n  useEffect,\n  useCallback,\n  useRef,\n  type RefObject,\n} from \"react\";\n\nexport interface GlassRegion {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n}\n\ninterface UseGlassRegionOptions {\n  targetRef: RefObject<HTMLElement | null>;\n  containerRef: RefObject<HTMLElement | null>;\n  padding?: number;\n  enabled?: boolean;\n}\n\nexport function useGlassRegion({\n  targetRef,\n  containerRef,\n  padding = 0,\n  enabled = true,\n}: UseGlassRegionOptions): [number, number, number, number] {\n  const [region, setRegion] = useState<[number, number, number, number]>([\n    0, 0, 1, 1,\n  ]);\n  const rafRef = useRef<number>(0);\n\n  const calculateRegion = useCallback(() => {\n    if (!enabled || !targetRef.current || !containerRef.current) {\n      return;\n    }\n\n    const target = targetRef.current.getBoundingClientRect();\n    const container = containerRef.current.getBoundingClientRect();\n\n    if (container.width === 0 || container.height === 0) {\n      return;\n    }\n\n    const x = Math.max(\n      0,\n      (target.left - container.left - padding) / container.width,\n    );\n    const y = Math.max(\n      0,\n      (target.top - container.top - padding) / container.height,\n    );\n    const width = Math.min(\n      1 - x,\n      (target.width + padding * 2) / container.width,\n    );\n    const height = Math.min(\n      1 - y,\n      (target.height + padding * 2) / container.height,\n    );\n\n    setRegion([x, 1 - y - height, width, height]);\n  }, [targetRef, containerRef, padding, enabled]);\n\n  const scheduleUpdate = useCallback(() => {\n    if (rafRef.current) {\n      cancelAnimationFrame(rafRef.current);\n    }\n    rafRef.current = requestAnimationFrame(calculateRegion);\n  }, [calculateRegion]);\n\n  useEffect(() => {\n    if (!enabled) return;\n\n    calculateRegion();\n\n    const targetObserver = new ResizeObserver(scheduleUpdate);\n    const containerObserver = new ResizeObserver(scheduleUpdate);\n\n    if (targetRef.current) {\n      targetObserver.observe(targetRef.current);\n    }\n    if (containerRef.current) {\n      containerObserver.observe(containerRef.current);\n    }\n\n    window.addEventListener(\"resize\", scheduleUpdate);\n    window.addEventListener(\"scroll\", scheduleUpdate, { passive: true });\n\n    return () => {\n      targetObserver.disconnect();\n      containerObserver.disconnect();\n      window.removeEventListener(\"resize\", scheduleUpdate);\n      window.removeEventListener(\"scroll\", scheduleUpdate);\n      if (rafRef.current) {\n        cancelAnimationFrame(rafRef.current);\n      }\n    };\n  }, [enabled, calculateRegion, scheduleUpdate, targetRef, containerRef]);\n\n  return region;\n}\n\nexport function useContainerQuery(\n  ref: RefObject<HTMLElement | null>,\n  breakpoints: { name: string; minWidth: number }[],\n): string {\n  const [activeBreakpoint, setActiveBreakpoint] = useState<string>(\"\");\n\n  useEffect(() => {\n    if (!ref.current) return;\n\n    const observer = new ResizeObserver((entries) => {\n      const entry = entries[0];\n      if (!entry) return;\n\n      const width = entry.contentRect.width;\n      let active = \"\";\n\n      for (const bp of breakpoints) {\n        if (width >= bp.minWidth) {\n          active = bp.name;\n        }\n      }\n\n      setActiveBreakpoint(active);\n    });\n\n    observer.observe(ref.current);\n    return () => observer.disconnect();\n  }, [ref, breakpoints]);\n\n  return activeBreakpoint;\n}\n\nexport function useAdaptiveGlassParams(\n  containerRef: RefObject<HTMLElement | null>,\n): {\n  refractionScale: number;\n  edgeWidth: number;\n  chromaticAberration: number;\n  specularIntensity: number;\n} {\n  const breakpoint = useContainerQuery(containerRef, [\n    { name: \"xs\", minWidth: 0 },\n    { name: \"sm\", minWidth: 320 },\n    { name: \"md\", minWidth: 480 },\n    { name: \"lg\", minWidth: 640 },\n  ]);\n\n  switch (breakpoint) {\n    case \"xs\":\n      return {\n        refractionScale: 20,\n        edgeWidth: 0.2,\n        chromaticAberration: 0.8,\n        specularIntensity: 1.0,\n      };\n    case \"sm\":\n      return {\n        refractionScale: 25,\n        edgeWidth: 0.18,\n        chromaticAberration: 1.0,\n        specularIntensity: 1.2,\n      };\n    case \"md\":\n      return {\n        refractionScale: 30,\n        edgeWidth: 0.15,\n        chromaticAberration: 1.2,\n        specularIntensity: 1.5,\n      };\n    case \"lg\":\n    default:\n      return {\n        refractionScale: 35,\n        edgeWidth: 0.12,\n        chromaticAberration: 1.4,\n        specularIntensity: 1.8,\n      };\n  }\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/use-glass-styles.ts",
    "content": "\"use client\";\n\nimport { useEffect, useMemo, useState, type CSSProperties } from \"react\";\n\ninterface GlassEffectOptions {\n  depth: number;\n  radius: number;\n  strength: number;\n  chromaticAberration: number;\n  blur: number;\n  brightness: number;\n  saturation: number;\n}\n\nconst DEFAULT_GLASS_OPTIONS: GlassEffectOptions = {\n  depth: 12,\n  radius: 12,\n  strength: 40,\n  chromaticAberration: 8,\n  blur: 2,\n  brightness: 1.05,\n  saturation: 1.2,\n} as const;\n\ninterface DisplacementMapParams {\n  width: number;\n  height: number;\n  radius: number;\n  depth: number;\n}\n\nfunction buildDisplacementMapSvg({\n  width,\n  height,\n  radius,\n  depth,\n}: DisplacementMapParams): string {\n  const radiusYPct = Math.ceil((radius / height) * 15);\n  const radiusXPct = Math.ceil((radius / width) * 15);\n  const innerWidth = Math.max(0, width - 2 * depth);\n  const innerHeight = Math.max(0, height - 2 * depth);\n\n  const svg = `<svg height=\"${height}\" width=\"${width}\" viewBox=\"0 0 ${width} ${height}\" xmlns=\"http://www.w3.org/2000/svg\">\n    <style>.mix { mix-blend-mode: screen; }</style>\n    <defs>\n      <linearGradient id=\"Y\" x1=\"0\" x2=\"0\" y1=\"${radiusYPct}%\" y2=\"${100 - radiusYPct}%\">\n        <stop offset=\"0%\" stop-color=\"#0F0\" />\n        <stop offset=\"100%\" stop-color=\"#000\" />\n      </linearGradient>\n      <linearGradient id=\"X\" x1=\"${radiusXPct}%\" x2=\"${100 - radiusXPct}%\" y1=\"0\" y2=\"0\">\n        <stop offset=\"0%\" stop-color=\"#F00\" />\n        <stop offset=\"100%\" stop-color=\"#000\" />\n      </linearGradient>\n    </defs>\n    <rect x=\"0\" y=\"0\" height=\"${height}\" width=\"${width}\" fill=\"#808080\" />\n    <g filter=\"blur(2px)\">\n      <rect x=\"0\" y=\"0\" height=\"${height}\" width=\"${width}\" fill=\"#000080\" />\n      <rect x=\"0\" y=\"0\" height=\"${height}\" width=\"${width}\" fill=\"url(#Y)\" class=\"mix\" />\n      <rect x=\"0\" y=\"0\" height=\"${height}\" width=\"${width}\" fill=\"url(#X)\" class=\"mix\" />\n      <rect x=\"${depth}\" y=\"${depth}\" height=\"${innerHeight}\" width=\"${innerWidth}\" fill=\"#808080\" rx=\"${radius}\" ry=\"${radius}\" filter=\"blur(${depth}px)\" />\n    </g>\n  </svg>`;\n\n  return \"data:image/svg+xml;utf8,\" + encodeURIComponent(svg);\n}\n\ninterface DisplacementFilterParams extends DisplacementMapParams {\n  strength: number;\n  chromaticAberration: number;\n}\n\nfunction buildDisplacementFilterUrl({\n  width,\n  height,\n  radius,\n  depth,\n  strength,\n  chromaticAberration,\n}: DisplacementFilterParams): string {\n  const mapUrl = buildDisplacementMapSvg({ width, height, radius, depth });\n  const feImage = `<feImage x=\"0\" y=\"0\" height=\"${height}\" width=\"${width}\" href=\"${mapUrl}\" result=\"displacementMap\" />`;\n\n  let filterContent: string;\n  if (chromaticAberration === 0) {\n    filterContent = `\n      ${feImage}\n      <feDisplacementMap in=\"SourceGraphic\" in2=\"displacementMap\" scale=\"${strength}\" xChannelSelector=\"R\" yChannelSelector=\"G\" />\n    `;\n  } else {\n    const redScale = strength + chromaticAberration * 2;\n    const greenScale = strength + chromaticAberration;\n    const blueScale = strength;\n    filterContent = `\n      ${feImage}\n      <feDisplacementMap in=\"SourceGraphic\" in2=\"displacementMap\" scale=\"${redScale}\" xChannelSelector=\"R\" yChannelSelector=\"G\" />\n      <feColorMatrix type=\"matrix\" values=\"1 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1 0\" result=\"displacedR\" />\n      <feDisplacementMap in=\"SourceGraphic\" in2=\"displacementMap\" scale=\"${greenScale}\" xChannelSelector=\"R\" yChannelSelector=\"G\" />\n      <feColorMatrix type=\"matrix\" values=\"0 0 0 0 0  0 1 0 0 0  0 0 0 0 0  0 0 0 1 0\" result=\"displacedG\" />\n      <feDisplacementMap in=\"SourceGraphic\" in2=\"displacementMap\" scale=\"${blueScale}\" xChannelSelector=\"R\" yChannelSelector=\"G\" />\n      <feColorMatrix type=\"matrix\" values=\"0 0 0 0 0  0 0 0 0 0  0 0 1 0 0  0 0 0 1 0\" result=\"displacedB\" />\n      <feBlend in=\"displacedR\" in2=\"displacedG\" mode=\"screen\"/>\n      <feBlend in2=\"displacedB\" mode=\"screen\"/>\n    `;\n  }\n\n  const svg = `<svg height=\"${height}\" width=\"${width}\" viewBox=\"0 0 ${width} ${height}\" xmlns=\"http://www.w3.org/2000/svg\">\n    <defs>\n      <filter id=\"displace\" color-interpolation-filters=\"sRGB\">${filterContent}</filter>\n    </defs>\n  </svg>`;\n\n  return \"data:image/svg+xml;utf8,\" + encodeURIComponent(svg) + \"#displace\";\n}\n\ninterface BackdropFilterParams {\n  filterUrl: string;\n  blur: number;\n  brightness: number;\n  saturation: number;\n}\n\nfunction buildBackdropFilterValue({\n  filterUrl,\n  blur,\n  brightness,\n  saturation,\n}: BackdropFilterParams): string {\n  return `blur(${blur / 2}px) url('${filterUrl}') blur(${blur}px) brightness(${brightness}) saturate(${saturation})`;\n}\n\nfunction useSupportsBackdropFilter(): boolean {\n  const [supported, setSupported] = useState(true);\n\n  useEffect(() => {\n    const hasSupport =\n      CSS.supports(\"backdrop-filter\", \"blur(1px)\") ||\n      CSS.supports(\"-webkit-backdrop-filter\", \"blur(1px)\");\n    setSupported(hasSupport);\n  }, []);\n\n  return supported;\n}\n\nexport interface UseGlassStylesOptions {\n  width: number;\n  height: number;\n  depth?: number;\n  radius?: number;\n  strength?: number;\n  chromaticAberration?: number;\n  blur?: number;\n  brightness?: number;\n  saturation?: number;\n  enabled?: boolean;\n}\n\nexport function useGlassStyles({\n  width,\n  height,\n  depth = DEFAULT_GLASS_OPTIONS.depth,\n  radius = DEFAULT_GLASS_OPTIONS.radius,\n  strength = DEFAULT_GLASS_OPTIONS.strength,\n  chromaticAberration = DEFAULT_GLASS_OPTIONS.chromaticAberration,\n  blur = DEFAULT_GLASS_OPTIONS.blur,\n  brightness = DEFAULT_GLASS_OPTIONS.brightness,\n  saturation = DEFAULT_GLASS_OPTIONS.saturation,\n  enabled = true,\n}: UseGlassStylesOptions): CSSProperties {\n  const supported = useSupportsBackdropFilter();\n\n  return useMemo(() => {\n    if (!enabled || !supported || width <= 0 || height <= 0) {\n      return {};\n    }\n\n    const filterUrl = buildDisplacementFilterUrl({\n      width,\n      height,\n      radius,\n      depth,\n      strength,\n      chromaticAberration,\n    });\n    const backdropFilter = buildBackdropFilterValue({\n      filterUrl,\n      blur,\n      brightness,\n      saturation,\n    });\n\n    return {\n      backdropFilter,\n      WebkitBackdropFilter: backdropFilter,\n    };\n  }, [\n    width,\n    height,\n    depth,\n    radius,\n    strength,\n    chromaticAberration,\n    blur,\n    brightness,\n    saturation,\n    enabled,\n    supported,\n  ]);\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/use-weather-effects-renderer.generated.js",
    "content": "// AUTO-GENERATED by `pnpm weather:compile`.\n// Source: lib/weather-authoring/weather-widget/effects/use-weather-effects-renderer.ts\n// DO NOT EDIT MANUALLY.\n\n\"use client\";\nimport { useCallback, useEffect, useRef } from \"react\";\nimport {\n  releaseWeatherWebglBudgetSlotOnInitFailure,\n  releaseWeatherWebglCanvasBudgetSlot,\n  tryAcquireWeatherWebglCanvasBudgetSlot,\n} from \"./weather-webgl-budget\";\nimport {\n  CELESTIAL_FRAGMENT,\n  CLOUD_FRAGMENT,\n  COMPOSITE_FRAGMENT,\n  FULLSCREEN_VERTEX,\n  LIGHTNING_FRAGMENT,\n  RAIN_FRAGMENT,\n  SNOW_FRAGMENT,\n} from \"./generated/weather-effect-shaders.generated\";\nimport {\n  createFramebuffer,\n  createProgram,\n  resizeFramebuffer,\n} from \"./weather-effect-gl\";\nimport {\n  clearOffscreenPass,\n  isLightningPassActive,\n  renderCelestialPass,\n  renderCloudPass,\n  renderCompositePass,\n  renderLightningPass,\n  renderRainPass,\n  renderSnowPass,\n} from \"./weather-effect-render-passes.generated.js\";\nexport function useWeatherEffectsRenderer(props) {\n  const canvasRef = useRef(null);\n  const glRef = useRef(null);\n  const animationFrameRef = useRef(0);\n  const startTimeRef = useRef(0);\n  const lastFlashTimeRef = useRef(-100);\n  const nextFlashTimeRef = useRef(0);\n  const strikeSeedRef = useRef(0);\n  const moonTextureRef = useRef(null);\n  const moonTextureLoadedRef = useRef(false);\n  const positionBufferRef = useRef(null);\n  const uniformLocationCacheRef = useRef(new WeakMap());\n  const isVisibleRef = useRef(false);\n  const isRunningRef = useRef(false);\n  const isContextLostRef = useRef(false);\n  const initFailedRef = useRef(false);\n  const hasWebglBudgetSlotRef = useRef(null);\n  const propsRef = useRef(props);\n  propsRef.current = props;\n  const programsRef = useRef({\n    celestial: null,\n    cloud: null,\n    rain: null,\n    lightning: null,\n    snow: null,\n    composite: null,\n  });\n  const fbRef = useRef({ a: null, b: null });\n  const getUniformLocationCached = useCallback((gl, program, name) => {\n    let programCache = uniformLocationCacheRef.current.get(program);\n    if (!programCache) {\n      programCache = new Map();\n      uniformLocationCacheRef.current.set(program, programCache);\n    }\n    const cached = programCache.get(name);\n    if (cached !== undefined) {\n      return cached;\n    }\n    const location = gl.getUniformLocation(program, name);\n    programCache.set(name, location);\n    return location;\n  }, []);\n  const stopRenderLoop = useCallback(() => {\n    if (animationFrameRef.current) {\n      cancelAnimationFrame(animationFrameRef.current);\n      animationFrameRef.current = 0;\n    }\n    isRunningRef.current = false;\n  }, []);\n  const releaseBudgetSlot = useCallback((canvas) => {\n    if (canvas && hasWebglBudgetSlotRef.current) {\n      releaseWeatherWebglCanvasBudgetSlot(canvas);\n    }\n    hasWebglBudgetSlotRef.current = null;\n  }, []);\n  const disposeGL = useCallback(() => {\n    stopRenderLoop();\n    const gl = glRef.current;\n    const isContextLost = isContextLostRef.current;\n    if (gl && !isContextLost) {\n      for (const program of Object.values(programsRef.current)) {\n        if (program) gl.deleteProgram(program);\n      }\n      for (const fb of [fbRef.current.a, fbRef.current.b]) {\n        if (!fb) continue;\n        gl.deleteFramebuffer(fb.fbo);\n        gl.deleteTexture(fb.texture);\n      }\n      if (moonTextureRef.current) {\n        gl.deleteTexture(moonTextureRef.current);\n      }\n      if (positionBufferRef.current) {\n        gl.deleteBuffer(positionBufferRef.current);\n      }\n    }\n    programsRef.current = {\n      celestial: null,\n      cloud: null,\n      rain: null,\n      lightning: null,\n      snow: null,\n      composite: null,\n    };\n    fbRef.current = { a: null, b: null };\n    moonTextureRef.current = null;\n    moonTextureLoadedRef.current = false;\n    positionBufferRef.current = null;\n    glRef.current = null;\n    uniformLocationCacheRef.current = new WeakMap();\n  }, [stopRenderLoop]);\n  const failInit = useCallback(\n    ({\n      canvas,\n      contextLost = false,\n      markInitFailed = true,\n      warnMessage,\n      errorMessage,\n    }) => {\n      if (contextLost) {\n        isContextLostRef.current = true;\n      }\n      if (markInitFailed) {\n        initFailedRef.current = true;\n      }\n      if (errorMessage) {\n        console.error(errorMessage);\n      }\n      if (warnMessage && process.env.NODE_ENV !== \"production\") {\n        console.warn(warnMessage);\n      }\n      disposeGL();\n      hasWebglBudgetSlotRef.current =\n        releaseWeatherWebglBudgetSlotOnInitFailure(\n          canvas,\n          hasWebglBudgetSlotRef.current,\n        );\n      return false;\n    },\n    [disposeGL],\n  );\n  const initGL = useCallback(() => {\n    if (initFailedRef.current) return false;\n    const canvas = canvasRef.current;\n    if (!canvas) return false;\n    if (hasWebglBudgetSlotRef.current === false) return false;\n    if (hasWebglBudgetSlotRef.current === null) {\n      const ok = tryAcquireWeatherWebglCanvasBudgetSlot(canvas);\n      if (!ok) {\n        hasWebglBudgetSlotRef.current = false;\n        if (process.env.NODE_ENV !== \"production\") {\n          console.warn(\n            \"[WeatherEffectsCanvas] Too many WebGL canvases on the page; rendering this widget without effects.\",\n          );\n        }\n        return false;\n      }\n      hasWebglBudgetSlotRef.current = true;\n    }\n    disposeGL();\n    isContextLostRef.current = false;\n    const gl = canvas.getContext(\"webgl2\");\n    if (!gl) {\n      return failInit({\n        canvas,\n        warnMessage:\n          \"[WeatherEffectsCanvas] WebGL2 not supported; rendering without effects.\",\n      });\n    }\n    glRef.current = gl;\n    if (gl.isContextLost()) {\n      return failInit({\n        canvas,\n        contextLost: true,\n        markInitFailed: false,\n      });\n    }\n    programsRef.current.celestial = createProgram(\n      gl,\n      FULLSCREEN_VERTEX,\n      CELESTIAL_FRAGMENT,\n    );\n    programsRef.current.cloud = createProgram(\n      gl,\n      FULLSCREEN_VERTEX,\n      CLOUD_FRAGMENT,\n    );\n    programsRef.current.rain = createProgram(\n      gl,\n      FULLSCREEN_VERTEX,\n      RAIN_FRAGMENT,\n    );\n    programsRef.current.lightning = createProgram(\n      gl,\n      FULLSCREEN_VERTEX,\n      LIGHTNING_FRAGMENT,\n    );\n    programsRef.current.snow = createProgram(\n      gl,\n      FULLSCREEN_VERTEX,\n      SNOW_FRAGMENT,\n    );\n    programsRef.current.composite = createProgram(\n      gl,\n      FULLSCREEN_VERTEX,\n      COMPOSITE_FRAGMENT,\n    );\n    if (!programsRef.current.celestial || !programsRef.current.composite) {\n      if (gl.isContextLost()) {\n        return failInit({\n          canvas,\n          contextLost: true,\n          markInitFailed: false,\n        });\n      }\n      return failInit({\n        canvas,\n        errorMessage: \"Failed to create required WebGL programs\",\n      });\n    }\n    const dpr = propsRef.current.dpr ?? window.devicePixelRatio;\n    const width = Math.max(1, Math.floor(canvas.clientWidth * dpr));\n    const height = Math.max(1, Math.floor(canvas.clientHeight * dpr));\n    const fbA = createFramebuffer(gl, width, height);\n    const fbB = createFramebuffer(gl, width, height);\n    if (!fbA || !fbB) {\n      if (gl.isContextLost()) {\n        return failInit({\n          canvas,\n          contextLost: true,\n          markInitFailed: false,\n        });\n      }\n      return failInit({\n        canvas,\n        errorMessage: \"Failed to create WebGL framebuffers\",\n      });\n    }\n    fbRef.current.a = fbA;\n    fbRef.current.b = fbB;\n    const moonTexture = gl.createTexture();\n    if (moonTexture) {\n      gl.bindTexture(gl.TEXTURE_2D, moonTexture);\n      gl.texImage2D(\n        gl.TEXTURE_2D,\n        0,\n        gl.RGBA,\n        1,\n        1,\n        0,\n        gl.RGBA,\n        gl.UNSIGNED_BYTE,\n        new Uint8Array([128, 128, 128, 255]),\n      );\n      moonTextureRef.current = moonTexture;\n      const image = new Image();\n      image.crossOrigin = \"anonymous\";\n      image.onload = () => {\n        const glCurrent = glRef.current;\n        if (!glCurrent || moonTextureRef.current !== moonTexture) return;\n        glCurrent.bindTexture(gl.TEXTURE_2D, moonTexture);\n        glCurrent.texImage2D(\n          gl.TEXTURE_2D,\n          0,\n          gl.RGBA,\n          gl.RGBA,\n          gl.UNSIGNED_BYTE,\n          image,\n        );\n        glCurrent.generateMipmap(gl.TEXTURE_2D);\n        glCurrent.texParameteri(\n          gl.TEXTURE_2D,\n          gl.TEXTURE_MIN_FILTER,\n          gl.LINEAR_MIPMAP_LINEAR,\n        );\n        glCurrent.texParameteri(\n          gl.TEXTURE_2D,\n          gl.TEXTURE_MAG_FILTER,\n          gl.LINEAR,\n        );\n        glCurrent.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);\n        glCurrent.texParameteri(\n          gl.TEXTURE_2D,\n          gl.TEXTURE_WRAP_T,\n          gl.CLAMP_TO_EDGE,\n        );\n        moonTextureLoadedRef.current = true;\n      };\n      image.src = new URL(\n        \"../assets/moon-texture.jpg\",\n        import.meta.url,\n      ).toString();\n    }\n    const positions = new Float32Array([\n      -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1,\n    ]);\n    const positionBuffer = gl.createBuffer();\n    if (!positionBuffer) {\n      if (gl.isContextLost()) {\n        return failInit({\n          canvas,\n          contextLost: true,\n          markInitFailed: false,\n        });\n      }\n      return failInit({\n        canvas,\n        errorMessage: \"Failed to create WebGL buffer\",\n      });\n    }\n    positionBufferRef.current = positionBuffer;\n    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);\n    gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);\n    for (const program of Object.values(programsRef.current)) {\n      if (!program) continue;\n      const positionLoc = gl.getAttribLocation(program, \"a_position\");\n      if (positionLoc >= 0) {\n        gl.enableVertexAttribArray(positionLoc);\n        gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);\n      }\n    }\n    startTimeRef.current = performance.now();\n    return true;\n  }, [disposeGL, failInit]);\n  const render = useCallback(() => {\n    const gl = glRef.current;\n    const canvas = canvasRef.current;\n    const programs = programsRef.current;\n    const fb = fbRef.current;\n    const runtimeProps = propsRef.current;\n    if (isContextLostRef.current || !isVisibleRef.current) {\n      isRunningRef.current = false;\n      animationFrameRef.current = 0;\n      return;\n    }\n    if (!gl || !canvas || !fb.a || !fb.b) {\n      isRunningRef.current = false;\n      return;\n    }\n    const dpr = runtimeProps.dpr ?? window.devicePixelRatio;\n    const displayWidth = Math.max(1, Math.floor(canvas.clientWidth * dpr));\n    const displayHeight = Math.max(1, Math.floor(canvas.clientHeight * dpr));\n    if (canvas.width !== displayWidth || canvas.height !== displayHeight) {\n      canvas.width = displayWidth;\n      canvas.height = displayHeight;\n      resizeFramebuffer(gl, fb.a, displayWidth, displayHeight);\n      resizeFramebuffer(gl, fb.b, displayWidth, displayHeight);\n    }\n    const time = (performance.now() - startTimeRef.current) / 1000;\n    const u = (program, name) => getUniformLocationCached(gl, program, name);\n    if (\n      runtimeProps.layers.lightning &&\n      runtimeProps.lightning.enabled &&\n      runtimeProps.lightning.autoMode &&\n      time >= nextFlashTimeRef.current\n    ) {\n      lastFlashTimeRef.current = time;\n      strikeSeedRef.current = Math.random();\n      nextFlashTimeRef.current =\n        time + runtimeProps.lightning.autoInterval * (0.5 + Math.random());\n    }\n    let readFB = fb.a;\n    let writeFB = fb.b;\n    const swapBuffers = () => {\n      const temp = readFB;\n      readFB = writeFB;\n      writeFB = temp;\n    };\n    if (runtimeProps.layers.celestial && programs.celestial) {\n      renderCelestialPass({\n        gl,\n        program: programs.celestial,\n        target: writeFB,\n        displayWidth,\n        displayHeight,\n        time,\n        params: runtimeProps.celestial,\n        moonTexture: moonTextureRef.current,\n        moonTextureLoaded: moonTextureLoadedRef.current,\n        getUniformLocation: u,\n      });\n      swapBuffers();\n    } else {\n      clearOffscreenPass(gl, writeFB, displayWidth, displayHeight);\n      swapBuffers();\n    }\n    if (runtimeProps.layers.clouds && programs.cloud) {\n      renderCloudPass({\n        gl,\n        program: programs.cloud,\n        target: writeFB,\n        sceneTexture: readFB.texture,\n        displayWidth,\n        displayHeight,\n        time,\n        params: runtimeProps.cloud,\n        celestial: runtimeProps.celestial,\n        getUniformLocation: u,\n      });\n      swapBuffers();\n    }\n    if (runtimeProps.layers.rain && programs.rain) {\n      renderRainPass({\n        gl,\n        program: programs.rain,\n        target: writeFB,\n        sceneTexture: readFB.texture,\n        displayWidth,\n        displayHeight,\n        time,\n        params: runtimeProps.rain,\n        interactions: runtimeProps.interactions,\n        getUniformLocation: u,\n      });\n      swapBuffers();\n    }\n    const lightningActive = isLightningPassActive(\n      runtimeProps.layers,\n      runtimeProps.lightning,\n      programs.lightning,\n      time,\n      lastFlashTimeRef.current,\n    );\n    if (lightningActive && programs.lightning) {\n      renderLightningPass({\n        gl,\n        program: programs.lightning,\n        target: writeFB,\n        sceneTexture: readFB.texture,\n        displayWidth,\n        displayHeight,\n        time,\n        params: runtimeProps.lightning,\n        interactions: runtimeProps.interactions,\n        lastFlashTime: lastFlashTimeRef.current,\n        strikeSeed: strikeSeedRef.current,\n        getUniformLocation: u,\n      });\n      swapBuffers();\n    }\n    if (runtimeProps.layers.snow && programs.snow) {\n      renderSnowPass({\n        gl,\n        program: programs.snow,\n        target: writeFB,\n        sceneTexture: readFB.texture,\n        displayWidth,\n        displayHeight,\n        time,\n        params: runtimeProps.snow,\n        getUniformLocation: u,\n      });\n      swapBuffers();\n    }\n    if (programs.composite) {\n      renderCompositePass({\n        gl,\n        program: programs.composite,\n        sceneTexture: readFB.texture,\n        displayWidth,\n        displayHeight,\n        time,\n        celestial: runtimeProps.celestial,\n        interactions: runtimeProps.interactions,\n        post: runtimeProps.post,\n        lastFlashTime: lastFlashTimeRef.current,\n        strikeSeed: strikeSeedRef.current,\n        getUniformLocation: u,\n      });\n    }\n    if (isVisibleRef.current && !isContextLostRef.current) {\n      isRunningRef.current = true;\n      animationFrameRef.current = requestAnimationFrame(render);\n    } else {\n      isRunningRef.current = false;\n      animationFrameRef.current = 0;\n    }\n  }, [getUniformLocationCached]);\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    const onContextLost = (e) => {\n      e.preventDefault();\n      isContextLostRef.current = true;\n      disposeGL();\n    };\n    const onContextRestored = () => {\n      isContextLostRef.current = false;\n      initFailedRef.current = false;\n      if (initGL() && isVisibleRef.current) {\n        isRunningRef.current = true;\n        render();\n      }\n    };\n    canvas.addEventListener(\"webglcontextlost\", onContextLost, {\n      passive: false,\n    });\n    canvas.addEventListener(\"webglcontextrestored\", onContextRestored);\n    const observer =\n      typeof IntersectionObserver !== \"undefined\"\n        ? new IntersectionObserver(\n            (entries) => {\n              const entry = entries[0];\n              const visible = Boolean(entry?.isIntersecting);\n              isVisibleRef.current = visible;\n              if (!visible) {\n                stopRenderLoop();\n                disposeGL();\n                releaseBudgetSlot(canvas);\n                return;\n              }\n              if (!isRunningRef.current && !isContextLostRef.current) {\n                if (glRef.current && fbRef.current.a && fbRef.current.b) {\n                  isRunningRef.current = true;\n                  render();\n                } else if (initGL()) {\n                  isRunningRef.current = true;\n                  render();\n                }\n              }\n            },\n            { threshold: 0 },\n          )\n        : null;\n    if (!observer) {\n      isVisibleRef.current = true;\n    } else {\n      observer.observe(canvas);\n    }\n    if (!observer && initGL() && isVisibleRef.current) {\n      isRunningRef.current = true;\n      render();\n    }\n    return () => {\n      observer?.disconnect();\n      canvas.removeEventListener(\"webglcontextlost\", onContextLost);\n      canvas.removeEventListener(\"webglcontextrestored\", onContextRestored);\n      disposeGL();\n      releaseBudgetSlot(canvas);\n    };\n  }, [disposeGL, initGL, releaseBudgetSlot, render, stopRenderLoop]);\n  return canvasRef;\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/use-weather-effects-renderer.ts",
    "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef } from \"react\";\nimport {\n  releaseWeatherWebglBudgetSlotOnInitFailure,\n  releaseWeatherWebglCanvasBudgetSlot,\n  tryAcquireWeatherWebglCanvasBudgetSlot,\n} from \"./weather-webgl-budget\";\nimport {\n  CELESTIAL_FRAGMENT,\n  CLOUD_FRAGMENT,\n  COMPOSITE_FRAGMENT,\n  FULLSCREEN_VERTEX,\n  LIGHTNING_FRAGMENT,\n  RAIN_FRAGMENT,\n  SNOW_FRAGMENT,\n} from \"./weather-effect-shaders\";\nimport {\n  createFramebuffer,\n  createProgram,\n  resizeFramebuffer,\n  type Framebuffer,\n} from \"./weather-effect-gl\";\nimport {\n  clearOffscreenPass,\n  isLightningPassActive,\n  renderCelestialPass,\n  renderCloudPass,\n  renderCompositePass,\n  renderLightningPass,\n  renderRainPass,\n  renderSnowPass,\n} from \"./weather-effect-render-passes\";\nimport type { ResolvedWeatherEffectsCanvasProps } from \"./weather-effects-types\";\n\ninterface WeatherEffectsPrograms {\n  celestial: WebGLProgram | null;\n  cloud: WebGLProgram | null;\n  rain: WebGLProgram | null;\n  lightning: WebGLProgram | null;\n  snow: WebGLProgram | null;\n  composite: WebGLProgram | null;\n}\n\ninterface InitFailureOptions {\n  canvas: HTMLCanvasElement;\n  contextLost?: boolean;\n  markInitFailed?: boolean;\n  warnMessage?: string;\n  errorMessage?: string;\n}\n\nexport function useWeatherEffectsRenderer(\n  props: ResolvedWeatherEffectsCanvasProps,\n) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const glRef = useRef<WebGL2RenderingContext | null>(null);\n  const animationFrameRef = useRef<number>(0);\n  const startTimeRef = useRef<number>(0);\n  const lastFlashTimeRef = useRef<number>(-100);\n  const nextFlashTimeRef = useRef<number>(0);\n  const strikeSeedRef = useRef<number>(0);\n  const moonTextureRef = useRef<WebGLTexture | null>(null);\n  const moonTextureLoadedRef = useRef<boolean>(false);\n  const positionBufferRef = useRef<WebGLBuffer | null>(null);\n  const uniformLocationCacheRef = useRef<\n    WeakMap<WebGLProgram, Map<string, WebGLUniformLocation | null>>\n  >(new WeakMap());\n  const isVisibleRef = useRef<boolean>(false);\n  const isRunningRef = useRef<boolean>(false);\n  const isContextLostRef = useRef<boolean>(false);\n  const initFailedRef = useRef<boolean>(false);\n  const hasWebglBudgetSlotRef = useRef<boolean | null>(null);\n\n  const propsRef = useRef(props);\n  propsRef.current = props;\n\n  const programsRef = useRef<WeatherEffectsPrograms>({\n    celestial: null,\n    cloud: null,\n    rain: null,\n    lightning: null,\n    snow: null,\n    composite: null,\n  });\n\n  const fbRef = useRef<{\n    a: Framebuffer | null;\n    b: Framebuffer | null;\n  }>({ a: null, b: null });\n\n  const getUniformLocationCached = useCallback(\n    (gl: WebGL2RenderingContext, program: WebGLProgram, name: string) => {\n      let programCache = uniformLocationCacheRef.current.get(program);\n      if (!programCache) {\n        programCache = new Map();\n        uniformLocationCacheRef.current.set(program, programCache);\n      }\n\n      const cached = programCache.get(name);\n      if (cached !== undefined) {\n        return cached;\n      }\n\n      const location = gl.getUniformLocation(program, name);\n      programCache.set(name, location);\n      return location;\n    },\n    [],\n  );\n\n  const stopRenderLoop = useCallback(() => {\n    if (animationFrameRef.current) {\n      cancelAnimationFrame(animationFrameRef.current);\n      animationFrameRef.current = 0;\n    }\n    isRunningRef.current = false;\n  }, []);\n\n  const releaseBudgetSlot = useCallback((canvas: HTMLCanvasElement | null) => {\n    if (canvas && hasWebglBudgetSlotRef.current) {\n      releaseWeatherWebglCanvasBudgetSlot(canvas);\n    }\n    hasWebglBudgetSlotRef.current = null;\n  }, []);\n\n  const disposeGL = useCallback(() => {\n    stopRenderLoop();\n\n    const gl = glRef.current;\n    const isContextLost = isContextLostRef.current;\n\n    if (gl && !isContextLost) {\n      for (const program of Object.values(programsRef.current)) {\n        if (program) gl.deleteProgram(program);\n      }\n\n      for (const fb of [fbRef.current.a, fbRef.current.b]) {\n        if (!fb) continue;\n        gl.deleteFramebuffer(fb.fbo);\n        gl.deleteTexture(fb.texture);\n      }\n\n      if (moonTextureRef.current) {\n        gl.deleteTexture(moonTextureRef.current);\n      }\n\n      if (positionBufferRef.current) {\n        gl.deleteBuffer(positionBufferRef.current);\n      }\n    }\n\n    programsRef.current = {\n      celestial: null,\n      cloud: null,\n      rain: null,\n      lightning: null,\n      snow: null,\n      composite: null,\n    };\n    fbRef.current = { a: null, b: null };\n    moonTextureRef.current = null;\n    moonTextureLoadedRef.current = false;\n    positionBufferRef.current = null;\n    glRef.current = null;\n    uniformLocationCacheRef.current = new WeakMap();\n  }, [stopRenderLoop]);\n\n  const failInit = useCallback(\n    ({\n      canvas,\n      contextLost = false,\n      markInitFailed = true,\n      warnMessage,\n      errorMessage,\n    }: InitFailureOptions): false => {\n      if (contextLost) {\n        isContextLostRef.current = true;\n      }\n\n      if (markInitFailed) {\n        initFailedRef.current = true;\n      }\n\n      if (errorMessage) {\n        console.error(errorMessage);\n      }\n\n      if (warnMessage && process.env.NODE_ENV !== \"production\") {\n        console.warn(warnMessage);\n      }\n\n      disposeGL();\n      hasWebglBudgetSlotRef.current =\n        releaseWeatherWebglBudgetSlotOnInitFailure(\n          canvas,\n          hasWebglBudgetSlotRef.current,\n        );\n      return false;\n    },\n    [disposeGL],\n  );\n\n  const initGL = useCallback(() => {\n    if (initFailedRef.current) return false;\n\n    const canvas = canvasRef.current;\n    if (!canvas) return false;\n\n    if (hasWebglBudgetSlotRef.current === false) return false;\n    if (hasWebglBudgetSlotRef.current === null) {\n      const ok = tryAcquireWeatherWebglCanvasBudgetSlot(canvas);\n      if (!ok) {\n        hasWebglBudgetSlotRef.current = false;\n        if (process.env.NODE_ENV !== \"production\") {\n          console.warn(\n            \"[WeatherEffectsCanvas] Too many WebGL canvases on the page; rendering this widget without effects.\",\n          );\n        }\n        return false;\n      }\n      hasWebglBudgetSlotRef.current = true;\n    }\n\n    disposeGL();\n    isContextLostRef.current = false;\n\n    const gl = canvas.getContext(\"webgl2\");\n    if (!gl) {\n      return failInit({\n        canvas,\n        warnMessage:\n          \"[WeatherEffectsCanvas] WebGL2 not supported; rendering without effects.\",\n      });\n    }\n\n    glRef.current = gl;\n    if (gl.isContextLost()) {\n      return failInit({\n        canvas,\n        contextLost: true,\n        markInitFailed: false,\n      });\n    }\n\n    programsRef.current.celestial = createProgram(\n      gl,\n      FULLSCREEN_VERTEX,\n      CELESTIAL_FRAGMENT,\n    );\n    programsRef.current.cloud = createProgram(\n      gl,\n      FULLSCREEN_VERTEX,\n      CLOUD_FRAGMENT,\n    );\n    programsRef.current.rain = createProgram(\n      gl,\n      FULLSCREEN_VERTEX,\n      RAIN_FRAGMENT,\n    );\n    programsRef.current.lightning = createProgram(\n      gl,\n      FULLSCREEN_VERTEX,\n      LIGHTNING_FRAGMENT,\n    );\n    programsRef.current.snow = createProgram(\n      gl,\n      FULLSCREEN_VERTEX,\n      SNOW_FRAGMENT,\n    );\n    programsRef.current.composite = createProgram(\n      gl,\n      FULLSCREEN_VERTEX,\n      COMPOSITE_FRAGMENT,\n    );\n\n    if (!programsRef.current.celestial || !programsRef.current.composite) {\n      if (gl.isContextLost()) {\n        return failInit({\n          canvas,\n          contextLost: true,\n          markInitFailed: false,\n        });\n      }\n\n      return failInit({\n        canvas,\n        errorMessage: \"Failed to create required WebGL programs\",\n      });\n    }\n\n    const dpr = propsRef.current.dpr ?? window.devicePixelRatio;\n    const width = Math.max(1, Math.floor(canvas.clientWidth * dpr));\n    const height = Math.max(1, Math.floor(canvas.clientHeight * dpr));\n    const fbA = createFramebuffer(gl, width, height);\n    const fbB = createFramebuffer(gl, width, height);\n\n    if (!fbA || !fbB) {\n      if (gl.isContextLost()) {\n        return failInit({\n          canvas,\n          contextLost: true,\n          markInitFailed: false,\n        });\n      }\n\n      return failInit({\n        canvas,\n        errorMessage: \"Failed to create WebGL framebuffers\",\n      });\n    }\n\n    fbRef.current.a = fbA;\n    fbRef.current.b = fbB;\n\n    const moonTexture = gl.createTexture();\n    if (moonTexture) {\n      gl.bindTexture(gl.TEXTURE_2D, moonTexture);\n      gl.texImage2D(\n        gl.TEXTURE_2D,\n        0,\n        gl.RGBA,\n        1,\n        1,\n        0,\n        gl.RGBA,\n        gl.UNSIGNED_BYTE,\n        new Uint8Array([128, 128, 128, 255]),\n      );\n      moonTextureRef.current = moonTexture;\n\n      const image = new Image();\n      image.crossOrigin = \"anonymous\";\n      image.onload = () => {\n        const glCurrent = glRef.current;\n        if (!glCurrent || moonTextureRef.current !== moonTexture) return;\n\n        glCurrent.bindTexture(gl.TEXTURE_2D, moonTexture);\n        glCurrent.texImage2D(\n          gl.TEXTURE_2D,\n          0,\n          gl.RGBA,\n          gl.RGBA,\n          gl.UNSIGNED_BYTE,\n          image,\n        );\n        glCurrent.generateMipmap(gl.TEXTURE_2D);\n        glCurrent.texParameteri(\n          gl.TEXTURE_2D,\n          gl.TEXTURE_MIN_FILTER,\n          gl.LINEAR_MIPMAP_LINEAR,\n        );\n        glCurrent.texParameteri(\n          gl.TEXTURE_2D,\n          gl.TEXTURE_MAG_FILTER,\n          gl.LINEAR,\n        );\n        glCurrent.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);\n        glCurrent.texParameteri(\n          gl.TEXTURE_2D,\n          gl.TEXTURE_WRAP_T,\n          gl.CLAMP_TO_EDGE,\n        );\n        moonTextureLoadedRef.current = true;\n      };\n      image.src = new URL(\n        \"../assets/moon-texture.jpg\",\n        import.meta.url,\n      ).toString();\n    }\n\n    const positions = new Float32Array([\n      -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1,\n    ]);\n    const positionBuffer = gl.createBuffer();\n\n    if (!positionBuffer) {\n      if (gl.isContextLost()) {\n        return failInit({\n          canvas,\n          contextLost: true,\n          markInitFailed: false,\n        });\n      }\n\n      return failInit({\n        canvas,\n        errorMessage: \"Failed to create WebGL buffer\",\n      });\n    }\n\n    positionBufferRef.current = positionBuffer;\n    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);\n    gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);\n\n    for (const program of Object.values(programsRef.current)) {\n      if (!program) continue;\n      const positionLoc = gl.getAttribLocation(program, \"a_position\");\n      if (positionLoc >= 0) {\n        gl.enableVertexAttribArray(positionLoc);\n        gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);\n      }\n    }\n\n    startTimeRef.current = performance.now();\n    return true;\n  }, [disposeGL, failInit]);\n\n  const render = useCallback(() => {\n    const gl = glRef.current;\n    const canvas = canvasRef.current;\n    const programs = programsRef.current;\n    const fb = fbRef.current;\n    const runtimeProps = propsRef.current;\n\n    if (isContextLostRef.current || !isVisibleRef.current) {\n      isRunningRef.current = false;\n      animationFrameRef.current = 0;\n      return;\n    }\n\n    if (!gl || !canvas || !fb.a || !fb.b) {\n      isRunningRef.current = false;\n      return;\n    }\n\n    const dpr = runtimeProps.dpr ?? window.devicePixelRatio;\n    const displayWidth = Math.max(1, Math.floor(canvas.clientWidth * dpr));\n    const displayHeight = Math.max(1, Math.floor(canvas.clientHeight * dpr));\n\n    if (canvas.width !== displayWidth || canvas.height !== displayHeight) {\n      canvas.width = displayWidth;\n      canvas.height = displayHeight;\n      resizeFramebuffer(gl, fb.a, displayWidth, displayHeight);\n      resizeFramebuffer(gl, fb.b, displayWidth, displayHeight);\n    }\n\n    const time = (performance.now() - startTimeRef.current) / 1000;\n\n    const u = (program: WebGLProgram, name: string) =>\n      getUniformLocationCached(gl, program, name);\n\n    if (\n      runtimeProps.layers.lightning &&\n      runtimeProps.lightning.enabled &&\n      runtimeProps.lightning.autoMode &&\n      time >= nextFlashTimeRef.current\n    ) {\n      lastFlashTimeRef.current = time;\n      strikeSeedRef.current = Math.random();\n      nextFlashTimeRef.current =\n        time + runtimeProps.lightning.autoInterval * (0.5 + Math.random());\n    }\n\n    let readFB = fb.a;\n    let writeFB = fb.b;\n\n    const swapBuffers = () => {\n      const temp = readFB;\n      readFB = writeFB;\n      writeFB = temp;\n    };\n\n    if (runtimeProps.layers.celestial && programs.celestial) {\n      renderCelestialPass({\n        gl,\n        program: programs.celestial,\n        target: writeFB,\n        displayWidth,\n        displayHeight,\n        time,\n        params: runtimeProps.celestial,\n        moonTexture: moonTextureRef.current,\n        moonTextureLoaded: moonTextureLoadedRef.current,\n        getUniformLocation: u,\n      });\n      swapBuffers();\n    } else {\n      clearOffscreenPass(gl, writeFB, displayWidth, displayHeight);\n      swapBuffers();\n    }\n\n    if (runtimeProps.layers.clouds && programs.cloud) {\n      renderCloudPass({\n        gl,\n        program: programs.cloud,\n        target: writeFB,\n        sceneTexture: readFB.texture,\n        displayWidth,\n        displayHeight,\n        time,\n        params: runtimeProps.cloud,\n        celestial: runtimeProps.celestial,\n        getUniformLocation: u,\n      });\n      swapBuffers();\n    }\n\n    if (runtimeProps.layers.rain && programs.rain) {\n      renderRainPass({\n        gl,\n        program: programs.rain,\n        target: writeFB,\n        sceneTexture: readFB.texture,\n        displayWidth,\n        displayHeight,\n        time,\n        params: runtimeProps.rain,\n        interactions: runtimeProps.interactions,\n        getUniformLocation: u,\n      });\n      swapBuffers();\n    }\n\n    const lightningActive = isLightningPassActive(\n      runtimeProps.layers,\n      runtimeProps.lightning,\n      programs.lightning,\n      time,\n      lastFlashTimeRef.current,\n    );\n\n    if (lightningActive && programs.lightning) {\n      renderLightningPass({\n        gl,\n        program: programs.lightning,\n        target: writeFB,\n        sceneTexture: readFB.texture,\n        displayWidth,\n        displayHeight,\n        time,\n        params: runtimeProps.lightning,\n        interactions: runtimeProps.interactions,\n        lastFlashTime: lastFlashTimeRef.current,\n        strikeSeed: strikeSeedRef.current,\n        getUniformLocation: u,\n      });\n      swapBuffers();\n    }\n\n    if (runtimeProps.layers.snow && programs.snow) {\n      renderSnowPass({\n        gl,\n        program: programs.snow,\n        target: writeFB,\n        sceneTexture: readFB.texture,\n        displayWidth,\n        displayHeight,\n        time,\n        params: runtimeProps.snow,\n        getUniformLocation: u,\n      });\n      swapBuffers();\n    }\n\n    if (programs.composite) {\n      renderCompositePass({\n        gl,\n        program: programs.composite,\n        sceneTexture: readFB.texture,\n        displayWidth,\n        displayHeight,\n        time,\n        celestial: runtimeProps.celestial,\n        interactions: runtimeProps.interactions,\n        post: runtimeProps.post,\n        lastFlashTime: lastFlashTimeRef.current,\n        strikeSeed: strikeSeedRef.current,\n        getUniformLocation: u,\n      });\n    }\n\n    if (isVisibleRef.current && !isContextLostRef.current) {\n      isRunningRef.current = true;\n      animationFrameRef.current = requestAnimationFrame(render);\n    } else {\n      isRunningRef.current = false;\n      animationFrameRef.current = 0;\n    }\n  }, [getUniformLocationCached]);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n\n    const onContextLost = (e: Event) => {\n      e.preventDefault();\n      isContextLostRef.current = true;\n      disposeGL();\n    };\n\n    const onContextRestored = () => {\n      isContextLostRef.current = false;\n      initFailedRef.current = false;\n      if (initGL() && isVisibleRef.current) {\n        isRunningRef.current = true;\n        render();\n      }\n    };\n\n    canvas.addEventListener(\"webglcontextlost\", onContextLost, {\n      passive: false,\n    } as AddEventListenerOptions);\n    canvas.addEventListener(\"webglcontextrestored\", onContextRestored);\n\n    const observer =\n      typeof IntersectionObserver !== \"undefined\"\n        ? new IntersectionObserver(\n            (entries) => {\n              const entry = entries[0];\n              const visible = Boolean(entry?.isIntersecting);\n              isVisibleRef.current = visible;\n\n              if (!visible) {\n                stopRenderLoop();\n                disposeGL();\n                releaseBudgetSlot(canvas);\n                return;\n              }\n\n              if (!isRunningRef.current && !isContextLostRef.current) {\n                if (glRef.current && fbRef.current.a && fbRef.current.b) {\n                  isRunningRef.current = true;\n                  render();\n                } else if (initGL()) {\n                  isRunningRef.current = true;\n                  render();\n                }\n              }\n            },\n            { threshold: 0 },\n          )\n        : null;\n\n    if (!observer) {\n      isVisibleRef.current = true;\n    } else {\n      observer.observe(canvas);\n    }\n\n    if (!observer && initGL() && isVisibleRef.current) {\n      isRunningRef.current = true;\n      render();\n    }\n\n    return () => {\n      observer?.disconnect();\n      canvas.removeEventListener(\n        \"webglcontextlost\",\n        onContextLost as EventListener,\n      );\n      canvas.removeEventListener(\n        \"webglcontextrestored\",\n        onContextRestored as EventListener,\n      );\n      disposeGL();\n      releaseBudgetSlot(canvas);\n    };\n  }, [disposeGL, initGL, releaseBudgetSlot, render, stopRenderLoop]);\n\n  return canvasRef;\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/weather-compositor-types.ts",
    "content": "import type {\n  LayerToggles,\n  PostProcessParams,\n  SnowParams,\n} from \"./weather-effects-types\";\n\nexport interface WeatherCompositorCelestialParams {\n  timeOfDay: number;\n  moonPhase: number;\n  starDensity: number;\n  celestialX: number;\n  celestialY: number;\n  sunSize: number;\n  moonSize: number;\n  sunGlowIntensity: number;\n  sunGlowSize: number;\n  sunRayCount: number;\n  sunRayLength: number;\n  sunRayIntensity: number;\n  sunRayShimmer: number;\n  sunRayShimmerSpeed: number;\n  moonGlowIntensity: number;\n  moonGlowSize: number;\n  skyBrightness: number;\n  skySaturation: number;\n  skyContrast: number;\n}\n\nexport interface WeatherCompositorCloudParams {\n  cloudScale: number;\n  coverage: number;\n  density: number;\n  softness: number;\n  windSpeed: number;\n  windAngle: number;\n  turbulence: number;\n  lightIntensity: number;\n  ambientDarkness: number;\n  backlightIntensity: number;\n  numLayers: number;\n}\n\nexport interface WeatherCompositorRainParams {\n  glassIntensity: number;\n  zoom: number;\n  fallingIntensity: number;\n  fallingSpeed: number;\n  fallingAngle: number;\n  fallingStreakLength: number;\n  fallingLayers: number;\n  fallingRefraction: number;\n}\n\nexport interface WeatherCompositorLightningParams {\n  autoMode: boolean;\n  autoInterval: number;\n  glowIntensity: number;\n  branchDensity: number;\n  sceneIllumination: number;\n}\n\nexport interface WeatherCompositorParams {\n  layers: LayerToggles;\n  celestial: WeatherCompositorCelestialParams;\n  cloud: WeatherCompositorCloudParams;\n  rain: WeatherCompositorRainParams;\n  lightning: WeatherCompositorLightningParams;\n  snow: SnowParams;\n  post: PostProcessParams;\n}\n\nexport type WeatherStudioCompositorParams = WeatherCompositorParams;\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/weather-effect-gl.ts",
    "content": "// Shared WebGL setup helpers for the weather effects renderer.\n\nexport interface Framebuffer {\n  fbo: WebGLFramebuffer;\n  texture: WebGLTexture;\n  width: number;\n  height: number;\n}\n\nfunction createShader(\n  gl: WebGL2RenderingContext,\n  type: GLenum,\n  source: string,\n): WebGLShader | null {\n  if (gl.isContextLost()) return null;\n  const shader = gl.createShader(type);\n  if (!shader) return null;\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n    const info = gl.getShaderInfoLog(shader);\n    if (!gl.isContextLost()) {\n      const kind =\n        type === gl.VERTEX_SHADER\n          ? \"vertex\"\n          : type === gl.FRAGMENT_SHADER\n            ? \"fragment\"\n            : String(type);\n      console.error(`Shader compile error (${kind}):`, info ?? \"(no info log)\");\n    }\n    gl.deleteShader(shader);\n    return null;\n  }\n  return shader;\n}\n\nexport function createProgram(\n  gl: WebGL2RenderingContext,\n  vertexSource: string,\n  fragmentSource: string,\n): WebGLProgram | null {\n  if (gl.isContextLost()) return null;\n  const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);\n  const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);\n  if (!vertexShader || !fragmentShader) {\n    if (vertexShader) gl.deleteShader(vertexShader);\n    if (fragmentShader) gl.deleteShader(fragmentShader);\n    return null;\n  }\n\n  const program = gl.createProgram();\n  if (!program) {\n    gl.deleteShader(vertexShader);\n    gl.deleteShader(fragmentShader);\n    return null;\n  }\n  gl.attachShader(program, vertexShader);\n  gl.attachShader(program, fragmentShader);\n  gl.linkProgram(program);\n\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n    const info = gl.getProgramInfoLog(program);\n    if (!gl.isContextLost()) {\n      console.error(\"Program link error:\", info ?? \"(no info log)\");\n    }\n    gl.deleteProgram(program);\n    gl.deleteShader(vertexShader);\n    gl.deleteShader(fragmentShader);\n    return null;\n  }\n\n  gl.detachShader(program, vertexShader);\n  gl.detachShader(program, fragmentShader);\n  gl.deleteShader(vertexShader);\n  gl.deleteShader(fragmentShader);\n\n  return program;\n}\n\nexport function createFramebuffer(\n  gl: WebGL2RenderingContext,\n  width: number,\n  height: number,\n): Framebuffer | null {\n  const texture = gl.createTexture();\n  if (!texture) return null;\n\n  gl.bindTexture(gl.TEXTURE_2D, texture);\n  gl.texImage2D(\n    gl.TEXTURE_2D,\n    0,\n    gl.RGBA,\n    width,\n    height,\n    0,\n    gl.RGBA,\n    gl.UNSIGNED_BYTE,\n    null,\n  );\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n\n  const fbo = gl.createFramebuffer();\n  if (!fbo) {\n    gl.deleteTexture(texture);\n    return null;\n  }\n\n  gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);\n  gl.framebufferTexture2D(\n    gl.FRAMEBUFFER,\n    gl.COLOR_ATTACHMENT0,\n    gl.TEXTURE_2D,\n    texture,\n    0,\n  );\n\n  const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);\n  if (status !== gl.FRAMEBUFFER_COMPLETE) {\n    if (!gl.isContextLost()) {\n      console.error(\"Framebuffer incomplete:\", status);\n    }\n    gl.deleteFramebuffer(fbo);\n    gl.deleteTexture(texture);\n    gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n    gl.bindTexture(gl.TEXTURE_2D, null);\n    return null;\n  }\n\n  gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n  gl.bindTexture(gl.TEXTURE_2D, null);\n\n  return { fbo, texture, width, height };\n}\n\nexport function resizeFramebuffer(\n  gl: WebGL2RenderingContext,\n  fb: Framebuffer,\n  width: number,\n  height: number,\n): void {\n  if (fb.width === width && fb.height === height) return;\n\n  gl.bindTexture(gl.TEXTURE_2D, fb.texture);\n  gl.texImage2D(\n    gl.TEXTURE_2D,\n    0,\n    gl.RGBA,\n    width,\n    height,\n    0,\n    gl.RGBA,\n    gl.UNSIGNED_BYTE,\n    null,\n  );\n  gl.bindTexture(gl.TEXTURE_2D, null);\n\n  fb.width = width;\n  fb.height = height;\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/weather-effect-render-passes.generated.js",
    "content": "// AUTO-GENERATED by `pnpm weather:compile`.\n// Source: lib/weather-authoring/weather-widget/effects/weather-effect-render-passes.ts\n// DO NOT EDIT MANUALLY.\n\nfunction smoothstep(edge0, edge1, x) {\n  const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));\n  return t * t * (3 - 2 * t);\n}\nfunction resolveCelestialBodyState(celestial) {\n  const t = celestial.timeOfDay;\n  const baseY = celestial.celestialY;\n  const belowHorizon = -0.25;\n  const sunRise = smoothstep(0.18, 0.32, t);\n  const sunSet = smoothstep(0.68, 0.82, t);\n  const sunVisible = sunRise * (1 - sunSet);\n  const sunY = belowHorizon + (baseY - belowHorizon) * sunVisible;\n  const moonRising = smoothstep(0.74, 0.88, t);\n  const moonSetting = 1 - smoothstep(0.12, 0.26, t);\n  const moonVisible = Math.max(moonRising, moonSetting);\n  const moonY = belowHorizon + (baseY - belowHorizon) * moonVisible;\n  const useSun = sunVisible > moonVisible;\n  const activeY = useSun ? sunY : moonY;\n  const activeSize = useSun ? celestial.sunSize : celestial.moonSize;\n  const activeBrightness = useSun\n    ? Math.min(1.0, celestial.sunGlowIntensity * 0.3) * sunVisible\n    : Math.min(0.5, celestial.moonGlowIntensity * 0.15) * moonVisible;\n  return {\n    sunY,\n    moonY,\n    sunVisible,\n    moonVisible,\n    activeY,\n    activeSize,\n    activeBrightness,\n  };\n}\nfunction bindOffscreenPass(gl, program, target, width, height) {\n  gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);\n  gl.viewport(0, 0, width, height);\n  gl.useProgram(program);\n}\nfunction bindSceneTexture(gl, program, sceneTexture, getUniformLocation) {\n  gl.activeTexture(gl.TEXTURE0);\n  gl.bindTexture(gl.TEXTURE_2D, sceneTexture);\n  gl.uniform1i(getUniformLocation(program, \"u_sceneTexture\"), 0);\n}\nexport function clearOffscreenPass(gl, target, displayWidth, displayHeight) {\n  gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);\n  gl.viewport(0, 0, displayWidth, displayHeight);\n  gl.clearColor(0, 0, 0, 0);\n  gl.clear(gl.COLOR_BUFFER_BIT);\n}\nexport function renderCelestialPass({\n  gl,\n  program,\n  target,\n  displayWidth,\n  displayHeight,\n  time,\n  params,\n  moonTexture,\n  moonTextureLoaded,\n  getUniformLocation,\n}) {\n  bindOffscreenPass(gl, program, target, displayWidth, displayHeight);\n  gl.uniform1f(getUniformLocation(program, \"u_time\"), time);\n  gl.uniform2f(\n    getUniformLocation(program, \"u_resolution\"),\n    displayWidth,\n    displayHeight,\n  );\n  gl.uniform1f(getUniformLocation(program, \"u_timeOfDay\"), params.timeOfDay);\n  gl.uniform1f(getUniformLocation(program, \"u_moonPhase\"), params.moonPhase);\n  gl.uniform1f(\n    getUniformLocation(program, \"u_starDensity\"),\n    params.starDensity,\n  );\n  gl.uniform2f(\n    getUniformLocation(program, \"u_celestialPos\"),\n    params.celestialX,\n    params.celestialY,\n  );\n  gl.uniform1f(getUniformLocation(program, \"u_sunSize\"), params.sunSize);\n  gl.uniform1f(getUniformLocation(program, \"u_moonSize\"), params.moonSize);\n  gl.uniform1f(\n    getUniformLocation(program, \"u_sunGlowIntensity\"),\n    params.sunGlowIntensity,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_sunGlowSize\"),\n    params.sunGlowSize,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_sunRayCount\"),\n    params.sunRayCount,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_sunRayLength\"),\n    params.sunRayLength,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_sunRayIntensity\"),\n    params.sunRayIntensity,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_sunRayShimmer\"),\n    params.sunRayShimmer,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_sunRayShimmerSpeed\"),\n    params.sunRayShimmerSpeed,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_moonGlowIntensity\"),\n    params.moonGlowIntensity,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_moonGlowSize\"),\n    params.moonGlowSize,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_skyBrightness\"),\n    params.skyBrightness,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_skySaturation\"),\n    params.skySaturation,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_skyContrast\"),\n    params.skyContrast,\n  );\n  gl.activeTexture(gl.TEXTURE0);\n  gl.bindTexture(gl.TEXTURE_2D, moonTexture);\n  gl.uniform1i(getUniformLocation(program, \"u_moonTexture\"), 0);\n  gl.uniform1i(\n    getUniformLocation(program, \"u_hasMoonTexture\"),\n    moonTextureLoaded ? 1 : 0,\n  );\n  gl.drawArrays(gl.TRIANGLES, 0, 6);\n}\nexport function renderCloudPass({\n  gl,\n  program,\n  target,\n  sceneTexture,\n  displayWidth,\n  displayHeight,\n  time,\n  params,\n  celestial,\n  getUniformLocation,\n}) {\n  bindOffscreenPass(gl, program, target, displayWidth, displayHeight);\n  bindSceneTexture(gl, program, sceneTexture, getUniformLocation);\n  gl.uniform1f(getUniformLocation(program, \"u_time\"), time);\n  gl.uniform2f(\n    getUniformLocation(program, \"u_resolution\"),\n    displayWidth,\n    displayHeight,\n  );\n  gl.uniform1f(getUniformLocation(program, \"u_timeOfDay\"), celestial.timeOfDay);\n  gl.uniform1f(getUniformLocation(program, \"u_coverage\"), params.coverage);\n  gl.uniform1f(getUniformLocation(program, \"u_density\"), params.density);\n  gl.uniform1f(getUniformLocation(program, \"u_softness\"), params.softness);\n  gl.uniform1f(getUniformLocation(program, \"u_windSpeed\"), params.windSpeed);\n  gl.uniform1f(getUniformLocation(program, \"u_windAngle\"), params.windAngle);\n  gl.uniform1f(getUniformLocation(program, \"u_turbulence\"), params.turbulence);\n  gl.uniform1f(\n    getUniformLocation(program, \"u_lightIntensity\"),\n    params.lightIntensity,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_ambientDarkness\"),\n    params.ambientDarkness,\n  );\n  gl.uniform1i(getUniformLocation(program, \"u_numLayers\"), params.numLayers);\n  gl.uniform1f(getUniformLocation(program, \"u_cloudScale\"), params.cloudScale);\n  const bodies = resolveCelestialBodyState(celestial);\n  gl.uniform2f(\n    getUniformLocation(program, \"u_celestialPos\"),\n    celestial.celestialX,\n    bodies.activeY,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_celestialSize\"),\n    bodies.activeSize,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_celestialBrightness\"),\n    bodies.activeBrightness,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_backlightIntensity\"),\n    params.backlightIntensity,\n  );\n  gl.drawArrays(gl.TRIANGLES, 0, 6);\n}\nexport function renderRainPass({\n  gl,\n  program,\n  target,\n  sceneTexture,\n  displayWidth,\n  displayHeight,\n  time,\n  params,\n  interactions,\n  getUniformLocation,\n}) {\n  bindOffscreenPass(gl, program, target, displayWidth, displayHeight);\n  bindSceneTexture(gl, program, sceneTexture, getUniformLocation);\n  gl.uniform1f(getUniformLocation(program, \"u_time\"), time);\n  gl.uniform2f(\n    getUniformLocation(program, \"u_resolution\"),\n    displayWidth,\n    displayHeight,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_glassIntensity\"),\n    params.glassIntensity,\n  );\n  gl.uniform1f(getUniformLocation(program, \"u_glassZoom\"), params.glassZoom);\n  gl.uniform1f(\n    getUniformLocation(program, \"u_fallingIntensity\"),\n    params.fallingIntensity,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_fallingSpeed\"),\n    params.fallingSpeed,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_fallingAngle\"),\n    params.fallingAngle,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_fallingStreakLength\"),\n    params.fallingStreakLength,\n  );\n  gl.uniform1i(\n    getUniformLocation(program, \"u_fallingLayers\"),\n    params.fallingLayers,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_refractionStrength\"),\n    interactions.rainRefractionStrength,\n  );\n  gl.drawArrays(gl.TRIANGLES, 0, 6);\n}\nexport function isLightningPassActive(\n  layers,\n  params,\n  program,\n  time,\n  lastFlashTime,\n) {\n  if (!layers.lightning || !program || !params.enabled) {\n    return false;\n  }\n  const lightningDurationSec = 0.8;\n  const lightningAfterimageDuration = lightningDurationSec * 1.5;\n  const lightningTimeSinceStrike = time - lastFlashTime;\n  return (\n    lightningTimeSinceStrike >= 0 &&\n    lightningTimeSinceStrike <= lightningAfterimageDuration\n  );\n}\nexport function renderLightningPass({\n  gl,\n  program,\n  target,\n  sceneTexture,\n  displayWidth,\n  displayHeight,\n  time,\n  params,\n  interactions,\n  lastFlashTime,\n  strikeSeed,\n  getUniformLocation,\n}) {\n  bindOffscreenPass(gl, program, target, displayWidth, displayHeight);\n  bindSceneTexture(gl, program, sceneTexture, getUniformLocation);\n  gl.uniform1f(getUniformLocation(program, \"u_time\"), time);\n  gl.uniform2f(\n    getUniformLocation(program, \"u_resolution\"),\n    displayWidth,\n    displayHeight,\n  );\n  gl.uniform1i(\n    getUniformLocation(program, \"u_enabled\"),\n    params.enabled ? 1 : 0,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_flashIntensity\"),\n    params.flashIntensity,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_branchDensity\"),\n    params.branchDensity,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_sceneIllumination\"),\n    interactions.lightningSceneIllumination,\n  );\n  gl.uniform1f(getUniformLocation(program, \"u_lastFlashTime\"), lastFlashTime);\n  gl.uniform1f(getUniformLocation(program, \"u_strikeSeed\"), strikeSeed);\n  gl.drawArrays(gl.TRIANGLES, 0, 6);\n}\nexport function renderSnowPass({\n  gl,\n  program,\n  target,\n  sceneTexture,\n  displayWidth,\n  displayHeight,\n  time,\n  params,\n  getUniformLocation,\n}) {\n  bindOffscreenPass(gl, program, target, displayWidth, displayHeight);\n  bindSceneTexture(gl, program, sceneTexture, getUniformLocation);\n  gl.uniform1f(getUniformLocation(program, \"u_time\"), time);\n  gl.uniform2f(\n    getUniformLocation(program, \"u_resolution\"),\n    displayWidth,\n    displayHeight,\n  );\n  gl.uniform1f(getUniformLocation(program, \"u_intensity\"), params.intensity);\n  gl.uniform1i(getUniformLocation(program, \"u_layers\"), params.layers);\n  gl.uniform1f(getUniformLocation(program, \"u_fallSpeed\"), params.fallSpeed);\n  gl.uniform1f(getUniformLocation(program, \"u_windSpeed\"), params.windSpeed);\n  gl.uniform1f(getUniformLocation(program, \"u_windAngle\"), params.windAngle);\n  gl.uniform1f(getUniformLocation(program, \"u_turbulence\"), params.turbulence);\n  gl.uniform1f(getUniformLocation(program, \"u_drift\"), params.drift);\n  gl.uniform1f(getUniformLocation(program, \"u_flutter\"), params.flutter);\n  gl.uniform1f(getUniformLocation(program, \"u_windShear\"), params.windShear);\n  gl.uniform1f(getUniformLocation(program, \"u_flakeSize\"), params.flakeSize);\n  gl.uniform1f(\n    getUniformLocation(program, \"u_sizeVariation\"),\n    params.sizeVariation,\n  );\n  gl.uniform1f(getUniformLocation(program, \"u_opacity\"), params.opacity);\n  gl.uniform1f(getUniformLocation(program, \"u_glowAmount\"), params.glowAmount);\n  gl.uniform1f(getUniformLocation(program, \"u_sparkle\"), params.sparkle);\n  gl.drawArrays(gl.TRIANGLES, 0, 6);\n}\nexport function renderCompositePass({\n  gl,\n  program,\n  sceneTexture,\n  displayWidth,\n  displayHeight,\n  time,\n  celestial,\n  interactions,\n  post,\n  lastFlashTime,\n  strikeSeed,\n  getUniformLocation,\n}) {\n  gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n  gl.viewport(0, 0, displayWidth, displayHeight);\n  gl.useProgram(program);\n  bindSceneTexture(gl, program, sceneTexture, getUniformLocation);\n  gl.uniform1f(getUniformLocation(program, \"u_time\"), time);\n  gl.uniform2f(\n    getUniformLocation(program, \"u_resolution\"),\n    displayWidth,\n    displayHeight,\n  );\n  gl.uniform1f(getUniformLocation(program, \"u_timeOfDay\"), celestial.timeOfDay);\n  const bodies = resolveCelestialBodyState(celestial);\n  gl.uniform2f(\n    getUniformLocation(program, \"u_sunPos\"),\n    celestial.celestialX,\n    bodies.sunY,\n  );\n  gl.uniform1f(getUniformLocation(program, \"u_sunVisible\"), bodies.sunVisible);\n  gl.uniform1f(getUniformLocation(program, \"u_lastFlashTime\"), lastFlashTime);\n  gl.uniform1f(getUniformLocation(program, \"u_strikeSeed\"), strikeSeed);\n  gl.uniform1f(\n    getUniformLocation(program, \"u_lightningSceneIllumination\"),\n    interactions.lightningSceneIllumination,\n  );\n  gl.uniform1i(\n    getUniformLocation(program, \"u_postEnabled\"),\n    post.enabled ? 1 : 0,\n  );\n  gl.uniform1f(getUniformLocation(program, \"u_haze\"), post.haze);\n  gl.uniform1f(getUniformLocation(program, \"u_hazeHorizon\"), post.hazeHorizon);\n  gl.uniform1f(\n    getUniformLocation(program, \"u_hazeDesaturation\"),\n    post.hazeDesaturation,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_hazeContrast\"),\n    post.hazeContrast,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_bloomIntensity\"),\n    post.bloomIntensity,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_bloomThreshold\"),\n    post.bloomThreshold,\n  );\n  gl.uniform1f(getUniformLocation(program, \"u_bloomKnee\"), post.bloomKnee);\n  gl.uniform1f(getUniformLocation(program, \"u_bloomRadius\"), post.bloomRadius);\n  gl.uniform1f(\n    getUniformLocation(program, \"u_bloomTapScale\"),\n    post.bloomTapScale,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_exposureIntensity\"),\n    post.exposureIntensity,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_exposureDesaturation\"),\n    post.exposureDesaturation,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_exposureRecovery\"),\n    post.exposureRecovery,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_godRayIntensity\"),\n    post.godRayIntensity,\n  );\n  gl.uniform1f(getUniformLocation(program, \"u_godRayDecay\"), post.godRayDecay);\n  gl.uniform1f(\n    getUniformLocation(program, \"u_godRayDensity\"),\n    post.godRayDensity,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_godRayWeight\"),\n    post.godRayWeight,\n  );\n  gl.uniform1i(\n    getUniformLocation(program, \"u_godRaySamples\"),\n    Math.max(0, Math.min(32, Math.floor(post.godRaySamples))),\n  );\n  gl.drawArrays(gl.TRIANGLES, 0, 6);\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/weather-effect-render-passes.ts",
    "content": "import type { Framebuffer } from \"./weather-effect-gl\";\nimport type {\n  CelestialParams,\n  CloudParams,\n  InteractionParams,\n  LayerToggles,\n  LightningParams,\n  PostProcessParams,\n  RainParams,\n  SnowParams,\n} from \"./weather-effects-types\";\n\nexport type UniformLocationGetter = (\n  program: WebGLProgram,\n  name: string,\n) => WebGLUniformLocation | null;\n\ninterface BasePassInput {\n  gl: WebGL2RenderingContext;\n  displayWidth: number;\n  displayHeight: number;\n  time: number;\n  getUniformLocation: UniformLocationGetter;\n}\n\ninterface CelestialPassInput extends BasePassInput {\n  program: WebGLProgram;\n  target: Framebuffer;\n  params: CelestialParams;\n  moonTexture: WebGLTexture | null;\n  moonTextureLoaded: boolean;\n}\n\ninterface CloudPassInput extends BasePassInput {\n  program: WebGLProgram;\n  target: Framebuffer;\n  sceneTexture: WebGLTexture;\n  params: CloudParams;\n  celestial: CelestialParams;\n}\n\ninterface RainPassInput extends BasePassInput {\n  program: WebGLProgram;\n  target: Framebuffer;\n  sceneTexture: WebGLTexture;\n  params: RainParams;\n  interactions: InteractionParams;\n}\n\ninterface LightningPassInput extends BasePassInput {\n  program: WebGLProgram;\n  target: Framebuffer;\n  sceneTexture: WebGLTexture;\n  params: LightningParams;\n  interactions: InteractionParams;\n  lastFlashTime: number;\n  strikeSeed: number;\n}\n\ninterface SnowPassInput extends BasePassInput {\n  program: WebGLProgram;\n  target: Framebuffer;\n  sceneTexture: WebGLTexture;\n  params: SnowParams;\n}\n\ninterface CompositePassInput extends BasePassInput {\n  program: WebGLProgram;\n  sceneTexture: WebGLTexture;\n  celestial: CelestialParams;\n  interactions: InteractionParams;\n  post: PostProcessParams;\n  lastFlashTime: number;\n  strikeSeed: number;\n}\n\ninterface CelestialBodyState {\n  sunY: number;\n  moonY: number;\n  sunVisible: number;\n  moonVisible: number;\n  activeY: number;\n  activeSize: number;\n  activeBrightness: number;\n}\n\nfunction smoothstep(edge0: number, edge1: number, x: number): number {\n  const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));\n  return t * t * (3 - 2 * t);\n}\n\nfunction resolveCelestialBodyState(\n  celestial: CelestialParams,\n): CelestialBodyState {\n  const t = celestial.timeOfDay;\n  const baseY = celestial.celestialY;\n  const belowHorizon = -0.25;\n\n  const sunRise = smoothstep(0.18, 0.32, t);\n  const sunSet = smoothstep(0.68, 0.82, t);\n  const sunVisible = sunRise * (1 - sunSet);\n  const sunY = belowHorizon + (baseY - belowHorizon) * sunVisible;\n\n  const moonRising = smoothstep(0.74, 0.88, t);\n  const moonSetting = 1 - smoothstep(0.12, 0.26, t);\n  const moonVisible = Math.max(moonRising, moonSetting);\n  const moonY = belowHorizon + (baseY - belowHorizon) * moonVisible;\n\n  const useSun = sunVisible > moonVisible;\n  const activeY = useSun ? sunY : moonY;\n  const activeSize = useSun ? celestial.sunSize : celestial.moonSize;\n  const activeBrightness = useSun\n    ? Math.min(1.0, celestial.sunGlowIntensity * 0.3) * sunVisible\n    : Math.min(0.5, celestial.moonGlowIntensity * 0.15) * moonVisible;\n\n  return {\n    sunY,\n    moonY,\n    sunVisible,\n    moonVisible,\n    activeY,\n    activeSize,\n    activeBrightness,\n  };\n}\n\nfunction bindOffscreenPass(\n  gl: WebGL2RenderingContext,\n  program: WebGLProgram,\n  target: Framebuffer,\n  width: number,\n  height: number,\n): void {\n  gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);\n  gl.viewport(0, 0, width, height);\n  gl.useProgram(program);\n}\n\nfunction bindSceneTexture(\n  gl: WebGL2RenderingContext,\n  program: WebGLProgram,\n  sceneTexture: WebGLTexture,\n  getUniformLocation: UniformLocationGetter,\n): void {\n  gl.activeTexture(gl.TEXTURE0);\n  gl.bindTexture(gl.TEXTURE_2D, sceneTexture);\n  gl.uniform1i(getUniformLocation(program, \"u_sceneTexture\"), 0);\n}\n\nexport function clearOffscreenPass(\n  gl: WebGL2RenderingContext,\n  target: Framebuffer,\n  displayWidth: number,\n  displayHeight: number,\n): void {\n  gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);\n  gl.viewport(0, 0, displayWidth, displayHeight);\n  gl.clearColor(0, 0, 0, 0);\n  gl.clear(gl.COLOR_BUFFER_BIT);\n}\n\nexport function renderCelestialPass({\n  gl,\n  program,\n  target,\n  displayWidth,\n  displayHeight,\n  time,\n  params,\n  moonTexture,\n  moonTextureLoaded,\n  getUniformLocation,\n}: CelestialPassInput): void {\n  bindOffscreenPass(gl, program, target, displayWidth, displayHeight);\n\n  gl.uniform1f(getUniformLocation(program, \"u_time\"), time);\n  gl.uniform2f(\n    getUniformLocation(program, \"u_resolution\"),\n    displayWidth,\n    displayHeight,\n  );\n  gl.uniform1f(getUniformLocation(program, \"u_timeOfDay\"), params.timeOfDay);\n  gl.uniform1f(getUniformLocation(program, \"u_moonPhase\"), params.moonPhase);\n  gl.uniform1f(\n    getUniformLocation(program, \"u_starDensity\"),\n    params.starDensity,\n  );\n  gl.uniform2f(\n    getUniformLocation(program, \"u_celestialPos\"),\n    params.celestialX,\n    params.celestialY,\n  );\n  gl.uniform1f(getUniformLocation(program, \"u_sunSize\"), params.sunSize);\n  gl.uniform1f(getUniformLocation(program, \"u_moonSize\"), params.moonSize);\n  gl.uniform1f(\n    getUniformLocation(program, \"u_sunGlowIntensity\"),\n    params.sunGlowIntensity,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_sunGlowSize\"),\n    params.sunGlowSize,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_sunRayCount\"),\n    params.sunRayCount,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_sunRayLength\"),\n    params.sunRayLength,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_sunRayIntensity\"),\n    params.sunRayIntensity,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_sunRayShimmer\"),\n    params.sunRayShimmer,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_sunRayShimmerSpeed\"),\n    params.sunRayShimmerSpeed,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_moonGlowIntensity\"),\n    params.moonGlowIntensity,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_moonGlowSize\"),\n    params.moonGlowSize,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_skyBrightness\"),\n    params.skyBrightness,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_skySaturation\"),\n    params.skySaturation,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_skyContrast\"),\n    params.skyContrast,\n  );\n\n  gl.activeTexture(gl.TEXTURE0);\n  gl.bindTexture(gl.TEXTURE_2D, moonTexture);\n  gl.uniform1i(getUniformLocation(program, \"u_moonTexture\"), 0);\n  gl.uniform1i(\n    getUniformLocation(program, \"u_hasMoonTexture\"),\n    moonTextureLoaded ? 1 : 0,\n  );\n\n  gl.drawArrays(gl.TRIANGLES, 0, 6);\n}\n\nexport function renderCloudPass({\n  gl,\n  program,\n  target,\n  sceneTexture,\n  displayWidth,\n  displayHeight,\n  time,\n  params,\n  celestial,\n  getUniformLocation,\n}: CloudPassInput): void {\n  bindOffscreenPass(gl, program, target, displayWidth, displayHeight);\n  bindSceneTexture(gl, program, sceneTexture, getUniformLocation);\n\n  gl.uniform1f(getUniformLocation(program, \"u_time\"), time);\n  gl.uniform2f(\n    getUniformLocation(program, \"u_resolution\"),\n    displayWidth,\n    displayHeight,\n  );\n  gl.uniform1f(getUniformLocation(program, \"u_timeOfDay\"), celestial.timeOfDay);\n  gl.uniform1f(getUniformLocation(program, \"u_coverage\"), params.coverage);\n  gl.uniform1f(getUniformLocation(program, \"u_density\"), params.density);\n  gl.uniform1f(getUniformLocation(program, \"u_softness\"), params.softness);\n  gl.uniform1f(getUniformLocation(program, \"u_windSpeed\"), params.windSpeed);\n  gl.uniform1f(getUniformLocation(program, \"u_windAngle\"), params.windAngle);\n  gl.uniform1f(getUniformLocation(program, \"u_turbulence\"), params.turbulence);\n  gl.uniform1f(\n    getUniformLocation(program, \"u_lightIntensity\"),\n    params.lightIntensity,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_ambientDarkness\"),\n    params.ambientDarkness,\n  );\n  gl.uniform1i(getUniformLocation(program, \"u_numLayers\"), params.numLayers);\n  gl.uniform1f(getUniformLocation(program, \"u_cloudScale\"), params.cloudScale);\n\n  const bodies = resolveCelestialBodyState(celestial);\n\n  gl.uniform2f(\n    getUniformLocation(program, \"u_celestialPos\"),\n    celestial.celestialX,\n    bodies.activeY,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_celestialSize\"),\n    bodies.activeSize,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_celestialBrightness\"),\n    bodies.activeBrightness,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_backlightIntensity\"),\n    params.backlightIntensity,\n  );\n\n  gl.drawArrays(gl.TRIANGLES, 0, 6);\n}\n\nexport function renderRainPass({\n  gl,\n  program,\n  target,\n  sceneTexture,\n  displayWidth,\n  displayHeight,\n  time,\n  params,\n  interactions,\n  getUniformLocation,\n}: RainPassInput): void {\n  bindOffscreenPass(gl, program, target, displayWidth, displayHeight);\n  bindSceneTexture(gl, program, sceneTexture, getUniformLocation);\n\n  gl.uniform1f(getUniformLocation(program, \"u_time\"), time);\n  gl.uniform2f(\n    getUniformLocation(program, \"u_resolution\"),\n    displayWidth,\n    displayHeight,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_glassIntensity\"),\n    params.glassIntensity,\n  );\n  gl.uniform1f(getUniformLocation(program, \"u_glassZoom\"), params.glassZoom);\n  gl.uniform1f(\n    getUniformLocation(program, \"u_fallingIntensity\"),\n    params.fallingIntensity,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_fallingSpeed\"),\n    params.fallingSpeed,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_fallingAngle\"),\n    params.fallingAngle,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_fallingStreakLength\"),\n    params.fallingStreakLength,\n  );\n  gl.uniform1i(\n    getUniformLocation(program, \"u_fallingLayers\"),\n    params.fallingLayers,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_refractionStrength\"),\n    interactions.rainRefractionStrength,\n  );\n\n  gl.drawArrays(gl.TRIANGLES, 0, 6);\n}\n\nexport function isLightningPassActive(\n  layers: LayerToggles,\n  params: LightningParams,\n  program: WebGLProgram | null,\n  time: number,\n  lastFlashTime: number,\n): boolean {\n  if (!layers.lightning || !program || !params.enabled) {\n    return false;\n  }\n\n  const lightningDurationSec = 0.8;\n  const lightningAfterimageDuration = lightningDurationSec * 1.5;\n  const lightningTimeSinceStrike = time - lastFlashTime;\n\n  return (\n    lightningTimeSinceStrike >= 0 &&\n    lightningTimeSinceStrike <= lightningAfterimageDuration\n  );\n}\n\nexport function renderLightningPass({\n  gl,\n  program,\n  target,\n  sceneTexture,\n  displayWidth,\n  displayHeight,\n  time,\n  params,\n  interactions,\n  lastFlashTime,\n  strikeSeed,\n  getUniformLocation,\n}: LightningPassInput): void {\n  bindOffscreenPass(gl, program, target, displayWidth, displayHeight);\n  bindSceneTexture(gl, program, sceneTexture, getUniformLocation);\n\n  gl.uniform1f(getUniformLocation(program, \"u_time\"), time);\n  gl.uniform2f(\n    getUniformLocation(program, \"u_resolution\"),\n    displayWidth,\n    displayHeight,\n  );\n  gl.uniform1i(\n    getUniformLocation(program, \"u_enabled\"),\n    params.enabled ? 1 : 0,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_flashIntensity\"),\n    params.flashIntensity,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_branchDensity\"),\n    params.branchDensity,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_sceneIllumination\"),\n    interactions.lightningSceneIllumination,\n  );\n  gl.uniform1f(getUniformLocation(program, \"u_lastFlashTime\"), lastFlashTime);\n  gl.uniform1f(getUniformLocation(program, \"u_strikeSeed\"), strikeSeed);\n\n  gl.drawArrays(gl.TRIANGLES, 0, 6);\n}\n\nexport function renderSnowPass({\n  gl,\n  program,\n  target,\n  sceneTexture,\n  displayWidth,\n  displayHeight,\n  time,\n  params,\n  getUniformLocation,\n}: SnowPassInput): void {\n  bindOffscreenPass(gl, program, target, displayWidth, displayHeight);\n  bindSceneTexture(gl, program, sceneTexture, getUniformLocation);\n\n  gl.uniform1f(getUniformLocation(program, \"u_time\"), time);\n  gl.uniform2f(\n    getUniformLocation(program, \"u_resolution\"),\n    displayWidth,\n    displayHeight,\n  );\n  gl.uniform1f(getUniformLocation(program, \"u_intensity\"), params.intensity);\n  gl.uniform1i(getUniformLocation(program, \"u_layers\"), params.layers);\n  gl.uniform1f(getUniformLocation(program, \"u_fallSpeed\"), params.fallSpeed);\n  gl.uniform1f(getUniformLocation(program, \"u_windSpeed\"), params.windSpeed);\n  gl.uniform1f(getUniformLocation(program, \"u_windAngle\"), params.windAngle);\n  gl.uniform1f(getUniformLocation(program, \"u_turbulence\"), params.turbulence);\n  gl.uniform1f(getUniformLocation(program, \"u_drift\"), params.drift);\n  gl.uniform1f(getUniformLocation(program, \"u_flutter\"), params.flutter);\n  gl.uniform1f(getUniformLocation(program, \"u_windShear\"), params.windShear);\n  gl.uniform1f(getUniformLocation(program, \"u_flakeSize\"), params.flakeSize);\n  gl.uniform1f(\n    getUniformLocation(program, \"u_sizeVariation\"),\n    params.sizeVariation,\n  );\n  gl.uniform1f(getUniformLocation(program, \"u_opacity\"), params.opacity);\n  gl.uniform1f(getUniformLocation(program, \"u_glowAmount\"), params.glowAmount);\n  gl.uniform1f(getUniformLocation(program, \"u_sparkle\"), params.sparkle);\n\n  gl.drawArrays(gl.TRIANGLES, 0, 6);\n}\n\nexport function renderCompositePass({\n  gl,\n  program,\n  sceneTexture,\n  displayWidth,\n  displayHeight,\n  time,\n  celestial,\n  interactions,\n  post,\n  lastFlashTime,\n  strikeSeed,\n  getUniformLocation,\n}: CompositePassInput): void {\n  gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n  gl.viewport(0, 0, displayWidth, displayHeight);\n  gl.useProgram(program);\n  bindSceneTexture(gl, program, sceneTexture, getUniformLocation);\n\n  gl.uniform1f(getUniformLocation(program, \"u_time\"), time);\n  gl.uniform2f(\n    getUniformLocation(program, \"u_resolution\"),\n    displayWidth,\n    displayHeight,\n  );\n  gl.uniform1f(getUniformLocation(program, \"u_timeOfDay\"), celestial.timeOfDay);\n\n  const bodies = resolveCelestialBodyState(celestial);\n  gl.uniform2f(\n    getUniformLocation(program, \"u_sunPos\"),\n    celestial.celestialX,\n    bodies.sunY,\n  );\n  gl.uniform1f(getUniformLocation(program, \"u_sunVisible\"), bodies.sunVisible);\n\n  gl.uniform1f(getUniformLocation(program, \"u_lastFlashTime\"), lastFlashTime);\n  gl.uniform1f(getUniformLocation(program, \"u_strikeSeed\"), strikeSeed);\n  gl.uniform1f(\n    getUniformLocation(program, \"u_lightningSceneIllumination\"),\n    interactions.lightningSceneIllumination,\n  );\n\n  gl.uniform1i(\n    getUniformLocation(program, \"u_postEnabled\"),\n    post.enabled ? 1 : 0,\n  );\n\n  gl.uniform1f(getUniformLocation(program, \"u_haze\"), post.haze);\n  gl.uniform1f(getUniformLocation(program, \"u_hazeHorizon\"), post.hazeHorizon);\n  gl.uniform1f(\n    getUniformLocation(program, \"u_hazeDesaturation\"),\n    post.hazeDesaturation,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_hazeContrast\"),\n    post.hazeContrast,\n  );\n\n  gl.uniform1f(\n    getUniformLocation(program, \"u_bloomIntensity\"),\n    post.bloomIntensity,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_bloomThreshold\"),\n    post.bloomThreshold,\n  );\n  gl.uniform1f(getUniformLocation(program, \"u_bloomKnee\"), post.bloomKnee);\n  gl.uniform1f(getUniformLocation(program, \"u_bloomRadius\"), post.bloomRadius);\n  gl.uniform1f(\n    getUniformLocation(program, \"u_bloomTapScale\"),\n    post.bloomTapScale,\n  );\n\n  gl.uniform1f(\n    getUniformLocation(program, \"u_exposureIntensity\"),\n    post.exposureIntensity,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_exposureDesaturation\"),\n    post.exposureDesaturation,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_exposureRecovery\"),\n    post.exposureRecovery,\n  );\n\n  gl.uniform1f(\n    getUniformLocation(program, \"u_godRayIntensity\"),\n    post.godRayIntensity,\n  );\n  gl.uniform1f(getUniformLocation(program, \"u_godRayDecay\"), post.godRayDecay);\n  gl.uniform1f(\n    getUniformLocation(program, \"u_godRayDensity\"),\n    post.godRayDensity,\n  );\n  gl.uniform1f(\n    getUniformLocation(program, \"u_godRayWeight\"),\n    post.godRayWeight,\n  );\n  gl.uniform1i(\n    getUniformLocation(program, \"u_godRaySamples\"),\n    Math.max(0, Math.min(32, Math.floor(post.godRaySamples))),\n  );\n\n  gl.drawArrays(gl.TRIANGLES, 0, 6);\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/weather-effect-shaders.ts",
    "content": "// Runtime entrypoint for generated weather shader sources.\n// Authoring sources: lib/weather-authoring/shaders/*.glsl\n\nexport {\n  CELESTIAL_FRAGMENT,\n  CLOUD_FRAGMENT,\n  COMPOSITE_FRAGMENT,\n  FULLSCREEN_VERTEX,\n  LIGHTNING_FRAGMENT,\n  RAIN_FRAGMENT,\n  SNOW_FRAGMENT,\n} from \"./generated/weather-effect-shaders.generated\";\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/weather-effects-canvas.tsx",
    "content": "\"use client\";\n\nimport {\n  __resetWeatherWebglCanvasBudgetForTests,\n  getAllocatedWeatherWebglCanvasCount,\n  getMaxConcurrentWeatherWebglCanvases,\n  releaseWeatherWebglBudgetSlotOnInitFailure,\n  releaseWeatherWebglCanvasBudgetSlot,\n  setMaxConcurrentWeatherWebglCanvases,\n  tryAcquireWeatherWebglCanvasBudgetSlot,\n} from \"./weather-webgl-budget\";\nimport { useWeatherEffectsRenderer } from \"./use-weather-effects-renderer\";\nimport { resolveWeatherEffectsCanvasRuntimeProps } from \"./weather-effects-props\";\nimport type { WeatherEffectsCanvasProps } from \"./weather-effects-types\";\n\nexport type {\n  CelestialParams,\n  CloudParams,\n  InteractionParams,\n  LayerToggles,\n  LightningParams,\n  PostProcessParams,\n  RainParams,\n  SnowParams,\n  WeatherEffectsCanvasProps,\n} from \"./weather-effects-types\";\n\nexport {\n  __resetWeatherWebglCanvasBudgetForTests,\n  getAllocatedWeatherWebglCanvasCount,\n  getMaxConcurrentWeatherWebglCanvases,\n  releaseWeatherWebglBudgetSlotOnInitFailure,\n  releaseWeatherWebglCanvasBudgetSlot,\n  setMaxConcurrentWeatherWebglCanvases,\n  tryAcquireWeatherWebglCanvasBudgetSlot,\n};\n\nexport function WeatherEffectsCanvas({\n  className,\n  dpr,\n  layers,\n  celestial,\n  cloud,\n  rain,\n  lightning,\n  snow,\n  interactions,\n  post,\n}: WeatherEffectsCanvasProps) {\n  const canvasRef = useWeatherEffectsRenderer(\n    resolveWeatherEffectsCanvasRuntimeProps({\n      dpr,\n      layers,\n      celestial,\n      cloud,\n      rain,\n      lightning,\n      snow,\n      interactions,\n      post,\n    }),\n  );\n\n  return (\n    <canvas\n      ref={canvasRef}\n      className={className}\n      style={{ width: \"100%\", height: \"100%\" }}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/weather-effects-defaults.ts",
    "content": "import type {\n  CelestialParams,\n  CloudParams,\n  InteractionParams,\n  LayerToggles,\n  LightningParams,\n  PostProcessParams,\n  RainParams,\n  SnowParams,\n} from \"./weather-effects-types\";\n\nexport const DEFAULT_LAYERS: LayerToggles = {\n  celestial: true,\n  clouds: true,\n  rain: false,\n  lightning: false,\n  snow: false,\n};\n\nexport const DEFAULT_CELESTIAL: CelestialParams = {\n  timeOfDay: 0.5,\n  moonPhase: 0.5,\n  starDensity: 0.5,\n  celestialX: 0.74,\n  celestialY: 0.78,\n  sunSize: 0.14,\n  moonSize: 0.17,\n  sunGlowIntensity: 3.05,\n  sunGlowSize: 0.3,\n  sunRayCount: 6,\n  sunRayLength: 3.0,\n  sunRayIntensity: 0.1,\n  sunRayShimmer: 1.0,\n  sunRayShimmerSpeed: 1.0,\n  moonGlowIntensity: 3.45,\n  moonGlowSize: 0.94,\n  skyBrightness: 1.0,\n  skySaturation: 1.0,\n  skyContrast: 1.0,\n};\n\nexport const DEFAULT_CLOUD: CloudParams = {\n  coverage: 0.5,\n  density: 0.7,\n  softness: 0.5,\n  cloudScale: 1.0,\n  windSpeed: 0.3,\n  windAngle: 0,\n  turbulence: 0.3,\n  lightIntensity: 1.0,\n  ambientDarkness: 0.2,\n  backlightIntensity: 0.5,\n  numLayers: 3,\n};\n\nexport const DEFAULT_RAIN: RainParams = {\n  glassIntensity: 0.5,\n  glassZoom: 1.0,\n  fallingIntensity: 0.6,\n  fallingSpeed: 2.0,\n  fallingAngle: 0.1,\n  fallingStreakLength: 1.0,\n  fallingLayers: 4,\n};\n\nexport const DEFAULT_LIGHTNING: LightningParams = {\n  enabled: false,\n  autoMode: true,\n  autoInterval: 8,\n  flashIntensity: 1.0,\n  branchDensity: 0.5,\n};\n\nexport const DEFAULT_SNOW: SnowParams = {\n  intensity: 0.5,\n  layers: 4,\n  fallSpeed: 0.6,\n  windSpeed: 0.3,\n  windAngle: 0.2,\n  turbulence: 0.3,\n  drift: 0.5,\n  flutter: 0.5,\n  windShear: 0.5,\n  flakeSize: 1.0,\n  sizeVariation: 0.5,\n  opacity: 0.5,\n  glowAmount: 0.25,\n  sparkle: 0.25,\n};\n\nexport const DEFAULT_INTERACTIONS: InteractionParams = {\n  rainRefractionStrength: 1.0,\n  lightningSceneIllumination: 0.6,\n};\n\nexport const DEFAULT_POST: PostProcessParams = {\n  enabled: true,\n\n  haze: 0.0,\n  hazeHorizon: 0.8,\n  hazeDesaturation: 0.35,\n  hazeContrast: 0.6,\n\n  bloomIntensity: 0.0,\n  bloomThreshold: 0.82,\n  bloomKnee: 0.35,\n  bloomRadius: 1.2,\n  bloomTapScale: 1.0,\n\n  exposureIntensity: 0.0,\n  exposureDesaturation: 0.25,\n  exposureRecovery: 1.0,\n\n  godRayIntensity: 0.0,\n  godRayDecay: 0.965,\n  godRayDensity: 0.9,\n  godRayWeight: 0.35,\n  godRaySamples: 16,\n};\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/weather-effects-props.ts",
    "content": "import {\n  DEFAULT_CELESTIAL,\n  DEFAULT_CLOUD,\n  DEFAULT_INTERACTIONS,\n  DEFAULT_LAYERS,\n  DEFAULT_LIGHTNING,\n  DEFAULT_POST,\n  DEFAULT_RAIN,\n  DEFAULT_SNOW,\n} from \"./weather-effects-defaults\";\nimport type {\n  ResolvedWeatherEffectsCanvasProps,\n  WeatherEffectsCanvasProps,\n} from \"./weather-effects-types\";\n\nfunction mergeDefined<T extends object>(\n  defaults: T,\n  overrides: Partial<T> | undefined,\n): T {\n  if (!overrides) {\n    return { ...defaults };\n  }\n\n  const merged = { ...defaults };\n  for (const key of Object.keys(overrides) as Array<keyof T>) {\n    const value = overrides[key];\n    if (value !== undefined) {\n      merged[key] = value;\n    }\n  }\n\n  return merged;\n}\n\nexport function resolveWeatherEffectsCanvasRuntimeProps(\n  props: WeatherEffectsCanvasProps,\n): ResolvedWeatherEffectsCanvasProps {\n  return {\n    layers: mergeDefined(DEFAULT_LAYERS, props.layers),\n    celestial: mergeDefined(DEFAULT_CELESTIAL, props.celestial),\n    cloud: mergeDefined(DEFAULT_CLOUD, props.cloud),\n    rain: mergeDefined(DEFAULT_RAIN, props.rain),\n    lightning: mergeDefined(DEFAULT_LIGHTNING, props.lightning),\n    snow: mergeDefined(DEFAULT_SNOW, props.snow),\n    interactions: mergeDefined(DEFAULT_INTERACTIONS, props.interactions),\n    post: mergeDefined(DEFAULT_POST, props.post),\n    dpr: props.dpr,\n  };\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/weather-effects-types.ts",
    "content": "export interface CelestialParams {\n  timeOfDay: number;\n  moonPhase: number;\n  starDensity: number;\n  celestialX: number;\n  celestialY: number;\n  sunSize: number;\n  moonSize: number;\n  sunGlowIntensity: number;\n  sunGlowSize: number;\n  sunRayCount: number;\n  sunRayLength: number;\n  sunRayIntensity: number;\n  /**\n   * Scales subtle, noise-driven ray motion (shimmer + slow \"breathing\").\n   * 0 disables motion; 1 is the default subtlety; >1 increases visibility.\n   */\n  sunRayShimmer: number;\n  /**\n   * Global speed multiplier for the ray shimmer/breath noise inputs.\n   * 1 is the default speed; >1 speeds up motion.\n   */\n  sunRayShimmerSpeed: number;\n  moonGlowIntensity: number;\n  moonGlowSize: number;\n  skyBrightness: number;\n  skySaturation: number;\n  skyContrast: number;\n}\n\nexport interface CloudParams {\n  coverage: number;\n  density: number;\n  softness: number;\n  cloudScale: number;\n  windSpeed: number;\n  windAngle: number;\n  turbulence: number;\n  lightIntensity: number;\n  ambientDarkness: number;\n  backlightIntensity: number;\n  numLayers: number;\n}\n\nexport interface RainParams {\n  glassIntensity: number;\n  glassZoom: number;\n  fallingIntensity: number;\n  fallingSpeed: number;\n  fallingAngle: number;\n  fallingStreakLength: number;\n  fallingLayers: number;\n}\n\nexport interface LightningParams {\n  enabled: boolean;\n  autoMode: boolean;\n  autoInterval: number;\n  flashIntensity: number;\n  branchDensity: number;\n}\n\nexport interface SnowParams {\n  intensity: number;\n  layers: number;\n  fallSpeed: number;\n  windSpeed: number;\n  windAngle: number;\n  turbulence: number;\n  drift: number;\n  flutter: number;\n  windShear: number;\n  flakeSize: number;\n  sizeVariation: number;\n  opacity: number;\n  glowAmount: number;\n  sparkle: number;\n}\n\nexport interface InteractionParams {\n  rainRefractionStrength: number;\n  lightningSceneIllumination: number;\n}\n\nexport interface PostProcessParams {\n  enabled: boolean;\n\n  // ---------------------------------------------------------------------------\n  // Aerial perspective / haze\n  // ---------------------------------------------------------------------------\n  /**\n   * 0 = clear air, 1 = heavy haze/fog.\n   * This is intended to be driven by visibility (and optionally condition).\n   */\n  haze: number;\n  /**\n   * Controls how much haze concentrates near the horizon (bottom of the scene).\n   * 0 = uniform, 1 = strongly horizon-weighted.\n   */\n  hazeHorizon: number;\n  /**\n   * Additional desaturation applied by haze (scaled by `haze`).\n   * 0 = none, 1 = strong.\n   */\n  hazeDesaturation: number;\n  /**\n   * Contrast compression applied by haze (scaled by `haze`).\n   * 0 = none, 1 = strong.\n   */\n  hazeContrast: number;\n\n  // ---------------------------------------------------------------------------\n  // Forward-scatter bloom / glare\n  // ---------------------------------------------------------------------------\n  bloomIntensity: number;\n  bloomThreshold: number;\n  bloomKnee: number;\n  /**\n   * Blur radius in pixels at 1x DPR. Higher values = softer bloom.\n   */\n  bloomRadius: number;\n  /**\n   * Scales sampling offsets so we can reduce bloom cost on large canvases.\n   * 1 = normal, >1 = fewer effective taps, <1 = higher quality.\n   */\n  bloomTapScale: number;\n\n  // ---------------------------------------------------------------------------\n  // Exposure response (lightning overexposure + recovery)\n  // ---------------------------------------------------------------------------\n  exposureIntensity: number;\n  exposureDesaturation: number;\n  exposureRecovery: number;\n\n  // ---------------------------------------------------------------------------\n  // Crepuscular rays (screen-space shafts)\n  // ---------------------------------------------------------------------------\n  godRayIntensity: number;\n  godRayDecay: number;\n  godRayDensity: number;\n  godRayWeight: number;\n  godRaySamples: number;\n}\n\nexport interface GlassParams {\n  enabled: boolean;\n  depth: number;\n  strength: number;\n  chromaticAberration: number;\n  blur: number;\n  brightness: number;\n  saturation: number;\n}\n\nexport interface LayerToggles {\n  celestial: boolean;\n  clouds: boolean;\n  rain: boolean;\n  lightning: boolean;\n  snow: boolean;\n}\n\nexport interface WeatherEffectsCanvasProps {\n  className?: string;\n  /**\n   * Override device pixel ratio used for rendering.\n   * When omitted, defaults to `window.devicePixelRatio`.\n   */\n  dpr?: number;\n  layers?: Partial<LayerToggles>;\n  celestial?: Partial<CelestialParams>;\n  cloud?: Partial<CloudParams>;\n  rain?: Partial<RainParams>;\n  lightning?: Partial<LightningParams>;\n  snow?: Partial<SnowParams>;\n  glass?: Partial<GlassParams>;\n  interactions?: Partial<InteractionParams>;\n  post?: Partial<PostProcessParams>;\n}\n\nexport interface ResolvedWeatherEffectsCanvasProps {\n  layers: LayerToggles;\n  celestial: CelestialParams;\n  cloud: CloudParams;\n  rain: RainParams;\n  lightning: LightningParams;\n  snow: SnowParams;\n  interactions: InteractionParams;\n  post: PostProcessParams;\n  dpr?: number;\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/effects/weather-webgl-budget.ts",
    "content": "// WebGL contexts are a limited resource and creating too many can cause\n// unpredictable \"context lost\" behavior or black canvases.\n//\n// We default to a conservative budget and allow sandbox tooling (tuning studio)\n// to raise it temporarily when rendering many previews at once.\nlet maxConcurrentWeatherWebglCanvases = 8;\nconst allocatedWeatherWebglCanvases = new Set<HTMLCanvasElement>();\n\nexport function getMaxConcurrentWeatherWebglCanvases(): number {\n  return maxConcurrentWeatherWebglCanvases;\n}\n\nexport function setMaxConcurrentWeatherWebglCanvases(value: number): void {\n  if (!Number.isFinite(value)) return;\n  const next = Math.max(1, Math.min(64, Math.floor(value)));\n  maxConcurrentWeatherWebglCanvases = next;\n}\n\nexport function getAllocatedWeatherWebglCanvasCount(): number {\n  return allocatedWeatherWebglCanvases.size;\n}\n\nexport function __resetWeatherWebglCanvasBudgetForTests(): void {\n  maxConcurrentWeatherWebglCanvases = 8;\n  allocatedWeatherWebglCanvases.clear();\n}\n\nexport function tryAcquireWeatherWebglCanvasBudgetSlot(\n  canvas: HTMLCanvasElement,\n): boolean {\n  if (allocatedWeatherWebglCanvases.has(canvas)) return true;\n  if (allocatedWeatherWebglCanvases.size >= maxConcurrentWeatherWebglCanvases)\n    return false;\n  allocatedWeatherWebglCanvases.add(canvas);\n  return true;\n}\n\nexport function releaseWeatherWebglCanvasBudgetSlot(\n  canvas: HTMLCanvasElement,\n): void {\n  allocatedWeatherWebglCanvases.delete(canvas);\n}\n\nexport function releaseWeatherWebglBudgetSlotOnInitFailure(\n  canvas: HTMLCanvasElement,\n  hadBudgetSlot: boolean | null,\n): boolean | null {\n  if (hadBudgetSlot) {\n    releaseWeatherWebglCanvasBudgetSlot(canvas);\n    return null;\n  }\n  return hadBudgetSlot;\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/index.tsx",
    "content": "export { WeatherWidget } from \"./weather-widget\";\nexport { WeatherDataOverlay } from \"./weather-data-overlay\";\nexport type {\n  WeatherDataOverlayProps,\n  GlassEffectParams,\n} from \"./weather-data-overlay\";\nexport {\n  resolveWeatherTime,\n  timeBucketToTimeOfDay,\n  snapTimeOfDayToNearestCheckpoint,\n} from \"./time\";\nexport {\n  type WeatherWidgetPayload,\n  type WeatherWidgetProps,\n  type WeatherWidgetCurrent,\n  type WeatherWidgetTime,\n  type WeatherWidgetLocation,\n  type WeatherConditionCode,\n  type ForecastDay,\n  type TemperatureUnit,\n  type WeatherEffectDrivers,\n  type PrecipitationLevel,\n} from \"./schema\";\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/schema-runtime.ts",
    "content": "import type { EffectSettings } from \"./effects/types\";\nimport type {\n  ForecastDay,\n  PrecipitationLevel,\n  TemperatureUnit,\n  WeatherConditionCode,\n  WeatherWidgetCurrent,\n  WeatherWidgetLocation,\n  WeatherWidgetPayload,\n  WeatherWidgetTime,\n} from \"./schema\";\n\nexport type {\n  ForecastDay,\n  PrecipitationLevel,\n  TemperatureUnit,\n  WeatherConditionCode,\n  WeatherWidgetCurrent,\n  WeatherWidgetLocation,\n  WeatherWidgetPayload,\n  WeatherWidgetTime,\n};\n\nexport interface WeatherWidgetRuntimeProps extends WeatherWidgetPayload {\n  className?: string;\n  effects?: EffectSettings;\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/schema.ts",
    "content": "import { z } from \"zod\";\nimport { defineToolUiContract } from \"../shared/contract\";\n\nimport type { CustomEffectProps } from \"./effects/custom-effect-props\";\nimport type { EffectSettings } from \"./effects/types\";\n\nexport const WeatherConditionCodeSchema = z.enum([\n  \"clear\",\n  \"partly-cloudy\",\n  \"cloudy\",\n  \"overcast\",\n  \"fog\",\n  \"drizzle\",\n  \"rain\",\n  \"heavy-rain\",\n  \"thunderstorm\",\n  \"snow\",\n  \"sleet\",\n  \"hail\",\n  \"windy\",\n]);\n\nexport type WeatherConditionCode = z.infer<typeof WeatherConditionCodeSchema>;\n\nexport const TemperatureUnitSchema = z.enum([\"celsius\", \"fahrenheit\"]);\n\nexport const ForecastDaySchema = z.object({\n  label: z.string().min(1),\n  conditionCode: WeatherConditionCodeSchema,\n  tempMin: z.number(),\n  tempMax: z.number(),\n});\n\nexport type ForecastDay = z.infer<typeof ForecastDaySchema>;\n\nexport type TemperatureUnit = z.infer<typeof TemperatureUnitSchema>;\n\nexport const PrecipitationLevelSchema = z.enum([\n  \"none\",\n  \"light\",\n  \"moderate\",\n  \"heavy\",\n]);\n\nexport type PrecipitationLevel = z.infer<typeof PrecipitationLevelSchema>;\n\nexport const TimeBucketSchema = z.number().int().min(0).max(11);\n\nexport const WeatherWidgetPayloadSchema = z\n  .object({\n    version: z.literal(\"3.1\"),\n    id: z.string().min(1),\n    location: z.object({\n      name: z.string().min(1),\n    }),\n    units: z.object({\n      temperature: TemperatureUnitSchema,\n    }),\n    current: z.object({\n      conditionCode: WeatherConditionCodeSchema,\n      temperature: z.number(),\n      tempMin: z.number(),\n      tempMax: z.number(),\n      windSpeed: z.number().optional(),\n      precipitationLevel: PrecipitationLevelSchema.optional(),\n      visibility: z.number().optional(),\n    }),\n    forecast: z.array(ForecastDaySchema).min(1).max(7),\n    // Rendering-time hints only (not weather data):\n    // - `timeBucket` selects one of 12 fixed scenes\n    // - `localTimeOfDay` gives a continuous 0..1 position in the day cycle\n    time: z.object({\n      timeBucket: TimeBucketSchema.optional(),\n      localTimeOfDay: z.number().min(0).max(1).optional(),\n    }),\n    updatedAt: z.string().datetime().optional(),\n  })\n  .superRefine((value, ctx) => {\n    const hasBucket = value.time.timeBucket !== undefined;\n    const hasLocalTime = value.time.localTimeOfDay !== undefined;\n\n    if (!hasBucket && !hasLocalTime) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        path: [\"time\"],\n        message: \"time must include timeBucket or localTimeOfDay\",\n      });\n      return;\n    }\n\n    if (hasBucket && hasLocalTime) {\n      const normalized = (((value.time.localTimeOfDay ?? 0) % 1) + 1) % 1;\n      const derivedBucket = Math.floor(normalized * 12) % 12;\n      if (derivedBucket !== value.time.timeBucket) {\n        ctx.addIssue({\n          code: z.ZodIssueCode.custom,\n          path: [\"time\"],\n          message: \"timeBucket must match localTimeOfDay bucket\",\n        });\n      }\n    }\n  });\n\nexport type WeatherWidgetPayload = z.infer<typeof WeatherWidgetPayloadSchema>;\n\nexport type WeatherWidgetCurrent = WeatherWidgetPayload[\"current\"];\n\nexport type WeatherWidgetTime = WeatherWidgetPayload[\"time\"];\n\nexport type WeatherWidgetLocation = WeatherWidgetPayload[\"location\"];\n\nexport const WeatherEffectDriversSchema = z.object({\n  windSpeed: z.number().optional(),\n  precipitationLevel: PrecipitationLevelSchema.optional(),\n  visibility: z.number().optional(),\n});\n\nexport type WeatherEffectDrivers = z.infer<typeof WeatherEffectDriversSchema>;\n\nconst WeatherWidgetPayloadSchemaContract = defineToolUiContract(\n  \"WeatherWidget\",\n  WeatherWidgetPayloadSchema,\n);\n\nexport const parseWeatherWidgetPayload: (\n  input: unknown,\n) => WeatherWidgetPayload = WeatherWidgetPayloadSchemaContract.parse;\n\nexport const safeParseWeatherWidgetPayload: (\n  input: unknown,\n) => WeatherWidgetPayload | null = WeatherWidgetPayloadSchemaContract.safeParse;\nexport interface WeatherWidgetProps extends WeatherWidgetPayload {\n  className?: string;\n  effects?: EffectSettings;\n  /**\n   * Custom effect props for direct control over all effect parameters.\n   * Used by the compositor sandbox for tuning effects in context.\n   */\n  customEffectProps?: CustomEffectProps;\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/time.ts",
    "content": "import { getTimeOfDay } from \"./effects/parameter-mapper\";\nimport { getNearestCheckpoint, TIME_CHECKPOINTS } from \"./effects/tuning\";\nimport type { WeatherWidgetTime } from \"./schema-runtime\";\n\nexport interface ResolveWeatherTimeInput {\n  time?: WeatherWidgetTime;\n  updatedAt?: string;\n}\n\nexport type WeatherTimeSource =\n  | \"timeBucket\"\n  | \"localTimeOfDay\"\n  | \"updatedAt\"\n  | \"defaultNoon\";\n\nexport interface ResolvedWeatherTime {\n  timeOfDay: number;\n  source: WeatherTimeSource;\n}\n\nfunction normalizeTimeOfDay(value: number): number {\n  const normalized = ((value % 1) + 1) % 1;\n  return normalized;\n}\n\nexport function snapTimeOfDayToNearestCheckpoint(timeOfDay: number): number {\n  const normalized = normalizeTimeOfDay(timeOfDay);\n  const checkpoint = getNearestCheckpoint(normalized);\n  return TIME_CHECKPOINTS[checkpoint];\n}\n\nexport function timeBucketToTimeOfDay(timeBucket: number): number {\n  const normalizedBucket = ((Math.floor(timeBucket) % 12) + 12) % 12;\n  return (normalizedBucket + 0.5) / 12;\n}\n\nexport function resolveWeatherTime(\n  input: ResolveWeatherTimeInput,\n): ResolvedWeatherTime {\n  const { time, updatedAt } = input;\n\n  if (typeof time?.timeBucket === \"number\") {\n    return {\n      timeOfDay: timeBucketToTimeOfDay(time.timeBucket),\n      source: \"timeBucket\",\n    };\n  }\n\n  if (typeof time?.localTimeOfDay === \"number\") {\n    return {\n      timeOfDay: normalizeTimeOfDay(time.localTimeOfDay),\n      source: \"localTimeOfDay\",\n    };\n  }\n\n  if (typeof updatedAt === \"string\") {\n    return {\n      timeOfDay: getTimeOfDay(updatedAt),\n      source: \"updatedAt\",\n    };\n  }\n\n  return {\n    timeOfDay: 0.5,\n    source: \"defaultNoon\",\n  };\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/weather-data-overlay.generated.ts",
    "content": "// @ts-nocheck\n// AUTO-GENERATED by `pnpm weather:compile`.\n// Source: lib/weather-authoring/weather-widget/weather-data-overlay.tsx\n// DO NOT EDIT MANUALLY.\n\n\"use client\";\nimport { jsx as _jsx, jsxs as _jsxs } from \"react/jsx-runtime\";\nimport { useEffect, useRef, useState, useCallback } from \"react\";\nimport { Sun, Cloud, CloudSun, CloudFog, CloudDrizzle, CloudRain, CloudLightning, Snowflake, CloudHail, Wind, } from \"lucide-react\";\nimport { cn } from \"./_adapter\";\nimport { getSceneBrightnessFromTimeOfDay, getTimeOfDay, getWeatherTheme, } from \"./effects/parameter-mapper\";\nimport { resolveGlassBackdropFilterStyles } from \"./effects/glass-style-resolver\";\nimport { useGlassStyles } from \"./effects/use-glass-styles\";\nfunction getPeakIntensity(timeOfDay) {\n    const noonDistance = Math.abs(timeOfDay - 0.5);\n    const midnightDistance = Math.min(timeOfDay, 1 - timeOfDay);\n    const minDistance = Math.min(noonDistance, midnightDistance);\n    return Math.max(0, 1 - minDistance * 4);\n}\nfunction sineEasedGradient(x, y, radius, peakOpacity, steps = 8) {\n    const stops = [];\n    for (let i = 0; i <= steps; i++) {\n        const t = i / steps;\n        const eased = Math.sin((t * Math.PI) / 2);\n        const opacity = peakOpacity * (1 - eased);\n        const position = t * 100;\n        stops.push(`rgba(255,255,255,${opacity.toFixed(4)}) ${position.toFixed(1)}%`);\n    }\n    return `radial-gradient(circle ${radius}px at ${x}px ${y}px, ${stops.join(\", \")})`;\n}\nconst conditionIcons = {\n    clear: Sun,\n    \"partly-cloudy\": CloudSun,\n    cloudy: Cloud,\n    overcast: Cloud,\n    fog: CloudFog,\n    drizzle: CloudDrizzle,\n    rain: CloudRain,\n    \"heavy-rain\": CloudRain,\n    thunderstorm: CloudLightning,\n    snow: Snowflake,\n    sleet: CloudHail,\n    hail: CloudHail,\n    windy: Wind,\n};\nexport function observeCardDimensions(element, onResize) {\n    if (!element || typeof ResizeObserver !== \"function\") {\n        return () => { };\n    }\n    const observer = new ResizeObserver(onResize);\n    observer.observe(element);\n    return () => observer.disconnect();\n}\nexport function WeatherDataOverlay({ location, conditionCode, temperature, tempHigh, tempLow, forecast = [], unit = \"fahrenheit\", theme: themeProp, timeOfDay: timeOfDayProp, timestamp, className, reducedMotion = false, glassParams, }) {\n    const timeOfDay = typeof timeOfDayProp === \"number\"\n        ? timeOfDayProp\n        : typeof timestamp === \"string\"\n            ? getTimeOfDay(timestamp)\n            : 0.5;\n    const [glowState, setGlowState] = useState({\n        x: 0,\n        y: 0,\n        intensity: 0,\n    });\n    const [cardDimensions, setCardDimensions] = useState({ width: 0, height: 0 });\n    const cardRef = useRef(null);\n    const containerRef = useRef(null);\n    const pendingGlowStateRef = useRef(null);\n    const pendingGlowFrameRef = useRef(null);\n    const glassEnabled = glassParams?.enabled !== false;\n    const glassStyles = useGlassStyles({\n        width: cardDimensions.width,\n        height: cardDimensions.height,\n        depth: glassParams?.depth ?? 3,\n        radius: 12,\n        strength: glassParams?.strength ?? 75,\n        chromaticAberration: glassParams?.chromaticAberration ?? 6,\n        blur: glassParams?.blur ?? 1.5,\n        brightness: glassParams?.brightness ?? 0.8,\n        saturation: glassParams?.saturation ?? 1.3,\n        enabled: glassEnabled,\n    });\n    const updateCardDimensions = useCallback(() => {\n        if (cardRef.current) {\n            const rect = cardRef.current.getBoundingClientRect();\n            setCardDimensions({\n                width: Math.round(rect.width),\n                height: Math.round(rect.height),\n            });\n        }\n    }, []);\n    const hasForecastStrip = forecast.length > 0;\n    useEffect(() => {\n        updateCardDimensions();\n        return observeCardDimensions(cardRef.current, updateCardDimensions);\n    }, [hasForecastStrip, updateCardDimensions]);\n    const theme = themeProp ??\n        getWeatherTheme(getSceneBrightnessFromTimeOfDay(timeOfDay, conditionCode));\n    const commitGlowState = useCallback((nextState) => {\n        setGlowState((prevState) => {\n            if (prevState.x === nextState.x &&\n                prevState.y === nextState.y &&\n                prevState.intensity === nextState.intensity) {\n                return prevState;\n            }\n            return nextState;\n        });\n    }, []);\n    const cancelPendingGlowFrame = useCallback(() => {\n        pendingGlowStateRef.current = null;\n        if (pendingGlowFrameRef.current !== null &&\n            typeof window !== \"undefined\" &&\n            typeof window.cancelAnimationFrame === \"function\") {\n            window.cancelAnimationFrame(pendingGlowFrameRef.current);\n        }\n        pendingGlowFrameRef.current = null;\n    }, []);\n    const scheduleGlowState = useCallback((nextState) => {\n        pendingGlowStateRef.current = nextState;\n        if (pendingGlowFrameRef.current !== null) {\n            return;\n        }\n        if (typeof window === \"undefined\" ||\n            typeof window.requestAnimationFrame !== \"function\") {\n            pendingGlowStateRef.current = null;\n            commitGlowState(nextState);\n            return;\n        }\n        pendingGlowFrameRef.current = window.requestAnimationFrame(() => {\n            pendingGlowFrameRef.current = null;\n            const pendingState = pendingGlowStateRef.current;\n            pendingGlowStateRef.current = null;\n            if (pendingState) {\n                commitGlowState(pendingState);\n            }\n        });\n    }, [commitGlowState]);\n    const clearGlowIntensity = useCallback(() => {\n        cancelPendingGlowFrame();\n        setGlowState((prevState) => {\n            if (prevState.intensity === 0) {\n                return prevState;\n            }\n            return { ...prevState, intensity: 0 };\n        });\n    }, [cancelPendingGlowFrame]);\n    useEffect(() => {\n        if (reducedMotion) {\n            clearGlowIntensity();\n            return;\n        }\n        const container = containerRef.current;\n        if (!container)\n            return;\n        const handleMouseMove = (e) => {\n            if (!cardRef.current)\n                return;\n            const cardRect = cardRef.current.getBoundingClientRect();\n            const clampedX = Math.max(cardRect.left, Math.min(e.clientX, cardRect.right));\n            const clampedY = Math.max(cardRect.top, Math.min(e.clientY, cardRect.bottom));\n            const distanceX = e.clientX < cardRect.left\n                ? cardRect.left - e.clientX\n                : e.clientX > cardRect.right\n                    ? e.clientX - cardRect.right\n                    : 0;\n            const distanceY = e.clientY < cardRect.top\n                ? cardRect.top - e.clientY\n                : e.clientY > cardRect.bottom\n                    ? e.clientY - cardRect.bottom\n                    : 0;\n            const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);\n            const maxDistance = 150;\n            const intensity = Math.max(0, 1 - distance / maxDistance);\n            scheduleGlowState({\n                x: clampedX - cardRect.left,\n                y: clampedY - cardRect.top,\n                intensity,\n            });\n        };\n        const handleMouseLeave = () => {\n            clearGlowIntensity();\n        };\n        container.addEventListener(\"mousemove\", handleMouseMove);\n        container.addEventListener(\"mouseleave\", handleMouseLeave);\n        return () => {\n            container.removeEventListener(\"mousemove\", handleMouseMove);\n            container.removeEventListener(\"mouseleave\", handleMouseLeave);\n            cancelPendingGlowFrame();\n        };\n    }, [\n        reducedMotion,\n        clearGlowIntensity,\n        scheduleGlowState,\n        cancelPendingGlowFrame,\n    ]);\n    const roundedTemperature = Math.round(temperature);\n    const unitSymbol = unit === \"celsius\" ? \"C\" : \"F\";\n    const spokenUnit = unit === \"celsius\" ? \"Celsius\" : \"Fahrenheit\";\n    const peakIntensity = getPeakIntensity(timeOfDay);\n    const isDark = theme === \"dark\";\n    const textPrimary = isDark ? \"text-white\" : \"text-black\";\n    const textPrimarySoft = isDark ? \"text-white/90\" : \"text-black/85\";\n    const textSecondary = isDark ? \"text-white/80\" : \"text-black/80\";\n    const textSubtle = isDark ? \"text-white/40\" : \"text-black/40\";\n    const baseBgOpacity = isDark ? 0.04 : 0.04;\n    const bgOpacity = baseBgOpacity * (1 - peakIntensity * 0.7);\n    const midnightDistance = Math.min(timeOfDay, 1 - timeOfDay);\n    const baseBlur = isDark ? 2 + midnightDistance * 38 : 24;\n    const blurAmount = isDark\n        ? baseBlur\n        : baseBlur - peakIntensity * (baseBlur - 8);\n    const isDawn = timeOfDay > 0.1 && timeOfDay < 0.4;\n    const dawnIntensity = isDawn ? 1 - Math.abs(timeOfDay - 0.25) * 4 : 0;\n    const forecastTextShadow = dawnIntensity > 0\n        ? `0 0.5px 1px rgba(0,0,0,${(dawnIntensity * 0.4).toFixed(2)})`\n        : undefined;\n    const shadowStyle = isDark\n        ? \"0 1px 8px rgba(0,0,0,0.3)\"\n        : \"0 1px 8px rgba(255,255,255,0.3)\";\n    const locationFontSize = \"clamp(13px, 7.5cqmin, 17px)\";\n    const temperatureFontSize = \"clamp(48px, 32cqmin, 72px)\";\n    const degreeFontSize = \"clamp(18px, 12cqmin, 28px)\";\n    const hiLoFontSize = \"clamp(11px, 6.5cqmin, 15px)\";\n    const forecastFontFamily = '\"SF Pro Text\", Inter, \"Noto Sans\", system-ui, sans-serif';\n    return (_jsxs(\"div\", { ref: containerRef, className: cn(\"pointer-events-auto absolute inset-0 z-10 flex select-none flex-col\", className), children: [_jsx(\"div\", { className: \"px-6 pt-6\", children: _jsxs(\"div\", { className: \"flex flex-col items-start\", children: [_jsx(\"h2\", { className: cn(\"font-medium leading-[1.08] tracking-tight\", textSecondary), style: {\n                                fontSize: locationFontSize,\n                                fontFamily: forecastFontFamily,\n                                textShadow: shadowStyle,\n                            }, children: location }), _jsxs(\"div\", { className: \"-mt-0.5 flex items-start gap-1\", children: [_jsx(\"span\", { className: cn(\"font-[250] tabular-nums leading-[1.02] tracking-[-0.015em]\", textPrimarySoft), style: {\n                                        fontSize: temperatureFontSize,\n                                        fontFamily: forecastFontFamily,\n                                        fontFeatureSettings: '\"tnum\" 1, \"case\" 1',\n                                        textShadow: isDark\n                                            ? \"0 2px 20px rgba(0,0,0,0.25)\"\n                                            : \"0 2px 20px rgba(255,255,255,0.3)\",\n                                    }, \"aria-hidden\": \"true\", children: roundedTemperature }), _jsxs(\"span\", { className: cn(\"mt-2 font-[250] tabular-nums\", textSecondary), style: {\n                                        fontSize: degreeFontSize,\n                                        fontFamily: forecastFontFamily,\n                                        fontFeatureSettings: '\"tnum\" 1, \"case\" 1',\n                                    }, \"aria-hidden\": \"true\", children: [\"\\u00B0\", unitSymbol] }), _jsxs(\"span\", { className: \"sr-only\", children: [roundedTemperature, \" degrees \", spokenUnit] })] }), _jsxs(\"div\", { className: \"mt-0.5 flex items-center gap-3\", style: {\n                                fontFamily: forecastFontFamily,\n                                fontFeatureSettings: '\"tnum\" 1, \"case\" 1',\n                            }, children: [_jsxs(\"span\", { className: \"font-medium tabular-nums\", style: { fontSize: hiLoFontSize }, children: [_jsx(\"span\", { className: textSubtle, children: \"H \" }), _jsxs(\"span\", { className: textPrimary, children: [Math.round(tempHigh), \"\\u00B0\"] })] }), _jsxs(\"span\", { className: \"font-medium tabular-nums\", style: { fontSize: hiLoFontSize }, children: [_jsx(\"span\", { className: textSubtle, children: \"L \" }), _jsxs(\"span\", { className: textPrimary, children: [Math.round(tempLow), \"\\u00B0\"] })] })] })] }) }), _jsx(\"div\", { className: \"flex-1\" }), forecast.length > 0 && (_jsx(\"div\", { className: \"px-3 pb-3\", children: _jsxs(\"div\", { ref: cardRef, className: \"weather-forecast-strip relative hidden\", children: [_jsx(\"div\", { className: \"pointer-events-none absolute inset-0 z-10 rounded-xl transition-opacity duration-300 ease-out\", style: {\n                                opacity: glowState.intensity,\n                                background: sineEasedGradient(glowState.x, glowState.y, 100, isDark ? 0.6 : 1),\n                                mask: \"linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)\",\n                                maskComposite: \"exclude\",\n                                WebkitMaskComposite: \"xor\",\n                                padding: \"0.5px\",\n                            } }), _jsxs(\"div\", { className: \"relative overflow-hidden rounded-xl px-3 py-2.5\", style: {\n                                backgroundColor: `rgba(255, 255, 255, ${bgOpacity})`,\n                                ...resolveGlassBackdropFilterStyles({\n                                    glassStyles,\n                                    blurAmount,\n                                }),\n                            }, children: [_jsx(\"div\", { className: \"pointer-events-none absolute inset-0 mix-blend-color-dodge transition-opacity duration-300 ease-out\", style: {\n                                        opacity: glowState.intensity,\n                                        background: sineEasedGradient(glowState.x, glowState.y, 120, isDark ? 0.06 : 0.15),\n                                    } }), _jsx(\"div\", { className: \"relative flex items-center justify-between\", children: forecast.slice(0, 5).map((day, index) => {\n                                        const DayIcon = conditionIcons[day.conditionCode];\n                                        return (_jsxs(\"div\", { className: \"flex flex-1 flex-col items-center gap-0.5\", style: {\n                                                fontFamily: forecastFontFamily,\n                                                fontFeatureSettings: '\"tnum\" 1, \"case\" 1',\n                                                textShadow: forecastTextShadow,\n                                            }, children: [_jsx(\"span\", { className: cn(\"text-[10px] uppercase tracking-[0.08em]\", index === 0 ? \"font-semibold\" : \"font-medium\", textPrimary), children: day.label }), _jsx(DayIcon, { className: cn(\"my-0.5 size-5\", textPrimary, index === 0 ? \"opacity-100\" : \"opacity-70\", \"weather-forecast-icon hidden\"), strokeWidth: 1.5, \"aria-hidden\": \"true\" }), _jsxs(\"div\", { className: \"flex flex-col items-center gap-0.5\", children: [_jsxs(\"span\", { className: cn(\"text-[15px] tabular-nums leading-[1.2] tracking-[-0.01em]\", index === 0 ? \"font-semibold\" : \"font-medium\", textPrimary), children: [Math.round(day.tempMax), \"\\u00B0\"] }), _jsxs(\"span\", { className: cn(\"font-normal text-[12px] tabular-nums leading-[1.3]\", textPrimary), children: [Math.round(day.tempMin), \"\\u00B0\"] })] })] }, `${day.label}-${index}`));\n                                    }) })] })] }) })), _jsx(\"style\", { children: `\n        @container weather (min-height: 245px) {\n          .weather-forecast-strip {\n            display: block !important;\n          }\n        }\n        @container weather (min-height: 280px) {\n          .weather-forecast-icon {\n            display: block !important;\n          }\n        }\n      ` })] }));\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/weather-data-overlay.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useRef, useState, useCallback } from \"react\";\nimport {\n  Sun,\n  Cloud,\n  CloudSun,\n  CloudFog,\n  CloudDrizzle,\n  CloudRain,\n  CloudLightning,\n  Snowflake,\n  CloudHail,\n  Wind,\n  type LucideIcon,\n} from \"lucide-react\";\nimport { cn } from \"./_adapter\";\nimport type {\n  ForecastDay,\n  TemperatureUnit,\n  WeatherConditionCode,\n} from \"./schema\";\nimport {\n  getSceneBrightnessFromTimeOfDay,\n  getTimeOfDay,\n  getWeatherTheme,\n  type WeatherTheme,\n} from \"./effects/parameter-mapper\";\nimport { resolveGlassBackdropFilterStyles } from \"./effects/glass-style-resolver\";\nimport { useGlassStyles } from \"./effects/glass-panel-svg\";\n\nfunction getPeakIntensity(timeOfDay: number): number {\n  const noonDistance = Math.abs(timeOfDay - 0.5);\n  const midnightDistance = Math.min(timeOfDay, 1 - timeOfDay);\n  const minDistance = Math.min(noonDistance, midnightDistance);\n  return Math.max(0, 1 - minDistance * 4);\n}\n\nfunction sineEasedGradient(\n  x: number,\n  y: number,\n  radius: number,\n  peakOpacity: number,\n  steps = 8,\n): string {\n  const stops: string[] = [];\n  for (let i = 0; i <= steps; i++) {\n    const t = i / steps;\n    const eased = Math.sin((t * Math.PI) / 2);\n    const opacity = peakOpacity * (1 - eased);\n    const position = t * 100;\n    stops.push(\n      `rgba(255,255,255,${opacity.toFixed(4)}) ${position.toFixed(1)}%`,\n    );\n  }\n  return `radial-gradient(circle ${radius}px at ${x}px ${y}px, ${stops.join(\", \")})`;\n}\n\nconst conditionIcons: Record<WeatherConditionCode, LucideIcon> = {\n  clear: Sun,\n  \"partly-cloudy\": CloudSun,\n  cloudy: Cloud,\n  overcast: Cloud,\n  fog: CloudFog,\n  drizzle: CloudDrizzle,\n  rain: CloudRain,\n  \"heavy-rain\": CloudRain,\n  thunderstorm: CloudLightning,\n  snow: Snowflake,\n  sleet: CloudHail,\n  hail: CloudHail,\n  windy: Wind,\n};\n\nexport interface GlassEffectParams {\n  enabled?: boolean;\n  depth?: number;\n  strength?: number;\n  chromaticAberration?: number;\n  blur?: number;\n  brightness?: number;\n  saturation?: number;\n}\n\nexport interface WeatherDataOverlayProps {\n  location: string;\n  conditionCode: WeatherConditionCode;\n  temperature: number;\n  tempHigh: number;\n  tempLow: number;\n  forecast?: ForecastDay[];\n  unit?: TemperatureUnit;\n  theme?: WeatherTheme;\n  /**\n   * Provide either `timeOfDay` (0-1) or a `timestamp` ISO string.\n   * If neither is provided, defaults to noon (0.5).\n   */\n  timeOfDay?: number;\n  timestamp?: string | undefined;\n  className?: string;\n  reducedMotion?: boolean;\n  /**\n   * Glass refraction effect parameters for the forecast card.\n   * When enabled, applies SVG displacement filter for realistic glass distortion.\n   */\n  glassParams?: GlassEffectParams | undefined;\n}\n\ninterface GlowState {\n  x: number;\n  y: number;\n  intensity: number;\n}\n\nexport function observeCardDimensions(\n  element: HTMLDivElement | null,\n  onResize: () => void,\n): () => void {\n  if (!element || typeof ResizeObserver !== \"function\") {\n    return () => {};\n  }\n\n  const observer = new ResizeObserver(onResize);\n  observer.observe(element);\n  return () => observer.disconnect();\n}\n\nexport function WeatherDataOverlay({\n  location,\n  conditionCode,\n  temperature,\n  tempHigh,\n  tempLow,\n  forecast = [],\n  unit = \"fahrenheit\",\n  theme: themeProp,\n  timeOfDay: timeOfDayProp,\n  timestamp,\n  className,\n  reducedMotion = false,\n  glassParams,\n}: WeatherDataOverlayProps) {\n  const timeOfDay =\n    typeof timeOfDayProp === \"number\"\n      ? timeOfDayProp\n      : typeof timestamp === \"string\"\n        ? getTimeOfDay(timestamp)\n        : 0.5;\n\n  const [glowState, setGlowState] = useState<GlowState>({\n    x: 0,\n    y: 0,\n    intensity: 0,\n  });\n  const [cardDimensions, setCardDimensions] = useState({ width: 0, height: 0 });\n  const cardRef = useRef<HTMLDivElement>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const pendingGlowStateRef = useRef<GlowState | null>(null);\n  const pendingGlowFrameRef = useRef<number | null>(null);\n\n  // Glass effect styles applied directly to forecast container.\n  // Enabled by default - falls back to simple blur if SVG filter unsupported.\n  const glassEnabled = glassParams?.enabled !== false;\n  const glassStyles = useGlassStyles({\n    width: cardDimensions.width,\n    height: cardDimensions.height,\n    depth: glassParams?.depth ?? 3,\n    radius: 12,\n    strength: glassParams?.strength ?? 75,\n    chromaticAberration: glassParams?.chromaticAberration ?? 6,\n    blur: glassParams?.blur ?? 1.5,\n    brightness: glassParams?.brightness ?? 0.8,\n    saturation: glassParams?.saturation ?? 1.3,\n    enabled: glassEnabled,\n  });\n\n  // Track forecast card dimensions for glass effect\n  const updateCardDimensions = useCallback(() => {\n    if (cardRef.current) {\n      const rect = cardRef.current.getBoundingClientRect();\n      setCardDimensions({\n        width: Math.round(rect.width),\n        height: Math.round(rect.height),\n      });\n    }\n  }, []);\n  const hasForecastStrip = forecast.length > 0;\n\n  useEffect(() => {\n    updateCardDimensions();\n    return observeCardDimensions(cardRef.current, updateCardDimensions);\n  }, [hasForecastStrip, updateCardDimensions]);\n\n  const theme =\n    themeProp ??\n    getWeatherTheme(getSceneBrightnessFromTimeOfDay(timeOfDay, conditionCode));\n\n  const commitGlowState = useCallback((nextState: GlowState) => {\n    setGlowState((prevState) => {\n      if (\n        prevState.x === nextState.x &&\n        prevState.y === nextState.y &&\n        prevState.intensity === nextState.intensity\n      ) {\n        return prevState;\n      }\n\n      return nextState;\n    });\n  }, []);\n\n  const cancelPendingGlowFrame = useCallback(() => {\n    pendingGlowStateRef.current = null;\n\n    if (\n      pendingGlowFrameRef.current !== null &&\n      typeof window !== \"undefined\" &&\n      typeof window.cancelAnimationFrame === \"function\"\n    ) {\n      window.cancelAnimationFrame(pendingGlowFrameRef.current);\n    }\n\n    pendingGlowFrameRef.current = null;\n  }, []);\n\n  const scheduleGlowState = useCallback(\n    (nextState: GlowState) => {\n      pendingGlowStateRef.current = nextState;\n\n      if (pendingGlowFrameRef.current !== null) {\n        return;\n      }\n\n      if (\n        typeof window === \"undefined\" ||\n        typeof window.requestAnimationFrame !== \"function\"\n      ) {\n        pendingGlowStateRef.current = null;\n        commitGlowState(nextState);\n        return;\n      }\n\n      pendingGlowFrameRef.current = window.requestAnimationFrame(() => {\n        pendingGlowFrameRef.current = null;\n        const pendingState = pendingGlowStateRef.current;\n        pendingGlowStateRef.current = null;\n\n        if (pendingState) {\n          commitGlowState(pendingState);\n        }\n      });\n    },\n    [commitGlowState],\n  );\n\n  const clearGlowIntensity = useCallback(() => {\n    cancelPendingGlowFrame();\n    setGlowState((prevState) => {\n      if (prevState.intensity === 0) {\n        return prevState;\n      }\n\n      return { ...prevState, intensity: 0 };\n    });\n  }, [cancelPendingGlowFrame]);\n\n  useEffect(() => {\n    if (reducedMotion) {\n      clearGlowIntensity();\n      return;\n    }\n\n    const container = containerRef.current;\n    if (!container) return;\n\n    const handleMouseMove = (e: MouseEvent) => {\n      if (!cardRef.current) return;\n      const cardRect = cardRef.current.getBoundingClientRect();\n\n      const clampedX = Math.max(\n        cardRect.left,\n        Math.min(e.clientX, cardRect.right),\n      );\n      const clampedY = Math.max(\n        cardRect.top,\n        Math.min(e.clientY, cardRect.bottom),\n      );\n\n      const distanceX =\n        e.clientX < cardRect.left\n          ? cardRect.left - e.clientX\n          : e.clientX > cardRect.right\n            ? e.clientX - cardRect.right\n            : 0;\n      const distanceY =\n        e.clientY < cardRect.top\n          ? cardRect.top - e.clientY\n          : e.clientY > cardRect.bottom\n            ? e.clientY - cardRect.bottom\n            : 0;\n      const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);\n\n      const maxDistance = 150;\n      const intensity = Math.max(0, 1 - distance / maxDistance);\n\n      scheduleGlowState({\n        x: clampedX - cardRect.left,\n        y: clampedY - cardRect.top,\n        intensity,\n      });\n    };\n\n    const handleMouseLeave = () => {\n      clearGlowIntensity();\n    };\n\n    container.addEventListener(\"mousemove\", handleMouseMove);\n    container.addEventListener(\"mouseleave\", handleMouseLeave);\n\n    return () => {\n      container.removeEventListener(\"mousemove\", handleMouseMove);\n      container.removeEventListener(\"mouseleave\", handleMouseLeave);\n      cancelPendingGlowFrame();\n    };\n  }, [\n    reducedMotion,\n    clearGlowIntensity,\n    scheduleGlowState,\n    cancelPendingGlowFrame,\n  ]);\n\n  const roundedTemperature = Math.round(temperature);\n  const unitSymbol = unit === \"celsius\" ? \"C\" : \"F\";\n  const spokenUnit = unit === \"celsius\" ? \"Celsius\" : \"Fahrenheit\";\n  const peakIntensity = getPeakIntensity(timeOfDay);\n\n  const isDark = theme === \"dark\";\n  const textPrimary = isDark ? \"text-white\" : \"text-black\";\n  const textPrimarySoft = isDark ? \"text-white/90\" : \"text-black/85\";\n  const textSecondary = isDark ? \"text-white/80\" : \"text-black/80\";\n  const textSubtle = isDark ? \"text-white/40\" : \"text-black/40\";\n\n  const baseBgOpacity = isDark ? 0.04 : 0.04;\n  const bgOpacity = baseBgOpacity * (1 - peakIntensity * 0.7);\n  const midnightDistance = Math.min(timeOfDay, 1 - timeOfDay);\n  const baseBlur = isDark ? 2 + midnightDistance * 38 : 24;\n  const blurAmount = isDark\n    ? baseBlur\n    : baseBlur - peakIntensity * (baseBlur - 8);\n\n  // Dawn intensity peaks around timeOfDay 0.2-0.3 (morning transition)\n  const isDawn = timeOfDay > 0.1 && timeOfDay < 0.4;\n  const dawnIntensity = isDawn ? 1 - Math.abs(timeOfDay - 0.25) * 4 : 0;\n  const forecastTextShadow =\n    dawnIntensity > 0\n      ? `0 0.5px 1px rgba(0,0,0,${(dawnIntensity * 0.4).toFixed(2)})`\n      : undefined;\n\n  const shadowStyle = isDark\n    ? \"0 1px 8px rgba(0,0,0,0.3)\"\n    : \"0 1px 8px rgba(255,255,255,0.3)\";\n\n  // Fluid type scales with the widget container size. (Requires container-type:size.)\n  const locationFontSize = \"clamp(13px, 7.5cqmin, 17px)\";\n  const temperatureFontSize = \"clamp(48px, 32cqmin, 72px)\";\n  const degreeFontSize = \"clamp(18px, 12cqmin, 28px)\";\n  const hiLoFontSize = \"clamp(11px, 6.5cqmin, 15px)\";\n  const forecastFontFamily =\n    '\"SF Pro Text\", Inter, \"Noto Sans\", system-ui, sans-serif';\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\n        \"pointer-events-auto absolute inset-0 z-10 flex select-none flex-col\",\n        className,\n      )}\n    >\n      {/* Current weather (more inset than forecast strip) */}\n      <div className=\"px-6 pt-6\">\n        <div className=\"flex flex-col items-start\">\n          <h2\n            className={cn(\n              \"font-medium leading-[1.08] tracking-tight\",\n              textSecondary,\n            )}\n            style={{\n              fontSize: locationFontSize,\n              fontFamily: forecastFontFamily,\n              textShadow: shadowStyle,\n            }}\n          >\n            {location}\n          </h2>\n\n          <div className=\"-mt-0.5 flex items-start gap-1\">\n            <span\n              className={cn(\n                \"font-[250] tabular-nums leading-[1.02] tracking-[-0.015em]\",\n                textPrimarySoft,\n              )}\n              style={{\n                fontSize: temperatureFontSize,\n                fontFamily: forecastFontFamily,\n                fontFeatureSettings: '\"tnum\" 1, \"case\" 1',\n                textShadow: isDark\n                  ? \"0 2px 20px rgba(0,0,0,0.25)\"\n                  : \"0 2px 20px rgba(255,255,255,0.3)\",\n              }}\n              aria-hidden=\"true\"\n            >\n              {roundedTemperature}\n            </span>\n            <span\n              className={cn(\"mt-2 font-[250] tabular-nums\", textSecondary)}\n              style={{\n                fontSize: degreeFontSize,\n                fontFamily: forecastFontFamily,\n                fontFeatureSettings: '\"tnum\" 1, \"case\" 1',\n              }}\n              aria-hidden=\"true\"\n            >\n              °{unitSymbol}\n            </span>\n            <span className=\"sr-only\">\n              {roundedTemperature} degrees {spokenUnit}\n            </span>\n          </div>\n\n          <div\n            className=\"mt-0.5 flex items-center gap-3\"\n            style={{\n              fontFamily: forecastFontFamily,\n              fontFeatureSettings: '\"tnum\" 1, \"case\" 1',\n            }}\n          >\n            <span\n              className=\"font-medium tabular-nums\"\n              style={{ fontSize: hiLoFontSize }}\n            >\n              <span className={textSubtle}>H </span>\n              <span className={textPrimary}>{Math.round(tempHigh)}°</span>\n            </span>\n            <span\n              className=\"font-medium tabular-nums\"\n              style={{ fontSize: hiLoFontSize }}\n            >\n              <span className={textSubtle}>L </span>\n              <span className={textPrimary}>{Math.round(tempLow)}°</span>\n            </span>\n          </div>\n        </div>\n      </div>\n\n      {/* Spacer */}\n      <div className=\"flex-1\" />\n\n      {/* Forecast strip - hidden at small container sizes (less inset than header) */}\n      {forecast.length > 0 && (\n        <div className=\"px-3 pb-3\">\n          {/* Show the strip earlier, but progressively reduce content as height shrinks. */}\n          <div ref={cardRef} className=\"weather-forecast-strip relative hidden\">\n            {/* Edge shine - outside overflow-hidden so it aligns with border */}\n            <div\n              className=\"pointer-events-none absolute inset-0 z-10 rounded-xl transition-opacity duration-300 ease-out\"\n              style={{\n                opacity: glowState.intensity,\n                background: sineEasedGradient(\n                  glowState.x,\n                  glowState.y,\n                  100,\n                  isDark ? 0.6 : 1,\n                ),\n                mask: \"linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)\",\n                maskComposite: \"exclude\",\n                WebkitMaskComposite: \"xor\",\n                padding: \"0.5px\",\n              }}\n            />\n            <div\n              className=\"relative overflow-hidden rounded-xl px-3 py-2.5\"\n              style={{\n                backgroundColor: `rgba(255, 255, 255, ${bgOpacity})`,\n                ...resolveGlassBackdropFilterStyles({\n                  glassStyles,\n                  blurAmount,\n                }),\n              }}\n            >\n              {/* Inner glow */}\n              <div\n                className=\"pointer-events-none absolute inset-0 mix-blend-color-dodge transition-opacity duration-300 ease-out\"\n                style={{\n                  opacity: glowState.intensity,\n                  background: sineEasedGradient(\n                    glowState.x,\n                    glowState.y,\n                    120,\n                    isDark ? 0.06 : 0.15,\n                  ),\n                }}\n              />\n              <div className=\"relative flex items-center justify-between\">\n                {forecast.slice(0, 5).map((day, index) => {\n                  const DayIcon = conditionIcons[day.conditionCode];\n                  return (\n                    <div\n                      key={`${day.label}-${index}`}\n                      className=\"flex flex-1 flex-col items-center gap-0.5\"\n                      style={{\n                        fontFamily: forecastFontFamily,\n                        fontFeatureSettings: '\"tnum\" 1, \"case\" 1',\n                        textShadow: forecastTextShadow,\n                      }}\n                    >\n                      <span\n                        className={cn(\n                          \"text-[10px] uppercase tracking-[0.08em]\",\n                          index === 0 ? \"font-semibold\" : \"font-medium\",\n                          textPrimary,\n                        )}\n                      >\n                        {day.label}\n                      </span>\n                      <DayIcon\n                        className={cn(\n                          \"my-0.5 size-5\",\n                          textPrimary,\n                          index === 0 ? \"opacity-100\" : \"opacity-70\",\n                          // At shorter containers (but still showing the strip),\n                          // omit the icon to preserve legibility.\n                          \"weather-forecast-icon hidden\",\n                        )}\n                        strokeWidth={1.5}\n                        aria-hidden=\"true\"\n                      />\n                      <div className=\"flex flex-col items-center gap-0.5\">\n                        <span\n                          className={cn(\n                            \"text-[15px] tabular-nums leading-[1.2] tracking-[-0.01em]\",\n                            index === 0 ? \"font-semibold\" : \"font-medium\",\n                            textPrimary,\n                          )}\n                        >\n                          {Math.round(day.tempMax)}°\n                        </span>\n                        <span\n                          className={cn(\n                            \"font-normal text-[12px] tabular-nums leading-[1.3]\",\n                            textPrimary,\n                          )}\n                        >\n                          {Math.round(day.tempMin)}°\n                        </span>\n                      </div>\n                    </div>\n                  );\n                })}\n              </div>\n            </div>\n          </div>\n        </div>\n      )}\n\n      {/* Height-based container queries (requires container-type:size on the weather container). */}\n      <style>{`\n        @container weather (min-height: 245px) {\n          .weather-forecast-strip {\n            display: block !important;\n          }\n        }\n        @container weather (min-height: 280px) {\n          .weather-forecast-icon {\n            display: block !important;\n          }\n        }\n      `}</style>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/weather-runtime-core.ts",
    "content": "export { EffectCompositorRuntime } from \"./effects/effect-compositor-runtime\";\nexport {\n  getSceneBrightnessFromTimeOfDay,\n  getTimeOfDay,\n  getWeatherTheme,\n} from \"./effects/parameter-mapper\";\nexport { getNearestCheckpoint } from \"./effects/tuning\";\nexport { TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES } from \"./effects/generated/tuned-presets.generated\";\nexport { resolveGlassBackdropFilterStyles } from \"./effects/glass-style-resolver\";\nexport { useGlassStyles } from \"./effects/use-glass-styles\";\nexport { resolveWeatherTime, snapTimeOfDayToNearestCheckpoint } from \"./time\";\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/weather-widget-container.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\nimport { cn, Card } from \"./_adapter\";\nimport { EffectCompositorRuntime } from \"./effects/effect-compositor-runtime\";\nimport {\n  getSceneBrightnessFromTimeOfDay,\n  getWeatherTheme,\n} from \"./effects/parameter-mapper\";\nimport { getNearestCheckpoint } from \"./effects/tuning\";\nimport { TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES } from \"./effects/generated/tuned-presets.generated\";\nimport type { WeatherWidgetRuntimeProps } from \"./schema-runtime\";\nimport { resolveWeatherTime, snapTimeOfDayToNearestCheckpoint } from \"./time\";\nimport { WeatherDataOverlay } from \"./weather-data-overlay\";\n\nexport function WeatherWidget({\n  version: _version,\n  id,\n  location,\n  units,\n  current,\n  forecast,\n  time,\n  updatedAt,\n  className,\n  effects,\n}: WeatherWidgetRuntimeProps) {\n  const [prefersReducedMotion, setPrefersReducedMotion] = useState(() => {\n    if (typeof window === \"undefined\") {\n      return false;\n    }\n\n    return (\n      window.matchMedia?.(\"(prefers-reduced-motion: reduce)\")?.matches ?? false\n    );\n  });\n\n  useEffect(() => {\n    if (\n      typeof window === \"undefined\" ||\n      typeof window.matchMedia !== \"function\"\n    ) {\n      return;\n    }\n\n    const mediaQueryList = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\",\n    );\n    setPrefersReducedMotion(mediaQueryList.matches);\n\n    const handleMotionPreferenceChange = (event: MediaQueryListEvent) => {\n      setPrefersReducedMotion(event.matches);\n    };\n\n    if (typeof mediaQueryList.addEventListener === \"function\") {\n      mediaQueryList.addEventListener(\"change\", handleMotionPreferenceChange);\n      return () => {\n        mediaQueryList.removeEventListener(\n          \"change\",\n          handleMotionPreferenceChange,\n        );\n      };\n    }\n\n    mediaQueryList.addListener(handleMotionPreferenceChange);\n    return () => {\n      mediaQueryList.removeListener(handleMotionPreferenceChange);\n    };\n  }, []);\n\n  const reducedMotion = effects?.reducedMotion ?? prefersReducedMotion;\n  const effectsEnabled = effects?.enabled !== false && !reducedMotion;\n\n  const resolvedTime = resolveWeatherTime({\n    time,\n    updatedAt,\n  });\n  const timeOfDay = snapTimeOfDayToNearestCheckpoint(resolvedTime.timeOfDay);\n  const tunedOverrides =\n    TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES[current.conditionCode];\n  const checkpointOverrides = tunedOverrides?.[getNearestCheckpoint(timeOfDay)];\n  const glassParams =\n    checkpointOverrides && \"glass\" in checkpointOverrides\n      ? checkpointOverrides.glass\n      : undefined;\n  const brightness = getSceneBrightnessFromTimeOfDay(\n    timeOfDay,\n    current.conditionCode,\n  );\n  const weatherTheme = getWeatherTheme(brightness);\n  const isWeatherDark = weatherTheme === \"dark\";\n  const backgroundClass = isWeatherDark\n    ? \"bg-gradient-to-b from-zinc-950 via-zinc-900/70 to-zinc-950\"\n    : \"bg-gradient-to-b from-sky-50 via-sky-100/70 to-white\";\n\n  return (\n    <article\n      data-slot=\"weather-widget\"\n      data-tool-ui-id={id}\n      className={cn(\"isolate w-full max-w-md\", className)}\n    >\n      <Card\n        className={cn(\n          \"@container/weather relative aspect-[4/3] overflow-clip rounded-2xl border-0 p-0 shadow-none [container-type:size]\",\n          backgroundClass,\n        )}\n      >\n        {effectsEnabled && (\n          <EffectCompositorRuntime\n            className=\"absolute inset-0\"\n            conditionCode={current.conditionCode}\n            windSpeed={current.windSpeed}\n            precipitationLevel={current.precipitationLevel}\n            visibility={current.visibility}\n            timestamp={updatedAt}\n            timeOfDay={timeOfDay}\n            settings={effects}\n          />\n        )}\n\n        <WeatherDataOverlay\n          location={location.name}\n          conditionCode={current.conditionCode}\n          temperature={current.temperature}\n          tempHigh={current.tempMax}\n          tempLow={current.tempMin}\n          forecast={forecast}\n          unit={units.temperature}\n          theme={weatherTheme}\n          timeOfDay={timeOfDay}\n          timestamp={updatedAt}\n          glassParams={glassParams}\n          reducedMotion={reducedMotion}\n        />\n      </Card>\n    </article>\n  );\n}\n"
  },
  {
    "path": "apps/www/lib/weather-authoring/weather-widget/weather-widget.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\nimport { cn, Card } from \"./_adapter\";\nimport { EffectCompositor } from \"./effects/effect-compositor\";\nimport {\n  getSceneBrightnessFromTimeOfDay,\n  getWeatherTheme,\n} from \"./effects/parameter-mapper\";\nimport { getNearestCheckpoint } from \"./effects/tuning\";\nimport { TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES } from \"./effects/generated/tuned-presets.generated\";\nimport type { WeatherWidgetProps } from \"./schema\";\nimport { resolveWeatherTime, snapTimeOfDayToNearestCheckpoint } from \"./time\";\nimport { WeatherDataOverlay } from \"./weather-data-overlay\";\n\nexport function WeatherWidget({\n  version: _version,\n  id,\n  location,\n  units,\n  current,\n  forecast,\n  time,\n  updatedAt,\n  className,\n  effects,\n  customEffectProps,\n}: WeatherWidgetProps) {\n  const [prefersReducedMotion, setPrefersReducedMotion] = useState(() => {\n    if (typeof window === \"undefined\") {\n      return false;\n    }\n\n    return (\n      window.matchMedia?.(\"(prefers-reduced-motion: reduce)\")?.matches ?? false\n    );\n  });\n\n  useEffect(() => {\n    if (\n      typeof window === \"undefined\" ||\n      typeof window.matchMedia !== \"function\"\n    ) {\n      return;\n    }\n\n    const mediaQueryList = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\",\n    );\n    setPrefersReducedMotion(mediaQueryList.matches);\n\n    const handleMotionPreferenceChange = (event: MediaQueryListEvent) => {\n      setPrefersReducedMotion(event.matches);\n    };\n\n    if (typeof mediaQueryList.addEventListener === \"function\") {\n      mediaQueryList.addEventListener(\"change\", handleMotionPreferenceChange);\n      return () => {\n        mediaQueryList.removeEventListener(\n          \"change\",\n          handleMotionPreferenceChange,\n        );\n      };\n    }\n\n    mediaQueryList.addListener(handleMotionPreferenceChange);\n    return () => {\n      mediaQueryList.removeListener(handleMotionPreferenceChange);\n    };\n  }, []);\n\n  const reducedMotion = effects?.reducedMotion ?? prefersReducedMotion;\n  const effectsEnabled = effects?.enabled !== false && !reducedMotion;\n\n  const overlayTimeOfDay = customEffectProps?.celestial?.timeOfDay;\n  const resolvedTime = resolveWeatherTime({\n    time,\n    updatedAt,\n  });\n  const timeOfDay =\n    typeof overlayTimeOfDay === \"number\"\n      ? overlayTimeOfDay\n      : snapTimeOfDayToNearestCheckpoint(resolvedTime.timeOfDay);\n\n  const tunedOverrides =\n    TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES[current.conditionCode];\n  const tunedGlass = tunedOverrides?.[getNearestCheckpoint(timeOfDay)]?.glass;\n  const glassParams = customEffectProps?.glass\n    ? { ...tunedGlass, ...customEffectProps.glass }\n    : tunedGlass;\n  const brightness = getSceneBrightnessFromTimeOfDay(\n    timeOfDay,\n    current.conditionCode,\n  );\n  const weatherTheme = getWeatherTheme(brightness);\n  const isWeatherDark = weatherTheme === \"dark\";\n  const backgroundClass = isWeatherDark\n    ? \"bg-gradient-to-b from-zinc-950 via-zinc-900/70 to-zinc-950\"\n    : \"bg-gradient-to-b from-sky-50 via-sky-100/70 to-white\";\n\n  return (\n    <article\n      data-slot=\"weather-widget\"\n      data-tool-ui-id={id}\n      className={cn(\"isolate w-full max-w-md\", className)}\n    >\n      <Card\n        className={cn(\n          \"@container/weather relative aspect-[4/3] overflow-clip rounded-2xl border-0 p-0 shadow-none [container-type:size]\",\n          backgroundClass,\n        )}\n      >\n        {effectsEnabled && (\n          <EffectCompositor\n            conditionCode={current.conditionCode}\n            windSpeed={current.windSpeed}\n            precipitationLevel={current.precipitationLevel}\n            visibility={current.visibility}\n            timestamp={updatedAt}\n            timeOfDay={timeOfDay}\n            settings={effects}\n            customProps={customEffectProps}\n          />\n        )}\n\n        <WeatherDataOverlay\n          location={location.name}\n          conditionCode={current.conditionCode}\n          temperature={current.temperature}\n          tempHigh={current.tempMax}\n          tempLow={current.tempMin}\n          forecast={forecast}\n          unit={units.temperature}\n          theme={weatherTheme}\n          timeOfDay={timeOfDay}\n          timestamp={updatedAt}\n          glassParams={glassParams}\n          reducedMotion={reducedMotion}\n        />\n      </Card>\n    </article>\n  );\n}\n"
  },
  {
    "path": "apps/www/lib/weather-codegen/compile-weather-runtime.ts",
    "content": "import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { Plugin } from \"esbuild\";\nimport ts from \"typescript\";\n\nexport interface WeatherRuntimeArtifact {\n  relativePath: string;\n  contents: string;\n}\n\nexport interface WeatherRuntimeWriteResult {\n  written: string[];\n  unchanged: string[];\n}\n\nconst PRESET_AUTHORING_RELATIVE =\n  \"lib/weather-authoring/presets/tuned-presets.json\";\nconst SHADER_AUTHORING_DIR_RELATIVE = \"lib/weather-authoring/shaders\";\nconst RUNTIME_AUTHORING_RELATIVE =\n  \"lib/weather-authoring/runtime/glass-panel-svg.tsx\";\nconst OVERLAY_AUTHORING_RELATIVE =\n  \"lib/weather-authoring/weather-widget/weather-data-overlay.tsx\";\nconst BUNDLE_ENTRY_AUTHORING_RELATIVE =\n  \"lib/weather-authoring/weather-widget/weather-runtime-core.ts\";\nconst MOON_TEXTURE_AUTHORING_RELATIVE =\n  \"lib/weather-authoring/weather-widget/assets/moon-texture.jpg\";\ninterface RuntimeModuleSpec {\n  sourceRelativePath: string;\n  outputRelativePath: string;\n  rewrites?: Record<string, string>;\n}\n\nconst PRESET_OUTPUT_RELATIVE =\n  \"lib/weather-authoring/weather-widget/effects/generated/tuned-presets.generated.ts\";\nconst SHADER_OUTPUT_RELATIVE =\n  \"lib/weather-authoring/weather-widget/effects/generated/weather-effect-shaders.generated.ts\";\nconst RUNTIME_OUTPUT_RELATIVE =\n  \"lib/weather-authoring/weather-widget/effects/generated/glass-panel-svg.generated.tsx\";\nconst BUNDLED_RUNTIME_OUTPUT_RELATIVE =\n  \"components/tool-ui/weather-widget/generated/weather-runtime-core.generated.ts\";\nconst BUNDLED_AUTHORING_WATCH_DIRS = [\n  \"lib/weather-authoring/weather-widget\",\n  \"lib/weather-authoring/weather-widget/effects\",\n  \"lib/weather-authoring/weather-widget/assets\",\n] as const;\nconst RUNTIME_MODULE_SPECS: RuntimeModuleSpec[] = [\n  {\n    sourceRelativePath: OVERLAY_AUTHORING_RELATIVE,\n    outputRelativePath:\n      \"lib/weather-authoring/weather-widget/weather-data-overlay.generated.ts\",\n    rewrites: {\n      \"./effects/glass-panel-svg\": \"./effects/use-glass-styles\",\n    },\n  },\n];\n\ninterface ShaderSpec {\n  exportName: string;\n  fileName: string;\n}\n\nconst WEATHER_SHADER_SPECS: ShaderSpec[] = [\n  { exportName: \"FULLSCREEN_VERTEX\", fileName: \"fullscreen.vert.glsl\" },\n  { exportName: \"CELESTIAL_FRAGMENT\", fileName: \"celestial.frag.glsl\" },\n  { exportName: \"CLOUD_FRAGMENT\", fileName: \"cloud.frag.glsl\" },\n  { exportName: \"RAIN_FRAGMENT\", fileName: \"rain.frag.glsl\" },\n  { exportName: \"LIGHTNING_FRAGMENT\", fileName: \"lightning.frag.glsl\" },\n  { exportName: \"SNOW_FRAGMENT\", fileName: \"snow.frag.glsl\" },\n  { exportName: \"COMPOSITE_FRAGMENT\", fileName: \"composite.frag.glsl\" },\n];\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n  return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction normalizeNumber(value: number): number {\n  if (!Number.isFinite(value)) {\n    throw new Error(`Invalid numeric value in presets: ${String(value)}`);\n  }\n\n  return Number(value.toFixed(4));\n}\n\nexport function canonicalizeWeatherPresetData(value: unknown): unknown {\n  if (typeof value === \"number\") {\n    return normalizeNumber(value);\n  }\n\n  if (Array.isArray(value)) {\n    return value.map((entry) => canonicalizeWeatherPresetData(entry));\n  }\n\n  if (isPlainObject(value)) {\n    const sortedKeys = Object.keys(value).sort((a, b) =>\n      a.localeCompare(b, \"en\"),\n    );\n    const out: Record<string, unknown> = {};\n\n    for (const key of sortedKeys) {\n      out[key] = canonicalizeWeatherPresetData(value[key]);\n    }\n\n    return out;\n  }\n\n  return value;\n}\n\nfunction serializeJsLiteral(value: unknown): string {\n  if (value === null) return \"null\";\n\n  if (typeof value === \"number\") {\n    const normalized = normalizeNumber(value);\n    const fixed = normalized.toFixed(4);\n    return fixed.replace(/(?:\\.0+|(\\.\\d*?[1-9])0+)$/, \"$1\");\n  }\n\n  if (typeof value === \"string\") {\n    return JSON.stringify(value);\n  }\n\n  if (typeof value === \"boolean\") {\n    return value ? \"true\" : \"false\";\n  }\n\n  if (Array.isArray(value)) {\n    return `[${value.map((entry) => serializeJsLiteral(entry)).join(\",\")}]`;\n  }\n\n  if (isPlainObject(value)) {\n    return `{${Object.entries(value)\n      .map(\n        ([key, entryValue]) =>\n          `${JSON.stringify(key)}:${serializeJsLiteral(entryValue)}`,\n      )\n      .join(\",\")}}`;\n  }\n\n  throw new Error(`Unsupported preset value type: ${typeof value}`);\n}\n\nfunction stripShaderComments(source: string): string {\n  let output = \"\";\n  let i = 0;\n  let inBlockComment = false;\n  let inLineComment = false;\n\n  while (i < source.length) {\n    const char = source[i];\n    const next = source[i + 1];\n\n    if (inBlockComment) {\n      if (char === \"*\" && next === \"/\") {\n        inBlockComment = false;\n        i += 2;\n        continue;\n      }\n\n      i += 1;\n      continue;\n    }\n\n    if (inLineComment) {\n      if (char === \"\\n\") {\n        inLineComment = false;\n        output += \"\\n\";\n      }\n\n      i += 1;\n      continue;\n    }\n\n    if (char === \"/\" && next === \"*\") {\n      inBlockComment = true;\n      i += 2;\n      continue;\n    }\n\n    if (char === \"/\" && next === \"/\") {\n      inLineComment = true;\n      i += 2;\n      continue;\n    }\n\n    output += char;\n    i += 1;\n  }\n\n  return output;\n}\n\nexport function minifyWeatherShaderSource(source: string): string {\n  const normalized = stripShaderComments(source.replace(/\\r\\n/g, \"\\n\"));\n\n  const minifiedLines = normalized\n    .split(\"\\n\")\n    .map((line) => line.trim())\n    .filter((line) => line.length > 0)\n    .map((line) => {\n      if (line.startsWith(\"#\")) {\n        return line;\n      }\n\n      return line\n        .replace(/\\s+/g, \" \")\n        .replace(/\\s*([{}()[\\];,+\\-*/%=<>!?:|&])\\s*/g, \"$1\");\n    });\n\n  let output = \"\";\n\n  for (const line of minifiedLines) {\n    if (line.startsWith(\"#\")) {\n      if (output.length > 0 && !output.endsWith(\"\\n\")) {\n        output += \"\\n\";\n      }\n\n      output += `${line}\\n`;\n      continue;\n    }\n\n    output += line;\n  }\n\n  return output.trimEnd();\n}\n\nfunction escapeRegExp(value: string): string {\n  return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nfunction rewriteModuleSpecifiers(\n  source: string,\n  rewrites?: Record<string, string>,\n): string {\n  if (!rewrites || Object.keys(rewrites).length === 0) {\n    return source;\n  }\n\n  let rewritten = source;\n  for (const [from, to] of Object.entries(rewrites)) {\n    const quotedSpecifier = new RegExp(`([\"'])${escapeRegExp(from)}\\\\1`, \"g\");\n    rewritten = rewritten.replace(quotedSpecifier, (_match, quote: string) => {\n      return `${quote}${to}${quote}`;\n    });\n  }\n\n  return rewritten;\n}\n\nfunction normalizeGeneratedModuleText(source: string): string {\n  return source\n    .replace(/\\r\\n/g, \"\\n\")\n    .replace(/\\n{3,}/g, \"\\n\\n\")\n    .trimEnd();\n}\n\nfunction toDataUrl(mimeType: string, binary: Buffer): string {\n  return `data:${mimeType};base64,${binary.toString(\"base64\")}`;\n}\n\nfunction inlineMoonTextureAssetUrl(\n  projectRoot: string,\n  bundledRuntime: string,\n): string {\n  const moonTexturePath = path.join(\n    projectRoot,\n    MOON_TEXTURE_AUTHORING_RELATIVE,\n  );\n  const moonTextureDataUrl = toDataUrl(\n    \"image/jpeg\",\n    readFileSync(moonTexturePath),\n  );\n  const moonTextureUrlExpression =\n    /new URL\\(\\s*[\"']\\.\\.\\/assets\\/moon-texture\\.jpg[\"']\\s*,\\s*import\\.meta\\.url\\s*\\)\\.toString\\(\\)/g;\n\n  return bundledRuntime.replace(\n    moonTextureUrlExpression,\n    JSON.stringify(moonTextureDataUrl),\n  );\n}\n\nexport function loadWeatherAuthoringPreset(projectRoot: string): unknown {\n  const presetPath = path.join(projectRoot, PRESET_AUTHORING_RELATIVE);\n  const raw = readFileSync(presetPath, \"utf8\");\n  return JSON.parse(raw) as unknown;\n}\n\nexport function loadWeatherAuthoringShaders(\n  projectRoot: string,\n): Record<string, string> {\n  const shaders: Record<string, string> = {};\n\n  for (const spec of WEATHER_SHADER_SPECS) {\n    const shaderPath = path.join(\n      projectRoot,\n      SHADER_AUTHORING_DIR_RELATIVE,\n      spec.fileName,\n    );\n\n    shaders[spec.exportName] = readFileSync(shaderPath, \"utf8\");\n  }\n\n  return shaders;\n}\n\nexport function compileWeatherPresetModule(authoringPreset: unknown): string {\n  const canonical = canonicalizeWeatherPresetData(authoringPreset);\n\n  return [\n    \"// AUTO-GENERATED by `pnpm weather:compile`.\",\n    \"// Source: lib/weather-authoring/presets/tuned-presets.json\",\n    \"// DO NOT EDIT MANUALLY.\",\n    \"\",\n    'import type { WeatherConditionCode } from \"../../schema-runtime\";',\n    'import type { WeatherEffectsCheckpointOverrides } from \"../tuning\";',\n    \"\",\n    \"export const TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES: Partial<\",\n    \"  Record<WeatherConditionCode, WeatherEffectsCheckpointOverrides>\",\n    `> = ${serializeJsLiteral(canonical)};`,\n    \"\",\n  ].join(\"\\n\");\n}\n\nexport function compileWeatherShaderModule(\n  authoringShaders: Record<string, string>,\n): string {\n  const lines = [\n    \"// AUTO-GENERATED by `pnpm weather:compile`.\",\n    \"// Source: lib/weather-authoring/shaders/*.glsl\",\n    \"// DO NOT EDIT MANUALLY.\",\n    \"\",\n  ];\n\n  for (const spec of WEATHER_SHADER_SPECS) {\n    const source = authoringShaders[spec.exportName];\n    if (typeof source !== \"string\") {\n      throw new Error(`Missing shader source for ${spec.exportName}`);\n    }\n\n    const minified = minifyWeatherShaderSource(source);\n    lines.push(\n      `export const ${spec.exportName} = ${JSON.stringify(minified)};`,\n      \"\",\n    );\n  }\n\n  return lines.join(\"\\n\");\n}\n\nexport function compileWeatherRuntimeModule(authoringSource: string): string {\n  const sourceFile = ts.createSourceFile(\n    \"glass-panel-svg.tsx\",\n    authoringSource,\n    ts.ScriptTarget.Latest,\n    true,\n    ts.ScriptKind.TSX,\n  );\n  const printer = ts.createPrinter({\n    removeComments: true,\n    newLine: ts.NewLineKind.LineFeed,\n  });\n  const printed = printer\n    .printFile(sourceFile)\n    .replace(/\\r\\n/g, \"\\n\")\n    .replace(/\\n{3,}/g, \"\\n\\n\")\n    .trimEnd();\n\n  return [\n    \"// AUTO-GENERATED by `pnpm weather:compile`.\",\n    \"// Source: lib/weather-authoring/runtime/glass-panel-svg.tsx\",\n    \"// DO NOT EDIT MANUALLY.\",\n    \"\",\n    printed,\n    \"\",\n  ].join(\"\\n\");\n}\n\nexport function compileWeatherRuntimeJsModule(\n  source: string,\n  options: {\n    sourceRelativePath: string;\n    rewrites?: Record<string, string>;\n  },\n): string {\n  const rewritten = rewriteModuleSpecifiers(source, options.rewrites);\n  const output = ts.transpileModule(rewritten, {\n    compilerOptions: {\n      target: ts.ScriptTarget.ES2020,\n      module: ts.ModuleKind.ESNext,\n      jsx: ts.JsxEmit.ReactJSX,\n      removeComments: true,\n      newLine: ts.NewLineKind.LineFeed,\n    },\n    fileName: options.sourceRelativePath,\n    reportDiagnostics: true,\n  });\n\n  if (output.diagnostics && output.diagnostics.length > 0) {\n    const diagnostics = ts.formatDiagnosticsWithColorAndContext(\n      output.diagnostics,\n      {\n        getCanonicalFileName: (fileName) => fileName,\n        getCurrentDirectory: () => process.cwd(),\n        getNewLine: () => \"\\n\",\n      },\n    );\n    throw new Error(\n      `Failed to compile weather runtime module ${options.sourceRelativePath}:\\n${diagnostics}`,\n    );\n  }\n\n  const transformed = normalizeGeneratedModuleText(output.outputText);\n  return [\n    \"// @ts-nocheck\",\n    \"// AUTO-GENERATED by `pnpm weather:compile`.\",\n    `// Source: ${options.sourceRelativePath}`,\n    \"// DO NOT EDIT MANUALLY.\",\n    \"\",\n    transformed,\n    \"\",\n  ].join(\"\\n\");\n}\n\nfunction toAbsoluteGeneratedModuleMap(\n  projectRoot: string,\n  artifacts: WeatherRuntimeArtifact[],\n): Map<string, string> {\n  const generatedMap = new Map<string, string>();\n\n  for (const artifact of artifacts) {\n    generatedMap.set(\n      path.join(projectRoot, artifact.relativePath),\n      artifact.contents,\n    );\n  }\n\n  return generatedMap;\n}\n\nfunction detectLoader(filePath: string): \"ts\" | \"tsx\" | \"js\" | \"jsx\" {\n  const ext = path.extname(filePath).toLowerCase();\n  if (ext === \".ts\") return \"ts\";\n  if (ext === \".tsx\") return \"tsx\";\n  if (ext === \".jsx\") return \"jsx\";\n  return \"js\";\n}\n\nfunction resolveGeneratedSpecifier(\n  projectRoot: string,\n  resolveDir: string,\n  specifier: string,\n  generatedModules: Map<string, string>,\n): string | null {\n  const absoluteBase = specifier.startsWith(\"@/\")\n    ? path.join(projectRoot, specifier.slice(2))\n    : path.resolve(resolveDir, specifier);\n  const candidates = [\n    absoluteBase,\n    `${absoluteBase}.ts`,\n    `${absoluteBase}.tsx`,\n    `${absoluteBase}.js`,\n    `${absoluteBase}.jsx`,\n    path.join(absoluteBase, \"index.ts\"),\n    path.join(absoluteBase, \"index.tsx\"),\n    path.join(absoluteBase, \"index.js\"),\n    path.join(absoluteBase, \"index.jsx\"),\n  ];\n\n  for (const candidate of candidates) {\n    if (generatedModules.has(candidate)) {\n      return candidate;\n    }\n  }\n\n  return null;\n}\n\nfunction createGeneratedModulePlugin(\n  projectRoot: string,\n  generatedModules: Map<string, string>,\n): Plugin {\n  return {\n    name: \"weather-generated-modules\",\n    setup(build) {\n      build.onResolve({ filter: /.*/ }, (args) => {\n        if (!args.path.startsWith(\".\") && !args.path.startsWith(\"@/\")) {\n          return null;\n        }\n\n        const resolved = resolveGeneratedSpecifier(\n          projectRoot,\n          args.resolveDir,\n          args.path,\n          generatedModules,\n        );\n        if (!resolved) {\n          return null;\n        }\n\n        return {\n          path: resolved,\n          namespace: \"weather-generated\",\n        };\n      });\n\n      build.onLoad({ filter: /.*/, namespace: \"weather-generated\" }, (args) => {\n        const contents = generatedModules.get(args.path);\n        if (contents === undefined) {\n          return null;\n        }\n\n        return {\n          contents,\n          loader: detectLoader(args.path),\n          resolveDir: path.dirname(args.path),\n        };\n      });\n    },\n  };\n}\n\nasync function compileWeatherBundledRuntimeModule(\n  projectRoot: string,\n  generatedArtifacts: WeatherRuntimeArtifact[],\n): Promise<string> {\n  const { build } = await import(\"esbuild\");\n  const generatedMap = toAbsoluteGeneratedModuleMap(\n    projectRoot,\n    generatedArtifacts,\n  );\n  const entryPath = path.join(projectRoot, BUNDLE_ENTRY_AUTHORING_RELATIVE);\n\n  const buildResult = await build({\n    entryPoints: [entryPath],\n    bundle: true,\n    write: false,\n    format: \"esm\",\n    platform: \"browser\",\n    target: [\"es2020\"],\n    jsx: \"automatic\",\n    minify: true,\n    logLevel: \"silent\",\n    sourcemap: false,\n    external: [\n      \"react\",\n      \"react/jsx-runtime\",\n      \"lucide-react\",\n      \"@/components/ui/*\",\n      \"@/lib/utils\",\n    ],\n    loader: {\n      \".jpg\": \"dataurl\",\n    },\n    plugins: [createGeneratedModulePlugin(projectRoot, generatedMap)],\n  });\n\n  const runtimeFile =\n    buildResult.outputFiles.find((file) => file.path.endsWith(\".js\")) ??\n    buildResult.outputFiles[0];\n\n  if (!runtimeFile) {\n    throw new Error(\n      \"Weather runtime bundle generation produced no JavaScript output.\",\n    );\n  }\n\n  const bundleText = normalizeGeneratedModuleText(runtimeFile.text);\n  const normalizedBundleText = inlineMoonTextureAssetUrl(\n    projectRoot,\n    bundleText,\n  );\n  return [\n    \"// @ts-nocheck\",\n    \"// AUTO-GENERATED by `pnpm weather:compile`.\",\n    `// Source: ${BUNDLE_ENTRY_AUTHORING_RELATIVE}`,\n    \"// DO NOT EDIT MANUALLY.\",\n    \"\",\n    normalizedBundleText,\n    \"\",\n  ].join(\"\\n\");\n}\n\nexport async function buildWeatherRuntimeArtifacts(\n  projectRoot: string,\n): Promise<WeatherRuntimeArtifact[]> {\n  const authoringPreset = loadWeatherAuthoringPreset(projectRoot);\n  const authoringShaders = loadWeatherAuthoringShaders(projectRoot);\n  const authoringRuntime = readFileSync(\n    path.join(projectRoot, RUNTIME_AUTHORING_RELATIVE),\n    \"utf8\",\n  );\n  const runtimeModules = RUNTIME_MODULE_SPECS.map((spec) => {\n    const sourcePath = path.join(projectRoot, spec.sourceRelativePath);\n    const source = readFileSync(sourcePath, \"utf8\");\n    return {\n      relativePath: spec.outputRelativePath,\n      contents: compileWeatherRuntimeJsModule(source, {\n        sourceRelativePath: spec.sourceRelativePath,\n        rewrites: spec.rewrites,\n      }),\n    } as WeatherRuntimeArtifact;\n  });\n\n  const nonBundledArtifacts: WeatherRuntimeArtifact[] = [\n    {\n      relativePath: PRESET_OUTPUT_RELATIVE,\n      contents: compileWeatherPresetModule(authoringPreset),\n    },\n    {\n      relativePath: SHADER_OUTPUT_RELATIVE,\n      contents: compileWeatherShaderModule(authoringShaders),\n    },\n    {\n      relativePath: RUNTIME_OUTPUT_RELATIVE,\n      contents: compileWeatherRuntimeModule(authoringRuntime),\n    },\n    ...runtimeModules,\n  ];\n\n  return [\n    ...nonBundledArtifacts,\n    {\n      relativePath: BUNDLED_RUNTIME_OUTPUT_RELATIVE,\n      contents: await compileWeatherBundledRuntimeModule(\n        projectRoot,\n        nonBundledArtifacts,\n      ),\n    },\n  ];\n}\n\nexport async function writeWeatherRuntimeArtifacts(\n  projectRoot: string,\n): Promise<WeatherRuntimeWriteResult> {\n  const artifacts = await buildWeatherRuntimeArtifacts(projectRoot);\n  const written: string[] = [];\n  const unchanged: string[] = [];\n\n  for (const artifact of artifacts) {\n    const outputPath = path.join(projectRoot, artifact.relativePath);\n    const outputDir = path.dirname(outputPath);\n\n    if (!existsSync(outputDir)) {\n      mkdirSync(outputDir, { recursive: true });\n    }\n\n    const current = existsSync(outputPath)\n      ? readFileSync(outputPath, \"utf8\")\n      : null;\n\n    if (current === artifact.contents) {\n      unchanged.push(artifact.relativePath);\n      continue;\n    }\n\n    writeFileSync(outputPath, artifact.contents, \"utf8\");\n    written.push(artifact.relativePath);\n  }\n\n  return { written, unchanged };\n}\n\nexport async function getStaleWeatherRuntimeArtifacts(\n  projectRoot: string,\n): Promise<string[]> {\n  const artifacts = await buildWeatherRuntimeArtifacts(projectRoot);\n  const stale: string[] = [];\n\n  for (const artifact of artifacts) {\n    const outputPath = path.join(projectRoot, artifact.relativePath);\n    const current = existsSync(outputPath)\n      ? readFileSync(outputPath, \"utf8\")\n      : null;\n\n    if (current !== artifact.contents) {\n      stale.push(artifact.relativePath);\n    }\n  }\n\n  return stale;\n}\n\nexport const WEATHER_AUTHORING_WATCH_DIRS = [\n  PRESET_AUTHORING_RELATIVE.replace(/\\/[^/]+$/, \"\"),\n  SHADER_AUTHORING_DIR_RELATIVE,\n  RUNTIME_AUTHORING_RELATIVE.replace(/\\/[^/]+$/, \"\"),\n  ...BUNDLED_AUTHORING_WATCH_DIRS,\n  ...Array.from(\n    new Set(\n      RUNTIME_MODULE_SPECS.map((spec) =>\n        spec.sourceRelativePath.replace(/\\/[^/]+$/, \"\"),\n      ),\n    ),\n  ),\n] as const;\n"
  },
  {
    "path": "apps/www/mdx-components.tsx",
    "content": "import type { MDXComponents } from \"mdx/types\";\nimport defaultMdxComponents from \"fumadocs-ui/mdx\";\nimport { Steps, Step } from \"fumadocs-ui/components/steps\";\nimport {\n  Tabs,\n  TabsList,\n  TabsTrigger,\n  TabsContent,\n  Tab,\n} from \"fumadocs-ui/components/tabs\";\nimport { TypeTable } from \"fumadocs-ui/components/type-table\";\nimport { Files, File, Folder } from \"fumadocs-ui/components/files\";\nimport * as React from \"react\";\nimport dynamic from \"next/dynamic\";\nimport { AutoLinkChildren, withAutoLink } from \"@/lib/docs/auto-link\";\nimport { TrackedDynamicCodeBlock } from \"@/app/docs/_components/tracked-dynamic-codeblock\";\nimport { InstallCommandBlock } from \"@/app/docs/_components/install-command-block\";\nimport { FeatureGrid, Feature } from \"@/app/components/mdx/features\";\n\nconst Mermaid = dynamic(() =>\n  import(\"@/app/components/mdx/mermaid\").then((m) => m.Mermaid),\n);\nconst ApprovalCardPresetExample = dynamic(() =>\n  import(\"@/app/docs/_components/preset-example\").then(\n    (m) => m.ApprovalCardPresetExample,\n  ),\n);\nconst ChartPresetExample = dynamic(() =>\n  import(\"@/app/docs/_components/preset-example\").then(\n    (m) => m.ChartPresetExample,\n  ),\n);\nconst OptionListPresetExample = dynamic(() =>\n  import(\"@/app/docs/_components/preset-example\").then(\n    (m) => m.OptionListPresetExample,\n  ),\n);\nconst CodeBlockPresetExample = dynamic(() =>\n  import(\"@/app/docs/_components/preset-example\").then(\n    (m) => m.CodeBlockPresetExample,\n  ),\n);\nconst TerminalPresetExample = dynamic(() =>\n  import(\"@/app/docs/_components/preset-example\").then(\n    (m) => m.TerminalPresetExample,\n  ),\n);\nconst PlanPresetExample = dynamic(() =>\n  import(\"@/app/docs/_components/preset-example\").then(\n    (m) => m.PlanPresetExample,\n  ),\n);\nconst ItemCarouselPresetExample = dynamic(() =>\n  import(\"@/app/docs/_components/preset-example\").then(\n    (m) => m.ItemCarouselPresetExample,\n  ),\n);\nconst QuestionFlowPresetExample = dynamic(() =>\n  import(\"@/app/docs/_components/preset-example\").then(\n    (m) => m.QuestionFlowPresetExample,\n  ),\n);\n\nexport function useMDXComponents(components: MDXComponents): MDXComponents {\n  // Wrap selected default components to auto-link Tool UI mentions\n  const Base = defaultMdxComponents as MDXComponents;\n  const P = withAutoLink((Base as Record<string, React.ElementType>).p ?? \"p\");\n  const LI = withAutoLink(\n    (Base as Record<string, React.ElementType>).li ?? \"li\",\n  );\n  const Blockquote = withAutoLink(\n    (Base as Record<string, React.ElementType>).blockquote ?? \"blockquote\",\n  );\n  const TD = withAutoLink(\n    (Base as Record<string, React.ElementType>).td ?? \"td\",\n  );\n  const TH = withAutoLink(\n    (Base as Record<string, React.ElementType>).th ?? \"th\",\n  );\n\n  return {\n    ...defaultMdxComponents,\n    ...components,\n\n    // Auto-link text mentions in common containers\n    p: P,\n    li: LI,\n    blockquote: Blockquote,\n    td: TD,\n    th: TH,\n\n    // Override `pre` to perform client-side Shiki highlighting using Fumadocs dynamic code block\n    pre: (props: React.ComponentPropsWithoutRef<\"pre\">) => {\n      const child = props?.children;\n      if (React.isValidElement(child)) {\n        type CodeChildProps = {\n          className?: string;\n          children?: React.ReactNode;\n        };\n        const element = child as React.ReactElement<CodeChildProps>;\n        const raw = element.props.children;\n        const content =\n          typeof raw === \"string\" ? raw : React.Children.toArray(raw).join(\"\");\n        const code = String(content).replace(/\\n+$/, \"\");\n        const className = element.props.className ?? \"\";\n        const match = /language-([\\w-]+)/.exec(className);\n        const lang = match?.[1] ?? \"txt\";\n\n        const attrs: Record<string, number | boolean> = {\n          \"data-line-numbers\": true,\n          \"data-line-numbers-start\": 1,\n        };\n\n        return (\n          <TrackedDynamicCodeBlock lang={lang} code={code} codeblock={attrs} />\n        );\n      }\n\n      return React.createElement(\"pre\", props);\n    },\n\n    Steps,\n    Step,\n    Tabs,\n    TabsList,\n    TabsTrigger,\n    TabsContent,\n    Tab,\n    TypeTable,\n    Files,\n    File,\n    Folder,\n    AutoLinkChildren,\n    Mermaid,\n    ApprovalCardPresetExample,\n    ChartPresetExample,\n    OptionListPresetExample,\n    CodeBlockPresetExample,\n    TerminalPresetExample,\n    PlanPresetExample,\n    ItemCarouselPresetExample,\n    QuestionFlowPresetExample,\n    FeatureGrid,\n    Feature,\n    InstallCommandBlock,\n  };\n}\n"
  },
  {
    "path": "apps/www/next.config.ts",
    "content": "import { NextConfig } from \"next\";\nimport createMDX from \"@next/mdx\";\n\nconst isDev = process.env.NODE_ENV === \"development\";\n\nconst cspHeader = `\n    default-src 'self';\n    connect-src *;\n    script-src 'self' 'unsafe-inline'${isDev ? \" 'unsafe-eval'\" : \"\"};\n    style-src 'self' 'unsafe-inline';\n    img-src * blob: data:;\n    font-src 'self';\n    object-src 'none';\n    base-uri 'self';\n    form-action 'self';\n    frame-ancestors 'none';\n    upgrade-insecure-requests;\n`;\n\nconst config: NextConfig = {\n  reactStrictMode: true,\n  pageExtensions: [\"js\", \"jsx\", \"md\", \"mdx\", \"ts\", \"tsx\"],\n  transpilePackages: [\"@pierre/diffs\"],\n  experimental: {\n    optimizePackageImports: [\n      \"@radix-ui/react-accordion\",\n      \"@radix-ui/react-alert-dialog\",\n      \"@radix-ui/react-checkbox\",\n      \"@radix-ui/react-collapsible\",\n      \"@radix-ui/react-dialog\",\n      \"@radix-ui/react-dropdown-menu\",\n      \"@radix-ui/react-label\",\n      \"@radix-ui/react-radio-group\",\n      \"@radix-ui/react-select\",\n      \"@radix-ui/react-separator\",\n      \"@radix-ui/react-slider\",\n      \"@radix-ui/react-slot\",\n      \"@radix-ui/react-switch\",\n      \"@radix-ui/react-tabs\",\n      \"lucide-react\",\n      \"recharts\",\n      \"react-icons\",\n      \"react-icons/fa\",\n      \"react-icons/fa6\",\n    ],\n  },\n  async redirects() {\n    return [\n      {\n        source: \"/components\",\n        destination: \"/docs/gallery\",\n        permanent: true,\n      },\n      {\n        source: \"/components/:path*\",\n        destination: \"/docs/:path*\",\n        permanent: true,\n      },\n    ];\n  },\n  async rewrites() {\n    return {\n      beforeFiles: [\n        {\n          source: \"/ph/static/:path*\",\n          destination: \"https://us-assets.i.posthog.com/static/:path*\",\n        },\n        {\n          source: \"/ph/:path*\",\n          destination: \"https://us.i.posthog.com/:path*\",\n        },\n      ],\n    };\n  },\n  async headers() {\n    return [\n      {\n        source: \"/(.*)\",\n        headers: [\n          {\n            key: \"Content-Security-Policy\",\n            value: cspHeader.replace(/\\n/g, \"\"),\n          },\n        ],\n      },\n    ];\n  },\n};\n\nconst withMDX = createMDX({\n  options: {\n    remarkPlugins: [\"remark-gfm\"],\n    rehypePlugins: [\"rehype-slug\"],\n  },\n});\n\nexport default withMDX(config);\n"
  },
  {
    "path": "apps/www/package.json",
    "content": "{\n  \"name\": \"tool-ui-www\",\n  \"version\": \"0.2.0\",\n  \"private\": true,\n  \"description\": \"Open source component library for tool call widgets\",\n  \"license\": \"MIT\",\n  \"sideEffects\": false,\n  \"scripts\": {\n    \"dev\": \"next dev --turbopack\",\n    \"build\": \"next build --experimental-build-mode=compile\",\n    \"start\": \"next start\",\n    \"component:new\": \"tsx scripts/new-tool-ui-component.ts\",\n    \"changelog:generate\": \"tsx scripts/generate-changelog.ts\",\n    \"changelog:generate:manual\": \"tsx scripts/generate-changelog.ts\",\n    \"changelog:generate:from-tag\": \"tsx scripts/generate-changelog.ts --from-tag\",\n    \"changelog:check\": \"tsx scripts/check-changelog.ts\",\n    \"registry:build\": \"tsx scripts/build-tool-ui-registry.ts\",\n    \"registry:check\": \"tsx scripts/registry-check.ts\",\n    \"weather:compile\": \"tsx scripts/compile-weather-runtime.ts\",\n    \"weather:watch\": \"tsx scripts/compile-weather-runtime.ts --watch\",\n    \"weather:check\": \"tsx scripts/compile-weather-runtime.ts --check\",\n    \"hooks:pre-commit\": \"tsx scripts/precommit-registry-sync.ts\",\n    \"hooks:pre-push\": \"pnpm lint && pnpm lint:format && pnpm registry:check && pnpm changelog:check\",\n    \"lint:oxlint\": \"oxlint\",\n    \"lint:eslint\": \"eslint .\",\n    \"lint\": \"pnpm lint:oxlint && pnpm lint:eslint\",\n    \"lint:fix\": \"oxlint --fix && eslint . --fix\",\n    \"lint:format\": \"oxfmt --check\",\n    \"typecheck\": \"tsgo --noEmit\",\n    \"test\": \"vitest run\",\n    \"test:watch\": \"vitest watch\",\n    \"check\": \"run-p typecheck lint:oxlint lint:eslint lint:format\",\n    \"format\": \"oxfmt\",\n    \"format:check\": \"oxfmt --check\"\n  },\n  \"dependencies\": {\n    \"@ai-sdk/anthropic\": \"^3.0.36\",\n    \"@ai-sdk/mcp\": \"^1.0.18\",\n    \"@ai-sdk/openai\": \"^3.0.25\",\n    \"@assistant-ui/react\": \"^0.12.3\",\n    \"@assistant-ui/react-ai-sdk\": \"^1.3.3\",\n    \"@assistant-ui/react-markdown\": \"^0.12.1\",\n    \"@codemirror/lang-json\": \"^6.0.2\",\n    \"@codemirror/lint\": \"^6.9.3\",\n    \"@codemirror/view\": \"^6.39.12\",\n    \"@mdx-js/loader\": \"^3.1.1\",\n    \"@mdx-js/react\": \"^3.1.1\",\n    \"@modelcontextprotocol/sdk\": \"^1.25.3\",\n    \"@next/mdx\": \"^16.1.6\",\n    \"@pierre/diffs\": \"^1.0.11\",\n    \"@radix-ui/react-accordion\": \"^1.2.12\",\n    \"@radix-ui/react-avatar\": \"^1.1.11\",\n    \"@radix-ui/react-collapsible\": \"^1.1.12\",\n    \"@radix-ui/react-dialog\": \"^1.1.15\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.1.16\",\n    \"@radix-ui/react-label\": \"^2.1.8\",\n    \"@radix-ui/react-popover\": \"^1.1.15\",\n    \"@radix-ui/react-radio-group\": \"^1.3.8\",\n    \"@radix-ui/react-select\": \"^2.2.6\",\n    \"@radix-ui/react-separator\": \"^1.1.8\",\n    \"@radix-ui/react-slider\": \"^1.3.6\",\n    \"@radix-ui/react-slot\": \"^1.2.4\",\n    \"@radix-ui/react-switch\": \"^1.2.6\",\n    \"@radix-ui/react-tabs\": \"^1.1.13\",\n    \"@radix-ui/react-toggle\": \"^1.1.10\",\n    \"@radix-ui/react-toggle-group\": \"^1.1.11\",\n    \"@radix-ui/react-tooltip\": \"^1.2.8\",\n    \"@react-three/drei\": \"^10.7.7\",\n    \"@react-three/fiber\": \"^9.5.0\",\n    \"@react-three/postprocessing\": \"^3.0.4\",\n    \"@silk-hq/components\": \"^0.10.0\",\n    \"@tailwindcss/typography\": \"^0.5.19\",\n    \"@types/mdx\": \"^2.0.13\",\n    \"@uiw/codemirror-theme-github\": \"^4.25.4\",\n    \"@uiw/react-codemirror\": \"^4.25.4\",\n    \"@upstash/ratelimit\": \"^2.0.8\",\n    \"@upstash/redis\": \"^1.36.2\",\n    \"@vercel/analytics\": \"^1.6.1\",\n    \"ai\": \"^6.0.69\",\n    \"ajv\": \"^8.17.1\",\n    \"ansi-to-react\": \"^6.2.6\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"freestyle-sandboxes\": \"^0.1.14\",\n    \"fumadocs-core\": \"^16.5.0\",\n    \"fumadocs-mdx\": \"^14.2.6\",\n    \"fumadocs-ui\": \"^16.5.0\",\n    \"geist\": \"^1.5.1\",\n    \"leaflet\": \"^1.9.4\",\n    \"leva\": \"^0.10.1\",\n    \"lucide-react\": \"^0.563.0\",\n    \"mermaid\": \"^11.12.2\",\n    \"motion\": \"^12.31.0\",\n    \"next\": \"16.1.6\",\n    \"next-themes\": \"^0.4.6\",\n    \"nuqs\": \"^2.8.8\",\n    \"posthog-js\": \"^1.339.1\",\n    \"postprocessing\": \"^6.38.2\",\n    \"radix-ui\": \"^1.4.3\",\n    \"react\": \"19.2.4\",\n    \"react-dom\": \"19.2.4\",\n    \"react-icons\": \"^5.5.0\",\n    \"react-leaflet\": \"^5.0.0\",\n    \"react-resizable-panels\": \"^3.0.6\",\n    \"react-shiki\": \"^0.9.1\",\n    \"recharts\": \"2.15.4\",\n    \"rehype-slug\": \"^6.0.0\",\n    \"remark-gfm\": \"^4.0.1\",\n    \"shiki\": \"^3.22.0\",\n    \"supercluster\": \"^8.0.1\",\n    \"tailwind-merge\": \"^3.4.0\",\n    \"three\": \"^0.182.0\",\n    \"tw-shimmer\": \"^0.4.4\",\n    \"zod\": \"4.3.6\",\n    \"zustand\": \"^5.0.11\"\n  },\n  \"devDependencies\": {\n    \"@nkzw/oxlint-config\": \"^1.0.1\",\n    \"@tailwindcss/postcss\": \"^4.1.18\",\n    \"@testing-library/react\": \"^16.3.2\",\n    \"@types/json-schema\": \"^7.0.15\",\n    \"@types/leaflet\": \"^1.9.21\",\n    \"@types/node\": \"^25.2.0\",\n    \"@types/react\": \"^19.2.10\",\n    \"@types/react-dom\": \"^19.2.3\",\n    \"@types/supercluster\": \"^7.1.3\",\n    \"@types/three\": \"^0.182.0\",\n    \"@typescript-eslint/eslint-plugin\": \"^8.56.0\",\n    \"@typescript-eslint/parser\": \"^8.54.0\",\n    \"@typescript/native-preview\": \"7.0.0-dev.20260222.1\",\n    \"baseline-browser-mapping\": \"^2.9.19\",\n    \"esbuild\": \"^0.27.3\",\n    \"eslint\": \"^9.39.2\",\n    \"eslint-plugin-oxlint\": \"^1.49.0\",\n    \"eslint-plugin-react-hooks\": \"^7.0.1\",\n    \"jsdom\": \"^28.0.0\",\n    \"npm-run-all2\": \"^8.0.4\",\n    \"oxfmt\": \"^0.34.0\",\n    \"oxlint\": \"^1.49.0\",\n    \"postcss\": \"^8.5.6\",\n    \"tailwindcss\": \"^4.1.18\",\n    \"tsx\": \"^4.21.0\",\n    \"tw-animate-css\": \"^1.4.0\",\n    \"typescript\": \"^5.9.3\",\n    \"vitest\": \"^4.0.18\"\n  }\n}\n"
  },
  {
    "path": "apps/www/postcss.config.mjs",
    "content": "const config = {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "apps/www/proxy.ts",
    "content": "import { NextResponse } from \"next/server\";\nimport type { NextRequest } from \"next/server\";\n\n/**\n * Proxy to block sandbox routes in production.\n * Sandbox pages are development-only tools for tuning weather effects, etc.\n */\nexport function proxy(request: NextRequest) {\n  const { pathname } = request.nextUrl;\n\n  // Block /sandbox/* routes in production\n  if (\n    process.env.NODE_ENV === \"production\" &&\n    pathname.startsWith(\"/sandbox\")\n  ) {\n    return NextResponse.rewrite(new URL(\"/404\", request.url));\n  }\n\n  return NextResponse.next();\n}\n\nexport const config = {\n  matcher: \"/sandbox/:path*\",\n};\n"
  },
  {
    "path": "apps/www/public/r/approval-card.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"approval-card\",\n  \"type\": \"registry:block\",\n  \"title\": \"Approval Card\",\n  \"description\": \"Binary confirmation for agent actions.\",\n  \"dependencies\": [\n    \"lucide-react\",\n    \"zod\"\n  ],\n  \"registryDependencies\": [\n    \"button\",\n    \"separator\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/approval-card/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/approval-card/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n */\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport { Separator } from \\\"@/components/ui/separator\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/approval-card/approval-card.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/approval-card/approval-card.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport * as React from \\\"react\\\";\\nimport { cn, Separator } from \\\"./_adapter\\\";\\nimport type { ApprovalCardProps, ApprovalDecision } from \\\"./schema\\\";\\nimport { ActionButtons } from \\\"../shared/action-buttons\\\";\\nimport { type Action } from \\\"../shared/schema\\\";\\n\\nimport { icons, Check, X } from \\\"lucide-react\\\";\\n\\ntype LucideIcon = React.ComponentType<{ className?: string }>;\\n\\nfunction getLucideIcon(name: string): LucideIcon | null {\\n  const pascalName = name\\n    .split(\\\"-\\\")\\n    .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\\n    .join(\\\"\\\");\\n\\n  const Icon = icons[pascalName as keyof typeof icons];\\n  return Icon ?? null;\\n}\\n\\ninterface ApprovalCardReceiptProps {\\n  id: string;\\n  title: string;\\n  choice: ApprovalDecision;\\n  actionLabel?: string;\\n  className?: string;\\n}\\n\\nfunction ApprovalCardReceipt({\\n  id,\\n  title,\\n  choice,\\n  actionLabel,\\n  className,\\n}: ApprovalCardReceiptProps) {\\n  const isApproved = choice === \\\"approved\\\";\\n  const displayLabel = actionLabel ?? (isApproved ? \\\"Approved\\\" : \\\"Denied\\\");\\n\\n  return (\\n    <div\\n      className={cn(\\n        \\\"flex w-full min-w-64 max-w-md flex-col\\\",\\n        \\\"text-foreground\\\",\\n        \\\"motion-safe:animate-in motion-safe:fade-in motion-safe:blur-in-sm motion-safe:zoom-in-95 motion-safe:duration-300 motion-safe:ease-[cubic-bezier(0.16,1,0.3,1)] motion-safe:fill-mode-both\\\",\\n        className,\\n      )}\\n      data-slot=\\\"approval-card\\\"\\n      data-tool-ui-id={id}\\n      data-receipt=\\\"true\\\"\\n      role=\\\"status\\\"\\n      aria-label={displayLabel}\\n    >\\n      <div\\n        className={cn(\\n          \\\"bg-card/60 flex w-full items-center gap-3 rounded-2xl border px-4 py-3 shadow-xs\\\",\\n        )}\\n      >\\n        <span\\n          className={cn(\\n            \\\"flex size-8 shrink-0 items-center justify-center rounded-full bg-muted\\\",\\n            isApproved ? \\\"text-primary\\\" : \\\"text-muted-foreground\\\",\\n          )}\\n        >\\n          {isApproved ? <Check className=\\\"size-4\\\" /> : <X className=\\\"size-4\\\" />}\\n        </span>\\n        <div className=\\\"flex flex-col\\\">\\n          <span className=\\\"text-sm font-medium\\\">{displayLabel}</span>\\n          <span className=\\\"text-muted-foreground text-sm\\\">{title}</span>\\n        </div>\\n      </div>\\n    </div>\\n  );\\n}\\n\\nexport function ApprovalCard({\\n  id,\\n  title,\\n  description,\\n  icon,\\n  metadata,\\n  variant,\\n  confirmLabel,\\n  cancelLabel,\\n  className,\\n  choice,\\n  onConfirm,\\n  onCancel,\\n}: ApprovalCardProps) {\\n  const resolvedVariant = variant ?? \\\"default\\\";\\n  const resolvedConfirmLabel = confirmLabel ?? \\\"Approve\\\";\\n  const resolvedCancelLabel = cancelLabel ?? \\\"Deny\\\";\\n  const Icon = icon ? getLucideIcon(icon) : null;\\n\\n  const handleAction = React.useCallback(\\n    async (actionId: string) => {\\n      if (actionId === \\\"confirm\\\") {\\n        await onConfirm?.();\\n      } else if (actionId === \\\"cancel\\\") {\\n        await onCancel?.();\\n      }\\n    },\\n    [onConfirm, onCancel],\\n  );\\n\\n  const handleKeyDown = React.useCallback(\\n    (event: React.KeyboardEvent) => {\\n      if (event.key === \\\"Escape\\\") {\\n        event.preventDefault();\\n        onCancel?.();\\n      }\\n    },\\n    [onCancel],\\n  );\\n\\n  const isDestructive = resolvedVariant === \\\"destructive\\\";\\n\\n  const actions: Action[] = [\\n    {\\n      id: \\\"cancel\\\",\\n      label: resolvedCancelLabel,\\n      variant: \\\"ghost\\\",\\n    },\\n    {\\n      id: \\\"confirm\\\",\\n      label: resolvedConfirmLabel,\\n      variant: isDestructive ? \\\"destructive\\\" : \\\"default\\\",\\n    },\\n  ];\\n\\n  const viewKey = choice ? `receipt-${choice}` : \\\"interactive\\\";\\n\\n  return (\\n    <div key={viewKey} className=\\\"contents\\\">\\n      {choice ? (\\n        <ApprovalCardReceipt\\n          id={id}\\n          title={title}\\n          choice={choice}\\n          className={className}\\n        />\\n      ) : (\\n        <article\\n          className={cn(\\n            \\\"flex w-full min-w-64 max-w-md flex-col gap-3\\\",\\n            \\\"text-foreground\\\",\\n            className,\\n          )}\\n          data-slot=\\\"approval-card\\\"\\n          data-tool-ui-id={id}\\n          role=\\\"dialog\\\"\\n          aria-labelledby={`${id}-title`}\\n          aria-describedby={description ? `${id}-description` : undefined}\\n          onKeyDown={handleKeyDown}\\n        >\\n          <div className=\\\"bg-card flex w-full flex-col gap-4 rounded-2xl border p-5 shadow-xs\\\">\\n            <div className=\\\"flex items-start gap-3\\\">\\n              {Icon && (\\n                <span\\n                  className={cn(\\n                    \\\"flex size-10 shrink-0 items-center justify-center rounded-xl\\\",\\n                    isDestructive\\n                      ? \\\"bg-destructive/10 text-destructive\\\"\\n                      : \\\"bg-primary/10 text-primary\\\",\\n                  )}\\n                >\\n                  <Icon className=\\\"size-5\\\" />\\n                </span>\\n              )}\\n              <div className=\\\"flex flex-1 flex-col gap-1\\\">\\n                <h2\\n                  id={`${id}-title`}\\n                  className=\\\"text-base font-semibold leading-tight\\\"\\n                >\\n                  {title}\\n                </h2>\\n                {description && (\\n                  <p\\n                    id={`${id}-description`}\\n                    className=\\\"text-muted-foreground text-sm\\\"\\n                  >\\n                    {description}\\n                  </p>\\n                )}\\n              </div>\\n            </div>\\n\\n            {metadata && metadata.length > 0 && (\\n              <>\\n                <Separator />\\n                <dl className=\\\"flex flex-col gap-2 text-sm\\\">\\n                  {metadata.map((item, index) => (\\n                    <div key={index} className=\\\"flex justify-between gap-4\\\">\\n                      <dt className=\\\"text-muted-foreground shrink-0\\\">\\n                        {item.key}\\n                      </dt>\\n                      <dd className=\\\"min-w-0 truncate\\\">{item.value}</dd>\\n                    </div>\\n                  ))}\\n                </dl>\\n              </>\\n            )}\\n          </div>\\n          <div className=\\\"@container/actions\\\">\\n            <ActionButtons actions={actions} onAction={handleAction} />\\n          </div>\\n        </article>\\n      )}\\n    </div>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/approval-card/index.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/approval-card/index.tsx\",\n      \"content\": \"export { ApprovalCard } from \\\"./approval-card\\\";\\nexport {\\n  type SerializableApprovalCard,\\n  type ApprovalCardProps,\\n  type ApprovalDecision,\\n  type MetadataItem,\\n} from \\\"./schema\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/approval-card/README.md\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/approval-card/README.md\",\n      \"content\": \"# Approval Card\\n\\nImplementation for the \\\"approval-card\\\" Tool UI surface.\\n\\n## Files\\n\\n- public exports: components/tool-ui/approval-card/index.tsx\\n- serializable schema + parse helpers: components/tool-ui/approval-card/schema.ts\\n\\n## Companion assets\\n\\n- Docs page: app/docs/approval-card/content.mdx\\n- Preset payload: lib/presets/approval-card.ts\\n\\n## Quick check\\n\\nRun this after edits:\\n\\npnpm test\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/approval-card/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/approval-card/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { ToolUIIdSchema, ToolUIRoleSchema } from \\\"../shared/schema\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\n\\nexport const MetadataItemSchema = z.object({\\n  key: z.string().min(1),\\n  value: z.string(),\\n});\\n\\nexport type MetadataItem = z.infer<typeof MetadataItemSchema>;\\n\\nexport const ApprovalDecisionSchema = z.enum([\\\"approved\\\", \\\"denied\\\"]);\\n\\nexport type ApprovalDecision = z.infer<typeof ApprovalDecisionSchema>;\\n\\nexport const SerializableApprovalCardSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n\\n  title: z.string().min(1),\\n  description: z.string().optional(),\\n  icon: z.string().optional(),\\n  metadata: z.array(MetadataItemSchema).optional(),\\n\\n  variant: z.enum([\\\"default\\\", \\\"destructive\\\"]).optional(),\\n\\n  confirmLabel: z.string().optional(),\\n  cancelLabel: z.string().optional(),\\n\\n  choice: ApprovalDecisionSchema.optional(),\\n});\\n\\nexport type SerializableApprovalCard = z.infer<\\n  typeof SerializableApprovalCardSchema\\n>;\\n\\nconst SerializableApprovalCardSchemaContract = defineToolUiContract(\\n  \\\"ApprovalCard\\\",\\n  SerializableApprovalCardSchema,\\n);\\n\\nexport const parseSerializableApprovalCard: (\\n  input: unknown,\\n) => SerializableApprovalCard = SerializableApprovalCardSchemaContract.parse;\\n\\nexport const safeParseSerializableApprovalCard: (\\n  input: unknown,\\n) => SerializableApprovalCard | null =\\n  SerializableApprovalCardSchemaContract.safeParse;\\nexport interface ApprovalCardProps extends SerializableApprovalCard {\\n  className?: string;\\n  onConfirm?: () => void | Promise<void>;\\n  onCancel?: () => void | Promise<void>;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/shared/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn     → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n *   Button → shadcn/ui Button\\n */\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport { Button } from \\\"@/components/ui/button\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/action-buttons.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/shared/action-buttons.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport type { Action } from \\\"./schema\\\";\\nimport { cn, Button } from \\\"./_adapter\\\";\\nimport { useActionButtons } from \\\"./use-action-buttons\\\";\\n\\nexport interface ActionButtonsProps {\\n  actions: Action[];\\n  onAction: (actionId: string) => void | Promise<void>;\\n  onBeforeAction?: (actionId: string) => boolean | Promise<boolean>;\\n  confirmTimeout?: number;\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  className?: string;\\n}\\n\\nexport function ActionButtons({\\n  actions,\\n  onAction,\\n  onBeforeAction,\\n  confirmTimeout = 3000,\\n  align = \\\"right\\\",\\n  className,\\n}: ActionButtonsProps) {\\n  const { actions: resolvedActions, runAction } = useActionButtons({\\n    actions,\\n    onAction,\\n    onBeforeAction,\\n    confirmTimeout,\\n  });\\n\\n  return (\\n    <div\\n      className={cn(\\n        \\\"flex flex-col gap-3\\\",\\n        \\\"@sm/actions:flex-row @sm/actions:flex-wrap @sm/actions:items-center @sm/actions:gap-2\\\",\\n        align === \\\"left\\\" && \\\"@sm/actions:justify-start\\\",\\n        align === \\\"center\\\" && \\\"@sm/actions:justify-center\\\",\\n        align === \\\"right\\\" && \\\"@sm/actions:justify-end\\\",\\n        className,\\n      )}\\n    >\\n      {resolvedActions.map((action) => {\\n        const label = action.currentLabel;\\n        const variant = action.variant || \\\"default\\\";\\n\\n        return (\\n          <Button\\n            key={action.id}\\n            variant={variant}\\n            onClick={() => runAction(action.id)}\\n            disabled={action.isDisabled}\\n            className={cn(\\n              \\\"rounded-full px-4!\\\",\\n              \\\"justify-center\\\",\\n              \\\"min-h-11 w-full text-base\\\",\\n              \\\"@sm/actions:min-h-0 @sm/actions:w-auto @sm/actions:px-3 @sm/actions:py-2 @sm/actions:text-sm\\\",\\n              action.isConfirming &&\\n                \\\"ring-destructive ring-2 ring-offset-2 motion-safe:animate-pulse\\\",\\n            )}\\n            aria-label={\\n              action.shortcut ? `${label} (${action.shortcut})` : label\\n            }\\n          >\\n            {action.isLoading && (\\n              <svg\\n                className=\\\"mr-2 h-4 w-4 motion-safe:animate-spin\\\"\\n                xmlns=\\\"http://www.w3.org/2000/svg\\\"\\n                fill=\\\"none\\\"\\n                viewBox=\\\"0 0 24 24\\\"\\n              >\\n                <circle\\n                  className=\\\"opacity-25\\\"\\n                  cx=\\\"12\\\"\\n                  cy=\\\"12\\\"\\n                  r=\\\"10\\\"\\n                  stroke=\\\"currentColor\\\"\\n                  strokeWidth=\\\"4\\\"\\n                />\\n                <path\\n                  className=\\\"opacity-75\\\"\\n                  fill=\\\"currentColor\\\"\\n                  d=\\\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\\\"\\n                />\\n              </svg>\\n            )}\\n            {action.icon && !action.isLoading && (\\n              <span className=\\\"mr-2\\\">{action.icon}</span>\\n            )}\\n            {label}\\n            {action.shortcut && !action.isLoading && (\\n              <kbd className=\\\"border-border bg-muted ml-2.5 hidden rounded-lg border px-2 py-0.5 font-mono text-xs font-medium sm:inline-block\\\">\\n                {action.shortcut}\\n              </kbd>\\n            )}\\n          </Button>\\n        );\\n      })}\\n    </div>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { ReactNode } from \\\"react\\\";\\n\\n/**\\n * Tool UI conventions:\\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\\n * - Schema: `SerializableXSchema`\\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\\n * - Root attrs: `data-tool-ui-id` + `data-slot`\\n */\\n\\n/**\\n * Schema for tool UI identity.\\n *\\n * Every tool UI should have a unique identifier that:\\n * - Is stable across re-renders\\n * - Is meaningful (not auto-generated)\\n * - Is unique within the conversation\\n *\\n * Format recommendation: `{component-type}-{semantic-identifier}`\\n * Examples: \\\"data-table-expenses-q3\\\", \\\"option-list-deploy-target\\\"\\n */\\nexport const ToolUIIdSchema = z.string().min(1);\\n\\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\\n\\n/**\\n * Primary role of a Tool UI surface in a chat context.\\n */\\nexport const ToolUIRoleSchema = z.enum([\\n  \\\"information\\\",\\n  \\\"decision\\\",\\n  \\\"control\\\",\\n  \\\"state\\\",\\n  \\\"composite\\\",\\n]);\\n\\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\\n\\nexport const ToolUIReceiptOutcomeSchema = z.enum([\\n  \\\"success\\\",\\n  \\\"partial\\\",\\n  \\\"failed\\\",\\n  \\\"cancelled\\\",\\n]);\\n\\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\\n\\n/**\\n * Optional receipt metadata: a durable summary of an outcome.\\n */\\nexport const ToolUIReceiptSchema = z.object({\\n  outcome: ToolUIReceiptOutcomeSchema,\\n  summary: z.string().min(1),\\n  identifiers: z.record(z.string(), z.string()).optional(),\\n  at: z.string().datetime(),\\n});\\n\\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\\n\\n/**\\n * Base schema for Tool UI payloads (id + optional role/receipt).\\n */\\nexport const ToolUISurfaceSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n});\\n\\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\\n\\nexport const ActionSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  /**\\n   * Canonical narration the assistant can use after this action is taken.\\n   *\\n   * Example: \\\"I exported the table as CSV.\\\" / \\\"I opened the link in a new tab.\\\"\\n   */\\n  sentence: z.string().optional(),\\n  confirmLabel: z.string().optional(),\\n  variant: z\\n    .enum([\\\"default\\\", \\\"destructive\\\", \\\"secondary\\\", \\\"ghost\\\", \\\"outline\\\"])\\n    .optional(),\\n  icon: z.custom<ReactNode>().optional(),\\n  loading: z.boolean().optional(),\\n  disabled: z.boolean().optional(),\\n  shortcut: z.string().optional(),\\n});\\n\\nexport type Action = z.infer<typeof ActionSchema>;\\nexport type LocalAction = Action;\\nexport type DecisionAction = Action;\\n\\nexport const DecisionResultSchema = z.object({\\n  kind: z.literal(\\\"decision\\\"),\\n  version: z.literal(1),\\n  decisionId: z.string().min(1),\\n  actionId: z.string().min(1),\\n  actionLabel: z.string().min(1),\\n  at: z.string().datetime(),\\n  payload: z.record(z.string(), z.unknown()).optional(),\\n});\\n\\nexport type DecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n> = Omit<z.infer<typeof DecisionResultSchema>, \\\"payload\\\"> & {\\n  payload?: TPayload;\\n};\\n\\nexport function createDecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n>(args: {\\n  decisionId: string;\\n  action: { id: string; label: string };\\n  payload?: TPayload;\\n}): DecisionResult<TPayload> {\\n  return {\\n    kind: \\\"decision\\\",\\n    version: 1,\\n    decisionId: args.decisionId,\\n    actionId: args.action.id,\\n    actionLabel: args.action.label,\\n    at: new Date().toISOString(),\\n    payload: args.payload,\\n  };\\n}\\n\\nexport const ActionButtonsPropsSchema = z.object({\\n  actions: z.array(ActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n  className: z.string().optional(),\\n});\\n\\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\\n  actions: z.array(SerializableActionSchema),\\n}).omit({ className: true });\\n\\nexport interface ActionsConfig {\\n  items: Action[];\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  confirmTimeout?: number;\\n}\\n\\nexport const SerializableActionsConfigSchema = z.object({\\n  items: z.array(SerializableActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n});\\n\\nexport type SerializableActionsConfig = z.infer<\\n  typeof SerializableActionsConfigSchema\\n>;\\n\\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/use-action-buttons.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/shared/use-action-buttons.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { useCallback, useEffect, useMemo, useRef, useState } from \\\"react\\\";\\nimport type { Action } from \\\"./schema\\\";\\n\\nexport type UseActionButtonsOptions = {\\n  actions: Action[];\\n  onAction: (actionId: string) => void | Promise<void>;\\n  onBeforeAction?: (actionId: string) => boolean | Promise<boolean>;\\n  confirmTimeout?: number;\\n};\\n\\nexport type UseActionButtonsResult = {\\n  actions: Array<\\n    Action & {\\n      currentLabel: string;\\n      isConfirming: boolean;\\n      isExecuting: boolean;\\n      isDisabled: boolean;\\n      isLoading: boolean;\\n    }\\n  >;\\n  runAction: (actionId: string) => Promise<void>;\\n  confirmingActionId: string | null;\\n  executingActionId: string | null;\\n};\\n\\ntype ActionExecutionLock = {\\n  tryAcquire: () => boolean;\\n  release: () => void;\\n};\\n\\nexport function createActionExecutionLock(): ActionExecutionLock {\\n  let locked = false;\\n\\n  return {\\n    tryAcquire: () => {\\n      if (locked) return false;\\n      locked = true;\\n      return true;\\n    },\\n    release: () => {\\n      locked = false;\\n    },\\n  };\\n}\\n\\nexport function useActionButtons(\\n  options: UseActionButtonsOptions,\\n): UseActionButtonsResult {\\n  const { actions, onAction, onBeforeAction, confirmTimeout = 3000 } = options;\\n\\n  const [confirmingActionId, setConfirmingActionId] = useState<string | null>(\\n    null,\\n  );\\n  const [executingActionId, setExecutingActionId] = useState<string | null>(\\n    null,\\n  );\\n  const executionLockRef = useRef<ActionExecutionLock>(\\n    createActionExecutionLock(),\\n  );\\n\\n  useEffect(() => {\\n    if (!confirmingActionId) return;\\n    const id = setTimeout(() => setConfirmingActionId(null), confirmTimeout);\\n    return () => clearTimeout(id);\\n  }, [confirmingActionId, confirmTimeout]);\\n\\n  useEffect(() => {\\n    if (!confirmingActionId) return;\\n    const handleKeyDown = (e: KeyboardEvent) => {\\n      if (e.key === \\\"Escape\\\") {\\n        setConfirmingActionId(null);\\n      }\\n    };\\n\\n    window.addEventListener(\\\"keydown\\\", handleKeyDown);\\n    return () => window.removeEventListener(\\\"keydown\\\", handleKeyDown);\\n  }, [confirmingActionId]);\\n\\n  const runAction = useCallback(\\n    async (actionId: string) => {\\n      const action = actions.find((a) => a.id === actionId);\\n      if (!action) return;\\n\\n      const isAnyActionExecuting = executingActionId !== null;\\n      if (action.disabled || action.loading || isAnyActionExecuting) {\\n        return;\\n      }\\n\\n      if (action.confirmLabel && confirmingActionId !== action.id) {\\n        setConfirmingActionId(action.id);\\n        return;\\n      }\\n\\n      if (!executionLockRef.current.tryAcquire()) {\\n        return;\\n      }\\n\\n      if (onBeforeAction) {\\n        const shouldProceed = await onBeforeAction(action.id);\\n        if (!shouldProceed) {\\n          setConfirmingActionId(null);\\n          executionLockRef.current.release();\\n          return;\\n        }\\n      }\\n\\n      try {\\n        setExecutingActionId(action.id);\\n        await onAction(action.id);\\n      } finally {\\n        executionLockRef.current.release();\\n        setExecutingActionId(null);\\n        setConfirmingActionId(null);\\n      }\\n    },\\n    [actions, confirmingActionId, executingActionId, onAction, onBeforeAction],\\n  );\\n\\n  const resolvedActions = useMemo(\\n    () =>\\n      actions.map((action) => {\\n        const isConfirming = confirmingActionId === action.id;\\n        const isThisActionExecuting = executingActionId === action.id;\\n        const isLoading = action.loading || isThisActionExecuting;\\n        const isDisabled =\\n          action.disabled ||\\n          (executingActionId !== null && !isThisActionExecuting);\\n        const currentLabel =\\n          isConfirming && action.confirmLabel\\n            ? action.confirmLabel\\n            : action.label;\\n\\n        return {\\n          ...action,\\n          currentLabel,\\n          isConfirming,\\n          isExecuting: isThisActionExecuting,\\n          isDisabled,\\n          isLoading,\\n        };\\n      }),\\n    [actions, confirmingActionId, executingActionId],\\n  );\\n\\n  return {\\n    actions: resolvedActions,\\n    runAction,\\n    confirmingActionId,\\n    executingActionId,\\n  };\\n}\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/audio.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"audio\",\n  \"type\": \"registry:block\",\n  \"title\": \"Audio\",\n  \"description\": \"Audio playback with artwork and metadata.\",\n  \"dependencies\": [\n    \"lucide-react\",\n    \"zod\"\n  ],\n  \"registryDependencies\": [\n    \"button\",\n    \"slider\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/audio/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/audio/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n */\\n\\\"use client\\\";\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport { Button } from \\\"@/components/ui/button\\\";\\nexport { Slider } from \\\"@/components/ui/slider\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/audio/audio.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/audio/audio.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport * as React from \\\"react\\\";\\nimport { Pause, Play } from \\\"lucide-react\\\";\\nimport { cn, Button, Slider } from \\\"./_adapter\\\";\\n\\nimport { AudioProvider, useAudio } from \\\"./context\\\";\\nimport type { SerializableAudio, AudioVariant } from \\\"./schema\\\";\\n\\nconst FALLBACK_LOCALE = \\\"en-US\\\";\\n\\nfunction formatTime(seconds: number): string {\\n  if (!Number.isFinite(seconds)) return \\\"0:00\\\";\\n  const mins = Math.floor(seconds / 60);\\n  const secs = Math.floor(seconds % 60);\\n  return `${mins}:${secs.toString().padStart(2, \\\"0\\\")}`;\\n}\\n\\nexport interface AudioProps extends SerializableAudio {\\n  variant?: AudioVariant;\\n  className?: string;\\n  onMediaEvent?: (type: \\\"play\\\" | \\\"pause\\\" | \\\"mute\\\" | \\\"unmute\\\") => void;\\n}\\n\\nexport function Audio(props: AudioProps) {\\n  return (\\n    <AudioProvider>\\n      <AudioInner {...props} />\\n    </AudioProvider>\\n  );\\n}\\n\\ninterface PlayerControls {\\n  isPlaying: boolean;\\n  currentTime: number;\\n  duration: number;\\n  onPlayPause: () => void;\\n  onSeek: (value: number[]) => void;\\n  onSeekStart: () => void;\\n  onSeekEnd: () => void;\\n}\\n\\ninterface FullPlayerProps {\\n  artwork?: string;\\n  title?: string;\\n  description?: string;\\n  controls: PlayerControls;\\n}\\n\\nfunction FullPlayer({\\n  artwork,\\n  title,\\n  description,\\n  controls,\\n}: FullPlayerProps) {\\n  return (\\n    <div className=\\\"flex w-full flex-col\\\">\\n      {artwork && (\\n        <div className=\\\"bg-muted relative aspect-[4/3] w-full overflow-hidden\\\">\\n          <img\\n            src={artwork}\\n            alt=\\\"\\\"\\n            aria-hidden=\\\"true\\\"\\n            loading=\\\"lazy\\\"\\n            decoding=\\\"async\\\"\\n            className=\\\"absolute inset-0 h-full w-full object-cover\\\"\\n          />\\n        </div>\\n      )}\\n      <div className=\\\"flex flex-col gap-5 p-4\\\">\\n        {(title || description) && (\\n          <div className=\\\"space-y-0.5\\\">\\n            {title && (\\n              <div className=\\\"text-foreground line-clamp-2 font-semibold leading-snug\\\">\\n                {title}\\n              </div>\\n            )}\\n            {description && (\\n              <div className=\\\"text-muted-foreground line-clamp-2 text-sm leading-snug\\\">\\n                {description}\\n              </div>\\n            )}\\n          </div>\\n        )}\\n        <div className=\\\"flex items-start gap-3\\\">\\n          <div className=\\\"flex flex-1 flex-col gap-2\\\">\\n            <Slider\\n              value={[controls.currentTime]}\\n              max={controls.duration || 100}\\n              step={0.1}\\n              onValueChange={controls.onSeek}\\n              onPointerDown={controls.onSeekStart}\\n              onPointerUp={controls.onSeekEnd}\\n              className=\\\"cursor-pointer [&_[data-slot=range]]:bg-foreground [&_[data-slot=thumb]]:size-3 [&_[data-slot=thumb]]:border-2 [&_[data-slot=thumb]]:border-background [&_[data-slot=thumb]]:bg-foreground\\\"\\n              aria-label=\\\"Audio progress\\\"\\n            />\\n            <div className=\\\"text-muted-foreground flex items-center justify-between text-xs tabular-nums\\\">\\n              <span>{formatTime(controls.currentTime)}</span>\\n              <span>{formatTime(controls.duration)}</span>\\n            </div>\\n          </div>\\n          <Button\\n            variant=\\\"default\\\"\\n            size=\\\"icon\\\"\\n            onClick={controls.onPlayPause}\\n            className=\\\"-mt-4 size-10 shrink-0 rounded-full\\\"\\n            aria-label={controls.isPlaying ? \\\"Pause\\\" : \\\"Play\\\"}\\n          >\\n            {controls.isPlaying ? (\\n              <Pause className=\\\"size-4\\\" fill=\\\"currentColor\\\" />\\n            ) : (\\n              <Play className=\\\"size-4 ml-0.5\\\" fill=\\\"currentColor\\\" />\\n            )}\\n          </Button>\\n        </div>\\n      </div>\\n    </div>\\n  );\\n}\\n\\ninterface CompactPlayerProps {\\n  artwork?: string;\\n  title?: string;\\n  description?: string;\\n  controls: PlayerControls;\\n}\\n\\nfunction CompactPlayer({\\n  artwork,\\n  title,\\n  description,\\n  controls,\\n}: CompactPlayerProps) {\\n  const progress =\\n    controls.duration > 0\\n      ? (controls.currentTime / controls.duration) * 100\\n      : 0;\\n\\n  return (\\n    <div className=\\\"relative flex w-full items-center gap-3 overflow-hidden p-3\\\">\\n      {artwork && (\\n        <>\\n          <img\\n            src={artwork}\\n            alt=\\\"\\\"\\n            aria-hidden=\\\"true\\\"\\n            className=\\\"pointer-events-none absolute -left-1/4 top-1/2 h-[200%] w-auto -translate-y-1/2 object-cover opacity-40 blur-2xl saturate-150\\\"\\n          />\\n          <div className=\\\"from-card/60 to-card/90 pointer-events-none absolute inset-0 bg-gradient-to-r\\\" />\\n        </>\\n      )}\\n      {artwork && (\\n        <div className=\\\"ring-background/20 relative size-12 shrink-0 overflow-hidden rounded-lg shadow-lg ring-1\\\">\\n          <img\\n            src={artwork}\\n            alt=\\\"\\\"\\n            aria-hidden=\\\"true\\\"\\n            loading=\\\"lazy\\\"\\n            decoding=\\\"async\\\"\\n            className=\\\"absolute inset-0 h-full w-full object-cover\\\"\\n          />\\n        </div>\\n      )}\\n      <div className=\\\"relative flex min-w-0 flex-1 flex-col justify-center\\\">\\n        {title && (\\n          <div className=\\\"text-foreground truncate text-sm font-semibold leading-tight\\\">\\n            {title}\\n          </div>\\n        )}\\n        {description && (\\n          <div className=\\\"text-muted-foreground mt-0.5 truncate text-xs leading-tight\\\">\\n            {description}\\n          </div>\\n        )}\\n        {controls.duration > 0 && (\\n          <div className=\\\"mt-1 flex items-center gap-2\\\">\\n            <div className=\\\"bg-foreground/20 relative h-1 flex-1 overflow-hidden rounded-full\\\">\\n              <div\\n                className=\\\"bg-foreground absolute inset-y-0 left-0 rounded-full transition-all duration-150\\\"\\n                style={{ width: `${progress}%` }}\\n              />\\n            </div>\\n            <span className=\\\"text-muted-foreground text-xs tabular-nums\\\">\\n              {formatTime(controls.currentTime)}\\n            </span>\\n          </div>\\n        )}\\n      </div>\\n      <Button\\n        variant=\\\"default\\\"\\n        size=\\\"icon\\\"\\n        onClick={controls.onPlayPause}\\n        className=\\\"relative size-10 shrink-0 rounded-full shadow-md\\\"\\n        aria-label={controls.isPlaying ? \\\"Pause\\\" : \\\"Play\\\"}\\n      >\\n        {controls.isPlaying ? (\\n          <Pause className=\\\"size-4\\\" fill=\\\"currentColor\\\" />\\n        ) : (\\n          <Play className=\\\"size-4 ml-0.5\\\" fill=\\\"currentColor\\\" />\\n        )}\\n      </Button>\\n    </div>\\n  );\\n}\\n\\nfunction AudioInner(props: AudioProps) {\\n  const { variant = \\\"full\\\", className, onMediaEvent, ...serializable } = props;\\n\\n  const {\\n    id,\\n    src,\\n    title,\\n    description,\\n    artwork,\\n    locale: providedLocale,\\n  } = serializable;\\n\\n  const locale = providedLocale ?? FALLBACK_LOCALE;\\n\\n  const { state, setState, setAudioElement } = useAudio();\\n  const audioRef = React.useRef<HTMLAudioElement | null>(null);\\n  const [currentTime, setCurrentTime] = React.useState(0);\\n  const [duration, setDuration] = React.useState(0);\\n  const [isSeeking, setIsSeeking] = React.useState(false);\\n\\n  React.useEffect(() => {\\n    setAudioElement(audioRef.current);\\n    return () => setAudioElement(null);\\n  }, [setAudioElement]);\\n\\n  React.useEffect(() => {\\n    const audio = audioRef.current;\\n    if (!audio) return;\\n    if (state.playing && audio.paused) {\\n      void audio.play().catch(() => undefined);\\n    } else if (!state.playing && !audio.paused) {\\n      audio.pause();\\n    }\\n  }, [state.playing]);\\n\\n  const handlePlayPause = () => {\\n    const audio = audioRef.current;\\n    if (!audio) return;\\n    if (audio.paused) {\\n      void audio.play().catch(() => undefined);\\n    } else {\\n      audio.pause();\\n    }\\n  };\\n\\n  const handleSeek = (value: number[]) => {\\n    const audio = audioRef.current;\\n    if (!audio) return;\\n    const newTime = value[0];\\n    audio.currentTime = newTime;\\n    setCurrentTime(newTime);\\n  };\\n\\n  const handleSeekStart = () => {\\n    setIsSeeking(true);\\n  };\\n\\n  const handleSeekEnd = () => {\\n    setIsSeeking(false);\\n  };\\n\\n  const controls: PlayerControls = {\\n    isPlaying: state.playing,\\n    currentTime,\\n    duration,\\n    onPlayPause: handlePlayPause,\\n    onSeek: handleSeek,\\n    onSeekStart: handleSeekStart,\\n    onSeekEnd: handleSeekEnd,\\n  };\\n\\n  const isCompact = variant === \\\"compact\\\";\\n\\n  return (\\n    <article\\n      className={cn(\\n        \\\"@container/actions relative w-full\\\",\\n        isCompact ? \\\"min-w-72 max-w-md\\\" : \\\"min-w-52 max-w-sm\\\",\\n        className,\\n      )}\\n      lang={locale}\\n      data-tool-ui-id={id}\\n      data-slot=\\\"audio\\\"\\n    >\\n      <div\\n        className={cn(\\n          \\\"group @container relative isolate flex w-full min-w-0 flex-col overflow-hidden\\\",\\n          \\\"border-border bg-card border text-sm shadow-xs\\\",\\n          \\\"rounded-xl\\\",\\n        )}\\n      >\\n        {isCompact ? (\\n          <CompactPlayer\\n            artwork={artwork}\\n            title={title}\\n            description={description}\\n            controls={controls}\\n          />\\n        ) : (\\n          <FullPlayer\\n            artwork={artwork}\\n            title={title}\\n            description={description}\\n            controls={controls}\\n          />\\n        )}\\n\\n        <audio\\n          ref={audioRef}\\n          src={src}\\n          preload=\\\"metadata\\\"\\n          className=\\\"hidden\\\"\\n          onPlay={() => {\\n            setState({ playing: true });\\n            onMediaEvent?.(\\\"play\\\");\\n          }}\\n          onPause={() => {\\n            setState({ playing: false });\\n            onMediaEvent?.(\\\"pause\\\");\\n          }}\\n          onTimeUpdate={(event) => {\\n            if (!isSeeking) {\\n              setCurrentTime(event.currentTarget.currentTime);\\n            }\\n          }}\\n          onLoadedMetadata={(event) => {\\n            setDuration(event.currentTarget.duration);\\n          }}\\n          onDurationChange={(event) => {\\n            setDuration(event.currentTarget.duration);\\n          }}\\n        />\\n      </div>\\n    </article>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/audio/context.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/audio/context.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport * as React from \\\"react\\\";\\n\\nexport interface AudioPlaybackState {\\n  playing: boolean;\\n  muted: boolean;\\n}\\n\\nexport interface AudioContextValue {\\n  state: AudioPlaybackState;\\n  setState: (patch: Partial<AudioPlaybackState>) => void;\\n  audioElement: HTMLAudioElement | null;\\n  setAudioElement: (node: HTMLAudioElement | null) => void;\\n}\\n\\nconst AudioContext = React.createContext<AudioContextValue | null>(null);\\n\\nexport function useAudio() {\\n  const ctx = React.use(AudioContext);\\n  if (!ctx) {\\n    throw new Error(\\\"useAudio must be used within an <AudioProvider />\\\");\\n  }\\n  return ctx;\\n}\\n\\nexport interface AudioProviderProps {\\n  children: React.ReactNode;\\n  defaultState?: Partial<AudioPlaybackState>;\\n}\\n\\nexport function AudioProvider({ children, defaultState }: AudioProviderProps) {\\n  const [state, setStateInternal] = React.useState<AudioPlaybackState>({\\n    playing: defaultState?.playing ?? false,\\n    muted: defaultState?.muted ?? false,\\n  });\\n\\n  const [audioElement, setAudioElement] =\\n    React.useState<HTMLAudioElement | null>(null);\\n\\n  const setState = React.useCallback((patch: Partial<AudioPlaybackState>) => {\\n    setStateInternal((prev) => ({ ...prev, ...patch }));\\n  }, []);\\n\\n  const value = React.useMemo(\\n    () => ({ state, setState, audioElement, setAudioElement }),\\n    [state, setState, audioElement],\\n  );\\n\\n  return (\\n    <AudioContext.Provider value={value}>{children}</AudioContext.Provider>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/audio/index.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/audio/index.ts\",\n      \"content\": \"export { Audio } from \\\"./audio\\\";\\nexport type { AudioProps } from \\\"./audio\\\";\\nexport { AudioProvider, useAudio } from \\\"./context\\\";\\nexport type { AudioPlaybackState, AudioContextValue } from \\\"./context\\\";\\nexport type { SerializableAudio, Source, AudioVariant } from \\\"./schema\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/audio/README.md\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/audio/README.md\",\n      \"content\": \"# Audio\\n\\nImplementation for the \\\"audio\\\" Tool UI surface.\\n\\n## Files\\n\\n- public exports: components/tool-ui/audio/index.ts\\n- serializable schema + parse helpers: components/tool-ui/audio/schema.ts\\n\\n## Companion assets\\n\\n- Docs page: app/docs/audio/content.mdx\\n- Preset payload: lib/presets/audio.ts\\n\\n## Quick check\\n\\nRun this after edits:\\n\\npnpm test\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/audio/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/audio/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\nimport {\\n  ToolUIIdSchema,\\n  ToolUIReceiptSchema,\\n  ToolUIRoleSchema,\\n} from \\\"../shared/schema\\\";\\n\\nexport const SourceSchema = z.object({\\n  label: z.string(),\\n  iconUrl: z.url().optional(),\\n  url: z.url().optional(),\\n});\\n\\nexport type Source = z.infer<typeof SourceSchema>;\\n\\nexport const SerializableAudioSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n  assetId: z.string(),\\n  src: z.url(),\\n  title: z.string().optional(),\\n  description: z.string().optional(),\\n  artwork: z.url().optional(),\\n  durationMs: z.number().int().positive().optional(),\\n  fileSizeBytes: z.number().int().positive().optional(),\\n  createdAt: z.string().datetime().optional(),\\n  locale: z.string().optional(),\\n  source: SourceSchema.optional(),\\n});\\n\\nexport type SerializableAudio = z.infer<typeof SerializableAudioSchema>;\\n\\nconst SerializableAudioSchemaContract = defineToolUiContract(\\n  \\\"Audio\\\",\\n  SerializableAudioSchema,\\n);\\n\\nexport const parseSerializableAudio: (input: unknown) => SerializableAudio =\\n  SerializableAudioSchemaContract.parse;\\n\\nexport const safeParseSerializableAudio: (\\n  input: unknown,\\n) => SerializableAudio | null = SerializableAudioSchemaContract.safeParse;\\nexport type AudioVariant = \\\"full\\\" | \\\"compact\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { ReactNode } from \\\"react\\\";\\n\\n/**\\n * Tool UI conventions:\\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\\n * - Schema: `SerializableXSchema`\\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\\n * - Root attrs: `data-tool-ui-id` + `data-slot`\\n */\\n\\n/**\\n * Schema for tool UI identity.\\n *\\n * Every tool UI should have a unique identifier that:\\n * - Is stable across re-renders\\n * - Is meaningful (not auto-generated)\\n * - Is unique within the conversation\\n *\\n * Format recommendation: `{component-type}-{semantic-identifier}`\\n * Examples: \\\"data-table-expenses-q3\\\", \\\"option-list-deploy-target\\\"\\n */\\nexport const ToolUIIdSchema = z.string().min(1);\\n\\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\\n\\n/**\\n * Primary role of a Tool UI surface in a chat context.\\n */\\nexport const ToolUIRoleSchema = z.enum([\\n  \\\"information\\\",\\n  \\\"decision\\\",\\n  \\\"control\\\",\\n  \\\"state\\\",\\n  \\\"composite\\\",\\n]);\\n\\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\\n\\nexport const ToolUIReceiptOutcomeSchema = z.enum([\\n  \\\"success\\\",\\n  \\\"partial\\\",\\n  \\\"failed\\\",\\n  \\\"cancelled\\\",\\n]);\\n\\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\\n\\n/**\\n * Optional receipt metadata: a durable summary of an outcome.\\n */\\nexport const ToolUIReceiptSchema = z.object({\\n  outcome: ToolUIReceiptOutcomeSchema,\\n  summary: z.string().min(1),\\n  identifiers: z.record(z.string(), z.string()).optional(),\\n  at: z.string().datetime(),\\n});\\n\\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\\n\\n/**\\n * Base schema for Tool UI payloads (id + optional role/receipt).\\n */\\nexport const ToolUISurfaceSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n});\\n\\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\\n\\nexport const ActionSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  /**\\n   * Canonical narration the assistant can use after this action is taken.\\n   *\\n   * Example: \\\"I exported the table as CSV.\\\" / \\\"I opened the link in a new tab.\\\"\\n   */\\n  sentence: z.string().optional(),\\n  confirmLabel: z.string().optional(),\\n  variant: z\\n    .enum([\\\"default\\\", \\\"destructive\\\", \\\"secondary\\\", \\\"ghost\\\", \\\"outline\\\"])\\n    .optional(),\\n  icon: z.custom<ReactNode>().optional(),\\n  loading: z.boolean().optional(),\\n  disabled: z.boolean().optional(),\\n  shortcut: z.string().optional(),\\n});\\n\\nexport type Action = z.infer<typeof ActionSchema>;\\nexport type LocalAction = Action;\\nexport type DecisionAction = Action;\\n\\nexport const DecisionResultSchema = z.object({\\n  kind: z.literal(\\\"decision\\\"),\\n  version: z.literal(1),\\n  decisionId: z.string().min(1),\\n  actionId: z.string().min(1),\\n  actionLabel: z.string().min(1),\\n  at: z.string().datetime(),\\n  payload: z.record(z.string(), z.unknown()).optional(),\\n});\\n\\nexport type DecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n> = Omit<z.infer<typeof DecisionResultSchema>, \\\"payload\\\"> & {\\n  payload?: TPayload;\\n};\\n\\nexport function createDecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n>(args: {\\n  decisionId: string;\\n  action: { id: string; label: string };\\n  payload?: TPayload;\\n}): DecisionResult<TPayload> {\\n  return {\\n    kind: \\\"decision\\\",\\n    version: 1,\\n    decisionId: args.decisionId,\\n    actionId: args.action.id,\\n    actionLabel: args.action.label,\\n    at: new Date().toISOString(),\\n    payload: args.payload,\\n  };\\n}\\n\\nexport const ActionButtonsPropsSchema = z.object({\\n  actions: z.array(ActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n  className: z.string().optional(),\\n});\\n\\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\\n  actions: z.array(SerializableActionSchema),\\n}).omit({ className: true });\\n\\nexport interface ActionsConfig {\\n  items: Action[];\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  confirmTimeout?: number;\\n}\\n\\nexport const SerializableActionsConfigSchema = z.object({\\n  items: z.array(SerializableActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n});\\n\\nexport type SerializableActionsConfig = z.infer<\\n  typeof SerializableActionsConfigSchema\\n>;\\n\\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/chart.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"chart\",\n  \"type\": \"registry:block\",\n  \"title\": \"Chart\",\n  \"description\": \"Visualize data with interactive charts.\",\n  \"dependencies\": [\n    \"recharts@2.15.4\",\n    \"zod\"\n  ],\n  \"registryDependencies\": [\n    \"card\",\n    \"chart\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/chart/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/chart/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn    → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n *   Chart → shadcn/ui Chart (recharts wrapper)\\n *   Card  → shadcn/ui Card\\n */\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport {\\n  ChartContainer,\\n  ChartTooltip,\\n  ChartTooltipContent,\\n  ChartLegend,\\n  ChartLegendContent,\\n  type ChartConfig,\\n} from \\\"@/components/ui/chart\\\";\\nexport {\\n  Card,\\n  CardHeader,\\n  CardTitle,\\n  CardDescription,\\n  CardContent,\\n} from \\\"@/components/ui/card\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/chart/chart.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/chart/chart.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { useMemo, useCallback, memo } from \\\"react\\\";\\nimport {\\n  BarChart,\\n  LineChart,\\n  Bar,\\n  Line,\\n  XAxis,\\n  YAxis,\\n  CartesianGrid,\\n} from \\\"recharts\\\";\\n\\nimport {\\n  cn,\\n  ChartContainer,\\n  ChartTooltip,\\n  ChartTooltipContent,\\n  ChartLegend,\\n  ChartLegendContent,\\n  Card,\\n  CardHeader,\\n  CardTitle,\\n  CardDescription,\\n  CardContent,\\n  type ChartConfig,\\n} from \\\"./_adapter\\\";\\nimport type { ChartProps } from \\\"./schema\\\";\\n\\nconst DEFAULT_COLORS = [\\n  \\\"var(--chart-1)\\\",\\n  \\\"var(--chart-2)\\\",\\n  \\\"var(--chart-3)\\\",\\n  \\\"var(--chart-4)\\\",\\n  \\\"var(--chart-5)\\\",\\n];\\n\\nexport const Chart = memo(function Chart({\\n  id,\\n  type,\\n  title,\\n  description,\\n  data,\\n  xKey,\\n  series,\\n  colors,\\n  showLegend = false,\\n  showGrid = true,\\n  className,\\n  onDataPointClick,\\n}: ChartProps) {\\n  const palette = colors?.length ? colors : DEFAULT_COLORS;\\n\\n  const seriesColors = useMemo(\\n    () =>\\n      series.map(\\n        (seriesItem, index) =>\\n          seriesItem.color ?? palette[index % palette.length],\\n      ),\\n    [series, palette],\\n  );\\n\\n  const chartConfig: ChartConfig = useMemo(\\n    () =>\\n      Object.fromEntries(\\n        series.map((seriesItem, index) => [\\n          seriesItem.key,\\n          {\\n            label: seriesItem.label,\\n            color: seriesColors[index],\\n          },\\n        ]),\\n      ),\\n    [series, seriesColors],\\n  );\\n\\n  const handleDataPointClick = useCallback(\\n    (\\n      seriesKey: string,\\n      seriesLabel: string,\\n      payload: Record<string, unknown>,\\n      index: number,\\n    ) => {\\n      onDataPointClick?.({\\n        seriesKey,\\n        seriesLabel,\\n        xValue: payload[xKey],\\n        yValue: payload[seriesKey],\\n        index,\\n        payload,\\n      });\\n    },\\n    [onDataPointClick, xKey],\\n  );\\n\\n  const ChartComponent = type === \\\"bar\\\" ? BarChart : LineChart;\\n\\n  const chartContent = (\\n    <ChartContainer\\n      config={chartConfig}\\n      className=\\\"min-h-[200px] w-full\\\"\\n      data-tool-ui-id={id}\\n    >\\n      <ChartComponent data={data} accessibilityLayer>\\n        {showGrid && <CartesianGrid vertical={false} />}\\n        <XAxis\\n          dataKey={xKey}\\n          tickLine={false}\\n          tickMargin={10}\\n          axisLine={false}\\n        />\\n        <YAxis tickLine={false} axisLine={false} tickMargin={10} />\\n        <ChartTooltip content={<ChartTooltipContent />} />\\n        {showLegend && <ChartLegend content={<ChartLegendContent />} />}\\n\\n        {type === \\\"bar\\\" &&\\n          series.map((s, i) => (\\n            <Bar\\n              key={s.key}\\n              dataKey={s.key}\\n              fill={seriesColors[i]}\\n              radius={4}\\n              onClick={(data) =>\\n                handleDataPointClick(s.key, s.label, data.payload, data.index)\\n              }\\n              cursor={onDataPointClick ? \\\"pointer\\\" : undefined}\\n            />\\n          ))}\\n\\n        {type === \\\"line\\\" &&\\n          series.map((s, i) => (\\n            <Line\\n              key={s.key}\\n              dataKey={s.key}\\n              type=\\\"monotone\\\"\\n              stroke={seriesColors[i]}\\n              strokeWidth={2}\\n              dot={{ r: 4, cursor: onDataPointClick ? \\\"pointer\\\" : undefined }}\\n              activeDot={{\\n                r: 6,\\n                cursor: onDataPointClick ? \\\"pointer\\\" : undefined,\\n                // Recharts types are incorrect - onClick receives (event, dotData) at runtime\\n                onClick: ((\\n                  _: unknown,\\n                  dotData: { payload: Record<string, unknown>; index: number },\\n                ) => {\\n                  handleDataPointClick(\\n                    s.key,\\n                    s.label,\\n                    dotData.payload,\\n                    dotData.index,\\n                  );\\n                }) as unknown as React.MouseEventHandler,\\n              }}\\n            />\\n          ))}\\n      </ChartComponent>\\n    </ChartContainer>\\n  );\\n\\n  return (\\n    <Card\\n      className={cn(\\\"w-full min-w-80\\\", className)}\\n      data-tool-ui-id={id}\\n      data-slot=\\\"chart\\\"\\n    >\\n      {(title || description) && (\\n        <CardHeader>\\n          {title && <CardTitle className=\\\"text-pretty\\\">{title}</CardTitle>}\\n          {description && (\\n            <CardDescription className=\\\"text-pretty\\\">\\n              {description}\\n            </CardDescription>\\n          )}\\n        </CardHeader>\\n      )}\\n      <CardContent>{chartContent}</CardContent>\\n    </Card>\\n  );\\n});\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/chart/index.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/chart/index.tsx\",\n      \"content\": \"export { Chart } from \\\"./chart\\\";\\nexport {\\n  type ChartProps,\\n  type ChartSeries,\\n  type ChartDataPoint,\\n  type ChartClientProps,\\n  type SerializableChart,\\n} from \\\"./schema\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/chart/README.md\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/chart/README.md\",\n      \"content\": \"# Chart\\n\\nImplementation for the \\\"chart\\\" Tool UI surface.\\n\\n## Files\\n\\n- public exports: components/tool-ui/chart/index.tsx\\n- serializable schema + parse helpers: components/tool-ui/chart/schema.ts\\n\\n## Companion assets\\n\\n- Docs page: app/docs/chart/content.mdx\\n- Preset payload: lib/presets/chart.ts\\n\\n## Quick check\\n\\nRun this after edits:\\n\\npnpm test\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/chart/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/chart/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\nimport {\\n  ToolUIIdSchema,\\n  ToolUIReceiptSchema,\\n  ToolUIRoleSchema,\\n} from \\\"../shared/schema\\\";\\n\\nexport const ChartSeriesSchema = z.object({\\n  key: z.string().min(1),\\n  label: z.string().min(1),\\n  color: z.string().optional(),\\n});\\n\\nexport type ChartSeries = z.infer<typeof ChartSeriesSchema>;\\n\\nexport const ChartPropsSchema = z\\n  .object({\\n    id: ToolUIIdSchema,\\n    role: ToolUIRoleSchema.optional(),\\n    receipt: ToolUIReceiptSchema.optional(),\\n    type: z.enum([\\\"bar\\\", \\\"line\\\"]),\\n    title: z.string().optional(),\\n    description: z.string().optional(),\\n    data: z.array(z.record(z.string(), z.unknown())).min(1),\\n    xKey: z.string().min(1),\\n    series: z.array(ChartSeriesSchema).min(1),\\n    /** Color palette applied to series in order. Individual series.color takes precedence. */\\n    colors: z.array(z.string().min(1)).min(1).optional(),\\n    showLegend: z.boolean().optional(),\\n    showGrid: z.boolean().optional(),\\n  })\\n  .superRefine((value, ctx) => {\\n    const seenSeriesKeys = new Set<string>();\\n    value.series.forEach((series, index) => {\\n      if (seenSeriesKeys.has(series.key)) {\\n        ctx.addIssue({\\n          code: \\\"custom\\\",\\n          path: [\\\"series\\\", index, \\\"key\\\"],\\n          message: `Duplicate series key \\\"${series.key}\\\".`,\\n        });\\n        return;\\n      }\\n      seenSeriesKeys.add(series.key);\\n    });\\n\\n    value.data.forEach((row, rowIndex) => {\\n      if (!(value.xKey in row)) {\\n        ctx.addIssue({\\n          code: \\\"custom\\\",\\n          path: [\\\"data\\\", rowIndex, value.xKey],\\n          message: `Missing xKey \\\"${value.xKey}\\\" in data row.`,\\n        });\\n      } else {\\n        const xVal = row[value.xKey];\\n        const isValidX = typeof xVal === \\\"string\\\" || typeof xVal === \\\"number\\\";\\n        if (!isValidX) {\\n          ctx.addIssue({\\n            code: \\\"custom\\\",\\n            path: [\\\"data\\\", rowIndex, value.xKey],\\n            message: `Expected \\\"${value.xKey}\\\" to be a string or number.`,\\n          });\\n        }\\n      }\\n\\n      value.series.forEach((series) => {\\n        if (!(series.key in row)) {\\n          ctx.addIssue({\\n            code: \\\"custom\\\",\\n            path: [\\\"data\\\", rowIndex, series.key],\\n            message: `Missing series key \\\"${series.key}\\\" in data row.`,\\n          });\\n          return;\\n        }\\n\\n        const yVal = row[series.key];\\n        if (yVal === null) {\\n          return;\\n        }\\n        if (typeof yVal !== \\\"number\\\" || !Number.isFinite(yVal)) {\\n          ctx.addIssue({\\n            code: \\\"custom\\\",\\n            path: [\\\"data\\\", rowIndex, series.key],\\n            message: `Expected \\\"${series.key}\\\" to be a finite number (or null).`,\\n          });\\n        }\\n      });\\n    });\\n  });\\n\\nexport type ChartDataPoint = {\\n  seriesKey: string;\\n  seriesLabel: string;\\n  xValue: unknown;\\n  yValue: unknown;\\n  index: number;\\n  payload: Record<string, unknown>;\\n};\\n\\nexport type ChartClientProps = {\\n  className?: string;\\n  onDataPointClick?: (point: ChartDataPoint) => void;\\n};\\n\\nexport type ChartProps = z.infer<typeof ChartPropsSchema> & ChartClientProps;\\n\\nexport const SerializableChartSchema = ChartPropsSchema;\\n\\nexport type SerializableChart = z.infer<typeof SerializableChartSchema>;\\n\\nconst SerializableChartSchemaContract = defineToolUiContract(\\n  \\\"Chart\\\",\\n  SerializableChartSchema,\\n);\\n\\nexport const parseSerializableChart: (input: unknown) => SerializableChart =\\n  SerializableChartSchemaContract.parse;\\n\\nexport const safeParseSerializableChart: (\\n  input: unknown,\\n) => SerializableChart | null = SerializableChartSchemaContract.safeParse;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { ReactNode } from \\\"react\\\";\\n\\n/**\\n * Tool UI conventions:\\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\\n * - Schema: `SerializableXSchema`\\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\\n * - Root attrs: `data-tool-ui-id` + `data-slot`\\n */\\n\\n/**\\n * Schema for tool UI identity.\\n *\\n * Every tool UI should have a unique identifier that:\\n * - Is stable across re-renders\\n * - Is meaningful (not auto-generated)\\n * - Is unique within the conversation\\n *\\n * Format recommendation: `{component-type}-{semantic-identifier}`\\n * Examples: \\\"data-table-expenses-q3\\\", \\\"option-list-deploy-target\\\"\\n */\\nexport const ToolUIIdSchema = z.string().min(1);\\n\\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\\n\\n/**\\n * Primary role of a Tool UI surface in a chat context.\\n */\\nexport const ToolUIRoleSchema = z.enum([\\n  \\\"information\\\",\\n  \\\"decision\\\",\\n  \\\"control\\\",\\n  \\\"state\\\",\\n  \\\"composite\\\",\\n]);\\n\\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\\n\\nexport const ToolUIReceiptOutcomeSchema = z.enum([\\n  \\\"success\\\",\\n  \\\"partial\\\",\\n  \\\"failed\\\",\\n  \\\"cancelled\\\",\\n]);\\n\\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\\n\\n/**\\n * Optional receipt metadata: a durable summary of an outcome.\\n */\\nexport const ToolUIReceiptSchema = z.object({\\n  outcome: ToolUIReceiptOutcomeSchema,\\n  summary: z.string().min(1),\\n  identifiers: z.record(z.string(), z.string()).optional(),\\n  at: z.string().datetime(),\\n});\\n\\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\\n\\n/**\\n * Base schema for Tool UI payloads (id + optional role/receipt).\\n */\\nexport const ToolUISurfaceSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n});\\n\\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\\n\\nexport const ActionSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  /**\\n   * Canonical narration the assistant can use after this action is taken.\\n   *\\n   * Example: \\\"I exported the table as CSV.\\\" / \\\"I opened the link in a new tab.\\\"\\n   */\\n  sentence: z.string().optional(),\\n  confirmLabel: z.string().optional(),\\n  variant: z\\n    .enum([\\\"default\\\", \\\"destructive\\\", \\\"secondary\\\", \\\"ghost\\\", \\\"outline\\\"])\\n    .optional(),\\n  icon: z.custom<ReactNode>().optional(),\\n  loading: z.boolean().optional(),\\n  disabled: z.boolean().optional(),\\n  shortcut: z.string().optional(),\\n});\\n\\nexport type Action = z.infer<typeof ActionSchema>;\\nexport type LocalAction = Action;\\nexport type DecisionAction = Action;\\n\\nexport const DecisionResultSchema = z.object({\\n  kind: z.literal(\\\"decision\\\"),\\n  version: z.literal(1),\\n  decisionId: z.string().min(1),\\n  actionId: z.string().min(1),\\n  actionLabel: z.string().min(1),\\n  at: z.string().datetime(),\\n  payload: z.record(z.string(), z.unknown()).optional(),\\n});\\n\\nexport type DecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n> = Omit<z.infer<typeof DecisionResultSchema>, \\\"payload\\\"> & {\\n  payload?: TPayload;\\n};\\n\\nexport function createDecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n>(args: {\\n  decisionId: string;\\n  action: { id: string; label: string };\\n  payload?: TPayload;\\n}): DecisionResult<TPayload> {\\n  return {\\n    kind: \\\"decision\\\",\\n    version: 1,\\n    decisionId: args.decisionId,\\n    actionId: args.action.id,\\n    actionLabel: args.action.label,\\n    at: new Date().toISOString(),\\n    payload: args.payload,\\n  };\\n}\\n\\nexport const ActionButtonsPropsSchema = z.object({\\n  actions: z.array(ActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n  className: z.string().optional(),\\n});\\n\\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\\n  actions: z.array(SerializableActionSchema),\\n}).omit({ className: true });\\n\\nexport interface ActionsConfig {\\n  items: Action[];\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  confirmTimeout?: number;\\n}\\n\\nexport const SerializableActionsConfigSchema = z.object({\\n  items: z.array(SerializableActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n});\\n\\nexport type SerializableActionsConfig = z.infer<\\n  typeof SerializableActionsConfigSchema\\n>;\\n\\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/citation.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"citation\",\n  \"type\": \"registry:block\",\n  \"title\": \"Citation\",\n  \"description\": \"Display source references with attribution.\",\n  \"dependencies\": [\n    \"lucide-react\",\n    \"zod\"\n  ],\n  \"registryDependencies\": [\n    \"popover\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/citation/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/citation/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn      → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n *   Tooltip → shadcn/ui Tooltip (only needed for variant=\\\"inline\\\")\\n *   Popover → shadcn/ui Popover (only needed for CitationList)\\n */\\n\\\"use client\\\";\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport {\\n  Popover,\\n  PopoverContent,\\n  PopoverTrigger,\\n} from \\\"@/components/ui/popover\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/citation/citation-list.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/citation/citation-list.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport * as React from \\\"react\\\";\\nimport type { LucideIcon } from \\\"lucide-react\\\";\\nimport {\\n  FileText,\\n  Globe,\\n  Code2,\\n  Newspaper,\\n  Database,\\n  File,\\n  ExternalLink,\\n} from \\\"lucide-react\\\";\\nimport { cn, Popover, PopoverContent, PopoverTrigger } from \\\"./_adapter\\\";\\nimport { Citation } from \\\"./citation\\\";\\nimport type {\\n  SerializableCitation,\\n  CitationType,\\n  CitationVariant,\\n} from \\\"./schema\\\";\\nimport {\\n  openSafeNavigationHref,\\n  resolveSafeNavigationHref,\\n} from \\\"../shared/media\\\";\\n\\nconst TYPE_ICONS: Record<CitationType, LucideIcon> = {\\n  webpage: Globe,\\n  document: FileText,\\n  article: Newspaper,\\n  api: Database,\\n  code: Code2,\\n  other: File,\\n};\\n\\nfunction useHoverPopover(delay = 100) {\\n  const [open, setOpen] = React.useState(false);\\n  const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\\n  const containerRef = React.useRef<HTMLDivElement>(null);\\n\\n  const handleMouseEnter = React.useCallback(() => {\\n    if (timeoutRef.current) clearTimeout(timeoutRef.current);\\n    timeoutRef.current = setTimeout(() => setOpen(true), delay);\\n  }, [delay]);\\n\\n  const handleMouseLeave = React.useCallback(() => {\\n    if (timeoutRef.current) clearTimeout(timeoutRef.current);\\n    timeoutRef.current = setTimeout(() => setOpen(false), delay);\\n  }, [delay]);\\n\\n  const handleFocus = React.useCallback(() => {\\n    if (timeoutRef.current) clearTimeout(timeoutRef.current);\\n    setOpen(true);\\n  }, []);\\n\\n  const handleBlur = React.useCallback(\\n    (e: React.FocusEvent) => {\\n      const relatedTarget = e.relatedTarget as HTMLElement | null;\\n      if (containerRef.current?.contains(relatedTarget)) {\\n        return;\\n      }\\n      if (relatedTarget?.closest(\\\"[data-radix-popper-content-wrapper]\\\")) {\\n        return;\\n      }\\n      if (timeoutRef.current) clearTimeout(timeoutRef.current);\\n      timeoutRef.current = setTimeout(() => setOpen(false), delay);\\n    },\\n    [delay],\\n  );\\n\\n  React.useEffect(() => {\\n    return () => {\\n      if (timeoutRef.current) clearTimeout(timeoutRef.current);\\n    };\\n  }, []);\\n\\n  return {\\n    open,\\n    setOpen,\\n    containerRef,\\n    handleMouseEnter,\\n    handleMouseLeave,\\n    handleFocus,\\n    handleBlur,\\n  };\\n}\\n\\nexport interface CitationListProps {\\n  id: string;\\n  citations: SerializableCitation[];\\n  variant?: CitationVariant;\\n  maxVisible?: number;\\n  className?: string;\\n  onNavigate?: (href: string, citation: SerializableCitation) => void;\\n}\\n\\nexport function CitationList(props: CitationListProps) {\\n  const {\\n    id,\\n    citations,\\n    variant = \\\"default\\\",\\n    maxVisible,\\n    className,\\n    onNavigate,\\n  } = props;\\n\\n  const shouldTruncate =\\n    maxVisible !== undefined && citations.length > maxVisible;\\n  const visibleCitations = shouldTruncate\\n    ? citations.slice(0, maxVisible)\\n    : citations;\\n  const overflowCitations = shouldTruncate ? citations.slice(maxVisible) : [];\\n  const overflowCount = overflowCitations.length;\\n\\n  const wrapperClass =\\n    variant === \\\"inline\\\"\\n      ? \\\"flex flex-wrap items-center gap-1.5\\\"\\n      : \\\"flex flex-col gap-2\\\";\\n\\n  // Stacked variant: overlapping favicons with popover\\n  if (variant === \\\"stacked\\\") {\\n    return (\\n      <StackedCitations\\n        id={id}\\n        citations={citations}\\n        className={className}\\n        onNavigate={onNavigate}\\n      />\\n    );\\n  }\\n\\n  if (variant === \\\"default\\\") {\\n    return (\\n      <div\\n        className={cn(\\\"isolate flex flex-col gap-4\\\", className)}\\n        data-tool-ui-id={id}\\n        data-slot=\\\"citation-list\\\"\\n      >\\n        {visibleCitations.map((citation) => (\\n          <Citation\\n            key={citation.id}\\n            {...citation}\\n            variant=\\\"default\\\"\\n            onNavigate={onNavigate}\\n          />\\n        ))}\\n        {shouldTruncate && (\\n          <OverflowIndicator\\n            citations={overflowCitations}\\n            count={overflowCount}\\n            variant=\\\"default\\\"\\n            onNavigate={onNavigate}\\n          />\\n        )}\\n      </div>\\n    );\\n  }\\n\\n  return (\\n    <div\\n      className={cn(\\\"isolate\\\", wrapperClass, className)}\\n      data-tool-ui-id={id}\\n      data-slot=\\\"citation-list\\\"\\n    >\\n      {visibleCitations.map((citation) => (\\n        <Citation\\n          key={citation.id}\\n          {...citation}\\n          variant={variant}\\n          onNavigate={onNavigate}\\n        />\\n      ))}\\n      {shouldTruncate && (\\n        <OverflowIndicator\\n          citations={overflowCitations}\\n          count={overflowCount}\\n          variant={variant}\\n          onNavigate={onNavigate}\\n        />\\n      )}\\n    </div>\\n  );\\n}\\n\\ninterface OverflowIndicatorProps {\\n  citations: SerializableCitation[];\\n  count: number;\\n  variant: CitationVariant;\\n  onNavigate?: (href: string, citation: SerializableCitation) => void;\\n}\\n\\nfunction OverflowIndicator({\\n  citations,\\n  count,\\n  variant,\\n  onNavigate,\\n}: OverflowIndicatorProps) {\\n  const { open, handleMouseEnter, handleMouseLeave } = useHoverPopover();\\n\\n  const handleClick = (citation: SerializableCitation) => {\\n    const href = resolveSafeNavigationHref(citation.href);\\n    if (!href) return;\\n    if (onNavigate) {\\n      onNavigate(href, citation);\\n    } else {\\n      openSafeNavigationHref(href);\\n    }\\n  };\\n\\n  const popoverContent = (\\n    <div className=\\\"flex max-h-72 flex-col overflow-y-auto\\\">\\n      {citations.map((citation) => (\\n        <OverflowItem\\n          key={citation.id}\\n          citation={citation}\\n          onClick={() => handleClick(citation)}\\n        />\\n      ))}\\n    </div>\\n  );\\n\\n  if (variant === \\\"inline\\\") {\\n    return (\\n      <Popover open={open}>\\n        <PopoverTrigger asChild>\\n          <button\\n            type=\\\"button\\\"\\n            onMouseEnter={handleMouseEnter}\\n            onMouseLeave={handleMouseLeave}\\n            className={cn(\\n              \\\"inline-flex items-center gap-1 rounded-md px-2 py-1\\\",\\n              \\\"bg-muted/60 text-sm tabular-nums\\\",\\n              \\\"transition-colors duration-150\\\",\\n              \\\"hover:bg-muted\\\",\\n              \\\"focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none\\\",\\n            )}\\n          >\\n            <span className=\\\"text-muted-foreground\\\">+{count} more</span>\\n          </button>\\n        </PopoverTrigger>\\n        <PopoverContent\\n          side=\\\"top\\\"\\n          align=\\\"start\\\"\\n          className=\\\"w-80 p-1\\\"\\n          onMouseEnter={handleMouseEnter}\\n          onMouseLeave={handleMouseLeave}\\n          onOpenAutoFocus={(e) => e.preventDefault()}\\n        >\\n          {popoverContent}\\n        </PopoverContent>\\n      </Popover>\\n    );\\n  }\\n\\n  // Default variant\\n  return (\\n    <Popover open={open}>\\n      <PopoverTrigger asChild>\\n        <button\\n          type=\\\"button\\\"\\n          onMouseEnter={handleMouseEnter}\\n          onMouseLeave={handleMouseLeave}\\n          className={cn(\\n            \\\"flex items-center justify-center rounded-xl px-4 py-3\\\",\\n            \\\"border-border bg-card border border-dashed\\\",\\n            \\\"transition-colors duration-150\\\",\\n            \\\"hover:border-foreground/25 hover:bg-muted/50\\\",\\n            \\\"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none\\\",\\n          )}\\n        >\\n          <span className=\\\"text-muted-foreground text-sm tabular-nums\\\">\\n            +{count} more sources\\n          </span>\\n        </button>\\n      </PopoverTrigger>\\n      <PopoverContent\\n        side=\\\"bottom\\\"\\n        align=\\\"start\\\"\\n        className=\\\"w-80 p-1\\\"\\n        onMouseEnter={handleMouseEnter}\\n        onMouseLeave={handleMouseLeave}\\n        onOpenAutoFocus={(e) => e.preventDefault()}\\n      >\\n        {popoverContent}\\n      </PopoverContent>\\n    </Popover>\\n  );\\n}\\n\\ninterface OverflowItemProps {\\n  citation: SerializableCitation;\\n  onClick: () => void;\\n}\\n\\nfunction OverflowItem({ citation, onClick }: OverflowItemProps) {\\n  const TypeIcon = TYPE_ICONS[citation.type ?? \\\"webpage\\\"] ?? Globe;\\n\\n  return (\\n    <button\\n      type=\\\"button\\\"\\n      onClick={onClick}\\n      className=\\\"group hover:bg-muted focus-visible:bg-muted flex w-full cursor-pointer items-center gap-2.5 rounded-md px-2 py-2 text-left transition-colors focus-visible:outline-none\\\"\\n    >\\n      {citation.favicon ? (\\n        <img\\n          src={citation.favicon}\\n          alt=\\\"\\\"\\n          aria-hidden=\\\"true\\\"\\n          width={16}\\n          height={16}\\n          className=\\\"bg-muted size-4 shrink-0 rounded object-cover\\\"\\n        />\\n      ) : (\\n        <TypeIcon\\n          className=\\\"text-muted-foreground size-4 shrink-0\\\"\\n          aria-hidden=\\\"true\\\"\\n        />\\n      )}\\n      <div className=\\\"min-w-0 flex-1\\\">\\n        <p className=\\\"group-hover:decoration-foreground/30 truncate text-sm font-medium group-hover:underline group-hover:underline-offset-2\\\">\\n          {citation.title}\\n        </p>\\n        <p className=\\\"text-muted-foreground truncate text-xs\\\">\\n          {citation.domain}\\n        </p>\\n      </div>\\n      <ExternalLink className=\\\"text-muted-foreground mt-0.5 size-3.5 shrink-0 self-start opacity-0 transition-opacity group-hover:opacity-100\\\" />\\n    </button>\\n  );\\n}\\n\\ninterface StackedCitationsProps {\\n  id: string;\\n  citations: SerializableCitation[];\\n  className?: string;\\n  onNavigate?: (href: string, citation: SerializableCitation) => void;\\n}\\n\\nfunction StackedCitations({\\n  id,\\n  citations,\\n  className,\\n  onNavigate,\\n}: StackedCitationsProps) {\\n  const {\\n    open,\\n    setOpen,\\n    containerRef,\\n    handleMouseEnter,\\n    handleMouseLeave,\\n    handleBlur,\\n  } = useHoverPopover();\\n  const maxIcons = 4;\\n  const visibleCitations = citations.slice(0, maxIcons);\\n  const remainingCount = Math.max(0, citations.length - maxIcons);\\n\\n  const handleClick = (citation: SerializableCitation) => {\\n    const href = resolveSafeNavigationHref(citation.href);\\n    if (!href) return;\\n    if (onNavigate) {\\n      onNavigate(href, citation);\\n    } else {\\n      openSafeNavigationHref(href);\\n    }\\n  };\\n\\n  return (\\n    <div ref={containerRef} onBlur={handleBlur} className=\\\"inline-flex\\\">\\n      <Popover open={open}>\\n        <PopoverTrigger asChild>\\n          <button\\n            type=\\\"button\\\"\\n            data-tool-ui-id={id}\\n            data-slot=\\\"citation-list\\\"\\n            onMouseEnter={handleMouseEnter}\\n            onMouseLeave={handleMouseLeave}\\n            onKeyDown={(e) => {\\n              if (e.key === \\\"Enter\\\" || e.key === \\\" \\\") {\\n                e.preventDefault();\\n                setOpen(true);\\n              }\\n            }}\\n            className={cn(\\n              \\\"isolate inline-flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2\\\",\\n              \\\"bg-muted/40 outline-none\\\",\\n              \\\"transition-colors duration-150\\\",\\n              \\\"hover:bg-muted/70\\\",\\n              \\\"focus-visible:ring-ring focus-visible:ring-2\\\",\\n              className,\\n            )}\\n          >\\n            <div className=\\\"flex items-center\\\">\\n              {visibleCitations.map((citation, index) => {\\n                const TypeIcon =\\n                  TYPE_ICONS[citation.type ?? \\\"webpage\\\"] ?? Globe;\\n                return (\\n                  <div\\n                    key={citation.id}\\n                    className={cn(\\n                      \\\"border-border bg-background dark:border-foreground/20 relative flex size-6 items-center justify-center rounded-full border shadow-xs\\\",\\n                      index > 0 && \\\"-ml-2\\\",\\n                    )}\\n                    style={{ zIndex: maxIcons - index }}\\n                  >\\n                    {citation.favicon ? (\\n                      <img\\n                        src={citation.favicon}\\n                        alt=\\\"\\\"\\n                        aria-hidden=\\\"true\\\"\\n                        width={18}\\n                        height={18}\\n                        className=\\\"size-4.5 rounded-full object-cover\\\"\\n                      />\\n                    ) : (\\n                      <TypeIcon\\n                        className=\\\"text-muted-foreground size-3\\\"\\n                        aria-hidden=\\\"true\\\"\\n                      />\\n                    )}\\n                  </div>\\n                );\\n              })}\\n              {remainingCount > 0 && (\\n                <div\\n                  className=\\\"border-border bg-background dark:border-foreground/20 relative -ml-2 flex size-6 items-center justify-center rounded-full border shadow-xs\\\"\\n                  style={{ zIndex: 0 }}\\n                >\\n                  <span className=\\\"text-muted-foreground text-[10px] font-medium tracking-tight\\\">\\n                    •••\\n                  </span>\\n                </div>\\n              )}\\n            </div>\\n            <span className=\\\"text-muted-foreground text-sm tabular-nums\\\">\\n              {citations.length} source{citations.length !== 1 && \\\"s\\\"}\\n            </span>\\n          </button>\\n        </PopoverTrigger>\\n        <PopoverContent\\n          side=\\\"bottom\\\"\\n          align=\\\"start\\\"\\n          className=\\\"w-80 p-1\\\"\\n          onMouseEnter={handleMouseEnter}\\n          onMouseLeave={handleMouseLeave}\\n          onBlur={handleBlur}\\n          onEscapeKeyDown={() => setOpen(false)}\\n        >\\n          <div className=\\\"flex max-h-72 flex-col overflow-y-auto\\\">\\n            {citations.map((citation) => (\\n              <OverflowItem\\n                key={citation.id}\\n                citation={citation}\\n                onClick={() => handleClick(citation)}\\n              />\\n            ))}\\n          </div>\\n        </PopoverContent>\\n      </Popover>\\n    </div>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/citation/citation.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/citation/citation.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport * as React from \\\"react\\\";\\nimport type { LucideIcon } from \\\"lucide-react\\\";\\nimport {\\n  FileText,\\n  Globe,\\n  Code2,\\n  Newspaper,\\n  Database,\\n  File,\\n  ExternalLink,\\n} from \\\"lucide-react\\\";\\nimport { cn, Popover, PopoverContent, PopoverTrigger } from \\\"./_adapter\\\";\\n\\nimport { openSafeNavigationHref, sanitizeHref } from \\\"../shared/media\\\";\\nimport type {\\n  SerializableCitation,\\n  CitationType,\\n  CitationVariant,\\n} from \\\"./schema\\\";\\n\\nconst FALLBACK_LOCALE = \\\"en-US\\\";\\n\\nconst TYPE_ICONS: Record<CitationType, LucideIcon> = {\\n  webpage: Globe,\\n  document: FileText,\\n  article: Newspaper,\\n  api: Database,\\n  code: Code2,\\n  other: File,\\n};\\n\\nfunction extractDomain(url: string): string | undefined {\\n  try {\\n    const urlObj = new URL(url);\\n    return urlObj.hostname.replace(/^www\\\\./, \\\"\\\");\\n  } catch {\\n    return undefined;\\n  }\\n}\\n\\nfunction formatDate(isoString: string, locale: string): string {\\n  try {\\n    const date = new Date(isoString);\\n    return date.toLocaleDateString(locale, {\\n      year: \\\"numeric\\\",\\n      month: \\\"short\\\",\\n    });\\n  } catch {\\n    return isoString;\\n  }\\n}\\n\\nfunction useHoverPopover(delay = 100) {\\n  const [open, setOpen] = React.useState(false);\\n  const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\\n\\n  const handleMouseEnter = React.useCallback(() => {\\n    if (timeoutRef.current) clearTimeout(timeoutRef.current);\\n    timeoutRef.current = setTimeout(() => setOpen(true), delay);\\n  }, [delay]);\\n\\n  const handleMouseLeave = React.useCallback(() => {\\n    if (timeoutRef.current) clearTimeout(timeoutRef.current);\\n    timeoutRef.current = setTimeout(() => setOpen(false), delay);\\n  }, [delay]);\\n\\n  React.useEffect(() => {\\n    return () => {\\n      if (timeoutRef.current) clearTimeout(timeoutRef.current);\\n    };\\n  }, []);\\n\\n  return { open, setOpen, handleMouseEnter, handleMouseLeave };\\n}\\n\\nexport interface CitationProps extends SerializableCitation {\\n  variant?: CitationVariant;\\n  className?: string;\\n  onNavigate?: (href: string, citation: SerializableCitation) => void;\\n}\\n\\nexport function Citation(props: CitationProps) {\\n  const { variant = \\\"default\\\", className, onNavigate, ...serializable } = props;\\n\\n  const {\\n    id,\\n    href: rawHref,\\n    title,\\n    snippet,\\n    domain: providedDomain,\\n    favicon,\\n    author,\\n    publishedAt,\\n    type = \\\"webpage\\\",\\n    locale: providedLocale,\\n  } = serializable;\\n\\n  const locale = providedLocale ?? FALLBACK_LOCALE;\\n  const sanitizedHref = sanitizeHref(rawHref);\\n  const domain = providedDomain ?? extractDomain(rawHref);\\n\\n  const citationData: SerializableCitation = {\\n    ...serializable,\\n    href: sanitizedHref ?? rawHref,\\n    domain,\\n    locale,\\n  };\\n\\n  const TypeIcon = TYPE_ICONS[type] ?? Globe;\\n\\n  const handleClick = () => {\\n    if (!sanitizedHref) return;\\n    if (onNavigate) {\\n      onNavigate(sanitizedHref, citationData);\\n    } else {\\n      openSafeNavigationHref(sanitizedHref);\\n    }\\n  };\\n\\n  const handleKeyDown = (e: React.KeyboardEvent) => {\\n    if (sanitizedHref && (e.key === \\\"Enter\\\" || e.key === \\\" \\\")) {\\n      e.preventDefault();\\n      handleClick();\\n    }\\n  };\\n\\n  const iconElement = favicon ? (\\n    <img\\n      src={favicon}\\n      alt=\\\"\\\"\\n      aria-hidden=\\\"true\\\"\\n      width={14}\\n      height={14}\\n      className=\\\"bg-muted size-3.5 shrink-0 rounded object-cover\\\"\\n    />\\n  ) : (\\n    <TypeIcon className=\\\"size-3.5 shrink-0 opacity-60\\\" aria-hidden=\\\"true\\\" />\\n  );\\n\\n  const { open, handleMouseEnter, handleMouseLeave } = useHoverPopover();\\n\\n  // Inline variant: compact chip with hover popover\\n  if (variant === \\\"inline\\\") {\\n    return (\\n      <Popover open={open}>\\n        <PopoverTrigger asChild>\\n          <button\\n            type=\\\"button\\\"\\n            aria-label={title}\\n            data-tool-ui-id={id}\\n            data-slot=\\\"citation\\\"\\n            onClick={handleClick}\\n            onMouseEnter={handleMouseEnter}\\n            onMouseLeave={handleMouseLeave}\\n            className={cn(\\n              \\\"inline-flex cursor-pointer items-center gap-1.5 rounded-md px-2 py-1\\\",\\n              \\\"bg-muted/60 text-sm outline-none\\\",\\n              \\\"transition-colors duration-150\\\",\\n              \\\"hover:bg-muted\\\",\\n              \\\"focus-visible:ring-ring focus-visible:ring-2\\\",\\n              className,\\n            )}\\n          >\\n            {iconElement}\\n            <span className=\\\"text-muted-foreground\\\">{domain}</span>\\n          </button>\\n        </PopoverTrigger>\\n        <PopoverContent\\n          side=\\\"top\\\"\\n          align=\\\"start\\\"\\n          className=\\\"w-72 cursor-pointer p-0\\\"\\n          onMouseEnter={handleMouseEnter}\\n          onMouseLeave={handleMouseLeave}\\n          onOpenAutoFocus={(e) => e.preventDefault()}\\n          onCloseAutoFocus={(e) => e.preventDefault()}\\n          onClick={handleClick}\\n        >\\n          <div className=\\\"hover:bg-muted/50 flex flex-col gap-2 p-3 transition-colors\\\">\\n            <div className=\\\"flex items-start gap-2\\\">\\n              {iconElement}\\n              <span className=\\\"text-muted-foreground text-xs\\\">{domain}</span>\\n            </div>\\n            <p className=\\\"text-sm leading-snug font-medium\\\">{title}</p>\\n            {snippet && (\\n              <p className=\\\"text-muted-foreground line-clamp-2 text-xs leading-relaxed\\\">\\n                {snippet}\\n              </p>\\n            )}\\n          </div>\\n        </PopoverContent>\\n      </Popover>\\n    );\\n  }\\n\\n  // Default variant: full card\\n  return (\\n    <article\\n      className={cn(\\\"relative w-full max-w-md min-w-72\\\", className)}\\n      lang={locale}\\n      data-tool-ui-id={id}\\n      data-slot=\\\"citation\\\"\\n    >\\n      <div\\n        className={cn(\\n          \\\"group @container relative isolate flex w-full min-w-0 flex-col overflow-hidden rounded-xl\\\",\\n          \\\"border-border bg-card border text-sm shadow-xs\\\",\\n          \\\"transition-colors duration-150\\\",\\n          sanitizedHref && [\\n            \\\"cursor-pointer\\\",\\n            \\\"hover:border-foreground/25\\\",\\n            \\\"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none\\\",\\n          ],\\n        )}\\n        onClick={sanitizedHref ? handleClick : undefined}\\n        role={sanitizedHref ? \\\"link\\\" : undefined}\\n        tabIndex={sanitizedHref ? 0 : undefined}\\n        onKeyDown={handleKeyDown}\\n      >\\n        <div className=\\\"flex flex-col gap-2 p-4\\\">\\n          <div className=\\\"text-muted-foreground flex min-w-0 items-center justify-between gap-1.5 text-xs\\\">\\n            <div className=\\\"flex min-w-0 items-center gap-1.5\\\">\\n              {iconElement}\\n              <span className=\\\"truncate font-medium\\\">{domain}</span>\\n              {(author || publishedAt) && (\\n                <span className=\\\"opacity-70\\\">\\n                  <span className=\\\"opacity-60\\\"> — </span>\\n                  {author}\\n                  {author && publishedAt && \\\", \\\"}\\n                  {publishedAt && (\\n                    <time dateTime={publishedAt} className=\\\"tabular-nums\\\">\\n                      {formatDate(publishedAt, locale)}\\n                    </time>\\n                  )}\\n                </span>\\n              )}\\n            </div>\\n            {sanitizedHref && (\\n              <ExternalLink className=\\\"size-3.5 shrink-0 opacity-0 transition-opacity group-hover:opacity-100\\\" />\\n            )}\\n          </div>\\n\\n          <h3 className=\\\"text-foreground text-[15px] leading-snug font-medium text-pretty\\\">\\n            <span className=\\\"group-hover:decoration-foreground/30 line-clamp-2 group-hover:underline group-hover:underline-offset-2\\\">\\n              {title}\\n            </span>\\n          </h3>\\n\\n          {snippet && (\\n            <p className=\\\"text-muted-foreground text-[13px] leading-relaxed text-pretty\\\">\\n              <span className=\\\"line-clamp-3\\\">{snippet}</span>\\n            </p>\\n          )}\\n        </div>\\n      </div>\\n    </article>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/citation/index.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/citation/index.ts\",\n      \"content\": \"export { Citation } from \\\"./citation\\\";\\nexport type { CitationProps } from \\\"./citation\\\";\\nexport { CitationList } from \\\"./citation-list\\\";\\nexport type { CitationListProps } from \\\"./citation-list\\\";\\nexport type {\\n  SerializableCitation,\\n  CitationType,\\n  CitationVariant,\\n} from \\\"./schema\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/citation/README.md\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/citation/README.md\",\n      \"content\": \"# Citation\\n\\nImplementation for the \\\"citation\\\" Tool UI surface.\\n\\n## Files\\n\\n- public exports: components/tool-ui/citation/index.ts\\n- serializable schema + parse helpers: components/tool-ui/citation/schema.ts\\n\\n## Companion assets\\n\\n- Docs page: app/docs/citation/content.mdx\\n- Preset payload: lib/presets/citation.ts\\n\\n## Quick check\\n\\nRun this after edits:\\n\\npnpm test\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/citation/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/citation/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\nimport {\\n  ToolUIIdSchema,\\n  ToolUIReceiptSchema,\\n  ToolUIRoleSchema,\\n} from \\\"../shared/schema\\\";\\n\\nexport const CitationTypeSchema = z.enum([\\n  \\\"webpage\\\",\\n  \\\"document\\\",\\n  \\\"article\\\",\\n  \\\"api\\\",\\n  \\\"code\\\",\\n  \\\"other\\\",\\n]);\\n\\nexport type CitationType = z.infer<typeof CitationTypeSchema>;\\n\\nexport const CitationVariantSchema = z.enum([\\\"default\\\", \\\"inline\\\", \\\"stacked\\\"]);\\n\\nexport type CitationVariant = z.infer<typeof CitationVariantSchema>;\\n\\nexport const SerializableCitationSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n  href: z.string().url(),\\n  title: z.string(),\\n  snippet: z.string().optional(),\\n  domain: z.string().optional(),\\n  favicon: z.string().url().optional(),\\n  author: z.string().optional(),\\n  publishedAt: z.string().datetime().optional(),\\n  type: CitationTypeSchema.optional(),\\n  locale: z.string().optional(),\\n});\\n\\nexport type SerializableCitation = z.infer<typeof SerializableCitationSchema>;\\n\\nconst SerializableCitationSchemaContract = defineToolUiContract(\\n  \\\"Citation\\\",\\n  SerializableCitationSchema,\\n);\\n\\nexport const parseSerializableCitation: (\\n  input: unknown,\\n) => SerializableCitation = SerializableCitationSchemaContract.parse;\\n\\nexport const safeParseSerializableCitation: (\\n  input: unknown,\\n) => SerializableCitation | null = SerializableCitationSchemaContract.safeParse;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/aspect-ratio.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/aspect-ratio.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nexport const AspectRatioSchema = z\\n  .enum([\\\"auto\\\", \\\"1:1\\\", \\\"4:3\\\", \\\"16:9\\\", \\\"9:16\\\"])\\n  .default(\\\"auto\\\");\\n\\nexport type AspectRatio = z.infer<typeof AspectRatioSchema>;\\n\\nexport const MediaFitSchema = z.enum([\\\"cover\\\", \\\"contain\\\"]).default(\\\"cover\\\");\\n\\nexport type MediaFit = z.infer<typeof MediaFitSchema>;\\n\\nexport const RATIO_CLASS_MAP: Record<AspectRatio, string> = {\\n  auto: \\\"\\\",\\n  \\\"1:1\\\": \\\"aspect-square\\\",\\n  \\\"4:3\\\": \\\"aspect-[4/3]\\\",\\n  \\\"16:9\\\": \\\"aspect-video\\\",\\n  \\\"9:16\\\": \\\"aspect-[9/16]\\\",\\n};\\n\\nexport function getRatioClass(ratio: AspectRatio): string {\\n  return RATIO_CLASS_MAP[ratio];\\n}\\n\\nexport function getFitClass(fit: MediaFit): string {\\n  return fit === \\\"cover\\\" ? \\\"object-cover\\\" : \\\"object-contain\\\";\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/format-utils.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/format-utils.ts\",\n      \"content\": \"/**\\n * Format duration in milliseconds to human-readable string.\\n * @example formatDuration(128000) => \\\"2:08\\\"\\n * @example formatDuration(3661000) => \\\"1:01:01\\\"\\n */\\nexport function formatDuration(durationMs: number): string {\\n  const totalSeconds = Math.round(durationMs / 1000);\\n  const hours = Math.floor(totalSeconds / 3600);\\n  const minutes = Math.floor((totalSeconds % 3600) / 60);\\n  const seconds = totalSeconds % 60;\\n\\n  if (hours > 0) {\\n    return `${hours}:${minutes.toString().padStart(2, \\\"0\\\")}:${seconds\\n      .toString()\\n      .padStart(2, \\\"0\\\")}`;\\n  }\\n  return `${minutes}:${seconds.toString().padStart(2, \\\"0\\\")}`;\\n}\\n\\n/**\\n * Format file size in bytes to human-readable string.\\n * @example formatFileSize(1024) => \\\"1 KB\\\"\\n * @example formatFileSize(1536000) => \\\"1.5 MB\\\"\\n */\\nexport function formatFileSize(bytes: number): string {\\n  if (bytes < 1024) return `${bytes} B`;\\n  const units = [\\\"KB\\\", \\\"MB\\\", \\\"GB\\\"];\\n  let size = bytes / 1024;\\n  let unit = 0;\\n  while (size >= 1024 && unit < units.length - 1) {\\n    size /= 1024;\\n    unit += 1;\\n  }\\n  return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unit]}`;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/index.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/index.ts\",\n      \"content\": \"export {\\n  AspectRatioSchema,\\n  MediaFitSchema,\\n  RATIO_CLASS_MAP,\\n  getRatioClass,\\n  getFitClass,\\n  type AspectRatio,\\n  type MediaFit,\\n} from \\\"./aspect-ratio\\\";\\n\\nexport { OVERLAY_GRADIENT } from \\\"./overlay-gradient\\\";\\n\\nexport { formatDuration, formatFileSize } from \\\"./format-utils\\\";\\n\\nexport { sanitizeHref } from \\\"./sanitize-href\\\";\\nexport {\\n  resolveSafeNavigationHref,\\n  openSafeNavigationHref,\\n} from \\\"./safe-navigation\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/overlay-gradient.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/overlay-gradient.ts\",\n      \"content\": \"/**\\n * Eased gradient for hover overlays on media elements.\\n * Creates a smooth fade from opaque black at top to transparent.\\n *\\n * @see https://larsenwork.com/easing-gradients/\\n */\\nexport const OVERLAY_GRADIENT = `linear-gradient(\\n  to bottom,\\n  hsl(0, 0%, 0%) 0%,\\n  hsla(0, 0%, 0%, 0.987) 8.3%,\\n  hsla(0, 0%, 0%, 0.951) 16.6%,\\n  hsla(0, 0%, 0%, 0.896) 24.6%,\\n  hsla(0, 0%, 0%, 0.825) 32.5%,\\n  hsla(0, 0%, 0%, 0.741) 40.1%,\\n  hsla(0, 0%, 0%, 0.648) 47.6%,\\n  hsla(0, 0%, 0%, 0.55) 54.8%,\\n  hsla(0, 0%, 0%, 0.45) 61.7%,\\n  hsla(0, 0%, 0%, 0.352) 68.3%,\\n  hsla(0, 0%, 0%, 0.259) 74.5%,\\n  hsla(0, 0%, 0%, 0.175) 80.4%,\\n  hsla(0, 0%, 0%, 0.104) 86%,\\n  hsla(0, 0%, 0%, 0.049) 91.1%,\\n  hsla(0, 0%, 0%, 0.013) 95.8%,\\n  hsla(0, 0%, 0%, 0) 100%\\n)` as const;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/safe-navigation.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/safe-navigation.ts\",\n      \"content\": \"import { sanitizeHref } from \\\"./sanitize-href\\\";\\n\\nexport function resolveSafeNavigationHref(\\n  ...candidates: Array<string | null | undefined>\\n): string | undefined {\\n  for (const candidate of candidates) {\\n    const safeHref = sanitizeHref(candidate ?? undefined);\\n    if (safeHref) {\\n      return safeHref;\\n    }\\n  }\\n\\n  return undefined;\\n}\\n\\nexport function openSafeNavigationHref(href: string | undefined): boolean {\\n  if (!href || typeof window === \\\"undefined\\\") {\\n    return false;\\n  }\\n\\n  window.open(href, \\\"_blank\\\", \\\"noopener,noreferrer\\\");\\n  return true;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/sanitize-href.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/sanitize-href.ts\",\n      \"content\": \"/**\\n * Sanitize a URL to ensure it's safe for use in href attributes.\\n * Allows:\\n * - Absolute http(s) URLs\\n * - Relative URLs (/path, ./path, ../path, ?query, #hash)\\n *\\n * @returns The sanitized URL string, or undefined if invalid/unsafe\\n */\\nexport function sanitizeHref(href?: string): string | undefined {\\n  if (!href) return undefined;\\n  const candidate = href.trim();\\n  if (!candidate) return undefined;\\n\\n  if (\\n    candidate.startsWith(\\\"/\\\") ||\\n    candidate.startsWith(\\\"./\\\") ||\\n    candidate.startsWith(\\\"../\\\") ||\\n    candidate.startsWith(\\\"?\\\") ||\\n    candidate.startsWith(\\\"#\\\")\\n  ) {\\n    if (candidate.startsWith(\\\"//\\\")) return undefined;\\n    // eslint-disable-next-line no-control-regex -- intentionally matching control characters\\n    if (/[\\\\u0000-\\\\u001F\\\\u007F]/.test(candidate)) return undefined;\\n    return candidate;\\n  }\\n\\n  try {\\n    const url = new URL(candidate);\\n    if (url.protocol === \\\"http:\\\" || url.protocol === \\\"https:\\\") {\\n      return url.toString();\\n    }\\n  } catch {\\n    return undefined;\\n  }\\n  return undefined;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { ReactNode } from \\\"react\\\";\\n\\n/**\\n * Tool UI conventions:\\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\\n * - Schema: `SerializableXSchema`\\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\\n * - Root attrs: `data-tool-ui-id` + `data-slot`\\n */\\n\\n/**\\n * Schema for tool UI identity.\\n *\\n * Every tool UI should have a unique identifier that:\\n * - Is stable across re-renders\\n * - Is meaningful (not auto-generated)\\n * - Is unique within the conversation\\n *\\n * Format recommendation: `{component-type}-{semantic-identifier}`\\n * Examples: \\\"data-table-expenses-q3\\\", \\\"option-list-deploy-target\\\"\\n */\\nexport const ToolUIIdSchema = z.string().min(1);\\n\\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\\n\\n/**\\n * Primary role of a Tool UI surface in a chat context.\\n */\\nexport const ToolUIRoleSchema = z.enum([\\n  \\\"information\\\",\\n  \\\"decision\\\",\\n  \\\"control\\\",\\n  \\\"state\\\",\\n  \\\"composite\\\",\\n]);\\n\\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\\n\\nexport const ToolUIReceiptOutcomeSchema = z.enum([\\n  \\\"success\\\",\\n  \\\"partial\\\",\\n  \\\"failed\\\",\\n  \\\"cancelled\\\",\\n]);\\n\\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\\n\\n/**\\n * Optional receipt metadata: a durable summary of an outcome.\\n */\\nexport const ToolUIReceiptSchema = z.object({\\n  outcome: ToolUIReceiptOutcomeSchema,\\n  summary: z.string().min(1),\\n  identifiers: z.record(z.string(), z.string()).optional(),\\n  at: z.string().datetime(),\\n});\\n\\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\\n\\n/**\\n * Base schema for Tool UI payloads (id + optional role/receipt).\\n */\\nexport const ToolUISurfaceSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n});\\n\\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\\n\\nexport const ActionSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  /**\\n   * Canonical narration the assistant can use after this action is taken.\\n   *\\n   * Example: \\\"I exported the table as CSV.\\\" / \\\"I opened the link in a new tab.\\\"\\n   */\\n  sentence: z.string().optional(),\\n  confirmLabel: z.string().optional(),\\n  variant: z\\n    .enum([\\\"default\\\", \\\"destructive\\\", \\\"secondary\\\", \\\"ghost\\\", \\\"outline\\\"])\\n    .optional(),\\n  icon: z.custom<ReactNode>().optional(),\\n  loading: z.boolean().optional(),\\n  disabled: z.boolean().optional(),\\n  shortcut: z.string().optional(),\\n});\\n\\nexport type Action = z.infer<typeof ActionSchema>;\\nexport type LocalAction = Action;\\nexport type DecisionAction = Action;\\n\\nexport const DecisionResultSchema = z.object({\\n  kind: z.literal(\\\"decision\\\"),\\n  version: z.literal(1),\\n  decisionId: z.string().min(1),\\n  actionId: z.string().min(1),\\n  actionLabel: z.string().min(1),\\n  at: z.string().datetime(),\\n  payload: z.record(z.string(), z.unknown()).optional(),\\n});\\n\\nexport type DecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n> = Omit<z.infer<typeof DecisionResultSchema>, \\\"payload\\\"> & {\\n  payload?: TPayload;\\n};\\n\\nexport function createDecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n>(args: {\\n  decisionId: string;\\n  action: { id: string; label: string };\\n  payload?: TPayload;\\n}): DecisionResult<TPayload> {\\n  return {\\n    kind: \\\"decision\\\",\\n    version: 1,\\n    decisionId: args.decisionId,\\n    actionId: args.action.id,\\n    actionLabel: args.action.label,\\n    at: new Date().toISOString(),\\n    payload: args.payload,\\n  };\\n}\\n\\nexport const ActionButtonsPropsSchema = z.object({\\n  actions: z.array(ActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n  className: z.string().optional(),\\n});\\n\\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\\n  actions: z.array(SerializableActionSchema),\\n}).omit({ className: true });\\n\\nexport interface ActionsConfig {\\n  items: Action[];\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  confirmTimeout?: number;\\n}\\n\\nexport const SerializableActionsConfigSchema = z.object({\\n  items: z.array(SerializableActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n});\\n\\nexport type SerializableActionsConfig = z.infer<\\n  typeof SerializableActionsConfigSchema\\n>;\\n\\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/code-block.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"code-block\",\n  \"type\": \"registry:block\",\n  \"title\": \"Code Block\",\n  \"description\": \"Display syntax-highlighted code snippets.\",\n  \"dependencies\": [\n    \"lucide-react\",\n    \"shiki\",\n    \"zod\"\n  ],\n  \"registryDependencies\": [\n    \"button\",\n    \"collapsible\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/code-block/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/code-block/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn          → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n *   Button      → shadcn/ui Button\\n *   Collapsible → shadcn/ui Collapsible\\n */\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport { Button } from \\\"@/components/ui/button\\\";\\nexport { Collapsible, CollapsibleTrigger } from \\\"@/components/ui/collapsible\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/code-block/code-block.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/code-block/code-block.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport {\\n  useState,\\n  useCallback,\\n  useEffect,\\n  createContext,\\n  use,\\n  type ReactNode,\\n} from \\\"react\\\";\\nimport {\\n  createHighlighter,\\n  createJavaScriptRegexEngine,\\n  type Highlighter,\\n} from \\\"shiki\\\";\\nimport { Copy, Check, ChevronDown, ChevronUp } from \\\"lucide-react\\\";\\nimport pierreDarkTheme from \\\"../shared/pierre-dark-theme.js\\\";\\nimport pierreLightTheme from \\\"../shared/pierre-light-theme.js\\\";\\nimport type { CodeBlockLineNumbersMode, CodeBlockProps } from \\\"./schema\\\";\\nimport { useCopyToClipboard } from \\\"../shared/use-copy-to-clipboard\\\";\\n\\nimport { Button, cn, Collapsible, CollapsibleTrigger } from \\\"./_adapter\\\";\\n\\nconst COPY_ID = \\\"codeblock-code\\\";\\nconst MAX_HTML_CACHE_ENTRIES = 64;\\n\\nlet highlighterPromise: Promise<Highlighter> | null = null;\\n\\nfunction getHighlighter(): Promise<Highlighter> {\\n  if (!highlighterPromise) {\\n    highlighterPromise = createHighlighter({\\n      themes: [pierreDarkTheme as never, pierreLightTheme as never],\\n      langs: [],\\n      engine: createJavaScriptRegexEngine(),\\n    });\\n  }\\n  return highlighterPromise;\\n}\\n\\nconst htmlCache = new Map<string, string>();\\n\\nfunction getCacheKey(\\n  code: string,\\n  language: string,\\n  theme: string,\\n  lineNumbers: CodeBlockLineNumbersMode,\\n  highlightLines?: number[],\\n): string {\\n  return JSON.stringify({\\n    code,\\n    language,\\n    theme,\\n    lineNumbers,\\n    highlightLines: highlightLines ?? null,\\n  });\\n}\\n\\nfunction setCachedHtml(cacheKey: string, html: string): void {\\n  if (htmlCache.has(cacheKey)) {\\n    htmlCache.set(cacheKey, html);\\n    return;\\n  }\\n\\n  if (htmlCache.size >= MAX_HTML_CACHE_ENTRIES) {\\n    const oldestKey = htmlCache.keys().next().value;\\n    if (typeof oldestKey === \\\"string\\\") {\\n      htmlCache.delete(oldestKey);\\n    }\\n  }\\n\\n  htmlCache.set(cacheKey, html);\\n}\\n\\nconst LANGUAGE_DISPLAY_NAMES: Record<string, string> = {\\n  typescript: \\\"TypeScript\\\",\\n  javascript: \\\"JavaScript\\\",\\n  python: \\\"Python\\\",\\n  tsx: \\\"TSX\\\",\\n  jsx: \\\"JSX\\\",\\n  json: \\\"JSON\\\",\\n  bash: \\\"Bash\\\",\\n  shell: \\\"Shell\\\",\\n  css: \\\"CSS\\\",\\n  html: \\\"HTML\\\",\\n  markdown: \\\"Markdown\\\",\\n  sql: \\\"SQL\\\",\\n  yaml: \\\"YAML\\\",\\n  go: \\\"Go\\\",\\n  rust: \\\"Rust\\\",\\n  text: \\\"Plain Text\\\",\\n};\\n\\nfunction getLanguageDisplayName(lang: string): string {\\n  return LANGUAGE_DISPLAY_NAMES[lang.toLowerCase()] || lang.toUpperCase();\\n}\\n\\nfunction getSystemTheme(): \\\"light\\\" | \\\"dark\\\" {\\n  if (typeof window === \\\"undefined\\\") return \\\"light\\\";\\n  return window.matchMedia?.(\\\"(prefers-color-scheme: dark)\\\").matches\\n    ? \\\"dark\\\"\\n    : \\\"light\\\";\\n}\\n\\nfunction getDocumentTheme(): \\\"light\\\" | \\\"dark\\\" | null {\\n  if (typeof document === \\\"undefined\\\") return null;\\n  const root = document.documentElement;\\n  const dataTheme = root.getAttribute(\\\"data-theme\\\")?.toLowerCase();\\n  if (dataTheme === \\\"dark\\\") return \\\"dark\\\";\\n  if (dataTheme === \\\"light\\\") return \\\"light\\\";\\n  if (root.classList.contains(\\\"dark\\\")) return \\\"dark\\\";\\n  if (root.classList.contains(\\\"light\\\")) return \\\"light\\\";\\n  return null;\\n}\\n\\nfunction useResolvedTheme(): \\\"light\\\" | \\\"dark\\\" {\\n  const [theme, setTheme] = useState<\\\"light\\\" | \\\"dark\\\">(() => {\\n    return getDocumentTheme() ?? getSystemTheme();\\n  });\\n\\n  useEffect(() => {\\n    if (typeof window === \\\"undefined\\\" || typeof document === \\\"undefined\\\") {\\n      return;\\n    }\\n\\n    const update = () => setTheme(getDocumentTheme() ?? getSystemTheme());\\n\\n    const mql = window.matchMedia?.(\\\"(prefers-color-scheme: dark)\\\");\\n    mql?.addEventListener(\\\"change\\\", update);\\n\\n    const observer = new MutationObserver(update);\\n    observer.observe(document.documentElement, {\\n      attributes: true,\\n      attributeFilter: [\\\"class\\\", \\\"data-theme\\\"],\\n    });\\n\\n    return () => {\\n      mql?.removeEventListener(\\\"change\\\", update);\\n      observer.disconnect();\\n    };\\n  }, []);\\n\\n  return theme;\\n}\\n\\nexport type CodeBlockRootProps = CodeBlockProps & {\\n  children: ReactNode;\\n  expanded?: boolean;\\n  defaultExpanded?: boolean;\\n  onExpandedChange?: (expanded: boolean) => void;\\n};\\n\\ntype CodeBlockSharedState = {\\n  id: string;\\n  code: string;\\n  language: string;\\n  filename?: string;\\n  highlightedHtml: string | null;\\n  isCopied: boolean;\\n  copyCode: () => void;\\n  lineCount: number;\\n  isCollapsed: boolean;\\n  shouldCollapse: boolean;\\n  toggleExpanded: () => void;\\n};\\n\\nconst CodeBlockContext = createContext<CodeBlockSharedState | null>(null);\\n\\nfunction useCodeBlock(): CodeBlockSharedState {\\n  const context = use(CodeBlockContext);\\n  if (!context) {\\n    throw new Error(\\n      \\\"CodeBlock subcomponents must be used within <CodeBlock.Root>.\\\",\\n    );\\n  }\\n  return context;\\n}\\n\\nfunction CodeBlockRoot({\\n  id,\\n  code,\\n  language = \\\"text\\\",\\n  lineNumbers = \\\"visible\\\",\\n  filename,\\n  highlightLines,\\n  maxCollapsedLines,\\n  className,\\n  children,\\n  expanded: expandedProp,\\n  defaultExpanded = false,\\n  onExpandedChange,\\n}: CodeBlockRootProps) {\\n  const resolvedTheme = useResolvedTheme();\\n  const [expandedState, setExpandedState] = useState(defaultExpanded);\\n  const { copiedId, copy } = useCopyToClipboard();\\n  const isCopied = copiedId === COPY_ID;\\n\\n  const expanded = expandedProp ?? expandedState;\\n  const setExpanded = useCallback(\\n    (nextExpanded: boolean) => {\\n      if (expandedProp === undefined) {\\n        setExpandedState(nextExpanded);\\n      }\\n      onExpandedChange?.(nextExpanded);\\n    },\\n    [expandedProp, onExpandedChange],\\n  );\\n\\n  const theme = resolvedTheme === \\\"dark\\\" ? \\\"pierre-dark\\\" : \\\"pierre-light\\\";\\n  const cacheKey = getCacheKey(\\n    code,\\n    language,\\n    theme,\\n    lineNumbers,\\n    highlightLines,\\n  );\\n\\n  const [highlightedHtml, setHighlightedHtml] = useState<string | null>(\\n    () => htmlCache.get(cacheKey) ?? null,\\n  );\\n\\n  useEffect(() => {\\n    const cached = htmlCache.get(cacheKey);\\n    if (cached) {\\n      setHighlightedHtml(cached);\\n      return;\\n    }\\n\\n    let cancelled = false;\\n    const showLineNumbers = lineNumbers === \\\"visible\\\";\\n\\n    async function highlight() {\\n      if (!code) {\\n        if (!cancelled) setHighlightedHtml(\\\"\\\");\\n        return;\\n      }\\n\\n      try {\\n        const highlighter = await getHighlighter();\\n        const loadedLangs = highlighter.getLoadedLanguages();\\n\\n        if (!loadedLangs.includes(language)) {\\n          await highlighter.loadLanguage(\\n            language as Parameters<Highlighter[\\\"loadLanguage\\\"]>[0],\\n          );\\n        }\\n\\n        const lineCount = code.split(\\\"\\\\n\\\").length;\\n        const lineNumberWidth = `${String(lineCount).length + 0.5}ch`;\\n\\n        const html = highlighter.codeToHtml(code, {\\n          lang: language,\\n          theme,\\n          transformers: [\\n            {\\n              line(node, line) {\\n                node.properties[\\\"data-line\\\"] = line;\\n                if (highlightLines?.includes(line)) {\\n                  const highlightBg =\\n                    resolvedTheme === \\\"dark\\\"\\n                      ? \\\"rgba(255,255,255,0.1)\\\"\\n                      : \\\"rgba(0,0,0,0.05)\\\";\\n                  node.properties.style = `background:${highlightBg};`;\\n                }\\n                if (showLineNumbers) {\\n                  node.children.unshift({\\n                    type: \\\"element\\\",\\n                    tagName: \\\"span\\\",\\n                    properties: {\\n                      style: `display:inline-block;width:${lineNumberWidth};text-align:right;margin-right:1.5em;user-select:none;opacity:0.5;`,\\n                      \\\"aria-hidden\\\": \\\"true\\\",\\n                    },\\n                    children: [{ type: \\\"text\\\", value: String(line) }],\\n                  });\\n                }\\n              },\\n            },\\n          ],\\n        });\\n        if (!cancelled) {\\n          setCachedHtml(cacheKey, html);\\n          setHighlightedHtml(html);\\n        }\\n      } catch {\\n        const escaped = code\\n          .replace(/&/g, \\\"&amp;\\\")\\n          .replace(/</g, \\\"&lt;\\\")\\n          .replace(/>/g, \\\"&gt;\\\");\\n        if (!cancelled) {\\n          setHighlightedHtml(`<pre><code>${escaped}</code></pre>`);\\n        }\\n      }\\n    }\\n    void highlight();\\n    return () => {\\n      cancelled = true;\\n    };\\n  }, [\\n    cacheKey,\\n    code,\\n    language,\\n    lineNumbers,\\n    theme,\\n    highlightLines,\\n    resolvedTheme,\\n  ]);\\n\\n  const lineCount = code.split(\\\"\\\\n\\\").length;\\n  const shouldCollapse = !!maxCollapsedLines && lineCount > maxCollapsedLines;\\n  const isCollapsed = shouldCollapse && !expanded;\\n\\n  const copyCode = useCallback(() => {\\n    void copy(code, COPY_ID);\\n  }, [code, copy]);\\n\\n  const toggleExpanded = useCallback(() => {\\n    setExpanded(!expanded);\\n  }, [expanded, setExpanded]);\\n\\n  const state: CodeBlockSharedState = {\\n    id,\\n    code,\\n    language,\\n    filename,\\n    highlightedHtml,\\n    isCopied,\\n    copyCode,\\n    lineCount,\\n    shouldCollapse,\\n    isCollapsed,\\n    toggleExpanded,\\n  };\\n\\n  return (\\n    <CodeBlockContext.Provider value={state}>\\n      <div\\n        className={cn(\\n          \\\"@container flex w-full min-w-80 flex-col gap-3\\\",\\n          className,\\n        )}\\n        data-tool-ui-id={id}\\n        data-slot=\\\"code-block\\\"\\n      >\\n        <div className=\\\"border-border bg-card overflow-hidden rounded-lg border shadow-xs\\\">\\n          <Collapsible open={!isCollapsed}>{children}</Collapsible>\\n        </div>\\n      </div>\\n    </CodeBlockContext.Provider>\\n  );\\n}\\n\\nexport type CodeBlockSectionProps = {\\n  className?: string;\\n};\\n\\nfunction CodeBlockHeader({ className }: CodeBlockSectionProps) {\\n  const { language, filename, isCopied, copyCode } = useCodeBlock();\\n  return (\\n    <div\\n      className={cn(\\n        \\\"bg-card flex items-center justify-between border-b px-4 py-2\\\",\\n        className,\\n      )}\\n    >\\n      <div className=\\\"flex items-center gap-1\\\">\\n        <span className=\\\"text-muted-foreground text-sm\\\">\\n          {getLanguageDisplayName(language)}\\n        </span>\\n        {filename && (\\n          <>\\n            <span className=\\\"text-muted-foreground/50\\\">•</span>\\n            <span className=\\\"text-foreground text-sm font-medium\\\">\\n              {filename}\\n            </span>\\n          </>\\n        )}\\n      </div>\\n      <Button\\n        variant=\\\"ghost\\\"\\n        size=\\\"sm\\\"\\n        onClick={copyCode}\\n        className=\\\"h-7 w-7 p-0\\\"\\n        aria-label={isCopied ? \\\"Copied\\\" : \\\"Copy code\\\"}\\n      >\\n        {isCopied ? (\\n          <Check className=\\\"h-4 w-4 text-green-700 dark:text-green-400\\\" />\\n        ) : (\\n          <Copy className=\\\"text-muted-foreground h-4 w-4\\\" />\\n        )}\\n      </Button>\\n    </div>\\n  );\\n}\\n\\nfunction CodeBlockContent({ className }: CodeBlockSectionProps) {\\n  const { highlightedHtml, isCollapsed } = useCodeBlock();\\n  return (\\n    <div\\n      className={cn(\\n        \\\"overflow-x-auto overflow-y-clip text-[13px] leading-[1.4] [&_pre]:bg-transparent [&_pre]:py-4\\\",\\n        isCollapsed && \\\"max-h-[200px]\\\",\\n        className,\\n      )}\\n    >\\n      {highlightedHtml && (\\n        <div dangerouslySetInnerHTML={{ __html: highlightedHtml }} />\\n      )}\\n    </div>\\n  );\\n}\\n\\nfunction CodeBlockCollapseToggle({ className }: CodeBlockSectionProps) {\\n  const { shouldCollapse, isCollapsed, toggleExpanded, lineCount } =\\n    useCodeBlock();\\n\\n  if (!shouldCollapse) return null;\\n\\n  return (\\n    <CollapsibleTrigger asChild>\\n      <Button\\n        variant=\\\"ghost\\\"\\n        onClick={toggleExpanded}\\n        className={cn(\\n          \\\"text-muted-foreground w-full rounded-none border-t font-normal\\\",\\n          className,\\n        )}\\n      >\\n        {isCollapsed ? (\\n          <>\\n            <ChevronDown className=\\\"mr-1 size-4\\\" />\\n            Show all {lineCount} lines\\n          </>\\n        ) : (\\n          <>\\n            <ChevronUp className=\\\"mr-2 h-4 w-4\\\" />\\n            Collapse\\n          </>\\n        )}\\n      </Button>\\n    </CollapsibleTrigger>\\n  );\\n}\\n\\nexport type CodeBlockComposedProps = Omit<CodeBlockRootProps, \\\"children\\\">;\\n\\nfunction CodeBlockComposed(props: CodeBlockComposedProps) {\\n  return (\\n    <CodeBlockRoot {...props}>\\n      <CodeBlockHeader />\\n      <CodeBlockContent />\\n      <CodeBlockCollapseToggle />\\n    </CodeBlockRoot>\\n  );\\n}\\n\\ntype CodeBlockComponent = typeof CodeBlockComposed & {\\n  Root: typeof CodeBlockRoot;\\n  Header: typeof CodeBlockHeader;\\n  Content: typeof CodeBlockContent;\\n  CollapseToggle: typeof CodeBlockCollapseToggle;\\n};\\n\\nexport const CodeBlock = Object.assign(CodeBlockComposed, {\\n  Root: CodeBlockRoot,\\n  Header: CodeBlockHeader,\\n  Content: CodeBlockContent,\\n  CollapseToggle: CodeBlockCollapseToggle,\\n}) as CodeBlockComponent;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/code-block/index.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/code-block/index.tsx\",\n      \"content\": \"export { CodeBlock } from \\\"./code-block\\\";\\nexport type {\\n  CodeBlockRootProps,\\n  CodeBlockComposedProps,\\n  CodeBlockSectionProps,\\n} from \\\"./code-block\\\";\\nexport type {\\n  CodeBlockProps,\\n  CodeBlockLineNumbersMode,\\n  SerializableCodeBlock,\\n} from \\\"./schema\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/code-block/README.md\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/code-block/README.md\",\n      \"content\": \"# Code Block\\n\\nImplementation for the \\\"code-block\\\" Tool UI surface.\\n\\n## Files\\n\\n- public exports: components/tool-ui/code-block/index.tsx\\n- serializable schema + parse helpers: components/tool-ui/code-block/schema.ts\\n\\n## Companion assets\\n\\n- Docs page: app/docs/code-block/content.mdx\\n- Preset payload: lib/presets/code-block.ts\\n\\n## Quick check\\n\\nRun this after edits:\\n\\npnpm test\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/code-block/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/code-block/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\nimport {\\n  ToolUIIdSchema,\\n  ToolUIReceiptSchema,\\n  ToolUIRoleSchema,\\n} from \\\"../shared/schema\\\";\\n\\nexport const CodeBlockPropsSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n  code: z.string(),\\n  language: z.string().trim().min(1).default(\\\"text\\\"),\\n  lineNumbers: z.enum([\\\"visible\\\", \\\"hidden\\\"]).default(\\\"visible\\\"),\\n  filename: z.string().optional(),\\n  highlightLines: z.array(z.number().int().positive()).optional(),\\n  maxCollapsedLines: z.number().min(1).optional(),\\n  className: z.string().optional(),\\n});\\n\\nexport type CodeBlockProps = z.infer<typeof CodeBlockPropsSchema>;\\nexport type CodeBlockLineNumbersMode = CodeBlockProps[\\\"lineNumbers\\\"];\\n\\nexport const SerializableCodeBlockSchema = CodeBlockPropsSchema.omit({\\n  className: true,\\n});\\n\\nexport type SerializableCodeBlock = z.infer<typeof SerializableCodeBlockSchema>;\\n\\nconst SerializableCodeBlockSchemaContract = defineToolUiContract(\\n  \\\"CodeBlock\\\",\\n  SerializableCodeBlockSchema,\\n);\\n\\nexport const parseSerializableCodeBlock: (\\n  input: unknown,\\n) => SerializableCodeBlock = SerializableCodeBlockSchemaContract.parse;\\n\\nexport const safeParseSerializableCodeBlock: (\\n  input: unknown,\\n) => SerializableCodeBlock | null =\\n  SerializableCodeBlockSchemaContract.safeParse;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/pierre-dark-theme.js\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/shared/pierre-dark-theme.js\",\n      \"content\": \"//#region src/themes/pierre-dark.json\\nvar name = \\\"pierre-dark\\\";\\nvar type = \\\"dark\\\";\\nvar colors = {\\n  \\\"editor.background\\\": \\\"#070707\\\",\\n  \\\"editor.foreground\\\": \\\"#fbfbfb\\\",\\n  foreground: \\\"#fbfbfb\\\",\\n  focusBorder: \\\"#009fff\\\",\\n  \\\"selection.background\\\": \\\"#19283c\\\",\\n  \\\"editor.selectionBackground\\\": \\\"#009fff4d\\\",\\n  \\\"editor.lineHighlightBackground\\\": \\\"#19283c8c\\\",\\n  \\\"editorCursor.foreground\\\": \\\"#009fff\\\",\\n  \\\"editorLineNumber.foreground\\\": \\\"#84848A\\\",\\n  \\\"editorLineNumber.activeForeground\\\": \\\"#adadb1\\\",\\n  \\\"editorIndentGuide.background\\\": \\\"#39393c\\\",\\n  \\\"editorIndentGuide.activeBackground\\\": \\\"#2e2e30\\\",\\n  \\\"diffEditor.insertedTextBackground\\\": \\\"#00cab11a\\\",\\n  \\\"diffEditor.deletedTextBackground\\\": \\\"#ff2e3f1a\\\",\\n  \\\"sideBar.background\\\": \\\"#141415\\\",\\n  \\\"sideBar.foreground\\\": \\\"#adadb1\\\",\\n  \\\"sideBar.border\\\": \\\"#070707\\\",\\n  \\\"sideBarTitle.foreground\\\": \\\"#fbfbfb\\\",\\n  \\\"sideBarSectionHeader.background\\\": \\\"#141415\\\",\\n  \\\"sideBarSectionHeader.foreground\\\": \\\"#adadb1\\\",\\n  \\\"sideBarSectionHeader.border\\\": \\\"#070707\\\",\\n  \\\"activityBar.background\\\": \\\"#141415\\\",\\n  \\\"activityBar.foreground\\\": \\\"#fbfbfb\\\",\\n  \\\"activityBar.border\\\": \\\"#070707\\\",\\n  \\\"activityBar.activeBorder\\\": \\\"#009fff\\\",\\n  \\\"activityBarBadge.background\\\": \\\"#009fff\\\",\\n  \\\"activityBarBadge.foreground\\\": \\\"#070707\\\",\\n  \\\"titleBar.activeBackground\\\": \\\"#141415\\\",\\n  \\\"titleBar.activeForeground\\\": \\\"#fbfbfb\\\",\\n  \\\"titleBar.inactiveBackground\\\": \\\"#141415\\\",\\n  \\\"titleBar.inactiveForeground\\\": \\\"#84848A\\\",\\n  \\\"titleBar.border\\\": \\\"#070707\\\",\\n  \\\"list.activeSelectionBackground\\\": \\\"#19283c99\\\",\\n  \\\"list.activeSelectionForeground\\\": \\\"#fbfbfb\\\",\\n  \\\"list.inactiveSelectionBackground\\\": \\\"#19283c73\\\",\\n  \\\"list.hoverBackground\\\": \\\"#19283c59\\\",\\n  \\\"list.focusOutline\\\": \\\"#009fff\\\",\\n  \\\"tab.activeBackground\\\": \\\"#070707\\\",\\n  \\\"tab.activeForeground\\\": \\\"#fbfbfb\\\",\\n  \\\"tab.activeBorderTop\\\": \\\"#009fff\\\",\\n  \\\"tab.inactiveBackground\\\": \\\"#141415\\\",\\n  \\\"tab.inactiveForeground\\\": \\\"#84848A\\\",\\n  \\\"tab.border\\\": \\\"#070707\\\",\\n  \\\"editorGroupHeader.tabsBackground\\\": \\\"#141415\\\",\\n  \\\"editorGroupHeader.tabsBorder\\\": \\\"#070707\\\",\\n  \\\"panel.background\\\": \\\"#141415\\\",\\n  \\\"panel.border\\\": \\\"#070707\\\",\\n  \\\"panelTitle.activeBorder\\\": \\\"#009fff\\\",\\n  \\\"panelTitle.activeForeground\\\": \\\"#fbfbfb\\\",\\n  \\\"panelTitle.inactiveForeground\\\": \\\"#84848A\\\",\\n  \\\"statusBar.background\\\": \\\"#141415\\\",\\n  \\\"statusBar.foreground\\\": \\\"#adadb1\\\",\\n  \\\"statusBar.border\\\": \\\"#070707\\\",\\n  \\\"statusBar.noFolderBackground\\\": \\\"#141415\\\",\\n  \\\"statusBar.debuggingBackground\\\": \\\"#ffca00\\\",\\n  \\\"statusBar.debuggingForeground\\\": \\\"#070707\\\",\\n  \\\"statusBarItem.remoteBackground\\\": \\\"#141415\\\",\\n  \\\"statusBarItem.remoteForeground\\\": \\\"#adadb1\\\",\\n  \\\"input.background\\\": \\\"#1F1F21\\\",\\n  \\\"input.border\\\": \\\"#424245\\\",\\n  \\\"input.foreground\\\": \\\"#fbfbfb\\\",\\n  \\\"input.placeholderForeground\\\": \\\"#79797F\\\",\\n  \\\"dropdown.background\\\": \\\"#1F1F21\\\",\\n  \\\"dropdown.border\\\": \\\"#424245\\\",\\n  \\\"dropdown.foreground\\\": \\\"#fbfbfb\\\",\\n  \\\"button.background\\\": \\\"#009fff\\\",\\n  \\\"button.foreground\\\": \\\"#070707\\\",\\n  \\\"button.hoverBackground\\\": \\\"#0190e6\\\",\\n  \\\"textLink.foreground\\\": \\\"#009fff\\\",\\n  \\\"textLink.activeForeground\\\": \\\"#009fff\\\",\\n  \\\"gitDecoration.addedResourceForeground\\\": \\\"#00cab1\\\",\\n  \\\"gitDecoration.conflictingResourceForeground\\\": \\\"#ffca00\\\",\\n  \\\"gitDecoration.modifiedResourceForeground\\\": \\\"#009fff\\\",\\n  \\\"gitDecoration.deletedResourceForeground\\\": \\\"#ff2e3f\\\",\\n  \\\"gitDecoration.untrackedResourceForeground\\\": \\\"#00cab1\\\",\\n  \\\"gitDecoration.ignoredResourceForeground\\\": \\\"#84848A\\\",\\n  \\\"terminal.titleForeground\\\": \\\"#adadb1\\\",\\n  \\\"terminal.titleInactiveForeground\\\": \\\"#84848A\\\",\\n  \\\"terminal.background\\\": \\\"#141415\\\",\\n  \\\"terminal.foreground\\\": \\\"#adadb1\\\",\\n  \\\"terminal.ansiBlack\\\": \\\"#141415\\\",\\n  \\\"terminal.ansiRed\\\": \\\"#ff2e3f\\\",\\n  \\\"terminal.ansiGreen\\\": \\\"#0dbe4e\\\",\\n  \\\"terminal.ansiYellow\\\": \\\"#ffca00\\\",\\n  \\\"terminal.ansiBlue\\\": \\\"#009fff\\\",\\n  \\\"terminal.ansiMagenta\\\": \\\"#c635e4\\\",\\n  \\\"terminal.ansiCyan\\\": \\\"#08c0ef\\\",\\n  \\\"terminal.ansiWhite\\\": \\\"#c6c6c8\\\",\\n  \\\"terminal.ansiBrightBlack\\\": \\\"#141415\\\",\\n  \\\"terminal.ansiBrightRed\\\": \\\"#ff2e3f\\\",\\n  \\\"terminal.ansiBrightGreen\\\": \\\"#0dbe4e\\\",\\n  \\\"terminal.ansiBrightYellow\\\": \\\"#ffca00\\\",\\n  \\\"terminal.ansiBrightBlue\\\": \\\"#009fff\\\",\\n  \\\"terminal.ansiBrightMagenta\\\": \\\"#c635e4\\\",\\n  \\\"terminal.ansiBrightCyan\\\": \\\"#08c0ef\\\",\\n  \\\"terminal.ansiBrightWhite\\\": \\\"#c6c6c8\\\",\\n};\\nvar tokenColors = [\\n  {\\n    scope: [\\\"comment\\\", \\\"punctuation.definition.comment\\\"],\\n    settings: { foreground: \\\"#84848A\\\" },\\n  },\\n  {\\n    scope: \\\"comment markup.link\\\",\\n    settings: { foreground: \\\"#84848A\\\" },\\n  },\\n  {\\n    scope: [\\\"string\\\", \\\"constant.other.symbol\\\"],\\n    settings: { foreground: \\\"#5ecc71\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.string.begin\\\",\\n      \\\"punctuation.definition.string.end\\\",\\n    ],\\n    settings: { foreground: \\\"#5ecc71\\\" },\\n  },\\n  {\\n    scope: [\\\"constant.numeric\\\", \\\"constant.language.boolean\\\"],\\n    settings: { foreground: \\\"#68cdf2\\\" },\\n  },\\n  {\\n    scope: \\\"constant\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.constant\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"constant.language\\\",\\n    settings: { foreground: \\\"#68cdf2\\\" },\\n  },\\n  {\\n    scope: \\\"variable.other.constant\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"keyword\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.control\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: [\\\"storage\\\", \\\"storage.type\\\", \\\"storage.modifier\\\"],\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"token.storage\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.new\\\",\\n      \\\"keyword.operator.expression.instanceof\\\",\\n      \\\"keyword.operator.expression.typeof\\\",\\n      \\\"keyword.operator.expression.void\\\",\\n      \\\"keyword.operator.expression.delete\\\",\\n      \\\"keyword.operator.expression.in\\\",\\n      \\\"keyword.operator.expression.of\\\",\\n      \\\"keyword.operator.expression.keyof\\\",\\n    ],\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.delete\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: [\\\"variable\\\", \\\"identifier\\\", \\\"meta.definition.variable\\\"],\\n    settings: { foreground: \\\"#ffa359\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"variable.other.readwrite\\\",\\n      \\\"meta.object-literal.key\\\",\\n      \\\"support.variable.property\\\",\\n      \\\"support.variable.object.process\\\",\\n      \\\"support.variable.object.node\\\",\\n    ],\\n    settings: { foreground: \\\"#ffa359\\\" },\\n  },\\n  {\\n    scope: \\\"variable.language\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"variable.parameter.function\\\",\\n    settings: { foreground: \\\"#adadb1\\\" },\\n  },\\n  {\\n    scope: \\\"function.parameter\\\",\\n    settings: { foreground: \\\"#adadb1\\\" },\\n  },\\n  {\\n    scope: \\\"variable.parameter\\\",\\n    settings: { foreground: \\\"#adadb1\\\" },\\n  },\\n  {\\n    scope: \\\"variable.parameter.function.language.python\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"variable.parameter.function.python\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.function\\\",\\n      \\\"entity.name.function\\\",\\n      \\\"meta.function-call\\\",\\n      \\\"meta.require\\\",\\n      \\\"support.function.any-method\\\",\\n      \\\"variable.function\\\",\\n    ],\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.other.special-method\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.function\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"support.function.console\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.type\\\",\\n      \\\"entity.name.type\\\",\\n      \\\"entity.name.class\\\",\\n      \\\"storage.type\\\",\\n    ],\\n    settings: { foreground: \\\"#d568ea\\\" },\\n  },\\n  {\\n    scope: [\\\"support.class\\\", \\\"entity.name.type.class\\\"],\\n    settings: { foreground: \\\"#d568ea\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"entity.name.class\\\",\\n      \\\"variable.other.class.js\\\",\\n      \\\"variable.other.class.ts\\\",\\n    ],\\n    settings: { foreground: \\\"#d568ea\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.class.identifier.namespace.type\\\",\\n    settings: { foreground: \\\"#d568ea\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.type.namespace\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"entity.other.inherited-class\\\",\\n    settings: { foreground: \\\"#d568ea\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.namespace\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.logical\\\",\\n      \\\"keyword.operator.bitwise\\\",\\n      \\\"keyword.operator.channel\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.arithmetic\\\",\\n      \\\"keyword.operator.comparison\\\",\\n      \\\"keyword.operator.relational\\\",\\n      \\\"keyword.operator.increment\\\",\\n      \\\"keyword.operator.decrement\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.assignment\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.assignment.compound\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.assignment.compound.js\\\",\\n      \\\"keyword.operator.assignment.compound.ts\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.ternary\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.optional\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.separator.delimiter\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.separator.key-value\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.terminator\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"meta.brace\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"meta.brace.square\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"meta.brace.round\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"function.brace\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.parameters\\\",\\n      \\\"punctuation.definition.typeparameters\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\\"punctuation.definition.block\\\", \\\"punctuation.definition.tag\\\"],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\\"meta.tag.tsx\\\", \\\"meta.tag.jsx\\\", \\\"meta.tag.js\\\", \\\"meta.tag.ts\\\"],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.expression.import\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.module\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.object.console\\\",\\n    settings: { foreground: \\\"#ffa359\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.module.node\\\",\\n      \\\"support.type.object.module\\\",\\n      \\\"entity.name.type.module\\\",\\n    ],\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.math\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.property.math\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.json\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.object.dom\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\\"support.variable.dom\\\", \\\"support.variable.property.dom\\\"],\\n    settings: { foreground: \\\"#ffa359\\\" },\\n  },\\n  {\\n    scope: \\\"support.variable.property.process\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"meta.property.object\\\",\\n    settings: { foreground: \\\"#ffa359\\\" },\\n  },\\n  {\\n    scope: \\\"variable.parameter.function.js\\\",\\n    settings: { foreground: \\\"#ffa359\\\" },\\n  },\\n  {\\n    scope: [\\\"keyword.other.template.begin\\\", \\\"keyword.other.template.end\\\"],\\n    settings: { foreground: \\\"#5ecc71\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.other.substitution.begin\\\",\\n      \\\"keyword.other.substitution.end\\\",\\n    ],\\n    settings: { foreground: \\\"#5ecc71\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.template-expression.begin\\\",\\n      \\\"punctuation.definition.template-expression.end\\\",\\n    ],\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"meta.template.expression\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.section.embedded\\\",\\n    settings: { foreground: \\\"#ffa359\\\" },\\n  },\\n  {\\n    scope: \\\"variable.interpolation\\\",\\n    settings: { foreground: \\\"#ffa359\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.section.embedded.begin\\\",\\n      \\\"punctuation.section.embedded.end\\\",\\n    ],\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.quasi.element\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.type.primitive.ts\\\",\\n      \\\"support.type.builtin.ts\\\",\\n      \\\"support.type.primitive.tsx\\\",\\n      \\\"support.type.builtin.tsx\\\",\\n    ],\\n    settings: { foreground: \\\"#d568ea\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.type.flowtype\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.primitive\\\",\\n    settings: { foreground: \\\"#d568ea\\\" },\\n  },\\n  {\\n    scope: \\\"support.variable.magic.python\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"variable.parameter.function.language.special.self.python\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.separator.period.python\\\",\\n      \\\"punctuation.separator.element.python\\\",\\n      \\\"punctuation.parenthesis.begin.python\\\",\\n      \\\"punctuation.parenthesis.end.python\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.arguments.begin.python\\\",\\n      \\\"punctuation.definition.arguments.end.python\\\",\\n      \\\"punctuation.separator.arguments.python\\\",\\n      \\\"punctuation.definition.list.begin.python\\\",\\n      \\\"punctuation.definition.list.end.python\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.python\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.logical.python\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"meta.function-call.generic.python\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"constant.character.format.placeholder.other.python\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"meta.function.decorator.python\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.token.decorator.python\\\",\\n      \\\"meta.function.decorator.identifier.python\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"storage.modifier.lifetime.rust\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"support.function.std.rust\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.lifetime.rust\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"variable.language.rust\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.misc.rust\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.sigil.rust\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.core.rust\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: [\\\"meta.function.c\\\", \\\"meta.function.cpp\\\"],\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.section.block.begin.bracket.curly.cpp\\\",\\n      \\\"punctuation.section.block.end.bracket.curly.cpp\\\",\\n      \\\"punctuation.terminator.statement.c\\\",\\n      \\\"punctuation.section.block.begin.bracket.curly.c\\\",\\n      \\\"punctuation.section.block.end.bracket.curly.c\\\",\\n      \\\"punctuation.section.parens.begin.bracket.round.c\\\",\\n      \\\"punctuation.section.parens.end.bracket.round.c\\\",\\n      \\\"punctuation.section.parameters.begin.bracket.round.c\\\",\\n      \\\"punctuation.section.parameters.end.bracket.round.c\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.assignment.c\\\",\\n      \\\"keyword.operator.comparison.c\\\",\\n      \\\"keyword.operator.c\\\",\\n      \\\"keyword.operator.increment.c\\\",\\n      \\\"keyword.operator.decrement.c\\\",\\n      \\\"keyword.operator.bitwise.shift.c\\\",\\n    ],\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.assignment.cpp\\\",\\n      \\\"keyword.operator.comparison.cpp\\\",\\n      \\\"keyword.operator.cpp\\\",\\n      \\\"keyword.operator.increment.cpp\\\",\\n      \\\"keyword.operator.decrement.cpp\\\",\\n      \\\"keyword.operator.bitwise.shift.cpp\\\",\\n    ],\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: [\\\"punctuation.separator.c\\\", \\\"punctuation.separator.cpp\\\"],\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: [\\\"support.type.posix-reserved.c\\\", \\\"support.type.posix-reserved.cpp\\\"],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\\"keyword.operator.sizeof.c\\\", \\\"keyword.operator.sizeof.cpp\\\"],\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"variable.c\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\\"storage.type.annotation.java\\\", \\\"storage.type.object.array.java\\\"],\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"source.java\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.section.block.begin.java\\\",\\n      \\\"punctuation.section.block.end.java\\\",\\n      \\\"punctuation.definition.method-parameters.begin.java\\\",\\n      \\\"punctuation.definition.method-parameters.end.java\\\",\\n      \\\"meta.method.identifier.java\\\",\\n      \\\"punctuation.section.method.begin.java\\\",\\n      \\\"punctuation.section.method.end.java\\\",\\n      \\\"punctuation.terminator.java\\\",\\n      \\\"punctuation.section.class.begin.java\\\",\\n      \\\"punctuation.section.class.end.java\\\",\\n      \\\"punctuation.section.inner-class.begin.java\\\",\\n      \\\"punctuation.section.inner-class.end.java\\\",\\n      \\\"meta.method-call.java\\\",\\n      \\\"punctuation.section.class.begin.bracket.curly.java\\\",\\n      \\\"punctuation.section.class.end.bracket.curly.java\\\",\\n      \\\"punctuation.section.method.begin.bracket.curly.java\\\",\\n      \\\"punctuation.section.method.end.bracket.curly.java\\\",\\n      \\\"punctuation.separator.period.java\\\",\\n      \\\"punctuation.bracket.angle.java\\\",\\n      \\\"punctuation.definition.annotation.java\\\",\\n      \\\"meta.method.body.java\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"meta.method.java\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"storage.modifier.import.java\\\",\\n      \\\"storage.type.java\\\",\\n      \\\"storage.type.generic.java\\\",\\n    ],\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.instanceof.java\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"meta.definition.variable.name.java\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"token.variable.parameter.java\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"import.storage.java\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"token.package.keyword\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"token.package\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"token.storage.type.java\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.assignment.go\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: [\\\"keyword.operator.arithmetic.go\\\", \\\"keyword.operator.address.go\\\"],\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.package.go\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.other.namespace.use.php\\\",\\n      \\\"support.other.namespace.use-as.php\\\",\\n      \\\"support.other.namespace.php\\\",\\n      \\\"entity.other.alias.php\\\",\\n      \\\"meta.interface.php\\\",\\n    ],\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.error-control.php\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.type.php\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.section.array.begin.php\\\",\\n      \\\"punctuation.section.array.end.php\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"storage.type.php\\\",\\n      \\\"meta.other.type.phpdoc.php\\\",\\n      \\\"keyword.other.type.php\\\",\\n      \\\"keyword.other.array.phpdoc.php\\\",\\n    ],\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"meta.function-call.php\\\",\\n      \\\"meta.function-call.object.php\\\",\\n      \\\"meta.function-call.static.php\\\",\\n    ],\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.parameters.begin.bracket.round.php\\\",\\n      \\\"punctuation.definition.parameters.end.bracket.round.php\\\",\\n      \\\"punctuation.separator.delimiter.php\\\",\\n      \\\"punctuation.section.scope.begin.php\\\",\\n      \\\"punctuation.section.scope.end.php\\\",\\n      \\\"punctuation.terminator.expression.php\\\",\\n      \\\"punctuation.definition.arguments.begin.bracket.round.php\\\",\\n      \\\"punctuation.definition.arguments.end.bracket.round.php\\\",\\n      \\\"punctuation.definition.storage-type.begin.bracket.round.php\\\",\\n      \\\"punctuation.definition.storage-type.end.bracket.round.php\\\",\\n      \\\"punctuation.definition.array.begin.bracket.round.php\\\",\\n      \\\"punctuation.definition.array.end.bracket.round.php\\\",\\n      \\\"punctuation.definition.begin.bracket.round.php\\\",\\n      \\\"punctuation.definition.end.bracket.round.php\\\",\\n      \\\"punctuation.definition.begin.bracket.curly.php\\\",\\n      \\\"punctuation.definition.end.bracket.curly.php\\\",\\n      \\\"punctuation.definition.section.switch-block.end.bracket.curly.php\\\",\\n      \\\"punctuation.definition.section.switch-block.start.bracket.curly.php\\\",\\n      \\\"punctuation.definition.section.switch-block.begin.bracket.curly.php\\\",\\n      \\\"punctuation.definition.section.switch-block.end.bracket.curly.php\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.constant.ext.php\\\",\\n      \\\"support.constant.std.php\\\",\\n      \\\"support.constant.core.php\\\",\\n      \\\"support.constant.parser-token.php\\\",\\n    ],\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: [\\\"entity.name.goto-label.php\\\", \\\"support.other.php\\\"],\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.logical.php\\\",\\n      \\\"keyword.operator.bitwise.php\\\",\\n      \\\"keyword.operator.arithmetic.php\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.regexp.php\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.comparison.php\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\\"keyword.operator.heredoc.php\\\", \\\"keyword.operator.nowdoc.php\\\"],\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"variable.other.class.php\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.illegal.non-null-typehinted.php\\\",\\n    settings: { foreground: \\\"#f44747\\\" },\\n  },\\n  {\\n    scope: \\\"variable.other.generic-type.haskell\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"storage.type.haskell\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"storage.type.cs\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.variable.local.cs\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.label.cs\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"entity.name.scope-resolution.function.call\\\",\\n      \\\"entity.name.scope-resolution.function.definition\\\",\\n    ],\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.delayed.unison\\\",\\n      \\\"punctuation.definition.list.begin.unison\\\",\\n      \\\"punctuation.definition.list.end.unison\\\",\\n      \\\"punctuation.definition.ability.begin.unison\\\",\\n      \\\"punctuation.definition.ability.end.unison\\\",\\n      \\\"punctuation.operator.assignment.as.unison\\\",\\n      \\\"punctuation.separator.pipe.unison\\\",\\n      \\\"punctuation.separator.delimiter.unison\\\",\\n      \\\"punctuation.definition.hash.unison\\\",\\n    ],\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.edge\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.prelude.elm\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.elm\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"entity.global.clojure\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"meta.symbol.clojure\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"constant.keyword.clojure\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\\"meta.arguments.coffee\\\", \\\"variable.parameter.function.coffee\\\"],\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"storage.modifier.import.groovy\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"meta.method.groovy\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"meta.definition.variable.name.groovy\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"meta.definition.class.inherited.classes.groovy\\\",\\n    settings: { foreground: \\\"#5ecc71\\\" },\\n  },\\n  {\\n    scope: \\\"support.variable.semantic.hlsl\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.type.texture.hlsl\\\",\\n      \\\"support.type.sampler.hlsl\\\",\\n      \\\"support.type.object.hlsl\\\",\\n      \\\"support.type.object.rw.hlsl\\\",\\n      \\\"support.type.fx.hlsl\\\",\\n      \\\"support.type.object.hlsl\\\",\\n    ],\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: [\\\"text.variable\\\", \\\"text.bracketed\\\"],\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: [\\\"support.type.swift\\\", \\\"support.type.vb.asp\\\"],\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"meta.scope.prerequisites.makefile\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"source.makefile\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"source.ini\\\",\\n    settings: { foreground: \\\"#5ecc71\\\" },\\n  },\\n  {\\n    scope: \\\"constant.language.symbol.ruby\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\\"function.parameter.ruby\\\", \\\"function.parameter.cs\\\"],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"constant.language.symbol.elixir\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope:\\n      \\\"text.html.laravel-blade source.php.embedded.line.html entity.name.tag.laravel-blade\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope:\\n      \\\"text.html.laravel-blade source.php.embedded.line.html support.constant.laravel-blade\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.function.xi\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.class.xi\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"constant.character.character-class.regexp.xi\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"constant.regexp.xi\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.control.xi\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.xi\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"beginning.punctuation.definition.quote.markdown.xi\\\",\\n    settings: { foreground: \\\"#5ecc71\\\" },\\n  },\\n  {\\n    scope: \\\"beginning.punctuation.definition.list.markdown.xi\\\",\\n    settings: { foreground: \\\"#84848A\\\" },\\n  },\\n  {\\n    scope: \\\"constant.character.xi\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"accent.xi\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"wikiword.xi\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"constant.other.color.rgb-value.xi\\\",\\n    settings: { foreground: \\\"#ffffff\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.tag.xi\\\",\\n    settings: { foreground: \\\"#84848A\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.constant.property-value.scss\\\",\\n      \\\"support.constant.property-value.css\\\",\\n    ],\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.css\\\",\\n      \\\"keyword.operator.scss\\\",\\n      \\\"keyword.operator.less\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.constant.color.w3c-standard-color-name.css\\\",\\n      \\\"support.constant.color.w3c-standard-color-name.scss\\\",\\n    ],\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.separator.list.comma.css\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.vendored.property-name.css\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.property-name.css\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.property-name\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.property-value\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.font-name\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"entity.other.attribute-name.class.css\\\",\\n    settings: {\\n      foreground: \\\"#61d5c0\\\",\\n      fontStyle: \\\"normal\\\",\\n    },\\n  },\\n  {\\n    scope: \\\"entity.other.attribute-name.id\\\",\\n    settings: {\\n      foreground: \\\"#9d6afb\\\",\\n      fontStyle: \\\"normal\\\",\\n    },\\n  },\\n  {\\n    scope: [\\n      \\\"entity.other.attribute-name.pseudo-element\\\",\\n      \\\"entity.other.attribute-name.pseudo-class\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"meta.selector\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"selector.sass\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"rgb-value\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"inline-color-decoration rgb-value\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"less rgb-value\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"control.elements\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.less\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.tag\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"entity.other.attribute-name\\\",\\n    settings: {\\n      foreground: \\\"#61d5c0\\\",\\n      fontStyle: \\\"normal\\\",\\n    },\\n  },\\n  {\\n    scope: \\\"constant.character.entity\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"meta.tag\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.illegal.bad-ampersand.html\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"markup.heading\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"markup.heading punctuation.definition.heading\\\",\\n      \\\"entity.name.section\\\",\\n    ],\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.section.markdown\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.heading.markdown\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"markup.heading.setext\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"markup.heading.setext.1.markdown\\\",\\n      \\\"markup.heading.setext.2.markdown\\\",\\n    ],\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: [\\\"markup.bold\\\", \\\"todo.bold\\\"],\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.bold\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.bold.markdown\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: [\\\"markup.italic\\\", \\\"punctuation.definition.italic\\\", \\\"todo.emphasis\\\"],\\n    settings: {\\n      foreground: \\\"#ff678d\\\",\\n      fontStyle: \\\"italic\\\",\\n    },\\n  },\\n  {\\n    scope: \\\"emphasis md\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"markup.italic.markdown\\\",\\n    settings: { fontStyle: \\\"italic\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"markup.underline.link.markdown\\\",\\n      \\\"markup.underline.link.image.markdown\\\",\\n    ],\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"string.other.link.title.markdown\\\",\\n      \\\"string.other.link.description.markdown\\\",\\n    ],\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.metadata.markdown\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: [\\\"markup.inline.raw.markdown\\\", \\\"markup.inline.raw.string.markdown\\\"],\\n    settings: { foreground: \\\"#5ecc71\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.list.begin.markdown\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.list.markdown\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"beginning.punctuation.definition.list.markdown\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.string.begin.markdown\\\",\\n      \\\"punctuation.definition.string.end.markdown\\\",\\n    ],\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"markup.quote.markdown\\\",\\n    settings: { foreground: \\\"#84848A\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.other.unit\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"markup.changed.diff\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"meta.diff.header.from-file\\\",\\n      \\\"meta.diff.header.to-file\\\",\\n      \\\"punctuation.definition.from-file.diff\\\",\\n      \\\"punctuation.definition.to-file.diff\\\",\\n    ],\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"markup.inserted.diff\\\",\\n    settings: { foreground: \\\"#5ecc71\\\" },\\n  },\\n  {\\n    scope: \\\"markup.deleted.diff\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"string.regexp\\\",\\n    settings: { foreground: \\\"#64d1db\\\" },\\n  },\\n  {\\n    scope: \\\"constant.other.character-class.regexp\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.quantifier.regexp\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"constant.character.escape\\\",\\n    settings: { foreground: \\\"#68cdf2\\\" },\\n  },\\n  {\\n    scope: \\\"source.json meta.structure.dictionary.json > string.quoted.json\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope:\\n      \\\"source.json meta.structure.dictionary.json > string.quoted.json > punctuation.string\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"source.json meta.structure.dictionary.json > value.json > string.quoted.json\\\",\\n      \\\"source.json meta.structure.array.json > value.json > string.quoted.json\\\",\\n      \\\"source.json meta.structure.dictionary.json > value.json > string.quoted.json > punctuation\\\",\\n      \\\"source.json meta.structure.array.json > value.json > string.quoted.json > punctuation\\\",\\n    ],\\n    settings: { foreground: \\\"#5ecc71\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"source.json meta.structure.dictionary.json > constant.language.json\\\",\\n      \\\"source.json meta.structure.array.json > constant.language.json\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.property-name.json\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.property-name.json punctuation\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.block.sequence.item.yaml\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"block.scope.end\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"block.scope.begin\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"token.info-token\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"token.warn-token\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"token.error-token\\\",\\n    settings: { foreground: \\\"#f44747\\\" },\\n  },\\n  {\\n    scope: \\\"token.debug-token\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.illegal\\\",\\n    settings: { foreground: \\\"#ffffff\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.broken\\\",\\n    settings: { foreground: \\\"#ffffff\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.deprecated\\\",\\n    settings: { foreground: \\\"#ffffff\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.unimplemented\\\",\\n    settings: { foreground: \\\"#ffffff\\\" },\\n  },\\n];\\nvar semanticTokenColors = {\\n  comment: \\\"#84848A\\\",\\n  string: \\\"#5ecc71\\\",\\n  number: \\\"#68cdf2\\\",\\n  regexp: \\\"#64d1db\\\",\\n  keyword: \\\"#ff678d\\\",\\n  variable: \\\"#ffa359\\\",\\n  parameter: \\\"#adadb1\\\",\\n  property: \\\"#ffa359\\\",\\n  function: \\\"#9d6afb\\\",\\n  method: \\\"#9d6afb\\\",\\n  type: \\\"#d568ea\\\",\\n  class: \\\"#d568ea\\\",\\n  namespace: \\\"#ffca00\\\",\\n  enumMember: \\\"#08c0ef\\\",\\n  \\\"variable.constant\\\": \\\"#ffd452\\\",\\n  \\\"variable.defaultLibrary\\\": \\\"#ffca00\\\",\\n};\\nvar pierre_dark_default = {\\n  name,\\n  type,\\n  colors,\\n  tokenColors,\\n  semanticTokenColors,\\n};\\n\\n//#endregion\\nexport {\\n  colors,\\n  pierre_dark_default as default,\\n  name,\\n  semanticTokenColors,\\n  tokenColors,\\n  type,\\n};\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/pierre-light-theme.js\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/shared/pierre-light-theme.js\",\n      \"content\": \"//#region src/themes/pierre-light.json\\nvar name = \\\"pierre-light\\\";\\nvar type = \\\"light\\\";\\nvar colors = {\\n  \\\"editor.background\\\": \\\"#ffffff\\\",\\n  \\\"editor.foreground\\\": \\\"#070707\\\",\\n  foreground: \\\"#070707\\\",\\n  focusBorder: \\\"#009fff\\\",\\n  \\\"selection.background\\\": \\\"#dfebff\\\",\\n  \\\"editor.selectionBackground\\\": \\\"#009fff2e\\\",\\n  \\\"editor.lineHighlightBackground\\\": \\\"#dfebff8c\\\",\\n  \\\"editorCursor.foreground\\\": \\\"#009fff\\\",\\n  \\\"editorLineNumber.foreground\\\": \\\"#84848A\\\",\\n  \\\"editorLineNumber.activeForeground\\\": \\\"#6C6C71\\\",\\n  \\\"editorIndentGuide.background\\\": \\\"#eeeeef\\\",\\n  \\\"editorIndentGuide.activeBackground\\\": \\\"#dbdbdd\\\",\\n  \\\"diffEditor.insertedTextBackground\\\": \\\"#00cab133\\\",\\n  \\\"diffEditor.deletedTextBackground\\\": \\\"#ff2e3f33\\\",\\n  \\\"sideBar.background\\\": \\\"#f8f8f8\\\",\\n  \\\"sideBar.foreground\\\": \\\"#6C6C71\\\",\\n  \\\"sideBar.border\\\": \\\"#eeeeef\\\",\\n  \\\"sideBarTitle.foreground\\\": \\\"#070707\\\",\\n  \\\"sideBarSectionHeader.background\\\": \\\"#f8f8f8\\\",\\n  \\\"sideBarSectionHeader.foreground\\\": \\\"#6C6C71\\\",\\n  \\\"sideBarSectionHeader.border\\\": \\\"#eeeeef\\\",\\n  \\\"activityBar.background\\\": \\\"#f8f8f8\\\",\\n  \\\"activityBar.foreground\\\": \\\"#070707\\\",\\n  \\\"activityBar.border\\\": \\\"#eeeeef\\\",\\n  \\\"activityBar.activeBorder\\\": \\\"#009fff\\\",\\n  \\\"activityBarBadge.background\\\": \\\"#009fff\\\",\\n  \\\"activityBarBadge.foreground\\\": \\\"#ffffff\\\",\\n  \\\"titleBar.activeBackground\\\": \\\"#f8f8f8\\\",\\n  \\\"titleBar.activeForeground\\\": \\\"#070707\\\",\\n  \\\"titleBar.inactiveBackground\\\": \\\"#f8f8f8\\\",\\n  \\\"titleBar.inactiveForeground\\\": \\\"#84848A\\\",\\n  \\\"titleBar.border\\\": \\\"#eeeeef\\\",\\n  \\\"list.activeSelectionBackground\\\": \\\"#dfebffcc\\\",\\n  \\\"list.activeSelectionForeground\\\": \\\"#070707\\\",\\n  \\\"list.inactiveSelectionBackground\\\": \\\"#dfebff73\\\",\\n  \\\"list.hoverBackground\\\": \\\"#dfebff59\\\",\\n  \\\"list.focusOutline\\\": \\\"#009fff\\\",\\n  \\\"tab.activeBackground\\\": \\\"#ffffff\\\",\\n  \\\"tab.activeForeground\\\": \\\"#070707\\\",\\n  \\\"tab.activeBorderTop\\\": \\\"#009fff\\\",\\n  \\\"tab.inactiveBackground\\\": \\\"#f8f8f8\\\",\\n  \\\"tab.inactiveForeground\\\": \\\"#84848A\\\",\\n  \\\"tab.border\\\": \\\"#eeeeef\\\",\\n  \\\"editorGroupHeader.tabsBackground\\\": \\\"#f8f8f8\\\",\\n  \\\"editorGroupHeader.tabsBorder\\\": \\\"#eeeeef\\\",\\n  \\\"panel.background\\\": \\\"#f8f8f8\\\",\\n  \\\"panel.border\\\": \\\"#eeeeef\\\",\\n  \\\"panelTitle.activeBorder\\\": \\\"#009fff\\\",\\n  \\\"panelTitle.activeForeground\\\": \\\"#070707\\\",\\n  \\\"panelTitle.inactiveForeground\\\": \\\"#84848A\\\",\\n  \\\"statusBar.background\\\": \\\"#f8f8f8\\\",\\n  \\\"statusBar.foreground\\\": \\\"#6C6C71\\\",\\n  \\\"statusBar.border\\\": \\\"#eeeeef\\\",\\n  \\\"statusBar.noFolderBackground\\\": \\\"#f8f8f8\\\",\\n  \\\"statusBar.debuggingBackground\\\": \\\"#ffca00\\\",\\n  \\\"statusBar.debuggingForeground\\\": \\\"#ffffff\\\",\\n  \\\"statusBarItem.remoteBackground\\\": \\\"#f8f8f8\\\",\\n  \\\"statusBarItem.remoteForeground\\\": \\\"#6C6C71\\\",\\n  \\\"input.background\\\": \\\"#f2f2f3\\\",\\n  \\\"input.border\\\": \\\"#dbdbdd\\\",\\n  \\\"input.foreground\\\": \\\"#070707\\\",\\n  \\\"input.placeholderForeground\\\": \\\"#8E8E95\\\",\\n  \\\"dropdown.background\\\": \\\"#f2f2f3\\\",\\n  \\\"dropdown.border\\\": \\\"#dbdbdd\\\",\\n  \\\"dropdown.foreground\\\": \\\"#070707\\\",\\n  \\\"button.background\\\": \\\"#009fff\\\",\\n  \\\"button.foreground\\\": \\\"#ffffff\\\",\\n  \\\"button.hoverBackground\\\": \\\"#1aa9ff\\\",\\n  \\\"textLink.foreground\\\": \\\"#009fff\\\",\\n  \\\"textLink.activeForeground\\\": \\\"#009fff\\\",\\n  \\\"gitDecoration.addedResourceForeground\\\": \\\"#00cab1\\\",\\n  \\\"gitDecoration.conflictingResourceForeground\\\": \\\"#ffca00\\\",\\n  \\\"gitDecoration.modifiedResourceForeground\\\": \\\"#009fff\\\",\\n  \\\"gitDecoration.deletedResourceForeground\\\": \\\"#ff2e3f\\\",\\n  \\\"gitDecoration.untrackedResourceForeground\\\": \\\"#00cab1\\\",\\n  \\\"gitDecoration.ignoredResourceForeground\\\": \\\"#84848A\\\",\\n  \\\"terminal.titleForeground\\\": \\\"#6C6C71\\\",\\n  \\\"terminal.titleInactiveForeground\\\": \\\"#84848A\\\",\\n  \\\"terminal.background\\\": \\\"#f8f8f8\\\",\\n  \\\"terminal.foreground\\\": \\\"#6C6C71\\\",\\n  \\\"terminal.ansiBlack\\\": \\\"#1F1F21\\\",\\n  \\\"terminal.ansiRed\\\": \\\"#ff2e3f\\\",\\n  \\\"terminal.ansiGreen\\\": \\\"#0dbe4e\\\",\\n  \\\"terminal.ansiYellow\\\": \\\"#ffca00\\\",\\n  \\\"terminal.ansiBlue\\\": \\\"#009fff\\\",\\n  \\\"terminal.ansiMagenta\\\": \\\"#c635e4\\\",\\n  \\\"terminal.ansiCyan\\\": \\\"#08c0ef\\\",\\n  \\\"terminal.ansiWhite\\\": \\\"#c6c6c8\\\",\\n  \\\"terminal.ansiBrightBlack\\\": \\\"#1F1F21\\\",\\n  \\\"terminal.ansiBrightRed\\\": \\\"#ff2e3f\\\",\\n  \\\"terminal.ansiBrightGreen\\\": \\\"#0dbe4e\\\",\\n  \\\"terminal.ansiBrightYellow\\\": \\\"#ffca00\\\",\\n  \\\"terminal.ansiBrightBlue\\\": \\\"#009fff\\\",\\n  \\\"terminal.ansiBrightMagenta\\\": \\\"#c635e4\\\",\\n  \\\"terminal.ansiBrightCyan\\\": \\\"#08c0ef\\\",\\n  \\\"terminal.ansiBrightWhite\\\": \\\"#c6c6c8\\\",\\n};\\nvar tokenColors = [\\n  {\\n    scope: [\\\"comment\\\", \\\"punctuation.definition.comment\\\"],\\n    settings: { foreground: \\\"#84848A\\\" },\\n  },\\n  {\\n    scope: \\\"comment markup.link\\\",\\n    settings: { foreground: \\\"#84848A\\\" },\\n  },\\n  {\\n    scope: [\\\"string\\\", \\\"constant.other.symbol\\\"],\\n    settings: { foreground: \\\"#199f43\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.string.begin\\\",\\n      \\\"punctuation.definition.string.end\\\",\\n    ],\\n    settings: { foreground: \\\"#199f43\\\" },\\n  },\\n  {\\n    scope: [\\\"constant.numeric\\\", \\\"constant.language.boolean\\\"],\\n    settings: { foreground: \\\"#1ca1c7\\\" },\\n  },\\n  {\\n    scope: \\\"constant\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.constant\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"constant.language\\\",\\n    settings: { foreground: \\\"#1ca1c7\\\" },\\n  },\\n  {\\n    scope: \\\"variable.other.constant\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"keyword\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.control\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: [\\\"storage\\\", \\\"storage.type\\\", \\\"storage.modifier\\\"],\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"token.storage\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.new\\\",\\n      \\\"keyword.operator.expression.instanceof\\\",\\n      \\\"keyword.operator.expression.typeof\\\",\\n      \\\"keyword.operator.expression.void\\\",\\n      \\\"keyword.operator.expression.delete\\\",\\n      \\\"keyword.operator.expression.in\\\",\\n      \\\"keyword.operator.expression.of\\\",\\n      \\\"keyword.operator.expression.keyof\\\",\\n    ],\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.delete\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: [\\\"variable\\\", \\\"identifier\\\", \\\"meta.definition.variable\\\"],\\n    settings: { foreground: \\\"#d47628\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"variable.other.readwrite\\\",\\n      \\\"meta.object-literal.key\\\",\\n      \\\"support.variable.property\\\",\\n      \\\"support.variable.object.process\\\",\\n      \\\"support.variable.object.node\\\",\\n    ],\\n    settings: { foreground: \\\"#d47628\\\" },\\n  },\\n  {\\n    scope: \\\"variable.language\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"variable.parameter.function\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"function.parameter\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"variable.parameter\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"variable.parameter.function.language.python\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"variable.parameter.function.python\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.function\\\",\\n      \\\"entity.name.function\\\",\\n      \\\"meta.function-call\\\",\\n      \\\"meta.require\\\",\\n      \\\"support.function.any-method\\\",\\n      \\\"variable.function\\\",\\n    ],\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.other.special-method\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.function\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"support.function.console\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.type\\\",\\n      \\\"entity.name.type\\\",\\n      \\\"entity.name.class\\\",\\n      \\\"storage.type\\\",\\n    ],\\n    settings: { foreground: \\\"#c635e4\\\" },\\n  },\\n  {\\n    scope: [\\\"support.class\\\", \\\"entity.name.type.class\\\"],\\n    settings: { foreground: \\\"#c635e4\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"entity.name.class\\\",\\n      \\\"variable.other.class.js\\\",\\n      \\\"variable.other.class.ts\\\",\\n    ],\\n    settings: { foreground: \\\"#c635e4\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.class.identifier.namespace.type\\\",\\n    settings: { foreground: \\\"#c635e4\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.type.namespace\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"entity.other.inherited-class\\\",\\n    settings: { foreground: \\\"#c635e4\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.namespace\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.logical\\\",\\n      \\\"keyword.operator.bitwise\\\",\\n      \\\"keyword.operator.channel\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.arithmetic\\\",\\n      \\\"keyword.operator.comparison\\\",\\n      \\\"keyword.operator.relational\\\",\\n      \\\"keyword.operator.increment\\\",\\n      \\\"keyword.operator.decrement\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.assignment\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.assignment.compound\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.assignment.compound.js\\\",\\n      \\\"keyword.operator.assignment.compound.ts\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.ternary\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.optional\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.separator.delimiter\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.separator.key-value\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.terminator\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"meta.brace\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"meta.brace.square\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"meta.brace.round\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"function.brace\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.parameters\\\",\\n      \\\"punctuation.definition.typeparameters\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\\"punctuation.definition.block\\\", \\\"punctuation.definition.tag\\\"],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\\"meta.tag.tsx\\\", \\\"meta.tag.jsx\\\", \\\"meta.tag.js\\\", \\\"meta.tag.ts\\\"],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.expression.import\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.module\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.object.console\\\",\\n    settings: { foreground: \\\"#d47628\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.module.node\\\",\\n      \\\"support.type.object.module\\\",\\n      \\\"entity.name.type.module\\\",\\n    ],\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.math\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.property.math\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.json\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.object.dom\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\\"support.variable.dom\\\", \\\"support.variable.property.dom\\\"],\\n    settings: { foreground: \\\"#d47628\\\" },\\n  },\\n  {\\n    scope: \\\"support.variable.property.process\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"meta.property.object\\\",\\n    settings: { foreground: \\\"#d47628\\\" },\\n  },\\n  {\\n    scope: \\\"variable.parameter.function.js\\\",\\n    settings: { foreground: \\\"#d47628\\\" },\\n  },\\n  {\\n    scope: [\\\"keyword.other.template.begin\\\", \\\"keyword.other.template.end\\\"],\\n    settings: { foreground: \\\"#199f43\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.other.substitution.begin\\\",\\n      \\\"keyword.other.substitution.end\\\",\\n    ],\\n    settings: { foreground: \\\"#199f43\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.template-expression.begin\\\",\\n      \\\"punctuation.definition.template-expression.end\\\",\\n    ],\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"meta.template.expression\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.section.embedded\\\",\\n    settings: { foreground: \\\"#d47628\\\" },\\n  },\\n  {\\n    scope: \\\"variable.interpolation\\\",\\n    settings: { foreground: \\\"#d47628\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.section.embedded.begin\\\",\\n      \\\"punctuation.section.embedded.end\\\",\\n    ],\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.quasi.element\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.type.primitive.ts\\\",\\n      \\\"support.type.builtin.ts\\\",\\n      \\\"support.type.primitive.tsx\\\",\\n      \\\"support.type.builtin.tsx\\\",\\n    ],\\n    settings: { foreground: \\\"#c635e4\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.type.flowtype\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.primitive\\\",\\n    settings: { foreground: \\\"#c635e4\\\" },\\n  },\\n  {\\n    scope: \\\"support.variable.magic.python\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"variable.parameter.function.language.special.self.python\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.separator.period.python\\\",\\n      \\\"punctuation.separator.element.python\\\",\\n      \\\"punctuation.parenthesis.begin.python\\\",\\n      \\\"punctuation.parenthesis.end.python\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.arguments.begin.python\\\",\\n      \\\"punctuation.definition.arguments.end.python\\\",\\n      \\\"punctuation.separator.arguments.python\\\",\\n      \\\"punctuation.definition.list.begin.python\\\",\\n      \\\"punctuation.definition.list.end.python\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.python\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.logical.python\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"meta.function-call.generic.python\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"constant.character.format.placeholder.other.python\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"meta.function.decorator.python\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.token.decorator.python\\\",\\n      \\\"meta.function.decorator.identifier.python\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"storage.modifier.lifetime.rust\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"support.function.std.rust\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.lifetime.rust\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"variable.language.rust\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.misc.rust\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.sigil.rust\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.core.rust\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\\"meta.function.c\\\", \\\"meta.function.cpp\\\"],\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.section.block.begin.bracket.curly.cpp\\\",\\n      \\\"punctuation.section.block.end.bracket.curly.cpp\\\",\\n      \\\"punctuation.terminator.statement.c\\\",\\n      \\\"punctuation.section.block.begin.bracket.curly.c\\\",\\n      \\\"punctuation.section.block.end.bracket.curly.c\\\",\\n      \\\"punctuation.section.parens.begin.bracket.round.c\\\",\\n      \\\"punctuation.section.parens.end.bracket.round.c\\\",\\n      \\\"punctuation.section.parameters.begin.bracket.round.c\\\",\\n      \\\"punctuation.section.parameters.end.bracket.round.c\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.assignment.c\\\",\\n      \\\"keyword.operator.comparison.c\\\",\\n      \\\"keyword.operator.c\\\",\\n      \\\"keyword.operator.increment.c\\\",\\n      \\\"keyword.operator.decrement.c\\\",\\n      \\\"keyword.operator.bitwise.shift.c\\\",\\n    ],\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.assignment.cpp\\\",\\n      \\\"keyword.operator.comparison.cpp\\\",\\n      \\\"keyword.operator.cpp\\\",\\n      \\\"keyword.operator.increment.cpp\\\",\\n      \\\"keyword.operator.decrement.cpp\\\",\\n      \\\"keyword.operator.bitwise.shift.cpp\\\",\\n    ],\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: [\\\"punctuation.separator.c\\\", \\\"punctuation.separator.cpp\\\"],\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: [\\\"support.type.posix-reserved.c\\\", \\\"support.type.posix-reserved.cpp\\\"],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\\"keyword.operator.sizeof.c\\\", \\\"keyword.operator.sizeof.cpp\\\"],\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"variable.c\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\\"storage.type.annotation.java\\\", \\\"storage.type.object.array.java\\\"],\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"source.java\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.section.block.begin.java\\\",\\n      \\\"punctuation.section.block.end.java\\\",\\n      \\\"punctuation.definition.method-parameters.begin.java\\\",\\n      \\\"punctuation.definition.method-parameters.end.java\\\",\\n      \\\"meta.method.identifier.java\\\",\\n      \\\"punctuation.section.method.begin.java\\\",\\n      \\\"punctuation.section.method.end.java\\\",\\n      \\\"punctuation.terminator.java\\\",\\n      \\\"punctuation.section.class.begin.java\\\",\\n      \\\"punctuation.section.class.end.java\\\",\\n      \\\"punctuation.section.inner-class.begin.java\\\",\\n      \\\"punctuation.section.inner-class.end.java\\\",\\n      \\\"meta.method-call.java\\\",\\n      \\\"punctuation.section.class.begin.bracket.curly.java\\\",\\n      \\\"punctuation.section.class.end.bracket.curly.java\\\",\\n      \\\"punctuation.section.method.begin.bracket.curly.java\\\",\\n      \\\"punctuation.section.method.end.bracket.curly.java\\\",\\n      \\\"punctuation.separator.period.java\\\",\\n      \\\"punctuation.bracket.angle.java\\\",\\n      \\\"punctuation.definition.annotation.java\\\",\\n      \\\"meta.method.body.java\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"meta.method.java\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"storage.modifier.import.java\\\",\\n      \\\"storage.type.java\\\",\\n      \\\"storage.type.generic.java\\\",\\n    ],\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.instanceof.java\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"meta.definition.variable.name.java\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"token.variable.parameter.java\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"import.storage.java\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"token.package.keyword\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"token.package\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"token.storage.type.java\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.assignment.go\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\\"keyword.operator.arithmetic.go\\\", \\\"keyword.operator.address.go\\\"],\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.package.go\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.other.namespace.use.php\\\",\\n      \\\"support.other.namespace.use-as.php\\\",\\n      \\\"support.other.namespace.php\\\",\\n      \\\"entity.other.alias.php\\\",\\n      \\\"meta.interface.php\\\",\\n    ],\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.error-control.php\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.type.php\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.section.array.begin.php\\\",\\n      \\\"punctuation.section.array.end.php\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"storage.type.php\\\",\\n      \\\"meta.other.type.phpdoc.php\\\",\\n      \\\"keyword.other.type.php\\\",\\n      \\\"keyword.other.array.phpdoc.php\\\",\\n    ],\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"meta.function-call.php\\\",\\n      \\\"meta.function-call.object.php\\\",\\n      \\\"meta.function-call.static.php\\\",\\n    ],\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.parameters.begin.bracket.round.php\\\",\\n      \\\"punctuation.definition.parameters.end.bracket.round.php\\\",\\n      \\\"punctuation.separator.delimiter.php\\\",\\n      \\\"punctuation.section.scope.begin.php\\\",\\n      \\\"punctuation.section.scope.end.php\\\",\\n      \\\"punctuation.terminator.expression.php\\\",\\n      \\\"punctuation.definition.arguments.begin.bracket.round.php\\\",\\n      \\\"punctuation.definition.arguments.end.bracket.round.php\\\",\\n      \\\"punctuation.definition.storage-type.begin.bracket.round.php\\\",\\n      \\\"punctuation.definition.storage-type.end.bracket.round.php\\\",\\n      \\\"punctuation.definition.array.begin.bracket.round.php\\\",\\n      \\\"punctuation.definition.array.end.bracket.round.php\\\",\\n      \\\"punctuation.definition.begin.bracket.round.php\\\",\\n      \\\"punctuation.definition.end.bracket.round.php\\\",\\n      \\\"punctuation.definition.begin.bracket.curly.php\\\",\\n      \\\"punctuation.definition.end.bracket.curly.php\\\",\\n      \\\"punctuation.definition.section.switch-block.end.bracket.curly.php\\\",\\n      \\\"punctuation.definition.section.switch-block.start.bracket.curly.php\\\",\\n      \\\"punctuation.definition.section.switch-block.begin.bracket.curly.php\\\",\\n      \\\"punctuation.definition.section.switch-block.end.bracket.curly.php\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.constant.ext.php\\\",\\n      \\\"support.constant.std.php\\\",\\n      \\\"support.constant.core.php\\\",\\n      \\\"support.constant.parser-token.php\\\",\\n    ],\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\\"entity.name.goto-label.php\\\", \\\"support.other.php\\\"],\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.logical.php\\\",\\n      \\\"keyword.operator.bitwise.php\\\",\\n      \\\"keyword.operator.arithmetic.php\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.regexp.php\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.comparison.php\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\\"keyword.operator.heredoc.php\\\", \\\"keyword.operator.nowdoc.php\\\"],\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"variable.other.class.php\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.illegal.non-null-typehinted.php\\\",\\n    settings: { foreground: \\\"#f44747\\\" },\\n  },\\n  {\\n    scope: \\\"variable.other.generic-type.haskell\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"storage.type.haskell\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"storage.type.cs\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.variable.local.cs\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.label.cs\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"entity.name.scope-resolution.function.call\\\",\\n      \\\"entity.name.scope-resolution.function.definition\\\",\\n    ],\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.delayed.unison\\\",\\n      \\\"punctuation.definition.list.begin.unison\\\",\\n      \\\"punctuation.definition.list.end.unison\\\",\\n      \\\"punctuation.definition.ability.begin.unison\\\",\\n      \\\"punctuation.definition.ability.end.unison\\\",\\n      \\\"punctuation.operator.assignment.as.unison\\\",\\n      \\\"punctuation.separator.pipe.unison\\\",\\n      \\\"punctuation.separator.delimiter.unison\\\",\\n      \\\"punctuation.definition.hash.unison\\\",\\n    ],\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.edge\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.prelude.elm\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.elm\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"entity.global.clojure\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"meta.symbol.clojure\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"constant.keyword.clojure\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\\"meta.arguments.coffee\\\", \\\"variable.parameter.function.coffee\\\"],\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"storage.modifier.import.groovy\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"meta.method.groovy\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"meta.definition.variable.name.groovy\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"meta.definition.class.inherited.classes.groovy\\\",\\n    settings: { foreground: \\\"#199f43\\\" },\\n  },\\n  {\\n    scope: \\\"support.variable.semantic.hlsl\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.type.texture.hlsl\\\",\\n      \\\"support.type.sampler.hlsl\\\",\\n      \\\"support.type.object.hlsl\\\",\\n      \\\"support.type.object.rw.hlsl\\\",\\n      \\\"support.type.fx.hlsl\\\",\\n      \\\"support.type.object.hlsl\\\",\\n    ],\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: [\\\"text.variable\\\", \\\"text.bracketed\\\"],\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: [\\\"support.type.swift\\\", \\\"support.type.vb.asp\\\"],\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"meta.scope.prerequisites.makefile\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"source.makefile\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"source.ini\\\",\\n    settings: { foreground: \\\"#199f43\\\" },\\n  },\\n  {\\n    scope: \\\"constant.language.symbol.ruby\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\\"function.parameter.ruby\\\", \\\"function.parameter.cs\\\"],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"constant.language.symbol.elixir\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope:\\n      \\\"text.html.laravel-blade source.php.embedded.line.html entity.name.tag.laravel-blade\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope:\\n      \\\"text.html.laravel-blade source.php.embedded.line.html support.constant.laravel-blade\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.function.xi\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.class.xi\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"constant.character.character-class.regexp.xi\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"constant.regexp.xi\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.control.xi\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.xi\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"beginning.punctuation.definition.quote.markdown.xi\\\",\\n    settings: { foreground: \\\"#199f43\\\" },\\n  },\\n  {\\n    scope: \\\"beginning.punctuation.definition.list.markdown.xi\\\",\\n    settings: { foreground: \\\"#84848A\\\" },\\n  },\\n  {\\n    scope: \\\"constant.character.xi\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"accent.xi\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"wikiword.xi\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"constant.other.color.rgb-value.xi\\\",\\n    settings: { foreground: \\\"#ffffff\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.tag.xi\\\",\\n    settings: { foreground: \\\"#84848A\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.constant.property-value.scss\\\",\\n      \\\"support.constant.property-value.css\\\",\\n    ],\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.css\\\",\\n      \\\"keyword.operator.scss\\\",\\n      \\\"keyword.operator.less\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.constant.color.w3c-standard-color-name.css\\\",\\n      \\\"support.constant.color.w3c-standard-color-name.scss\\\",\\n    ],\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.separator.list.comma.css\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.vendored.property-name.css\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.property-name.css\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.property-name\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.property-value\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.font-name\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"entity.other.attribute-name.class.css\\\",\\n    settings: {\\n      foreground: \\\"#16a994\\\",\\n      fontStyle: \\\"normal\\\",\\n    },\\n  },\\n  {\\n    scope: \\\"entity.other.attribute-name.id\\\",\\n    settings: {\\n      foreground: \\\"#7b43f8\\\",\\n      fontStyle: \\\"normal\\\",\\n    },\\n  },\\n  {\\n    scope: [\\n      \\\"entity.other.attribute-name.pseudo-element\\\",\\n      \\\"entity.other.attribute-name.pseudo-class\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"meta.selector\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"selector.sass\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"rgb-value\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"inline-color-decoration rgb-value\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"less rgb-value\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"control.elements\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.less\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.tag\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"entity.other.attribute-name\\\",\\n    settings: {\\n      foreground: \\\"#16a994\\\",\\n      fontStyle: \\\"normal\\\",\\n    },\\n  },\\n  {\\n    scope: \\\"constant.character.entity\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"meta.tag\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.illegal.bad-ampersand.html\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"markup.heading\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"markup.heading punctuation.definition.heading\\\",\\n      \\\"entity.name.section\\\",\\n    ],\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.section.markdown\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.heading.markdown\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"markup.heading.setext\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"markup.heading.setext.1.markdown\\\",\\n      \\\"markup.heading.setext.2.markdown\\\",\\n    ],\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: [\\\"markup.bold\\\", \\\"todo.bold\\\"],\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.bold\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.bold.markdown\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\\"markup.italic\\\", \\\"punctuation.definition.italic\\\", \\\"todo.emphasis\\\"],\\n    settings: {\\n      foreground: \\\"#fc2b73\\\",\\n      fontStyle: \\\"italic\\\",\\n    },\\n  },\\n  {\\n    scope: \\\"emphasis md\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"markup.italic.markdown\\\",\\n    settings: { fontStyle: \\\"italic\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"markup.underline.link.markdown\\\",\\n      \\\"markup.underline.link.image.markdown\\\",\\n    ],\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"string.other.link.title.markdown\\\",\\n      \\\"string.other.link.description.markdown\\\",\\n    ],\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.metadata.markdown\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: [\\\"markup.inline.raw.markdown\\\", \\\"markup.inline.raw.string.markdown\\\"],\\n    settings: { foreground: \\\"#199f43\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.list.begin.markdown\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.list.markdown\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"beginning.punctuation.definition.list.markdown\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.string.begin.markdown\\\",\\n      \\\"punctuation.definition.string.end.markdown\\\",\\n    ],\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"markup.quote.markdown\\\",\\n    settings: { foreground: \\\"#84848A\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.other.unit\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"markup.changed.diff\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"meta.diff.header.from-file\\\",\\n      \\\"meta.diff.header.to-file\\\",\\n      \\\"punctuation.definition.from-file.diff\\\",\\n      \\\"punctuation.definition.to-file.diff\\\",\\n    ],\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"markup.inserted.diff\\\",\\n    settings: { foreground: \\\"#199f43\\\" },\\n  },\\n  {\\n    scope: \\\"markup.deleted.diff\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"string.regexp\\\",\\n    settings: { foreground: \\\"#17a5af\\\" },\\n  },\\n  {\\n    scope: \\\"constant.other.character-class.regexp\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.quantifier.regexp\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"constant.character.escape\\\",\\n    settings: { foreground: \\\"#1ca1c7\\\" },\\n  },\\n  {\\n    scope: \\\"source.json meta.structure.dictionary.json > string.quoted.json\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope:\\n      \\\"source.json meta.structure.dictionary.json > string.quoted.json > punctuation.string\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"source.json meta.structure.dictionary.json > value.json > string.quoted.json\\\",\\n      \\\"source.json meta.structure.array.json > value.json > string.quoted.json\\\",\\n      \\\"source.json meta.structure.dictionary.json > value.json > string.quoted.json > punctuation\\\",\\n      \\\"source.json meta.structure.array.json > value.json > string.quoted.json > punctuation\\\",\\n    ],\\n    settings: { foreground: \\\"#199f43\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"source.json meta.structure.dictionary.json > constant.language.json\\\",\\n      \\\"source.json meta.structure.array.json > constant.language.json\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.property-name.json\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.property-name.json punctuation\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.block.sequence.item.yaml\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"block.scope.end\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"block.scope.begin\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"token.info-token\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"token.warn-token\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"token.error-token\\\",\\n    settings: { foreground: \\\"#f44747\\\" },\\n  },\\n  {\\n    scope: \\\"token.debug-token\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.illegal\\\",\\n    settings: { foreground: \\\"#ffffff\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.broken\\\",\\n    settings: { foreground: \\\"#ffffff\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.deprecated\\\",\\n    settings: { foreground: \\\"#ffffff\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.unimplemented\\\",\\n    settings: { foreground: \\\"#ffffff\\\" },\\n  },\\n];\\nvar semanticTokenColors = {\\n  comment: \\\"#84848A\\\",\\n  string: \\\"#199f43\\\",\\n  number: \\\"#1ca1c7\\\",\\n  regexp: \\\"#17a5af\\\",\\n  keyword: \\\"#fc2b73\\\",\\n  variable: \\\"#d47628\\\",\\n  parameter: \\\"#79797F\\\",\\n  property: \\\"#d47628\\\",\\n  function: \\\"#7b43f8\\\",\\n  method: \\\"#7b43f8\\\",\\n  type: \\\"#c635e4\\\",\\n  class: \\\"#c635e4\\\",\\n  namespace: \\\"#d5a910\\\",\\n  enumMember: \\\"#08c0ef\\\",\\n  \\\"variable.constant\\\": \\\"#d5a910\\\",\\n  \\\"variable.defaultLibrary\\\": \\\"#d5a910\\\",\\n};\\nvar pierre_light_default = {\\n  name,\\n  type,\\n  colors,\\n  tokenColors,\\n  semanticTokenColors,\\n};\\n\\n//#endregion\\nexport {\\n  colors,\\n  pierre_light_default as default,\\n  name,\\n  semanticTokenColors,\\n  tokenColors,\\n  type,\\n};\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { ReactNode } from \\\"react\\\";\\n\\n/**\\n * Tool UI conventions:\\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\\n * - Schema: `SerializableXSchema`\\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\\n * - Root attrs: `data-tool-ui-id` + `data-slot`\\n */\\n\\n/**\\n * Schema for tool UI identity.\\n *\\n * Every tool UI should have a unique identifier that:\\n * - Is stable across re-renders\\n * - Is meaningful (not auto-generated)\\n * - Is unique within the conversation\\n *\\n * Format recommendation: `{component-type}-{semantic-identifier}`\\n * Examples: \\\"data-table-expenses-q3\\\", \\\"option-list-deploy-target\\\"\\n */\\nexport const ToolUIIdSchema = z.string().min(1);\\n\\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\\n\\n/**\\n * Primary role of a Tool UI surface in a chat context.\\n */\\nexport const ToolUIRoleSchema = z.enum([\\n  \\\"information\\\",\\n  \\\"decision\\\",\\n  \\\"control\\\",\\n  \\\"state\\\",\\n  \\\"composite\\\",\\n]);\\n\\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\\n\\nexport const ToolUIReceiptOutcomeSchema = z.enum([\\n  \\\"success\\\",\\n  \\\"partial\\\",\\n  \\\"failed\\\",\\n  \\\"cancelled\\\",\\n]);\\n\\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\\n\\n/**\\n * Optional receipt metadata: a durable summary of an outcome.\\n */\\nexport const ToolUIReceiptSchema = z.object({\\n  outcome: ToolUIReceiptOutcomeSchema,\\n  summary: z.string().min(1),\\n  identifiers: z.record(z.string(), z.string()).optional(),\\n  at: z.string().datetime(),\\n});\\n\\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\\n\\n/**\\n * Base schema for Tool UI payloads (id + optional role/receipt).\\n */\\nexport const ToolUISurfaceSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n});\\n\\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\\n\\nexport const ActionSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  /**\\n   * Canonical narration the assistant can use after this action is taken.\\n   *\\n   * Example: \\\"I exported the table as CSV.\\\" / \\\"I opened the link in a new tab.\\\"\\n   */\\n  sentence: z.string().optional(),\\n  confirmLabel: z.string().optional(),\\n  variant: z\\n    .enum([\\\"default\\\", \\\"destructive\\\", \\\"secondary\\\", \\\"ghost\\\", \\\"outline\\\"])\\n    .optional(),\\n  icon: z.custom<ReactNode>().optional(),\\n  loading: z.boolean().optional(),\\n  disabled: z.boolean().optional(),\\n  shortcut: z.string().optional(),\\n});\\n\\nexport type Action = z.infer<typeof ActionSchema>;\\nexport type LocalAction = Action;\\nexport type DecisionAction = Action;\\n\\nexport const DecisionResultSchema = z.object({\\n  kind: z.literal(\\\"decision\\\"),\\n  version: z.literal(1),\\n  decisionId: z.string().min(1),\\n  actionId: z.string().min(1),\\n  actionLabel: z.string().min(1),\\n  at: z.string().datetime(),\\n  payload: z.record(z.string(), z.unknown()).optional(),\\n});\\n\\nexport type DecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n> = Omit<z.infer<typeof DecisionResultSchema>, \\\"payload\\\"> & {\\n  payload?: TPayload;\\n};\\n\\nexport function createDecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n>(args: {\\n  decisionId: string;\\n  action: { id: string; label: string };\\n  payload?: TPayload;\\n}): DecisionResult<TPayload> {\\n  return {\\n    kind: \\\"decision\\\",\\n    version: 1,\\n    decisionId: args.decisionId,\\n    actionId: args.action.id,\\n    actionLabel: args.action.label,\\n    at: new Date().toISOString(),\\n    payload: args.payload,\\n  };\\n}\\n\\nexport const ActionButtonsPropsSchema = z.object({\\n  actions: z.array(ActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n  className: z.string().optional(),\\n});\\n\\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\\n  actions: z.array(SerializableActionSchema),\\n}).omit({ className: true });\\n\\nexport interface ActionsConfig {\\n  items: Action[];\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  confirmTimeout?: number;\\n}\\n\\nexport const SerializableActionsConfigSchema = z.object({\\n  items: z.array(SerializableActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n});\\n\\nexport type SerializableActionsConfig = z.infer<\\n  typeof SerializableActionsConfigSchema\\n>;\\n\\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/use-copy-to-clipboard.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/use-copy-to-clipboard.ts\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { useCallback, useEffect, useState } from \\\"react\\\";\\n\\nfunction fallbackCopyToClipboard(text: string): boolean {\\n  const textArea = document.createElement(\\\"textarea\\\");\\n  try {\\n    textArea.value = text;\\n    textArea.setAttribute(\\\"readonly\\\", \\\"\\\");\\n    textArea.style.position = \\\"fixed\\\";\\n    textArea.style.top = \\\"-9999px\\\";\\n    textArea.style.left = \\\"-9999px\\\";\\n    document.body.appendChild(textArea);\\n    textArea.select();\\n    return document.execCommand(\\\"copy\\\");\\n  } catch {\\n    return false;\\n  } finally {\\n    if (textArea.parentNode) {\\n      textArea.parentNode.removeChild(textArea);\\n    }\\n  }\\n}\\n\\nexport function useCopyToClipboard(options?: { resetAfterMs?: number }): {\\n  copiedId: string | null;\\n  copy: (text: string, id?: string) => Promise<boolean>;\\n} {\\n  const resetAfterMs = options?.resetAfterMs ?? 2000;\\n  const [copiedId, setCopiedId] = useState<string | null>(null);\\n\\n  const copy = useCallback(async (text: string, id: string = \\\"default\\\") => {\\n    let ok = false;\\n    try {\\n      if (navigator.clipboard?.writeText) {\\n        await navigator.clipboard.writeText(text);\\n        ok = true;\\n      } else {\\n        ok = fallbackCopyToClipboard(text);\\n      }\\n    } catch {\\n      ok = fallbackCopyToClipboard(text);\\n    }\\n\\n    if (ok) {\\n      setCopiedId(id);\\n    }\\n\\n    return ok;\\n  }, []);\\n\\n  useEffect(() => {\\n    if (!copiedId) return;\\n    const timeout = setTimeout(() => setCopiedId(null), resetAfterMs);\\n    return () => clearTimeout(timeout);\\n  }, [copiedId, resetAfterMs]);\\n\\n  return { copiedId, copy };\\n}\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/code-diff.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"code-diff\",\n  \"type\": \"registry:block\",\n  \"title\": \"Code Diff\",\n  \"description\": \"Code Diff component for AI interfaces.\",\n  \"dependencies\": [\n    \"@pierre/diffs\",\n    \"lucide-react\",\n    \"zod\"\n  ],\n  \"registryDependencies\": [\n    \"button\",\n    \"collapsible\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/code-diff/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/code-diff/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn          -> Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n *   Button      -> shadcn/ui Button\\n *   Collapsible -> shadcn/ui Collapsible\\n */\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport { Button } from \\\"@/components/ui/button\\\";\\nexport { Collapsible, CollapsibleTrigger } from \\\"@/components/ui/collapsible\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/code-diff/code-diff.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/code-diff/code-diff.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport {\\n  useState,\\n  useCallback,\\n  useEffect,\\n  useMemo,\\n  createContext,\\n  use,\\n  type ReactNode,\\n} from \\\"react\\\";\\nimport {\\n  FileDiff as PierreFileDiff,\\n  PatchDiff as PierrePatchDiff,\\n} from \\\"@pierre/diffs/react\\\";\\nimport { parseDiffFromFile, RegisteredCustomThemes } from \\\"@pierre/diffs\\\";\\nimport type { FileDiffMetadata, ThemesType } from \\\"@pierre/diffs\\\";\\nimport { Copy, Check, ChevronDown, ChevronUp } from \\\"lucide-react\\\";\\nimport type { CodeDiffProps } from \\\"./schema\\\";\\nimport { useCopyToClipboard } from \\\"../shared/use-copy-to-clipboard\\\";\\nimport { Button, cn, Collapsible, CollapsibleTrigger } from \\\"./_adapter\\\";\\n\\n/*\\n * Pierre's shared_highlighter registers custom themes with dynamic imports\\n * (`import(\\\"../themes/pierre-dark.js\\\")`) that fail under Turbopack because the\\n * package `exports` field doesn't include those subpaths. We override the\\n * RegisteredCustomThemes map entries with loaders that point to local vendored\\n * theme files in `components/tool-ui/shared`, which Turbopack can resolve.\\n */\\nRegisteredCustomThemes.set(\\\"pierre-dark\\\", () =>\\n  import(\\\"../shared/pierre-dark-theme.js\\\").then((m) => m.default as never),\\n);\\nRegisteredCustomThemes.set(\\\"pierre-light\\\", () =>\\n  import(\\\"../shared/pierre-light-theme.js\\\").then((m) => m.default as never),\\n);\\n\\nconst COPY_ID = \\\"codediff-code\\\";\\n\\n/* ── Theme detection (mirrors CodeBlock) ────────────────────────── */\\n\\nfunction getSystemTheme(): \\\"light\\\" | \\\"dark\\\" {\\n  if (typeof window === \\\"undefined\\\") return \\\"light\\\";\\n  return window.matchMedia?.(\\\"(prefers-color-scheme: dark)\\\").matches\\n    ? \\\"dark\\\"\\n    : \\\"light\\\";\\n}\\n\\nfunction getDocumentTheme(): \\\"light\\\" | \\\"dark\\\" | null {\\n  if (typeof document === \\\"undefined\\\") return null;\\n  const root = document.documentElement;\\n  const dataTheme = root.getAttribute(\\\"data-theme\\\")?.toLowerCase();\\n  if (dataTheme === \\\"dark\\\") return \\\"dark\\\";\\n  if (dataTheme === \\\"light\\\") return \\\"light\\\";\\n  if (root.classList.contains(\\\"dark\\\")) return \\\"dark\\\";\\n  if (root.classList.contains(\\\"light\\\")) return \\\"light\\\";\\n  return null;\\n}\\n\\nfunction useResolvedTheme(): \\\"light\\\" | \\\"dark\\\" {\\n  const [theme, setTheme] = useState<\\\"light\\\" | \\\"dark\\\">(() => {\\n    return getDocumentTheme() ?? getSystemTheme();\\n  });\\n\\n  useEffect(() => {\\n    if (typeof window === \\\"undefined\\\" || typeof document === \\\"undefined\\\") {\\n      return;\\n    }\\n\\n    const update = () => setTheme(getDocumentTheme() ?? getSystemTheme());\\n\\n    const mql = window.matchMedia?.(\\\"(prefers-color-scheme: dark)\\\");\\n    mql?.addEventListener(\\\"change\\\", update);\\n\\n    const observer = new MutationObserver(update);\\n    observer.observe(document.documentElement, {\\n      attributes: true,\\n      attributeFilter: [\\\"class\\\", \\\"data-theme\\\"],\\n    });\\n\\n    return () => {\\n      mql?.removeEventListener(\\\"change\\\", update);\\n      observer.disconnect();\\n    };\\n  }, []);\\n\\n  return theme;\\n}\\n\\n/* ── Language display names (mirrors CodeBlock) ─────────────────── */\\n\\nconst LANGUAGE_DISPLAY_NAMES: Record<string, string> = {\\n  typescript: \\\"TypeScript\\\",\\n  javascript: \\\"JavaScript\\\",\\n  python: \\\"Python\\\",\\n  tsx: \\\"TSX\\\",\\n  jsx: \\\"JSX\\\",\\n  json: \\\"JSON\\\",\\n  bash: \\\"Bash\\\",\\n  shell: \\\"Shell\\\",\\n  css: \\\"CSS\\\",\\n  html: \\\"HTML\\\",\\n  markdown: \\\"Markdown\\\",\\n  sql: \\\"SQL\\\",\\n  yaml: \\\"YAML\\\",\\n  go: \\\"Go\\\",\\n  rust: \\\"Rust\\\",\\n  text: \\\"Plain Text\\\",\\n};\\n\\nfunction getLanguageDisplayName(lang: string): string {\\n  return LANGUAGE_DISPLAY_NAMES[lang.toLowerCase()] || lang.toUpperCase();\\n}\\n\\n/* ── Shared context ─────────────────────────────────────────────── */\\n\\ntype CodeDiffSharedState = {\\n  id: string;\\n  isPatchMode: boolean;\\n  language: string;\\n  lineNumbers: \\\"visible\\\" | \\\"hidden\\\";\\n  filename?: string;\\n  diffStyle: \\\"unified\\\" | \\\"split\\\";\\n  copyableCode: string;\\n  isCopied: boolean;\\n  copyCode: () => void;\\n  isCollapsed: boolean;\\n  shouldCollapse: boolean;\\n  toggleExpanded: () => void;\\n  resolvedTheme: \\\"light\\\" | \\\"dark\\\";\\n  pierreThemes: ThemesType;\\n  fileDiffMetadata: FileDiffMetadata | null;\\n  patch: string | null;\\n  additions: number;\\n  deletions: number;\\n};\\n\\nconst CodeDiffContext = createContext<CodeDiffSharedState | null>(null);\\n\\nfunction useCodeDiff(): CodeDiffSharedState {\\n  const context = use(CodeDiffContext);\\n  if (!context) {\\n    throw new Error(\\n      \\\"CodeDiff subcomponents must be used within <CodeDiff.Root>.\\\",\\n    );\\n  }\\n  return context;\\n}\\n\\n/* ── Subcomponents ──────────────────────────────────────────────── */\\n\\nexport type CodeDiffRootProps = CodeDiffProps & {\\n  children: ReactNode;\\n  expanded?: boolean;\\n  defaultExpanded?: boolean;\\n  onExpandedChange?: (expanded: boolean) => void;\\n};\\n\\nfunction CodeDiffRoot({\\n  id,\\n  oldCode,\\n  newCode,\\n  patch,\\n  language = \\\"text\\\",\\n  filename,\\n  lineNumbers = \\\"visible\\\",\\n  diffStyle = \\\"unified\\\",\\n  maxCollapsedLines,\\n  className,\\n  children,\\n  expanded: expandedProp,\\n  defaultExpanded = false,\\n  onExpandedChange,\\n}: CodeDiffRootProps) {\\n  const resolvedTheme = useResolvedTheme();\\n  const [expandedState, setExpandedState] = useState(defaultExpanded);\\n  const { copiedId, copy } = useCopyToClipboard();\\n  const isCopied = copiedId === COPY_ID;\\n\\n  const expanded = expandedProp ?? expandedState;\\n  const setExpanded = useCallback(\\n    (nextExpanded: boolean) => {\\n      if (expandedProp === undefined) {\\n        setExpandedState(nextExpanded);\\n      }\\n      onExpandedChange?.(nextExpanded);\\n    },\\n    [expandedProp, onExpandedChange],\\n  );\\n\\n  const pierreThemes: ThemesType = {\\n    dark: \\\"pierre-dark\\\",\\n    light: \\\"pierre-light\\\",\\n  };\\n\\n  // Auto-detect mode: if `patch` is provided, use patch mode; otherwise files mode\\n  const isPatchMode = !!patch;\\n\\n  const fileDiffMetadata = useMemo(() => {\\n    if (isPatchMode) return null;\\n    return parseDiffFromFile(\\n      {\\n        name: filename ?? \\\"file\\\",\\n        contents: oldCode ?? \\\"\\\",\\n        lang: language as never,\\n      },\\n      {\\n        name: filename ?? \\\"file\\\",\\n        contents: newCode ?? \\\"\\\",\\n        lang: language as never,\\n      },\\n    );\\n  }, [isPatchMode, oldCode, newCode, filename, language]);\\n\\n  const copyableCode = isPatchMode ? (patch ?? \\\"\\\") : (newCode ?? oldCode ?? \\\"\\\");\\n\\n  const lineCount = useMemo(() => {\\n    if (isPatchMode) {\\n      return (patch ?? \\\"\\\").split(\\\"\\\\n\\\").length;\\n    }\\n    if (fileDiffMetadata) {\\n      return fileDiffMetadata.unifiedLineCount;\\n    }\\n    return 0;\\n  }, [isPatchMode, patch, fileDiffMetadata]);\\n\\n  const { additions, deletions } = useMemo(() => {\\n    if (!isPatchMode && fileDiffMetadata) {\\n      let add = 0;\\n      let del = 0;\\n      for (const hunk of fileDiffMetadata.hunks) {\\n        add += hunk.additionLines;\\n        del += hunk.deletionLines;\\n      }\\n      return { additions: add, deletions: del };\\n    }\\n    if (isPatchMode && patch) {\\n      let add = 0;\\n      let del = 0;\\n      for (const line of patch.split(\\\"\\\\n\\\")) {\\n        if (line.startsWith(\\\"+\\\") && !line.startsWith(\\\"+++ \\\")) add++;\\n        else if (line.startsWith(\\\"-\\\") && !line.startsWith(\\\"--- \\\")) del++;\\n      }\\n      return { additions: add, deletions: del };\\n    }\\n    return { additions: 0, deletions: 0 };\\n  }, [isPatchMode, fileDiffMetadata, patch]);\\n\\n  const shouldCollapse = !!maxCollapsedLines && lineCount > maxCollapsedLines;\\n  const isCollapsed = shouldCollapse && !expanded;\\n\\n  const copyCode = useCallback(() => {\\n    void copy(copyableCode, COPY_ID);\\n  }, [copyableCode, copy]);\\n\\n  const toggleExpanded = useCallback(() => {\\n    setExpanded(!expanded);\\n  }, [expanded, setExpanded]);\\n\\n  const state: CodeDiffSharedState = {\\n    id,\\n    isPatchMode,\\n    language,\\n    lineNumbers,\\n    filename,\\n    diffStyle,\\n    copyableCode,\\n    isCopied,\\n    copyCode,\\n    isCollapsed,\\n    shouldCollapse,\\n    toggleExpanded,\\n    resolvedTheme,\\n    pierreThemes,\\n    fileDiffMetadata,\\n    patch: isPatchMode ? (patch ?? null) : null,\\n    additions,\\n    deletions,\\n  };\\n\\n  return (\\n    <CodeDiffContext.Provider value={state}>\\n      <div\\n        className={cn(\\n          \\\"@container flex w-full min-w-80 flex-col gap-3\\\",\\n          className,\\n        )}\\n        data-tool-ui-id={id}\\n        data-slot=\\\"code-diff\\\"\\n      >\\n        <div className=\\\"border-border bg-card overflow-hidden rounded-lg border shadow-xs\\\">\\n          <Collapsible open={!isCollapsed}>{children}</Collapsible>\\n        </div>\\n      </div>\\n    </CodeDiffContext.Provider>\\n  );\\n}\\n\\nexport type CodeDiffSectionProps = {\\n  className?: string;\\n};\\n\\nfunction CodeDiffHeader({ className }: CodeDiffSectionProps) {\\n  const { language, filename, isCopied, copyCode, additions, deletions } =\\n    useCodeDiff();\\n  const hasChanges = additions > 0 || deletions > 0;\\n  return (\\n    <div\\n      className={cn(\\n        \\\"bg-card flex items-center justify-between gap-2 border-b px-4 py-2\\\",\\n        className,\\n      )}\\n    >\\n      <div className=\\\"flex items-center gap-1\\\">\\n        <span className=\\\"text-muted-foreground text-sm\\\">\\n          {getLanguageDisplayName(language)}\\n        </span>\\n        {filename && (\\n          <>\\n            <span className=\\\"text-muted-foreground/50\\\">&bull;</span>\\n            <span className=\\\"text-foreground text-sm font-medium\\\">\\n              {filename}\\n            </span>\\n          </>\\n        )}\\n      </div>\\n      {hasChanges && (\\n        <span className=\\\"ml-auto text-xs font-mono tabular-nums\\\">\\n          {additions > 0 && (\\n            <span style={{ color: \\\"#00cab1\\\" }}>+{additions}</span>\\n          )}\\n          {additions > 0 && deletions > 0 && \\\" \\\"}\\n          {deletions > 0 && (\\n            <span style={{ color: \\\"#ff2e3f\\\" }}>-{deletions}</span>\\n          )}\\n        </span>\\n      )}\\n      <Button\\n        variant=\\\"ghost\\\"\\n        size=\\\"sm\\\"\\n        onClick={copyCode}\\n        className=\\\"h-7 w-7 p-0\\\"\\n        aria-label={isCopied ? \\\"Copied\\\" : \\\"Copy code\\\"}\\n      >\\n        {isCopied ? (\\n          <Check className=\\\"h-4 w-4 text-green-700 dark:text-green-400\\\" />\\n        ) : (\\n          <Copy className=\\\"text-muted-foreground h-4 w-4\\\" />\\n        )}\\n      </Button>\\n    </div>\\n  );\\n}\\n\\nfunction CodeDiffContent({ className }: CodeDiffSectionProps) {\\n  const {\\n    isPatchMode,\\n    diffStyle,\\n    lineNumbers,\\n    isCollapsed,\\n    resolvedTheme,\\n    pierreThemes,\\n    fileDiffMetadata,\\n    patch,\\n  } = useCodeDiff();\\n\\n  const disableLineNumbers = lineNumbers === \\\"hidden\\\";\\n\\n  return (\\n    <div\\n      className={cn(\\n        \\\"overflow-x-auto overflow-y-clip text-sm\\\",\\n        isCollapsed && \\\"max-h-[200px]\\\",\\n        className,\\n      )}\\n    >\\n      {!isPatchMode && fileDiffMetadata && (\\n        <PierreFileDiff\\n          fileDiff={fileDiffMetadata}\\n          options={{\\n            theme: pierreThemes,\\n            themeType: resolvedTheme,\\n            diffStyle,\\n            disableFileHeader: true,\\n            disableLineNumbers,\\n          }}\\n        />\\n      )}\\n      {isPatchMode && patch && (\\n        <PierrePatchDiff\\n          patch={patch}\\n          options={{\\n            theme: pierreThemes,\\n            themeType: resolvedTheme,\\n            diffStyle,\\n            disableFileHeader: true,\\n            disableLineNumbers,\\n          }}\\n        />\\n      )}\\n    </div>\\n  );\\n}\\n\\nfunction CodeDiffCollapseToggle({ className }: CodeDiffSectionProps) {\\n  const { shouldCollapse, isCollapsed, toggleExpanded } = useCodeDiff();\\n\\n  if (!shouldCollapse) return null;\\n\\n  return (\\n    <CollapsibleTrigger asChild>\\n      <Button\\n        variant=\\\"ghost\\\"\\n        onClick={toggleExpanded}\\n        className={cn(\\n          \\\"text-muted-foreground w-full rounded-none border-t font-normal\\\",\\n          className,\\n        )}\\n      >\\n        {isCollapsed ? (\\n          <>\\n            <ChevronDown className=\\\"mr-1 size-4\\\" />\\n            Show full diff\\n          </>\\n        ) : (\\n          <>\\n            <ChevronUp className=\\\"mr-2 h-4 w-4\\\" />\\n            Collapse\\n          </>\\n        )}\\n      </Button>\\n    </CollapsibleTrigger>\\n  );\\n}\\n\\n/* ── Composed preset (callable as a flat component) ─────────────── */\\n\\nexport type CodeDiffComposedProps = Omit<CodeDiffRootProps, \\\"children\\\">;\\n\\nfunction CodeDiffComposed(props: CodeDiffComposedProps) {\\n  return (\\n    <CodeDiffRoot {...props}>\\n      <CodeDiffHeader />\\n      <CodeDiffContent />\\n      <CodeDiffCollapseToggle />\\n    </CodeDiffRoot>\\n  );\\n}\\n\\n/* ── Compound export: CodeDiff is callable AND has subcomponents ── */\\n\\ntype CodeDiffComponent = typeof CodeDiffComposed & {\\n  Root: typeof CodeDiffRoot;\\n  Header: typeof CodeDiffHeader;\\n  Content: typeof CodeDiffContent;\\n  CollapseToggle: typeof CodeDiffCollapseToggle;\\n};\\n\\nexport const CodeDiff = Object.assign(CodeDiffComposed, {\\n  Root: CodeDiffRoot,\\n  Header: CodeDiffHeader,\\n  Content: CodeDiffContent,\\n  CollapseToggle: CodeDiffCollapseToggle,\\n}) as CodeDiffComponent;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/code-diff/index.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/code-diff/index.tsx\",\n      \"content\": \"export { CodeDiff } from \\\"./code-diff\\\";\\nexport type {\\n  CodeDiffRootProps,\\n  CodeDiffComposedProps,\\n  CodeDiffSectionProps,\\n} from \\\"./code-diff\\\";\\nexport type { CodeDiffProps, SerializableCodeDiff } from \\\"./schema\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/code-diff/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/code-diff/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\nimport {\\n  ToolUIIdSchema,\\n  ToolUIReceiptSchema,\\n  ToolUIRoleSchema,\\n} from \\\"../shared/schema\\\";\\n\\nconst CodeDiffPropsSchemaBase = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n  oldCode: z.string().optional(),\\n  newCode: z.string().optional(),\\n  patch: z.string().optional(),\\n  language: z.string().trim().min(1).default(\\\"text\\\"),\\n  filename: z.string().optional(),\\n  lineNumbers: z.enum([\\\"visible\\\", \\\"hidden\\\"]).default(\\\"visible\\\"),\\n  diffStyle: z.enum([\\\"unified\\\", \\\"split\\\"]).default(\\\"unified\\\"),\\n  maxCollapsedLines: z.number().min(1).optional(),\\n  className: z.string().optional(),\\n});\\n\\nfunction validateCodeDiffInputMode(\\n  data: { patch?: string; oldCode?: string; newCode?: string },\\n  ctx: z.RefinementCtx,\\n) {\\n  const hasPatch = !!data.patch;\\n  const hasFiles = !!data.oldCode || !!data.newCode;\\n\\n  if (!hasPatch && !hasFiles) {\\n    ctx.addIssue({\\n      code: \\\"custom\\\",\\n      message:\\n        \\\"Provide either a patch string or at least one of oldCode/newCode\\\",\\n    });\\n  }\\n\\n  if (hasPatch && hasFiles) {\\n    ctx.addIssue({\\n      code: \\\"custom\\\",\\n      message:\\n        \\\"Cannot mix patch mode with oldCode/newCode — use one or the other\\\",\\n    });\\n  }\\n}\\n\\nexport const CodeDiffPropsSchema = CodeDiffPropsSchemaBase.superRefine(\\n  validateCodeDiffInputMode,\\n);\\n\\nexport type CodeDiffProps = z.infer<typeof CodeDiffPropsSchema>;\\n\\nexport const SerializableCodeDiffSchema = CodeDiffPropsSchemaBase.omit({\\n  className: true,\\n}).superRefine(validateCodeDiffInputMode);\\n\\nexport type SerializableCodeDiff = z.infer<typeof SerializableCodeDiffSchema>;\\n\\nconst SerializableCodeDiffSchemaContract = defineToolUiContract(\\n  \\\"CodeDiff\\\",\\n  SerializableCodeDiffSchema,\\n);\\n\\nexport const parseSerializableCodeDiff: (\\n  input: unknown,\\n) => SerializableCodeDiff = SerializableCodeDiffSchemaContract.parse;\\n\\nexport const safeParseSerializableCodeDiff: (\\n  input: unknown,\\n) => SerializableCodeDiff | null = SerializableCodeDiffSchemaContract.safeParse;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/pierre-dark-theme.js\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/shared/pierre-dark-theme.js\",\n      \"content\": \"//#region src/themes/pierre-dark.json\\nvar name = \\\"pierre-dark\\\";\\nvar type = \\\"dark\\\";\\nvar colors = {\\n  \\\"editor.background\\\": \\\"#070707\\\",\\n  \\\"editor.foreground\\\": \\\"#fbfbfb\\\",\\n  foreground: \\\"#fbfbfb\\\",\\n  focusBorder: \\\"#009fff\\\",\\n  \\\"selection.background\\\": \\\"#19283c\\\",\\n  \\\"editor.selectionBackground\\\": \\\"#009fff4d\\\",\\n  \\\"editor.lineHighlightBackground\\\": \\\"#19283c8c\\\",\\n  \\\"editorCursor.foreground\\\": \\\"#009fff\\\",\\n  \\\"editorLineNumber.foreground\\\": \\\"#84848A\\\",\\n  \\\"editorLineNumber.activeForeground\\\": \\\"#adadb1\\\",\\n  \\\"editorIndentGuide.background\\\": \\\"#39393c\\\",\\n  \\\"editorIndentGuide.activeBackground\\\": \\\"#2e2e30\\\",\\n  \\\"diffEditor.insertedTextBackground\\\": \\\"#00cab11a\\\",\\n  \\\"diffEditor.deletedTextBackground\\\": \\\"#ff2e3f1a\\\",\\n  \\\"sideBar.background\\\": \\\"#141415\\\",\\n  \\\"sideBar.foreground\\\": \\\"#adadb1\\\",\\n  \\\"sideBar.border\\\": \\\"#070707\\\",\\n  \\\"sideBarTitle.foreground\\\": \\\"#fbfbfb\\\",\\n  \\\"sideBarSectionHeader.background\\\": \\\"#141415\\\",\\n  \\\"sideBarSectionHeader.foreground\\\": \\\"#adadb1\\\",\\n  \\\"sideBarSectionHeader.border\\\": \\\"#070707\\\",\\n  \\\"activityBar.background\\\": \\\"#141415\\\",\\n  \\\"activityBar.foreground\\\": \\\"#fbfbfb\\\",\\n  \\\"activityBar.border\\\": \\\"#070707\\\",\\n  \\\"activityBar.activeBorder\\\": \\\"#009fff\\\",\\n  \\\"activityBarBadge.background\\\": \\\"#009fff\\\",\\n  \\\"activityBarBadge.foreground\\\": \\\"#070707\\\",\\n  \\\"titleBar.activeBackground\\\": \\\"#141415\\\",\\n  \\\"titleBar.activeForeground\\\": \\\"#fbfbfb\\\",\\n  \\\"titleBar.inactiveBackground\\\": \\\"#141415\\\",\\n  \\\"titleBar.inactiveForeground\\\": \\\"#84848A\\\",\\n  \\\"titleBar.border\\\": \\\"#070707\\\",\\n  \\\"list.activeSelectionBackground\\\": \\\"#19283c99\\\",\\n  \\\"list.activeSelectionForeground\\\": \\\"#fbfbfb\\\",\\n  \\\"list.inactiveSelectionBackground\\\": \\\"#19283c73\\\",\\n  \\\"list.hoverBackground\\\": \\\"#19283c59\\\",\\n  \\\"list.focusOutline\\\": \\\"#009fff\\\",\\n  \\\"tab.activeBackground\\\": \\\"#070707\\\",\\n  \\\"tab.activeForeground\\\": \\\"#fbfbfb\\\",\\n  \\\"tab.activeBorderTop\\\": \\\"#009fff\\\",\\n  \\\"tab.inactiveBackground\\\": \\\"#141415\\\",\\n  \\\"tab.inactiveForeground\\\": \\\"#84848A\\\",\\n  \\\"tab.border\\\": \\\"#070707\\\",\\n  \\\"editorGroupHeader.tabsBackground\\\": \\\"#141415\\\",\\n  \\\"editorGroupHeader.tabsBorder\\\": \\\"#070707\\\",\\n  \\\"panel.background\\\": \\\"#141415\\\",\\n  \\\"panel.border\\\": \\\"#070707\\\",\\n  \\\"panelTitle.activeBorder\\\": \\\"#009fff\\\",\\n  \\\"panelTitle.activeForeground\\\": \\\"#fbfbfb\\\",\\n  \\\"panelTitle.inactiveForeground\\\": \\\"#84848A\\\",\\n  \\\"statusBar.background\\\": \\\"#141415\\\",\\n  \\\"statusBar.foreground\\\": \\\"#adadb1\\\",\\n  \\\"statusBar.border\\\": \\\"#070707\\\",\\n  \\\"statusBar.noFolderBackground\\\": \\\"#141415\\\",\\n  \\\"statusBar.debuggingBackground\\\": \\\"#ffca00\\\",\\n  \\\"statusBar.debuggingForeground\\\": \\\"#070707\\\",\\n  \\\"statusBarItem.remoteBackground\\\": \\\"#141415\\\",\\n  \\\"statusBarItem.remoteForeground\\\": \\\"#adadb1\\\",\\n  \\\"input.background\\\": \\\"#1F1F21\\\",\\n  \\\"input.border\\\": \\\"#424245\\\",\\n  \\\"input.foreground\\\": \\\"#fbfbfb\\\",\\n  \\\"input.placeholderForeground\\\": \\\"#79797F\\\",\\n  \\\"dropdown.background\\\": \\\"#1F1F21\\\",\\n  \\\"dropdown.border\\\": \\\"#424245\\\",\\n  \\\"dropdown.foreground\\\": \\\"#fbfbfb\\\",\\n  \\\"button.background\\\": \\\"#009fff\\\",\\n  \\\"button.foreground\\\": \\\"#070707\\\",\\n  \\\"button.hoverBackground\\\": \\\"#0190e6\\\",\\n  \\\"textLink.foreground\\\": \\\"#009fff\\\",\\n  \\\"textLink.activeForeground\\\": \\\"#009fff\\\",\\n  \\\"gitDecoration.addedResourceForeground\\\": \\\"#00cab1\\\",\\n  \\\"gitDecoration.conflictingResourceForeground\\\": \\\"#ffca00\\\",\\n  \\\"gitDecoration.modifiedResourceForeground\\\": \\\"#009fff\\\",\\n  \\\"gitDecoration.deletedResourceForeground\\\": \\\"#ff2e3f\\\",\\n  \\\"gitDecoration.untrackedResourceForeground\\\": \\\"#00cab1\\\",\\n  \\\"gitDecoration.ignoredResourceForeground\\\": \\\"#84848A\\\",\\n  \\\"terminal.titleForeground\\\": \\\"#adadb1\\\",\\n  \\\"terminal.titleInactiveForeground\\\": \\\"#84848A\\\",\\n  \\\"terminal.background\\\": \\\"#141415\\\",\\n  \\\"terminal.foreground\\\": \\\"#adadb1\\\",\\n  \\\"terminal.ansiBlack\\\": \\\"#141415\\\",\\n  \\\"terminal.ansiRed\\\": \\\"#ff2e3f\\\",\\n  \\\"terminal.ansiGreen\\\": \\\"#0dbe4e\\\",\\n  \\\"terminal.ansiYellow\\\": \\\"#ffca00\\\",\\n  \\\"terminal.ansiBlue\\\": \\\"#009fff\\\",\\n  \\\"terminal.ansiMagenta\\\": \\\"#c635e4\\\",\\n  \\\"terminal.ansiCyan\\\": \\\"#08c0ef\\\",\\n  \\\"terminal.ansiWhite\\\": \\\"#c6c6c8\\\",\\n  \\\"terminal.ansiBrightBlack\\\": \\\"#141415\\\",\\n  \\\"terminal.ansiBrightRed\\\": \\\"#ff2e3f\\\",\\n  \\\"terminal.ansiBrightGreen\\\": \\\"#0dbe4e\\\",\\n  \\\"terminal.ansiBrightYellow\\\": \\\"#ffca00\\\",\\n  \\\"terminal.ansiBrightBlue\\\": \\\"#009fff\\\",\\n  \\\"terminal.ansiBrightMagenta\\\": \\\"#c635e4\\\",\\n  \\\"terminal.ansiBrightCyan\\\": \\\"#08c0ef\\\",\\n  \\\"terminal.ansiBrightWhite\\\": \\\"#c6c6c8\\\",\\n};\\nvar tokenColors = [\\n  {\\n    scope: [\\\"comment\\\", \\\"punctuation.definition.comment\\\"],\\n    settings: { foreground: \\\"#84848A\\\" },\\n  },\\n  {\\n    scope: \\\"comment markup.link\\\",\\n    settings: { foreground: \\\"#84848A\\\" },\\n  },\\n  {\\n    scope: [\\\"string\\\", \\\"constant.other.symbol\\\"],\\n    settings: { foreground: \\\"#5ecc71\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.string.begin\\\",\\n      \\\"punctuation.definition.string.end\\\",\\n    ],\\n    settings: { foreground: \\\"#5ecc71\\\" },\\n  },\\n  {\\n    scope: [\\\"constant.numeric\\\", \\\"constant.language.boolean\\\"],\\n    settings: { foreground: \\\"#68cdf2\\\" },\\n  },\\n  {\\n    scope: \\\"constant\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.constant\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"constant.language\\\",\\n    settings: { foreground: \\\"#68cdf2\\\" },\\n  },\\n  {\\n    scope: \\\"variable.other.constant\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"keyword\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.control\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: [\\\"storage\\\", \\\"storage.type\\\", \\\"storage.modifier\\\"],\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"token.storage\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.new\\\",\\n      \\\"keyword.operator.expression.instanceof\\\",\\n      \\\"keyword.operator.expression.typeof\\\",\\n      \\\"keyword.operator.expression.void\\\",\\n      \\\"keyword.operator.expression.delete\\\",\\n      \\\"keyword.operator.expression.in\\\",\\n      \\\"keyword.operator.expression.of\\\",\\n      \\\"keyword.operator.expression.keyof\\\",\\n    ],\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.delete\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: [\\\"variable\\\", \\\"identifier\\\", \\\"meta.definition.variable\\\"],\\n    settings: { foreground: \\\"#ffa359\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"variable.other.readwrite\\\",\\n      \\\"meta.object-literal.key\\\",\\n      \\\"support.variable.property\\\",\\n      \\\"support.variable.object.process\\\",\\n      \\\"support.variable.object.node\\\",\\n    ],\\n    settings: { foreground: \\\"#ffa359\\\" },\\n  },\\n  {\\n    scope: \\\"variable.language\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"variable.parameter.function\\\",\\n    settings: { foreground: \\\"#adadb1\\\" },\\n  },\\n  {\\n    scope: \\\"function.parameter\\\",\\n    settings: { foreground: \\\"#adadb1\\\" },\\n  },\\n  {\\n    scope: \\\"variable.parameter\\\",\\n    settings: { foreground: \\\"#adadb1\\\" },\\n  },\\n  {\\n    scope: \\\"variable.parameter.function.language.python\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"variable.parameter.function.python\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.function\\\",\\n      \\\"entity.name.function\\\",\\n      \\\"meta.function-call\\\",\\n      \\\"meta.require\\\",\\n      \\\"support.function.any-method\\\",\\n      \\\"variable.function\\\",\\n    ],\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.other.special-method\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.function\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"support.function.console\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.type\\\",\\n      \\\"entity.name.type\\\",\\n      \\\"entity.name.class\\\",\\n      \\\"storage.type\\\",\\n    ],\\n    settings: { foreground: \\\"#d568ea\\\" },\\n  },\\n  {\\n    scope: [\\\"support.class\\\", \\\"entity.name.type.class\\\"],\\n    settings: { foreground: \\\"#d568ea\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"entity.name.class\\\",\\n      \\\"variable.other.class.js\\\",\\n      \\\"variable.other.class.ts\\\",\\n    ],\\n    settings: { foreground: \\\"#d568ea\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.class.identifier.namespace.type\\\",\\n    settings: { foreground: \\\"#d568ea\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.type.namespace\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"entity.other.inherited-class\\\",\\n    settings: { foreground: \\\"#d568ea\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.namespace\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.logical\\\",\\n      \\\"keyword.operator.bitwise\\\",\\n      \\\"keyword.operator.channel\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.arithmetic\\\",\\n      \\\"keyword.operator.comparison\\\",\\n      \\\"keyword.operator.relational\\\",\\n      \\\"keyword.operator.increment\\\",\\n      \\\"keyword.operator.decrement\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.assignment\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.assignment.compound\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.assignment.compound.js\\\",\\n      \\\"keyword.operator.assignment.compound.ts\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.ternary\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.optional\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.separator.delimiter\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.separator.key-value\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.terminator\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"meta.brace\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"meta.brace.square\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"meta.brace.round\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"function.brace\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.parameters\\\",\\n      \\\"punctuation.definition.typeparameters\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\\"punctuation.definition.block\\\", \\\"punctuation.definition.tag\\\"],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\\"meta.tag.tsx\\\", \\\"meta.tag.jsx\\\", \\\"meta.tag.js\\\", \\\"meta.tag.ts\\\"],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.expression.import\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.module\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.object.console\\\",\\n    settings: { foreground: \\\"#ffa359\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.module.node\\\",\\n      \\\"support.type.object.module\\\",\\n      \\\"entity.name.type.module\\\",\\n    ],\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.math\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.property.math\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.json\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.object.dom\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\\"support.variable.dom\\\", \\\"support.variable.property.dom\\\"],\\n    settings: { foreground: \\\"#ffa359\\\" },\\n  },\\n  {\\n    scope: \\\"support.variable.property.process\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"meta.property.object\\\",\\n    settings: { foreground: \\\"#ffa359\\\" },\\n  },\\n  {\\n    scope: \\\"variable.parameter.function.js\\\",\\n    settings: { foreground: \\\"#ffa359\\\" },\\n  },\\n  {\\n    scope: [\\\"keyword.other.template.begin\\\", \\\"keyword.other.template.end\\\"],\\n    settings: { foreground: \\\"#5ecc71\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.other.substitution.begin\\\",\\n      \\\"keyword.other.substitution.end\\\",\\n    ],\\n    settings: { foreground: \\\"#5ecc71\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.template-expression.begin\\\",\\n      \\\"punctuation.definition.template-expression.end\\\",\\n    ],\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"meta.template.expression\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.section.embedded\\\",\\n    settings: { foreground: \\\"#ffa359\\\" },\\n  },\\n  {\\n    scope: \\\"variable.interpolation\\\",\\n    settings: { foreground: \\\"#ffa359\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.section.embedded.begin\\\",\\n      \\\"punctuation.section.embedded.end\\\",\\n    ],\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.quasi.element\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.type.primitive.ts\\\",\\n      \\\"support.type.builtin.ts\\\",\\n      \\\"support.type.primitive.tsx\\\",\\n      \\\"support.type.builtin.tsx\\\",\\n    ],\\n    settings: { foreground: \\\"#d568ea\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.type.flowtype\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.primitive\\\",\\n    settings: { foreground: \\\"#d568ea\\\" },\\n  },\\n  {\\n    scope: \\\"support.variable.magic.python\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"variable.parameter.function.language.special.self.python\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.separator.period.python\\\",\\n      \\\"punctuation.separator.element.python\\\",\\n      \\\"punctuation.parenthesis.begin.python\\\",\\n      \\\"punctuation.parenthesis.end.python\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.arguments.begin.python\\\",\\n      \\\"punctuation.definition.arguments.end.python\\\",\\n      \\\"punctuation.separator.arguments.python\\\",\\n      \\\"punctuation.definition.list.begin.python\\\",\\n      \\\"punctuation.definition.list.end.python\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.python\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.logical.python\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"meta.function-call.generic.python\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"constant.character.format.placeholder.other.python\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"meta.function.decorator.python\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.token.decorator.python\\\",\\n      \\\"meta.function.decorator.identifier.python\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"storage.modifier.lifetime.rust\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"support.function.std.rust\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.lifetime.rust\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"variable.language.rust\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.misc.rust\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.sigil.rust\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.core.rust\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: [\\\"meta.function.c\\\", \\\"meta.function.cpp\\\"],\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.section.block.begin.bracket.curly.cpp\\\",\\n      \\\"punctuation.section.block.end.bracket.curly.cpp\\\",\\n      \\\"punctuation.terminator.statement.c\\\",\\n      \\\"punctuation.section.block.begin.bracket.curly.c\\\",\\n      \\\"punctuation.section.block.end.bracket.curly.c\\\",\\n      \\\"punctuation.section.parens.begin.bracket.round.c\\\",\\n      \\\"punctuation.section.parens.end.bracket.round.c\\\",\\n      \\\"punctuation.section.parameters.begin.bracket.round.c\\\",\\n      \\\"punctuation.section.parameters.end.bracket.round.c\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.assignment.c\\\",\\n      \\\"keyword.operator.comparison.c\\\",\\n      \\\"keyword.operator.c\\\",\\n      \\\"keyword.operator.increment.c\\\",\\n      \\\"keyword.operator.decrement.c\\\",\\n      \\\"keyword.operator.bitwise.shift.c\\\",\\n    ],\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.assignment.cpp\\\",\\n      \\\"keyword.operator.comparison.cpp\\\",\\n      \\\"keyword.operator.cpp\\\",\\n      \\\"keyword.operator.increment.cpp\\\",\\n      \\\"keyword.operator.decrement.cpp\\\",\\n      \\\"keyword.operator.bitwise.shift.cpp\\\",\\n    ],\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: [\\\"punctuation.separator.c\\\", \\\"punctuation.separator.cpp\\\"],\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: [\\\"support.type.posix-reserved.c\\\", \\\"support.type.posix-reserved.cpp\\\"],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\\"keyword.operator.sizeof.c\\\", \\\"keyword.operator.sizeof.cpp\\\"],\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"variable.c\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\\"storage.type.annotation.java\\\", \\\"storage.type.object.array.java\\\"],\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"source.java\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.section.block.begin.java\\\",\\n      \\\"punctuation.section.block.end.java\\\",\\n      \\\"punctuation.definition.method-parameters.begin.java\\\",\\n      \\\"punctuation.definition.method-parameters.end.java\\\",\\n      \\\"meta.method.identifier.java\\\",\\n      \\\"punctuation.section.method.begin.java\\\",\\n      \\\"punctuation.section.method.end.java\\\",\\n      \\\"punctuation.terminator.java\\\",\\n      \\\"punctuation.section.class.begin.java\\\",\\n      \\\"punctuation.section.class.end.java\\\",\\n      \\\"punctuation.section.inner-class.begin.java\\\",\\n      \\\"punctuation.section.inner-class.end.java\\\",\\n      \\\"meta.method-call.java\\\",\\n      \\\"punctuation.section.class.begin.bracket.curly.java\\\",\\n      \\\"punctuation.section.class.end.bracket.curly.java\\\",\\n      \\\"punctuation.section.method.begin.bracket.curly.java\\\",\\n      \\\"punctuation.section.method.end.bracket.curly.java\\\",\\n      \\\"punctuation.separator.period.java\\\",\\n      \\\"punctuation.bracket.angle.java\\\",\\n      \\\"punctuation.definition.annotation.java\\\",\\n      \\\"meta.method.body.java\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"meta.method.java\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"storage.modifier.import.java\\\",\\n      \\\"storage.type.java\\\",\\n      \\\"storage.type.generic.java\\\",\\n    ],\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.instanceof.java\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"meta.definition.variable.name.java\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"token.variable.parameter.java\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"import.storage.java\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"token.package.keyword\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"token.package\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"token.storage.type.java\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.assignment.go\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: [\\\"keyword.operator.arithmetic.go\\\", \\\"keyword.operator.address.go\\\"],\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.package.go\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.other.namespace.use.php\\\",\\n      \\\"support.other.namespace.use-as.php\\\",\\n      \\\"support.other.namespace.php\\\",\\n      \\\"entity.other.alias.php\\\",\\n      \\\"meta.interface.php\\\",\\n    ],\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.error-control.php\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.type.php\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.section.array.begin.php\\\",\\n      \\\"punctuation.section.array.end.php\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"storage.type.php\\\",\\n      \\\"meta.other.type.phpdoc.php\\\",\\n      \\\"keyword.other.type.php\\\",\\n      \\\"keyword.other.array.phpdoc.php\\\",\\n    ],\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"meta.function-call.php\\\",\\n      \\\"meta.function-call.object.php\\\",\\n      \\\"meta.function-call.static.php\\\",\\n    ],\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.parameters.begin.bracket.round.php\\\",\\n      \\\"punctuation.definition.parameters.end.bracket.round.php\\\",\\n      \\\"punctuation.separator.delimiter.php\\\",\\n      \\\"punctuation.section.scope.begin.php\\\",\\n      \\\"punctuation.section.scope.end.php\\\",\\n      \\\"punctuation.terminator.expression.php\\\",\\n      \\\"punctuation.definition.arguments.begin.bracket.round.php\\\",\\n      \\\"punctuation.definition.arguments.end.bracket.round.php\\\",\\n      \\\"punctuation.definition.storage-type.begin.bracket.round.php\\\",\\n      \\\"punctuation.definition.storage-type.end.bracket.round.php\\\",\\n      \\\"punctuation.definition.array.begin.bracket.round.php\\\",\\n      \\\"punctuation.definition.array.end.bracket.round.php\\\",\\n      \\\"punctuation.definition.begin.bracket.round.php\\\",\\n      \\\"punctuation.definition.end.bracket.round.php\\\",\\n      \\\"punctuation.definition.begin.bracket.curly.php\\\",\\n      \\\"punctuation.definition.end.bracket.curly.php\\\",\\n      \\\"punctuation.definition.section.switch-block.end.bracket.curly.php\\\",\\n      \\\"punctuation.definition.section.switch-block.start.bracket.curly.php\\\",\\n      \\\"punctuation.definition.section.switch-block.begin.bracket.curly.php\\\",\\n      \\\"punctuation.definition.section.switch-block.end.bracket.curly.php\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.constant.ext.php\\\",\\n      \\\"support.constant.std.php\\\",\\n      \\\"support.constant.core.php\\\",\\n      \\\"support.constant.parser-token.php\\\",\\n    ],\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: [\\\"entity.name.goto-label.php\\\", \\\"support.other.php\\\"],\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.logical.php\\\",\\n      \\\"keyword.operator.bitwise.php\\\",\\n      \\\"keyword.operator.arithmetic.php\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.regexp.php\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.comparison.php\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\\"keyword.operator.heredoc.php\\\", \\\"keyword.operator.nowdoc.php\\\"],\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"variable.other.class.php\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.illegal.non-null-typehinted.php\\\",\\n    settings: { foreground: \\\"#f44747\\\" },\\n  },\\n  {\\n    scope: \\\"variable.other.generic-type.haskell\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"storage.type.haskell\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"storage.type.cs\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.variable.local.cs\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.label.cs\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"entity.name.scope-resolution.function.call\\\",\\n      \\\"entity.name.scope-resolution.function.definition\\\",\\n    ],\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.delayed.unison\\\",\\n      \\\"punctuation.definition.list.begin.unison\\\",\\n      \\\"punctuation.definition.list.end.unison\\\",\\n      \\\"punctuation.definition.ability.begin.unison\\\",\\n      \\\"punctuation.definition.ability.end.unison\\\",\\n      \\\"punctuation.operator.assignment.as.unison\\\",\\n      \\\"punctuation.separator.pipe.unison\\\",\\n      \\\"punctuation.separator.delimiter.unison\\\",\\n      \\\"punctuation.definition.hash.unison\\\",\\n    ],\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.edge\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.prelude.elm\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.elm\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"entity.global.clojure\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"meta.symbol.clojure\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"constant.keyword.clojure\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\\"meta.arguments.coffee\\\", \\\"variable.parameter.function.coffee\\\"],\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"storage.modifier.import.groovy\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"meta.method.groovy\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"meta.definition.variable.name.groovy\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"meta.definition.class.inherited.classes.groovy\\\",\\n    settings: { foreground: \\\"#5ecc71\\\" },\\n  },\\n  {\\n    scope: \\\"support.variable.semantic.hlsl\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.type.texture.hlsl\\\",\\n      \\\"support.type.sampler.hlsl\\\",\\n      \\\"support.type.object.hlsl\\\",\\n      \\\"support.type.object.rw.hlsl\\\",\\n      \\\"support.type.fx.hlsl\\\",\\n      \\\"support.type.object.hlsl\\\",\\n    ],\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: [\\\"text.variable\\\", \\\"text.bracketed\\\"],\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: [\\\"support.type.swift\\\", \\\"support.type.vb.asp\\\"],\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"meta.scope.prerequisites.makefile\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"source.makefile\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"source.ini\\\",\\n    settings: { foreground: \\\"#5ecc71\\\" },\\n  },\\n  {\\n    scope: \\\"constant.language.symbol.ruby\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\\"function.parameter.ruby\\\", \\\"function.parameter.cs\\\"],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"constant.language.symbol.elixir\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope:\\n      \\\"text.html.laravel-blade source.php.embedded.line.html entity.name.tag.laravel-blade\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope:\\n      \\\"text.html.laravel-blade source.php.embedded.line.html support.constant.laravel-blade\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.function.xi\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.class.xi\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"constant.character.character-class.regexp.xi\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"constant.regexp.xi\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.control.xi\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.xi\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"beginning.punctuation.definition.quote.markdown.xi\\\",\\n    settings: { foreground: \\\"#5ecc71\\\" },\\n  },\\n  {\\n    scope: \\\"beginning.punctuation.definition.list.markdown.xi\\\",\\n    settings: { foreground: \\\"#84848A\\\" },\\n  },\\n  {\\n    scope: \\\"constant.character.xi\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"accent.xi\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"wikiword.xi\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"constant.other.color.rgb-value.xi\\\",\\n    settings: { foreground: \\\"#ffffff\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.tag.xi\\\",\\n    settings: { foreground: \\\"#84848A\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.constant.property-value.scss\\\",\\n      \\\"support.constant.property-value.css\\\",\\n    ],\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.css\\\",\\n      \\\"keyword.operator.scss\\\",\\n      \\\"keyword.operator.less\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.constant.color.w3c-standard-color-name.css\\\",\\n      \\\"support.constant.color.w3c-standard-color-name.scss\\\",\\n    ],\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.separator.list.comma.css\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.vendored.property-name.css\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.property-name.css\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.property-name\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.property-value\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.font-name\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"entity.other.attribute-name.class.css\\\",\\n    settings: {\\n      foreground: \\\"#61d5c0\\\",\\n      fontStyle: \\\"normal\\\",\\n    },\\n  },\\n  {\\n    scope: \\\"entity.other.attribute-name.id\\\",\\n    settings: {\\n      foreground: \\\"#9d6afb\\\",\\n      fontStyle: \\\"normal\\\",\\n    },\\n  },\\n  {\\n    scope: [\\n      \\\"entity.other.attribute-name.pseudo-element\\\",\\n      \\\"entity.other.attribute-name.pseudo-class\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"meta.selector\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"selector.sass\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"rgb-value\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"inline-color-decoration rgb-value\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"less rgb-value\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"control.elements\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.less\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.tag\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"entity.other.attribute-name\\\",\\n    settings: {\\n      foreground: \\\"#61d5c0\\\",\\n      fontStyle: \\\"normal\\\",\\n    },\\n  },\\n  {\\n    scope: \\\"constant.character.entity\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"meta.tag\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.illegal.bad-ampersand.html\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"markup.heading\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"markup.heading punctuation.definition.heading\\\",\\n      \\\"entity.name.section\\\",\\n    ],\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.section.markdown\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.heading.markdown\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"markup.heading.setext\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"markup.heading.setext.1.markdown\\\",\\n      \\\"markup.heading.setext.2.markdown\\\",\\n    ],\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: [\\\"markup.bold\\\", \\\"todo.bold\\\"],\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.bold\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.bold.markdown\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: [\\\"markup.italic\\\", \\\"punctuation.definition.italic\\\", \\\"todo.emphasis\\\"],\\n    settings: {\\n      foreground: \\\"#ff678d\\\",\\n      fontStyle: \\\"italic\\\",\\n    },\\n  },\\n  {\\n    scope: \\\"emphasis md\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"markup.italic.markdown\\\",\\n    settings: { fontStyle: \\\"italic\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"markup.underline.link.markdown\\\",\\n      \\\"markup.underline.link.image.markdown\\\",\\n    ],\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"string.other.link.title.markdown\\\",\\n      \\\"string.other.link.description.markdown\\\",\\n    ],\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.metadata.markdown\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: [\\\"markup.inline.raw.markdown\\\", \\\"markup.inline.raw.string.markdown\\\"],\\n    settings: { foreground: \\\"#5ecc71\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.list.begin.markdown\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.list.markdown\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"beginning.punctuation.definition.list.markdown\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.string.begin.markdown\\\",\\n      \\\"punctuation.definition.string.end.markdown\\\",\\n    ],\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"markup.quote.markdown\\\",\\n    settings: { foreground: \\\"#84848A\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.other.unit\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"markup.changed.diff\\\",\\n    settings: { foreground: \\\"#ffca00\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"meta.diff.header.from-file\\\",\\n      \\\"meta.diff.header.to-file\\\",\\n      \\\"punctuation.definition.from-file.diff\\\",\\n      \\\"punctuation.definition.to-file.diff\\\",\\n    ],\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"markup.inserted.diff\\\",\\n    settings: { foreground: \\\"#5ecc71\\\" },\\n  },\\n  {\\n    scope: \\\"markup.deleted.diff\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"string.regexp\\\",\\n    settings: { foreground: \\\"#64d1db\\\" },\\n  },\\n  {\\n    scope: \\\"constant.other.character-class.regexp\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.quantifier.regexp\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"constant.character.escape\\\",\\n    settings: { foreground: \\\"#68cdf2\\\" },\\n  },\\n  {\\n    scope: \\\"source.json meta.structure.dictionary.json > string.quoted.json\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope:\\n      \\\"source.json meta.structure.dictionary.json > string.quoted.json > punctuation.string\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"source.json meta.structure.dictionary.json > value.json > string.quoted.json\\\",\\n      \\\"source.json meta.structure.array.json > value.json > string.quoted.json\\\",\\n      \\\"source.json meta.structure.dictionary.json > value.json > string.quoted.json > punctuation\\\",\\n      \\\"source.json meta.structure.array.json > value.json > string.quoted.json > punctuation\\\",\\n    ],\\n    settings: { foreground: \\\"#5ecc71\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"source.json meta.structure.dictionary.json > constant.language.json\\\",\\n      \\\"source.json meta.structure.array.json > constant.language.json\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.property-name.json\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.property-name.json punctuation\\\",\\n    settings: { foreground: \\\"#ff6762\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.block.sequence.item.yaml\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"block.scope.end\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"block.scope.begin\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"token.info-token\\\",\\n    settings: { foreground: \\\"#9d6afb\\\" },\\n  },\\n  {\\n    scope: \\\"token.warn-token\\\",\\n    settings: { foreground: \\\"#ffd452\\\" },\\n  },\\n  {\\n    scope: \\\"token.error-token\\\",\\n    settings: { foreground: \\\"#f44747\\\" },\\n  },\\n  {\\n    scope: \\\"token.debug-token\\\",\\n    settings: { foreground: \\\"#ff678d\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.illegal\\\",\\n    settings: { foreground: \\\"#ffffff\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.broken\\\",\\n    settings: { foreground: \\\"#ffffff\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.deprecated\\\",\\n    settings: { foreground: \\\"#ffffff\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.unimplemented\\\",\\n    settings: { foreground: \\\"#ffffff\\\" },\\n  },\\n];\\nvar semanticTokenColors = {\\n  comment: \\\"#84848A\\\",\\n  string: \\\"#5ecc71\\\",\\n  number: \\\"#68cdf2\\\",\\n  regexp: \\\"#64d1db\\\",\\n  keyword: \\\"#ff678d\\\",\\n  variable: \\\"#ffa359\\\",\\n  parameter: \\\"#adadb1\\\",\\n  property: \\\"#ffa359\\\",\\n  function: \\\"#9d6afb\\\",\\n  method: \\\"#9d6afb\\\",\\n  type: \\\"#d568ea\\\",\\n  class: \\\"#d568ea\\\",\\n  namespace: \\\"#ffca00\\\",\\n  enumMember: \\\"#08c0ef\\\",\\n  \\\"variable.constant\\\": \\\"#ffd452\\\",\\n  \\\"variable.defaultLibrary\\\": \\\"#ffca00\\\",\\n};\\nvar pierre_dark_default = {\\n  name,\\n  type,\\n  colors,\\n  tokenColors,\\n  semanticTokenColors,\\n};\\n\\n//#endregion\\nexport {\\n  colors,\\n  pierre_dark_default as default,\\n  name,\\n  semanticTokenColors,\\n  tokenColors,\\n  type,\\n};\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/pierre-light-theme.js\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/shared/pierre-light-theme.js\",\n      \"content\": \"//#region src/themes/pierre-light.json\\nvar name = \\\"pierre-light\\\";\\nvar type = \\\"light\\\";\\nvar colors = {\\n  \\\"editor.background\\\": \\\"#ffffff\\\",\\n  \\\"editor.foreground\\\": \\\"#070707\\\",\\n  foreground: \\\"#070707\\\",\\n  focusBorder: \\\"#009fff\\\",\\n  \\\"selection.background\\\": \\\"#dfebff\\\",\\n  \\\"editor.selectionBackground\\\": \\\"#009fff2e\\\",\\n  \\\"editor.lineHighlightBackground\\\": \\\"#dfebff8c\\\",\\n  \\\"editorCursor.foreground\\\": \\\"#009fff\\\",\\n  \\\"editorLineNumber.foreground\\\": \\\"#84848A\\\",\\n  \\\"editorLineNumber.activeForeground\\\": \\\"#6C6C71\\\",\\n  \\\"editorIndentGuide.background\\\": \\\"#eeeeef\\\",\\n  \\\"editorIndentGuide.activeBackground\\\": \\\"#dbdbdd\\\",\\n  \\\"diffEditor.insertedTextBackground\\\": \\\"#00cab133\\\",\\n  \\\"diffEditor.deletedTextBackground\\\": \\\"#ff2e3f33\\\",\\n  \\\"sideBar.background\\\": \\\"#f8f8f8\\\",\\n  \\\"sideBar.foreground\\\": \\\"#6C6C71\\\",\\n  \\\"sideBar.border\\\": \\\"#eeeeef\\\",\\n  \\\"sideBarTitle.foreground\\\": \\\"#070707\\\",\\n  \\\"sideBarSectionHeader.background\\\": \\\"#f8f8f8\\\",\\n  \\\"sideBarSectionHeader.foreground\\\": \\\"#6C6C71\\\",\\n  \\\"sideBarSectionHeader.border\\\": \\\"#eeeeef\\\",\\n  \\\"activityBar.background\\\": \\\"#f8f8f8\\\",\\n  \\\"activityBar.foreground\\\": \\\"#070707\\\",\\n  \\\"activityBar.border\\\": \\\"#eeeeef\\\",\\n  \\\"activityBar.activeBorder\\\": \\\"#009fff\\\",\\n  \\\"activityBarBadge.background\\\": \\\"#009fff\\\",\\n  \\\"activityBarBadge.foreground\\\": \\\"#ffffff\\\",\\n  \\\"titleBar.activeBackground\\\": \\\"#f8f8f8\\\",\\n  \\\"titleBar.activeForeground\\\": \\\"#070707\\\",\\n  \\\"titleBar.inactiveBackground\\\": \\\"#f8f8f8\\\",\\n  \\\"titleBar.inactiveForeground\\\": \\\"#84848A\\\",\\n  \\\"titleBar.border\\\": \\\"#eeeeef\\\",\\n  \\\"list.activeSelectionBackground\\\": \\\"#dfebffcc\\\",\\n  \\\"list.activeSelectionForeground\\\": \\\"#070707\\\",\\n  \\\"list.inactiveSelectionBackground\\\": \\\"#dfebff73\\\",\\n  \\\"list.hoverBackground\\\": \\\"#dfebff59\\\",\\n  \\\"list.focusOutline\\\": \\\"#009fff\\\",\\n  \\\"tab.activeBackground\\\": \\\"#ffffff\\\",\\n  \\\"tab.activeForeground\\\": \\\"#070707\\\",\\n  \\\"tab.activeBorderTop\\\": \\\"#009fff\\\",\\n  \\\"tab.inactiveBackground\\\": \\\"#f8f8f8\\\",\\n  \\\"tab.inactiveForeground\\\": \\\"#84848A\\\",\\n  \\\"tab.border\\\": \\\"#eeeeef\\\",\\n  \\\"editorGroupHeader.tabsBackground\\\": \\\"#f8f8f8\\\",\\n  \\\"editorGroupHeader.tabsBorder\\\": \\\"#eeeeef\\\",\\n  \\\"panel.background\\\": \\\"#f8f8f8\\\",\\n  \\\"panel.border\\\": \\\"#eeeeef\\\",\\n  \\\"panelTitle.activeBorder\\\": \\\"#009fff\\\",\\n  \\\"panelTitle.activeForeground\\\": \\\"#070707\\\",\\n  \\\"panelTitle.inactiveForeground\\\": \\\"#84848A\\\",\\n  \\\"statusBar.background\\\": \\\"#f8f8f8\\\",\\n  \\\"statusBar.foreground\\\": \\\"#6C6C71\\\",\\n  \\\"statusBar.border\\\": \\\"#eeeeef\\\",\\n  \\\"statusBar.noFolderBackground\\\": \\\"#f8f8f8\\\",\\n  \\\"statusBar.debuggingBackground\\\": \\\"#ffca00\\\",\\n  \\\"statusBar.debuggingForeground\\\": \\\"#ffffff\\\",\\n  \\\"statusBarItem.remoteBackground\\\": \\\"#f8f8f8\\\",\\n  \\\"statusBarItem.remoteForeground\\\": \\\"#6C6C71\\\",\\n  \\\"input.background\\\": \\\"#f2f2f3\\\",\\n  \\\"input.border\\\": \\\"#dbdbdd\\\",\\n  \\\"input.foreground\\\": \\\"#070707\\\",\\n  \\\"input.placeholderForeground\\\": \\\"#8E8E95\\\",\\n  \\\"dropdown.background\\\": \\\"#f2f2f3\\\",\\n  \\\"dropdown.border\\\": \\\"#dbdbdd\\\",\\n  \\\"dropdown.foreground\\\": \\\"#070707\\\",\\n  \\\"button.background\\\": \\\"#009fff\\\",\\n  \\\"button.foreground\\\": \\\"#ffffff\\\",\\n  \\\"button.hoverBackground\\\": \\\"#1aa9ff\\\",\\n  \\\"textLink.foreground\\\": \\\"#009fff\\\",\\n  \\\"textLink.activeForeground\\\": \\\"#009fff\\\",\\n  \\\"gitDecoration.addedResourceForeground\\\": \\\"#00cab1\\\",\\n  \\\"gitDecoration.conflictingResourceForeground\\\": \\\"#ffca00\\\",\\n  \\\"gitDecoration.modifiedResourceForeground\\\": \\\"#009fff\\\",\\n  \\\"gitDecoration.deletedResourceForeground\\\": \\\"#ff2e3f\\\",\\n  \\\"gitDecoration.untrackedResourceForeground\\\": \\\"#00cab1\\\",\\n  \\\"gitDecoration.ignoredResourceForeground\\\": \\\"#84848A\\\",\\n  \\\"terminal.titleForeground\\\": \\\"#6C6C71\\\",\\n  \\\"terminal.titleInactiveForeground\\\": \\\"#84848A\\\",\\n  \\\"terminal.background\\\": \\\"#f8f8f8\\\",\\n  \\\"terminal.foreground\\\": \\\"#6C6C71\\\",\\n  \\\"terminal.ansiBlack\\\": \\\"#1F1F21\\\",\\n  \\\"terminal.ansiRed\\\": \\\"#ff2e3f\\\",\\n  \\\"terminal.ansiGreen\\\": \\\"#0dbe4e\\\",\\n  \\\"terminal.ansiYellow\\\": \\\"#ffca00\\\",\\n  \\\"terminal.ansiBlue\\\": \\\"#009fff\\\",\\n  \\\"terminal.ansiMagenta\\\": \\\"#c635e4\\\",\\n  \\\"terminal.ansiCyan\\\": \\\"#08c0ef\\\",\\n  \\\"terminal.ansiWhite\\\": \\\"#c6c6c8\\\",\\n  \\\"terminal.ansiBrightBlack\\\": \\\"#1F1F21\\\",\\n  \\\"terminal.ansiBrightRed\\\": \\\"#ff2e3f\\\",\\n  \\\"terminal.ansiBrightGreen\\\": \\\"#0dbe4e\\\",\\n  \\\"terminal.ansiBrightYellow\\\": \\\"#ffca00\\\",\\n  \\\"terminal.ansiBrightBlue\\\": \\\"#009fff\\\",\\n  \\\"terminal.ansiBrightMagenta\\\": \\\"#c635e4\\\",\\n  \\\"terminal.ansiBrightCyan\\\": \\\"#08c0ef\\\",\\n  \\\"terminal.ansiBrightWhite\\\": \\\"#c6c6c8\\\",\\n};\\nvar tokenColors = [\\n  {\\n    scope: [\\\"comment\\\", \\\"punctuation.definition.comment\\\"],\\n    settings: { foreground: \\\"#84848A\\\" },\\n  },\\n  {\\n    scope: \\\"comment markup.link\\\",\\n    settings: { foreground: \\\"#84848A\\\" },\\n  },\\n  {\\n    scope: [\\\"string\\\", \\\"constant.other.symbol\\\"],\\n    settings: { foreground: \\\"#199f43\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.string.begin\\\",\\n      \\\"punctuation.definition.string.end\\\",\\n    ],\\n    settings: { foreground: \\\"#199f43\\\" },\\n  },\\n  {\\n    scope: [\\\"constant.numeric\\\", \\\"constant.language.boolean\\\"],\\n    settings: { foreground: \\\"#1ca1c7\\\" },\\n  },\\n  {\\n    scope: \\\"constant\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.constant\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"constant.language\\\",\\n    settings: { foreground: \\\"#1ca1c7\\\" },\\n  },\\n  {\\n    scope: \\\"variable.other.constant\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"keyword\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.control\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: [\\\"storage\\\", \\\"storage.type\\\", \\\"storage.modifier\\\"],\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"token.storage\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.new\\\",\\n      \\\"keyword.operator.expression.instanceof\\\",\\n      \\\"keyword.operator.expression.typeof\\\",\\n      \\\"keyword.operator.expression.void\\\",\\n      \\\"keyword.operator.expression.delete\\\",\\n      \\\"keyword.operator.expression.in\\\",\\n      \\\"keyword.operator.expression.of\\\",\\n      \\\"keyword.operator.expression.keyof\\\",\\n    ],\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.delete\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: [\\\"variable\\\", \\\"identifier\\\", \\\"meta.definition.variable\\\"],\\n    settings: { foreground: \\\"#d47628\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"variable.other.readwrite\\\",\\n      \\\"meta.object-literal.key\\\",\\n      \\\"support.variable.property\\\",\\n      \\\"support.variable.object.process\\\",\\n      \\\"support.variable.object.node\\\",\\n    ],\\n    settings: { foreground: \\\"#d47628\\\" },\\n  },\\n  {\\n    scope: \\\"variable.language\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"variable.parameter.function\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"function.parameter\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"variable.parameter\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"variable.parameter.function.language.python\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"variable.parameter.function.python\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.function\\\",\\n      \\\"entity.name.function\\\",\\n      \\\"meta.function-call\\\",\\n      \\\"meta.require\\\",\\n      \\\"support.function.any-method\\\",\\n      \\\"variable.function\\\",\\n    ],\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.other.special-method\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.function\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"support.function.console\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.type\\\",\\n      \\\"entity.name.type\\\",\\n      \\\"entity.name.class\\\",\\n      \\\"storage.type\\\",\\n    ],\\n    settings: { foreground: \\\"#c635e4\\\" },\\n  },\\n  {\\n    scope: [\\\"support.class\\\", \\\"entity.name.type.class\\\"],\\n    settings: { foreground: \\\"#c635e4\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"entity.name.class\\\",\\n      \\\"variable.other.class.js\\\",\\n      \\\"variable.other.class.ts\\\",\\n    ],\\n    settings: { foreground: \\\"#c635e4\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.class.identifier.namespace.type\\\",\\n    settings: { foreground: \\\"#c635e4\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.type.namespace\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"entity.other.inherited-class\\\",\\n    settings: { foreground: \\\"#c635e4\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.namespace\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.logical\\\",\\n      \\\"keyword.operator.bitwise\\\",\\n      \\\"keyword.operator.channel\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.arithmetic\\\",\\n      \\\"keyword.operator.comparison\\\",\\n      \\\"keyword.operator.relational\\\",\\n      \\\"keyword.operator.increment\\\",\\n      \\\"keyword.operator.decrement\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.assignment\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.assignment.compound\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.assignment.compound.js\\\",\\n      \\\"keyword.operator.assignment.compound.ts\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.ternary\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.optional\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.separator.delimiter\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.separator.key-value\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.terminator\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"meta.brace\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"meta.brace.square\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"meta.brace.round\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"function.brace\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.parameters\\\",\\n      \\\"punctuation.definition.typeparameters\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\\"punctuation.definition.block\\\", \\\"punctuation.definition.tag\\\"],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\\"meta.tag.tsx\\\", \\\"meta.tag.jsx\\\", \\\"meta.tag.js\\\", \\\"meta.tag.ts\\\"],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.expression.import\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.module\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.object.console\\\",\\n    settings: { foreground: \\\"#d47628\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.module.node\\\",\\n      \\\"support.type.object.module\\\",\\n      \\\"entity.name.type.module\\\",\\n    ],\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.math\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.property.math\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.json\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.object.dom\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\\"support.variable.dom\\\", \\\"support.variable.property.dom\\\"],\\n    settings: { foreground: \\\"#d47628\\\" },\\n  },\\n  {\\n    scope: \\\"support.variable.property.process\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"meta.property.object\\\",\\n    settings: { foreground: \\\"#d47628\\\" },\\n  },\\n  {\\n    scope: \\\"variable.parameter.function.js\\\",\\n    settings: { foreground: \\\"#d47628\\\" },\\n  },\\n  {\\n    scope: [\\\"keyword.other.template.begin\\\", \\\"keyword.other.template.end\\\"],\\n    settings: { foreground: \\\"#199f43\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.other.substitution.begin\\\",\\n      \\\"keyword.other.substitution.end\\\",\\n    ],\\n    settings: { foreground: \\\"#199f43\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.template-expression.begin\\\",\\n      \\\"punctuation.definition.template-expression.end\\\",\\n    ],\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"meta.template.expression\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.section.embedded\\\",\\n    settings: { foreground: \\\"#d47628\\\" },\\n  },\\n  {\\n    scope: \\\"variable.interpolation\\\",\\n    settings: { foreground: \\\"#d47628\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.section.embedded.begin\\\",\\n      \\\"punctuation.section.embedded.end\\\",\\n    ],\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.quasi.element\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.type.primitive.ts\\\",\\n      \\\"support.type.builtin.ts\\\",\\n      \\\"support.type.primitive.tsx\\\",\\n      \\\"support.type.builtin.tsx\\\",\\n    ],\\n    settings: { foreground: \\\"#c635e4\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.type.flowtype\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.primitive\\\",\\n    settings: { foreground: \\\"#c635e4\\\" },\\n  },\\n  {\\n    scope: \\\"support.variable.magic.python\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"variable.parameter.function.language.special.self.python\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.separator.period.python\\\",\\n      \\\"punctuation.separator.element.python\\\",\\n      \\\"punctuation.parenthesis.begin.python\\\",\\n      \\\"punctuation.parenthesis.end.python\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.arguments.begin.python\\\",\\n      \\\"punctuation.definition.arguments.end.python\\\",\\n      \\\"punctuation.separator.arguments.python\\\",\\n      \\\"punctuation.definition.list.begin.python\\\",\\n      \\\"punctuation.definition.list.end.python\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.python\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.logical.python\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"meta.function-call.generic.python\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"constant.character.format.placeholder.other.python\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"meta.function.decorator.python\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.token.decorator.python\\\",\\n      \\\"meta.function.decorator.identifier.python\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"storage.modifier.lifetime.rust\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"support.function.std.rust\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.lifetime.rust\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"variable.language.rust\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.misc.rust\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.sigil.rust\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.core.rust\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\\"meta.function.c\\\", \\\"meta.function.cpp\\\"],\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.section.block.begin.bracket.curly.cpp\\\",\\n      \\\"punctuation.section.block.end.bracket.curly.cpp\\\",\\n      \\\"punctuation.terminator.statement.c\\\",\\n      \\\"punctuation.section.block.begin.bracket.curly.c\\\",\\n      \\\"punctuation.section.block.end.bracket.curly.c\\\",\\n      \\\"punctuation.section.parens.begin.bracket.round.c\\\",\\n      \\\"punctuation.section.parens.end.bracket.round.c\\\",\\n      \\\"punctuation.section.parameters.begin.bracket.round.c\\\",\\n      \\\"punctuation.section.parameters.end.bracket.round.c\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.assignment.c\\\",\\n      \\\"keyword.operator.comparison.c\\\",\\n      \\\"keyword.operator.c\\\",\\n      \\\"keyword.operator.increment.c\\\",\\n      \\\"keyword.operator.decrement.c\\\",\\n      \\\"keyword.operator.bitwise.shift.c\\\",\\n    ],\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.assignment.cpp\\\",\\n      \\\"keyword.operator.comparison.cpp\\\",\\n      \\\"keyword.operator.cpp\\\",\\n      \\\"keyword.operator.increment.cpp\\\",\\n      \\\"keyword.operator.decrement.cpp\\\",\\n      \\\"keyword.operator.bitwise.shift.cpp\\\",\\n    ],\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: [\\\"punctuation.separator.c\\\", \\\"punctuation.separator.cpp\\\"],\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: [\\\"support.type.posix-reserved.c\\\", \\\"support.type.posix-reserved.cpp\\\"],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\\"keyword.operator.sizeof.c\\\", \\\"keyword.operator.sizeof.cpp\\\"],\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"variable.c\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\\"storage.type.annotation.java\\\", \\\"storage.type.object.array.java\\\"],\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"source.java\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.section.block.begin.java\\\",\\n      \\\"punctuation.section.block.end.java\\\",\\n      \\\"punctuation.definition.method-parameters.begin.java\\\",\\n      \\\"punctuation.definition.method-parameters.end.java\\\",\\n      \\\"meta.method.identifier.java\\\",\\n      \\\"punctuation.section.method.begin.java\\\",\\n      \\\"punctuation.section.method.end.java\\\",\\n      \\\"punctuation.terminator.java\\\",\\n      \\\"punctuation.section.class.begin.java\\\",\\n      \\\"punctuation.section.class.end.java\\\",\\n      \\\"punctuation.section.inner-class.begin.java\\\",\\n      \\\"punctuation.section.inner-class.end.java\\\",\\n      \\\"meta.method-call.java\\\",\\n      \\\"punctuation.section.class.begin.bracket.curly.java\\\",\\n      \\\"punctuation.section.class.end.bracket.curly.java\\\",\\n      \\\"punctuation.section.method.begin.bracket.curly.java\\\",\\n      \\\"punctuation.section.method.end.bracket.curly.java\\\",\\n      \\\"punctuation.separator.period.java\\\",\\n      \\\"punctuation.bracket.angle.java\\\",\\n      \\\"punctuation.definition.annotation.java\\\",\\n      \\\"meta.method.body.java\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"meta.method.java\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"storage.modifier.import.java\\\",\\n      \\\"storage.type.java\\\",\\n      \\\"storage.type.generic.java\\\",\\n    ],\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.instanceof.java\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"meta.definition.variable.name.java\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"token.variable.parameter.java\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"import.storage.java\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"token.package.keyword\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"token.package\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"token.storage.type.java\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.assignment.go\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\\"keyword.operator.arithmetic.go\\\", \\\"keyword.operator.address.go\\\"],\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.package.go\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.other.namespace.use.php\\\",\\n      \\\"support.other.namespace.use-as.php\\\",\\n      \\\"support.other.namespace.php\\\",\\n      \\\"entity.other.alias.php\\\",\\n      \\\"meta.interface.php\\\",\\n    ],\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.error-control.php\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.type.php\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.section.array.begin.php\\\",\\n      \\\"punctuation.section.array.end.php\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"storage.type.php\\\",\\n      \\\"meta.other.type.phpdoc.php\\\",\\n      \\\"keyword.other.type.php\\\",\\n      \\\"keyword.other.array.phpdoc.php\\\",\\n    ],\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"meta.function-call.php\\\",\\n      \\\"meta.function-call.object.php\\\",\\n      \\\"meta.function-call.static.php\\\",\\n    ],\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.parameters.begin.bracket.round.php\\\",\\n      \\\"punctuation.definition.parameters.end.bracket.round.php\\\",\\n      \\\"punctuation.separator.delimiter.php\\\",\\n      \\\"punctuation.section.scope.begin.php\\\",\\n      \\\"punctuation.section.scope.end.php\\\",\\n      \\\"punctuation.terminator.expression.php\\\",\\n      \\\"punctuation.definition.arguments.begin.bracket.round.php\\\",\\n      \\\"punctuation.definition.arguments.end.bracket.round.php\\\",\\n      \\\"punctuation.definition.storage-type.begin.bracket.round.php\\\",\\n      \\\"punctuation.definition.storage-type.end.bracket.round.php\\\",\\n      \\\"punctuation.definition.array.begin.bracket.round.php\\\",\\n      \\\"punctuation.definition.array.end.bracket.round.php\\\",\\n      \\\"punctuation.definition.begin.bracket.round.php\\\",\\n      \\\"punctuation.definition.end.bracket.round.php\\\",\\n      \\\"punctuation.definition.begin.bracket.curly.php\\\",\\n      \\\"punctuation.definition.end.bracket.curly.php\\\",\\n      \\\"punctuation.definition.section.switch-block.end.bracket.curly.php\\\",\\n      \\\"punctuation.definition.section.switch-block.start.bracket.curly.php\\\",\\n      \\\"punctuation.definition.section.switch-block.begin.bracket.curly.php\\\",\\n      \\\"punctuation.definition.section.switch-block.end.bracket.curly.php\\\",\\n    ],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.constant.ext.php\\\",\\n      \\\"support.constant.std.php\\\",\\n      \\\"support.constant.core.php\\\",\\n      \\\"support.constant.parser-token.php\\\",\\n    ],\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\\"entity.name.goto-label.php\\\", \\\"support.other.php\\\"],\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.logical.php\\\",\\n      \\\"keyword.operator.bitwise.php\\\",\\n      \\\"keyword.operator.arithmetic.php\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.regexp.php\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.comparison.php\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\\"keyword.operator.heredoc.php\\\", \\\"keyword.operator.nowdoc.php\\\"],\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"variable.other.class.php\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.illegal.non-null-typehinted.php\\\",\\n    settings: { foreground: \\\"#f44747\\\" },\\n  },\\n  {\\n    scope: \\\"variable.other.generic-type.haskell\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"storage.type.haskell\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"storage.type.cs\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.variable.local.cs\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.label.cs\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"entity.name.scope-resolution.function.call\\\",\\n      \\\"entity.name.scope-resolution.function.definition\\\",\\n    ],\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.delayed.unison\\\",\\n      \\\"punctuation.definition.list.begin.unison\\\",\\n      \\\"punctuation.definition.list.end.unison\\\",\\n      \\\"punctuation.definition.ability.begin.unison\\\",\\n      \\\"punctuation.definition.ability.end.unison\\\",\\n      \\\"punctuation.operator.assignment.as.unison\\\",\\n      \\\"punctuation.separator.pipe.unison\\\",\\n      \\\"punctuation.separator.delimiter.unison\\\",\\n      \\\"punctuation.definition.hash.unison\\\",\\n    ],\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.edge\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.prelude.elm\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.elm\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"entity.global.clojure\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"meta.symbol.clojure\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"constant.keyword.clojure\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\\"meta.arguments.coffee\\\", \\\"variable.parameter.function.coffee\\\"],\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"storage.modifier.import.groovy\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"meta.method.groovy\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"meta.definition.variable.name.groovy\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"meta.definition.class.inherited.classes.groovy\\\",\\n    settings: { foreground: \\\"#199f43\\\" },\\n  },\\n  {\\n    scope: \\\"support.variable.semantic.hlsl\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.type.texture.hlsl\\\",\\n      \\\"support.type.sampler.hlsl\\\",\\n      \\\"support.type.object.hlsl\\\",\\n      \\\"support.type.object.rw.hlsl\\\",\\n      \\\"support.type.fx.hlsl\\\",\\n      \\\"support.type.object.hlsl\\\",\\n    ],\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: [\\\"text.variable\\\", \\\"text.bracketed\\\"],\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: [\\\"support.type.swift\\\", \\\"support.type.vb.asp\\\"],\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"meta.scope.prerequisites.makefile\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"source.makefile\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"source.ini\\\",\\n    settings: { foreground: \\\"#199f43\\\" },\\n  },\\n  {\\n    scope: \\\"constant.language.symbol.ruby\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\\"function.parameter.ruby\\\", \\\"function.parameter.cs\\\"],\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"constant.language.symbol.elixir\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope:\\n      \\\"text.html.laravel-blade source.php.embedded.line.html entity.name.tag.laravel-blade\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope:\\n      \\\"text.html.laravel-blade source.php.embedded.line.html support.constant.laravel-blade\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.function.xi\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.class.xi\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"constant.character.character-class.regexp.xi\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"constant.regexp.xi\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.control.xi\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.xi\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"beginning.punctuation.definition.quote.markdown.xi\\\",\\n    settings: { foreground: \\\"#199f43\\\" },\\n  },\\n  {\\n    scope: \\\"beginning.punctuation.definition.list.markdown.xi\\\",\\n    settings: { foreground: \\\"#84848A\\\" },\\n  },\\n  {\\n    scope: \\\"constant.character.xi\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"accent.xi\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"wikiword.xi\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"constant.other.color.rgb-value.xi\\\",\\n    settings: { foreground: \\\"#ffffff\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.tag.xi\\\",\\n    settings: { foreground: \\\"#84848A\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.constant.property-value.scss\\\",\\n      \\\"support.constant.property-value.css\\\",\\n    ],\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"keyword.operator.css\\\",\\n      \\\"keyword.operator.scss\\\",\\n      \\\"keyword.operator.less\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"support.constant.color.w3c-standard-color-name.css\\\",\\n      \\\"support.constant.color.w3c-standard-color-name.scss\\\",\\n    ],\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.separator.list.comma.css\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.vendored.property-name.css\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.property-name.css\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.property-name\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.property-value\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"support.constant.font-name\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"entity.other.attribute-name.class.css\\\",\\n    settings: {\\n      foreground: \\\"#16a994\\\",\\n      fontStyle: \\\"normal\\\",\\n    },\\n  },\\n  {\\n    scope: \\\"entity.other.attribute-name.id\\\",\\n    settings: {\\n      foreground: \\\"#7b43f8\\\",\\n      fontStyle: \\\"normal\\\",\\n    },\\n  },\\n  {\\n    scope: [\\n      \\\"entity.other.attribute-name.pseudo-element\\\",\\n      \\\"entity.other.attribute-name.pseudo-class\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"meta.selector\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"selector.sass\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"rgb-value\\\",\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"inline-color-decoration rgb-value\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"less rgb-value\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"control.elements\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.less\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.tag\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"entity.other.attribute-name\\\",\\n    settings: {\\n      foreground: \\\"#16a994\\\",\\n      fontStyle: \\\"normal\\\",\\n    },\\n  },\\n  {\\n    scope: \\\"constant.character.entity\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"meta.tag\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.illegal.bad-ampersand.html\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"markup.heading\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"markup.heading punctuation.definition.heading\\\",\\n      \\\"entity.name.section\\\",\\n    ],\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"entity.name.section.markdown\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.heading.markdown\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"markup.heading.setext\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"markup.heading.setext.1.markdown\\\",\\n      \\\"markup.heading.setext.2.markdown\\\",\\n    ],\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: [\\\"markup.bold\\\", \\\"todo.bold\\\"],\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.bold\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.bold.markdown\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\\"markup.italic\\\", \\\"punctuation.definition.italic\\\", \\\"todo.emphasis\\\"],\\n    settings: {\\n      foreground: \\\"#fc2b73\\\",\\n      fontStyle: \\\"italic\\\",\\n    },\\n  },\\n  {\\n    scope: \\\"emphasis md\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"markup.italic.markdown\\\",\\n    settings: { fontStyle: \\\"italic\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"markup.underline.link.markdown\\\",\\n      \\\"markup.underline.link.image.markdown\\\",\\n    ],\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"string.other.link.title.markdown\\\",\\n      \\\"string.other.link.description.markdown\\\",\\n    ],\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.metadata.markdown\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: [\\\"markup.inline.raw.markdown\\\", \\\"markup.inline.raw.string.markdown\\\"],\\n    settings: { foreground: \\\"#199f43\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.list.begin.markdown\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.list.markdown\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"beginning.punctuation.definition.list.markdown\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"punctuation.definition.string.begin.markdown\\\",\\n      \\\"punctuation.definition.string.end.markdown\\\",\\n    ],\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"markup.quote.markdown\\\",\\n    settings: { foreground: \\\"#84848A\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.other.unit\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"markup.changed.diff\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"meta.diff.header.from-file\\\",\\n      \\\"meta.diff.header.to-file\\\",\\n      \\\"punctuation.definition.from-file.diff\\\",\\n      \\\"punctuation.definition.to-file.diff\\\",\\n    ],\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"markup.inserted.diff\\\",\\n    settings: { foreground: \\\"#199f43\\\" },\\n  },\\n  {\\n    scope: \\\"markup.deleted.diff\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"string.regexp\\\",\\n    settings: { foreground: \\\"#17a5af\\\" },\\n  },\\n  {\\n    scope: \\\"constant.other.character-class.regexp\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"keyword.operator.quantifier.regexp\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"constant.character.escape\\\",\\n    settings: { foreground: \\\"#1ca1c7\\\" },\\n  },\\n  {\\n    scope: \\\"source.json meta.structure.dictionary.json > string.quoted.json\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope:\\n      \\\"source.json meta.structure.dictionary.json > string.quoted.json > punctuation.string\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"source.json meta.structure.dictionary.json > value.json > string.quoted.json\\\",\\n      \\\"source.json meta.structure.array.json > value.json > string.quoted.json\\\",\\n      \\\"source.json meta.structure.dictionary.json > value.json > string.quoted.json > punctuation\\\",\\n      \\\"source.json meta.structure.array.json > value.json > string.quoted.json > punctuation\\\",\\n    ],\\n    settings: { foreground: \\\"#199f43\\\" },\\n  },\\n  {\\n    scope: [\\n      \\\"source.json meta.structure.dictionary.json > constant.language.json\\\",\\n      \\\"source.json meta.structure.array.json > constant.language.json\\\",\\n    ],\\n    settings: { foreground: \\\"#08c0ef\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.property-name.json\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"support.type.property-name.json punctuation\\\",\\n    settings: { foreground: \\\"#d52c36\\\" },\\n  },\\n  {\\n    scope: \\\"punctuation.definition.block.sequence.item.yaml\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"block.scope.end\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"block.scope.begin\\\",\\n    settings: { foreground: \\\"#79797F\\\" },\\n  },\\n  {\\n    scope: \\\"token.info-token\\\",\\n    settings: { foreground: \\\"#7b43f8\\\" },\\n  },\\n  {\\n    scope: \\\"token.warn-token\\\",\\n    settings: { foreground: \\\"#d5a910\\\" },\\n  },\\n  {\\n    scope: \\\"token.error-token\\\",\\n    settings: { foreground: \\\"#f44747\\\" },\\n  },\\n  {\\n    scope: \\\"token.debug-token\\\",\\n    settings: { foreground: \\\"#fc2b73\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.illegal\\\",\\n    settings: { foreground: \\\"#ffffff\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.broken\\\",\\n    settings: { foreground: \\\"#ffffff\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.deprecated\\\",\\n    settings: { foreground: \\\"#ffffff\\\" },\\n  },\\n  {\\n    scope: \\\"invalid.unimplemented\\\",\\n    settings: { foreground: \\\"#ffffff\\\" },\\n  },\\n];\\nvar semanticTokenColors = {\\n  comment: \\\"#84848A\\\",\\n  string: \\\"#199f43\\\",\\n  number: \\\"#1ca1c7\\\",\\n  regexp: \\\"#17a5af\\\",\\n  keyword: \\\"#fc2b73\\\",\\n  variable: \\\"#d47628\\\",\\n  parameter: \\\"#79797F\\\",\\n  property: \\\"#d47628\\\",\\n  function: \\\"#7b43f8\\\",\\n  method: \\\"#7b43f8\\\",\\n  type: \\\"#c635e4\\\",\\n  class: \\\"#c635e4\\\",\\n  namespace: \\\"#d5a910\\\",\\n  enumMember: \\\"#08c0ef\\\",\\n  \\\"variable.constant\\\": \\\"#d5a910\\\",\\n  \\\"variable.defaultLibrary\\\": \\\"#d5a910\\\",\\n};\\nvar pierre_light_default = {\\n  name,\\n  type,\\n  colors,\\n  tokenColors,\\n  semanticTokenColors,\\n};\\n\\n//#endregion\\nexport {\\n  colors,\\n  pierre_light_default as default,\\n  name,\\n  semanticTokenColors,\\n  tokenColors,\\n  type,\\n};\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { ReactNode } from \\\"react\\\";\\n\\n/**\\n * Tool UI conventions:\\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\\n * - Schema: `SerializableXSchema`\\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\\n * - Root attrs: `data-tool-ui-id` + `data-slot`\\n */\\n\\n/**\\n * Schema for tool UI identity.\\n *\\n * Every tool UI should have a unique identifier that:\\n * - Is stable across re-renders\\n * - Is meaningful (not auto-generated)\\n * - Is unique within the conversation\\n *\\n * Format recommendation: `{component-type}-{semantic-identifier}`\\n * Examples: \\\"data-table-expenses-q3\\\", \\\"option-list-deploy-target\\\"\\n */\\nexport const ToolUIIdSchema = z.string().min(1);\\n\\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\\n\\n/**\\n * Primary role of a Tool UI surface in a chat context.\\n */\\nexport const ToolUIRoleSchema = z.enum([\\n  \\\"information\\\",\\n  \\\"decision\\\",\\n  \\\"control\\\",\\n  \\\"state\\\",\\n  \\\"composite\\\",\\n]);\\n\\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\\n\\nexport const ToolUIReceiptOutcomeSchema = z.enum([\\n  \\\"success\\\",\\n  \\\"partial\\\",\\n  \\\"failed\\\",\\n  \\\"cancelled\\\",\\n]);\\n\\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\\n\\n/**\\n * Optional receipt metadata: a durable summary of an outcome.\\n */\\nexport const ToolUIReceiptSchema = z.object({\\n  outcome: ToolUIReceiptOutcomeSchema,\\n  summary: z.string().min(1),\\n  identifiers: z.record(z.string(), z.string()).optional(),\\n  at: z.string().datetime(),\\n});\\n\\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\\n\\n/**\\n * Base schema for Tool UI payloads (id + optional role/receipt).\\n */\\nexport const ToolUISurfaceSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n});\\n\\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\\n\\nexport const ActionSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  /**\\n   * Canonical narration the assistant can use after this action is taken.\\n   *\\n   * Example: \\\"I exported the table as CSV.\\\" / \\\"I opened the link in a new tab.\\\"\\n   */\\n  sentence: z.string().optional(),\\n  confirmLabel: z.string().optional(),\\n  variant: z\\n    .enum([\\\"default\\\", \\\"destructive\\\", \\\"secondary\\\", \\\"ghost\\\", \\\"outline\\\"])\\n    .optional(),\\n  icon: z.custom<ReactNode>().optional(),\\n  loading: z.boolean().optional(),\\n  disabled: z.boolean().optional(),\\n  shortcut: z.string().optional(),\\n});\\n\\nexport type Action = z.infer<typeof ActionSchema>;\\nexport type LocalAction = Action;\\nexport type DecisionAction = Action;\\n\\nexport const DecisionResultSchema = z.object({\\n  kind: z.literal(\\\"decision\\\"),\\n  version: z.literal(1),\\n  decisionId: z.string().min(1),\\n  actionId: z.string().min(1),\\n  actionLabel: z.string().min(1),\\n  at: z.string().datetime(),\\n  payload: z.record(z.string(), z.unknown()).optional(),\\n});\\n\\nexport type DecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n> = Omit<z.infer<typeof DecisionResultSchema>, \\\"payload\\\"> & {\\n  payload?: TPayload;\\n};\\n\\nexport function createDecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n>(args: {\\n  decisionId: string;\\n  action: { id: string; label: string };\\n  payload?: TPayload;\\n}): DecisionResult<TPayload> {\\n  return {\\n    kind: \\\"decision\\\",\\n    version: 1,\\n    decisionId: args.decisionId,\\n    actionId: args.action.id,\\n    actionLabel: args.action.label,\\n    at: new Date().toISOString(),\\n    payload: args.payload,\\n  };\\n}\\n\\nexport const ActionButtonsPropsSchema = z.object({\\n  actions: z.array(ActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n  className: z.string().optional(),\\n});\\n\\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\\n  actions: z.array(SerializableActionSchema),\\n}).omit({ className: true });\\n\\nexport interface ActionsConfig {\\n  items: Action[];\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  confirmTimeout?: number;\\n}\\n\\nexport const SerializableActionsConfigSchema = z.object({\\n  items: z.array(SerializableActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n});\\n\\nexport type SerializableActionsConfig = z.infer<\\n  typeof SerializableActionsConfigSchema\\n>;\\n\\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/use-copy-to-clipboard.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/use-copy-to-clipboard.ts\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { useCallback, useEffect, useState } from \\\"react\\\";\\n\\nfunction fallbackCopyToClipboard(text: string): boolean {\\n  const textArea = document.createElement(\\\"textarea\\\");\\n  try {\\n    textArea.value = text;\\n    textArea.setAttribute(\\\"readonly\\\", \\\"\\\");\\n    textArea.style.position = \\\"fixed\\\";\\n    textArea.style.top = \\\"-9999px\\\";\\n    textArea.style.left = \\\"-9999px\\\";\\n    document.body.appendChild(textArea);\\n    textArea.select();\\n    return document.execCommand(\\\"copy\\\");\\n  } catch {\\n    return false;\\n  } finally {\\n    if (textArea.parentNode) {\\n      textArea.parentNode.removeChild(textArea);\\n    }\\n  }\\n}\\n\\nexport function useCopyToClipboard(options?: { resetAfterMs?: number }): {\\n  copiedId: string | null;\\n  copy: (text: string, id?: string) => Promise<boolean>;\\n} {\\n  const resetAfterMs = options?.resetAfterMs ?? 2000;\\n  const [copiedId, setCopiedId] = useState<string | null>(null);\\n\\n  const copy = useCallback(async (text: string, id: string = \\\"default\\\") => {\\n    let ok = false;\\n    try {\\n      if (navigator.clipboard?.writeText) {\\n        await navigator.clipboard.writeText(text);\\n        ok = true;\\n      } else {\\n        ok = fallbackCopyToClipboard(text);\\n      }\\n    } catch {\\n      ok = fallbackCopyToClipboard(text);\\n    }\\n\\n    if (ok) {\\n      setCopiedId(id);\\n    }\\n\\n    return ok;\\n  }, []);\\n\\n  useEffect(() => {\\n    if (!copiedId) return;\\n    const timeout = setTimeout(() => setCopiedId(null), resetAfterMs);\\n    return () => clearTimeout(timeout);\\n  }, [copiedId, resetAfterMs]);\\n\\n  return { copiedId, copy };\\n}\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/data-table.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"data-table\",\n  \"type\": \"registry:block\",\n  \"title\": \"Data Table\",\n  \"description\": \"Sortable, responsive data tables for tool call results.\",\n  \"dependencies\": [\n    \"zod\"\n  ],\n  \"registryDependencies\": [\n    \"accordion\",\n    \"badge\",\n    \"button\",\n    \"dropdown-menu\",\n    \"table\",\n    \"tooltip\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/data-table/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/data-table/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn           → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n *   Button       → shadcn/ui Button\\n *   DropdownMenu → shadcn/ui DropdownMenu\\n *   Accordion    → shadcn/ui Accordion\\n *   Tooltip      → shadcn/ui Tooltip\\n *   Badge        → shadcn/ui Badge\\n *   Table        → shadcn/ui Table\\n */\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport { Button } from \\\"@/components/ui/button\\\";\\nexport {\\n  DropdownMenu,\\n  DropdownMenuContent,\\n  DropdownMenuItem,\\n  DropdownMenuTrigger,\\n} from \\\"@/components/ui/dropdown-menu\\\";\\nexport {\\n  Accordion,\\n  AccordionContent,\\n  AccordionItem,\\n  AccordionTrigger,\\n} from \\\"@/components/ui/accordion\\\";\\nexport {\\n  Tooltip,\\n  TooltipContent,\\n  TooltipProvider,\\n  TooltipTrigger,\\n} from \\\"@/components/ui/tooltip\\\";\\nexport { Badge } from \\\"@/components/ui/badge\\\";\\nexport {\\n  Table,\\n  TableHeader,\\n  TableBody,\\n  TableHead,\\n  TableRow,\\n  TableCell,\\n} from \\\"@/components/ui/table\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/data-table/data-table.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/data-table/data-table.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport * as React from \\\"react\\\";\\nimport {\\n  cn,\\n  Table,\\n  TableBody,\\n  TableRow,\\n  TableCell,\\n  TableHeader,\\n  TableHead,\\n  Button,\\n  Tooltip,\\n  TooltipContent,\\n  TooltipProvider,\\n  TooltipTrigger,\\n  Accordion,\\n  AccordionContent,\\n  AccordionItem,\\n  AccordionTrigger,\\n} from \\\"./_adapter\\\";\\nimport {\\n  sortData,\\n  createDataTableRowKeys,\\n  getDataTableMobileDescriptionId,\\n} from \\\"./utilities\\\";\\nimport { renderFormattedValue } from \\\"./formatters\\\";\\nimport type {\\n  DataTableProps,\\n  DataTableContextValue,\\n  RowData,\\n  DataTableRowData,\\n  ColumnKey,\\n  Column,\\n} from \\\"./types\\\";\\nimport type { FormatConfig } from \\\"./formatters\\\";\\n\\nexport const DEFAULT_LOCALE = \\\"en-US\\\" as const;\\n\\nfunction isNumericFormat(format?: FormatConfig): boolean {\\n  const kind = format?.kind;\\n  return (\\n    kind === \\\"number\\\" ||\\n    kind === \\\"currency\\\" ||\\n    kind === \\\"percent\\\" ||\\n    kind === \\\"delta\\\"\\n  );\\n}\\n\\nfunction getAlignmentClass(\\n  align?: \\\"left\\\" | \\\"right\\\" | \\\"center\\\",\\n): string | undefined {\\n  if (align === \\\"right\\\") return \\\"text-right\\\";\\n  if (align === \\\"center\\\") return \\\"text-center\\\";\\n  return undefined;\\n}\\n\\nconst DataTableContext = React.createContext<\\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\\n  DataTableContextValue<any> | undefined\\n>(undefined);\\n\\nexport function useDataTable<T extends object = RowData>() {\\n  const context = React.use(DataTableContext) as\\n    | DataTableContextValue<T>\\n    | undefined;\\n  if (!context) {\\n    throw new Error(\\\"useDataTable must be used within <DataTable.Provider />\\\");\\n  }\\n  return context;\\n}\\n\\ntype DataTableLayout = \\\"auto\\\" | \\\"table\\\" | \\\"cards\\\";\\n\\ntype DataTableBaseProps<T extends object = RowData> = DataTableProps<T> & {\\n  layout: DataTableLayout;\\n};\\n\\ntype DataTableProviderProps<T extends object = RowData> = Pick<\\n  DataTableProps<T>,\\n  | \\\"columns\\\"\\n  | \\\"data\\\"\\n  | \\\"rowIdKey\\\"\\n  | \\\"defaultSort\\\"\\n  | \\\"sort\\\"\\n  | \\\"onSortChange\\\"\\n  | \\\"id\\\"\\n  | \\\"locale\\\"\\n> & {\\n  children: React.ReactNode;\\n};\\n\\nfunction DataTableProvider<T extends object = RowData>({\\n  columns,\\n  data: rawData,\\n  rowIdKey,\\n  defaultSort,\\n  sort: controlledSort,\\n  id,\\n  onSortChange,\\n  locale,\\n  children,\\n}: DataTableProviderProps<T>) {\\n  // Default locale avoids SSR/client formatting mismatches.\\n  const resolvedLocale = locale ?? DEFAULT_LOCALE;\\n\\n  const [internalSortBy, setInternalSortBy] = React.useState<\\n    ColumnKey<T> | undefined\\n  >(defaultSort?.by);\\n  const [internalSortDirection, setInternalSortDirection] = React.useState<\\n    \\\"asc\\\" | \\\"desc\\\" | undefined\\n  >(defaultSort?.direction);\\n\\n  const sortBy = controlledSort?.by ?? internalSortBy;\\n  const sortDirection = controlledSort?.direction ?? internalSortDirection;\\n\\n  const data = React.useMemo(() => {\\n    if (!sortBy || !sortDirection) return rawData;\\n    return sortData(rawData, sortBy, sortDirection, resolvedLocale);\\n  }, [rawData, sortBy, sortDirection, resolvedLocale]);\\n\\n  const handleSort = React.useCallback(\\n    (key: ColumnKey<T>) => {\\n      let newDirection: \\\"asc\\\" | \\\"desc\\\" | undefined;\\n\\n      if (sortBy === key) {\\n        if (sortDirection === \\\"asc\\\") {\\n          newDirection = \\\"desc\\\";\\n        } else if (sortDirection === \\\"desc\\\") {\\n          newDirection = undefined;\\n        } else {\\n          newDirection = \\\"asc\\\";\\n        }\\n      } else {\\n        newDirection = \\\"asc\\\";\\n      }\\n\\n      const next = {\\n        by: newDirection ? key : undefined,\\n        direction: newDirection,\\n      } as const;\\n\\n      if (controlledSort) {\\n        onSortChange?.(next);\\n      } else {\\n        setInternalSortBy(next.by);\\n        setInternalSortDirection(next.direction);\\n      }\\n    },\\n    [sortBy, sortDirection, controlledSort, onSortChange],\\n  );\\n\\n  const contextValue: DataTableContextValue<T> = {\\n    columns,\\n    data,\\n    rowIdKey,\\n    sortBy,\\n    sortDirection,\\n    toggleSort: handleSort,\\n    id,\\n    locale: resolvedLocale,\\n  };\\n\\n  return (\\n    <DataTableContext.Provider value={contextValue}>\\n      {children}\\n    </DataTableContext.Provider>\\n  );\\n}\\n\\ninterface DataTableLayoutProps {\\n  layout: DataTableLayout;\\n  emptyMessage: string;\\n  maxHeight?: string;\\n  className?: string;\\n}\\n\\nfunction DataTableLayout({\\n  layout,\\n  emptyMessage,\\n  maxHeight,\\n  className,\\n}: DataTableLayoutProps) {\\n  const { columns, data, rowIdKey, sortBy, sortDirection, id } = useDataTable();\\n  const rowKeys = React.useMemo(\\n    () =>\\n      createDataTableRowKeys(\\n        data as Array<Record<string, unknown>>,\\n        rowIdKey ? String(rowIdKey) : undefined,\\n      ),\\n    [data, rowIdKey],\\n  );\\n  const mobileDescriptionId = React.useMemo(\\n    () => getDataTableMobileDescriptionId(String(id ?? \\\"data-table\\\")),\\n    [id],\\n  );\\n\\n  const sortAnnouncement = React.useMemo(() => {\\n    const col = columns.find((c) => c.key === sortBy);\\n    const label = col?.label ?? sortBy;\\n    return sortBy && sortDirection\\n      ? `Sorted by ${label}, ${sortDirection === \\\"asc\\\" ? \\\"ascending\\\" : \\\"descending\\\"}`\\n      : \\\"\\\";\\n  }, [columns, sortBy, sortDirection]);\\n\\n  return (\\n    <div\\n      className={cn(\\\"@container w-full min-w-80\\\", className)}\\n      data-tool-ui-id={id}\\n      data-slot=\\\"data-table\\\"\\n      data-layout={layout}\\n    >\\n      <div\\n        className={cn(\\n          layout === \\\"table\\\"\\n            ? \\\"block\\\"\\n            : layout === \\\"cards\\\"\\n              ? \\\"hidden\\\"\\n              : \\\"hidden @md:block\\\",\\n        )}\\n      >\\n        <div className=\\\"relative\\\">\\n          <div\\n            className={cn(\\n              \\\"bg-card relative w-full overflow-clip overflow-y-auto rounded-lg border\\\",\\n              \\\"touch-pan-x\\\",\\n              maxHeight && \\\"max-h-[--max-height]\\\",\\n            )}\\n            style={\\n              maxHeight\\n                ? ({ \\\"--max-height\\\": maxHeight } as React.CSSProperties)\\n                : undefined\\n            }\\n          >\\n            <Table>\\n              {columns.length > 0 && (\\n                <colgroup>\\n                  {columns.map((col) => (\\n                    <col\\n                      key={String(col.key)}\\n                      style={col.width ? { width: col.width } : undefined}\\n                    />\\n                  ))}\\n                </colgroup>\\n              )}\\n              {data.length === 0 ? (\\n                <DataTableEmpty message={emptyMessage} />\\n              ) : (\\n                <DataTableContent />\\n              )}\\n            </Table>\\n          </div>\\n        </div>\\n      </div>\\n\\n      <div\\n        className={cn(\\n          layout === \\\"cards\\\"\\n            ? \\\"\\\"\\n            : layout === \\\"table\\\"\\n              ? \\\"hidden\\\"\\n              : \\\"@md:hidden\\\",\\n        )}\\n        role=\\\"list\\\"\\n        aria-label=\\\"Data table (mobile card view)\\\"\\n        aria-describedby={mobileDescriptionId}\\n      >\\n        <div id={mobileDescriptionId} className=\\\"sr-only\\\">\\n          Table data shown as expandable cards. Each card represents one row.\\n          {columns.length > 0 &&\\n            ` Columns: ${columns.map((c) => c.label).join(\\\", \\\")}.`}\\n        </div>\\n\\n        {data.length === 0 ? (\\n          <div className=\\\"text-muted-foreground py-8 text-center\\\">\\n            {emptyMessage}\\n          </div>\\n        ) : (\\n          <div className=\\\"bg-card flex flex-col overflow-hidden rounded-2xl border shadow-xs\\\">\\n            {data.map((row, i) => {\\n              const rowKey = rowKeys[i];\\n              return (\\n                <DataTableAccordionCard\\n                  key={rowKey}\\n                  row={row as unknown as DataTableRowData}\\n                  index={i}\\n                  rowKey={rowKey}\\n                  isFirst={i === 0}\\n                />\\n              );\\n            })}\\n          </div>\\n        )}\\n      </div>\\n\\n      {sortAnnouncement && (\\n        <div className=\\\"sr-only\\\" aria-live=\\\"polite\\\">\\n          {sortAnnouncement}\\n        </div>\\n      )}\\n    </div>\\n  );\\n}\\n\\nfunction DataTableBase<T extends object = RowData>(\\n  props: DataTableBaseProps<T>,\\n) {\\n  const {\\n    columns,\\n    data,\\n    rowIdKey,\\n    defaultSort,\\n    sort,\\n    onSortChange,\\n    id,\\n    locale,\\n    layout,\\n    emptyMessage = \\\"No data available\\\",\\n    maxHeight,\\n    className,\\n  } = props;\\n\\n  return (\\n    <DataTableProvider\\n      columns={columns}\\n      data={data}\\n      rowIdKey={rowIdKey}\\n      defaultSort={defaultSort}\\n      sort={sort}\\n      onSortChange={onSortChange}\\n      id={id}\\n      locale={locale}\\n    >\\n      <DataTableLayout\\n        layout={layout}\\n        emptyMessage={emptyMessage}\\n        maxHeight={maxHeight}\\n        className={className}\\n      />\\n    </DataTableProvider>\\n  );\\n}\\n\\nfunction DataTableRoot<T extends object = RowData>(props: DataTableProps<T>) {\\n  return <DataTableBase {...props} layout=\\\"auto\\\" />;\\n}\\n\\nfunction DataTableTable<T extends object = RowData>(props: DataTableProps<T>) {\\n  return <DataTableBase {...props} layout=\\\"table\\\" />;\\n}\\n\\nfunction DataTableCards<T extends object = RowData>(props: DataTableProps<T>) {\\n  return <DataTableBase {...props} layout=\\\"cards\\\" />;\\n}\\n\\ntype DataTableComponent = {\\n  <T extends object = RowData>(props: DataTableProps<T>): React.ReactElement;\\n  Table: typeof DataTableTable;\\n  Cards: typeof DataTableCards;\\n  Provider: typeof DataTableProvider;\\n};\\n\\nexport const DataTable = Object.assign(DataTableRoot, {\\n  Table: DataTableTable,\\n  Cards: DataTableCards,\\n  Provider: DataTableProvider,\\n}) as DataTableComponent;\\n\\nfunction DataTableContent() {\\n  return (\\n    <>\\n      <DataTableHeader />\\n      <DataTableBody />\\n    </>\\n  );\\n}\\n\\nfunction DataTableEmpty({ message }: { message: string }) {\\n  const { columns } = useDataTable();\\n\\n  return (\\n    <TableBody>\\n      <TableRow className=\\\"bg-card h-24 text-center\\\">\\n        <TableCell colSpan={columns.length} role=\\\"status\\\" aria-live=\\\"polite\\\">\\n          {message}\\n        </TableCell>\\n      </TableRow>\\n    </TableBody>\\n  );\\n}\\n\\nfunction SortIcon({ state }: { state?: \\\"asc\\\" | \\\"desc\\\" }) {\\n  let char = \\\"⇅\\\";\\n  let className = \\\"opacity-20\\\";\\n\\n  if (state === \\\"asc\\\") {\\n    char = \\\"↑\\\";\\n    className = \\\"\\\";\\n  }\\n\\n  if (state === \\\"desc\\\") {\\n    char = \\\"↓\\\";\\n    className = \\\"\\\";\\n  }\\n\\n  return (\\n    <span aria-hidden className={cn(\\\"min-w-4 shrink-0 text-center\\\", className)}>\\n      {char}\\n    </span>\\n  );\\n}\\n\\nfunction DataTableHeader() {\\n  const { columns } = useDataTable();\\n\\n  return (\\n    <TooltipProvider delayDuration={300}>\\n      <TableHeader>\\n        <TableRow className=\\\"hover:bg-transparent\\\">\\n          {columns.map((column, columnIndex) => (\\n            <DataTableHead\\n              key={column.key}\\n              column={column}\\n              columnIndex={columnIndex}\\n              totalColumns={columns.length}\\n            />\\n          ))}\\n        </TableRow>\\n      </TableHeader>\\n    </TooltipProvider>\\n  );\\n}\\n\\ninterface DataTableHeadProps {\\n  column: Column;\\n  columnIndex?: number;\\n  totalColumns?: number;\\n}\\n\\nfunction DataTableHead({\\n  column,\\n  columnIndex = 0,\\n  totalColumns = 1,\\n}: DataTableHeadProps) {\\n  const { sortBy, sortDirection, toggleSort } = useDataTable();\\n  const isFirstColumn = columnIndex === 0;\\n  const isLastColumn = columnIndex === totalColumns - 1;\\n\\n  const isSortable = column.sortable !== false;\\n\\n  const isSorted = sortBy === column.key;\\n  const direction = isSorted ? sortDirection : undefined;\\n  const isDisabled = !isSortable;\\n\\n  const handleClick = () => {\\n    if (!isDisabled && toggleSort) {\\n      toggleSort(column.key);\\n    }\\n  };\\n\\n  const displayText = column.abbr || column.label;\\n  const shouldShowTooltip = column.abbr || displayText.length > 15;\\n  const isNumericKind = isNumericFormat(column.format);\\n  const align =\\n    column.align ??\\n    (columnIndex === 0 ? \\\"left\\\" : isNumericKind ? \\\"right\\\" : \\\"left\\\");\\n  const alignClass = getAlignmentClass(align);\\n  const buttonAlignClass = cn(\\n    \\\"min-w-0 gap-1 font-normal\\\",\\n    align === \\\"right\\\" && \\\"text-right\\\",\\n    align === \\\"center\\\" && \\\"text-center\\\",\\n    align === \\\"left\\\" && \\\"text-left\\\",\\n  );\\n  const labelAlignClass =\\n    align === \\\"right\\\"\\n      ? \\\"text-right\\\"\\n      : align === \\\"center\\\"\\n        ? \\\"text-center\\\"\\n        : \\\"text-left\\\";\\n\\n  return (\\n    <TableHead\\n      scope=\\\"col\\\"\\n      className={cn(\\n        alignClass,\\n        isFirstColumn && \\\"pl-1\\\",\\n        isLastColumn && \\\"pr-1\\\",\\n      )}\\n      style={column.width ? { width: column.width } : undefined}\\n      aria-sort={\\n        isSorted\\n          ? direction === \\\"asc\\\"\\n            ? \\\"ascending\\\"\\n            : \\\"descending\\\"\\n          : undefined\\n      }\\n    >\\n      <Button\\n        type=\\\"button\\\"\\n        size=\\\"sm\\\"\\n        onClick={handleClick}\\n        onKeyDown={(e) => {\\n          if (isDisabled) return;\\n          if (e.key === \\\"Enter\\\" || e.key === \\\" \\\") {\\n            e.preventDefault();\\n            handleClick();\\n          }\\n        }}\\n        disabled={isDisabled}\\n        variant=\\\"ghost\\\"\\n        className={cn(\\n          buttonAlignClass,\\n          \\\"w-fit min-w-10\\\",\\n          isFirstColumn && \\\"pl-4\\\",\\n          isLastColumn && \\\"pr-4\\\",\\n        )}\\n        aria-label={\\n          `Sort by ${column.label}` +\\n          (isSorted && direction\\n            ? ` (${direction === \\\"asc\\\" ? \\\"ascending\\\" : \\\"descending\\\"})`\\n            : \\\"\\\")\\n        }\\n        aria-disabled={isDisabled || undefined}\\n      >\\n        {shouldShowTooltip ? (\\n          <Tooltip>\\n            <TooltipTrigger asChild>\\n              <span className={cn(\\\"truncate\\\", labelAlignClass)}>\\n                {column.abbr ? (\\n                  <abbr\\n                    title={column.label}\\n                    className={cn(\\n                      \\\"cursor-help border-b border-dotted border-current no-underline\\\",\\n                      labelAlignClass,\\n                    )}\\n                  >\\n                    {column.abbr}\\n                  </abbr>\\n                ) : (\\n                  <span className={labelAlignClass}>{column.label}</span>\\n                )}\\n              </span>\\n            </TooltipTrigger>\\n            <TooltipContent>\\n              <p>{column.label}</p>\\n            </TooltipContent>\\n          </Tooltip>\\n        ) : (\\n          <span className={cn(\\\"truncate\\\", labelAlignClass)}>\\n            {column.label}\\n          </span>\\n        )}\\n        {isSortable && <SortIcon state={direction} />}\\n      </Button>\\n    </TableHead>\\n  );\\n}\\n\\nfunction DataTableBody() {\\n  const { data, rowIdKey } = useDataTable<DataTableRowData>();\\n  const rowKeys = React.useMemo(\\n    () =>\\n      createDataTableRowKeys(\\n        data as Array<Record<string, unknown>>,\\n        rowIdKey ? String(rowIdKey) : undefined,\\n      ),\\n    [data, rowIdKey],\\n  );\\n  const hasWarnedRowKeyRef = React.useRef(false);\\n\\n  React.useEffect(() => {\\n    if (hasWarnedRowKeyRef.current) return;\\n    if (process.env.NODE_ENV !== \\\"production\\\" && !rowIdKey && data.length > 0) {\\n      hasWarnedRowKeyRef.current = true;\\n      console.warn(\\n        \\\"[DataTable] Missing `rowIdKey` prop. Falling back to inferred/content-derived row keys. \\\" +\\n          \\\"Strongly recommended: Pass a `rowIdKey` prop that points to a unique identifier in your row data (e.g., 'id', 'uuid', 'symbol').\\\\n\\\" +\\n          'Example: <DataTable rowIdKey=\\\"id\\\" columns={...} data={...} />',\\n      );\\n    }\\n  }, [rowIdKey, data.length]);\\n\\n  return (\\n    <TableBody>\\n      {data.map((row, index) => {\\n        const rowKey = rowKeys[index];\\n        return <DataTableRow key={rowKey} row={row} />;\\n      })}\\n    </TableBody>\\n  );\\n}\\n\\ninterface DataTableRowProps {\\n  row: DataTableRowData;\\n  className?: string;\\n}\\n\\nfunction DataTableRow({ row, className }: DataTableRowProps) {\\n  const { columns } = useDataTable();\\n\\n  return (\\n    <TableRow className={className}>\\n      {columns.map((column, columnIndex) => (\\n        <DataTableCell\\n          key={column.key}\\n          value={row[column.key]}\\n          column={column}\\n          row={row}\\n          columnIndex={columnIndex}\\n        />\\n      ))}\\n    </TableRow>\\n  );\\n}\\n\\ninterface DataTableCellProps {\\n  value:\\n    | string\\n    | number\\n    | boolean\\n    | null\\n    | (string | number | boolean | null)[];\\n  column: Column;\\n  row: DataTableRowData;\\n  className?: string;\\n  columnIndex?: number;\\n}\\n\\nfunction DataTableCell({\\n  value,\\n  column,\\n  row,\\n  className,\\n  columnIndex = 0,\\n}: DataTableCellProps) {\\n  const { locale } = useDataTable();\\n  const isNumericKind = isNumericFormat(column.format);\\n  const isNumericValue = typeof value === \\\"number\\\";\\n  const displayValue = renderFormattedValue({ value, column, row, locale });\\n  const align =\\n    column.align ??\\n    (columnIndex === 0\\n      ? \\\"left\\\"\\n      : isNumericKind || isNumericValue\\n        ? \\\"right\\\"\\n        : \\\"left\\\");\\n  const alignClass = getAlignmentClass(align);\\n\\n  return (\\n    <TableCell className={cn(\\\"px-5 py-3\\\", alignClass, className)}>\\n      {displayValue}\\n    </TableCell>\\n  );\\n}\\n\\nfunction categorizeColumns(columns: Column[]) {\\n  const primary: Column[] = [];\\n  const secondary: Column[] = [];\\n\\n  let visibleColumnCount = 0;\\n  columns.forEach((col) => {\\n    if (col.hideOnMobile) return;\\n\\n    if (col.priority === \\\"primary\\\") {\\n      primary.push(col);\\n    } else if (col.priority === \\\"secondary\\\") {\\n      secondary.push(col);\\n    } else if (col.priority === \\\"tertiary\\\") {\\n      return;\\n    } else {\\n      if (visibleColumnCount < 2) {\\n        primary.push(col);\\n      } else {\\n        secondary.push(col);\\n      }\\n      visibleColumnCount++;\\n    }\\n  });\\n\\n  return { primary, secondary };\\n}\\n\\ninterface DataTableAccordionCardProps {\\n  row: DataTableRowData;\\n  index: number;\\n  rowKey: string;\\n  isFirst?: boolean;\\n}\\n\\nfunction getDataTableRowDomId(rowKey: string): string {\\n  return encodeURIComponent(rowKey).replace(/%/g, \\\"_\\\");\\n}\\n\\nfunction DataTableAccordionCard({\\n  row,\\n  index,\\n  rowKey,\\n  isFirst = false,\\n}: DataTableAccordionCardProps) {\\n  const { columns, locale } = useDataTable();\\n\\n  const { primary, secondary } = React.useMemo(\\n    () => categorizeColumns(columns),\\n    [columns],\\n  );\\n\\n  if (secondary.length === 0) {\\n    return (\\n      <SimpleCard\\n        row={row}\\n        columns={primary}\\n        index={index}\\n        rowKey={rowKey}\\n        isFirst={isFirst}\\n      />\\n    );\\n  }\\n\\n  const primaryColumn = primary[0];\\n  const remainingPrimaryColumns = primary.slice(1);\\n\\n  const stableRowId = getDataTableRowDomId(rowKey);\\n\\n  const headingId = `row-${stableRowId}-heading`;\\n  const detailsId = `row-${stableRowId}-details`;\\n  const remainingPrimaryDataIds = remainingPrimaryColumns.map(\\n    (col) => `row-${stableRowId}-${String(col.key)}`,\\n  );\\n\\n  const primaryValue = primaryColumn\\n    ? String(row[primaryColumn.key] ?? \\\"\\\")\\n    : \\\"\\\";\\n  const rowLabel = `Row ${index + 1}: ${primaryValue}`;\\n  const accordionItemId = `row-${stableRowId}`;\\n\\n  return (\\n    <Accordion\\n      type=\\\"single\\\"\\n      collapsible\\n      className={cn(!isFirst && \\\"border-t\\\")}\\n      role=\\\"listitem\\\"\\n      aria-label={rowLabel}\\n    >\\n      <AccordionItem value={accordionItemId} className=\\\"group border-0\\\">\\n        <AccordionTrigger\\n          className=\\\"group-data-[state=closed]:hover:bg-accent/50 active:bg-accent/50 group-data-[state=open]:bg-muted w-full rounded-none px-4 py-3 hover:no-underline\\\"\\n          aria-controls={detailsId}\\n          aria-label={`${rowLabel}. ${secondary.length > 0 ? \\\"Expand for details\\\" : \\\"\\\"}`}\\n        >\\n          <div className=\\\"flex min-w-0 flex-1 flex-col gap-2\\\">\\n            {primaryColumn && (\\n              <div\\n                id={headingId}\\n                role=\\\"heading\\\"\\n                aria-level={3}\\n                className=\\\"truncate\\\"\\n                aria-label={`${primaryColumn.label}: ${row[primaryColumn.key]}`}\\n              >\\n                {renderFormattedValue({\\n                  value: row[primaryColumn.key],\\n                  column: primaryColumn,\\n                  row,\\n                  locale,\\n                })}\\n              </div>\\n            )}\\n\\n            {remainingPrimaryColumns.length > 0 && (\\n              <div\\n                className=\\\"text-muted-foreground flex w-full flex-wrap gap-x-4 gap-y-0.5\\\"\\n                role=\\\"group\\\"\\n                aria-label=\\\"Summary information\\\"\\n              >\\n                {remainingPrimaryColumns.map((col, idx) => (\\n                  <span\\n                    key={col.key}\\n                    id={remainingPrimaryDataIds[idx]}\\n                    className=\\\"flex min-w-0 gap-1 font-normal\\\"\\n                    role=\\\"cell\\\"\\n                    aria-label={`${col.label}: ${row[col.key]}`}\\n                  >\\n                    <span className=\\\"sr-only\\\">{col.label}:</span>\\n                    <span aria-hidden=\\\"true\\\">{col.label}:</span>\\n                    <span className=\\\"truncate\\\">\\n                      {renderFormattedValue({\\n                        value: row[col.key],\\n                        column: col,\\n                        row,\\n                        locale,\\n                      })}\\n                    </span>\\n                  </span>\\n                ))}\\n              </div>\\n            )}\\n          </div>\\n        </AccordionTrigger>\\n\\n        <AccordionContent\\n          className={\\\"flex flex-col gap-4 px-4 pb-4\\\"}\\n          id={detailsId}\\n          role=\\\"region\\\"\\n          aria-labelledby={headingId}\\n        >\\n          {secondary.length > 0 && (\\n            <dl\\n              className={cn(\\n                \\\"flex flex-col gap-2 pt-4\\\",\\n                \\\"motion-safe:group-data-[state=open]:animate-in motion-safe:group-data-[state=open]:fade-in-0\\\",\\n                \\\"motion-safe:group-data-[state=open]:slide-in-from-top-1\\\",\\n                \\\"motion-safe:group-data-[state=closed]:animate-out motion-safe:group-data-[state=closed]:fade-out-0\\\",\\n                \\\"motion-safe:group-data-[state=closed]:slide-out-to-top-1\\\",\\n                \\\"duration-150\\\",\\n              )}\\n              role=\\\"list\\\"\\n              aria-label=\\\"Additional data\\\"\\n            >\\n              {secondary.map((col) => (\\n                <div\\n                  key={col.key}\\n                  className=\\\"flex items-start justify-between gap-4\\\"\\n                  role=\\\"listitem\\\"\\n                >\\n                  <dt\\n                    className=\\\"text-muted-foreground shrink-0\\\"\\n                    id={`row-${stableRowId}-${String(col.key)}-label`}\\n                  >\\n                    {col.label}\\n                  </dt>\\n                  <dd\\n                    className={cn(\\n                      \\\"text-foreground min-w-0 text-pretty wrap-break-word\\\",\\n                      col.align === \\\"right\\\" && \\\"text-right\\\",\\n                      col.align === \\\"center\\\" && \\\"text-center\\\",\\n                    )}\\n                    role=\\\"cell\\\"\\n                    aria-labelledby={`row-${stableRowId}-${String(col.key)}-label`}\\n                  >\\n                    {renderFormattedValue({\\n                      value: row[col.key],\\n                      column: col,\\n                      row,\\n                      locale,\\n                    })}\\n                  </dd>\\n                </div>\\n              ))}\\n            </dl>\\n          )}\\n        </AccordionContent>\\n      </AccordionItem>\\n    </Accordion>\\n  );\\n}\\n\\n/**\\n * Simple card with no accordion,   for when there are only primary columns\\n */\\nfunction SimpleCard({\\n  row,\\n  columns,\\n  index,\\n  rowKey,\\n  isFirst = false,\\n}: {\\n  row: DataTableRowData;\\n  columns: Column[];\\n  index: number;\\n  rowKey: string;\\n  isFirst?: boolean;\\n}) {\\n  const { locale } = useDataTable();\\n  const primaryColumn = columns[0];\\n  const otherColumns = columns.slice(1);\\n\\n  const stableRowId = getDataTableRowDomId(rowKey);\\n\\n  const primaryValue = primaryColumn\\n    ? String(row[primaryColumn.key] ?? \\\"\\\")\\n    : \\\"\\\";\\n  const rowLabel = `Row ${index + 1}: ${primaryValue}`;\\n\\n  return (\\n    <div\\n      className={cn(\\\"flex flex-col gap-2 p-4\\\", !isFirst && \\\"border-t\\\")}\\n      role=\\\"listitem\\\"\\n      aria-label={rowLabel}\\n    >\\n      {primaryColumn && (\\n        <div\\n          role=\\\"heading\\\"\\n          aria-level={3}\\n          aria-label={`${primaryColumn.label}: ${row[primaryColumn.key]}`}\\n        >\\n          {renderFormattedValue({\\n            value: row[primaryColumn.key],\\n            column: primaryColumn,\\n            row,\\n            locale,\\n          })}\\n        </div>\\n      )}\\n\\n      {otherColumns.map((col) => (\\n        <div\\n          key={col.key}\\n          className=\\\"flex items-start justify-between gap-4\\\"\\n          role=\\\"group\\\"\\n        >\\n          <span\\n            className=\\\"text-muted-foreground\\\"\\n            id={`row-${stableRowId}-${String(col.key)}-label`}\\n          >\\n            {col.label}:\\n          </span>\\n          <span\\n            className={cn(\\n              \\\"min-w-0 wrap-break-word\\\",\\n              col.align === \\\"right\\\" && \\\"text-right\\\",\\n              col.align === \\\"center\\\" && \\\"text-center\\\",\\n            )}\\n            role=\\\"cell\\\"\\n            aria-labelledby={`row-${stableRowId}-${String(col.key)}-label`}\\n          >\\n            {renderFormattedValue({\\n              value: row[col.key],\\n              column: col,\\n              row,\\n              locale,\\n            })}\\n          </span>\\n        </div>\\n      ))}\\n    </div>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/data-table/formatters.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/data-table/formatters.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport * as React from \\\"react\\\";\\nimport { cn, Badge, Tooltip, TooltipContent, TooltipTrigger } from \\\"./_adapter\\\";\\nimport { resolveSafeNavigationHref } from \\\"../shared/media\\\";\\n\\ntype Tone = \\\"success\\\" | \\\"warning\\\" | \\\"danger\\\" | \\\"info\\\" | \\\"neutral\\\";\\n\\nexport type FormatConfig =\\n  | { kind: \\\"text\\\" }\\n  | {\\n      kind: \\\"number\\\";\\n      decimals?: number;\\n      unit?: string;\\n      compact?: boolean;\\n      showSign?: boolean;\\n    }\\n  | { kind: \\\"currency\\\"; currency: string; decimals?: number }\\n  | {\\n      kind: \\\"percent\\\";\\n      decimals?: number;\\n      showSign?: boolean;\\n      basis?: \\\"fraction\\\" | \\\"unit\\\";\\n    }\\n  | { kind: \\\"date\\\"; dateFormat?: \\\"short\\\" | \\\"long\\\" | \\\"relative\\\" }\\n  | {\\n      kind: \\\"delta\\\";\\n      decimals?: number;\\n      upIsPositive?: boolean;\\n      showSign?: boolean;\\n    }\\n  | {\\n      kind: \\\"status\\\";\\n      statusMap: Record<string, { tone: Tone; label?: string }>;\\n    }\\n  | { kind: \\\"boolean\\\"; labels?: { true: string; false: string } }\\n  | { kind: \\\"link\\\"; hrefKey?: string; external?: boolean }\\n  | { kind: \\\"badge\\\"; colorMap?: Record<string, Tone> }\\n  | { kind: \\\"array\\\"; maxVisible?: number };\\n\\ninterface DeltaValueProps {\\n  value: number;\\n  options?: Extract<FormatConfig, { kind: \\\"delta\\\" }>;\\n  locale?: string;\\n}\\n\\nexport function DeltaValue({ value, options, locale }: DeltaValueProps) {\\n  const decimals = options?.decimals ?? 2;\\n  const upIsPositive = options?.upIsPositive ?? true;\\n  const showSign = options?.showSign ?? true;\\n\\n  const isPositive = value > 0;\\n  const isNegative = value < 0;\\n  const isNeutral = value === 0;\\n\\n  const isGood = upIsPositive ? isPositive : isNegative;\\n  const isBad = upIsPositive ? isNegative : isPositive;\\n\\n  const colorClass = isGood\\n    ? \\\"text-green-700 dark:text-green-500\\\"\\n    : isBad\\n      ? \\\"text-destructive\\\"\\n      : \\\"text-muted-foreground\\\";\\n\\n  const absValue = Math.abs(value);\\n  const formatted = new Intl.NumberFormat(locale, {\\n    minimumFractionDigits: decimals,\\n    maximumFractionDigits: decimals,\\n  }).format(absValue);\\n\\n  const display =\\n    showSign && !isNeutral\\n      ? isNegative\\n        ? `-${formatted}`\\n        : `+${formatted}`\\n      : formatted;\\n\\n  const arrow = isPositive ? \\\"↑\\\" : isNegative ? \\\"↓\\\" : \\\"\\\";\\n\\n  return (\\n    <span className={cn(\\\"tabular-nums\\\", colorClass)}>\\n      {display}\\n      {!isNeutral && <span className=\\\"ml-0.5\\\">{arrow}</span>}\\n    </span>\\n  );\\n}\\n\\ninterface StatusBadgeProps {\\n  value: string;\\n  options?: Extract<FormatConfig, { kind: \\\"status\\\" }>;\\n}\\n\\nexport function StatusBadge({ value, options }: StatusBadgeProps) {\\n  const config = options?.statusMap?.[value] ?? {\\n    tone: \\\"neutral\\\" as Tone,\\n    label: value,\\n  };\\n  const label = config.label ?? value;\\n\\n  const variant =\\n    config.tone === \\\"danger\\\"\\n      ? \\\"destructive\\\"\\n      : config.tone === \\\"neutral\\\"\\n        ? \\\"outline\\\"\\n        : \\\"secondary\\\";\\n\\n  return (\\n    <Badge\\n      variant={variant}\\n      className={cn(\\n        \\\"border\\\",\\n        config.tone === \\\"warning\\\" &&\\n          \\\"bg-amber-100 text-amber-700 dark:bg-amber-950 dark:text-amber-100\\\",\\n        config.tone === \\\"success\\\" &&\\n          \\\"bg-green-100 text-green-700 dark:bg-green-950 dark:text-green-100\\\",\\n        config.tone === \\\"info\\\" &&\\n          \\\"bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-100\\\",\\n        config.tone === \\\"danger\\\" &&\\n          \\\"bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-100\\\",\\n      )}\\n    >\\n      {label}\\n    </Badge>\\n  );\\n}\\n\\ninterface CurrencyValueProps {\\n  value: number;\\n  options?: Extract<FormatConfig, { kind: \\\"currency\\\" }>;\\n  locale?: string;\\n}\\n\\nexport function CurrencyValue({ value, options, locale }: CurrencyValueProps) {\\n  const currency = options?.currency ?? \\\"USD\\\";\\n  const decimals = options?.decimals ?? 2;\\n\\n  const formatted = new Intl.NumberFormat(locale, {\\n    style: \\\"currency\\\",\\n    currency,\\n    minimumFractionDigits: decimals,\\n    maximumFractionDigits: decimals,\\n  }).format(value);\\n\\n  return <span className=\\\"tabular-nums\\\">{formatted}</span>;\\n}\\n\\ninterface PercentValueProps {\\n  value: number;\\n  options?: Extract<FormatConfig, { kind: \\\"percent\\\" }>;\\n  locale?: string;\\n}\\n\\nexport function PercentValue({ value, options, locale }: PercentValueProps) {\\n  const decimals = options?.decimals ?? 2;\\n  const showSign = options?.showSign ?? false;\\n  const basis = options?.basis ?? \\\"fraction\\\";\\n\\n  const numeric = basis === \\\"fraction\\\" ? value : value / 100;\\n\\n  const formatted = new Intl.NumberFormat(locale, {\\n    style: \\\"percent\\\",\\n    minimumFractionDigits: decimals,\\n    maximumFractionDigits: decimals,\\n    signDisplay: showSign ? \\\"always\\\" : \\\"auto\\\",\\n  }).format(numeric);\\n\\n  return <span className=\\\"tabular-nums\\\">{formatted}</span>;\\n}\\n\\ninterface DateValueProps {\\n  value: string;\\n  options?: Extract<FormatConfig, { kind: \\\"date\\\" }>;\\n  locale?: string;\\n}\\n\\nexport function DateValue({ value, options, locale }: DateValueProps) {\\n  const dateFormat = options?.dateFormat ?? \\\"short\\\";\\n  const date = new Date(value);\\n\\n  if (isNaN(date.getTime())) {\\n    return <span className=\\\"text-muted-foreground\\\">{value}</span>;\\n  }\\n\\n  let formatted: string;\\n\\n  if (dateFormat === \\\"relative\\\") {\\n    formatted = getRelativeTime(date, locale);\\n  } else if (dateFormat === \\\"long\\\") {\\n    formatted = new Intl.DateTimeFormat(locale, {\\n      year: \\\"numeric\\\",\\n      month: \\\"long\\\",\\n      day: \\\"numeric\\\",\\n    }).format(date);\\n  } else {\\n    formatted = new Intl.DateTimeFormat(locale, {\\n      year: \\\"numeric\\\",\\n      month: \\\"short\\\",\\n      day: \\\"numeric\\\",\\n    }).format(date);\\n  }\\n\\n  const title = new Intl.DateTimeFormat(locale, {\\n    year: \\\"numeric\\\",\\n    month: \\\"long\\\",\\n    day: \\\"numeric\\\",\\n    hour: \\\"2-digit\\\",\\n    minute: \\\"2-digit\\\",\\n  }).format(date);\\n\\n  return (\\n    <span className=\\\"tabular-nums\\\" title={title}>\\n      {formatted}\\n    </span>\\n  );\\n}\\n\\nfunction getRelativeTime(date: Date, locale?: string): string {\\n  const now = new Date();\\n  const diffInSeconds = Math.trunc((date.getTime() - now.getTime()) / 1000);\\n  const absDiffInSeconds = Math.abs(diffInSeconds);\\n\\n  if (absDiffInSeconds < 60) return \\\"just now\\\";\\n\\n  const rtf = new Intl.RelativeTimeFormat(locale, { numeric: \\\"auto\\\" });\\n\\n  if (absDiffInSeconds < 3600) {\\n    const mins = Math.trunc(diffInSeconds / 60);\\n    return rtf.format(mins, \\\"minute\\\");\\n  }\\n  if (absDiffInSeconds < 86400) {\\n    const hours = Math.trunc(diffInSeconds / 3600);\\n    return rtf.format(hours, \\\"hour\\\");\\n  }\\n  if (absDiffInSeconds < 604800) {\\n    const days = Math.trunc(diffInSeconds / 86400);\\n    return rtf.format(days, \\\"day\\\");\\n  }\\n\\n  return new Intl.DateTimeFormat(locale, {\\n    year: \\\"numeric\\\",\\n    month: \\\"short\\\",\\n    day: \\\"numeric\\\",\\n  }).format(date);\\n}\\n\\ninterface BooleanValueProps {\\n  value: boolean;\\n  options?: Extract<FormatConfig, { kind: \\\"boolean\\\" }>;\\n}\\n\\nexport function BooleanValue({ value, options }: BooleanValueProps) {\\n  const labels = options?.labels ?? { true: \\\"Yes\\\", false: \\\"No\\\" };\\n  const label = value ? labels.true : labels.false;\\n  const variant = value ? \\\"secondary\\\" : \\\"outline\\\";\\n\\n  return <Badge variant={variant}>{label}</Badge>;\\n}\\n\\ninterface LinkValueProps {\\n  value: string;\\n  options?: Extract<FormatConfig, { kind: \\\"link\\\" }>;\\n  row?: Record<\\n    string,\\n    string | number | boolean | null | (string | number | boolean | null)[]\\n  >;\\n}\\n\\nexport function LinkValue({ value, options, row }: LinkValueProps) {\\n  const rawHref =\\n    options?.hrefKey && row ? String(row[options.hrefKey] ?? \\\"\\\") : value;\\n  const href = resolveSafeNavigationHref(rawHref);\\n  const external = options?.external ?? false;\\n\\n  if (!href) {\\n    return <span>{value}</span>;\\n  }\\n\\n  return (\\n    <a\\n      href={href}\\n      target={external ? \\\"_blank\\\" : undefined}\\n      rel={external ? \\\"noopener noreferrer\\\" : undefined}\\n      className=\\\"text-accent-foreground inline-block max-w-full break-words underline underline-offset-2 hover:opacity-90\\\"\\n      aria-label={external ? `${value} (opens in a new tab)` : undefined}\\n      onClick={(e) => e.stopPropagation()}\\n    >\\n      {value}\\n      {external && (\\n        <span className=\\\"ml-1 inline-block\\\" aria-label=\\\"Opens in new tab\\\">\\n          ↗\\n        </span>\\n      )}\\n    </a>\\n  );\\n}\\n\\ninterface NumberValueProps {\\n  value: number;\\n  options?: Extract<FormatConfig, { kind: \\\"number\\\" }>;\\n  locale?: string;\\n}\\n\\nexport function NumberValue({ value, options, locale }: NumberValueProps) {\\n  const decimals = options?.decimals ?? 0;\\n  const unit = options?.unit ?? \\\"\\\";\\n  const compact = options?.compact ?? false;\\n  const showSign = options?.showSign ?? false;\\n\\n  const formatted = new Intl.NumberFormat(locale, {\\n    minimumFractionDigits: decimals,\\n    maximumFractionDigits: decimals,\\n    notation: compact ? \\\"compact\\\" : \\\"standard\\\",\\n  }).format(value);\\n\\n  const display = showSign && value > 0 ? `+${formatted}` : formatted;\\n\\n  return (\\n    <span className=\\\"tabular-nums\\\">\\n      {display}\\n      {unit}\\n    </span>\\n  );\\n}\\n\\ninterface BadgeValueProps {\\n  value: string;\\n  options?: Extract<FormatConfig, { kind: \\\"badge\\\" }>;\\n}\\n\\nexport function BadgeValue({ value, options }: BadgeValueProps) {\\n  const tone = options?.colorMap?.[value] ?? \\\"neutral\\\";\\n\\n  const variant =\\n    tone === \\\"danger\\\"\\n      ? \\\"destructive\\\"\\n      : tone === \\\"neutral\\\"\\n        ? \\\"outline\\\"\\n        : \\\"secondary\\\";\\n\\n  return (\\n    <Badge\\n      variant={variant}\\n      className={cn(\\n        \\\"border\\\",\\n        tone === \\\"warning\\\" &&\\n          \\\"bg-amber-100 text-amber-700 dark:bg-amber-950 dark:text-amber-100\\\",\\n        tone === \\\"success\\\" &&\\n          \\\"bg-green-100 text-green-700 dark:bg-green-950 dark:text-green-100\\\",\\n        tone === \\\"info\\\" &&\\n          \\\"bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-100\\\",\\n        tone === \\\"danger\\\" &&\\n          \\\"bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-100\\\",\\n      )}\\n    >\\n      {value}\\n    </Badge>\\n  );\\n}\\n\\ninterface ArrayValueProps {\\n  value: (string | number | boolean | null)[] | string;\\n  options?: Extract<FormatConfig, { kind: \\\"array\\\" }>;\\n}\\n\\nexport function ArrayValue({ value, options }: ArrayValueProps) {\\n  const maxVisible = options?.maxVisible ?? 3;\\n  const items: (string | number | boolean | null)[] = Array.isArray(value)\\n    ? value\\n    : typeof value === \\\"string\\\"\\n      ? value.split(\\\",\\\").map((s) => s.trim())\\n      : [];\\n\\n  if (items.length === 0) {\\n    return <span className=\\\"text-muted\\\">—</span>;\\n  }\\n\\n  const visible = items.slice(0, maxVisible);\\n  const remaining = items.length - maxVisible;\\n\\n  const hidden = items.slice(maxVisible);\\n\\n  return (\\n    <span className=\\\"inline-flex flex-wrap items-center gap-1\\\">\\n      {visible.map((item, i) => (\\n        <span\\n          key={i}\\n          className=\\\"bg-muted text-muted-foreground inline-flex items-center rounded-md px-2 py-0.5\\\"\\n        >\\n          {item === null ? \\\"null\\\" : String(item)}\\n        </span>\\n      ))}\\n      {remaining > 0 && (\\n        <Tooltip>\\n          <TooltipTrigger asChild>\\n            <span className=\\\"text-muted-foreground cursor-default\\\">\\n              +{remaining} more\\n            </span>\\n          </TooltipTrigger>\\n          <TooltipContent>\\n            {hidden\\n              .map((item) => (item === null ? \\\"null\\\" : String(item)))\\n              .join(\\\", \\\")}\\n          </TooltipContent>\\n        </Tooltip>\\n      )}\\n    </span>\\n  );\\n}\\n\\ninterface RenderFormattedValueParams {\\n  value:\\n    | string\\n    | number\\n    | boolean\\n    | null\\n    | (string | number | boolean | null)[];\\n  column: { format?: FormatConfig };\\n  row?: Record<\\n    string,\\n    string | number | boolean | null | (string | number | boolean | null)[]\\n  >;\\n  locale?: string;\\n}\\n\\nexport function renderFormattedValue({\\n  value,\\n  column,\\n  row,\\n  locale,\\n}: RenderFormattedValueParams): React.ReactNode {\\n  if (value == null || value === \\\"\\\") {\\n    return <span className=\\\"text-muted\\\">—</span>;\\n  }\\n\\n  const fmt = column.format;\\n\\n  switch (fmt?.kind) {\\n    case \\\"delta\\\":\\n      return <DeltaValue value={Number(value)} options={fmt} locale={locale} />;\\n    case \\\"status\\\":\\n      return <StatusBadge value={String(value)} options={fmt} />;\\n    case \\\"currency\\\":\\n      return (\\n        <CurrencyValue value={Number(value)} options={fmt} locale={locale} />\\n      );\\n    case \\\"percent\\\":\\n      return (\\n        <PercentValue value={Number(value)} options={fmt} locale={locale} />\\n      );\\n    case \\\"date\\\":\\n      return <DateValue value={String(value)} options={fmt} locale={locale} />;\\n    case \\\"boolean\\\":\\n      return <BooleanValue value={Boolean(value)} options={fmt} />;\\n    case \\\"link\\\":\\n      return <LinkValue value={String(value)} options={fmt} row={row} />;\\n    case \\\"number\\\":\\n      return (\\n        <NumberValue value={Number(value)} options={fmt} locale={locale} />\\n      );\\n    case \\\"badge\\\":\\n      return <BadgeValue value={String(value)} options={fmt} />;\\n    case \\\"array\\\":\\n      return (\\n        <ArrayValue\\n          value={Array.isArray(value) ? value : String(value)}\\n          options={fmt}\\n        />\\n      );\\n    case \\\"text\\\":\\n    default:\\n      return String(value);\\n  }\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/data-table/index.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/data-table/index.tsx\",\n      \"content\": \"export { DataTable, useDataTable } from \\\"./data-table\\\";\\n\\nexport { renderFormattedValue } from \\\"./formatters\\\";\\nexport {\\n  NumberValue,\\n  CurrencyValue,\\n  PercentValue,\\n  DeltaValue,\\n  DateValue,\\n  BooleanValue,\\n  LinkValue,\\n  BadgeValue,\\n  StatusBadge,\\n  ArrayValue,\\n} from \\\"./formatters\\\";\\n\\nexport type {\\n  Column,\\n  DataTableProps,\\n  DataTableSerializableProps,\\n  DataTableClientProps,\\n  DataTableRowData,\\n  RowPrimitive,\\n  RowData,\\n  ColumnKey,\\n} from \\\"./types\\\";\\nexport type { FormatConfig } from \\\"./formatters\\\";\\n\\nexport { sortData, parseNumericLike } from \\\"./utilities\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/data-table/README.md\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/data-table/README.md\",\n      \"content\": \"# Data Table\\n\\nImplementation for the \\\"data-table\\\" Tool UI surface.\\n\\n## Files\\n\\n- public exports: components/tool-ui/data-table/index.tsx\\n- serializable schema + parse helpers: components/tool-ui/data-table/schema.ts\\n\\n## Companion assets\\n\\n- Docs page: app/docs/data-table/content.mdx\\n- Preset payload: lib/presets/data-table.ts\\n\\n## Quick check\\n\\nRun this after edits:\\n\\npnpm test\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/data-table/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/data-table/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport {\\n  ToolUIIdSchema,\\n  ToolUIReceiptSchema,\\n  ToolUIRoleSchema,\\n} from \\\"../shared/schema\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\nimport type { Column, DataTableProps, RowData } from \\\"./types\\\";\\n\\nconst AlignEnum = z.enum([\\\"left\\\", \\\"right\\\", \\\"center\\\"]);\\nconst PriorityEnum = z.enum([\\\"primary\\\", \\\"secondary\\\", \\\"tertiary\\\"]);\\n\\nconst formatSchema = z.discriminatedUnion(\\\"kind\\\", [\\n  z.object({ kind: z.literal(\\\"text\\\") }),\\n  z.object({\\n    kind: z.literal(\\\"number\\\"),\\n    decimals: z.number().optional(),\\n    unit: z.string().optional(),\\n    compact: z.boolean().optional(),\\n    showSign: z.boolean().optional(),\\n  }),\\n  z.object({\\n    kind: z.literal(\\\"currency\\\"),\\n    currency: z.string(),\\n    decimals: z.number().optional(),\\n  }),\\n  z.object({\\n    kind: z.literal(\\\"percent\\\"),\\n    decimals: z.number().optional(),\\n    showSign: z.boolean().optional(),\\n    basis: z.enum([\\\"fraction\\\", \\\"unit\\\"]).optional(),\\n  }),\\n  z.object({\\n    kind: z.literal(\\\"date\\\"),\\n    dateFormat: z.enum([\\\"short\\\", \\\"long\\\", \\\"relative\\\"]).optional(),\\n  }),\\n  z.object({\\n    kind: z.literal(\\\"delta\\\"),\\n    decimals: z.number().optional(),\\n    upIsPositive: z.boolean().optional(),\\n    showSign: z.boolean().optional(),\\n  }),\\n  z.object({\\n    kind: z.literal(\\\"status\\\"),\\n    statusMap: z.record(\\n      z.string(),\\n      z.object({\\n        tone: z.enum([\\\"success\\\", \\\"warning\\\", \\\"danger\\\", \\\"info\\\", \\\"neutral\\\"]),\\n        label: z.string().optional(),\\n      }),\\n    ),\\n  }),\\n  z.object({\\n    kind: z.literal(\\\"boolean\\\"),\\n    labels: z\\n      .object({\\n        true: z.string(),\\n        false: z.string(),\\n      })\\n      .optional(),\\n  }),\\n  z.object({\\n    kind: z.literal(\\\"link\\\"),\\n    hrefKey: z.string().optional(),\\n    external: z.boolean().optional(),\\n  }),\\n  z.object({\\n    kind: z.literal(\\\"badge\\\"),\\n    colorMap: z\\n      .record(\\n        z.string(),\\n        z.enum([\\\"success\\\", \\\"warning\\\", \\\"danger\\\", \\\"info\\\", \\\"neutral\\\"]),\\n      )\\n      .optional(),\\n  }),\\n  z.object({\\n    kind: z.literal(\\\"array\\\"),\\n    maxVisible: z.number().optional(),\\n  }),\\n]);\\n\\nexport const serializableColumnSchema = z.object({\\n  key: z.string(),\\n  label: z.string(),\\n  abbr: z.string().optional(),\\n  sortable: z.boolean().optional(),\\n  align: AlignEnum.optional(),\\n  width: z.string().optional(),\\n  truncate: z.boolean().optional(),\\n  priority: PriorityEnum.optional(),\\n  hideOnMobile: z.boolean().optional(),\\n  format: formatSchema.optional(),\\n});\\n\\nconst JsonPrimitiveSchema = z.union([\\n  z.string(),\\n  z.number(),\\n  z.boolean(),\\n  z.null(),\\n]);\\n\\n/**\\n * Schema for serializable row data.\\n *\\n * Supports:\\n * - Primitives: string, number, boolean, null\\n * - Arrays of primitives: string[], number[], boolean[], or mixed primitive arrays\\n *\\n * Does NOT support:\\n * - Functions\\n * - Class instances (Date, Map, Set, etc.)\\n * - Plain objects (use format configs instead)\\n *\\n * @example\\n * Valid row data:\\n * ```json\\n * {\\n *   \\\"name\\\": \\\"Widget\\\",\\n *   \\\"price\\\": 29.99,\\n *   \\\"active\\\": true,\\n *   \\\"tags\\\": [\\\"electronics\\\", \\\"featured\\\"],\\n *   \\\"metrics\\\": [1.2, 3.4, 5.6],\\n *   \\\"flags\\\": [true, false, true],\\n *   \\\"mixed\\\": [\\\"label\\\", 42, true]\\n * }\\n * ```\\n */\\nexport const serializableDataSchema = z.record(\\n  z.string(),\\n  z.union([JsonPrimitiveSchema, z.array(JsonPrimitiveSchema)]),\\n);\\n\\n/**\\n * Zod schema for validating DataTable payloads from LLM tool calls.\\n *\\n * This schema validates the serializable parts of a DataTable:\\n * - id: Unique identifier for this tool UI in the conversation\\n * - columns: Column definitions (keys, labels, formatting, etc.)\\n * - data: Data rows (primitives only - no functions or class instances)\\n * - optional presentation props: rowIdKey, sort/defaultSort, locale, etc.\\n *\\n * Non-serializable props like `onSortChange`, `className`, and sibling action surfaces\\n * must be provided separately in your React component.\\n *\\n * @example\\n * ```ts\\n * const result = SerializableDataTableSchema.safeParse(llmResponse)\\n * if (result.success) {\\n *   // result.data contains validated id, columns, and data\\n * }\\n * ```\\n */\\nexport const SerializableDataTableSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n  columns: z.array(serializableColumnSchema),\\n  data: z.array(serializableDataSchema),\\n  rowIdKey: z.string().optional(),\\n  defaultSort: z\\n    .object({\\n      by: z.string().optional(),\\n      direction: z.enum([\\\"asc\\\", \\\"desc\\\"]).optional(),\\n    })\\n    .optional(),\\n  sort: z\\n    .object({\\n      by: z.string().optional(),\\n      direction: z.enum([\\\"asc\\\", \\\"desc\\\"]).optional(),\\n    })\\n    .optional(),\\n  emptyMessage: z.string().optional(),\\n  maxHeight: z.string().optional(),\\n  locale: z.string().optional(),\\n});\\n\\nconst SerializableDataTableSchemaContract = defineToolUiContract(\\n  \\\"DataTable\\\",\\n  SerializableDataTableSchema,\\n);\\n\\n/**\\n * Type representing the serializable parts of a DataTable payload.\\n *\\n * This type includes only JSON-serializable data that can come from LLM tool calls:\\n * - Column definitions (format configs, alignment, labels, etc.)\\n * - Row data (primitives: strings, numbers, booleans, null, string arrays)\\n *\\n * Excluded from this type:\\n * - Event handlers (`onSortChange`)\\n * - React-specific props (`className`)\\n *\\n * @example\\n * ```ts\\n * const payload: SerializableDataTable = {\\n *   id: \\\"data-table-expenses\\\",\\n *   columns: [\\n *     { key: \\\"name\\\", label: \\\"Name\\\" },\\n *     { key: \\\"price\\\", label: \\\"Price\\\", format: { kind: \\\"currency\\\", currency: \\\"USD\\\" } }\\n *   ],\\n *   data: [\\n *     { name: \\\"Widget\\\", price: 29.99 }\\n *   ]\\n * }\\n * ```\\n */\\nexport type SerializableDataTable = z.infer<typeof SerializableDataTableSchema>;\\n\\n/**\\n * Validates and parses a DataTable payload from unknown data (e.g., LLM tool call result).\\n *\\n * This function:\\n * 1. Validates the input against the `SerializableDataTableSchema`\\n * 2. Throws a descriptive error if validation fails\\n * 3. Returns typed serializable props ready to pass to the `<DataTable>` component\\n *\\n * The returned props are **serializable only** - you must provide client-side props\\n * separately (onSortChange, className).\\n *\\n * @param input - Unknown data to validate (typically from an LLM tool call)\\n * @returns Validated and typed DataTable serializable props (id, columns, data)\\n * @throws Error with validation details if input is invalid\\n *\\n * @example\\n * ```tsx\\n * function MyToolUI({ result }: { result: unknown }) {\\n *   const serializableProps = parseSerializableDataTable(result)\\n *\\n *   return (\\n *     <DataTable\\n *       {...serializableProps}\\n *     />\\n *   )\\n * }\\n * ```\\n */\\nexport function parseSerializableDataTable(\\n  input: unknown,\\n): Pick<\\n  DataTableProps<RowData>,\\n  | \\\"id\\\"\\n  | \\\"role\\\"\\n  | \\\"receipt\\\"\\n  | \\\"columns\\\"\\n  | \\\"data\\\"\\n  | \\\"rowIdKey\\\"\\n  | \\\"defaultSort\\\"\\n  | \\\"sort\\\"\\n  | \\\"emptyMessage\\\"\\n  | \\\"maxHeight\\\"\\n  | \\\"locale\\\"\\n> {\\n  const {\\n    id,\\n    role,\\n    receipt,\\n    columns,\\n    data,\\n    rowIdKey,\\n    defaultSort,\\n    sort,\\n    emptyMessage,\\n    maxHeight,\\n    locale,\\n  } = SerializableDataTableSchemaContract.parse(input);\\n  return {\\n    id,\\n    role,\\n    receipt,\\n    columns: columns as unknown as Column<RowData>[],\\n    data: data as RowData[],\\n    rowIdKey: rowIdKey as keyof RowData | undefined,\\n    defaultSort: defaultSort\\n      ? {\\n          by: defaultSort.by as keyof RowData | undefined,\\n          direction: defaultSort.direction,\\n        }\\n      : undefined,\\n    sort: sort\\n      ? {\\n          by: sort.by as keyof RowData | undefined,\\n          direction: sort.direction,\\n        }\\n      : undefined,\\n    emptyMessage,\\n    maxHeight,\\n    locale,\\n  };\\n}\\n\\nexport function safeParseSerializableDataTable(\\n  input: unknown,\\n): Pick<\\n  DataTableProps<RowData>,\\n  | \\\"id\\\"\\n  | \\\"role\\\"\\n  | \\\"receipt\\\"\\n  | \\\"columns\\\"\\n  | \\\"data\\\"\\n  | \\\"rowIdKey\\\"\\n  | \\\"defaultSort\\\"\\n  | \\\"sort\\\"\\n  | \\\"emptyMessage\\\"\\n  | \\\"maxHeight\\\"\\n  | \\\"locale\\\"\\n> | null {\\n  const res = SerializableDataTableSchemaContract.safeParse(input);\\n  if (!res) return null;\\n  const {\\n    id,\\n    role,\\n    receipt,\\n    columns,\\n    data,\\n    rowIdKey,\\n    defaultSort,\\n    sort,\\n    emptyMessage,\\n    maxHeight,\\n    locale,\\n  } = res;\\n  return {\\n    id,\\n    role,\\n    receipt,\\n    columns: columns as unknown as Column<RowData>[],\\n    data: data as RowData[],\\n    rowIdKey: rowIdKey as keyof RowData | undefined,\\n    defaultSort: defaultSort\\n      ? {\\n          by: defaultSort.by as keyof RowData | undefined,\\n          direction: defaultSort.direction,\\n        }\\n      : undefined,\\n    sort: sort\\n      ? {\\n          by: sort.by as keyof RowData | undefined,\\n          direction: sort.direction,\\n        }\\n      : undefined,\\n    emptyMessage,\\n    maxHeight,\\n    locale,\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/data-table/types.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/data-table/types.ts\",\n      \"content\": \"import type { ToolUIId, ToolUIReceipt, ToolUIRole } from \\\"../shared/schema\\\";\\nimport type { FormatConfig } from \\\"./formatters\\\";\\n\\n/**\\n * JSON primitive type that can be serialized.\\n */\\ntype JsonPrimitive = string | number | boolean | null;\\n\\n/**\\n * Valid row value types for serializable DataTable data.\\n *\\n * Supports:\\n * - Primitives: string, number, boolean, null\\n * - Arrays of primitives: string[], number[], boolean[], or mixed primitive arrays\\n *\\n * For complex data (objects with href/label, etc.), use column format configs\\n * instead of putting objects in row data.\\n *\\n * @example\\n * ```ts\\n * // 👍 Good: Use primitives and primitive arrays\\n * const row = {\\n *   name: \\\"Widget\\\",\\n *   price: 29.99,\\n *   tags: [\\\"electronics\\\", \\\"featured\\\"],\\n *   metrics: [1.2, 3.4, 5.6]\\n * }\\n *\\n * // 🚫 Bad: Don't put objects in row data\\n * const row = {\\n *   link: { href: \\\"/path\\\", label: \\\"Click\\\" }  // Use format: { kind: 'link' } instead\\n * }\\n * ```\\n */\\nexport type RowPrimitive = JsonPrimitive | JsonPrimitive[];\\nexport type DataTableRowData = Record<string, RowPrimitive>;\\nexport type RowData = Record<string, unknown>;\\nexport type ColumnKey<T extends object> = Extract<keyof T, string>;\\n\\nexport type FormatFor<V> = V extends number\\n  ? Extract<FormatConfig, { kind: \\\"number\\\" | \\\"currency\\\" | \\\"percent\\\" | \\\"delta\\\" }>\\n  : V extends boolean\\n    ? Extract<FormatConfig, { kind: \\\"boolean\\\" | \\\"status\\\" | \\\"badge\\\" }>\\n    : V extends (string | number | boolean | null)[]\\n      ? Extract<FormatConfig, { kind: \\\"array\\\" }>\\n      : V extends string\\n        ? Extract<\\n            FormatConfig,\\n            { kind: \\\"text\\\" | \\\"link\\\" | \\\"date\\\" | \\\"badge\\\" | \\\"status\\\" }\\n          >\\n        : Extract<FormatConfig, { kind: \\\"text\\\" }>;\\n\\n/**\\n * Column definition for DataTable\\n *\\n * @remarks\\n * **Important:** Columns are sortable by default (opt-out pattern).\\n * Set `sortable: false` explicitly to disable sorting for specific columns.\\n */\\nexport interface Column<\\n  T extends object = DataTableRowData,\\n  K extends ColumnKey<T> = ColumnKey<T>,\\n> {\\n  /** Unique identifier that maps to a key in the row data */\\n  key: K;\\n  /** Display text for the column header */\\n  label: string;\\n  /** Abbreviated label for narrow viewports */\\n  abbr?: string;\\n  /** Whether column is sortable. Default: true (opt-out pattern) */\\n  sortable?: boolean;\\n  /** Text alignment for column cells */\\n  align?: \\\"left\\\" | \\\"right\\\" | \\\"center\\\";\\n  /** Optional fixed width (CSS value) */\\n  width?: string;\\n  /** Enable text truncation with ellipsis */\\n  truncate?: boolean;\\n  /** Mobile display priority (primary = always visible, secondary = expandable, tertiary = hidden) */\\n  priority?: \\\"primary\\\" | \\\"secondary\\\" | \\\"tertiary\\\";\\n  /** Completely hide column on mobile viewports */\\n  hideOnMobile?: boolean;\\n  /** Formatting configuration for cell values */\\n  format?: FormatFor<T[K]>;\\n}\\n\\n/**\\n * Serializable props that can come from LLM tool calls or be JSON-serialized.\\n *\\n * These props contain only primitive values, arrays, and plain objects -\\n * no functions, class instances, or other non-serializable values.\\n *\\n * @example\\n * ```tsx\\n * const serializableProps: DataTableSerializableProps = {\\n *   columns: [...],\\n *   data: [...],\\n *   rowIdKey: \\\"id\\\",\\n *   defaultSort: { by: \\\"price\\\", direction: \\\"desc\\\" }\\n * }\\n * ```\\n */\\nexport interface DataTableSerializableProps<T extends object = RowData> {\\n  /**\\n   * Unique identifier for this tool UI instance in the conversation.\\n   *\\n   * Used for:\\n   * - Assistant referencing (\\\"the table above\\\")\\n   * - Receipt generation (linking actions to their source)\\n   * - Narration context\\n   *\\n   * Should be stable across re-renders, meaningful, and unique within the conversation.\\n   *\\n   * @example \\\"data-table-expenses-q3\\\", \\\"search-results-repos\\\"\\n   */\\n  id: ToolUIId;\\n  /** Optional surface role metadata (serializable) */\\n  role?: ToolUIRole;\\n  /** Optional receipt metadata for consequential outcomes (serializable) */\\n  receipt?: ToolUIReceipt;\\n  /** Column definitions */\\n  columns: Column<T>[];\\n  /** Row data (primitives only - no functions or class instances) */\\n  data: T[];\\n  /**\\n   * Key in row data to use as unique identifier for React keys\\n   *\\n   * **Strongly recommended:** Always provide this for dynamic data to prevent\\n   * reconciliation issues (focus traps, animation glitches, incorrect state preservation)\\n   * when data reorders. Falls back to array index if omitted (only acceptable for static mock data).\\n   *\\n   * @example rowIdKey=\\\"id\\\" or rowIdKey=\\\"uuid\\\"\\n   */\\n  rowIdKey?: ColumnKey<T>;\\n  /**\\n   * Uncontrolled initial sort state (table manages its own sort state internally)\\n   *\\n   * **Sorting cycle:** Clicking column headers cycles through tri-state:\\n   * 1. none (unsorted) → 2. asc → 3. desc → 4. none (back to unsorted)\\n   *\\n   * @example\\n   * ```tsx\\n   * // Start with descending price sort\\n   * <DataTable defaultSort={{ by: \\\"price\\\", direction: \\\"desc\\\" }} />\\n   * ```\\n   */\\n  defaultSort?: { by?: ColumnKey<T>; direction?: \\\"asc\\\" | \\\"desc\\\" };\\n  /**\\n   * Controlled sort state (use with onSortChange from client props)\\n   *\\n   * When provided, you must also provide `onSortChange` to handle sort updates.\\n   * The table will cycle through: none → asc → desc → none.\\n   *\\n   * @example\\n   * ```tsx\\n   * const [sort, setSort] = useState({ by: \\\"price\\\", direction: \\\"desc\\\" })\\n   * <DataTable sort={sort} onSortChange={setSort} />\\n   * ```\\n   */\\n  sort?: { by?: ColumnKey<T>; direction?: \\\"asc\\\" | \\\"desc\\\" };\\n  /** Empty state message */\\n  emptyMessage?: string;\\n  /** Max table height with vertical scroll (CSS value) */\\n  maxHeight?: string;\\n  /**\\n   * BCP47 locale for formatting and sorting (e.g., 'en-US', 'de-DE', 'ja-JP')\\n   *\\n   * Defaults to 'en-US' to ensure consistent server/client rendering.\\n   * Pass explicit locale for internationalization.\\n   *\\n   * @example\\n   * ```tsx\\n   * <DataTable locale=\\\"de-DE\\\" /> // German formatting\\n   * <DataTable locale=\\\"ja-JP\\\" /> // Japanese formatting\\n   * <DataTable />               // Uses 'en-US' default\\n   * ```\\n   */\\n  locale?: string;\\n}\\n\\n/**\\n * Client-side React-only props that cannot be serialized.\\n *\\n * These props contain functions, component state, or other React-specific values\\n * that must be provided by your React code (not from LLM tool calls).\\n *\\n * @example\\n * ```tsx\\n * const clientProps: DataTableClientProps = {\\n *   className: \\\"my-table\\\",\\n *   onSortChange: (next) => setSort(next),\\n *   // Compose local/decision actions externally via LocalActions/DecisionActions\\n * }\\n * ```\\n */\\nexport interface DataTableClientProps<T extends object = RowData> {\\n  /** Additional CSS classes */\\n  className?: string;\\n  /**\\n   * Sort change handler for controlled mode (required if sort is provided)\\n   *\\n   * **Tri-state cycle behavior:**\\n   * - Click unsorted column: `{ by: \\\"column\\\", direction: \\\"asc\\\" }`\\n   * - Click asc column: `{ by: \\\"column\\\", direction: \\\"desc\\\" }`\\n   * - Click desc column: `{ by: \\\"column\\\", direction: undefined }` (returns to unsorted)\\n   * - Click different column: `{ by: \\\"newColumn\\\", direction: \\\"asc\\\" }`\\n   *\\n   * @example\\n   * ```tsx\\n   * const [sort, setSort] = useState<{ by?: string; direction?: \\\"asc\\\" | \\\"desc\\\" }>({})\\n   *\\n   * <DataTable\\n   *   sort={sort}\\n   *   onSortChange={(next) => {\\n   *     console.log(\\\"Sort changed:\\\", next)\\n   *     setSort(next)\\n   *   }}\\n   * />\\n   * ```\\n   */\\n  onSortChange?: (next: {\\n    by?: ColumnKey<T>;\\n    direction?: \\\"asc\\\" | \\\"desc\\\";\\n  }) => void;\\n}\\n\\n/**\\n * Complete props for the DataTable component.\\n *\\n * Combines serializable props (can come from LLM tool calls) with client-side\\n * React-only props. This separation makes the boundary explicit and prevents\\n * accidental serialization of non-serializable values.\\n *\\n * @see {@link DataTableSerializableProps} for props that can be JSON-serialized\\n * @see {@link DataTableClientProps} for React-only props\\n * @see {@link parseSerializableDataTable} for parsing LLM tool call results\\n *\\n * @example\\n * ```tsx\\n * // From LLM tool call\\n * const serializableProps = parseSerializableDataTable(llmResult)\\n *\\n * // Combine with React-specific props\\n * <DataTable\\n *   {...serializableProps}\\n *   onSortChange={setSort}\\n *   // Render sibling LocalActions / DecisionActions where needed\\n * />\\n * ```\\n */\\nexport interface DataTableProps<T extends object = RowData>\\n  extends DataTableSerializableProps<T>, DataTableClientProps<T> {}\\n\\nexport interface DataTableContextValue<T extends object = RowData> {\\n  columns: Column<T>[];\\n  data: T[];\\n  rowIdKey?: ColumnKey<T>;\\n  sortBy?: ColumnKey<T>;\\n  sortDirection?: \\\"asc\\\" | \\\"desc\\\";\\n  toggleSort?: (key: ColumnKey<T>) => void;\\n  id?: string;\\n  locale?: string;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/data-table/utilities.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/data-table/utilities.ts\",\n      \"content\": \"/**\\n * Sort an array of objects by a key\\n */\\nexport function sortData<T, K extends Extract<keyof T, string>>(\\n  data: T[],\\n  key: K,\\n  direction: \\\"asc\\\" | \\\"desc\\\",\\n  locale?: string,\\n): T[] {\\n  const get = (obj: T, k: K): unknown => (obj as Record<string, unknown>)[k];\\n  const collator = new Intl.Collator(locale, {\\n    numeric: true,\\n    sensitivity: \\\"base\\\",\\n  });\\n  return [...data].sort((a, b) => {\\n    const aVal = get(a, key);\\n    const bVal = get(b, key);\\n\\n    // Handle nulls\\n    if (aVal == null && bVal == null) return 0;\\n    if (aVal == null) return 1;\\n    if (bVal == null) return -1;\\n\\n    // Type-specific comparison\\n    // Numbers\\n    if (typeof aVal === \\\"number\\\" && typeof bVal === \\\"number\\\") {\\n      return direction === \\\"asc\\\" ? aVal - bVal : bVal - aVal;\\n    }\\n    // Dates (Date instances)\\n    if (aVal instanceof Date && bVal instanceof Date) {\\n      const diff = aVal.getTime() - bVal.getTime();\\n      return direction === \\\"asc\\\" ? diff : -diff;\\n    }\\n    // Booleans: false < true\\n    if (typeof aVal === \\\"boolean\\\" && typeof bVal === \\\"boolean\\\") {\\n      const diff = aVal === bVal ? 0 : aVal ? 1 : -1;\\n      return direction === \\\"asc\\\" ? diff : -diff;\\n    }\\n    // Arrays: compare length\\n    if (Array.isArray(aVal) && Array.isArray(bVal)) {\\n      const diff = aVal.length - bVal.length;\\n      return direction === \\\"asc\\\" ? diff : -diff;\\n    }\\n    // Strings that look like numbers -> numeric compare\\n    if (typeof aVal === \\\"string\\\" && typeof bVal === \\\"string\\\") {\\n      const numA = parseNumericLike(aVal);\\n      const numB = parseNumericLike(bVal);\\n      if (numA != null && numB != null) {\\n        const diff = numA - numB;\\n        return direction === \\\"asc\\\" ? diff : -diff;\\n      }\\n      // ISO-like date strings\\n      if (/^\\\\d{4}-\\\\d{2}-\\\\d{2}/.test(aVal) && /^\\\\d{4}-\\\\d{2}-\\\\d{2}/.test(bVal)) {\\n        const da = new Date(aVal).getTime();\\n        const db = new Date(bVal).getTime();\\n        const diff = da - db;\\n        return direction === \\\"asc\\\" ? diff : -diff;\\n      }\\n    }\\n\\n    // Fallback: locale-aware string compare with numeric collation\\n    const aStr = String(aVal);\\n    const bStr = String(bVal);\\n    const comparison = collator.compare(aStr, bStr);\\n    return direction === \\\"asc\\\" ? comparison : -comparison;\\n  });\\n}\\n\\n/**\\n * Return a human-friendly identifier for a row using common keys\\n *\\n * Accepts any JSON-serializable primitive or array of primitives.\\n * Arrays are converted to comma-separated strings.\\n */\\nexport function getRowIdentifier(\\n  row: Record<\\n    string,\\n    string | number | boolean | null | (string | number | boolean | null)[]\\n  >,\\n  identifierKey?: string,\\n): string {\\n  const candidate =\\n    (identifierKey ? row[identifierKey] : undefined) ??\\n    (row as Record<string, unknown>).name ??\\n    (row as Record<string, unknown>).title ??\\n    (row as Record<string, unknown>).id;\\n\\n  if (candidate == null) {\\n    return \\\"\\\";\\n  }\\n\\n  // Handle arrays by joining them\\n  if (Array.isArray(candidate)) {\\n    return candidate.map((v) => (v === null ? \\\"null\\\" : String(v))).join(\\\", \\\");\\n  }\\n\\n  return String(candidate).trim();\\n}\\n\\nfunction stableStringify(value: unknown): string {\\n  if (value == null) return \\\"null\\\";\\n  if (typeof value === \\\"string\\\") return JSON.stringify(value);\\n  if (\\n    typeof value === \\\"number\\\" ||\\n    typeof value === \\\"boolean\\\" ||\\n    typeof value === \\\"bigint\\\"\\n  ) {\\n    return String(value);\\n  }\\n  if (Array.isArray(value)) {\\n    return `[${value.map((item) => stableStringify(item)).join(\\\",\\\")}]`;\\n  }\\n  if (typeof value === \\\"object\\\") {\\n    const entries = Object.entries(value as Record<string, unknown>).sort(\\n      ([a], [b]) => a.localeCompare(b),\\n    );\\n    return `{${entries\\n      .map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`)\\n      .join(\\\",\\\")}}`;\\n  }\\n  return JSON.stringify(String(value));\\n}\\n\\nfunction hashString(value: string): string {\\n  let hash = 5381;\\n  for (let i = 0; i < value.length; i++) {\\n    hash = (hash * 33) ^ value.charCodeAt(i);\\n  }\\n  return (hash >>> 0).toString(36);\\n}\\n\\n/**\\n * Create deterministic, reorder-stable React keys for DataTable rows.\\n *\\n * - Uses `identifierKey` or common identifier fields as the primary base.\\n * - Falls back to stable content fingerprints when no identifier exists.\\n * - Disambiguates duplicates without relying on array index.\\n */\\nexport function createDataTableRowKeys(\\n  rows: Array<Record<string, unknown>>,\\n  identifierKey?: string,\\n): string[] {\\n  const canonicalRows = rows.map((row) => stableStringify(row));\\n\\n  const baseKeys = rows.map((row, index) => {\\n    const identifier = getRowIdentifier(\\n      row as Record<\\n        string,\\n        string | number | boolean | null | (string | number | boolean | null)[]\\n      >,\\n      identifierKey,\\n    );\\n\\n    if (identifier) {\\n      return `id:${identifier}`;\\n    }\\n\\n    return `row:${hashString(canonicalRows[index])}`;\\n  });\\n\\n  const baseCounts = new Map<string, number>();\\n  baseKeys.forEach((key) => {\\n    baseCounts.set(key, (baseCounts.get(key) ?? 0) + 1);\\n  });\\n\\n  const usedKeys = new Map<string, number>();\\n\\n  return rows.map((row, index) => {\\n    const baseKey = baseKeys[index];\\n    if ((baseCounts.get(baseKey) ?? 0) === 1) {\\n      return baseKey;\\n    }\\n\\n    const rowFingerprint = hashString(canonicalRows[index]);\\n    let disambiguatedKey = `${baseKey}::${rowFingerprint}`;\\n\\n    const seenCount = usedKeys.get(disambiguatedKey) ?? 0;\\n    usedKeys.set(disambiguatedKey, seenCount + 1);\\n    if (seenCount > 0) {\\n      disambiguatedKey = `${disambiguatedKey}::d${seenCount + 1}`;\\n    }\\n\\n    return disambiguatedKey;\\n  });\\n}\\n\\nfunction sanitizeDomIdToken(value: string): string {\\n  return encodeURIComponent(value).replace(/%/g, \\\"_\\\");\\n}\\n\\nexport function getDataTableMobileDescriptionId(surfaceId: string): string {\\n  return `${sanitizeDomIdToken(surfaceId)}-mobile-table-description`;\\n}\\n\\n/**\\n * Parse a string that represents a numeric value, handling various formats:\\n * - Currency symbols: $, €, £, ¥, etc.\\n * - Percent symbols: %\\n * - Accounting negatives: (1234) → -1234\\n * - Thousands/decimal separators: 1,234.56 or 1.234,56\\n * - Compact notation: 2.8T (trillion), 1.5M (million), 500K (thousand)\\n * - Byte suffixes: 768B (bytes), 1.5KB, 2GB, 1TB\\n *\\n * Note: Single \\\"B\\\" is disambiguated - integers < 1024 are bytes, otherwise billions.\\n *\\n * @param input - String to parse\\n * @returns Parsed number or null if unparseable\\n *\\n * @example\\n * parseNumericLike(\\\"$1,234.56\\\") // 1234.56\\n * parseNumericLike(\\\"2.8T\\\") // 2800000000000\\n * parseNumericLike(\\\"768B\\\") // 768\\n * parseNumericLike(\\\"50%\\\") // 50\\n * parseNumericLike(\\\"(1234)\\\") // -1234\\n */\\nexport function parseNumericLike(input: string): number | null {\\n  // Normalize whitespace (spaces, NBSPs, thin spaces)\\n  let s = input.replace(/[\\\\u00A0\\\\u202F\\\\s]/g, \\\"\\\").trim();\\n  if (!s) return null;\\n\\n  // Accounting negatives: (1234) -> -1234\\n  s = s.replace(/^\\\\((.*)\\\\)$/g, \\\"-$1\\\");\\n\\n  // Strip common currency and percent symbols\\n  s = s.replace(/[%$€£¥₩₹₽₺₪₫฿₦₴₡₲₵₸]/g, \\\"\\\");\\n\\n  function hasGroupedThousands(value: string, sep: \\\",\\\" | \\\".\\\"): boolean {\\n    const unsigned = value.replace(/^[+-]/, \\\"\\\");\\n    const parts = unsigned.split(sep);\\n    if (parts.length < 2) return false;\\n    if (parts.some((part) => part.length === 0)) return false;\\n    if (!/^\\\\d{1,3}$/.test(parts[0])) return false;\\n    if (parts[0] === \\\"0\\\") return false;\\n    return parts.slice(1).every((part) => /^\\\\d{3}$/.test(part));\\n  }\\n\\n  const lastComma = s.lastIndexOf(\\\",\\\");\\n  const lastDot = s.lastIndexOf(\\\".\\\");\\n  if (lastComma !== -1 && lastDot !== -1) {\\n    // Decide decimal by whichever occurs last\\n    const decimalSep = lastComma > lastDot ? \\\",\\\" : \\\".\\\";\\n    const thousandSep = decimalSep === \\\",\\\" ? \\\".\\\" : \\\",\\\";\\n    s = s.split(thousandSep).join(\\\"\\\");\\n    s = s.replace(decimalSep, \\\".\\\");\\n  } else if (lastComma !== -1) {\\n    // Only comma present\\n    if (hasGroupedThousands(s, \\\",\\\")) {\\n      s = s.replace(/,/g, \\\"\\\");\\n    } else {\\n      const frac = s.length - lastComma - 1;\\n      if (frac >= 1 && frac <= 3) s = s.replace(/,/g, \\\".\\\");\\n      else s = s.replace(/,/g, \\\"\\\");\\n    }\\n  } else if (lastDot !== -1) {\\n    // Only dot present; normalize grouped thousands separators.\\n    if (hasGroupedThousands(s, \\\".\\\")) {\\n      s = s.replace(/\\\\./g, \\\"\\\");\\n    } else if ((s.match(/\\\\./g) || []).length > 1) {\\n      s = s.replace(/\\\\./g, \\\"\\\");\\n    }\\n  }\\n\\n  // Handle compact notation (K, M, B, T, P, G) and byte suffixes (KB, MB, GB, TB, PB)\\n  const compactMatch = s.match(/^([+-]?\\\\d+\\\\.?\\\\d*|\\\\d*\\\\.\\\\d+)([KMBTPG]B?|B)$/i);\\n  if (compactMatch) {\\n    const baseNum = Number(compactMatch[1]);\\n    if (Number.isNaN(baseNum)) return null;\\n\\n    const suffix = compactMatch[2].toUpperCase();\\n\\n    // Disambiguate single \\\"B\\\" (bytes vs billions)\\n    // If whole number < 1024, treat as bytes. Otherwise, billions.\\n    if (suffix === \\\"B\\\") {\\n      const isLikelyBytes = Number.isInteger(baseNum) && baseNum < 1024;\\n      return isLikelyBytes ? baseNum : baseNum * 1e9;\\n    }\\n\\n    const multipliers: Record<string, number> = {\\n      K: 1e3,\\n      KB: 1024, // Kilo: metric vs binary\\n      M: 1e6,\\n      MB: 1024 ** 2, // Mega\\n      G: 1e9,\\n      GB: 1024 ** 3, // Giga\\n      T: 1e12,\\n      TB: 1024 ** 4, // Tera\\n      P: 1e15,\\n      PB: 1024 ** 5, // Peta\\n    };\\n\\n    return baseNum * (multipliers[suffix] ?? 1);\\n  }\\n\\n  if (/^[+-]?(?:\\\\d+\\\\.?\\\\d*|\\\\d*\\\\.\\\\d+)$/.test(s)) {\\n    const n = Number(s);\\n    return Number.isNaN(n) ? null : n;\\n  }\\n  return null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/aspect-ratio.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/aspect-ratio.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nexport const AspectRatioSchema = z\\n  .enum([\\\"auto\\\", \\\"1:1\\\", \\\"4:3\\\", \\\"16:9\\\", \\\"9:16\\\"])\\n  .default(\\\"auto\\\");\\n\\nexport type AspectRatio = z.infer<typeof AspectRatioSchema>;\\n\\nexport const MediaFitSchema = z.enum([\\\"cover\\\", \\\"contain\\\"]).default(\\\"cover\\\");\\n\\nexport type MediaFit = z.infer<typeof MediaFitSchema>;\\n\\nexport const RATIO_CLASS_MAP: Record<AspectRatio, string> = {\\n  auto: \\\"\\\",\\n  \\\"1:1\\\": \\\"aspect-square\\\",\\n  \\\"4:3\\\": \\\"aspect-[4/3]\\\",\\n  \\\"16:9\\\": \\\"aspect-video\\\",\\n  \\\"9:16\\\": \\\"aspect-[9/16]\\\",\\n};\\n\\nexport function getRatioClass(ratio: AspectRatio): string {\\n  return RATIO_CLASS_MAP[ratio];\\n}\\n\\nexport function getFitClass(fit: MediaFit): string {\\n  return fit === \\\"cover\\\" ? \\\"object-cover\\\" : \\\"object-contain\\\";\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/format-utils.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/format-utils.ts\",\n      \"content\": \"/**\\n * Format duration in milliseconds to human-readable string.\\n * @example formatDuration(128000) => \\\"2:08\\\"\\n * @example formatDuration(3661000) => \\\"1:01:01\\\"\\n */\\nexport function formatDuration(durationMs: number): string {\\n  const totalSeconds = Math.round(durationMs / 1000);\\n  const hours = Math.floor(totalSeconds / 3600);\\n  const minutes = Math.floor((totalSeconds % 3600) / 60);\\n  const seconds = totalSeconds % 60;\\n\\n  if (hours > 0) {\\n    return `${hours}:${minutes.toString().padStart(2, \\\"0\\\")}:${seconds\\n      .toString()\\n      .padStart(2, \\\"0\\\")}`;\\n  }\\n  return `${minutes}:${seconds.toString().padStart(2, \\\"0\\\")}`;\\n}\\n\\n/**\\n * Format file size in bytes to human-readable string.\\n * @example formatFileSize(1024) => \\\"1 KB\\\"\\n * @example formatFileSize(1536000) => \\\"1.5 MB\\\"\\n */\\nexport function formatFileSize(bytes: number): string {\\n  if (bytes < 1024) return `${bytes} B`;\\n  const units = [\\\"KB\\\", \\\"MB\\\", \\\"GB\\\"];\\n  let size = bytes / 1024;\\n  let unit = 0;\\n  while (size >= 1024 && unit < units.length - 1) {\\n    size /= 1024;\\n    unit += 1;\\n  }\\n  return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unit]}`;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/index.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/index.ts\",\n      \"content\": \"export {\\n  AspectRatioSchema,\\n  MediaFitSchema,\\n  RATIO_CLASS_MAP,\\n  getRatioClass,\\n  getFitClass,\\n  type AspectRatio,\\n  type MediaFit,\\n} from \\\"./aspect-ratio\\\";\\n\\nexport { OVERLAY_GRADIENT } from \\\"./overlay-gradient\\\";\\n\\nexport { formatDuration, formatFileSize } from \\\"./format-utils\\\";\\n\\nexport { sanitizeHref } from \\\"./sanitize-href\\\";\\nexport {\\n  resolveSafeNavigationHref,\\n  openSafeNavigationHref,\\n} from \\\"./safe-navigation\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/overlay-gradient.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/overlay-gradient.ts\",\n      \"content\": \"/**\\n * Eased gradient for hover overlays on media elements.\\n * Creates a smooth fade from opaque black at top to transparent.\\n *\\n * @see https://larsenwork.com/easing-gradients/\\n */\\nexport const OVERLAY_GRADIENT = `linear-gradient(\\n  to bottom,\\n  hsl(0, 0%, 0%) 0%,\\n  hsla(0, 0%, 0%, 0.987) 8.3%,\\n  hsla(0, 0%, 0%, 0.951) 16.6%,\\n  hsla(0, 0%, 0%, 0.896) 24.6%,\\n  hsla(0, 0%, 0%, 0.825) 32.5%,\\n  hsla(0, 0%, 0%, 0.741) 40.1%,\\n  hsla(0, 0%, 0%, 0.648) 47.6%,\\n  hsla(0, 0%, 0%, 0.55) 54.8%,\\n  hsla(0, 0%, 0%, 0.45) 61.7%,\\n  hsla(0, 0%, 0%, 0.352) 68.3%,\\n  hsla(0, 0%, 0%, 0.259) 74.5%,\\n  hsla(0, 0%, 0%, 0.175) 80.4%,\\n  hsla(0, 0%, 0%, 0.104) 86%,\\n  hsla(0, 0%, 0%, 0.049) 91.1%,\\n  hsla(0, 0%, 0%, 0.013) 95.8%,\\n  hsla(0, 0%, 0%, 0) 100%\\n)` as const;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/safe-navigation.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/safe-navigation.ts\",\n      \"content\": \"import { sanitizeHref } from \\\"./sanitize-href\\\";\\n\\nexport function resolveSafeNavigationHref(\\n  ...candidates: Array<string | null | undefined>\\n): string | undefined {\\n  for (const candidate of candidates) {\\n    const safeHref = sanitizeHref(candidate ?? undefined);\\n    if (safeHref) {\\n      return safeHref;\\n    }\\n  }\\n\\n  return undefined;\\n}\\n\\nexport function openSafeNavigationHref(href: string | undefined): boolean {\\n  if (!href || typeof window === \\\"undefined\\\") {\\n    return false;\\n  }\\n\\n  window.open(href, \\\"_blank\\\", \\\"noopener,noreferrer\\\");\\n  return true;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/sanitize-href.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/sanitize-href.ts\",\n      \"content\": \"/**\\n * Sanitize a URL to ensure it's safe for use in href attributes.\\n * Allows:\\n * - Absolute http(s) URLs\\n * - Relative URLs (/path, ./path, ../path, ?query, #hash)\\n *\\n * @returns The sanitized URL string, or undefined if invalid/unsafe\\n */\\nexport function sanitizeHref(href?: string): string | undefined {\\n  if (!href) return undefined;\\n  const candidate = href.trim();\\n  if (!candidate) return undefined;\\n\\n  if (\\n    candidate.startsWith(\\\"/\\\") ||\\n    candidate.startsWith(\\\"./\\\") ||\\n    candidate.startsWith(\\\"../\\\") ||\\n    candidate.startsWith(\\\"?\\\") ||\\n    candidate.startsWith(\\\"#\\\")\\n  ) {\\n    if (candidate.startsWith(\\\"//\\\")) return undefined;\\n    // eslint-disable-next-line no-control-regex -- intentionally matching control characters\\n    if (/[\\\\u0000-\\\\u001F\\\\u007F]/.test(candidate)) return undefined;\\n    return candidate;\\n  }\\n\\n  try {\\n    const url = new URL(candidate);\\n    if (url.protocol === \\\"http:\\\" || url.protocol === \\\"https:\\\") {\\n      return url.toString();\\n    }\\n  } catch {\\n    return undefined;\\n  }\\n  return undefined;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { ReactNode } from \\\"react\\\";\\n\\n/**\\n * Tool UI conventions:\\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\\n * - Schema: `SerializableXSchema`\\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\\n * - Root attrs: `data-tool-ui-id` + `data-slot`\\n */\\n\\n/**\\n * Schema for tool UI identity.\\n *\\n * Every tool UI should have a unique identifier that:\\n * - Is stable across re-renders\\n * - Is meaningful (not auto-generated)\\n * - Is unique within the conversation\\n *\\n * Format recommendation: `{component-type}-{semantic-identifier}`\\n * Examples: \\\"data-table-expenses-q3\\\", \\\"option-list-deploy-target\\\"\\n */\\nexport const ToolUIIdSchema = z.string().min(1);\\n\\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\\n\\n/**\\n * Primary role of a Tool UI surface in a chat context.\\n */\\nexport const ToolUIRoleSchema = z.enum([\\n  \\\"information\\\",\\n  \\\"decision\\\",\\n  \\\"control\\\",\\n  \\\"state\\\",\\n  \\\"composite\\\",\\n]);\\n\\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\\n\\nexport const ToolUIReceiptOutcomeSchema = z.enum([\\n  \\\"success\\\",\\n  \\\"partial\\\",\\n  \\\"failed\\\",\\n  \\\"cancelled\\\",\\n]);\\n\\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\\n\\n/**\\n * Optional receipt metadata: a durable summary of an outcome.\\n */\\nexport const ToolUIReceiptSchema = z.object({\\n  outcome: ToolUIReceiptOutcomeSchema,\\n  summary: z.string().min(1),\\n  identifiers: z.record(z.string(), z.string()).optional(),\\n  at: z.string().datetime(),\\n});\\n\\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\\n\\n/**\\n * Base schema for Tool UI payloads (id + optional role/receipt).\\n */\\nexport const ToolUISurfaceSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n});\\n\\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\\n\\nexport const ActionSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  /**\\n   * Canonical narration the assistant can use after this action is taken.\\n   *\\n   * Example: \\\"I exported the table as CSV.\\\" / \\\"I opened the link in a new tab.\\\"\\n   */\\n  sentence: z.string().optional(),\\n  confirmLabel: z.string().optional(),\\n  variant: z\\n    .enum([\\\"default\\\", \\\"destructive\\\", \\\"secondary\\\", \\\"ghost\\\", \\\"outline\\\"])\\n    .optional(),\\n  icon: z.custom<ReactNode>().optional(),\\n  loading: z.boolean().optional(),\\n  disabled: z.boolean().optional(),\\n  shortcut: z.string().optional(),\\n});\\n\\nexport type Action = z.infer<typeof ActionSchema>;\\nexport type LocalAction = Action;\\nexport type DecisionAction = Action;\\n\\nexport const DecisionResultSchema = z.object({\\n  kind: z.literal(\\\"decision\\\"),\\n  version: z.literal(1),\\n  decisionId: z.string().min(1),\\n  actionId: z.string().min(1),\\n  actionLabel: z.string().min(1),\\n  at: z.string().datetime(),\\n  payload: z.record(z.string(), z.unknown()).optional(),\\n});\\n\\nexport type DecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n> = Omit<z.infer<typeof DecisionResultSchema>, \\\"payload\\\"> & {\\n  payload?: TPayload;\\n};\\n\\nexport function createDecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n>(args: {\\n  decisionId: string;\\n  action: { id: string; label: string };\\n  payload?: TPayload;\\n}): DecisionResult<TPayload> {\\n  return {\\n    kind: \\\"decision\\\",\\n    version: 1,\\n    decisionId: args.decisionId,\\n    actionId: args.action.id,\\n    actionLabel: args.action.label,\\n    at: new Date().toISOString(),\\n    payload: args.payload,\\n  };\\n}\\n\\nexport const ActionButtonsPropsSchema = z.object({\\n  actions: z.array(ActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n  className: z.string().optional(),\\n});\\n\\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\\n  actions: z.array(SerializableActionSchema),\\n}).omit({ className: true });\\n\\nexport interface ActionsConfig {\\n  items: Action[];\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  confirmTimeout?: number;\\n}\\n\\nexport const SerializableActionsConfigSchema = z.object({\\n  items: z.array(SerializableActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n});\\n\\nexport type SerializableActionsConfig = z.infer<\\n  typeof SerializableActionsConfigSchema\\n>;\\n\\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/geo-map.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"geo-map\",\n  \"type\": \"registry:block\",\n  \"title\": \"Geo Map\",\n  \"description\": \"Display geolocated entities and fleet positions.\",\n  \"dependencies\": [\n    \"leaflet\",\n    \"react-leaflet\",\n    \"supercluster\",\n    \"zod\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/geo-map/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/geo-map/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn      → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n *   Leaflet → map primitives from react-leaflet\\n */\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport {\\n  CircleMarker,\\n  MapContainer,\\n  Marker,\\n  Polyline,\\n  Popup,\\n  TileLayer,\\n  Tooltip,\\n  ZoomControl,\\n  useMap,\\n  useMapEvents,\\n} from \\\"react-leaflet\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/geo-map/geo-map-engine.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/geo-map/geo-map-engine.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport type { Map as LeafletMap } from \\\"leaflet\\\";\\nimport { memo, useCallback, useEffect, useMemo, useRef, useState } from \\\"react\\\";\\nimport Supercluster from \\\"supercluster\\\";\\nimport {\\n  CircleMarker,\\n  MapContainer,\\n  Marker,\\n  Polyline,\\n  TileLayer,\\n  ZoomControl,\\n  useMap,\\n  useMapEvents,\\n} from \\\"./_adapter\\\";\\nimport { createClusterIcon, resolveMarkerIcon } from \\\"./geo-map-icons\\\";\\nimport { GeoMapOverlays } from \\\"./geo-map-overlays\\\";\\nimport type {\\n  GeoMapClustering,\\n  GeoMapFitTarget,\\n  GeoMapMarker,\\n  GeoMapRoute,\\n  GeoMapViewport,\\n} from \\\"./schema\\\";\\n\\nconst TILE_ATTRIBUTION =\\n  '&copy; <a href=\\\"https://www.openstreetmap.org/copyright\\\">OpenStreetMap</a> contributors &copy; <a href=\\\"https://carto.com/attributions\\\">CARTO</a>';\\nconst ROUTE_DEFAULT_COLOR = \\\"var(--primary)\\\";\\nconst ROUTE_DEFAULT_WEIGHT = 3;\\nconst ROUTE_DEFAULT_OPACITY = 0.85;\\nconst EMPTY_ROUTES: GeoMapRoute[] = [];\\n\\nconst CLUSTER_RADIUS_DEFAULT = 60;\\nconst CLUSTER_MAX_ZOOM_DEFAULT = 16;\\nconst CLUSTER_MIN_POINTS_DEFAULT = 2;\\n\\nconst DEFAULT_CENTER: [number, number] = [20, 0];\\nexport const DEFAULT_VIEW_ZOOM = 2;\\nconst SINGLE_LOCATION_ZOOM = 13;\\nconst DEFAULT_VIEWPORT_PADDING = 32;\\n\\ntype LeafletRuntime = Pick<\\n  typeof import(\\\"leaflet\\\"),\\n  \\\"divIcon\\\" | \\\"latLngBounds\\\"\\n>;\\n\\nexport type GeoMapBbox = [\\n  west: number,\\n  south: number,\\n  east: number,\\n  north: number,\\n];\\nexport type GeoMapLatLng = [lat: number, lng: number];\\n\\nexport type GeoMapClusterProperties = {\\n  cluster?: boolean;\\n  cluster_id?: number;\\n  point_count?: number;\\n  markerId?: string;\\n};\\n\\nexport type GeoMapClusterFeature = GeoJSON.Feature<\\n  GeoJSON.Point,\\n  GeoMapClusterProperties\\n>;\\n\\ntype MarkerClusterPointProperties = GeoMapClusterProperties & {\\n  markerId?: string;\\n  marker?: GeoMapMarker;\\n};\\n\\ntype MapViewportState = {\\n  bbox: GeoMapBbox;\\n  zoom: number;\\n};\\n\\nfunction roundCoordinate(value: number): number {\\n  return Math.round(value * 1_000_000) / 1_000_000;\\n}\\n\\nfunction normalizeViewportState(state: MapViewportState): MapViewportState {\\n  return {\\n    bbox: [\\n      roundCoordinate(state.bbox[0]),\\n      roundCoordinate(state.bbox[1]),\\n      roundCoordinate(state.bbox[2]),\\n      roundCoordinate(state.bbox[3]),\\n    ],\\n    zoom: state.zoom,\\n  };\\n}\\n\\nfunction areViewportStatesEqual(\\n  a: MapViewportState | null,\\n  b: MapViewportState,\\n): boolean {\\n  if (!a) {\\n    return false;\\n  }\\n\\n  return (\\n    a.zoom === b.zoom &&\\n    a.bbox[0] === b.bbox[0] &&\\n    a.bbox[1] === b.bbox[1] &&\\n    a.bbox[2] === b.bbox[2] &&\\n    a.bbox[3] === b.bbox[3]\\n  );\\n}\\n\\nfunction serializeFitPoints(points: [number, number][]): string {\\n  return points\\n    .map(([lat, lng]) => `${roundCoordinate(lat)},${roundCoordinate(lng)}`)\\n    .join(\\\"|\\\");\\n}\\n\\nfunction readViewportState(map: LeafletMap): MapViewportState {\\n  const bounds = map.getBounds();\\n  return normalizeViewportState({\\n    bbox: [\\n      bounds.getWest(),\\n      bounds.getSouth(),\\n      bounds.getEast(),\\n      bounds.getNorth(),\\n    ],\\n    zoom: Math.round(map.getZoom()),\\n  });\\n}\\n\\nexport function collectFitPoints(\\n  markers: GeoMapMarker[],\\n  routes: GeoMapRoute[],\\n  target: GeoMapFitTarget,\\n): GeoMapLatLng[] {\\n  const markerPoints =\\n    target === \\\"markers\\\" || target === \\\"all\\\"\\n      ? markers.map((marker) => [marker.lat, marker.lng] as GeoMapLatLng)\\n      : [];\\n\\n  const routePoints =\\n    target === \\\"routes\\\" || target === \\\"all\\\"\\n      ? routes.flatMap((route) =>\\n          route.points.map((point) => [point.lat, point.lng] as GeoMapLatLng),\\n        )\\n      : [];\\n\\n  return [...markerPoints, ...routePoints];\\n}\\n\\nexport function resolveFitPointsWithFallback(\\n  markers: GeoMapMarker[],\\n  routes: GeoMapRoute[],\\n  target: GeoMapFitTarget,\\n): GeoMapLatLng[] {\\n  const selected = collectFitPoints(markers, routes, target);\\n  if (selected.length > 0) {\\n    return selected;\\n  }\\n\\n  if (target !== \\\"markers\\\") {\\n    return collectFitPoints(markers, routes, \\\"markers\\\");\\n  }\\n\\n  return [];\\n}\\n\\nexport function splitDatelineBbox(bbox: GeoMapBbox): GeoMapBbox[] {\\n  const [west, south, east, north] = bbox;\\n\\n  if (west <= east) {\\n    return [bbox];\\n  }\\n\\n  return [\\n    [west, south, 180, north],\\n    [-180, south, east, north],\\n  ];\\n}\\n\\nfunction getClusterFeatureKey(feature: GeoMapClusterFeature): string {\\n  const properties = feature.properties ?? {};\\n\\n  if (properties.cluster && typeof properties.cluster_id === \\\"number\\\") {\\n    return `cluster:${properties.cluster_id}`;\\n  }\\n\\n  if (\\n    typeof properties.markerId === \\\"string\\\" &&\\n    properties.markerId.length > 0\\n  ) {\\n    return `marker:${properties.markerId}`;\\n  }\\n\\n  if (feature.id !== undefined && feature.id !== null) {\\n    return `id:${String(feature.id)}`;\\n  }\\n\\n  const [lng, lat] = feature.geometry.coordinates;\\n  return `point:${lat}:${lng}`;\\n}\\n\\nfunction dedupeClusterFeatures(\\n  features: GeoMapClusterFeature[],\\n): GeoMapClusterFeature[] {\\n  const seen = new Set<string>();\\n  const deduped: GeoMapClusterFeature[] = [];\\n\\n  features.forEach((feature) => {\\n    const key = getClusterFeatureKey(feature);\\n    if (seen.has(key)) {\\n      return;\\n    }\\n\\n    seen.add(key);\\n    deduped.push(feature);\\n  });\\n\\n  return deduped;\\n}\\n\\nexport function getClustersForDatelineAwareBbox(\\n  bbox: GeoMapBbox,\\n  zoom: number,\\n  getClustersForBbox: (\\n    candidateBbox: GeoMapBbox,\\n    zoom: number,\\n  ) => GeoMapClusterFeature[],\\n): GeoMapClusterFeature[] {\\n  const queried = splitDatelineBbox(bbox).flatMap((candidateBbox) =>\\n    getClustersForBbox(candidateBbox, zoom),\\n  );\\n\\n  return dedupeClusterFeatures(queried);\\n}\\n\\nexport function toSafeExpansionZoom(\\n  zoom: number,\\n  options?: { minZoom?: number; maxZoom?: number; fallback?: number },\\n): number {\\n  const minZoom = options?.minZoom ?? 1;\\n  const maxZoom = options?.maxZoom ?? 22;\\n  const fallback = options?.fallback ?? 2;\\n\\n  if (!Number.isFinite(zoom)) {\\n    return fallback;\\n  }\\n\\n  return Math.min(maxZoom, Math.max(minZoom, Math.round(zoom)));\\n}\\n\\nfunction resolveInitialView(\\n  markers: GeoMapMarker[],\\n  routes: GeoMapRoute[],\\n  viewport: GeoMapViewport | undefined,\\n): { center: [number, number]; zoom: number } {\\n  if (viewport?.mode === \\\"center\\\") {\\n    return {\\n      center: [viewport.center.lat, viewport.center.lng],\\n      zoom: viewport.zoom,\\n    };\\n  }\\n\\n  const fitTarget = viewport?.target ?? \\\"all\\\";\\n  const fitPoints = resolveFitPointsWithFallback(markers, routes, fitTarget);\\n\\n  if (fitPoints.length === 1) {\\n    return {\\n      center: [fitPoints[0][0], fitPoints[0][1]],\\n      zoom: viewport?.maxZoom\\n        ? Math.min(SINGLE_LOCATION_ZOOM, viewport.maxZoom)\\n        : SINGLE_LOCATION_ZOOM,\\n    };\\n  }\\n\\n  return { center: DEFAULT_CENTER, zoom: DEFAULT_VIEW_ZOOM };\\n}\\n\\nfunction ViewportController({\\n  markers,\\n  routes,\\n  viewport,\\n  leafletRuntime,\\n}: {\\n  markers: GeoMapMarker[];\\n  routes: GeoMapRoute[];\\n  viewport: GeoMapViewport | undefined;\\n  leafletRuntime: LeafletRuntime;\\n}) {\\n  const map = useMap();\\n  const lastAppliedViewportRef = useRef<string | null>(null);\\n\\n  useEffect(() => {\\n    lastAppliedViewportRef.current = null;\\n  }, [map]);\\n\\n  useEffect(() => {\\n    if (viewport?.mode === \\\"center\\\") {\\n      const viewportKey = `center:${roundCoordinate(viewport.center.lat)}:${roundCoordinate(viewport.center.lng)}:${viewport.zoom}`;\\n      if (lastAppliedViewportRef.current === viewportKey) {\\n        return;\\n      }\\n\\n      lastAppliedViewportRef.current = viewportKey;\\n      map.setView([viewport.center.lat, viewport.center.lng], viewport.zoom);\\n      return;\\n    }\\n\\n    const fitTarget = viewport?.target ?? \\\"all\\\";\\n    const fitPoints = resolveFitPointsWithFallback(markers, routes, fitTarget);\\n    if (fitPoints.length === 0) {\\n      return;\\n    }\\n\\n    const maxZoom = viewport?.maxZoom;\\n    if (fitPoints.length === 1) {\\n      const [lat, lng] = fitPoints[0];\\n      const zoom = maxZoom\\n        ? Math.min(SINGLE_LOCATION_ZOOM, maxZoom)\\n        : SINGLE_LOCATION_ZOOM;\\n      const viewportKey = `fit-single:${roundCoordinate(lat)}:${roundCoordinate(lng)}:${zoom}`;\\n      if (lastAppliedViewportRef.current === viewportKey) {\\n        return;\\n      }\\n\\n      lastAppliedViewportRef.current = viewportKey;\\n      map.setView([lat, lng], zoom);\\n      return;\\n    }\\n\\n    const padding = viewport?.padding ?? DEFAULT_VIEWPORT_PADDING;\\n    const viewportKey = `fit:${fitTarget}:${padding}:${maxZoom ?? \\\"none\\\"}:${serializeFitPoints(fitPoints)}`;\\n    if (lastAppliedViewportRef.current === viewportKey) {\\n      return;\\n    }\\n\\n    lastAppliedViewportRef.current = viewportKey;\\n    const bounds = leafletRuntime.latLngBounds(fitPoints);\\n    map.fitBounds(bounds, {\\n      maxZoom,\\n      padding: [padding, padding],\\n    });\\n  }, [leafletRuntime, map, markers, routes, viewport]);\\n\\n  return null;\\n}\\n\\nfunction MapObserver({\\n  onViewportChange,\\n  onMapReady,\\n}: {\\n  onViewportChange: (state: MapViewportState) => void;\\n  onMapReady: (map: LeafletMap) => void;\\n}) {\\n  const map = useMapEvents({\\n    moveend: () => {\\n      onViewportChange(readViewportState(map));\\n    },\\n    zoomend: () => {\\n      onViewportChange(readViewportState(map));\\n    },\\n  });\\n\\n  useEffect(() => {\\n    onMapReady(map);\\n    onViewportChange(readViewportState(map));\\n  }, [map, onMapReady, onViewportChange]);\\n\\n  return null;\\n}\\n\\nfunction resolveMarkerAriaLabel(marker: GeoMapMarker): string {\\n  if (marker.label && marker.description) {\\n    return `${marker.label}. ${marker.description}`;\\n  }\\n\\n  return (\\n    marker.label ??\\n    marker.description ??\\n    `Marker at ${marker.lat.toFixed(4)}, ${marker.lng.toFixed(4)}`\\n  );\\n}\\n\\nexport const GeoMapEngine = memo(function GeoMapEngine({\\n  id,\\n  markers,\\n  routes,\\n  clustering,\\n  viewport,\\n  showZoomControl,\\n  tileUrl,\\n  mapAriaLabel,\\n  tooltipClassName,\\n  popupClassName,\\n  onMarkerClick,\\n  onRouteClick,\\n  onReadyChange,\\n}: {\\n  id: string;\\n  markers: GeoMapMarker[];\\n  routes?: GeoMapRoute[];\\n  clustering?: GeoMapClustering;\\n  viewport?: GeoMapViewport;\\n  showZoomControl: boolean;\\n  tileUrl: string;\\n  mapAriaLabel: string;\\n  tooltipClassName?: string;\\n  popupClassName?: string;\\n  onMarkerClick?: (marker: GeoMapMarker) => void;\\n  onRouteClick?: (route: GeoMapRoute) => void;\\n  onReadyChange?: (isReady: boolean) => void;\\n}) {\\n  const resolvedRoutes = routes ?? EMPTY_ROUTES;\\n  const [leafletRuntime, setLeafletRuntime] = useState<LeafletRuntime | null>(\\n    null,\\n  );\\n  const [mapInstance, setMapInstance] = useState<LeafletMap | null>(null);\\n  const [viewportState, setViewportState] = useState<MapViewportState | null>(\\n    null,\\n  );\\n\\n  const handleViewportChange = useCallback((nextState: MapViewportState) => {\\n    const normalized = normalizeViewportState(nextState);\\n    setViewportState((previousState) =>\\n      areViewportStatesEqual(previousState, normalized)\\n        ? previousState\\n        : normalized,\\n    );\\n  }, []);\\n\\n  useEffect(() => {\\n    let isActive = true;\\n\\n    void import(\\\"leaflet\\\").then((module) => {\\n      if (!isActive) {\\n        return;\\n      }\\n\\n      setLeafletRuntime({\\n        divIcon: module.divIcon,\\n        latLngBounds: module.latLngBounds,\\n      });\\n    });\\n\\n    return () => {\\n      isActive = false;\\n    };\\n  }, []);\\n\\n  const isReady = leafletRuntime !== null;\\n\\n  useEffect(() => {\\n    onReadyChange?.(isReady);\\n  }, [isReady, onReadyChange]);\\n\\n  useEffect(() => {\\n    if (!mapInstance) {\\n      return;\\n    }\\n\\n    const container = mapInstance.getContainer();\\n    container.setAttribute(\\\"role\\\", \\\"region\\\");\\n    container.setAttribute(\\\"aria-label\\\", mapAriaLabel);\\n  }, [mapAriaLabel, mapInstance]);\\n\\n  useEffect(() => {\\n    if (!mapInstance) {\\n      return;\\n    }\\n\\n    const handleEscape = (event: KeyboardEvent) => {\\n      if (event.key === \\\"Escape\\\") {\\n        mapInstance.closePopup();\\n      }\\n    };\\n\\n    document.addEventListener(\\\"keydown\\\", handleEscape);\\n    return () => {\\n      document.removeEventListener(\\\"keydown\\\", handleEscape);\\n    };\\n  }, [mapInstance]);\\n\\n  const initialView = useMemo(\\n    () => resolveInitialView(markers, resolvedRoutes, viewport),\\n    [markers, resolvedRoutes, viewport],\\n  );\\n\\n  const markerById = useMemo(() => {\\n    const map = new Map<string, GeoMapMarker>();\\n    markers.forEach((marker, index) => {\\n      map.set(marker.id ?? `marker-${index}`, marker);\\n    });\\n    return map;\\n  }, [markers]);\\n\\n  const clusterConfig = useMemo(\\n    () => ({\\n      enabled: clustering?.enabled === true,\\n      radius: clustering?.radius ?? CLUSTER_RADIUS_DEFAULT,\\n      maxZoom: clustering?.maxZoom ?? CLUSTER_MAX_ZOOM_DEFAULT,\\n      minPoints: clustering?.minPoints ?? CLUSTER_MIN_POINTS_DEFAULT,\\n    }),\\n    [clustering],\\n  );\\n\\n  const clusterIndex = useMemo(() => {\\n    if (!clusterConfig.enabled) {\\n      return null;\\n    }\\n\\n    const index = new Supercluster<MarkerClusterPointProperties>({\\n      radius: clusterConfig.radius,\\n      maxZoom: clusterConfig.maxZoom,\\n      minPoints: clusterConfig.minPoints,\\n    });\\n\\n    const points = markers.map((marker, index) => {\\n      const markerId = marker.id ?? `marker-${index}`;\\n      return {\\n        type: \\\"Feature\\\" as const,\\n        id: markerId,\\n        geometry: {\\n          type: \\\"Point\\\" as const,\\n          coordinates: [marker.lng, marker.lat] as [number, number],\\n        },\\n        properties: {\\n          markerId,\\n          marker,\\n        },\\n      };\\n    });\\n\\n    index.load(points);\\n    return index;\\n  }, [\\n    clusterConfig.enabled,\\n    clusterConfig.maxZoom,\\n    clusterConfig.minPoints,\\n    clusterConfig.radius,\\n    markers,\\n  ]);\\n\\n  const clusteredFeatures = useMemo(() => {\\n    if (!clusterConfig.enabled || !clusterIndex || !viewportState) {\\n      return [] as GeoMapClusterFeature[];\\n    }\\n\\n    return getClustersForDatelineAwareBbox(\\n      viewportState.bbox,\\n      viewportState.zoom,\\n      (bbox, zoom) =>\\n        clusterIndex.getClusters(bbox, zoom) as GeoMapClusterFeature[],\\n    );\\n  }, [clusterConfig.enabled, clusterIndex, viewportState]);\\n\\n  const renderMarker = useCallback(\\n    (\\n      marker: GeoMapMarker,\\n      markerKey: string,\\n      markerPositionOverride?: [number, number],\\n    ) => {\\n      const markerPosition: [number, number] = markerPositionOverride ?? [\\n        marker.lat,\\n        marker.lng,\\n      ];\\n      const tooltipMode = marker.tooltip ?? \\\"hover\\\";\\n      const tooltipContent = marker.label ?? marker.description;\\n      const icon = marker.icon;\\n      const markerAriaLabel = resolveMarkerAriaLabel(marker);\\n\\n      if (!leafletRuntime) {\\n        return null;\\n      }\\n\\n      const leafletIcon = resolveMarkerIcon(icon, leafletRuntime);\\n      if (leafletIcon) {\\n        return (\\n          <Marker\\n            key={markerKey}\\n            position={markerPosition}\\n            icon={leafletIcon}\\n            title={markerAriaLabel}\\n            alt={markerAriaLabel}\\n            eventHandlers={{\\n              click: () => onMarkerClick?.(marker),\\n            }}\\n          >\\n            <GeoMapOverlays\\n              tooltipMode={tooltipMode}\\n              tooltipContent={tooltipContent}\\n              label={marker.label}\\n              description={marker.description}\\n              tooltipClassName={tooltipClassName}\\n              popupClassName={popupClassName}\\n            />\\n          </Marker>\\n        );\\n      }\\n\\n      const markerStroke =\\n        icon?.type === \\\"dot\\\"\\n          ? (icon.borderColor ?? \\\"var(--border)\\\")\\n          : \\\"var(--border)\\\";\\n      const markerFill =\\n        icon?.type === \\\"dot\\\"\\n          ? (icon.color ?? \\\"var(--primary)\\\")\\n          : \\\"var(--primary)\\\";\\n      const markerRadius = icon?.type === \\\"dot\\\" ? (icon.radius ?? 7) : 7;\\n\\n      return (\\n        <CircleMarker\\n          key={markerKey}\\n          center={markerPosition}\\n          radius={markerRadius}\\n          pathOptions={{\\n            color: markerStroke,\\n            fillColor: markerFill,\\n            fillOpacity: 0.95,\\n            weight: 2,\\n          }}\\n          eventHandlers={{\\n            click: () => onMarkerClick?.(marker),\\n          }}\\n        >\\n          <GeoMapOverlays\\n            tooltipMode={tooltipMode}\\n            tooltipContent={tooltipContent}\\n            label={marker.label}\\n            description={marker.description}\\n            tooltipClassName={tooltipClassName}\\n            popupClassName={popupClassName}\\n          />\\n        </CircleMarker>\\n      );\\n    },\\n    [leafletRuntime, onMarkerClick, popupClassName, tooltipClassName],\\n  );\\n\\n  if (!leafletRuntime) {\\n    return null;\\n  }\\n\\n  return (\\n    <MapContainer\\n      center={initialView.center}\\n      zoom={initialView.zoom}\\n      zoomControl={false}\\n      className=\\\"h-full w-full\\\"\\n      scrollWheelZoom\\n    >\\n      <TileLayer attribution={TILE_ATTRIBUTION} url={tileUrl} />\\n      {showZoomControl && <ZoomControl position=\\\"topright\\\" />}\\n      <MapObserver\\n        onMapReady={setMapInstance}\\n        onViewportChange={handleViewportChange}\\n      />\\n      <ViewportController\\n        leafletRuntime={leafletRuntime}\\n        markers={markers}\\n        routes={resolvedRoutes}\\n        viewport={viewport}\\n      />\\n\\n      {resolvedRoutes.map((route, routeIndex) => {\\n        const routeKey = route.id ?? `${id}-route-${routeIndex}`;\\n        const positions = route.points.map((point) => [\\n          point.lat,\\n          point.lng,\\n        ]) as [number, number][];\\n        const tooltipMode = route.tooltip ?? \\\"hover\\\";\\n        const tooltipContent = route.label ?? route.description;\\n\\n        return (\\n          <Polyline\\n            key={routeKey}\\n            positions={positions}\\n            pathOptions={{\\n              color: route.color ?? ROUTE_DEFAULT_COLOR,\\n              weight: route.weight ?? ROUTE_DEFAULT_WEIGHT,\\n              opacity: route.opacity ?? ROUTE_DEFAULT_OPACITY,\\n              dashArray: route.dashArray,\\n            }}\\n            eventHandlers={{\\n              click: () => onRouteClick?.(route),\\n            }}\\n          >\\n            <GeoMapOverlays\\n              tooltipMode={tooltipMode}\\n              tooltipContent={tooltipContent}\\n              label={route.label}\\n              description={route.description}\\n              tooltipClassName={tooltipClassName}\\n              popupClassName={popupClassName}\\n            />\\n          </Polyline>\\n        );\\n      })}\\n\\n      {clusterConfig.enabled && clusterIndex && viewportState\\n        ? clusteredFeatures.map((feature, index) => {\\n            const [lng, lat] = feature.geometry.coordinates;\\n            const properties = (feature.properties ??\\n              {}) as MarkerClusterPointProperties;\\n\\n            if (\\n              properties.cluster &&\\n              typeof properties.cluster_id === \\\"number\\\"\\n            ) {\\n              const pointCount = properties.point_count ?? 0;\\n              const clusterId = properties.cluster_id;\\n              const clusterIcon = createClusterIcon(pointCount, leafletRuntime);\\n              const clusterAriaLabel = `Cluster containing ${pointCount} locations`;\\n\\n              return (\\n                <Marker\\n                  key={`cluster-${clusterId}`}\\n                  position={[lat, lng]}\\n                  icon={clusterIcon}\\n                  title={clusterAriaLabel}\\n                  alt={clusterAriaLabel}\\n                  eventHandlers={{\\n                    click: () => {\\n                      if (!mapInstance) {\\n                        return;\\n                      }\\n\\n                      const expansionZoom = toSafeExpansionZoom(\\n                        clusterIndex.getClusterExpansionZoom(clusterId),\\n                        {\\n                          maxZoom: 22,\\n                          fallback:\\n                            (viewportState.zoom ?? DEFAULT_VIEW_ZOOM) + 2,\\n                        },\\n                      );\\n                      mapInstance.flyTo([lat, lng], expansionZoom);\\n                    },\\n                  }}\\n                />\\n              );\\n            }\\n\\n            const marker =\\n              properties.marker ??\\n              markerById.get(properties.markerId ?? `marker-${index}`);\\n            if (!marker) {\\n              return null;\\n            }\\n\\n            const markerKey =\\n              marker.id ?? properties.markerId ?? `${id}-cluster-leaf-${index}`;\\n            return renderMarker(marker, markerKey, [lat, lng]);\\n          })\\n        : markers.map((marker, index) =>\\n            renderMarker(marker, marker.id ?? `${id}-marker-${index}`),\\n          )}\\n    </MapContainer>\\n  );\\n});\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/geo-map/geo-map-icons.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/geo-map/geo-map-icons.ts\",\n      \"content\": \"import type { DivIcon } from \\\"leaflet\\\";\\nimport type { GeoMapMarker } from \\\"./schema\\\";\\n\\ntype LeafletIconRuntime = Pick<typeof import(\\\"leaflet\\\"), \\\"divIcon\\\">;\\n\\nfunction isSafeHttpUrl(value: string | undefined): boolean {\\n  if (!value) {\\n    return false;\\n  }\\n\\n  try {\\n    const parsed = new URL(value);\\n    return parsed.protocol === \\\"http:\\\" || parsed.protocol === \\\"https:\\\";\\n  } catch {\\n    return false;\\n  }\\n}\\n\\nfunction escapeHtml(value: string): string {\\n  return value\\n    .replaceAll(\\\"&\\\", \\\"&amp;\\\")\\n    .replaceAll(\\\"<\\\", \\\"&lt;\\\")\\n    .replaceAll(\\\">\\\", \\\"&gt;\\\")\\n    .replaceAll('\\\"', \\\"&quot;\\\")\\n    .replaceAll(\\\"'\\\", \\\"&#39;\\\");\\n}\\n\\nfunction createEmojiIcon(\\n  icon: Extract<NonNullable<GeoMapMarker[\\\"icon\\\"]>, { type: \\\"emoji\\\" }>,\\n  leafletRuntime: LeafletIconRuntime,\\n): DivIcon {\\n  const size = icon.size ?? 24;\\n  const background = icon.bgColor ?? \\\"var(--card)\\\";\\n  const border = icon.borderColor ?? \\\"var(--border)\\\";\\n\\n  return leafletRuntime.divIcon({\\n    className: \\\"\\\",\\n    html: `<span style=\\\"\\ndisplay:flex;\\nalign-items:center;\\njustify-content:center;\\nwidth:${size}px;\\nheight:${size}px;\\nborder-radius:999px;\\nbackground:${background};\\nborder:1px solid ${border};\\nfont-size:${Math.round(size * 0.62)}px;\\nline-height:1;\\nbox-shadow:0 1px 3px oklch(from var(--foreground) l c h / 0.22);\\n\\\">${escapeHtml(icon.value)}</span>`,\\n    iconSize: [size, size],\\n    iconAnchor: [size / 2, size / 2],\\n    popupAnchor: [0, -Math.round(size / 2)],\\n    tooltipAnchor: [0, -Math.round(size / 2)],\\n  });\\n}\\n\\nfunction createImageIcon(\\n  icon: Extract<NonNullable<GeoMapMarker[\\\"icon\\\"]>, { type: \\\"image\\\" }>,\\n  leafletRuntime: LeafletIconRuntime,\\n): DivIcon {\\n  const width = icon.width ?? 28;\\n  const height = icon.height ?? 28;\\n  const borderRadius = icon.borderRadius ?? Math.min(width, height) / 2;\\n  const border = icon.borderColor ?? \\\"var(--border)\\\";\\n\\n  return leafletRuntime.divIcon({\\n    className: \\\"\\\",\\n    html: `<span style=\\\"\\ndisplay:block;\\nwidth:${width}px;\\nheight:${height}px;\\nborder-radius:${borderRadius}px;\\noverflow:hidden;\\nborder:1px solid ${border};\\nbackground:var(--card);\\nbox-shadow:0 1px 3px oklch(from var(--foreground) l c h / 0.22);\\n\\\"><img src=\\\"${escapeHtml(icon.url)}\\\" alt=\\\"\\\" style=\\\"width:100%;height:100%;object-fit:cover;display:block;\\\" /></span>`,\\n    iconSize: [width, height],\\n    iconAnchor: [width / 2, height / 2],\\n    popupAnchor: [0, -Math.round(height / 2)],\\n    tooltipAnchor: [0, -Math.round(height / 2)],\\n  });\\n}\\n\\nexport function createClusterIcon(\\n  count: number,\\n  leafletRuntime: LeafletIconRuntime,\\n): DivIcon {\\n  const size = count >= 100 ? 42 : count >= 10 ? 38 : 34;\\n  const background = \\\"var(--primary)\\\";\\n  const border = \\\"var(--background)\\\";\\n\\n  return leafletRuntime.divIcon({\\n    className: \\\"\\\",\\n    html: `<span style=\\\"\\ndisplay:flex;\\nalign-items:center;\\njustify-content:center;\\nwidth:${size}px;\\nheight:${size}px;\\nborder-radius:999px;\\nbackground:${background};\\nborder:2px solid ${border};\\ncolor:var(--primary-foreground);\\nfont-size:12px;\\nfont-weight:700;\\nline-height:1;\\nbox-shadow:0 2px 6px oklch(from var(--foreground) l c h / 0.25);\\n\\\">${count}</span>`,\\n    iconSize: [size, size],\\n    iconAnchor: [size / 2, size / 2],\\n    popupAnchor: [0, -Math.round(size / 2)],\\n    tooltipAnchor: [0, -Math.round(size / 2)],\\n  });\\n}\\n\\nexport function resolveMarkerIcon(\\n  icon: GeoMapMarker[\\\"icon\\\"] | undefined,\\n  leafletRuntime: LeafletIconRuntime,\\n): DivIcon | null {\\n  if (icon?.type === \\\"emoji\\\") {\\n    return createEmojiIcon(icon, leafletRuntime);\\n  }\\n\\n  if (icon?.type === \\\"image\\\" && isSafeHttpUrl(icon.url)) {\\n    return createImageIcon(icon, leafletRuntime);\\n  }\\n\\n  return null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/geo-map/geo-map-overlays.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/geo-map/geo-map-overlays.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { useMemo, useState } from \\\"react\\\";\\n\\nimport { Popup, Tooltip, cn } from \\\"./_adapter\\\";\\n\\nfunction GeoMapPopupContent({\\n  label,\\n  description,\\n}: {\\n  label?: string;\\n  description?: string;\\n}) {\\n  return (\\n    <div className=\\\"flex flex-col gap-0.5\\\">\\n      {label && (\\n        <p className=\\\"block text-sm leading-tight font-semibold tracking-tight text-foreground\\\">\\n          {label}\\n        </p>\\n      )}\\n      {description && (\\n        <p className=\\\"block text-xs leading-relaxed text-muted-foreground\\\">\\n          {description}\\n        </p>\\n      )}\\n    </div>\\n  );\\n}\\n\\nfunction GeoMapTooltipContent({ text }: { text: string }) {\\n  return <span className=\\\"block\\\">{text}</span>;\\n}\\n\\nexport function GeoMapOverlays({\\n  tooltipMode,\\n  tooltipContent,\\n  label,\\n  description,\\n  tooltipClassName,\\n  popupClassName,\\n}: {\\n  tooltipMode: \\\"none\\\" | \\\"hover\\\" | \\\"always\\\";\\n  tooltipContent?: string;\\n  label?: string;\\n  description?: string;\\n  tooltipClassName?: string;\\n  popupClassName?: string;\\n}) {\\n  const hasPopup = Boolean(label || description);\\n  const [isPopupOpen, setIsPopupOpen] = useState(false);\\n  const shouldRenderTooltip =\\n    tooltipMode !== \\\"none\\\" && tooltipContent && (!hasPopup || !isPopupOpen);\\n  const popupEventHandlers = useMemo(\\n    () => ({\\n      add: () => setIsPopupOpen(true),\\n      remove: () => setIsPopupOpen(false),\\n    }),\\n    [],\\n  );\\n\\n  return (\\n    <>\\n      {shouldRenderTooltip && (\\n        <Tooltip\\n          direction=\\\"top\\\"\\n          permanent={tooltipMode === \\\"always\\\"}\\n          className={cn(\\\"geo-map-tooltip\\\", tooltipClassName)}\\n        >\\n          <GeoMapTooltipContent text={tooltipContent} />\\n        </Tooltip>\\n      )}\\n      {hasPopup && (\\n        <Popup\\n          className={cn(\\\"geo-map-popup\\\", popupClassName)}\\n          closeButton\\n          closeOnEscapeKey\\n          minWidth={0}\\n          maxWidth={288}\\n          eventHandlers={popupEventHandlers}\\n        >\\n          <GeoMapPopupContent label={label} description={description} />\\n        </Popup>\\n      )}\\n    </>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/geo-map/geo-map-theme.module.css\",\n      \"type\": \"registry:style\",\n      \"target\": \"components/tool-ui/geo-map/geo-map-theme.module.css\",\n      \"content\": \".root[data-slot=\\\"geo-map\\\"] {\\n  --geo-map-canvas-bg: var(--muted);\\n  --geo-map-tooltip-bg: var(--foreground);\\n  --geo-map-tooltip-fg: var(--background);\\n  --geo-map-tooltip-shadow: 0 8px 20px\\n    oklch(from var(--foreground) l c h / 0.18);\\n  --geo-map-tooltip-radius: calc(var(--radius) - 2px);\\n  --geo-map-tooltip-padding: 0.375rem 0.625rem;\\n  --geo-map-tooltip-font-size: 0.75rem;\\n  --geo-map-tooltip-font-weight: 500;\\n  --geo-map-tooltip-line-height: 1.2;\\n  --geo-map-popup-margin-bottom: 12px;\\n  --geo-map-popup-border: var(--border);\\n  --geo-map-popup-radius: calc(var(--radius) + 2px);\\n  --geo-map-popup-bg: oklch(from var(--popover) l c h / 0.96);\\n  --geo-map-popup-fg: var(--popover-foreground);\\n  --geo-map-popup-shadow: 0 10px 30px oklch(from var(--foreground) l c h / 0.12);\\n  --geo-map-popup-blur: 8px;\\n  --geo-map-popup-content-padding: 0.625rem 0.75rem;\\n  --geo-map-popup-max-width: min(80vw, 18rem);\\n  --geo-map-popup-font-family: var(\\n    --font-sans,\\n    ui-sans-serif,\\n    system-ui,\\n    sans-serif\\n  );\\n  --geo-map-zoom-bg: oklch(from var(--background) l c h / 0.78);\\n  --geo-map-zoom-fg: var(--foreground);\\n  --geo-map-zoom-border: var(--border);\\n  --geo-map-zoom-hover-bg: oklch(from var(--accent) l c h / 0.82);\\n  --geo-map-zoom-hover-fg: var(--accent-foreground);\\n  --geo-map-zoom-disabled-bg: oklch(from var(--muted) l c h / 0.72);\\n  --geo-map-zoom-disabled-fg: var(--muted-foreground);\\n  --geo-map-zoom-shadow: 0 1px 2px oklch(from var(--foreground) l c h / 0.08);\\n  --geo-map-zoom-focus-ring: var(--ring);\\n  --geo-map-zoom-radius: 0.5rem;\\n  --geo-map-zoom-size: 2.25rem;\\n  --geo-map-zoom-font-size: 1.125rem;\\n}\\n\\n.root[data-slot=\\\"geo-map\\\"] :global(.leaflet-container) {\\n  background: var(--geo-map-canvas-bg);\\n}\\n\\n.root[data-slot=\\\"geo-map\\\"] :global(.leaflet-control-zoom) {\\n  border: 1px solid var(--geo-map-zoom-border);\\n  box-shadow: var(--geo-map-zoom-shadow);\\n  background: var(--geo-map-zoom-bg);\\n  backdrop-filter: blur(var(--geo-map-popup-blur));\\n  -webkit-backdrop-filter: blur(var(--geo-map-popup-blur));\\n}\\n\\n.root[data-slot=\\\"geo-map\\\"] :global(.leaflet-control-zoom.leaflet-bar) {\\n  border-radius: var(--geo-map-zoom-radius) !important;\\n}\\n\\n.root[data-slot=\\\"geo-map\\\"] :global(.leaflet-control-zoom a) {\\n  display: flex;\\n  align-items: center;\\n  justify-content: center;\\n  width: var(--geo-map-zoom-size);\\n  height: var(--geo-map-zoom-size);\\n  line-height: 1;\\n  text-indent: 0;\\n  border: 0;\\n  background: transparent;\\n  color: var(--geo-map-zoom-fg);\\n  font-size: var(--geo-map-zoom-font-size);\\n  font-weight: 500;\\n  box-shadow: none;\\n  cursor: default;\\n  transition:\\n    background-color 150ms ease,\\n    color 150ms ease,\\n    border-color 150ms ease,\\n    box-shadow 150ms ease,\\n    opacity 150ms ease;\\n  border-radius: 0 !important;\\n}\\n\\n.root[data-slot=\\\"geo-map\\\"] :global(.leaflet-control-zoom a + a) {\\n  border-top: 1px solid var(--geo-map-zoom-border);\\n}\\n\\n.root[data-slot=\\\"geo-map\\\"] :global(.leaflet-control-zoom a:first-child),\\n.root[data-slot=\\\"geo-map\\\"]\\n  :global(.leaflet-touch .leaflet-control-zoom a:first-child),\\n.root[data-slot=\\\"geo-map\\\"]\\n  :global(.leaflet-control-zoom .leaflet-control-zoom-in) {\\n  border-radius: var(--geo-map-zoom-radius) var(--geo-map-zoom-radius) 0 0 !important;\\n}\\n\\n.root[data-slot=\\\"geo-map\\\"] :global(.leaflet-control-zoom a:last-child),\\n.root[data-slot=\\\"geo-map\\\"]\\n  :global(.leaflet-touch .leaflet-control-zoom a:last-child),\\n.root[data-slot=\\\"geo-map\\\"]\\n  :global(.leaflet-control-zoom .leaflet-control-zoom-out) {\\n  border-top: 0;\\n  border-radius: 0 0 var(--geo-map-zoom-radius) var(--geo-map-zoom-radius) !important;\\n}\\n\\n.root[data-slot=\\\"geo-map\\\"] :global(.leaflet-control-zoom a:hover) {\\n  background: var(--geo-map-zoom-hover-bg);\\n  color: var(--geo-map-zoom-hover-fg);\\n}\\n\\n.root[data-slot=\\\"geo-map\\\"] :global(.leaflet-control-zoom a:focus),\\n.root[data-slot=\\\"geo-map\\\"] :global(.leaflet-control-zoom a:focus-visible) {\\n  position: relative;\\n  z-index: 1;\\n  outline: 2px solid var(--geo-map-zoom-focus-ring);\\n  outline-offset: 1px;\\n}\\n\\n.root[data-slot=\\\"geo-map\\\"] :global(.leaflet-control-zoom a.leaflet-disabled),\\n.root[data-slot=\\\"geo-map\\\"]\\n  :global(.leaflet-control-zoom a.leaflet-disabled:hover) {\\n  background: var(--geo-map-zoom-disabled-bg);\\n  color: var(--geo-map-zoom-disabled-fg);\\n  opacity: 0.55;\\n}\\n\\n.root[data-slot=\\\"geo-map\\\"] :global(.leaflet-tooltip.geo-map-tooltip) {\\n  border: 0;\\n  border-radius: var(--geo-map-tooltip-radius);\\n  background: var(--geo-map-tooltip-bg);\\n  color: var(--geo-map-tooltip-fg);\\n  box-shadow: var(--geo-map-tooltip-shadow);\\n  font-size: var(--geo-map-tooltip-font-size);\\n  font-weight: var(--geo-map-tooltip-font-weight);\\n  line-height: var(--geo-map-tooltip-line-height);\\n  padding: var(--geo-map-tooltip-padding);\\n}\\n\\n.root[data-slot=\\\"geo-map\\\"]\\n  :global(.leaflet-tooltip-top.geo-map-tooltip::before) {\\n  border-top-color: var(--geo-map-tooltip-bg);\\n}\\n\\n.root[data-slot=\\\"geo-map\\\"]\\n  :global(.leaflet-tooltip-bottom.geo-map-tooltip::before) {\\n  border-bottom-color: var(--geo-map-tooltip-bg);\\n}\\n\\n.root[data-slot=\\\"geo-map\\\"]\\n  :global(.leaflet-tooltip-left.geo-map-tooltip::before) {\\n  border-left-color: var(--geo-map-tooltip-bg);\\n}\\n\\n.root[data-slot=\\\"geo-map\\\"]\\n  :global(.leaflet-tooltip-right.geo-map-tooltip::before) {\\n  border-right-color: var(--geo-map-tooltip-bg);\\n}\\n\\n.root[data-slot=\\\"geo-map\\\"] :global(.leaflet-popup.geo-map-popup) {\\n  margin-bottom: var(--geo-map-popup-margin-bottom);\\n}\\n\\n.root[data-slot=\\\"geo-map\\\"]\\n  :global(.leaflet-popup.geo-map-popup .leaflet-popup-content-wrapper) {\\n  border: 1px solid var(--geo-map-popup-border);\\n  border-radius: var(--geo-map-popup-radius);\\n  background: var(--geo-map-popup-bg);\\n  color: var(--geo-map-popup-fg);\\n  box-shadow: var(--geo-map-popup-shadow);\\n  backdrop-filter: blur(var(--geo-map-popup-blur));\\n  -webkit-backdrop-filter: blur(var(--geo-map-popup-blur));\\n  padding: 0;\\n}\\n\\n.root[data-slot=\\\"geo-map\\\"]\\n  :global(.leaflet-popup.geo-map-popup .leaflet-popup-content) {\\n  margin: 0;\\n  min-width: 0;\\n  width: max-content;\\n  max-width: var(--geo-map-popup-max-width);\\n  padding: var(--geo-map-popup-content-padding);\\n  font-family: var(--geo-map-popup-font-family);\\n}\\n\\n.root[data-slot=\\\"geo-map\\\"]\\n  :global(.leaflet-popup.geo-map-popup .leaflet-popup-content p) {\\n  margin: 0;\\n}\\n\\n.root[data-slot=\\\"geo-map\\\"]\\n  :global(.leaflet-popup.geo-map-popup .leaflet-popup-tip-container) {\\n  display: none;\\n}\\n\\n.root[data-slot=\\\"geo-map\\\"]\\n  :global(.leaflet-popup.geo-map-popup .leaflet-popup-close-button) {\\n  color: var(--geo-map-popup-fg);\\n  opacity: 0.75;\\n  top: 0.25rem;\\n  right: 0.25rem;\\n  width: 1.5rem;\\n  height: 1.5rem;\\n  font-size: 1rem;\\n  line-height: 1.5rem;\\n  border-radius: calc(var(--radius) - 2px);\\n}\\n\\n.root[data-slot=\\\"geo-map\\\"]\\n  :global(.leaflet-popup.geo-map-popup .leaflet-popup-close-button:hover) {\\n  opacity: 1;\\n  background: oklch(from var(--muted) l c h / 0.65);\\n}\\n\\n.root[data-slot=\\\"geo-map\\\"]\\n  :global(\\n    .leaflet-popup.geo-map-popup .leaflet-popup-close-button:focus-visible\\n  ) {\\n  outline: 2px solid var(--ring);\\n  outline-offset: 1px;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/geo-map/geo-map.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/geo-map/geo-map.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { memo, useEffect, useState } from \\\"react\\\";\\nimport { cn } from \\\"./_adapter\\\";\\nimport { GeoMapEngine } from \\\"./geo-map-engine\\\";\\nimport styles from \\\"./geo-map-theme.module.css\\\";\\nimport type { GeoMapProps, GeoMapStyle } from \\\"./schema\\\";\\n\\nconst LIGHT_TILE_URL =\\n  \\\"https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png\\\";\\nconst DARK_TILE_URL =\\n  \\\"https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png\\\";\\n\\nfunction getSystemTheme(): \\\"light\\\" | \\\"dark\\\" {\\n  if (typeof window === \\\"undefined\\\") return \\\"light\\\";\\n  return window.matchMedia?.(\\\"(prefers-color-scheme: dark)\\\").matches\\n    ? \\\"dark\\\"\\n    : \\\"light\\\";\\n}\\n\\nfunction getDocumentTheme(): \\\"light\\\" | \\\"dark\\\" | null {\\n  if (typeof document === \\\"undefined\\\") return null;\\n\\n  const root = document.documentElement;\\n  const dataTheme = root.getAttribute(\\\"data-theme\\\")?.toLowerCase();\\n  if (dataTheme === \\\"dark\\\") return \\\"dark\\\";\\n  if (dataTheme === \\\"light\\\") return \\\"light\\\";\\n  if (root.classList.contains(\\\"dark\\\")) return \\\"dark\\\";\\n  if (root.classList.contains(\\\"light\\\")) return \\\"light\\\";\\n\\n  return null;\\n}\\n\\nfunction useInheritedTheme(): \\\"light\\\" | \\\"dark\\\" {\\n  const [theme, setTheme] = useState<\\\"light\\\" | \\\"dark\\\">(() => {\\n    return getDocumentTheme() ?? getSystemTheme();\\n  });\\n\\n  useEffect(() => {\\n    if (typeof window === \\\"undefined\\\" || typeof document === \\\"undefined\\\") {\\n      return;\\n    }\\n\\n    const update = () => setTheme(getDocumentTheme() ?? getSystemTheme());\\n\\n    const mql = window.matchMedia?.(\\\"(prefers-color-scheme: dark)\\\");\\n    mql?.addEventListener(\\\"change\\\", update);\\n\\n    const observer = new MutationObserver(update);\\n    observer.observe(document.documentElement, {\\n      attributes: true,\\n      attributeFilter: [\\\"class\\\", \\\"data-theme\\\"],\\n    });\\n\\n    return () => {\\n      mql?.removeEventListener(\\\"change\\\", update);\\n      observer.disconnect();\\n    };\\n  }, []);\\n\\n  return theme;\\n}\\n\\nfunction resolveMapAriaLabel(title?: string, description?: string): string {\\n  if (title && description) {\\n    return `${title}. ${description}`;\\n  }\\n\\n  return title ?? description ?? \\\"Geographic map\\\";\\n}\\n\\nexport const GeoMap = memo(function GeoMap({\\n  id,\\n  role: _role,\\n  receipt: _receipt,\\n  title,\\n  description,\\n  markers,\\n  routes,\\n  clustering,\\n  viewport,\\n  showZoomControl = true,\\n  theme,\\n  className,\\n  style,\\n  tooltipClassName,\\n  popupClassName,\\n  onMarkerClick,\\n  onRouteClick,\\n}: GeoMapProps) {\\n  const inheritedTheme = useInheritedTheme();\\n  const resolvedTheme = theme ?? inheritedTheme;\\n  const [isMapReady, setIsMapReady] = useState(false);\\n  const tileUrl = resolvedTheme === \\\"dark\\\" ? DARK_TILE_URL : LIGHT_TILE_URL;\\n  const mapAriaLabel = resolveMapAriaLabel(title, description);\\n  const resolvedRootStyle: GeoMapStyle = {\\n    \\\"--geo-map-canvas-bg\\\":\\n      resolvedTheme === \\\"dark\\\" ? \\\"var(--background)\\\" : \\\"var(--muted)\\\",\\n    ...style,\\n  };\\n\\n  return (\\n    <div\\n      className={cn(\\\"w-full min-w-80\\\", styles.root, className)}\\n      style={resolvedRootStyle}\\n      data-slot=\\\"geo-map\\\"\\n      data-tool-ui-id={id}\\n    >\\n      <div\\n        className=\\\"bg-muted/20 relative h-[320px] w-full overflow-hidden rounded-lg border\\\"\\n        role=\\\"region\\\"\\n        aria-label={mapAriaLabel}\\n      >\\n        <GeoMapEngine\\n          id={id}\\n          markers={markers}\\n          routes={routes}\\n          clustering={clustering}\\n          viewport={viewport}\\n          showZoomControl={showZoomControl}\\n          tileUrl={tileUrl}\\n          mapAriaLabel={mapAriaLabel}\\n          tooltipClassName={tooltipClassName}\\n          popupClassName={popupClassName}\\n          onMarkerClick={onMarkerClick}\\n          onRouteClick={onRouteClick}\\n          onReadyChange={setIsMapReady}\\n        />\\n\\n        {(title || description) && (\\n          <div\\n            className={cn(\\n              \\\"pointer-events-none absolute top-3 left-3 z-[900]\\\",\\n              \\\"max-w-[min(75%,22rem)] rounded-lg border border-border/70 bg-background/70 px-3 py-2\\\",\\n              \\\"shadow-sm backdrop-blur-md\\\",\\n            )}\\n          >\\n            {title && (\\n              <p className=\\\"text-foreground text-sm leading-tight font-semibold\\\">\\n                {title}\\n              </p>\\n            )}\\n            {description && (\\n              <p className=\\\"text-muted-foreground mt-1 text-xs leading-snug\\\">\\n                {description}\\n              </p>\\n            )}\\n          </div>\\n        )}\\n\\n        {!isMapReady && (\\n          <div\\n            data-slot=\\\"geo-map-loading\\\"\\n            className=\\\"bg-muted/30 text-muted-foreground pointer-events-none absolute inset-0 flex items-center justify-center\\\"\\n          >\\n            <span data-slot=\\\"geo-map-loading-label\\\">Loading map...</span>\\n          </div>\\n        )}\\n      </div>\\n    </div>\\n  );\\n});\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/geo-map/index.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/geo-map/index.tsx\",\n      \"content\": \"export { GeoMap } from \\\"./geo-map\\\";\\nexport {\\n  type GeoMapClustering,\\n  type GeoMapFitTarget,\\n  type GeoMapMarker,\\n  type GeoMapMarkerIcon,\\n  type GeoMapStyle,\\n  type GeoMapRoute,\\n  type GeoMapViewport,\\n  type GeoMapProps,\\n  type GeoMapClientProps,\\n  type SerializableGeoMap,\\n} from \\\"./schema\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/geo-map/README.md\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/geo-map/README.md\",\n      \"content\": \"# Geo Map\\n\\nImplementation for the \\\"geo-map\\\" Tool UI surface.\\n\\n## Files\\n\\n- public exports: components/tool-ui/geo-map/index.tsx\\n- serializable schema + parse helpers: components/tool-ui/geo-map/schema.ts\\n- public facade component: components/tool-ui/geo-map/geo-map.tsx\\n- internal Leaflet engine: components/tool-ui/geo-map/geo-map-engine.tsx\\n- colocated Leaflet shell theme styles: components/tool-ui/geo-map/geo-map-theme.module.css\\n- icon construction helpers: components/tool-ui/geo-map/geo-map-icons.ts\\n- popup/tooltip overlay renderer: components/tool-ui/geo-map/geo-map-overlays.tsx\\n\\n## Companion assets\\n\\n- Docs page: app/docs/geo-map/content.mdx\\n- Preset payload: lib/presets/geo-map.ts\\n\\n## Quick check\\n\\nRun this after edits:\\n\\npnpm test\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/geo-map/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/geo-map/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { CSSProperties } from \\\"react\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\nimport {\\n  ToolUIIdSchema,\\n  ToolUIReceiptSchema,\\n  ToolUIRoleSchema,\\n} from \\\"../shared/schema\\\";\\n\\nconst LatitudeSchema = z.number().finite().min(-90).max(90);\\nconst LongitudeSchema = z.number().finite().min(-180).max(180);\\nconst HttpUrlSchema = z\\n  .string()\\n  .url()\\n  .refine((value) => /^https?:\\\\/\\\\//i.test(value), {\\n    message: \\\"Expected an http or https URL.\\\",\\n  });\\n\\nconst GeoMapMarkerIconDotSchema = z.object({\\n  type: z.literal(\\\"dot\\\"),\\n  color: z.string().optional(),\\n  borderColor: z.string().optional(),\\n  radius: z.number().min(3).max(16).optional(),\\n});\\n\\nconst GeoMapMarkerIconEmojiSchema = z.object({\\n  type: z.literal(\\\"emoji\\\"),\\n  value: z.string().min(1),\\n  size: z.number().min(16).max(40).optional(),\\n  bgColor: z.string().optional(),\\n  borderColor: z.string().optional(),\\n});\\n\\nconst GeoMapMarkerIconImageSchema = z.object({\\n  type: z.literal(\\\"image\\\"),\\n  url: HttpUrlSchema,\\n  width: z.number().min(16).max(64).optional(),\\n  height: z.number().min(16).max(64).optional(),\\n  borderRadius: z.number().min(0).max(999).optional(),\\n  borderColor: z.string().optional(),\\n});\\n\\nexport const GeoMapMarkerIconSchema = z.union([\\n  GeoMapMarkerIconDotSchema,\\n  GeoMapMarkerIconEmojiSchema,\\n  GeoMapMarkerIconImageSchema,\\n]);\\n\\nexport type GeoMapMarkerIcon = z.infer<typeof GeoMapMarkerIconSchema>;\\n\\nexport const GeoMapMarkerSchema = z.object({\\n  id: z.string().min(1).optional(),\\n  lat: LatitudeSchema,\\n  lng: LongitudeSchema,\\n  label: z.string().optional(),\\n  description: z.string().optional(),\\n  tooltip: z.enum([\\\"none\\\", \\\"hover\\\", \\\"always\\\"]).optional(),\\n  icon: GeoMapMarkerIconSchema.optional(),\\n});\\n\\nexport type GeoMapMarker = z.infer<typeof GeoMapMarkerSchema>;\\n\\nexport const GeoMapRoutePointSchema = z.object({\\n  lat: LatitudeSchema,\\n  lng: LongitudeSchema,\\n});\\n\\nexport const GeoMapRouteSchema = z.object({\\n  id: z.string().min(1).optional(),\\n  points: z.array(GeoMapRoutePointSchema).min(2),\\n  label: z.string().optional(),\\n  description: z.string().optional(),\\n  tooltip: z.enum([\\\"none\\\", \\\"hover\\\", \\\"always\\\"]).optional(),\\n  color: z.string().optional(),\\n  weight: z.number().min(1).max(12).optional(),\\n  opacity: z.number().min(0).max(1).optional(),\\n  dashArray: z.string().optional(),\\n});\\n\\nexport type GeoMapRoute = z.infer<typeof GeoMapRouteSchema>;\\n\\nexport const GeoMapClusteringSchema = z.object({\\n  enabled: z.boolean().optional(),\\n  radius: z.number().min(20).max(120).optional(),\\n  maxZoom: z.number().min(1).max(22).optional(),\\n  minPoints: z.number().min(2).max(20).optional(),\\n});\\n\\nexport type GeoMapClustering = z.infer<typeof GeoMapClusteringSchema>;\\n\\nexport const GeoMapFitTargetSchema = z.enum([\\\"markers\\\", \\\"routes\\\", \\\"all\\\"]);\\nexport type GeoMapFitTarget = z.infer<typeof GeoMapFitTargetSchema>;\\n\\nconst GeoMapFitViewportSchema = z.object({\\n  mode: z.literal(\\\"fit\\\"),\\n  padding: z.number().nonnegative().optional(),\\n  maxZoom: z.number().min(1).max(22).optional(),\\n  target: GeoMapFitTargetSchema.optional(),\\n});\\n\\nconst GeoMapCenterViewportSchema = z.object({\\n  mode: z.literal(\\\"center\\\"),\\n  center: z.object({\\n    lat: LatitudeSchema,\\n    lng: LongitudeSchema,\\n  }),\\n  zoom: z.number().min(1).max(22),\\n});\\n\\nexport const GeoMapViewportSchema = z.union([\\n  GeoMapFitViewportSchema,\\n  GeoMapCenterViewportSchema,\\n]);\\n\\nexport type GeoMapViewport = z.infer<typeof GeoMapViewportSchema>;\\n\\nexport const GeoMapPropsSchema = z\\n  .object({\\n    id: ToolUIIdSchema,\\n    role: ToolUIRoleSchema.optional(),\\n    receipt: ToolUIReceiptSchema.optional(),\\n    title: z.string().optional(),\\n    description: z.string().optional(),\\n    markers: z.array(GeoMapMarkerSchema).min(1),\\n    routes: z.array(GeoMapRouteSchema).optional(),\\n    clustering: GeoMapClusteringSchema.optional(),\\n    viewport: GeoMapViewportSchema.optional(),\\n    showZoomControl: z.boolean().optional(),\\n    theme: z.enum([\\\"light\\\", \\\"dark\\\"]).optional(),\\n  })\\n  .superRefine((value, ctx) => {\\n    const seenMarkerIds = new Set<string>();\\n\\n    value.markers.forEach((marker, index) => {\\n      if (!marker.id) {\\n        return;\\n      }\\n\\n      if (seenMarkerIds.has(marker.id)) {\\n        ctx.addIssue({\\n          code: \\\"custom\\\",\\n          path: [\\\"markers\\\", index, \\\"id\\\"],\\n          message: `Duplicate marker id \\\"${marker.id}\\\".`,\\n        });\\n        return;\\n      }\\n\\n      seenMarkerIds.add(marker.id);\\n    });\\n\\n    const seenRouteIds = new Set<string>();\\n    value.routes?.forEach((route, index) => {\\n      if (!route.id) {\\n        return;\\n      }\\n\\n      if (seenRouteIds.has(route.id)) {\\n        ctx.addIssue({\\n          code: \\\"custom\\\",\\n          path: [\\\"routes\\\", index, \\\"id\\\"],\\n          message: `Duplicate route id \\\"${route.id}\\\".`,\\n        });\\n        return;\\n      }\\n\\n      seenRouteIds.add(route.id);\\n    });\\n  });\\n\\nexport type GeoMapStyle = CSSProperties &\\n  Partial<Record<`--${string}`, string | number>>;\\n\\nexport type GeoMapClientProps = {\\n  className?: string;\\n  style?: GeoMapStyle;\\n  tooltipClassName?: string;\\n  popupClassName?: string;\\n  onMarkerClick?: (marker: GeoMapMarker) => void;\\n  onRouteClick?: (route: GeoMapRoute) => void;\\n};\\n\\nexport type GeoMapProps = z.infer<typeof GeoMapPropsSchema> & GeoMapClientProps;\\n\\nexport const SerializableGeoMapSchema = GeoMapPropsSchema;\\n\\nexport type SerializableGeoMap = z.infer<typeof SerializableGeoMapSchema>;\\n\\nconst SerializableGeoMapSchemaContract = defineToolUiContract(\\n  \\\"GeoMap\\\",\\n  SerializableGeoMapSchema,\\n);\\n\\nexport const parseSerializableGeoMap: (input: unknown) => SerializableGeoMap =\\n  SerializableGeoMapSchemaContract.parse;\\n\\nexport const safeParseSerializableGeoMap: (\\n  input: unknown,\\n) => SerializableGeoMap | null = SerializableGeoMapSchemaContract.safeParse;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { ReactNode } from \\\"react\\\";\\n\\n/**\\n * Tool UI conventions:\\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\\n * - Schema: `SerializableXSchema`\\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\\n * - Root attrs: `data-tool-ui-id` + `data-slot`\\n */\\n\\n/**\\n * Schema for tool UI identity.\\n *\\n * Every tool UI should have a unique identifier that:\\n * - Is stable across re-renders\\n * - Is meaningful (not auto-generated)\\n * - Is unique within the conversation\\n *\\n * Format recommendation: `{component-type}-{semantic-identifier}`\\n * Examples: \\\"data-table-expenses-q3\\\", \\\"option-list-deploy-target\\\"\\n */\\nexport const ToolUIIdSchema = z.string().min(1);\\n\\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\\n\\n/**\\n * Primary role of a Tool UI surface in a chat context.\\n */\\nexport const ToolUIRoleSchema = z.enum([\\n  \\\"information\\\",\\n  \\\"decision\\\",\\n  \\\"control\\\",\\n  \\\"state\\\",\\n  \\\"composite\\\",\\n]);\\n\\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\\n\\nexport const ToolUIReceiptOutcomeSchema = z.enum([\\n  \\\"success\\\",\\n  \\\"partial\\\",\\n  \\\"failed\\\",\\n  \\\"cancelled\\\",\\n]);\\n\\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\\n\\n/**\\n * Optional receipt metadata: a durable summary of an outcome.\\n */\\nexport const ToolUIReceiptSchema = z.object({\\n  outcome: ToolUIReceiptOutcomeSchema,\\n  summary: z.string().min(1),\\n  identifiers: z.record(z.string(), z.string()).optional(),\\n  at: z.string().datetime(),\\n});\\n\\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\\n\\n/**\\n * Base schema for Tool UI payloads (id + optional role/receipt).\\n */\\nexport const ToolUISurfaceSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n});\\n\\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\\n\\nexport const ActionSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  /**\\n   * Canonical narration the assistant can use after this action is taken.\\n   *\\n   * Example: \\\"I exported the table as CSV.\\\" / \\\"I opened the link in a new tab.\\\"\\n   */\\n  sentence: z.string().optional(),\\n  confirmLabel: z.string().optional(),\\n  variant: z\\n    .enum([\\\"default\\\", \\\"destructive\\\", \\\"secondary\\\", \\\"ghost\\\", \\\"outline\\\"])\\n    .optional(),\\n  icon: z.custom<ReactNode>().optional(),\\n  loading: z.boolean().optional(),\\n  disabled: z.boolean().optional(),\\n  shortcut: z.string().optional(),\\n});\\n\\nexport type Action = z.infer<typeof ActionSchema>;\\nexport type LocalAction = Action;\\nexport type DecisionAction = Action;\\n\\nexport const DecisionResultSchema = z.object({\\n  kind: z.literal(\\\"decision\\\"),\\n  version: z.literal(1),\\n  decisionId: z.string().min(1),\\n  actionId: z.string().min(1),\\n  actionLabel: z.string().min(1),\\n  at: z.string().datetime(),\\n  payload: z.record(z.string(), z.unknown()).optional(),\\n});\\n\\nexport type DecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n> = Omit<z.infer<typeof DecisionResultSchema>, \\\"payload\\\"> & {\\n  payload?: TPayload;\\n};\\n\\nexport function createDecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n>(args: {\\n  decisionId: string;\\n  action: { id: string; label: string };\\n  payload?: TPayload;\\n}): DecisionResult<TPayload> {\\n  return {\\n    kind: \\\"decision\\\",\\n    version: 1,\\n    decisionId: args.decisionId,\\n    actionId: args.action.id,\\n    actionLabel: args.action.label,\\n    at: new Date().toISOString(),\\n    payload: args.payload,\\n  };\\n}\\n\\nexport const ActionButtonsPropsSchema = z.object({\\n  actions: z.array(ActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n  className: z.string().optional(),\\n});\\n\\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\\n  actions: z.array(SerializableActionSchema),\\n}).omit({ className: true });\\n\\nexport interface ActionsConfig {\\n  items: Action[];\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  confirmTimeout?: number;\\n}\\n\\nexport const SerializableActionsConfigSchema = z.object({\\n  items: z.array(SerializableActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n});\\n\\nexport type SerializableActionsConfig = z.infer<\\n  typeof SerializableActionsConfigSchema\\n>;\\n\\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/image-gallery.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"image-gallery\",\n  \"type\": \"registry:block\",\n  \"title\": \"Image Gallery\",\n  \"description\": \"Grid layout for browsing image collections.\",\n  \"dependencies\": [\n    \"lucide-react\",\n    \"zod\"\n  ],\n  \"registryDependencies\": [\n    \"button\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/image-gallery/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/image-gallery/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn     → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n *   Button → shadcn/ui Button\\n */\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport { Button } from \\\"@/components/ui/button\\\";\\nexport { ChevronLeft, ChevronRight, X, ImageOff } from \\\"lucide-react\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/image-gallery/context.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/image-gallery/context.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport {\\n  createContext,\\n  use,\\n  useState,\\n  useCallback,\\n  useMemo,\\n  useRef,\\n} from \\\"react\\\";\\nimport { flushSync } from \\\"react-dom\\\";\\nimport type { ImageGalleryItem } from \\\"./schema\\\";\\n\\nconst VIEW_TRANSITION_NAME = \\\"active-gallery-image\\\";\\n\\ninterface ImageGalleryContextValue {\\n  images: ImageGalleryItem[];\\n  activeIndex: number | null;\\n  openLightbox: (index: number) => void;\\n  closeLightbox: () => void;\\n  registerImage: (id: string, element: HTMLElement | null) => void;\\n  lightboxContentRef: React.RefObject<HTMLDivElement | null>;\\n  setDialogRef: (element: HTMLDialogElement | null) => void;\\n}\\n\\nconst ImageGalleryContext = createContext<ImageGalleryContextValue | null>(\\n  null,\\n);\\n\\nexport function useImageGallery(): ImageGalleryContextValue {\\n  const context = use(ImageGalleryContext);\\n  if (!context) {\\n    throw new Error(\\\"useImageGallery must be used within ImageGalleryProvider\\\");\\n  }\\n  return context;\\n}\\n\\nfunction supportsViewTransitions(): boolean {\\n  return (\\n    typeof document !== \\\"undefined\\\" &&\\n    \\\"startViewTransition\\\" in document &&\\n    typeof window !== \\\"undefined\\\" &&\\n    !window.matchMedia?.(\\\"(prefers-reduced-motion: reduce)\\\")?.matches\\n  );\\n}\\n\\nfunction withViewTransition(\\n  element: HTMLElement,\\n  domUpdate: () => void,\\n  onFinished?: () => void,\\n): void {\\n  if (!supportsViewTransitions()) {\\n    domUpdate();\\n    onFinished?.();\\n    return;\\n  }\\n\\n  element.style.viewTransitionName = VIEW_TRANSITION_NAME;\\n\\n  const transition = document.startViewTransition(() => domUpdate());\\n\\n  transition.finished.finally(() => {\\n    element.style.removeProperty(\\\"view-transition-name\\\");\\n    onFinished?.();\\n  });\\n}\\n\\ninterface ImageGalleryProviderProps {\\n  images: ImageGalleryItem[];\\n  children: React.ReactNode;\\n}\\n\\nexport function ImageGalleryProvider({\\n  images,\\n  children,\\n}: ImageGalleryProviderProps) {\\n  const [activeIndex, setActiveIndex] = useState<number | null>(null);\\n\\n  const imageElementsRef = useRef<Map<string, HTMLElement>>(new Map());\\n  const lightboxContentRef = useRef<HTMLDivElement>(null);\\n  const dialogRef = useRef<HTMLDialogElement | null>(null);\\n  const originalParentRef = useRef<HTMLElement | null>(null);\\n\\n  const registerImage = useCallback(\\n    (id: string, element: HTMLElement | null) => {\\n      if (element) {\\n        imageElementsRef.current.set(id, element);\\n      } else {\\n        imageElementsRef.current.delete(id);\\n      }\\n    },\\n    [],\\n  );\\n\\n  const setDialogRef = useCallback((element: HTMLDialogElement | null) => {\\n    dialogRef.current = element;\\n  }, []);\\n\\n  const openLightbox = useCallback(\\n    (index: number) => {\\n      const image = images[index];\\n      if (!image) return;\\n\\n      const imageElement = imageElementsRef.current.get(image.id);\\n      const container = lightboxContentRef.current;\\n      const dialog = dialogRef.current;\\n\\n      if (!imageElement || !container || !dialog) {\\n        setActiveIndex(index);\\n        dialog?.showModal();\\n        return;\\n      }\\n\\n      originalParentRef.current = imageElement.parentElement;\\n\\n      withViewTransition(imageElement, () => {\\n        container.appendChild(imageElement);\\n        flushSync(() => setActiveIndex(index));\\n        dialog.showModal();\\n      });\\n    },\\n    [images],\\n  );\\n\\n  const closeLightbox = useCallback(() => {\\n    if (activeIndex === null) return;\\n\\n    const image = images[activeIndex];\\n    const dialog = dialogRef.current;\\n\\n    if (!image) {\\n      setActiveIndex(null);\\n      dialog?.close();\\n      return;\\n    }\\n\\n    const imageElement = imageElementsRef.current.get(image.id);\\n    const originalParent = originalParentRef.current;\\n\\n    if (!imageElement || !originalParent) {\\n      setActiveIndex(null);\\n      dialog?.close();\\n      return;\\n    }\\n\\n    withViewTransition(\\n      imageElement,\\n      () => {\\n        originalParent.appendChild(imageElement);\\n        flushSync(() => setActiveIndex(null));\\n        dialog?.close();\\n      },\\n      () => {\\n        originalParentRef.current = null;\\n      },\\n    );\\n  }, [activeIndex, images]);\\n\\n  const value = useMemo<ImageGalleryContextValue>(\\n    () => ({\\n      images,\\n      activeIndex,\\n      openLightbox,\\n      closeLightbox,\\n      registerImage,\\n      lightboxContentRef,\\n      setDialogRef,\\n    }),\\n    [\\n      images,\\n      activeIndex,\\n      openLightbox,\\n      closeLightbox,\\n      registerImage,\\n      setDialogRef,\\n    ],\\n  );\\n\\n  return (\\n    <ImageGalleryContext.Provider value={value}>\\n      {children}\\n    </ImageGalleryContext.Provider>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/image-gallery/gallery-grid.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/image-gallery/gallery-grid.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { useState, useCallback, useEffect, useRef } from \\\"react\\\";\\nimport { cn, ImageOff } from \\\"./_adapter\\\";\\nimport { useImageGallery } from \\\"./context\\\";\\nimport type { ImageGalleryItem } from \\\"./schema\\\";\\n\\ntype GridImage = Pick<\\n  ImageGalleryItem,\\n  \\\"id\\\" | \\\"src\\\" | \\\"alt\\\" | \\\"width\\\" | \\\"height\\\"\\n>;\\n\\ninterface GalleryGridProps {\\n  onImageClick?: (imageId: string) => void;\\n}\\n\\nexport function GalleryGrid({ onImageClick }: GalleryGridProps) {\\n  const { images, openLightbox } = useImageGallery();\\n\\n  const handleOpen = useCallback(\\n    (index: number) => {\\n      const image = images[index];\\n      if (image && onImageClick) {\\n        onImageClick(image.id);\\n      }\\n      openLightbox(index);\\n    },\\n    [images, onImageClick, openLightbox],\\n  );\\n\\n  return (\\n    <div\\n      className=\\\"grid grid-cols-2 gap-2 @md:grid-cols-3 @lg:grid-cols-4\\\"\\n      role=\\\"list\\\"\\n    >\\n      {images.map((image, index) => (\\n        <GridImageCard\\n          key={image.id}\\n          image={image}\\n          index={index}\\n          onClick={handleOpen}\\n        />\\n      ))}\\n    </div>\\n  );\\n}\\n\\ninterface GridImageCardProps {\\n  image: GridImage;\\n  index: number;\\n  onClick: (index: number) => void;\\n}\\n\\nfunction GridImageCard({ image, index, onClick }: GridImageCardProps) {\\n  const [hasError, setHasError] = useState(false);\\n  const wrapperRef = useRef<HTMLDivElement>(null);\\n\\n  const { registerImage } = useImageGallery();\\n\\n  const shouldSpanTwoRows = isPortraitImage(image);\\n\\n  useEffect(() => {\\n    const wrapper = wrapperRef.current;\\n    const img = wrapper?.querySelector(\\\"img\\\");\\n    if (img) {\\n      registerImage(image.id, img);\\n    }\\n    return () => {\\n      registerImage(image.id, null);\\n    };\\n  }, [image.id, registerImage]);\\n\\n  const handleClick = useCallback(() => {\\n    onClick(index);\\n  }, [onClick, index]);\\n\\n  return (\\n    <div\\n      role=\\\"listitem\\\"\\n      className={cn(\\n        \\\"group relative cursor-pointer\\\",\\n        shouldSpanTwoRows && \\\"row-span-2\\\",\\n      )}\\n      style={{ aspectRatio: shouldSpanTwoRows ? undefined : \\\"1 / 1\\\" }}\\n    >\\n      <button\\n        type=\\\"button\\\"\\n        onClick={handleClick}\\n        className=\\\"absolute inset-0 z-20 h-full w-full rounded-lg outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2\\\"\\n        aria-label={image.alt}\\n      />\\n\\n      <div\\n        ref={wrapperRef}\\n        className=\\\"bg-muted relative h-full w-full overflow-hidden rounded-lg transition-transform duration-200 ease-[cubic-bezier(0.4,0,0.2,1)] group-hover:scale-[1.02] group-active:scale-[0.98]\\\"\\n      >\\n        {hasError ? (\\n          <ImageErrorState alt={image.alt} />\\n        ) : (\\n          <img\\n            src={image.src}\\n            alt={image.alt}\\n            width={image.width}\\n            height={image.height}\\n            loading=\\\"lazy\\\"\\n            decoding=\\\"async\\\"\\n            draggable={false}\\n            onError={() => setHasError(true)}\\n            className=\\\"h-full w-full object-cover\\\"\\n          />\\n        )}\\n      </div>\\n    </div>\\n  );\\n}\\n\\nfunction isPortraitImage(image: GridImage): boolean {\\n  const aspectRatio = image.width / image.height;\\n  const isPortrait = aspectRatio < 1;\\n  const isSquarish = aspectRatio >= 0.9 && aspectRatio <= 1.1;\\n  return isPortrait && !isSquarish;\\n}\\n\\nfunction ImageErrorState({ alt }: { alt: string }) {\\n  return (\\n    <div className=\\\"absolute inset-0 flex flex-col items-center justify-center gap-2 p-4\\\">\\n      <ImageOff className=\\\"text-muted-foreground h-8 w-8\\\" />\\n      <span className=\\\"text-muted-foreground line-clamp-2 text-center text-xs\\\">\\n        {alt}\\n      </span>\\n    </div>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/image-gallery/gallery-lightbox.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/image-gallery/gallery-lightbox.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { useRef, useCallback } from \\\"react\\\";\\nimport { cn, Button, X } from \\\"./_adapter\\\";\\nimport { useImageGallery } from \\\"./context\\\";\\nimport type { ImageGalleryItem } from \\\"./schema\\\";\\nimport { resolveSafeNavigationHref } from \\\"../shared/media\\\";\\n\\ntype LightboxImage = Pick<ImageGalleryItem, \\\"title\\\" | \\\"caption\\\" | \\\"source\\\">;\\n\\nexport function GalleryLightbox() {\\n  const dialogRef = useRef<HTMLDialogElement>(null);\\n\\n  const {\\n    images,\\n    activeIndex,\\n    closeLightbox,\\n    lightboxContentRef,\\n    setDialogRef,\\n  } = useImageGallery();\\n\\n  const isOpen = activeIndex !== null;\\n  const currentImage = isOpen ? images[activeIndex] : null;\\n\\n  const handleDialogRef = useCallback(\\n    (element: HTMLDialogElement | null) => {\\n      dialogRef.current = element;\\n      setDialogRef(element);\\n    },\\n    [setDialogRef],\\n  );\\n\\n  const handleBackdropClick = useCallback(\\n    (e: React.MouseEvent<HTMLDialogElement>) => {\\n      if (e.target === dialogRef.current) {\\n        closeLightbox();\\n      }\\n    },\\n    [closeLightbox],\\n  );\\n\\n  const handleCancel = useCallback(\\n    (e: React.SyntheticEvent<HTMLDialogElement>) => {\\n      e.preventDefault();\\n      closeLightbox();\\n    },\\n    [closeLightbox],\\n  );\\n\\n  return (\\n    <dialog\\n      ref={handleDialogRef}\\n      onClick={handleBackdropClick}\\n      onCancel={handleCancel}\\n      className={cn(\\n        \\\"m-0 h-full max-h-full w-full max-w-full\\\",\\n        \\\"overflow-hidden p-0\\\",\\n        \\\"bg-transparent backdrop:bg-black/95 dark:backdrop:bg-black/90\\\",\\n        \\\"focus-visible:outline-none\\\",\\n      )}\\n      aria-label=\\\"Image lightbox\\\"\\n    >\\n      <div className=\\\"relative h-full w-full\\\">\\n        {isOpen && <CloseButton onClose={closeLightbox} />}\\n        <div className=\\\"relative z-10 flex h-full w-full flex-col items-center justify-center gap-4 p-8\\\">\\n          <div\\n            ref={lightboxContentRef}\\n            className={cn(\\n              \\\"pointer-events-auto relative w-fit max-w-full overflow-hidden rounded-lg shadow-2xl\\\",\\n              \\\"[&>img]:block [&>img]:max-h-[80vh] [&>img]:max-w-full\\\",\\n              \\\"[&>img]:h-auto [&>img]:w-auto [&>img]:object-contain [&>img]:select-none\\\",\\n            )}\\n          />\\n          {currentImage && <Metadata image={currentImage} />}\\n        </div>\\n      </div>\\n    </dialog>\\n  );\\n}\\n\\nfunction CloseButton({ onClose }: { onClose: () => void }) {\\n  return (\\n    <div className=\\\"absolute top-4 right-4 z-20\\\">\\n      <Button\\n        type=\\\"button\\\"\\n        variant=\\\"ghost\\\"\\n        size=\\\"icon\\\"\\n        onClick={onClose}\\n        className=\\\"text-white/80 hover:bg-white/10 hover:text-white\\\"\\n        aria-label=\\\"Close\\\"\\n      >\\n        <X className=\\\"h-5 w-5\\\" />\\n      </Button>\\n    </div>\\n  );\\n}\\n\\nfunction Metadata({ image }: { image: LightboxImage }) {\\n  const { title, caption, source } = image;\\n  const hasTitle = Boolean(title);\\n  const hasCaption = Boolean(caption);\\n  const hasSource = Boolean(source?.label);\\n\\n  if (!hasTitle && !hasCaption && !hasSource) {\\n    return null;\\n  }\\n\\n  return (\\n    <div className=\\\"text-center\\\">\\n      {hasTitle && (\\n        <h3 className=\\\"text-base font-medium tracking-tight text-white\\\">\\n          {title}\\n        </h3>\\n      )}\\n      {(hasCaption || hasSource) && (\\n        <p className=\\\"mt-1 text-sm text-white/60\\\">\\n          {caption}\\n          {hasCaption && hasSource && \\\" · \\\"}\\n          {hasSource && <SourceLink source={source!} />}\\n        </p>\\n      )}\\n    </div>\\n  );\\n}\\n\\nfunction SourceLink({\\n  source,\\n}: {\\n  source: NonNullable<LightboxImage[\\\"source\\\"]>;\\n}) {\\n  const href = resolveSafeNavigationHref(source.url);\\n  if (!href) {\\n    return <>{source.label}</>;\\n  }\\n\\n  return (\\n    <a\\n      href={href}\\n      target=\\\"_blank\\\"\\n      rel=\\\"noopener noreferrer\\\"\\n      className=\\\"hover:text-white/80 hover:underline\\\"\\n    >\\n      {source.label}\\n    </a>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/image-gallery/image-gallery.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/image-gallery/image-gallery.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport \\\"./styles.css\\\";\\nimport { cn } from \\\"./_adapter\\\";\\nimport { ImageGalleryProvider } from \\\"./context\\\";\\nimport { GalleryGrid } from \\\"./gallery-grid\\\";\\nimport { GalleryLightbox } from \\\"./gallery-lightbox\\\";\\nimport type { ImageGalleryProps } from \\\"./schema\\\";\\n\\nexport function ImageGallery({\\n  id,\\n  images,\\n  title,\\n  description,\\n  className,\\n  onImageClick,\\n}: ImageGalleryProps) {\\n  const handleImageClick = (imageId: string) => {\\n    if (!onImageClick) return;\\n\\n    const image = images.find((img) => img.id === imageId);\\n    if (image) {\\n      onImageClick(imageId, image);\\n    }\\n  };\\n\\n  return (\\n    <article\\n      className={cn(\\\"relative w-full min-w-80 max-w-lg\\\", className)}\\n      data-tool-ui-id={id}\\n      data-slot=\\\"image-gallery\\\"\\n    >\\n      <div\\n        className={cn(\\n          \\\"@container relative isolate flex w-full min-w-0 flex-col rounded-xl\\\",\\n          \\\"border border-border bg-card text-sm shadow-xs\\\",\\n        )}\\n      >\\n        <ImageGalleryProvider images={images}>\\n          <Header title={title} description={description} />\\n          <div className=\\\"p-3\\\">\\n            <GalleryGrid onImageClick={handleImageClick} />\\n          </div>\\n          <GalleryLightbox />\\n        </ImageGalleryProvider>\\n      </div>\\n    </article>\\n  );\\n}\\n\\ninterface HeaderProps {\\n  title?: string;\\n  description?: string;\\n}\\n\\nfunction Header({ title, description }: HeaderProps) {\\n  if (!title && !description) {\\n    return null;\\n  }\\n\\n  return (\\n    <div className=\\\"border-border/60 border-b px-4 pt-4 pb-3\\\">\\n      {title && (\\n        <h3 className=\\\"text-[15px] leading-tight font-semibold tracking-tight\\\">\\n          {title}\\n        </h3>\\n      )}\\n      {description && (\\n        <p className=\\\"text-muted-foreground mt-1 text-sm leading-snug\\\">\\n          {description}\\n        </p>\\n      )}\\n    </div>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/image-gallery/index.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/image-gallery/index.tsx\",\n      \"content\": \"export { ImageGallery } from \\\"./image-gallery\\\";\\nexport type {\\n  ImageGalleryProps,\\n  ImageGalleryItem,\\n  SerializableImageGallery,\\n} from \\\"./schema\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/image-gallery/README.md\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/image-gallery/README.md\",\n      \"content\": \"# Image Gallery\\n\\nImplementation for the \\\"image-gallery\\\" Tool UI surface.\\n\\n## Files\\n\\n- public exports: components/tool-ui/image-gallery/index.tsx\\n- serializable schema + parse helpers: components/tool-ui/image-gallery/schema.ts\\n\\n## Companion assets\\n\\n- Docs page: app/docs/image-gallery/content.mdx\\n- Preset payload: lib/presets/image-gallery.ts\\n\\n## Quick check\\n\\nRun this after edits:\\n\\npnpm test\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/image-gallery/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/image-gallery/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\nimport {\\n  ToolUIIdSchema,\\n  ToolUIReceiptSchema,\\n  ToolUIRoleSchema,\\n} from \\\"../shared/schema\\\";\\n\\nexport const ImageGallerySourceSchema = z.object({\\n  label: z.string(),\\n  url: z.string().url().optional(),\\n});\\n\\nexport type ImageGallerySource = z.infer<typeof ImageGallerySourceSchema>;\\n\\nexport const ImageGalleryItemSchema = z.object({\\n  id: z.string().min(1),\\n  src: z.string().url(),\\n  alt: z.string().min(1, \\\"Images require alt text for accessibility\\\"),\\n  width: z.number().positive(),\\n  height: z.number().positive(),\\n  title: z.string().optional(),\\n  caption: z.string().optional(),\\n  source: ImageGallerySourceSchema.optional(),\\n});\\n\\nexport type ImageGalleryItem = z.infer<typeof ImageGalleryItemSchema>;\\n\\nexport const SerializableImageGallerySchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n  images: z.array(ImageGalleryItemSchema).min(1),\\n  title: z.string().optional(),\\n  description: z.string().optional(),\\n});\\n\\nexport type SerializableImageGallery = z.infer<\\n  typeof SerializableImageGallerySchema\\n>;\\n\\nexport interface ImageGalleryProps extends SerializableImageGallery {\\n  className?: string;\\n  onImageClick?: (imageId: string, image: ImageGalleryItem) => void;\\n}\\n\\nconst SerializableImageGallerySchemaContract = defineToolUiContract(\\n  \\\"ImageGallery\\\",\\n  SerializableImageGallerySchema,\\n);\\n\\nexport const parseSerializableImageGallery: (\\n  input: unknown,\\n) => SerializableImageGallery = SerializableImageGallerySchemaContract.parse;\\n\\nexport const safeParseSerializableImageGallery: (\\n  input: unknown,\\n) => SerializableImageGallery | null =\\n  SerializableImageGallerySchemaContract.safeParse;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/image-gallery/styles.css\",\n      \"type\": \"registry:style\",\n      \"target\": \"components/tool-ui/image-gallery/styles.css\",\n      \"content\": \"@supports (view-transition-name: none) {\\n  ::view-transition-group(active-gallery-image) {\\n    animation-duration: 300ms;\\n    animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);\\n    overflow: clip;\\n    border-radius: 0.75rem;\\n  }\\n\\n  ::view-transition-image-pair(active-gallery-image) {\\n    overflow: clip;\\n    border-radius: 0.75rem;\\n  }\\n\\n  ::view-transition-old(active-gallery-image),\\n  ::view-transition-new(active-gallery-image) {\\n    border-radius: 0.75rem;\\n    mix-blend-mode: normal;\\n  }\\n\\n  @media (prefers-reduced-motion: reduce) {\\n    ::view-transition-group(active-gallery-image) {\\n      animation-duration: 0ms;\\n    }\\n  }\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/aspect-ratio.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/aspect-ratio.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nexport const AspectRatioSchema = z\\n  .enum([\\\"auto\\\", \\\"1:1\\\", \\\"4:3\\\", \\\"16:9\\\", \\\"9:16\\\"])\\n  .default(\\\"auto\\\");\\n\\nexport type AspectRatio = z.infer<typeof AspectRatioSchema>;\\n\\nexport const MediaFitSchema = z.enum([\\\"cover\\\", \\\"contain\\\"]).default(\\\"cover\\\");\\n\\nexport type MediaFit = z.infer<typeof MediaFitSchema>;\\n\\nexport const RATIO_CLASS_MAP: Record<AspectRatio, string> = {\\n  auto: \\\"\\\",\\n  \\\"1:1\\\": \\\"aspect-square\\\",\\n  \\\"4:3\\\": \\\"aspect-[4/3]\\\",\\n  \\\"16:9\\\": \\\"aspect-video\\\",\\n  \\\"9:16\\\": \\\"aspect-[9/16]\\\",\\n};\\n\\nexport function getRatioClass(ratio: AspectRatio): string {\\n  return RATIO_CLASS_MAP[ratio];\\n}\\n\\nexport function getFitClass(fit: MediaFit): string {\\n  return fit === \\\"cover\\\" ? \\\"object-cover\\\" : \\\"object-contain\\\";\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/format-utils.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/format-utils.ts\",\n      \"content\": \"/**\\n * Format duration in milliseconds to human-readable string.\\n * @example formatDuration(128000) => \\\"2:08\\\"\\n * @example formatDuration(3661000) => \\\"1:01:01\\\"\\n */\\nexport function formatDuration(durationMs: number): string {\\n  const totalSeconds = Math.round(durationMs / 1000);\\n  const hours = Math.floor(totalSeconds / 3600);\\n  const minutes = Math.floor((totalSeconds % 3600) / 60);\\n  const seconds = totalSeconds % 60;\\n\\n  if (hours > 0) {\\n    return `${hours}:${minutes.toString().padStart(2, \\\"0\\\")}:${seconds\\n      .toString()\\n      .padStart(2, \\\"0\\\")}`;\\n  }\\n  return `${minutes}:${seconds.toString().padStart(2, \\\"0\\\")}`;\\n}\\n\\n/**\\n * Format file size in bytes to human-readable string.\\n * @example formatFileSize(1024) => \\\"1 KB\\\"\\n * @example formatFileSize(1536000) => \\\"1.5 MB\\\"\\n */\\nexport function formatFileSize(bytes: number): string {\\n  if (bytes < 1024) return `${bytes} B`;\\n  const units = [\\\"KB\\\", \\\"MB\\\", \\\"GB\\\"];\\n  let size = bytes / 1024;\\n  let unit = 0;\\n  while (size >= 1024 && unit < units.length - 1) {\\n    size /= 1024;\\n    unit += 1;\\n  }\\n  return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unit]}`;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/index.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/index.ts\",\n      \"content\": \"export {\\n  AspectRatioSchema,\\n  MediaFitSchema,\\n  RATIO_CLASS_MAP,\\n  getRatioClass,\\n  getFitClass,\\n  type AspectRatio,\\n  type MediaFit,\\n} from \\\"./aspect-ratio\\\";\\n\\nexport { OVERLAY_GRADIENT } from \\\"./overlay-gradient\\\";\\n\\nexport { formatDuration, formatFileSize } from \\\"./format-utils\\\";\\n\\nexport { sanitizeHref } from \\\"./sanitize-href\\\";\\nexport {\\n  resolveSafeNavigationHref,\\n  openSafeNavigationHref,\\n} from \\\"./safe-navigation\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/overlay-gradient.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/overlay-gradient.ts\",\n      \"content\": \"/**\\n * Eased gradient for hover overlays on media elements.\\n * Creates a smooth fade from opaque black at top to transparent.\\n *\\n * @see https://larsenwork.com/easing-gradients/\\n */\\nexport const OVERLAY_GRADIENT = `linear-gradient(\\n  to bottom,\\n  hsl(0, 0%, 0%) 0%,\\n  hsla(0, 0%, 0%, 0.987) 8.3%,\\n  hsla(0, 0%, 0%, 0.951) 16.6%,\\n  hsla(0, 0%, 0%, 0.896) 24.6%,\\n  hsla(0, 0%, 0%, 0.825) 32.5%,\\n  hsla(0, 0%, 0%, 0.741) 40.1%,\\n  hsla(0, 0%, 0%, 0.648) 47.6%,\\n  hsla(0, 0%, 0%, 0.55) 54.8%,\\n  hsla(0, 0%, 0%, 0.45) 61.7%,\\n  hsla(0, 0%, 0%, 0.352) 68.3%,\\n  hsla(0, 0%, 0%, 0.259) 74.5%,\\n  hsla(0, 0%, 0%, 0.175) 80.4%,\\n  hsla(0, 0%, 0%, 0.104) 86%,\\n  hsla(0, 0%, 0%, 0.049) 91.1%,\\n  hsla(0, 0%, 0%, 0.013) 95.8%,\\n  hsla(0, 0%, 0%, 0) 100%\\n)` as const;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/safe-navigation.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/safe-navigation.ts\",\n      \"content\": \"import { sanitizeHref } from \\\"./sanitize-href\\\";\\n\\nexport function resolveSafeNavigationHref(\\n  ...candidates: Array<string | null | undefined>\\n): string | undefined {\\n  for (const candidate of candidates) {\\n    const safeHref = sanitizeHref(candidate ?? undefined);\\n    if (safeHref) {\\n      return safeHref;\\n    }\\n  }\\n\\n  return undefined;\\n}\\n\\nexport function openSafeNavigationHref(href: string | undefined): boolean {\\n  if (!href || typeof window === \\\"undefined\\\") {\\n    return false;\\n  }\\n\\n  window.open(href, \\\"_blank\\\", \\\"noopener,noreferrer\\\");\\n  return true;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/sanitize-href.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/sanitize-href.ts\",\n      \"content\": \"/**\\n * Sanitize a URL to ensure it's safe for use in href attributes.\\n * Allows:\\n * - Absolute http(s) URLs\\n * - Relative URLs (/path, ./path, ../path, ?query, #hash)\\n *\\n * @returns The sanitized URL string, or undefined if invalid/unsafe\\n */\\nexport function sanitizeHref(href?: string): string | undefined {\\n  if (!href) return undefined;\\n  const candidate = href.trim();\\n  if (!candidate) return undefined;\\n\\n  if (\\n    candidate.startsWith(\\\"/\\\") ||\\n    candidate.startsWith(\\\"./\\\") ||\\n    candidate.startsWith(\\\"../\\\") ||\\n    candidate.startsWith(\\\"?\\\") ||\\n    candidate.startsWith(\\\"#\\\")\\n  ) {\\n    if (candidate.startsWith(\\\"//\\\")) return undefined;\\n    // eslint-disable-next-line no-control-regex -- intentionally matching control characters\\n    if (/[\\\\u0000-\\\\u001F\\\\u007F]/.test(candidate)) return undefined;\\n    return candidate;\\n  }\\n\\n  try {\\n    const url = new URL(candidate);\\n    if (url.protocol === \\\"http:\\\" || url.protocol === \\\"https:\\\") {\\n      return url.toString();\\n    }\\n  } catch {\\n    return undefined;\\n  }\\n  return undefined;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { ReactNode } from \\\"react\\\";\\n\\n/**\\n * Tool UI conventions:\\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\\n * - Schema: `SerializableXSchema`\\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\\n * - Root attrs: `data-tool-ui-id` + `data-slot`\\n */\\n\\n/**\\n * Schema for tool UI identity.\\n *\\n * Every tool UI should have a unique identifier that:\\n * - Is stable across re-renders\\n * - Is meaningful (not auto-generated)\\n * - Is unique within the conversation\\n *\\n * Format recommendation: `{component-type}-{semantic-identifier}`\\n * Examples: \\\"data-table-expenses-q3\\\", \\\"option-list-deploy-target\\\"\\n */\\nexport const ToolUIIdSchema = z.string().min(1);\\n\\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\\n\\n/**\\n * Primary role of a Tool UI surface in a chat context.\\n */\\nexport const ToolUIRoleSchema = z.enum([\\n  \\\"information\\\",\\n  \\\"decision\\\",\\n  \\\"control\\\",\\n  \\\"state\\\",\\n  \\\"composite\\\",\\n]);\\n\\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\\n\\nexport const ToolUIReceiptOutcomeSchema = z.enum([\\n  \\\"success\\\",\\n  \\\"partial\\\",\\n  \\\"failed\\\",\\n  \\\"cancelled\\\",\\n]);\\n\\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\\n\\n/**\\n * Optional receipt metadata: a durable summary of an outcome.\\n */\\nexport const ToolUIReceiptSchema = z.object({\\n  outcome: ToolUIReceiptOutcomeSchema,\\n  summary: z.string().min(1),\\n  identifiers: z.record(z.string(), z.string()).optional(),\\n  at: z.string().datetime(),\\n});\\n\\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\\n\\n/**\\n * Base schema for Tool UI payloads (id + optional role/receipt).\\n */\\nexport const ToolUISurfaceSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n});\\n\\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\\n\\nexport const ActionSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  /**\\n   * Canonical narration the assistant can use after this action is taken.\\n   *\\n   * Example: \\\"I exported the table as CSV.\\\" / \\\"I opened the link in a new tab.\\\"\\n   */\\n  sentence: z.string().optional(),\\n  confirmLabel: z.string().optional(),\\n  variant: z\\n    .enum([\\\"default\\\", \\\"destructive\\\", \\\"secondary\\\", \\\"ghost\\\", \\\"outline\\\"])\\n    .optional(),\\n  icon: z.custom<ReactNode>().optional(),\\n  loading: z.boolean().optional(),\\n  disabled: z.boolean().optional(),\\n  shortcut: z.string().optional(),\\n});\\n\\nexport type Action = z.infer<typeof ActionSchema>;\\nexport type LocalAction = Action;\\nexport type DecisionAction = Action;\\n\\nexport const DecisionResultSchema = z.object({\\n  kind: z.literal(\\\"decision\\\"),\\n  version: z.literal(1),\\n  decisionId: z.string().min(1),\\n  actionId: z.string().min(1),\\n  actionLabel: z.string().min(1),\\n  at: z.string().datetime(),\\n  payload: z.record(z.string(), z.unknown()).optional(),\\n});\\n\\nexport type DecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n> = Omit<z.infer<typeof DecisionResultSchema>, \\\"payload\\\"> & {\\n  payload?: TPayload;\\n};\\n\\nexport function createDecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n>(args: {\\n  decisionId: string;\\n  action: { id: string; label: string };\\n  payload?: TPayload;\\n}): DecisionResult<TPayload> {\\n  return {\\n    kind: \\\"decision\\\",\\n    version: 1,\\n    decisionId: args.decisionId,\\n    actionId: args.action.id,\\n    actionLabel: args.action.label,\\n    at: new Date().toISOString(),\\n    payload: args.payload,\\n  };\\n}\\n\\nexport const ActionButtonsPropsSchema = z.object({\\n  actions: z.array(ActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n  className: z.string().optional(),\\n});\\n\\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\\n  actions: z.array(SerializableActionSchema),\\n}).omit({ className: true });\\n\\nexport interface ActionsConfig {\\n  items: Action[];\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  confirmTimeout?: number;\\n}\\n\\nexport const SerializableActionsConfigSchema = z.object({\\n  items: z.array(SerializableActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n});\\n\\nexport type SerializableActionsConfig = z.infer<\\n  typeof SerializableActionsConfigSchema\\n>;\\n\\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/image.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"image\",\n  \"type\": \"registry:block\",\n  \"title\": \"Image\",\n  \"description\": \"Display images with metadata and attribution.\",\n  \"dependencies\": [\n    \"zod\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/image/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/image/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n */\\n\\\"use client\\\";\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/image/image.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/image/image.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport * as React from \\\"react\\\";\\nimport { cn } from \\\"./_adapter\\\";\\n\\nimport {\\n  RATIO_CLASS_MAP,\\n  getFitClass,\\n  openSafeNavigationHref,\\n  resolveSafeNavigationHref,\\n  sanitizeHref,\\n} from \\\"../shared/media\\\";\\nimport type { SerializableImage, Source } from \\\"./schema\\\";\\n\\nconst FALLBACK_LOCALE = \\\"en-US\\\";\\n\\nexport interface ImageProps extends SerializableImage {\\n  className?: string;\\n  onNavigate?: (href: string, image: SerializableImage) => void;\\n}\\n\\nexport function Image(props: ImageProps) {\\n  const { className, onNavigate, ...serializable } = props;\\n\\n  const {\\n    id,\\n    src,\\n    alt,\\n    title,\\n    href: rawHref,\\n    domain,\\n    ratio = \\\"auto\\\",\\n    fit = \\\"cover\\\",\\n    source,\\n    locale: providedLocale,\\n  } = serializable;\\n\\n  const locale = providedLocale ?? FALLBACK_LOCALE;\\n  const sanitizedHref = sanitizeHref(rawHref);\\n  const resolvedSourceUrl = sanitizeHref(source?.url);\\n\\n  const imageData: SerializableImage = {\\n    ...serializable,\\n    href: sanitizedHref,\\n    source: source ? { ...source, url: resolvedSourceUrl } : undefined,\\n    locale,\\n  };\\n\\n  const sourceLabel = source?.label ?? domain;\\n  const fallbackInitial = (sourceLabel ?? \\\"\\\").trim().charAt(0).toUpperCase();\\n  const hasSource = Boolean(sourceLabel || source?.iconUrl);\\n\\n  const handleSourceClick = (event: React.MouseEvent<HTMLButtonElement>) => {\\n    event.preventDefault();\\n    event.stopPropagation();\\n    const targetUrl = resolveSafeNavigationHref(\\n      resolvedSourceUrl,\\n      source?.url,\\n      sanitizedHref,\\n      src,\\n    );\\n    if (!targetUrl) return;\\n    if (onNavigate) {\\n      onNavigate(targetUrl, imageData);\\n    } else {\\n      openSafeNavigationHref(targetUrl);\\n    }\\n  };\\n\\n  const handleImageClick = () => {\\n    if (!sanitizedHref) return;\\n    if (onNavigate) {\\n      onNavigate(sanitizedHref, imageData);\\n    } else {\\n      openSafeNavigationHref(sanitizedHref);\\n    }\\n  };\\n\\n  const hasMetadata = title || hasSource;\\n\\n  return (\\n    <article\\n      className={cn(\\\"relative w-full max-w-md min-w-80\\\", className)}\\n      lang={locale}\\n      data-tool-ui-id={id}\\n      data-slot=\\\"image\\\"\\n    >\\n      <div\\n        className={cn(\\n          \\\"group @container relative isolate flex w-full min-w-0 flex-col overflow-hidden rounded-xl\\\",\\n          \\\"border-border bg-card border text-sm shadow-xs\\\",\\n        )}\\n      >\\n        <>\\n          <div\\n            className={cn(\\n              \\\"bg-muted group relative w-full overflow-hidden\\\",\\n              ratio !== \\\"auto\\\" ? RATIO_CLASS_MAP[ratio] : \\\"min-h-[160px]\\\",\\n              sanitizedHref && \\\"cursor-pointer\\\",\\n            )}\\n            onClick={sanitizedHref ? handleImageClick : undefined}\\n            role={sanitizedHref ? \\\"link\\\" : undefined}\\n            tabIndex={sanitizedHref ? 0 : undefined}\\n            onKeyDown={\\n              sanitizedHref\\n                ? (e) => {\\n                    if (e.key === \\\"Enter\\\" || e.key === \\\" \\\") {\\n                      e.preventDefault();\\n                      handleImageClick();\\n                    }\\n                  }\\n                : undefined\\n            }\\n          >\\n            <img\\n              src={src}\\n              alt={alt}\\n              loading=\\\"lazy\\\"\\n              decoding=\\\"async\\\"\\n              className={cn(\\\"absolute inset-0 h-full w-full\\\", getFitClass(fit))}\\n            />\\n          </div>\\n          {hasMetadata && (\\n            <div className=\\\"flex items-center gap-3 px-4 py-3\\\">\\n              <SourceAttribution\\n                source={source}\\n                sourceLabel={sourceLabel}\\n                fallbackInitial={fallbackInitial}\\n                hasClickableUrl={Boolean(resolvedSourceUrl)}\\n                onSourceClick={handleSourceClick}\\n                title={title}\\n              />\\n            </div>\\n          )}\\n        </>\\n      </div>\\n    </article>\\n  );\\n}\\n\\ninterface SourceAttributionProps {\\n  source?: Source;\\n  sourceLabel?: string;\\n  fallbackInitial: string;\\n  hasClickableUrl: boolean;\\n  onSourceClick: (event: React.MouseEvent<HTMLButtonElement>) => void;\\n  title?: string;\\n}\\n\\nfunction SourceAttribution({\\n  source,\\n  sourceLabel,\\n  fallbackInitial,\\n  hasClickableUrl,\\n  onSourceClick,\\n  title,\\n}: SourceAttributionProps) {\\n  const hasSource = Boolean(sourceLabel || source?.iconUrl);\\n\\n  const content = (\\n    <div className=\\\"flex min-w-0 flex-1 items-center gap-3\\\">\\n      {source?.iconUrl ? (\\n        <img\\n          src={source.iconUrl}\\n          alt=\\\"\\\"\\n          aria-hidden=\\\"true\\\"\\n          width={32}\\n          height={32}\\n          className=\\\"size-8 shrink-0 rounded-full object-cover\\\"\\n          loading=\\\"lazy\\\"\\n          decoding=\\\"async\\\"\\n        />\\n      ) : fallbackInitial ? (\\n        <div className=\\\"bg-muted text-muted-foreground flex size-8 shrink-0 items-center justify-center rounded-full text-xs font-semibold uppercase\\\">\\n          {fallbackInitial}\\n        </div>\\n      ) : null}\\n      <div className=\\\"min-w-0 flex-1\\\">\\n        {title && (\\n          <div className=\\\"text-foreground line-clamp-1 text-sm font-medium\\\">\\n            {title}\\n          </div>\\n        )}\\n        {sourceLabel && (\\n          <div className=\\\"text-muted-foreground line-clamp-1 text-xs\\\">\\n            {sourceLabel}\\n          </div>\\n        )}\\n      </div>\\n    </div>\\n  );\\n\\n  if (hasClickableUrl && hasSource) {\\n    return (\\n      <button\\n        type=\\\"button\\\"\\n        onClick={onSourceClick}\\n        className=\\\"focus-visible:ring-ring flex w-full items-center gap-3 text-left hover:opacity-80 focus-visible:ring-2 focus-visible:outline-none\\\"\\n      >\\n        {content}\\n      </button>\\n    );\\n  }\\n\\n  return <div className=\\\"flex w-full items-center gap-3\\\">{content}</div>;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/image/index.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/image/index.ts\",\n      \"content\": \"export { Image } from \\\"./image\\\";\\nexport type { ImageProps } from \\\"./image\\\";\\nexport type { SerializableImage, Source } from \\\"./schema\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/image/README.md\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/image/README.md\",\n      \"content\": \"# Image\\n\\nImplementation for the \\\"image\\\" Tool UI surface.\\n\\n## Files\\n\\n- public exports: components/tool-ui/image/index.ts\\n- serializable schema + parse helpers: components/tool-ui/image/schema.ts\\n\\n## Companion assets\\n\\n- Docs page: app/docs/image/content.mdx\\n- Preset payload: lib/presets/image.ts\\n\\n## Quick check\\n\\nRun this after edits:\\n\\npnpm test\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/image/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/image/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\nimport {\\n  ToolUIIdSchema,\\n  ToolUIReceiptSchema,\\n  ToolUIRoleSchema,\\n} from \\\"../shared/schema\\\";\\n\\nimport { AspectRatioSchema, MediaFitSchema } from \\\"../shared/media\\\";\\n\\nexport const SourceSchema = z.object({\\n  label: z.string(),\\n  iconUrl: z.url().optional(),\\n  url: z.url().optional(),\\n});\\n\\nexport type Source = z.infer<typeof SourceSchema>;\\n\\nexport const SerializableImageSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n  assetId: z.string(),\\n  src: z.url(),\\n  alt: z.string().min(1, \\\"Images require alt text for accessibility\\\"),\\n  title: z.string().optional(),\\n  description: z.string().optional(),\\n  href: z.url().optional(),\\n  domain: z.string().optional(),\\n  ratio: AspectRatioSchema.optional(),\\n  fit: MediaFitSchema.optional(),\\n  fileSizeBytes: z.number().int().positive().optional(),\\n  createdAt: z.string().datetime().optional(),\\n  locale: z.string().optional(),\\n  source: SourceSchema.optional(),\\n});\\n\\nexport type SerializableImage = z.infer<typeof SerializableImageSchema>;\\n\\nconst SerializableImageSchemaContract = defineToolUiContract(\\n  \\\"Image\\\",\\n  SerializableImageSchema,\\n);\\n\\nexport const parseSerializableImage: (input: unknown) => SerializableImage =\\n  SerializableImageSchemaContract.parse;\\n\\nexport const safeParseSerializableImage: (\\n  input: unknown,\\n) => SerializableImage | null = SerializableImageSchemaContract.safeParse;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/aspect-ratio.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/aspect-ratio.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nexport const AspectRatioSchema = z\\n  .enum([\\\"auto\\\", \\\"1:1\\\", \\\"4:3\\\", \\\"16:9\\\", \\\"9:16\\\"])\\n  .default(\\\"auto\\\");\\n\\nexport type AspectRatio = z.infer<typeof AspectRatioSchema>;\\n\\nexport const MediaFitSchema = z.enum([\\\"cover\\\", \\\"contain\\\"]).default(\\\"cover\\\");\\n\\nexport type MediaFit = z.infer<typeof MediaFitSchema>;\\n\\nexport const RATIO_CLASS_MAP: Record<AspectRatio, string> = {\\n  auto: \\\"\\\",\\n  \\\"1:1\\\": \\\"aspect-square\\\",\\n  \\\"4:3\\\": \\\"aspect-[4/3]\\\",\\n  \\\"16:9\\\": \\\"aspect-video\\\",\\n  \\\"9:16\\\": \\\"aspect-[9/16]\\\",\\n};\\n\\nexport function getRatioClass(ratio: AspectRatio): string {\\n  return RATIO_CLASS_MAP[ratio];\\n}\\n\\nexport function getFitClass(fit: MediaFit): string {\\n  return fit === \\\"cover\\\" ? \\\"object-cover\\\" : \\\"object-contain\\\";\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/format-utils.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/format-utils.ts\",\n      \"content\": \"/**\\n * Format duration in milliseconds to human-readable string.\\n * @example formatDuration(128000) => \\\"2:08\\\"\\n * @example formatDuration(3661000) => \\\"1:01:01\\\"\\n */\\nexport function formatDuration(durationMs: number): string {\\n  const totalSeconds = Math.round(durationMs / 1000);\\n  const hours = Math.floor(totalSeconds / 3600);\\n  const minutes = Math.floor((totalSeconds % 3600) / 60);\\n  const seconds = totalSeconds % 60;\\n\\n  if (hours > 0) {\\n    return `${hours}:${minutes.toString().padStart(2, \\\"0\\\")}:${seconds\\n      .toString()\\n      .padStart(2, \\\"0\\\")}`;\\n  }\\n  return `${minutes}:${seconds.toString().padStart(2, \\\"0\\\")}`;\\n}\\n\\n/**\\n * Format file size in bytes to human-readable string.\\n * @example formatFileSize(1024) => \\\"1 KB\\\"\\n * @example formatFileSize(1536000) => \\\"1.5 MB\\\"\\n */\\nexport function formatFileSize(bytes: number): string {\\n  if (bytes < 1024) return `${bytes} B`;\\n  const units = [\\\"KB\\\", \\\"MB\\\", \\\"GB\\\"];\\n  let size = bytes / 1024;\\n  let unit = 0;\\n  while (size >= 1024 && unit < units.length - 1) {\\n    size /= 1024;\\n    unit += 1;\\n  }\\n  return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unit]}`;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/index.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/index.ts\",\n      \"content\": \"export {\\n  AspectRatioSchema,\\n  MediaFitSchema,\\n  RATIO_CLASS_MAP,\\n  getRatioClass,\\n  getFitClass,\\n  type AspectRatio,\\n  type MediaFit,\\n} from \\\"./aspect-ratio\\\";\\n\\nexport { OVERLAY_GRADIENT } from \\\"./overlay-gradient\\\";\\n\\nexport { formatDuration, formatFileSize } from \\\"./format-utils\\\";\\n\\nexport { sanitizeHref } from \\\"./sanitize-href\\\";\\nexport {\\n  resolveSafeNavigationHref,\\n  openSafeNavigationHref,\\n} from \\\"./safe-navigation\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/overlay-gradient.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/overlay-gradient.ts\",\n      \"content\": \"/**\\n * Eased gradient for hover overlays on media elements.\\n * Creates a smooth fade from opaque black at top to transparent.\\n *\\n * @see https://larsenwork.com/easing-gradients/\\n */\\nexport const OVERLAY_GRADIENT = `linear-gradient(\\n  to bottom,\\n  hsl(0, 0%, 0%) 0%,\\n  hsla(0, 0%, 0%, 0.987) 8.3%,\\n  hsla(0, 0%, 0%, 0.951) 16.6%,\\n  hsla(0, 0%, 0%, 0.896) 24.6%,\\n  hsla(0, 0%, 0%, 0.825) 32.5%,\\n  hsla(0, 0%, 0%, 0.741) 40.1%,\\n  hsla(0, 0%, 0%, 0.648) 47.6%,\\n  hsla(0, 0%, 0%, 0.55) 54.8%,\\n  hsla(0, 0%, 0%, 0.45) 61.7%,\\n  hsla(0, 0%, 0%, 0.352) 68.3%,\\n  hsla(0, 0%, 0%, 0.259) 74.5%,\\n  hsla(0, 0%, 0%, 0.175) 80.4%,\\n  hsla(0, 0%, 0%, 0.104) 86%,\\n  hsla(0, 0%, 0%, 0.049) 91.1%,\\n  hsla(0, 0%, 0%, 0.013) 95.8%,\\n  hsla(0, 0%, 0%, 0) 100%\\n)` as const;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/safe-navigation.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/safe-navigation.ts\",\n      \"content\": \"import { sanitizeHref } from \\\"./sanitize-href\\\";\\n\\nexport function resolveSafeNavigationHref(\\n  ...candidates: Array<string | null | undefined>\\n): string | undefined {\\n  for (const candidate of candidates) {\\n    const safeHref = sanitizeHref(candidate ?? undefined);\\n    if (safeHref) {\\n      return safeHref;\\n    }\\n  }\\n\\n  return undefined;\\n}\\n\\nexport function openSafeNavigationHref(href: string | undefined): boolean {\\n  if (!href || typeof window === \\\"undefined\\\") {\\n    return false;\\n  }\\n\\n  window.open(href, \\\"_blank\\\", \\\"noopener,noreferrer\\\");\\n  return true;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/sanitize-href.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/sanitize-href.ts\",\n      \"content\": \"/**\\n * Sanitize a URL to ensure it's safe for use in href attributes.\\n * Allows:\\n * - Absolute http(s) URLs\\n * - Relative URLs (/path, ./path, ../path, ?query, #hash)\\n *\\n * @returns The sanitized URL string, or undefined if invalid/unsafe\\n */\\nexport function sanitizeHref(href?: string): string | undefined {\\n  if (!href) return undefined;\\n  const candidate = href.trim();\\n  if (!candidate) return undefined;\\n\\n  if (\\n    candidate.startsWith(\\\"/\\\") ||\\n    candidate.startsWith(\\\"./\\\") ||\\n    candidate.startsWith(\\\"../\\\") ||\\n    candidate.startsWith(\\\"?\\\") ||\\n    candidate.startsWith(\\\"#\\\")\\n  ) {\\n    if (candidate.startsWith(\\\"//\\\")) return undefined;\\n    // eslint-disable-next-line no-control-regex -- intentionally matching control characters\\n    if (/[\\\\u0000-\\\\u001F\\\\u007F]/.test(candidate)) return undefined;\\n    return candidate;\\n  }\\n\\n  try {\\n    const url = new URL(candidate);\\n    if (url.protocol === \\\"http:\\\" || url.protocol === \\\"https:\\\") {\\n      return url.toString();\\n    }\\n  } catch {\\n    return undefined;\\n  }\\n  return undefined;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { ReactNode } from \\\"react\\\";\\n\\n/**\\n * Tool UI conventions:\\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\\n * - Schema: `SerializableXSchema`\\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\\n * - Root attrs: `data-tool-ui-id` + `data-slot`\\n */\\n\\n/**\\n * Schema for tool UI identity.\\n *\\n * Every tool UI should have a unique identifier that:\\n * - Is stable across re-renders\\n * - Is meaningful (not auto-generated)\\n * - Is unique within the conversation\\n *\\n * Format recommendation: `{component-type}-{semantic-identifier}`\\n * Examples: \\\"data-table-expenses-q3\\\", \\\"option-list-deploy-target\\\"\\n */\\nexport const ToolUIIdSchema = z.string().min(1);\\n\\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\\n\\n/**\\n * Primary role of a Tool UI surface in a chat context.\\n */\\nexport const ToolUIRoleSchema = z.enum([\\n  \\\"information\\\",\\n  \\\"decision\\\",\\n  \\\"control\\\",\\n  \\\"state\\\",\\n  \\\"composite\\\",\\n]);\\n\\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\\n\\nexport const ToolUIReceiptOutcomeSchema = z.enum([\\n  \\\"success\\\",\\n  \\\"partial\\\",\\n  \\\"failed\\\",\\n  \\\"cancelled\\\",\\n]);\\n\\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\\n\\n/**\\n * Optional receipt metadata: a durable summary of an outcome.\\n */\\nexport const ToolUIReceiptSchema = z.object({\\n  outcome: ToolUIReceiptOutcomeSchema,\\n  summary: z.string().min(1),\\n  identifiers: z.record(z.string(), z.string()).optional(),\\n  at: z.string().datetime(),\\n});\\n\\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\\n\\n/**\\n * Base schema for Tool UI payloads (id + optional role/receipt).\\n */\\nexport const ToolUISurfaceSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n});\\n\\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\\n\\nexport const ActionSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  /**\\n   * Canonical narration the assistant can use after this action is taken.\\n   *\\n   * Example: \\\"I exported the table as CSV.\\\" / \\\"I opened the link in a new tab.\\\"\\n   */\\n  sentence: z.string().optional(),\\n  confirmLabel: z.string().optional(),\\n  variant: z\\n    .enum([\\\"default\\\", \\\"destructive\\\", \\\"secondary\\\", \\\"ghost\\\", \\\"outline\\\"])\\n    .optional(),\\n  icon: z.custom<ReactNode>().optional(),\\n  loading: z.boolean().optional(),\\n  disabled: z.boolean().optional(),\\n  shortcut: z.string().optional(),\\n});\\n\\nexport type Action = z.infer<typeof ActionSchema>;\\nexport type LocalAction = Action;\\nexport type DecisionAction = Action;\\n\\nexport const DecisionResultSchema = z.object({\\n  kind: z.literal(\\\"decision\\\"),\\n  version: z.literal(1),\\n  decisionId: z.string().min(1),\\n  actionId: z.string().min(1),\\n  actionLabel: z.string().min(1),\\n  at: z.string().datetime(),\\n  payload: z.record(z.string(), z.unknown()).optional(),\\n});\\n\\nexport type DecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n> = Omit<z.infer<typeof DecisionResultSchema>, \\\"payload\\\"> & {\\n  payload?: TPayload;\\n};\\n\\nexport function createDecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n>(args: {\\n  decisionId: string;\\n  action: { id: string; label: string };\\n  payload?: TPayload;\\n}): DecisionResult<TPayload> {\\n  return {\\n    kind: \\\"decision\\\",\\n    version: 1,\\n    decisionId: args.decisionId,\\n    actionId: args.action.id,\\n    actionLabel: args.action.label,\\n    at: new Date().toISOString(),\\n    payload: args.payload,\\n  };\\n}\\n\\nexport const ActionButtonsPropsSchema = z.object({\\n  actions: z.array(ActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n  className: z.string().optional(),\\n});\\n\\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\\n  actions: z.array(SerializableActionSchema),\\n}).omit({ className: true });\\n\\nexport interface ActionsConfig {\\n  items: Action[];\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  confirmTimeout?: number;\\n}\\n\\nexport const SerializableActionsConfigSchema = z.object({\\n  items: z.array(SerializableActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n});\\n\\nexport type SerializableActionsConfig = z.infer<\\n  typeof SerializableActionsConfigSchema\\n>;\\n\\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/instagram-post.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"instagram-post\",\n  \"type\": \"registry:block\",\n  \"title\": \"Instagram Post\",\n  \"description\": \"Render Instagram post previews.\",\n  \"dependencies\": [\n    \"lucide-react\",\n    \"zod\"\n  ],\n  \"registryDependencies\": [\n    \"button\",\n    \"tooltip\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/instagram-post/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/instagram-post/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn      → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n *   Button  → shadcn/ui Button\\n *   Tooltip → shadcn/ui Tooltip\\n */\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport { Button } from \\\"@/components/ui/button\\\";\\nexport {\\n  Tooltip,\\n  TooltipContent,\\n  TooltipProvider,\\n  TooltipTrigger,\\n} from \\\"@/components/ui/tooltip\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/instagram-post/index.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/instagram-post/index.ts\",\n      \"content\": \"export { InstagramPost } from \\\"./instagram-post\\\";\\nexport type { InstagramPostProps } from \\\"./instagram-post\\\";\\nexport type {\\n  InstagramPostData,\\n  InstagramPostAuthor,\\n  InstagramPostMedia,\\n  InstagramPostStats,\\n} from \\\"./schema\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/instagram-post/instagram-post.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/instagram-post/instagram-post.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport * as React from \\\"react\\\";\\nimport { BadgeCheck, Heart, Share } from \\\"lucide-react\\\";\\nimport {\\n  cn,\\n  Button,\\n  Tooltip,\\n  TooltipContent,\\n  TooltipProvider,\\n  TooltipTrigger,\\n} from \\\"./_adapter\\\";\\nimport { formatRelativeTime } from \\\"../shared/utils\\\";\\n\\nimport type { InstagramPostData, InstagramPostMedia } from \\\"./schema\\\";\\n\\nexport interface InstagramPostProps {\\n  post: InstagramPostData;\\n  className?: string;\\n  onAction?: (action: string, post: InstagramPostData) => void;\\n}\\n\\nfunction InstagramLogo({ className }: { className?: string }) {\\n  const id = React.useId();\\n  const gradientPrimaryId = `ig-primary-${id}`;\\n  const gradientSecondaryId = `ig-secondary-${id}`;\\n\\n  return (\\n    <svg\\n      viewBox=\\\"0 0 132 132\\\"\\n      className={className}\\n      role=\\\"img\\\"\\n      aria-label=\\\"Instagram logo\\\"\\n    >\\n      <defs>\\n        <radialGradient\\n          id={gradientPrimaryId}\\n          cx=\\\"158.429\\\"\\n          cy=\\\"578.088\\\"\\n          r=\\\"65\\\"\\n          gradientUnits=\\\"userSpaceOnUse\\\"\\n          gradientTransform=\\\"matrix(0 -1.982 1.844 0 -1031.4 454)\\\"\\n        >\\n          <stop offset=\\\"0\\\" stopColor=\\\"#fd5\\\" />\\n          <stop offset=\\\".1\\\" stopColor=\\\"#fd5\\\" />\\n          <stop offset=\\\".5\\\" stopColor=\\\"#ff543e\\\" />\\n          <stop offset=\\\"1\\\" stopColor=\\\"#c837ab\\\" />\\n        </radialGradient>\\n        <radialGradient\\n          id={gradientSecondaryId}\\n          cx=\\\"147.694\\\"\\n          cy=\\\"473.455\\\"\\n          r=\\\"65\\\"\\n          gradientUnits=\\\"userSpaceOnUse\\\"\\n          gradientTransform=\\\"matrix(.174 .869 -3.58 .717 1648 -458.5)\\\"\\n        >\\n          <stop offset=\\\"0\\\" stopColor=\\\"#3771c8\\\" />\\n          <stop offset=\\\".128\\\" stopColor=\\\"#3771c8\\\" />\\n          <stop offset=\\\"1\\\" stopColor=\\\"#60f\\\" stopOpacity=\\\"0\\\" />\\n        </radialGradient>\\n      </defs>\\n      <path\\n        fill={`url(#${gradientPrimaryId})`}\\n        d=\\\"M65 0C37.9 0 30 .03 28.4.16c-5.6.46-9 1.34-12.8 3.22-2.9 1.44-5.2 3.12-7.5 5.47C4 13.1 1.5 18.4.6 24.66c-.44 3.04-.57 3.66-.6 19.2-.01 5.16 0 12 0 21.1 0 27.12.03 35.05.16 36.6.45 5.4 1.3 8.82 3.1 12.55 3.44 7.14 10 12.5 17.76 14.5 2.68.7 5.64 1.1 9.44 1.26 1.6.07 18 .12 34.44.12s32.84-.02 34.4-.1c4.4-.2 6.96-.55 9.8-1.28 7.78-2.01 14.23-7.3 17.74-14.53 1.76-3.64 2.66-7.18 3.07-12.32.08-1.12.12-18.97.12-36.8 0-17.85-.04-35.67-.13-36.8-.4-5.2-1.3-8.7-3.13-12.43-1.5-3.04-3.16-5.3-5.56-7.62C116.9 4 111.64 1.5 105.37.6 102.34.16 101.73.03 86.2 0H65z\\\"\\n        transform=\\\"translate(1 1)\\\"\\n      />\\n      <path\\n        fill={`url(#${gradientSecondaryId})`}\\n        d=\\\"M65 0C37.9 0 30 .03 28.4.16c-5.6.46-9 1.34-12.8 3.22-2.9 1.44-5.2 3.12-7.5 5.47C4 13.1 1.5 18.4.6 24.66c-.44 3.04-.57 3.66-.6 19.2-.01 5.16 0 12 0 21.1 0 27.12.03 35.05.16 36.6.45 5.4 1.3 8.82 3.1 12.55 3.44 7.14 10 12.5 17.76 14.5 2.68.7 5.64 1.1 9.44 1.26 1.6.07 18 .12 34.44.12s32.84-.02 34.4-.1c4.4-.2 6.96-.55 9.8-1.28 7.78-2.01 14.23-7.3 17.74-14.53 1.76-3.64 2.66-7.18 3.07-12.32.08-1.12.12-18.97.12-36.8 0-17.85-.04-35.67-.13-36.8-.4-5.2-1.3-8.7-3.13-12.43-1.5-3.04-3.16-5.3-5.56-7.62C116.9 4 111.64 1.5 105.37.6 102.34.16 101.73.03 86.2 0H65z\\\"\\n        transform=\\\"translate(1 1)\\\"\\n      />\\n      <path\\n        fill=\\\"#fff\\\"\\n        d=\\\"M66 18c-13 0-14.67.06-19.8.3-5.1.23-8.6 1.04-11.64 2.22-3.16 1.23-5.84 2.87-8.5 5.54-2.67 2.67-4.3 5.35-5.54 8.5-1.2 3.05-2 6.54-2.23 11.65C18.06 51.33 18 52.96 18 66s.06 14.67.3 19.78c.22 5.12 1.03 8.6 2.22 11.66 1.22 3.15 2.86 5.83 5.53 8.5 2.67 2.67 5.35 4.3 8.5 5.53 3.06 1.2 6.55 2 11.65 2.23 5.12.23 6.76.3 19.8.3 13 0 14.66-.07 19.78-.3 5.12-.23 8.6-1.03 11.66-2.23 3.15-1.23 5.83-2.87 8.5-5.53 2.67-2.67 4.3-5.35 5.53-8.5 1.2-3.06 2-6.54 2.23-11.66.23-5.1.3-6.75.3-19.78 0-13.04-.07-14.68-.3-19.8-.23-5.1-1.04-8.6-2.22-11.64-1.23-3.16-2.87-5.84-5.54-8.5-2.67-2.67-5.35-4.3-8.5-5.54-3.06-1.18-6.55-2-11.66-2.22-5.12-.24-6.75-.3-19.8-.3zm-4.3 8.65c1.28 0 2.7 0 4.3 0 12.82 0 14.34.05 19.4.28 4.67.2 7.22 1 8.9 1.65 2.25.87 3.84 1.9 5.52 3.6 1.68 1.67 2.72 3.27 3.6 5.5.65 1.7 1.43 4.24 1.64 8.92.23 5.05.28 6.57.28 19.4s-.05 14.32-.28 19.4c-.2 4.67-1 7.2-1.64 8.9-.88 2.25-1.92 3.84-3.6 5.52-1.68 1.68-3.27 2.72-5.52 3.6-1.7.65-4.23 1.43-8.9 1.64-5.06.23-6.58.28-19.4.28-12.82 0-14.34-.05-19.4-.28-4.68-.2-7.22-1-8.9-1.64-2.25-.88-3.84-1.92-5.52-3.6-1.68-1.68-2.72-3.27-3.6-5.52-.65-1.7-1.43-4.23-1.64-8.9-.23-5.06-.28-6.58-.28-19.4s.05-14.34.28-19.4c.2-4.68 1-7.22 1.64-8.9.88-2.24 1.92-3.83 3.6-5.52 1.68-1.68 3.27-2.72 5.52-3.6 1.7-.65 4.23-1.43 8.9-1.65 4.43-.2 6.15-.26 15.1-.27zm30 8c-3.2 0-5.77 2.57-5.77 5.75 0 3.2 2.58 5.77 5.77 5.77 3.18 0 5.76-2.58 5.76-5.77 0-3.18-2.58-5.76-5.76-5.76zm-25.63 6.72c-13.6 0-24.64 11.04-24.64 24.65 0 13.6 11.03 24.64 24.64 24.64 13.6 0 24.65-11.03 24.65-24.64 0-13.6-11.04-24.64-24.65-24.64zm0 8.65c8.84 0 16 7.16 16 16 0 8.84-7.16 16-16 16-8.84 0-16-7.16-16-16 0-8.84 7.16-16 16-16z\\\"\\n      />\\n    </svg>\\n  );\\n}\\n\\nfunction Header({\\n  author,\\n  createdAt,\\n}: {\\n  author: InstagramPostData[\\\"author\\\"];\\n  createdAt?: string;\\n}) {\\n  return (\\n    <header className=\\\"flex items-center gap-3 p-3\\\">\\n      <img\\n        src={author.avatarUrl}\\n        alt={`${author.name} avatar`}\\n        width={32}\\n        height={32}\\n        className=\\\"size-8 rounded-full object-cover\\\"\\n      />\\n      <div className=\\\"flex min-w-0 flex-1 items-center gap-1.5\\\">\\n        <span className=\\\"truncate text-sm font-semibold\\\">{author.handle}</span>\\n        {author.verified && (\\n          <BadgeCheck\\n            aria-label=\\\"Verified\\\"\\n            className=\\\"size-3.5 shrink-0 text-sky-500\\\"\\n          />\\n        )}\\n        {createdAt && (\\n          <>\\n            <span className=\\\"text-muted-foreground\\\">·</span>\\n            <span className=\\\"text-muted-foreground text-sm\\\">\\n              {formatRelativeTime(createdAt)}\\n            </span>\\n          </>\\n        )}\\n      </div>\\n      <InstagramLogo className=\\\"size-5\\\" />\\n    </header>\\n  );\\n}\\n\\nfunction MediaGrid({\\n  media,\\n  onOpen,\\n}: {\\n  media: InstagramPostMedia[];\\n  onOpen?: (index: number) => void;\\n}) {\\n  if (media.length === 0) return null;\\n\\n  const renderItem = (item: InstagramPostMedia, index: number) => (\\n    <button\\n      key={index}\\n      type=\\\"button\\\"\\n      className=\\\"bg-muted relative block size-full overflow-hidden\\\"\\n      onClick={() => onOpen?.(index)}\\n    >\\n      {item.type === \\\"image\\\" ? (\\n        <img\\n          src={item.url}\\n          alt={item.alt}\\n          className=\\\"size-full object-cover\\\"\\n          loading=\\\"lazy\\\"\\n        />\\n      ) : (\\n        <video src={item.url} playsInline className=\\\"size-full object-cover\\\" />\\n      )}\\n    </button>\\n  );\\n\\n  if (media.length === 1) {\\n    return (\\n      <div className=\\\"aspect-square w-full overflow-hidden\\\">\\n        {renderItem(media[0], 0)}\\n      </div>\\n    );\\n  }\\n\\n  if (media.length === 2) {\\n    return (\\n      <div className=\\\"grid aspect-square w-full grid-cols-2 gap-0.5 overflow-hidden\\\">\\n        {media.map(renderItem)}\\n      </div>\\n    );\\n  }\\n\\n  if (media.length === 3) {\\n    return (\\n      <div className=\\\"grid aspect-square w-full grid-cols-2 gap-0.5 overflow-hidden\\\">\\n        <div className=\\\"h-full\\\">{renderItem(media[0], 0)}</div>\\n        <div className=\\\"grid h-full grid-rows-2 gap-0.5\\\">\\n          {media.slice(1).map((item, i) => (\\n            <div key={i + 1} className=\\\"h-full\\\">\\n              {renderItem(item, i + 1)}\\n            </div>\\n          ))}\\n        </div>\\n      </div>\\n    );\\n  }\\n\\n  return (\\n    <div className=\\\"grid aspect-square w-full grid-cols-2 gap-0.5 overflow-hidden\\\">\\n      {media.slice(0, 4).map((item, index) => (\\n        <div key={index} className=\\\"relative h-full w-full\\\">\\n          {renderItem(item, index)}\\n          {index === 3 && media.length > 4 && (\\n            <div className=\\\"pointer-events-none absolute inset-0 flex items-center justify-center bg-black/50\\\">\\n              <span className=\\\"text-2xl font-semibold text-white\\\">\\n                +{media.length - 4}\\n              </span>\\n            </div>\\n          )}\\n        </div>\\n      ))}\\n    </div>\\n  );\\n}\\n\\nfunction PostBody({ text }: { text?: string }) {\\n  if (!text) return null;\\n  return (\\n    <span className=\\\"text-sm leading-relaxed text-pretty wrap-break-word whitespace-pre-wrap\\\">\\n      {text}\\n    </span>\\n  );\\n}\\n\\nfunction ActionButton({\\n  icon: Icon,\\n  label,\\n  active,\\n  hoverColor,\\n  activeColor,\\n  onClick,\\n}: {\\n  icon: React.ComponentType<{ className?: string }>;\\n  label: string;\\n  active?: boolean;\\n  hoverColor: string;\\n  activeColor?: string;\\n  onClick: () => void;\\n}) {\\n  return (\\n    <Tooltip>\\n      <TooltipTrigger asChild>\\n        <Button\\n          variant=\\\"ghost\\\"\\n          size=\\\"sm\\\"\\n          onClick={(e) => {\\n            e.stopPropagation();\\n            onClick();\\n          }}\\n          className={cn(\\\"h-auto\\\", hoverColor, active && activeColor)}\\n          aria-label={label}\\n        >\\n          <Icon className=\\\"size-5\\\" />\\n        </Button>\\n      </TooltipTrigger>\\n      <TooltipContent>{label}</TooltipContent>\\n    </Tooltip>\\n  );\\n}\\n\\nfunction PostActions({\\n  stats,\\n  onAction,\\n}: {\\n  stats?: InstagramPostData[\\\"stats\\\"];\\n  onAction: (action: string) => void;\\n}) {\\n  return (\\n    <TooltipProvider delayDuration={300}>\\n      <div className=\\\"flex items-center gap-1\\\">\\n        <ActionButton\\n          icon={Heart}\\n          label=\\\"Like\\\"\\n          active={stats?.isLiked}\\n          hoverColor=\\\"hover:opacity-60\\\"\\n          activeColor=\\\"text-red-500 fill-red-500\\\"\\n          onClick={() => onAction(\\\"like\\\")}\\n        />\\n        <ActionButton\\n          icon={Share}\\n          label=\\\"Share\\\"\\n          hoverColor=\\\"hover:opacity-60\\\"\\n          onClick={() => onAction(\\\"share\\\")}\\n        />\\n      </div>\\n    </TooltipProvider>\\n  );\\n}\\n\\nexport function InstagramPost({\\n  post,\\n  className,\\n  onAction,\\n}: InstagramPostProps) {\\n  return (\\n    <div\\n      className={cn(\\\"flex max-w-xl flex-col gap-3\\\", className)}\\n      data-tool-ui-id={post.id}\\n      data-slot=\\\"instagram-post\\\"\\n    >\\n      <article className=\\\"bg-card overflow-hidden rounded-lg border shadow-sm\\\">\\n        <Header author={post.author} createdAt={post.createdAt} />\\n\\n        {post.media && post.media.length > 0 && (\\n          <MediaGrid media={post.media} />\\n        )}\\n\\n        <div className=\\\"flex flex-col gap-2 p-3\\\">\\n          <PostActions\\n            stats={post.stats}\\n            onAction={(action) => onAction?.(action, post)}\\n          />\\n          {post.text && (\\n            <div>\\n              <span className=\\\"text-sm font-semibold\\\">\\n                {post.author.handle}\\n              </span>{\\\" \\\"}\\n              <PostBody text={post.text} />\\n            </div>\\n          )}\\n        </div>\\n      </article>\\n    </div>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/instagram-post/README.md\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/instagram-post/README.md\",\n      \"content\": \"# Instagram Post\\n\\nImplementation for the \\\"instagram-post\\\" Tool UI surface.\\n\\n## Files\\n\\n- public exports: components/tool-ui/instagram-post/index.ts\\n- serializable schema + parse helpers: components/tool-ui/instagram-post/schema.ts\\n\\n## Companion assets\\n\\n- Docs page: app/docs/social-post/content.mdx\\n- Preset payload: lib/presets/instagram-post.ts\\n\\n## Quick check\\n\\nRun this after edits:\\n\\npnpm test\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/instagram-post/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/instagram-post/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\n\\nexport const InstagramPostAuthorSchema = z.object({\\n  name: z.string(),\\n  handle: z.string(),\\n  avatarUrl: z.string(),\\n  verified: z.boolean().optional(),\\n});\\n\\nexport const InstagramPostMediaSchema = z.object({\\n  type: z.enum([\\\"image\\\", \\\"video\\\"]),\\n  url: z.string(),\\n  alt: z.string(),\\n});\\n\\nexport const InstagramPostStatsSchema = z.object({\\n  likes: z.number().optional(),\\n  isLiked: z.boolean().optional(),\\n});\\n\\nexport interface InstagramPostData {\\n  id: string;\\n  author: z.infer<typeof InstagramPostAuthorSchema>;\\n  text?: string;\\n  media?: z.infer<typeof InstagramPostMediaSchema>[];\\n  stats?: z.infer<typeof InstagramPostStatsSchema>;\\n  createdAt?: string;\\n}\\n\\nexport const SerializableInstagramPostSchema: z.ZodType<InstagramPostData> =\\n  z.object({\\n    id: z.string(),\\n    author: InstagramPostAuthorSchema,\\n    text: z.string().optional(),\\n    media: z.array(InstagramPostMediaSchema).optional(),\\n    stats: InstagramPostStatsSchema.optional(),\\n    createdAt: z.string().optional(),\\n  });\\n\\nexport type InstagramPostAuthor = z.infer<typeof InstagramPostAuthorSchema>;\\nexport type InstagramPostMedia = z.infer<typeof InstagramPostMediaSchema>;\\nexport type InstagramPostStats = z.infer<typeof InstagramPostStatsSchema>;\\n\\nconst SerializableInstagramPostSchemaContract = defineToolUiContract(\\n  \\\"InstagramPost\\\",\\n  SerializableInstagramPostSchema,\\n);\\n\\nexport const parseSerializableInstagramPost: (\\n  input: unknown,\\n) => InstagramPostData = SerializableInstagramPostSchemaContract.parse;\\n\\nexport const safeParseSerializableInstagramPost: (\\n  input: unknown,\\n) => InstagramPostData | null =\\n  SerializableInstagramPostSchemaContract.safeParse;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/utils.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/utils.ts\",\n      \"content\": \"export function formatRelativeTime(iso: string): string {\\n  const seconds = Math.round((Date.now() - new Date(iso).getTime()) / 1000);\\n  if (seconds < 60) return `${seconds}s`;\\n  if (seconds < 3600) return `${Math.round(seconds / 60)}m`;\\n  if (seconds < 86400) return `${Math.round(seconds / 3600)}h`;\\n  if (seconds < 604800) return `${Math.round(seconds / 86400)}d`;\\n  return `${Math.round(seconds / 604800)}w`;\\n}\\n\\nexport function formatCount(count: number): string {\\n  if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;\\n  if (count >= 1_000) return `${(count / 1_000).toFixed(1)}K`;\\n  return String(count);\\n}\\n\\nexport function getDomain(url: string): string {\\n  try {\\n    return new URL(url).hostname.replace(/^www\\\\./, \\\"\\\");\\n  } catch {\\n    return \\\"\\\";\\n  }\\n}\\n\\nexport function prefersReducedMotion(): boolean {\\n  return (\\n    typeof window !== \\\"undefined\\\" &&\\n    window.matchMedia?.(\\\"(prefers-reduced-motion: reduce)\\\").matches\\n  );\\n}\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/item-carousel.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"item-carousel\",\n  \"type\": \"registry:block\",\n  \"title\": \"Item Carousel\",\n  \"description\": \"Horizontal carousel for browsing collections.\",\n  \"dependencies\": [\n    \"lucide-react\",\n    \"zod\"\n  ],\n  \"registryDependencies\": [\n    \"button\",\n    \"card\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/item-carousel/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/item-carousel/_adapter.tsx\",\n      \"content\": \"/**\\n * UI and utility re-exports for copy-standalone portability.\\n *\\n * This file centralizes dependencies so the component can be easily\\n * copied to another project by updating these imports to match the target\\n * project's paths.\\n */\\nexport { cn } from \\\"@/lib/utils\\\";\\n\\nexport { Button } from \\\"@/components/ui/button\\\";\\nexport { Card } from \\\"@/components/ui/card\\\";\\nexport { ChevronLeft, ChevronRight } from \\\"lucide-react\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/item-carousel/index.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/item-carousel/index.tsx\",\n      \"content\": \"export { ItemCarousel } from \\\"./item-carousel\\\";\\nexport { ItemCard } from \\\"./item-card\\\";\\nexport type {\\n  Item,\\n  ItemCarouselProps,\\n  SerializableItem,\\n  SerializableItemCarousel,\\n} from \\\"./schema\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/item-carousel/item-card.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/item-carousel/item-card.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { cn, Button, Card } from \\\"./_adapter\\\";\\nimport type { Item } from \\\"./schema\\\";\\n\\ninterface ItemCardProps {\\n  item: Item;\\n  onItemClick?: (itemId: string) => void;\\n  onItemAction?: (itemId: string, actionId: string) => void;\\n}\\n\\nexport function ItemCard({ item, onItemClick, onItemAction }: ItemCardProps) {\\n  const { id, name, subtitle, image, color, actions } = item;\\n  const isCardInteractive = typeof onItemClick === \\\"function\\\";\\n\\n  const handleCardClick = () => {\\n    if (!isCardInteractive) return;\\n    onItemClick?.(id);\\n  };\\n\\n  const handleActionClick = (actionId: string) => {\\n    onItemAction?.(id, actionId);\\n  };\\n\\n  return (\\n    <Card\\n      className={cn(\\n        \\\"group @container/card relative flex w-52 min-w-48 flex-col gap-0 self-stretch overflow-clip rounded-md p-0 @lg:w-56\\\",\\n        isCardInteractive && \\\"cursor-pointer hover:shadow\\\",\\n        \\\"touch-manipulation\\\",\\n      )}\\n    >\\n      {isCardInteractive && (\\n        <button\\n          type=\\\"button\\\"\\n          aria-label={`View item: ${name}`}\\n          className={cn(\\n            \\\"absolute inset-0 z-10 rounded-md\\\",\\n            \\\"cursor-pointer touch-manipulation\\\",\\n            \\\"focus-visible:ring-ring focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none\\\",\\n          )}\\n          onClick={handleCardClick}\\n        />\\n      )}\\n\\n      <div className=\\\"bg-muted relative aspect-square w-full overflow-hidden\\\">\\n        {image ? (\\n          <img\\n            src={image}\\n            alt={name}\\n            loading=\\\"lazy\\\"\\n            decoding=\\\"async\\\"\\n            draggable={false}\\n            className={cn(\\n              \\\"h-full w-full object-cover transition-transform duration-200\\\",\\n              isCardInteractive && \\\"group-hover:scale-105\\\",\\n            )}\\n          />\\n        ) : (\\n          <div\\n            className={cn(\\n              \\\"h-full w-full transition-transform duration-200\\\",\\n              isCardInteractive && \\\"group-hover:scale-105\\\",\\n            )}\\n            style={color ? { backgroundColor: color } : undefined}\\n            role=\\\"img\\\"\\n            aria-label={name}\\n          />\\n        )}\\n      </div>\\n\\n      <div className=\\\"flex flex-1 flex-col gap-1 p-3\\\">\\n        <div className=\\\"flex flex-col gap-1\\\">\\n          <h3 className=\\\"line-clamp-2 text-sm leading-tight font-medium\\\">\\n            {name}\\n          </h3>\\n\\n          {subtitle && (\\n            <p className=\\\"text-muted-foreground line-clamp-1 text-sm\\\">\\n              {subtitle}\\n            </p>\\n          )}\\n        </div>\\n\\n        {actions && actions.length > 0 && (\\n          <div\\n            className={cn(\\n              \\\"relative z-20 mt-auto flex flex-col-reverse gap-2 pt-2 @[176px]/card:flex-row\\\",\\n            )}\\n          >\\n            {actions.map((action) => (\\n              <Button\\n                key={action.id}\\n                type=\\\"button\\\"\\n                variant={action.variant ?? \\\"default\\\"}\\n                size=\\\"sm\\\"\\n                disabled={action.disabled}\\n                className=\\\"min-h-11 w-full px-3 md:min-h-8 @[176px]/card:h-8 @[176px]/card:w-auto @[176px]/card:flex-1\\\"\\n                onClick={() => handleActionClick(action.id)}\\n              >\\n                {action.icon}\\n                {action.label}\\n              </Button>\\n            ))}\\n          </div>\\n        )}\\n      </div>\\n    </Card>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/item-carousel/item-carousel.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/item-carousel/item-carousel.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { useRef, useState, useEffect, useCallback } from \\\"react\\\";\\nimport { cn, Button, Card, ChevronLeft, ChevronRight } from \\\"./_adapter\\\";\\nimport { ItemCard } from \\\"./item-card\\\";\\nimport { prefersReducedMotion } from \\\"../shared/utils\\\";\\nimport type { ItemCarouselProps } from \\\"./schema\\\";\\n\\nconst SCROLL_PADDING_STYLE = { scrollPaddingInline: \\\"1rem\\\" };\\n\\nconst SCROLL_EDGE_THRESHOLD_PX = 8;\\nconst SNAP_EPSILON_PX = 5;\\nconst SCROLL_ANIMATION_DURATION_MS = 300;\\nconst PAGE_SCROLL_RATIO = 0.8;\\nconst PAGE_SCROLL_BREAKPOINT_PX = 640;\\n\\ntype ScrollDirection = \\\"left\\\" | \\\"right\\\";\\n\\ninterface ScrollAnimationState {\\n  target: number;\\n  start: number;\\n  startTime: number;\\n  duration: number;\\n  onComplete?: () => void;\\n}\\n\\nfunction useSmoothScroll() {\\n  const animationRef = useRef<ScrollAnimationState | null>(null);\\n  const frameRef = useRef<number | null>(null);\\n\\n  const cancelAnimation = useCallback(() => {\\n    if (frameRef.current !== null) {\\n      cancelAnimationFrame(frameRef.current);\\n      frameRef.current = null;\\n    }\\n    animationRef.current = null;\\n  }, []);\\n\\n  useEffect(() => cancelAnimation, [cancelAnimation]);\\n\\n  const scrollTo = useCallback(\\n    (\\n      element: HTMLElement,\\n      target: number,\\n      duration = SCROLL_ANIMATION_DURATION_MS,\\n      onComplete?: () => void,\\n    ) => {\\n      if (prefersReducedMotion() || duration <= 0) {\\n        element.scrollLeft = target;\\n        onComplete?.();\\n        return;\\n      }\\n\\n      cancelAnimation();\\n\\n      animationRef.current = {\\n        target,\\n        start: element.scrollLeft,\\n        startTime: performance.now(),\\n        duration,\\n        onComplete,\\n      };\\n\\n      element.style.scrollSnapType = \\\"none\\\";\\n\\n      const step = () => {\\n        const anim = animationRef.current;\\n        if (!anim) return;\\n\\n        const elapsed = performance.now() - anim.startTime;\\n        const progress = Math.min(elapsed / anim.duration, 1);\\n        const eased = 1 - Math.pow(1 - progress, 3);\\n\\n        element.scrollLeft = anim.start + (anim.target - anim.start) * eased;\\n\\n        if (progress < 1) {\\n          frameRef.current = requestAnimationFrame(step);\\n          return;\\n        }\\n\\n        element.scrollLeft = anim.target;\\n        const callback = anim.onComplete;\\n        cancelAnimation();\\n\\n        requestAnimationFrame(() => {\\n          element.style.scrollSnapType = \\\"\\\";\\n          callback?.();\\n        });\\n      };\\n\\n      frameRef.current = requestAnimationFrame(step);\\n    },\\n    [cancelAnimation],\\n  );\\n\\n  const isAnimating = useCallback(\\n    () => animationRef.current !== null && frameRef.current !== null,\\n    [],\\n  );\\n\\n  return { scrollTo, isAnimating, cancelAnimation };\\n}\\n\\nfunction useScrollEdgeState(\\n  scrollRef: React.RefObject<HTMLDivElement | null>,\\n  itemCount: number,\\n) {\\n  const [canScrollLeft, setCanScrollLeft] = useState(false);\\n  const [canScrollRight, setCanScrollRight] = useState(false);\\n\\n  const updateState = useCallback(() => {\\n    const container = scrollRef.current;\\n    if (!container) return;\\n\\n    const scrollLeft = Math.round(container.scrollLeft);\\n    const maxScroll = Math.max(\\n      0,\\n      Math.round(container.scrollWidth - container.clientWidth),\\n    );\\n\\n    setCanScrollLeft(scrollLeft > SCROLL_EDGE_THRESHOLD_PX);\\n    setCanScrollRight(scrollLeft < maxScroll - SCROLL_EDGE_THRESHOLD_PX);\\n  }, [scrollRef]);\\n\\n  useEffect(() => {\\n    const container = scrollRef.current;\\n    if (!container) return;\\n\\n    let rafId: number | null = null;\\n\\n    const scheduleUpdate = () => {\\n      if (rafId !== null) cancelAnimationFrame(rafId);\\n      rafId = requestAnimationFrame(() => {\\n        rafId = null;\\n        updateState();\\n      });\\n    };\\n\\n    scheduleUpdate();\\n\\n    container.addEventListener(\\\"scroll\\\", scheduleUpdate, { passive: true });\\n    const resizeObserver = new ResizeObserver(scheduleUpdate);\\n    resizeObserver.observe(container);\\n\\n    return () => {\\n      container.removeEventListener(\\\"scroll\\\", scheduleUpdate);\\n      resizeObserver.disconnect();\\n      if (rafId !== null) cancelAnimationFrame(rafId);\\n    };\\n  }, [scrollRef, updateState, itemCount]);\\n\\n  return { canScrollLeft, canScrollRight };\\n}\\n\\nfunction CarouselNavButton({\\n  direction,\\n  visible,\\n  onClick,\\n}: {\\n  direction: ScrollDirection;\\n  visible: boolean;\\n  onClick: () => void;\\n}) {\\n  const isLeft = direction === \\\"left\\\";\\n  const Icon = isLeft ? ChevronLeft : ChevronRight;\\n\\n  return (\\n    <Button\\n      type=\\\"button\\\"\\n      variant=\\\"secondary\\\"\\n      size=\\\"icon-sm\\\"\\n      className={cn(\\n        \\\"pointer-events-none scale-90 border-none opacity-0\\\",\\n        \\\"bg-background/60 absolute inset-y-0 z-20 my-auto hidden h-[6cqh] min-h-[50px] rounded-2xl backdrop-blur-lg\\\",\\n        \\\"transition-[opacity,transform] duration-250 ease-[cubic-bezier(0.16,1,0.3,1)] motion-reduce:transition-none\\\",\\n        \\\"@md:flex\\\",\\n        isLeft ? \\\"left-1.5\\\" : \\\"right-1.5\\\",\\n        visible &&\\n          \\\"pointer-events-auto scale-100 opacity-100 @md:group-focus-within:pointer-events-auto @md:group-focus-within:scale-100 @md:group-focus-within:opacity-100 @md:group-hover:pointer-events-auto @md:group-hover:scale-100 @md:group-hover:opacity-100\\\",\\n      )}\\n      onClick={onClick}\\n      aria-label={isLeft ? \\\"Scroll left\\\" : \\\"Scroll right\\\"}\\n      tabIndex={visible ? 0 : -1}\\n      aria-hidden={!visible}\\n    >\\n      <Icon className=\\\"h-4 w-4\\\" />\\n    </Button>\\n  );\\n}\\n\\ninterface ItemCarouselHeaderProps {\\n  title?: string;\\n  description?: string;\\n}\\n\\nfunction ItemCarouselHeader({ title, description }: ItemCarouselHeaderProps) {\\n  if (!title && !description) return null;\\n\\n  return (\\n    <div className=\\\"px-4 pt-4 pb-1\\\">\\n      {title && (\\n        <h3 className=\\\"text-[15px] leading-tight font-semibold tracking-tight\\\">\\n          {title}\\n        </h3>\\n      )}\\n      {description && (\\n        <p className=\\\"text-muted-foreground mt-1 text-sm leading-snug\\\">\\n          {description}\\n        </p>\\n      )}\\n    </div>\\n  );\\n}\\n\\ninterface EmptyStateProps {\\n  id: string;\\n  className?: string;\\n}\\n\\nfunction EmptyState({ id, className }: EmptyStateProps) {\\n  return (\\n    <Card\\n      data-tool-ui-id={id}\\n      data-slot=\\\"item-carousel\\\"\\n      className={cn(\\\"flex h-48 items-center justify-center\\\", className)}\\n    >\\n      <p className=\\\"text-muted-foreground text-sm\\\">No items to display</p>\\n    </Card>\\n  );\\n}\\n\\nfunction ItemCarouselRoot({\\n  id,\\n  title,\\n  description,\\n  items,\\n  className,\\n  onItemClick,\\n  onItemAction,\\n}: ItemCarouselProps) {\\n  const scrollRef = useRef<HTMLDivElement>(null);\\n  const targetIndexRef = useRef<number | null>(null);\\n\\n  const { scrollTo, isAnimating } = useSmoothScroll();\\n  const { canScrollLeft, canScrollRight } = useScrollEdgeState(\\n    scrollRef,\\n    items.length,\\n  );\\n\\n  const scroll = useCallback(\\n    (direction: ScrollDirection) => {\\n      const container = scrollRef.current;\\n      if (!container) return;\\n\\n      const paddingValue = window.getComputedStyle(container).scrollPaddingLeft;\\n      const scrollPaddingLeft = Number.isFinite(Number.parseFloat(paddingValue))\\n        ? Number.parseFloat(paddingValue)\\n        : 0;\\n\\n      const itemElements = Array.from(\\n        container.querySelectorAll<HTMLElement>(\\\"[data-carousel-item]\\\"),\\n      );\\n      if (itemElements.length === 0) return;\\n\\n      const snapPositions = itemElements.map((el) =>\\n        Math.max(0, el.offsetLeft - scrollPaddingLeft),\\n      );\\n\\n      const scrollLeft = Math.round(container.scrollLeft);\\n      let currentIndex: number;\\n      if (isAnimating()) {\\n        currentIndex = Math.min(\\n          targetIndexRef.current ?? 0,\\n          snapPositions.length - 1,\\n        );\\n      } else {\\n        currentIndex = snapPositions.length - 1;\\n        for (let i = 0; i < snapPositions.length; i++) {\\n          const snap = snapPositions[i];\\n          if (Math.abs(snap - scrollLeft) < SNAP_EPSILON_PX) {\\n            currentIndex = i;\\n            break;\\n          }\\n          if (snap > scrollLeft) {\\n            currentIndex = Math.max(0, i - 1);\\n            break;\\n          }\\n        }\\n      }\\n\\n      const itemStep =\\n        itemElements.length > 1\\n          ? itemElements[1].offsetLeft - itemElements[0].offsetLeft\\n          : 0;\\n      const safeStep =\\n        itemStep > 0 ? itemStep : itemElements[0].offsetWidth || 1;\\n\\n      const pageIndexStep =\\n        container.clientWidth >= PAGE_SCROLL_BREAKPOINT_PX\\n          ? Math.max(\\n              1,\\n              Math.floor(\\n                (container.clientWidth * PAGE_SCROLL_RATIO) / safeStep,\\n              ),\\n            )\\n          : 1;\\n\\n      const targetIndex =\\n        direction === \\\"right\\\"\\n          ? Math.min(currentIndex + pageIndexStep, itemElements.length - 1)\\n          : Math.max(currentIndex - pageIndexStep, 0);\\n\\n      targetIndexRef.current = targetIndex;\\n      const targetScrollLeft = snapPositions[targetIndex];\\n\\n      if (Math.abs(targetScrollLeft - container.scrollLeft) > 1) {\\n        scrollTo(\\n          container,\\n          targetScrollLeft,\\n          SCROLL_ANIMATION_DURATION_MS,\\n          () => {\\n            targetIndexRef.current = null;\\n          },\\n        );\\n      }\\n    },\\n    [scrollTo, isAnimating],\\n  );\\n\\n  const handleScrollLeft = useCallback(() => scroll(\\\"left\\\"), [scroll]);\\n  const handleScrollRight = useCallback(() => scroll(\\\"right\\\"), [scroll]);\\n\\n  if (items.length === 0) {\\n    return <EmptyState id={id} className={className} />;\\n  }\\n\\n  return (\\n    <div\\n      data-tool-ui-id={id}\\n      data-slot=\\\"item-carousel\\\"\\n      className={cn(\\n        \\\"bg-background @container relative isolate w-full gap-0 overflow-hidden rounded-2xl border p-0\\\",\\n        className,\\n      )}\\n    >\\n      <ItemCarouselHeader title={title} description={description} />\\n\\n      <div className=\\\"group relative\\\">\\n        <CarouselNavButton\\n          direction=\\\"left\\\"\\n          visible={canScrollLeft}\\n          onClick={handleScrollLeft}\\n        />\\n        <CarouselNavButton\\n          direction=\\\"right\\\"\\n          visible={canScrollRight}\\n          onClick={handleScrollRight}\\n        />\\n\\n        <div\\n          ref={scrollRef}\\n          className={cn(\\n            \\\"grid auto-cols-max grid-flow-col gap-4 overflow-x-auto overscroll-x-contain p-4\\\",\\n            \\\"snap-x snap-mandatory\\\",\\n          )}\\n          role=\\\"list\\\"\\n          style={SCROLL_PADDING_STYLE}\\n        >\\n          {items.map((item) => (\\n            <div\\n              key={item.id}\\n              data-carousel-item\\n              data-item-id={item.id}\\n              role=\\\"listitem\\\"\\n              className=\\\"flex snap-start snap-always\\\"\\n            >\\n              <ItemCard\\n                item={item}\\n                onItemClick={onItemClick}\\n                onItemAction={onItemAction}\\n              />\\n            </div>\\n          ))}\\n        </div>\\n      </div>\\n    </div>\\n  );\\n}\\n\\ntype ItemCarouselComponent = typeof ItemCarouselRoot & {\\n  Root: typeof ItemCarouselRoot;\\n  Header: typeof ItemCarouselHeader;\\n  EmptyState: typeof EmptyState;\\n  NavButton: typeof CarouselNavButton;\\n  Card: typeof ItemCard;\\n};\\n\\nexport const ItemCarousel = Object.assign(ItemCarouselRoot, {\\n  Root: ItemCarouselRoot,\\n  Header: ItemCarouselHeader,\\n  EmptyState,\\n  NavButton: CarouselNavButton,\\n  Card: ItemCard,\\n}) as ItemCarouselComponent;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/item-carousel/README.md\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/item-carousel/README.md\",\n      \"content\": \"# Item Carousel\\n\\nImplementation for the \\\"item-carousel\\\" Tool UI surface.\\n\\n## Files\\n\\n- public exports: components/tool-ui/item-carousel/index.tsx\\n- serializable schema + parse helpers: components/tool-ui/item-carousel/schema.ts\\n\\n## Companion assets\\n\\n- Docs page: app/docs/item-carousel/content.mdx\\n- Preset payload: lib/presets/item-carousel.ts\\n\\n## Quick check\\n\\nRun this after edits:\\n\\npnpm test\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/item-carousel/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/item-carousel/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\nimport {\\n  ActionSchema,\\n  SerializableActionSchema,\\n  ToolUIIdSchema,\\n} from \\\"../shared/schema\\\";\\n\\nexport const ItemSchema = z.object({\\n  id: z.string().min(1),\\n  name: z.string().min(1),\\n  subtitle: z.string().optional(),\\n  image: z.url().optional(),\\n  color: z.string().optional(),\\n  actions: z.array(ActionSchema).optional(),\\n});\\n\\nexport const ItemCarouselPropsSchema = z.object({\\n  id: ToolUIIdSchema,\\n  title: z.string().optional(),\\n  description: z.string().optional(),\\n  items: z.array(ItemSchema),\\n  className: z.string().optional(),\\n});\\n\\nexport type Item = z.infer<typeof ItemSchema>;\\n\\nexport type ItemCarouselProps = z.infer<typeof ItemCarouselPropsSchema> & {\\n  onItemClick?: (itemId: string) => void;\\n  onItemAction?: (itemId: string, actionId: string) => void;\\n};\\n\\nexport const SerializableItemSchema = ItemSchema.extend({\\n  actions: z.array(SerializableActionSchema).optional(),\\n});\\n\\nexport const SerializableItemCarouselSchema = ItemCarouselPropsSchema.omit({\\n  className: true,\\n})\\n  .extend({\\n    items: z.array(SerializableItemSchema),\\n  })\\n  .superRefine((payload, ctx) => {\\n    const seenItemIds = new Map<string, number>();\\n\\n    payload.items.forEach((item, index) => {\\n      const firstSeenAt = seenItemIds.get(item.id);\\n      if (firstSeenAt !== undefined) {\\n        ctx.addIssue({\\n          code: z.ZodIssueCode.custom,\\n          path: [\\\"items\\\", index, \\\"id\\\"],\\n          message: `duplicate item id '${item.id}' (first seen at index ${firstSeenAt})`,\\n        });\\n        return;\\n      }\\n      seenItemIds.set(item.id, index);\\n    });\\n  });\\n\\nexport type SerializableItem = z.infer<typeof SerializableItemSchema>;\\nexport type SerializableItemCarousel = z.infer<\\n  typeof SerializableItemCarouselSchema\\n>;\\n\\nconst SerializableItemCarouselSchemaContract = defineToolUiContract(\\n  \\\"ItemCarousel\\\",\\n  SerializableItemCarouselSchema,\\n);\\n\\nexport const parseSerializableItemCarousel: (\\n  input: unknown,\\n) => SerializableItemCarousel = SerializableItemCarouselSchemaContract.parse;\\n\\nexport const safeParseSerializableItemCarousel: (\\n  input: unknown,\\n) => SerializableItemCarousel | null =\\n  SerializableItemCarouselSchemaContract.safeParse;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { ReactNode } from \\\"react\\\";\\n\\n/**\\n * Tool UI conventions:\\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\\n * - Schema: `SerializableXSchema`\\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\\n * - Root attrs: `data-tool-ui-id` + `data-slot`\\n */\\n\\n/**\\n * Schema for tool UI identity.\\n *\\n * Every tool UI should have a unique identifier that:\\n * - Is stable across re-renders\\n * - Is meaningful (not auto-generated)\\n * - Is unique within the conversation\\n *\\n * Format recommendation: `{component-type}-{semantic-identifier}`\\n * Examples: \\\"data-table-expenses-q3\\\", \\\"option-list-deploy-target\\\"\\n */\\nexport const ToolUIIdSchema = z.string().min(1);\\n\\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\\n\\n/**\\n * Primary role of a Tool UI surface in a chat context.\\n */\\nexport const ToolUIRoleSchema = z.enum([\\n  \\\"information\\\",\\n  \\\"decision\\\",\\n  \\\"control\\\",\\n  \\\"state\\\",\\n  \\\"composite\\\",\\n]);\\n\\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\\n\\nexport const ToolUIReceiptOutcomeSchema = z.enum([\\n  \\\"success\\\",\\n  \\\"partial\\\",\\n  \\\"failed\\\",\\n  \\\"cancelled\\\",\\n]);\\n\\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\\n\\n/**\\n * Optional receipt metadata: a durable summary of an outcome.\\n */\\nexport const ToolUIReceiptSchema = z.object({\\n  outcome: ToolUIReceiptOutcomeSchema,\\n  summary: z.string().min(1),\\n  identifiers: z.record(z.string(), z.string()).optional(),\\n  at: z.string().datetime(),\\n});\\n\\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\\n\\n/**\\n * Base schema for Tool UI payloads (id + optional role/receipt).\\n */\\nexport const ToolUISurfaceSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n});\\n\\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\\n\\nexport const ActionSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  /**\\n   * Canonical narration the assistant can use after this action is taken.\\n   *\\n   * Example: \\\"I exported the table as CSV.\\\" / \\\"I opened the link in a new tab.\\\"\\n   */\\n  sentence: z.string().optional(),\\n  confirmLabel: z.string().optional(),\\n  variant: z\\n    .enum([\\\"default\\\", \\\"destructive\\\", \\\"secondary\\\", \\\"ghost\\\", \\\"outline\\\"])\\n    .optional(),\\n  icon: z.custom<ReactNode>().optional(),\\n  loading: z.boolean().optional(),\\n  disabled: z.boolean().optional(),\\n  shortcut: z.string().optional(),\\n});\\n\\nexport type Action = z.infer<typeof ActionSchema>;\\nexport type LocalAction = Action;\\nexport type DecisionAction = Action;\\n\\nexport const DecisionResultSchema = z.object({\\n  kind: z.literal(\\\"decision\\\"),\\n  version: z.literal(1),\\n  decisionId: z.string().min(1),\\n  actionId: z.string().min(1),\\n  actionLabel: z.string().min(1),\\n  at: z.string().datetime(),\\n  payload: z.record(z.string(), z.unknown()).optional(),\\n});\\n\\nexport type DecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n> = Omit<z.infer<typeof DecisionResultSchema>, \\\"payload\\\"> & {\\n  payload?: TPayload;\\n};\\n\\nexport function createDecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n>(args: {\\n  decisionId: string;\\n  action: { id: string; label: string };\\n  payload?: TPayload;\\n}): DecisionResult<TPayload> {\\n  return {\\n    kind: \\\"decision\\\",\\n    version: 1,\\n    decisionId: args.decisionId,\\n    actionId: args.action.id,\\n    actionLabel: args.action.label,\\n    at: new Date().toISOString(),\\n    payload: args.payload,\\n  };\\n}\\n\\nexport const ActionButtonsPropsSchema = z.object({\\n  actions: z.array(ActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n  className: z.string().optional(),\\n});\\n\\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\\n  actions: z.array(SerializableActionSchema),\\n}).omit({ className: true });\\n\\nexport interface ActionsConfig {\\n  items: Action[];\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  confirmTimeout?: number;\\n}\\n\\nexport const SerializableActionsConfigSchema = z.object({\\n  items: z.array(SerializableActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n});\\n\\nexport type SerializableActionsConfig = z.infer<\\n  typeof SerializableActionsConfigSchema\\n>;\\n\\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/utils.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/utils.ts\",\n      \"content\": \"export function formatRelativeTime(iso: string): string {\\n  const seconds = Math.round((Date.now() - new Date(iso).getTime()) / 1000);\\n  if (seconds < 60) return `${seconds}s`;\\n  if (seconds < 3600) return `${Math.round(seconds / 60)}m`;\\n  if (seconds < 86400) return `${Math.round(seconds / 3600)}h`;\\n  if (seconds < 604800) return `${Math.round(seconds / 86400)}d`;\\n  return `${Math.round(seconds / 604800)}w`;\\n}\\n\\nexport function formatCount(count: number): string {\\n  if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;\\n  if (count >= 1_000) return `${(count / 1_000).toFixed(1)}K`;\\n  return String(count);\\n}\\n\\nexport function getDomain(url: string): string {\\n  try {\\n    return new URL(url).hostname.replace(/^www\\\\./, \\\"\\\");\\n  } catch {\\n    return \\\"\\\";\\n  }\\n}\\n\\nexport function prefersReducedMotion(): boolean {\\n  return (\\n    typeof window !== \\\"undefined\\\" &&\\n    window.matchMedia?.(\\\"(prefers-reduced-motion: reduce)\\\").matches\\n  );\\n}\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/link-preview.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"link-preview\",\n  \"type\": \"registry:block\",\n  \"title\": \"Link Preview\",\n  \"description\": \"Rich link previews with OG data.\",\n  \"dependencies\": [\n    \"lucide-react\",\n    \"zod\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/link-preview/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/link-preview/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n */\\n\\\"use client\\\";\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/link-preview/index.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/link-preview/index.ts\",\n      \"content\": \"export { LinkPreview } from \\\"./link-preview\\\";\\nexport type { LinkPreviewProps } from \\\"./link-preview\\\";\\nexport type { SerializableLinkPreview } from \\\"./schema\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/link-preview/link-preview.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/link-preview/link-preview.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { Globe } from \\\"lucide-react\\\";\\nimport { cn } from \\\"./_adapter\\\";\\n\\nimport {\\n  RATIO_CLASS_MAP,\\n  getFitClass,\\n  openSafeNavigationHref,\\n  sanitizeHref,\\n} from \\\"../shared/media\\\";\\nimport type { SerializableLinkPreview } from \\\"./schema\\\";\\n\\nconst FALLBACK_LOCALE = \\\"en-US\\\";\\nconst CONTENT_SPACING = \\\"px-5 py-4 gap-2\\\";\\n\\nexport interface LinkPreviewProps extends SerializableLinkPreview {\\n  className?: string;\\n  onNavigate?: (href: string, preview: SerializableLinkPreview) => void;\\n}\\n\\nexport function LinkPreview(props: LinkPreviewProps) {\\n  const { className, onNavigate, ...serializable } = props;\\n\\n  const {\\n    id,\\n    href: rawHref,\\n    title,\\n    description,\\n    image,\\n    domain,\\n    favicon,\\n    ratio = \\\"16:9\\\",\\n    fit = \\\"cover\\\",\\n    locale: providedLocale,\\n  } = serializable;\\n\\n  const locale = providedLocale ?? FALLBACK_LOCALE;\\n  const sanitizedHref = sanitizeHref(rawHref);\\n\\n  const previewData: SerializableLinkPreview = {\\n    ...serializable,\\n    href: sanitizedHref ?? rawHref,\\n    locale,\\n  };\\n\\n  const handleClick = () => {\\n    if (!sanitizedHref) return;\\n    if (onNavigate) {\\n      onNavigate(sanitizedHref, previewData);\\n    } else {\\n      openSafeNavigationHref(sanitizedHref);\\n    }\\n  };\\n\\n  return (\\n    <article\\n      className={cn(\\\"relative w-full max-w-md min-w-80\\\", className)}\\n      lang={locale}\\n      data-tool-ui-id={id}\\n      data-slot=\\\"link-preview\\\"\\n    >\\n      <div\\n        className={cn(\\n          \\\"group @container relative isolate flex w-full min-w-0 flex-col overflow-hidden rounded-xl\\\",\\n          \\\"border-border bg-card border text-sm shadow-xs\\\",\\n          sanitizedHref && \\\"cursor-pointer\\\",\\n        )}\\n        onClick={sanitizedHref ? handleClick : undefined}\\n        role={sanitizedHref ? \\\"link\\\" : undefined}\\n        tabIndex={sanitizedHref ? 0 : undefined}\\n        onKeyDown={\\n          sanitizedHref\\n            ? (e) => {\\n                if (e.key === \\\"Enter\\\" || e.key === \\\" \\\") {\\n                  e.preventDefault();\\n                  handleClick();\\n                }\\n              }\\n            : undefined\\n        }\\n      >\\n        <div className=\\\"flex flex-col\\\">\\n          {image && (\\n            <div\\n              className={cn(\\n                \\\"bg-muted relative w-full overflow-hidden\\\",\\n                ratio !== \\\"auto\\\" ? RATIO_CLASS_MAP[ratio] : \\\"aspect-[5/3]\\\",\\n              )}\\n            >\\n              <img\\n                src={image}\\n                alt=\\\"\\\"\\n                loading=\\\"lazy\\\"\\n                decoding=\\\"async\\\"\\n                className={cn(\\n                  \\\"absolute inset-0 h-full w-full\\\",\\n                  getFitClass(fit),\\n                  \\\"object-center transition-transform duration-200 group-hover:scale-[1.01]\\\",\\n                )}\\n              />\\n            </div>\\n          )}\\n          <div className={cn(\\\"flex flex-col\\\", CONTENT_SPACING)}>\\n            {domain && (\\n              <div className=\\\"text-muted-foreground flex items-center gap-2 text-xs\\\">\\n                {favicon ? (\\n                  <img\\n                    src={favicon}\\n                    alt=\\\"\\\"\\n                    aria-hidden=\\\"true\\\"\\n                    width={16}\\n                    height={16}\\n                    className=\\\"size-4 rounded-full object-cover\\\"\\n                    loading=\\\"lazy\\\"\\n                    decoding=\\\"async\\\"\\n                  />\\n                ) : (\\n                  <div className=\\\"border-border/60 bg-muted flex size-4 shrink-0 items-center justify-center rounded-full border\\\">\\n                    <Globe className=\\\"h-2.5 w-2.5\\\" aria-hidden=\\\"true\\\" />\\n                  </div>\\n                )}\\n                <span>{domain}</span>\\n              </div>\\n            )}\\n            {title && (\\n              <h3 className=\\\"text-foreground text-base font-medium text-pretty\\\">\\n                <span className=\\\"line-clamp-2\\\">{title}</span>\\n              </h3>\\n            )}\\n            {description && (\\n              <p className=\\\"text-muted-foreground leading-snug text-pretty\\\">\\n                <span className=\\\"line-clamp-2\\\">{description}</span>\\n              </p>\\n            )}\\n          </div>\\n        </div>\\n      </div>\\n    </article>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/link-preview/README.md\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/link-preview/README.md\",\n      \"content\": \"# Link Preview\\n\\nImplementation for the \\\"link-preview\\\" Tool UI surface.\\n\\n## Files\\n\\n- public exports: components/tool-ui/link-preview/index.ts\\n- serializable schema + parse helpers: components/tool-ui/link-preview/schema.ts\\n\\n## Companion assets\\n\\n- Docs page: app/docs/link-preview/content.mdx\\n- Preset payload: lib/presets/link-preview.ts\\n\\n## Quick check\\n\\nRun this after edits:\\n\\npnpm test\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/link-preview/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/link-preview/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\nimport {\\n  ToolUIIdSchema,\\n  ToolUIReceiptSchema,\\n  ToolUIRoleSchema,\\n} from \\\"../shared/schema\\\";\\n\\nimport { AspectRatioSchema, MediaFitSchema } from \\\"../shared/media\\\";\\n\\nexport const SerializableLinkPreviewSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n  href: z.url(),\\n  title: z.string().optional(),\\n  description: z.string().optional(),\\n  image: z.url().optional(),\\n  domain: z.string().optional(),\\n  favicon: z.url().optional(),\\n  ratio: AspectRatioSchema.optional(),\\n  fit: MediaFitSchema.optional(),\\n  createdAt: z.string().datetime().optional(),\\n  locale: z.string().optional(),\\n});\\n\\nexport type SerializableLinkPreview = z.infer<\\n  typeof SerializableLinkPreviewSchema\\n>;\\n\\nconst SerializableLinkPreviewSchemaContract = defineToolUiContract(\\n  \\\"LinkPreview\\\",\\n  SerializableLinkPreviewSchema,\\n);\\n\\nexport const parseSerializableLinkPreview: (\\n  input: unknown,\\n) => SerializableLinkPreview = SerializableLinkPreviewSchemaContract.parse;\\n\\nexport const safeParseSerializableLinkPreview: (\\n  input: unknown,\\n) => SerializableLinkPreview | null =\\n  SerializableLinkPreviewSchemaContract.safeParse;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/aspect-ratio.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/aspect-ratio.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nexport const AspectRatioSchema = z\\n  .enum([\\\"auto\\\", \\\"1:1\\\", \\\"4:3\\\", \\\"16:9\\\", \\\"9:16\\\"])\\n  .default(\\\"auto\\\");\\n\\nexport type AspectRatio = z.infer<typeof AspectRatioSchema>;\\n\\nexport const MediaFitSchema = z.enum([\\\"cover\\\", \\\"contain\\\"]).default(\\\"cover\\\");\\n\\nexport type MediaFit = z.infer<typeof MediaFitSchema>;\\n\\nexport const RATIO_CLASS_MAP: Record<AspectRatio, string> = {\\n  auto: \\\"\\\",\\n  \\\"1:1\\\": \\\"aspect-square\\\",\\n  \\\"4:3\\\": \\\"aspect-[4/3]\\\",\\n  \\\"16:9\\\": \\\"aspect-video\\\",\\n  \\\"9:16\\\": \\\"aspect-[9/16]\\\",\\n};\\n\\nexport function getRatioClass(ratio: AspectRatio): string {\\n  return RATIO_CLASS_MAP[ratio];\\n}\\n\\nexport function getFitClass(fit: MediaFit): string {\\n  return fit === \\\"cover\\\" ? \\\"object-cover\\\" : \\\"object-contain\\\";\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/format-utils.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/format-utils.ts\",\n      \"content\": \"/**\\n * Format duration in milliseconds to human-readable string.\\n * @example formatDuration(128000) => \\\"2:08\\\"\\n * @example formatDuration(3661000) => \\\"1:01:01\\\"\\n */\\nexport function formatDuration(durationMs: number): string {\\n  const totalSeconds = Math.round(durationMs / 1000);\\n  const hours = Math.floor(totalSeconds / 3600);\\n  const minutes = Math.floor((totalSeconds % 3600) / 60);\\n  const seconds = totalSeconds % 60;\\n\\n  if (hours > 0) {\\n    return `${hours}:${minutes.toString().padStart(2, \\\"0\\\")}:${seconds\\n      .toString()\\n      .padStart(2, \\\"0\\\")}`;\\n  }\\n  return `${minutes}:${seconds.toString().padStart(2, \\\"0\\\")}`;\\n}\\n\\n/**\\n * Format file size in bytes to human-readable string.\\n * @example formatFileSize(1024) => \\\"1 KB\\\"\\n * @example formatFileSize(1536000) => \\\"1.5 MB\\\"\\n */\\nexport function formatFileSize(bytes: number): string {\\n  if (bytes < 1024) return `${bytes} B`;\\n  const units = [\\\"KB\\\", \\\"MB\\\", \\\"GB\\\"];\\n  let size = bytes / 1024;\\n  let unit = 0;\\n  while (size >= 1024 && unit < units.length - 1) {\\n    size /= 1024;\\n    unit += 1;\\n  }\\n  return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unit]}`;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/index.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/index.ts\",\n      \"content\": \"export {\\n  AspectRatioSchema,\\n  MediaFitSchema,\\n  RATIO_CLASS_MAP,\\n  getRatioClass,\\n  getFitClass,\\n  type AspectRatio,\\n  type MediaFit,\\n} from \\\"./aspect-ratio\\\";\\n\\nexport { OVERLAY_GRADIENT } from \\\"./overlay-gradient\\\";\\n\\nexport { formatDuration, formatFileSize } from \\\"./format-utils\\\";\\n\\nexport { sanitizeHref } from \\\"./sanitize-href\\\";\\nexport {\\n  resolveSafeNavigationHref,\\n  openSafeNavigationHref,\\n} from \\\"./safe-navigation\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/overlay-gradient.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/overlay-gradient.ts\",\n      \"content\": \"/**\\n * Eased gradient for hover overlays on media elements.\\n * Creates a smooth fade from opaque black at top to transparent.\\n *\\n * @see https://larsenwork.com/easing-gradients/\\n */\\nexport const OVERLAY_GRADIENT = `linear-gradient(\\n  to bottom,\\n  hsl(0, 0%, 0%) 0%,\\n  hsla(0, 0%, 0%, 0.987) 8.3%,\\n  hsla(0, 0%, 0%, 0.951) 16.6%,\\n  hsla(0, 0%, 0%, 0.896) 24.6%,\\n  hsla(0, 0%, 0%, 0.825) 32.5%,\\n  hsla(0, 0%, 0%, 0.741) 40.1%,\\n  hsla(0, 0%, 0%, 0.648) 47.6%,\\n  hsla(0, 0%, 0%, 0.55) 54.8%,\\n  hsla(0, 0%, 0%, 0.45) 61.7%,\\n  hsla(0, 0%, 0%, 0.352) 68.3%,\\n  hsla(0, 0%, 0%, 0.259) 74.5%,\\n  hsla(0, 0%, 0%, 0.175) 80.4%,\\n  hsla(0, 0%, 0%, 0.104) 86%,\\n  hsla(0, 0%, 0%, 0.049) 91.1%,\\n  hsla(0, 0%, 0%, 0.013) 95.8%,\\n  hsla(0, 0%, 0%, 0) 100%\\n)` as const;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/safe-navigation.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/safe-navigation.ts\",\n      \"content\": \"import { sanitizeHref } from \\\"./sanitize-href\\\";\\n\\nexport function resolveSafeNavigationHref(\\n  ...candidates: Array<string | null | undefined>\\n): string | undefined {\\n  for (const candidate of candidates) {\\n    const safeHref = sanitizeHref(candidate ?? undefined);\\n    if (safeHref) {\\n      return safeHref;\\n    }\\n  }\\n\\n  return undefined;\\n}\\n\\nexport function openSafeNavigationHref(href: string | undefined): boolean {\\n  if (!href || typeof window === \\\"undefined\\\") {\\n    return false;\\n  }\\n\\n  window.open(href, \\\"_blank\\\", \\\"noopener,noreferrer\\\");\\n  return true;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/sanitize-href.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/sanitize-href.ts\",\n      \"content\": \"/**\\n * Sanitize a URL to ensure it's safe for use in href attributes.\\n * Allows:\\n * - Absolute http(s) URLs\\n * - Relative URLs (/path, ./path, ../path, ?query, #hash)\\n *\\n * @returns The sanitized URL string, or undefined if invalid/unsafe\\n */\\nexport function sanitizeHref(href?: string): string | undefined {\\n  if (!href) return undefined;\\n  const candidate = href.trim();\\n  if (!candidate) return undefined;\\n\\n  if (\\n    candidate.startsWith(\\\"/\\\") ||\\n    candidate.startsWith(\\\"./\\\") ||\\n    candidate.startsWith(\\\"../\\\") ||\\n    candidate.startsWith(\\\"?\\\") ||\\n    candidate.startsWith(\\\"#\\\")\\n  ) {\\n    if (candidate.startsWith(\\\"//\\\")) return undefined;\\n    // eslint-disable-next-line no-control-regex -- intentionally matching control characters\\n    if (/[\\\\u0000-\\\\u001F\\\\u007F]/.test(candidate)) return undefined;\\n    return candidate;\\n  }\\n\\n  try {\\n    const url = new URL(candidate);\\n    if (url.protocol === \\\"http:\\\" || url.protocol === \\\"https:\\\") {\\n      return url.toString();\\n    }\\n  } catch {\\n    return undefined;\\n  }\\n  return undefined;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { ReactNode } from \\\"react\\\";\\n\\n/**\\n * Tool UI conventions:\\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\\n * - Schema: `SerializableXSchema`\\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\\n * - Root attrs: `data-tool-ui-id` + `data-slot`\\n */\\n\\n/**\\n * Schema for tool UI identity.\\n *\\n * Every tool UI should have a unique identifier that:\\n * - Is stable across re-renders\\n * - Is meaningful (not auto-generated)\\n * - Is unique within the conversation\\n *\\n * Format recommendation: `{component-type}-{semantic-identifier}`\\n * Examples: \\\"data-table-expenses-q3\\\", \\\"option-list-deploy-target\\\"\\n */\\nexport const ToolUIIdSchema = z.string().min(1);\\n\\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\\n\\n/**\\n * Primary role of a Tool UI surface in a chat context.\\n */\\nexport const ToolUIRoleSchema = z.enum([\\n  \\\"information\\\",\\n  \\\"decision\\\",\\n  \\\"control\\\",\\n  \\\"state\\\",\\n  \\\"composite\\\",\\n]);\\n\\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\\n\\nexport const ToolUIReceiptOutcomeSchema = z.enum([\\n  \\\"success\\\",\\n  \\\"partial\\\",\\n  \\\"failed\\\",\\n  \\\"cancelled\\\",\\n]);\\n\\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\\n\\n/**\\n * Optional receipt metadata: a durable summary of an outcome.\\n */\\nexport const ToolUIReceiptSchema = z.object({\\n  outcome: ToolUIReceiptOutcomeSchema,\\n  summary: z.string().min(1),\\n  identifiers: z.record(z.string(), z.string()).optional(),\\n  at: z.string().datetime(),\\n});\\n\\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\\n\\n/**\\n * Base schema for Tool UI payloads (id + optional role/receipt).\\n */\\nexport const ToolUISurfaceSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n});\\n\\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\\n\\nexport const ActionSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  /**\\n   * Canonical narration the assistant can use after this action is taken.\\n   *\\n   * Example: \\\"I exported the table as CSV.\\\" / \\\"I opened the link in a new tab.\\\"\\n   */\\n  sentence: z.string().optional(),\\n  confirmLabel: z.string().optional(),\\n  variant: z\\n    .enum([\\\"default\\\", \\\"destructive\\\", \\\"secondary\\\", \\\"ghost\\\", \\\"outline\\\"])\\n    .optional(),\\n  icon: z.custom<ReactNode>().optional(),\\n  loading: z.boolean().optional(),\\n  disabled: z.boolean().optional(),\\n  shortcut: z.string().optional(),\\n});\\n\\nexport type Action = z.infer<typeof ActionSchema>;\\nexport type LocalAction = Action;\\nexport type DecisionAction = Action;\\n\\nexport const DecisionResultSchema = z.object({\\n  kind: z.literal(\\\"decision\\\"),\\n  version: z.literal(1),\\n  decisionId: z.string().min(1),\\n  actionId: z.string().min(1),\\n  actionLabel: z.string().min(1),\\n  at: z.string().datetime(),\\n  payload: z.record(z.string(), z.unknown()).optional(),\\n});\\n\\nexport type DecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n> = Omit<z.infer<typeof DecisionResultSchema>, \\\"payload\\\"> & {\\n  payload?: TPayload;\\n};\\n\\nexport function createDecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n>(args: {\\n  decisionId: string;\\n  action: { id: string; label: string };\\n  payload?: TPayload;\\n}): DecisionResult<TPayload> {\\n  return {\\n    kind: \\\"decision\\\",\\n    version: 1,\\n    decisionId: args.decisionId,\\n    actionId: args.action.id,\\n    actionLabel: args.action.label,\\n    at: new Date().toISOString(),\\n    payload: args.payload,\\n  };\\n}\\n\\nexport const ActionButtonsPropsSchema = z.object({\\n  actions: z.array(ActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n  className: z.string().optional(),\\n});\\n\\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\\n  actions: z.array(SerializableActionSchema),\\n}).omit({ className: true });\\n\\nexport interface ActionsConfig {\\n  items: Action[];\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  confirmTimeout?: number;\\n}\\n\\nexport const SerializableActionsConfigSchema = z.object({\\n  items: z.array(SerializableActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n});\\n\\nexport type SerializableActionsConfig = z.infer<\\n  typeof SerializableActionsConfigSchema\\n>;\\n\\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/linkedin-post.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"linkedin-post\",\n  \"type\": \"registry:block\",\n  \"title\": \"Linkedin Post\",\n  \"description\": \"Render LinkedIn post previews.\",\n  \"dependencies\": [\n    \"lucide-react\",\n    \"zod\"\n  ],\n  \"registryDependencies\": [\n    \"button\",\n    \"tooltip\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/linkedin-post/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/linkedin-post/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn      → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n *   Button  → shadcn/ui Button\\n *   Tooltip → shadcn/ui Tooltip\\n */\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport { Button } from \\\"@/components/ui/button\\\";\\nexport {\\n  Tooltip,\\n  TooltipContent,\\n  TooltipProvider,\\n  TooltipTrigger,\\n} from \\\"@/components/ui/tooltip\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/linkedin-post/index.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/linkedin-post/index.ts\",\n      \"content\": \"export { LinkedInPost } from \\\"./linkedin-post\\\";\\nexport type { LinkedInPostProps } from \\\"./linkedin-post\\\";\\nexport type {\\n  LinkedInPostData,\\n  LinkedInPostAuthor,\\n  LinkedInPostMedia,\\n  LinkedInPostLinkPreview,\\n  LinkedInPostStats,\\n} from \\\"./schema\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/linkedin-post/linkedin-post.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/linkedin-post/linkedin-post.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport * as React from \\\"react\\\";\\nimport { ThumbsUp, Share } from \\\"lucide-react\\\";\\nimport {\\n  cn,\\n  Button,\\n  Tooltip,\\n  TooltipContent,\\n  TooltipProvider,\\n  TooltipTrigger,\\n} from \\\"./_adapter\\\";\\nimport { formatCount, formatRelativeTime, getDomain } from \\\"../shared/utils\\\";\\n\\nimport { resolveSafeNavigationHref } from \\\"../shared/media\\\";\\nimport type {\\n  LinkedInPostData,\\n  LinkedInPostMedia,\\n  LinkedInPostLinkPreview,\\n} from \\\"./schema\\\";\\n\\nconst TEXT_PREVIEW_LENGTH = 280;\\n\\nexport interface LinkedInPostProps {\\n  post: LinkedInPostData;\\n  className?: string;\\n  onAction?: (action: string, post: LinkedInPostData) => void;\\n}\\n\\nfunction LinkedInLogo({ className }: { className?: string }) {\\n  return (\\n    <svg\\n      viewBox=\\\"0 0 72 72\\\"\\n      className={className}\\n      role=\\\"img\\\"\\n      aria-label=\\\"LinkedIn logo\\\"\\n    >\\n      <g fill=\\\"none\\\" fillRule=\\\"evenodd\\\">\\n        <path\\n          d=\\\"M8 72h56c4.42 0 8-3.58 8-8V8c0-4.42-3.58-8-8-8H8C3.58 0 0 3.58 0 8v56c0 4.42 3.58 8 8 8z\\\"\\n          fill=\\\"currentColor\\\"\\n        />\\n        <path\\n          d=\\\"M62 62H51.3V43.8c0-4.98-1.9-7.78-5.83-7.78-4.3 0-6.54 2.9-6.54 7.78V62H28.63V27.33h10.3v4.67c0 0 3.1-5.73 10.45-5.73 7.36 0 12.62 4.5 12.62 13.8V62zM16.35 22.8c-3.5 0-6.35-2.86-6.35-6.4 0-3.52 2.85-6.4 6.35-6.4 3.5 0 6.35 2.88 6.35 6.4 0 3.54-2.85 6.4-6.35 6.4zM11.03 62h10.74V27.33H11.03V62z\\\"\\n          fill=\\\"#FFF\\\"\\n        />\\n      </g>\\n    </svg>\\n  );\\n}\\n\\nfunction Header({\\n  author,\\n  createdAt,\\n}: {\\n  author: LinkedInPostData[\\\"author\\\"];\\n  createdAt?: string;\\n}) {\\n  return (\\n    <header className=\\\"flex items-start gap-3\\\">\\n      <img\\n        src={author.avatarUrl}\\n        alt={`${author.name} avatar`}\\n        width={48}\\n        height={48}\\n        className=\\\"size-12 rounded-full object-cover\\\"\\n      />\\n      <div className=\\\"flex min-w-0 flex-1 flex-col leading-tight\\\">\\n        <span className=\\\"text-sm font-semibold\\\">{author.name}</span>\\n        {author.headline && (\\n          <span className=\\\"text-muted-foreground line-clamp-1 text-xs\\\">\\n            {author.headline}\\n          </span>\\n        )}\\n        {createdAt && (\\n          <div className=\\\"text-muted-foreground mt-0.5 flex items-center gap-1 text-xs\\\">\\n            <span>{formatRelativeTime(createdAt)}</span>\\n            <span>·</span>\\n            <span>Edited</span>\\n          </div>\\n        )}\\n      </div>\\n      <LinkedInLogo className=\\\"size-5 text-[#0077b5]\\\" />\\n    </header>\\n  );\\n}\\n\\nfunction PostBody({ text }: { text?: string }) {\\n  const [isExpanded, setIsExpanded] = React.useState(false);\\n  const shouldTruncate = text && text.length > TEXT_PREVIEW_LENGTH;\\n\\n  if (!text) return null;\\n\\n  return (\\n    <div className=\\\"text-sm leading-relaxed text-pretty wrap-break-word whitespace-pre-wrap\\\">\\n      {shouldTruncate && !isExpanded ? (\\n        <>\\n          {text.slice(0, TEXT_PREVIEW_LENGTH)}\\n          ...\\n          <button\\n            onClick={() => setIsExpanded(true)}\\n            className=\\\"text-muted-foreground hover:text-foreground ml-1 font-medium hover:underline\\\"\\n          >\\n            see more\\n          </button>\\n        </>\\n      ) : (\\n        text\\n      )}\\n    </div>\\n  );\\n}\\n\\nfunction PostMedia({ media }: { media: LinkedInPostMedia }) {\\n  return (\\n    <div className=\\\"overflow-hidden rounded-lg\\\">\\n      {media.type === \\\"image\\\" ? (\\n        <img\\n          src={media.url}\\n          alt={media.alt}\\n          className=\\\"w-full object-cover\\\"\\n          style={{ aspectRatio: \\\"16/9\\\" }}\\n          loading=\\\"lazy\\\"\\n        />\\n      ) : (\\n        <video\\n          src={media.url}\\n          controls\\n          playsInline\\n          className=\\\"w-full object-contain\\\"\\n          style={{ aspectRatio: \\\"16/9\\\" }}\\n        />\\n      )}\\n    </div>\\n  );\\n}\\n\\nfunction PostLinkPreview({ preview }: { preview: LinkedInPostLinkPreview }) {\\n  const href = resolveSafeNavigationHref(preview.url);\\n  const domain = preview.domain ?? getDomain(preview.url);\\n  const content = (\\n    <>\\n      {preview.imageUrl && (\\n        <img\\n          src={preview.imageUrl}\\n          alt=\\\"\\\"\\n          className=\\\"h-40 w-full object-cover\\\"\\n          loading=\\\"lazy\\\"\\n        />\\n      )}\\n      <div className=\\\"p-3\\\">\\n        {preview.title && (\\n          <div className=\\\"line-clamp-2 font-medium text-pretty\\\">\\n            {preview.title}\\n          </div>\\n        )}\\n        {domain && (\\n          <div className=\\\"text-muted-foreground mt-1 text-xs\\\">{domain}</div>\\n        )}\\n      </div>\\n    </>\\n  );\\n\\n  if (!href) {\\n    return (\\n      <div className=\\\"block overflow-hidden rounded-lg border\\\">{content}</div>\\n    );\\n  }\\n\\n  return (\\n    <a\\n      href={href}\\n      target=\\\"_blank\\\"\\n      rel=\\\"noopener noreferrer\\\"\\n      className=\\\"hover:bg-muted/50 block overflow-hidden rounded-lg border transition-colors\\\"\\n    >\\n      {content}\\n    </a>\\n  );\\n}\\n\\nfunction ActionButton({\\n  icon: Icon,\\n  label,\\n  count,\\n  active,\\n  hoverColor,\\n  activeColor,\\n  onClick,\\n}: {\\n  icon: React.ComponentType<{ className?: string }>;\\n  label: string;\\n  count?: number;\\n  active?: boolean;\\n  hoverColor: string;\\n  activeColor?: string;\\n  onClick: () => void;\\n}) {\\n  return (\\n    <Tooltip>\\n      <TooltipTrigger asChild>\\n        <Button\\n          variant=\\\"ghost\\\"\\n          size=\\\"sm\\\"\\n          onClick={(e) => {\\n            e.stopPropagation();\\n            onClick();\\n          }}\\n          className={cn(\\n            \\\"h-auto gap-1.5 px-3 py-2\\\",\\n            hoverColor,\\n            active && activeColor,\\n          )}\\n          aria-label={label}\\n        >\\n          <Icon className=\\\"size-4\\\" />\\n          <span className=\\\"text-xs font-medium\\\">{label}</span>\\n          {count !== undefined && (\\n            <span className=\\\"text-muted-foreground text-xs\\\">\\n              ({formatCount(count)})\\n            </span>\\n          )}\\n        </Button>\\n      </TooltipTrigger>\\n      <TooltipContent>{label}</TooltipContent>\\n    </Tooltip>\\n  );\\n}\\n\\nfunction PostActions({\\n  stats,\\n  onAction,\\n}: {\\n  stats?: LinkedInPostData[\\\"stats\\\"];\\n  onAction: (action: string) => void;\\n}) {\\n  return (\\n    <TooltipProvider delayDuration={300}>\\n      <div className=\\\"mt-1 flex items-center gap-1 border-t pt-1.5\\\">\\n        <ActionButton\\n          icon={ThumbsUp}\\n          label=\\\"Like\\\"\\n          active={stats?.isLiked}\\n          hoverColor=\\\"hover:bg-muted\\\"\\n          activeColor=\\\"text-blue-600 fill-blue-600\\\"\\n          onClick={() => onAction(\\\"like\\\")}\\n        />\\n        <ActionButton\\n          icon={Share}\\n          label=\\\"Share\\\"\\n          hoverColor=\\\"hover:bg-muted\\\"\\n          onClick={() => onAction(\\\"share\\\")}\\n        />\\n      </div>\\n    </TooltipProvider>\\n  );\\n}\\n\\nexport function LinkedInPost({ post, className, onAction }: LinkedInPostProps) {\\n  return (\\n    <div\\n      className={cn(\\\"flex max-w-xl flex-col gap-3\\\", className)}\\n      data-tool-ui-id={post.id}\\n      data-slot=\\\"linkedin-post\\\"\\n    >\\n      <article className=\\\"bg-card flex flex-col gap-3 rounded-lg border p-3 shadow-sm\\\">\\n        <Header author={post.author} createdAt={post.createdAt} />\\n        <PostBody text={post.text} />\\n\\n        {post.media && <PostMedia media={post.media} />}\\n\\n        {post.linkPreview && !post.media && (\\n          <PostLinkPreview preview={post.linkPreview} />\\n        )}\\n\\n        <PostActions\\n          stats={post.stats}\\n          onAction={(action) => onAction?.(action, post)}\\n        />\\n      </article>\\n    </div>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/linkedin-post/README.md\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/linkedin-post/README.md\",\n      \"content\": \"# Linkedin Post\\n\\nImplementation for the \\\"linkedin-post\\\" Tool UI surface.\\n\\n## Files\\n\\n- public exports: components/tool-ui/linkedin-post/index.ts\\n- serializable schema + parse helpers: components/tool-ui/linkedin-post/schema.ts\\n\\n## Companion assets\\n\\n- Docs page: app/docs/social-post/content.mdx\\n- Preset payload: lib/presets/linkedin-post.ts\\n\\n## Quick check\\n\\nRun this after edits:\\n\\npnpm test\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/linkedin-post/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/linkedin-post/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\n\\nexport const LinkedInPostAuthorSchema = z.object({\\n  name: z.string(),\\n  avatarUrl: z.string(),\\n  headline: z.string().optional(),\\n});\\n\\nexport const LinkedInPostMediaSchema = z.object({\\n  type: z.enum([\\\"image\\\", \\\"video\\\"]),\\n  url: z.string(),\\n  alt: z.string(),\\n});\\n\\nexport const LinkedInPostLinkPreviewSchema = z.object({\\n  url: z.string(),\\n  title: z.string().optional(),\\n  description: z.string().optional(),\\n  imageUrl: z.string().optional(),\\n  domain: z.string().optional(),\\n});\\n\\nexport const LinkedInPostStatsSchema = z.object({\\n  likes: z.number().optional(),\\n  isLiked: z.boolean().optional(),\\n});\\n\\nexport const SerializableLinkedInPostSchema = z.object({\\n  id: z.string(),\\n  author: LinkedInPostAuthorSchema,\\n  text: z.string().optional(),\\n  media: LinkedInPostMediaSchema.optional(),\\n  linkPreview: LinkedInPostLinkPreviewSchema.optional(),\\n  stats: LinkedInPostStatsSchema.optional(),\\n  createdAt: z.string().optional(),\\n});\\n\\nexport type LinkedInPostData = z.infer<typeof SerializableLinkedInPostSchema>;\\n\\nexport type LinkedInPostAuthor = z.infer<typeof LinkedInPostAuthorSchema>;\\nexport type LinkedInPostMedia = z.infer<typeof LinkedInPostMediaSchema>;\\nexport type LinkedInPostLinkPreview = z.infer<\\n  typeof LinkedInPostLinkPreviewSchema\\n>;\\nexport type LinkedInPostStats = z.infer<typeof LinkedInPostStatsSchema>;\\n\\nconst SerializableLinkedInPostSchemaContract = defineToolUiContract(\\n  \\\"LinkedInPost\\\",\\n  SerializableLinkedInPostSchema,\\n);\\n\\nexport const parseSerializableLinkedInPost: (\\n  input: unknown,\\n) => LinkedInPostData = SerializableLinkedInPostSchemaContract.parse;\\n\\nexport const safeParseSerializableLinkedInPost: (\\n  input: unknown,\\n) => LinkedInPostData | null = SerializableLinkedInPostSchemaContract.safeParse;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/aspect-ratio.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/aspect-ratio.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nexport const AspectRatioSchema = z\\n  .enum([\\\"auto\\\", \\\"1:1\\\", \\\"4:3\\\", \\\"16:9\\\", \\\"9:16\\\"])\\n  .default(\\\"auto\\\");\\n\\nexport type AspectRatio = z.infer<typeof AspectRatioSchema>;\\n\\nexport const MediaFitSchema = z.enum([\\\"cover\\\", \\\"contain\\\"]).default(\\\"cover\\\");\\n\\nexport type MediaFit = z.infer<typeof MediaFitSchema>;\\n\\nexport const RATIO_CLASS_MAP: Record<AspectRatio, string> = {\\n  auto: \\\"\\\",\\n  \\\"1:1\\\": \\\"aspect-square\\\",\\n  \\\"4:3\\\": \\\"aspect-[4/3]\\\",\\n  \\\"16:9\\\": \\\"aspect-video\\\",\\n  \\\"9:16\\\": \\\"aspect-[9/16]\\\",\\n};\\n\\nexport function getRatioClass(ratio: AspectRatio): string {\\n  return RATIO_CLASS_MAP[ratio];\\n}\\n\\nexport function getFitClass(fit: MediaFit): string {\\n  return fit === \\\"cover\\\" ? \\\"object-cover\\\" : \\\"object-contain\\\";\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/format-utils.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/format-utils.ts\",\n      \"content\": \"/**\\n * Format duration in milliseconds to human-readable string.\\n * @example formatDuration(128000) => \\\"2:08\\\"\\n * @example formatDuration(3661000) => \\\"1:01:01\\\"\\n */\\nexport function formatDuration(durationMs: number): string {\\n  const totalSeconds = Math.round(durationMs / 1000);\\n  const hours = Math.floor(totalSeconds / 3600);\\n  const minutes = Math.floor((totalSeconds % 3600) / 60);\\n  const seconds = totalSeconds % 60;\\n\\n  if (hours > 0) {\\n    return `${hours}:${minutes.toString().padStart(2, \\\"0\\\")}:${seconds\\n      .toString()\\n      .padStart(2, \\\"0\\\")}`;\\n  }\\n  return `${minutes}:${seconds.toString().padStart(2, \\\"0\\\")}`;\\n}\\n\\n/**\\n * Format file size in bytes to human-readable string.\\n * @example formatFileSize(1024) => \\\"1 KB\\\"\\n * @example formatFileSize(1536000) => \\\"1.5 MB\\\"\\n */\\nexport function formatFileSize(bytes: number): string {\\n  if (bytes < 1024) return `${bytes} B`;\\n  const units = [\\\"KB\\\", \\\"MB\\\", \\\"GB\\\"];\\n  let size = bytes / 1024;\\n  let unit = 0;\\n  while (size >= 1024 && unit < units.length - 1) {\\n    size /= 1024;\\n    unit += 1;\\n  }\\n  return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unit]}`;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/index.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/index.ts\",\n      \"content\": \"export {\\n  AspectRatioSchema,\\n  MediaFitSchema,\\n  RATIO_CLASS_MAP,\\n  getRatioClass,\\n  getFitClass,\\n  type AspectRatio,\\n  type MediaFit,\\n} from \\\"./aspect-ratio\\\";\\n\\nexport { OVERLAY_GRADIENT } from \\\"./overlay-gradient\\\";\\n\\nexport { formatDuration, formatFileSize } from \\\"./format-utils\\\";\\n\\nexport { sanitizeHref } from \\\"./sanitize-href\\\";\\nexport {\\n  resolveSafeNavigationHref,\\n  openSafeNavigationHref,\\n} from \\\"./safe-navigation\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/overlay-gradient.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/overlay-gradient.ts\",\n      \"content\": \"/**\\n * Eased gradient for hover overlays on media elements.\\n * Creates a smooth fade from opaque black at top to transparent.\\n *\\n * @see https://larsenwork.com/easing-gradients/\\n */\\nexport const OVERLAY_GRADIENT = `linear-gradient(\\n  to bottom,\\n  hsl(0, 0%, 0%) 0%,\\n  hsla(0, 0%, 0%, 0.987) 8.3%,\\n  hsla(0, 0%, 0%, 0.951) 16.6%,\\n  hsla(0, 0%, 0%, 0.896) 24.6%,\\n  hsla(0, 0%, 0%, 0.825) 32.5%,\\n  hsla(0, 0%, 0%, 0.741) 40.1%,\\n  hsla(0, 0%, 0%, 0.648) 47.6%,\\n  hsla(0, 0%, 0%, 0.55) 54.8%,\\n  hsla(0, 0%, 0%, 0.45) 61.7%,\\n  hsla(0, 0%, 0%, 0.352) 68.3%,\\n  hsla(0, 0%, 0%, 0.259) 74.5%,\\n  hsla(0, 0%, 0%, 0.175) 80.4%,\\n  hsla(0, 0%, 0%, 0.104) 86%,\\n  hsla(0, 0%, 0%, 0.049) 91.1%,\\n  hsla(0, 0%, 0%, 0.013) 95.8%,\\n  hsla(0, 0%, 0%, 0) 100%\\n)` as const;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/safe-navigation.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/safe-navigation.ts\",\n      \"content\": \"import { sanitizeHref } from \\\"./sanitize-href\\\";\\n\\nexport function resolveSafeNavigationHref(\\n  ...candidates: Array<string | null | undefined>\\n): string | undefined {\\n  for (const candidate of candidates) {\\n    const safeHref = sanitizeHref(candidate ?? undefined);\\n    if (safeHref) {\\n      return safeHref;\\n    }\\n  }\\n\\n  return undefined;\\n}\\n\\nexport function openSafeNavigationHref(href: string | undefined): boolean {\\n  if (!href || typeof window === \\\"undefined\\\") {\\n    return false;\\n  }\\n\\n  window.open(href, \\\"_blank\\\", \\\"noopener,noreferrer\\\");\\n  return true;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/sanitize-href.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/sanitize-href.ts\",\n      \"content\": \"/**\\n * Sanitize a URL to ensure it's safe for use in href attributes.\\n * Allows:\\n * - Absolute http(s) URLs\\n * - Relative URLs (/path, ./path, ../path, ?query, #hash)\\n *\\n * @returns The sanitized URL string, or undefined if invalid/unsafe\\n */\\nexport function sanitizeHref(href?: string): string | undefined {\\n  if (!href) return undefined;\\n  const candidate = href.trim();\\n  if (!candidate) return undefined;\\n\\n  if (\\n    candidate.startsWith(\\\"/\\\") ||\\n    candidate.startsWith(\\\"./\\\") ||\\n    candidate.startsWith(\\\"../\\\") ||\\n    candidate.startsWith(\\\"?\\\") ||\\n    candidate.startsWith(\\\"#\\\")\\n  ) {\\n    if (candidate.startsWith(\\\"//\\\")) return undefined;\\n    // eslint-disable-next-line no-control-regex -- intentionally matching control characters\\n    if (/[\\\\u0000-\\\\u001F\\\\u007F]/.test(candidate)) return undefined;\\n    return candidate;\\n  }\\n\\n  try {\\n    const url = new URL(candidate);\\n    if (url.protocol === \\\"http:\\\" || url.protocol === \\\"https:\\\") {\\n      return url.toString();\\n    }\\n  } catch {\\n    return undefined;\\n  }\\n  return undefined;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/utils.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/utils.ts\",\n      \"content\": \"export function formatRelativeTime(iso: string): string {\\n  const seconds = Math.round((Date.now() - new Date(iso).getTime()) / 1000);\\n  if (seconds < 60) return `${seconds}s`;\\n  if (seconds < 3600) return `${Math.round(seconds / 60)}m`;\\n  if (seconds < 86400) return `${Math.round(seconds / 3600)}h`;\\n  if (seconds < 604800) return `${Math.round(seconds / 86400)}d`;\\n  return `${Math.round(seconds / 604800)}w`;\\n}\\n\\nexport function formatCount(count: number): string {\\n  if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;\\n  if (count >= 1_000) return `${(count / 1_000).toFixed(1)}K`;\\n  return String(count);\\n}\\n\\nexport function getDomain(url: string): string {\\n  try {\\n    return new URL(url).hostname.replace(/^www\\\\./, \\\"\\\");\\n  } catch {\\n    return \\\"\\\";\\n  }\\n}\\n\\nexport function prefersReducedMotion(): boolean {\\n  return (\\n    typeof window !== \\\"undefined\\\" &&\\n    window.matchMedia?.(\\\"(prefers-reduced-motion: reduce)\\\").matches\\n  );\\n}\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/message-draft.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"message-draft\",\n  \"type\": \"registry:block\",\n  \"title\": \"Message Draft\",\n  \"description\": \"Review and confirm drafted messages before sending.\",\n  \"dependencies\": [\n    \"lucide-react\",\n    \"zod\"\n  ],\n  \"registryDependencies\": [\n    \"button\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/message-draft/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/message-draft/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn        → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n *   Button    → shadcn/ui Button\\n */\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport { Button } from \\\"@/components/ui/button\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/message-draft/index.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/message-draft/index.tsx\",\n      \"content\": \"export { MessageDraft } from \\\"./message-draft\\\";\\nexport {\\n  type SerializableMessageDraft,\\n  type SerializableEmailDraft,\\n  type SerializableSlackDraft,\\n  type MessageDraftChannel,\\n  type MessageDraftOutcome,\\n  type SlackTarget,\\n  type MessageDraftProps,\\n} from \\\"./schema\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/message-draft/message-draft.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/message-draft/message-draft.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport * as React from \\\"react\\\";\\nimport { cn, Button } from \\\"./_adapter\\\";\\nimport type {\\n  MessageDraftProps,\\n  SerializableEmailDraft,\\n  SerializableSlackDraft,\\n} from \\\"./schema\\\";\\nimport { ActionButtons } from \\\"../shared/action-buttons\\\";\\nimport type { Action } from \\\"../shared/schema\\\";\\nimport { Check, ChevronDown } from \\\"lucide-react\\\";\\n\\ntype DraftState = \\\"review\\\" | \\\"sending\\\" | \\\"sent\\\" | \\\"cancelled\\\";\\ntype DraftOutcome = MessageDraftProps[\\\"outcome\\\"];\\n\\nconst DEFAULT_GRACE_PERIOD = 5000;\\nconst COLLAPSED_BODY_HEIGHT = 280;\\n\\ninterface RecipientRowProps {\\n  label: string;\\n  recipients: string[];\\n  maxVisible?: number;\\n  muted?: boolean;\\n}\\n\\nfunction RecipientRow({\\n  label,\\n  recipients,\\n  maxVisible = 3,\\n  muted = false,\\n}: RecipientRowProps) {\\n  const visibleRecipients = recipients.slice(0, maxVisible);\\n  const overflowCount = recipients.length - maxVisible;\\n\\n  return (\\n    <tr className=\\\"text-sm\\\">\\n      <td className=\\\"text-muted-foreground w-0 pr-4 pb-1 text-right align-top font-medium whitespace-nowrap\\\">\\n        {label}\\n      </td>\\n      <td className={cn(\\\"pb-1 align-top\\\", muted && \\\"text-muted-foreground\\\")}>\\n        {visibleRecipients.join(\\\", \\\")}\\n        {overflowCount > 0 && (\\n          <span className=\\\"text-muted-foreground\\\"> +{overflowCount} more</span>\\n        )}\\n      </td>\\n    </tr>\\n  );\\n}\\n\\ninterface SingleFieldRowProps {\\n  label: string;\\n  value: string;\\n}\\n\\nfunction SingleFieldRow({ label, value }: SingleFieldRowProps) {\\n  return (\\n    <tr className=\\\"text-sm\\\">\\n      <td className=\\\"text-muted-foreground w-0 pr-4 pb-1 text-right align-top font-medium whitespace-nowrap\\\">\\n        {label}\\n      </td>\\n      <td className=\\\"pb-1 align-top\\\">{value}</td>\\n    </tr>\\n  );\\n}\\n\\ninterface ExpandableBodyProps {\\n  body: string;\\n  isExpanded: boolean;\\n  onNeedsExpansionChange?: (needsExpansion: boolean) => void;\\n}\\n\\nfunction ExpandableBody({\\n  body,\\n  isExpanded,\\n  onNeedsExpansionChange,\\n}: ExpandableBodyProps) {\\n  const [needsExpansion, setNeedsExpansion] = React.useState<boolean | null>(\\n    null,\\n  );\\n  const contentRef = React.useRef<HTMLDivElement>(null);\\n\\n  React.useLayoutEffect(() => {\\n    if (contentRef.current) {\\n      const needs = contentRef.current.scrollHeight > COLLAPSED_BODY_HEIGHT;\\n      setNeedsExpansion(needs);\\n      onNeedsExpansionChange?.(needs);\\n    }\\n  }, [body, onNeedsExpansionChange]);\\n\\n  return (\\n    <div className=\\\"relative\\\">\\n      <div\\n        ref={contentRef}\\n        className={cn(\\n          \\\"overflow-hidden text-sm leading-relaxed\\\",\\n          needsExpansion !== null &&\\n            \\\"transition-[max-height] duration-300 ease-in-out\\\",\\n        )}\\n        style={{\\n          maxHeight:\\n            needsExpansion === null\\n              ? `${COLLAPSED_BODY_HEIGHT}px`\\n              : isExpanded || !needsExpansion\\n                ? `${contentRef.current?.scrollHeight ?? 1000}px`\\n                : `${COLLAPSED_BODY_HEIGHT}px`,\\n        }}\\n      >\\n        <p className=\\\"pt-1 whitespace-pre-wrap\\\">{body}</p>\\n      </div>\\n      {needsExpansion && (\\n        <div\\n          className={cn(\\n            \\\"from-card pointer-events-none absolute inset-x-0 bottom-0 bg-gradient-to-t to-transparent transition-[height] duration-300 ease-in-out\\\",\\n            isExpanded ? \\\"h-0\\\" : \\\"h-12\\\",\\n          )}\\n        />\\n      )}\\n    </div>\\n  );\\n}\\n\\ninterface EmailDraftContentProps {\\n  draft: SerializableEmailDraft;\\n  titleId: string;\\n  isExpanded: boolean;\\n  onNeedsExpansionChange?: (needsExpansion: boolean) => void;\\n}\\n\\nfunction EmailDraftContent({\\n  draft,\\n  titleId,\\n  isExpanded,\\n  onNeedsExpansionChange,\\n}: EmailDraftContentProps) {\\n  return (\\n    <>\\n      <h2 id={titleId} className=\\\"pt-2 text-base leading-tight font-semibold\\\">\\n        {draft.subject}\\n      </h2>\\n\\n      <table className=\\\"w-full\\\">\\n        <tbody>\\n          {draft.from && <SingleFieldRow label=\\\"From\\\" value={draft.from} />}\\n          <RecipientRow label=\\\"To\\\" recipients={draft.to} />\\n          {draft.cc && draft.cc.length > 0 && (\\n            <RecipientRow label=\\\"Cc\\\" recipients={draft.cc} />\\n          )}\\n          {draft.bcc && draft.bcc.length > 0 && (\\n            <RecipientRow label=\\\"Bcc\\\" recipients={draft.bcc} muted />\\n          )}\\n        </tbody>\\n      </table>\\n\\n      <div className=\\\"bg-border -mx-5 h-px\\\" role=\\\"separator\\\" />\\n\\n      <ExpandableBody\\n        body={draft.body}\\n        isExpanded={isExpanded}\\n        onNeedsExpansionChange={onNeedsExpansionChange}\\n      />\\n    </>\\n  );\\n}\\n\\ninterface SlackDraftContentProps {\\n  draft: SerializableSlackDraft;\\n  titleId: string;\\n  isExpanded: boolean;\\n  onNeedsExpansionChange?: (needsExpansion: boolean) => void;\\n}\\n\\nfunction SlackLogo({ className }: { className?: string }) {\\n  return (\\n    <svg className={className} viewBox=\\\"0 0 24 24\\\" aria-hidden=\\\"true\\\">\\n      <path\\n        fill=\\\"#E01E5A\\\"\\n        d=\\\"M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zm1.271 0a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313z\\\"\\n      />\\n      <path\\n        fill=\\\"#36C5F0\\\"\\n        d=\\\"M8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zm0 1.271a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312z\\\"\\n      />\\n      <path\\n        fill=\\\"#2EB67D\\\"\\n        d=\\\"M18.958 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.52 2.521h-2.522V8.834zm-1.271 0a2.528 2.528 0 0 1-2.521 2.521 2.528 2.528 0 0 1-2.521-2.521V2.522A2.528 2.528 0 0 1 15.165 0a2.528 2.528 0 0 1 2.522 2.522v6.312z\\\"\\n      />\\n      <path\\n        fill=\\\"#ECB22E\\\"\\n        d=\\\"M15.165 18.958a2.528 2.528 0 0 1 2.522 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.521-2.52v-2.522h2.521zm0-1.271a2.527 2.527 0 0 1-2.521-2.521 2.526 2.526 0 0 1 2.521-2.521h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.521h-6.313z\\\"\\n      />\\n    </svg>\\n  );\\n}\\n\\nfunction SlackDraftContent({\\n  draft,\\n  titleId,\\n  isExpanded,\\n  onNeedsExpansionChange,\\n}: SlackDraftContentProps) {\\n  const { target } = draft;\\n  const isChannel = target.type === \\\"channel\\\";\\n  const targetDisplay = isChannel\\n    ? `#${target.name}`\\n    : `Message to @${target.name}`;\\n  const memberCount = isChannel ? target.memberCount : undefined;\\n\\n  return (\\n    <>\\n      <div\\n        id={titleId}\\n        className=\\\"flex items-center gap-1.5 text-sm font-medium\\\"\\n      >\\n        <SlackLogo className=\\\"size-4\\\" />\\n        <span>{targetDisplay}</span>\\n        {memberCount !== undefined && (\\n          <span className=\\\"text-muted-foreground ml-auto text-sm font-normal\\\">\\n            {memberCount.toLocaleString()} members\\n          </span>\\n        )}\\n      </div>\\n\\n      <div className=\\\"bg-border -mx-5 h-px\\\" role=\\\"separator\\\" />\\n\\n      <ExpandableBody\\n        body={draft.body}\\n        isExpanded={isExpanded}\\n        onNeedsExpansionChange={onNeedsExpansionChange}\\n      />\\n    </>\\n  );\\n}\\n\\nfunction formatSentTime(date: Date): string {\\n  return date.toLocaleTimeString(undefined, {\\n    hour: \\\"numeric\\\",\\n    minute: \\\"2-digit\\\",\\n  });\\n}\\n\\nexport function resolveStateFromOutcome(outcome: DraftOutcome): DraftState {\\n  if (outcome === \\\"sent\\\") return \\\"sent\\\";\\n  if (outcome === \\\"cancelled\\\") return \\\"cancelled\\\";\\n  return \\\"review\\\";\\n}\\n\\nexport function resolveOutcomeTransition(\\n  previousOutcome: DraftOutcome,\\n  nextOutcome: DraftOutcome,\\n): DraftState | null {\\n  if (previousOutcome === nextOutcome) {\\n    return null;\\n  }\\n\\n  return resolveStateFromOutcome(nextOutcome);\\n}\\n\\ninterface SentConfirmationProps {\\n  sentAt: Date;\\n}\\n\\nfunction SentConfirmation({ sentAt }: SentConfirmationProps) {\\n  return (\\n    <div\\n      className=\\\"flex items-center justify-end gap-2 text-sm\\\"\\n      role=\\\"status\\\"\\n      aria-label=\\\"Message sent\\\"\\n    >\\n      <span className=\\\"text-muted-foreground\\\">\\n        Sent at {formatSentTime(sentAt)}\\n      </span>\\n      <span className=\\\"bg-primary/10 text-primary flex size-6 shrink-0 items-center justify-center rounded-full\\\">\\n        <Check className=\\\"size-3.5\\\" />\\n      </span>\\n    </div>\\n  );\\n}\\n\\nexport function MessageDraft(props: MessageDraftProps) {\\n  const {\\n    id,\\n    className,\\n    outcome,\\n    undoGracePeriod = DEFAULT_GRACE_PERIOD,\\n    onSend,\\n    onUndo,\\n    onCancel,\\n  } = props;\\n\\n  const [state, setState] = React.useState<DraftState>(() =>\\n    resolveStateFromOutcome(outcome),\\n  );\\n  const [countdown, setCountdown] = React.useState(\\n    Math.ceil(undoGracePeriod / 1000),\\n  );\\n  const [sentAt, setSentAt] = React.useState<Date | null>(() =>\\n    outcome === \\\"sent\\\" ? new Date() : null,\\n  );\\n  const [isExpanded, setIsExpanded] = React.useState(false);\\n  const [needsExpansion, setNeedsExpansion] = React.useState(false);\\n  const undoButtonRef = React.useRef<HTMLButtonElement>(null);\\n  const timerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\\n  const countdownRef = React.useRef<ReturnType<typeof setInterval> | null>(\\n    null,\\n  );\\n  const previousOutcomeRef = React.useRef<DraftOutcome>(outcome);\\n\\n  const clearTimers = React.useCallback(() => {\\n    if (timerRef.current) {\\n      clearTimeout(timerRef.current);\\n      timerRef.current = null;\\n    }\\n    if (countdownRef.current) {\\n      clearInterval(countdownRef.current);\\n      countdownRef.current = null;\\n    }\\n  }, []);\\n\\n  React.useEffect(() => {\\n    return clearTimers;\\n  }, [clearTimers]);\\n\\n  React.useEffect(() => {\\n    const nextState = resolveOutcomeTransition(\\n      previousOutcomeRef.current,\\n      outcome,\\n    );\\n\\n    previousOutcomeRef.current = outcome;\\n\\n    if (nextState === null) {\\n      return;\\n    }\\n\\n    clearTimers();\\n    setState(nextState);\\n    setCountdown(Math.ceil(undoGracePeriod / 1000));\\n    setSentAt(nextState === \\\"sent\\\" ? new Date() : null);\\n  }, [outcome, undoGracePeriod, clearTimers]);\\n\\n  React.useEffect(() => {\\n    if (state === \\\"sending\\\") {\\n      undoButtonRef.current?.focus();\\n\\n      setCountdown(Math.ceil(undoGracePeriod / 1000));\\n\\n      countdownRef.current = setInterval(() => {\\n        setCountdown((prev) => {\\n          if (prev <= 1) {\\n            if (countdownRef.current) {\\n              clearInterval(countdownRef.current);\\n              countdownRef.current = null;\\n            }\\n            return 0;\\n          }\\n          return prev - 1;\\n        });\\n      }, 1000);\\n\\n      timerRef.current = setTimeout(async () => {\\n        clearTimers();\\n        await onSend?.();\\n        setSentAt(new Date());\\n        setState(\\\"sent\\\");\\n      }, undoGracePeriod);\\n    }\\n  }, [state, undoGracePeriod, onSend, clearTimers]);\\n\\n  const handleSend = React.useCallback(() => {\\n    setState(\\\"sending\\\");\\n  }, []);\\n\\n  const handleUndo = React.useCallback(() => {\\n    clearTimers();\\n    setState(\\\"review\\\");\\n    onUndo?.();\\n  }, [clearTimers, onUndo]);\\n\\n  const handleCancel = React.useCallback(() => {\\n    clearTimers();\\n    setState(\\\"cancelled\\\");\\n    onCancel?.();\\n  }, [clearTimers, onCancel]);\\n\\n  const handleKeyDown = React.useCallback(\\n    (event: React.KeyboardEvent) => {\\n      if (event.key === \\\"Escape\\\" && state === \\\"review\\\") {\\n        event.preventDefault();\\n        handleCancel();\\n      }\\n    },\\n    [state, handleCancel],\\n  );\\n\\n  const handleNeedsExpansionChange = React.useCallback((needs: boolean) => {\\n    setNeedsExpansion(needs);\\n  }, []);\\n\\n  const handleToggleExpand = React.useCallback(() => {\\n    setIsExpanded((prev) => !prev);\\n  }, []);\\n\\n  const handleAction = React.useCallback(\\n    async (actionId: string) => {\\n      if (actionId === \\\"send\\\") {\\n        handleSend();\\n      } else if (actionId === \\\"cancel\\\") {\\n        handleCancel();\\n      }\\n    },\\n    [handleSend, handleCancel],\\n  );\\n\\n  const actions: Action[] = [\\n    {\\n      id: \\\"cancel\\\",\\n      label: \\\"Cancel\\\",\\n      variant: \\\"ghost\\\",\\n    },\\n    {\\n      id: \\\"send\\\",\\n      label: \\\"Send\\\",\\n      variant: \\\"default\\\",\\n    },\\n  ];\\n\\n  const expandButton = needsExpansion ? (\\n    <Button\\n      variant=\\\"ghost\\\"\\n      size=\\\"sm\\\"\\n      onClick={handleToggleExpand}\\n      className=\\\"h-7 gap-1 px-2 text-sm\\\"\\n    >\\n      {isExpanded ? \\\"Show less\\\" : \\\"Read more\\\"}\\n      <ChevronDown className={cn(\\\"size-3\\\", isExpanded && \\\"rotate-180\\\")} />\\n    </Button>\\n  ) : null;\\n\\n  const renderActions = () => {\\n    switch (state) {\\n      case \\\"sending\\\":\\n        return (\\n          <div\\n            className=\\\"flex items-center justify-end gap-3\\\"\\n            aria-live=\\\"polite\\\"\\n          >\\n            <span className=\\\"text-muted-foreground text-sm\\\">\\n              Sending in {countdown}s\\n            </span>\\n            <Button\\n              ref={undoButtonRef}\\n              variant=\\\"outline\\\"\\n              size=\\\"sm\\\"\\n              onClick={handleUndo}\\n              className=\\\"rounded-full\\\"\\n            >\\n              Undo\\n            </Button>\\n          </div>\\n        );\\n      case \\\"sent\\\":\\n        return <SentConfirmation sentAt={sentAt ?? new Date()} />;\\n      case \\\"cancelled\\\":\\n        return null;\\n      default:\\n        return <ActionButtons actions={actions} onAction={handleAction} />;\\n    }\\n  };\\n\\n  if (state === \\\"cancelled\\\") {\\n    return null;\\n  }\\n\\n  return (\\n    <article\\n      className={cn(\\n        \\\"flex w-full max-w-lg min-w-64 flex-col gap-3\\\",\\n        \\\"text-foreground\\\",\\n        className,\\n      )}\\n      data-slot=\\\"message-draft\\\"\\n      data-tool-ui-id={id}\\n      data-state={state}\\n      aria-labelledby={`${id}-title`}\\n      onKeyDown={handleKeyDown}\\n    >\\n      <div className=\\\"bg-card flex w-full flex-col gap-3 rounded-2xl border px-5 pt-3 pb-5 shadow-xs transition-none\\\">\\n        {props.channel === \\\"email\\\" ? (\\n          <EmailDraftContent\\n            draft={props}\\n            titleId={`${id}-title`}\\n            isExpanded={isExpanded}\\n            onNeedsExpansionChange={handleNeedsExpansionChange}\\n          />\\n        ) : (\\n          <SlackDraftContent\\n            draft={props}\\n            titleId={`${id}-title`}\\n            isExpanded={isExpanded}\\n            onNeedsExpansionChange={handleNeedsExpansionChange}\\n          />\\n        )}\\n\\n        {expandButton}\\n      </div>\\n\\n      <div className=\\\"@container/actions\\\">{renderActions()}</div>\\n    </article>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/message-draft/README.md\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/message-draft/README.md\",\n      \"content\": \"# Message Draft\\n\\nImplementation for the \\\"message-draft\\\" Tool UI surface.\\n\\n## Files\\n\\n- public exports: components/tool-ui/message-draft/index.tsx\\n- serializable schema + parse helpers: components/tool-ui/message-draft/schema.ts\\n\\n## Companion assets\\n\\n- Docs page: app/docs/message-draft/content.mdx\\n- Preset payload: lib/presets/message-draft.ts\\n\\n## Quick check\\n\\nRun this after edits:\\n\\npnpm test\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/message-draft/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/message-draft/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { ToolUIIdSchema, ToolUIRoleSchema } from \\\"../shared/schema\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\n\\nexport const MessageDraftChannelSchema = z.enum([\\\"email\\\", \\\"slack\\\"]);\\n\\nexport type MessageDraftChannel = z.infer<typeof MessageDraftChannelSchema>;\\n\\nexport const MessageDraftOutcomeSchema = z.enum([\\\"sent\\\", \\\"cancelled\\\"]);\\n\\nexport type MessageDraftOutcome = z.infer<typeof MessageDraftOutcomeSchema>;\\n\\nconst SlackTargetSchema = z.discriminatedUnion(\\\"type\\\", [\\n  z.object({\\n    type: z.literal(\\\"channel\\\"),\\n    name: z.string().min(1),\\n    memberCount: z.number().optional(),\\n  }),\\n  z.object({ type: z.literal(\\\"dm\\\"), name: z.string().min(1) }),\\n]);\\n\\nexport type SlackTarget = z.infer<typeof SlackTargetSchema>;\\n\\nexport const SerializableEmailDraftSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  body: z.string().min(1),\\n  outcome: MessageDraftOutcomeSchema.optional(),\\n  channel: z.literal(\\\"email\\\"),\\n  subject: z.string().min(1),\\n  from: z.string().optional(),\\n  to: z.array(z.string()).min(1),\\n  cc: z.array(z.string()).optional(),\\n  bcc: z.array(z.string()).optional(),\\n});\\n\\nexport const SerializableSlackDraftSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  body: z.string().min(1),\\n  outcome: MessageDraftOutcomeSchema.optional(),\\n  channel: z.literal(\\\"slack\\\"),\\n  target: SlackTargetSchema,\\n});\\n\\nexport const SerializableMessageDraftSchema = z.discriminatedUnion(\\\"channel\\\", [\\n  SerializableEmailDraftSchema,\\n  SerializableSlackDraftSchema,\\n]);\\n\\nexport type SerializableMessageDraft = z.infer<\\n  typeof SerializableMessageDraftSchema\\n>;\\n\\nexport type SerializableEmailDraft = z.infer<\\n  typeof SerializableEmailDraftSchema\\n>;\\n\\nexport type SerializableSlackDraft = z.infer<\\n  typeof SerializableSlackDraftSchema\\n>;\\n\\nconst SerializableMessageDraftSchemaContract = defineToolUiContract(\\n  \\\"MessageDraft\\\",\\n  SerializableMessageDraftSchema,\\n);\\n\\nexport const parseSerializableMessageDraft: (\\n  input: unknown,\\n) => SerializableMessageDraft = SerializableMessageDraftSchemaContract.parse;\\n\\nexport const safeParseSerializableMessageDraft: (\\n  input: unknown,\\n) => SerializableMessageDraft | null =\\n  SerializableMessageDraftSchemaContract.safeParse;\\n\\nexport type MessageDraftProps = SerializableMessageDraft & {\\n  className?: string;\\n  undoGracePeriod?: number;\\n  onSend?: () => void | Promise<void>;\\n  onUndo?: () => void;\\n  onCancel?: () => void;\\n};\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/shared/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn     → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n *   Button → shadcn/ui Button\\n */\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport { Button } from \\\"@/components/ui/button\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/action-buttons.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/shared/action-buttons.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport type { Action } from \\\"./schema\\\";\\nimport { cn, Button } from \\\"./_adapter\\\";\\nimport { useActionButtons } from \\\"./use-action-buttons\\\";\\n\\nexport interface ActionButtonsProps {\\n  actions: Action[];\\n  onAction: (actionId: string) => void | Promise<void>;\\n  onBeforeAction?: (actionId: string) => boolean | Promise<boolean>;\\n  confirmTimeout?: number;\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  className?: string;\\n}\\n\\nexport function ActionButtons({\\n  actions,\\n  onAction,\\n  onBeforeAction,\\n  confirmTimeout = 3000,\\n  align = \\\"right\\\",\\n  className,\\n}: ActionButtonsProps) {\\n  const { actions: resolvedActions, runAction } = useActionButtons({\\n    actions,\\n    onAction,\\n    onBeforeAction,\\n    confirmTimeout,\\n  });\\n\\n  return (\\n    <div\\n      className={cn(\\n        \\\"flex flex-col gap-3\\\",\\n        \\\"@sm/actions:flex-row @sm/actions:flex-wrap @sm/actions:items-center @sm/actions:gap-2\\\",\\n        align === \\\"left\\\" && \\\"@sm/actions:justify-start\\\",\\n        align === \\\"center\\\" && \\\"@sm/actions:justify-center\\\",\\n        align === \\\"right\\\" && \\\"@sm/actions:justify-end\\\",\\n        className,\\n      )}\\n    >\\n      {resolvedActions.map((action) => {\\n        const label = action.currentLabel;\\n        const variant = action.variant || \\\"default\\\";\\n\\n        return (\\n          <Button\\n            key={action.id}\\n            variant={variant}\\n            onClick={() => runAction(action.id)}\\n            disabled={action.isDisabled}\\n            className={cn(\\n              \\\"rounded-full px-4!\\\",\\n              \\\"justify-center\\\",\\n              \\\"min-h-11 w-full text-base\\\",\\n              \\\"@sm/actions:min-h-0 @sm/actions:w-auto @sm/actions:px-3 @sm/actions:py-2 @sm/actions:text-sm\\\",\\n              action.isConfirming &&\\n                \\\"ring-destructive ring-2 ring-offset-2 motion-safe:animate-pulse\\\",\\n            )}\\n            aria-label={\\n              action.shortcut ? `${label} (${action.shortcut})` : label\\n            }\\n          >\\n            {action.isLoading && (\\n              <svg\\n                className=\\\"mr-2 h-4 w-4 motion-safe:animate-spin\\\"\\n                xmlns=\\\"http://www.w3.org/2000/svg\\\"\\n                fill=\\\"none\\\"\\n                viewBox=\\\"0 0 24 24\\\"\\n              >\\n                <circle\\n                  className=\\\"opacity-25\\\"\\n                  cx=\\\"12\\\"\\n                  cy=\\\"12\\\"\\n                  r=\\\"10\\\"\\n                  stroke=\\\"currentColor\\\"\\n                  strokeWidth=\\\"4\\\"\\n                />\\n                <path\\n                  className=\\\"opacity-75\\\"\\n                  fill=\\\"currentColor\\\"\\n                  d=\\\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\\\"\\n                />\\n              </svg>\\n            )}\\n            {action.icon && !action.isLoading && (\\n              <span className=\\\"mr-2\\\">{action.icon}</span>\\n            )}\\n            {label}\\n            {action.shortcut && !action.isLoading && (\\n              <kbd className=\\\"border-border bg-muted ml-2.5 hidden rounded-lg border px-2 py-0.5 font-mono text-xs font-medium sm:inline-block\\\">\\n                {action.shortcut}\\n              </kbd>\\n            )}\\n          </Button>\\n        );\\n      })}\\n    </div>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { ReactNode } from \\\"react\\\";\\n\\n/**\\n * Tool UI conventions:\\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\\n * - Schema: `SerializableXSchema`\\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\\n * - Root attrs: `data-tool-ui-id` + `data-slot`\\n */\\n\\n/**\\n * Schema for tool UI identity.\\n *\\n * Every tool UI should have a unique identifier that:\\n * - Is stable across re-renders\\n * - Is meaningful (not auto-generated)\\n * - Is unique within the conversation\\n *\\n * Format recommendation: `{component-type}-{semantic-identifier}`\\n * Examples: \\\"data-table-expenses-q3\\\", \\\"option-list-deploy-target\\\"\\n */\\nexport const ToolUIIdSchema = z.string().min(1);\\n\\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\\n\\n/**\\n * Primary role of a Tool UI surface in a chat context.\\n */\\nexport const ToolUIRoleSchema = z.enum([\\n  \\\"information\\\",\\n  \\\"decision\\\",\\n  \\\"control\\\",\\n  \\\"state\\\",\\n  \\\"composite\\\",\\n]);\\n\\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\\n\\nexport const ToolUIReceiptOutcomeSchema = z.enum([\\n  \\\"success\\\",\\n  \\\"partial\\\",\\n  \\\"failed\\\",\\n  \\\"cancelled\\\",\\n]);\\n\\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\\n\\n/**\\n * Optional receipt metadata: a durable summary of an outcome.\\n */\\nexport const ToolUIReceiptSchema = z.object({\\n  outcome: ToolUIReceiptOutcomeSchema,\\n  summary: z.string().min(1),\\n  identifiers: z.record(z.string(), z.string()).optional(),\\n  at: z.string().datetime(),\\n});\\n\\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\\n\\n/**\\n * Base schema for Tool UI payloads (id + optional role/receipt).\\n */\\nexport const ToolUISurfaceSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n});\\n\\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\\n\\nexport const ActionSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  /**\\n   * Canonical narration the assistant can use after this action is taken.\\n   *\\n   * Example: \\\"I exported the table as CSV.\\\" / \\\"I opened the link in a new tab.\\\"\\n   */\\n  sentence: z.string().optional(),\\n  confirmLabel: z.string().optional(),\\n  variant: z\\n    .enum([\\\"default\\\", \\\"destructive\\\", \\\"secondary\\\", \\\"ghost\\\", \\\"outline\\\"])\\n    .optional(),\\n  icon: z.custom<ReactNode>().optional(),\\n  loading: z.boolean().optional(),\\n  disabled: z.boolean().optional(),\\n  shortcut: z.string().optional(),\\n});\\n\\nexport type Action = z.infer<typeof ActionSchema>;\\nexport type LocalAction = Action;\\nexport type DecisionAction = Action;\\n\\nexport const DecisionResultSchema = z.object({\\n  kind: z.literal(\\\"decision\\\"),\\n  version: z.literal(1),\\n  decisionId: z.string().min(1),\\n  actionId: z.string().min(1),\\n  actionLabel: z.string().min(1),\\n  at: z.string().datetime(),\\n  payload: z.record(z.string(), z.unknown()).optional(),\\n});\\n\\nexport type DecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n> = Omit<z.infer<typeof DecisionResultSchema>, \\\"payload\\\"> & {\\n  payload?: TPayload;\\n};\\n\\nexport function createDecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n>(args: {\\n  decisionId: string;\\n  action: { id: string; label: string };\\n  payload?: TPayload;\\n}): DecisionResult<TPayload> {\\n  return {\\n    kind: \\\"decision\\\",\\n    version: 1,\\n    decisionId: args.decisionId,\\n    actionId: args.action.id,\\n    actionLabel: args.action.label,\\n    at: new Date().toISOString(),\\n    payload: args.payload,\\n  };\\n}\\n\\nexport const ActionButtonsPropsSchema = z.object({\\n  actions: z.array(ActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n  className: z.string().optional(),\\n});\\n\\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\\n  actions: z.array(SerializableActionSchema),\\n}).omit({ className: true });\\n\\nexport interface ActionsConfig {\\n  items: Action[];\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  confirmTimeout?: number;\\n}\\n\\nexport const SerializableActionsConfigSchema = z.object({\\n  items: z.array(SerializableActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n});\\n\\nexport type SerializableActionsConfig = z.infer<\\n  typeof SerializableActionsConfigSchema\\n>;\\n\\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/use-action-buttons.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/shared/use-action-buttons.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { useCallback, useEffect, useMemo, useRef, useState } from \\\"react\\\";\\nimport type { Action } from \\\"./schema\\\";\\n\\nexport type UseActionButtonsOptions = {\\n  actions: Action[];\\n  onAction: (actionId: string) => void | Promise<void>;\\n  onBeforeAction?: (actionId: string) => boolean | Promise<boolean>;\\n  confirmTimeout?: number;\\n};\\n\\nexport type UseActionButtonsResult = {\\n  actions: Array<\\n    Action & {\\n      currentLabel: string;\\n      isConfirming: boolean;\\n      isExecuting: boolean;\\n      isDisabled: boolean;\\n      isLoading: boolean;\\n    }\\n  >;\\n  runAction: (actionId: string) => Promise<void>;\\n  confirmingActionId: string | null;\\n  executingActionId: string | null;\\n};\\n\\ntype ActionExecutionLock = {\\n  tryAcquire: () => boolean;\\n  release: () => void;\\n};\\n\\nexport function createActionExecutionLock(): ActionExecutionLock {\\n  let locked = false;\\n\\n  return {\\n    tryAcquire: () => {\\n      if (locked) return false;\\n      locked = true;\\n      return true;\\n    },\\n    release: () => {\\n      locked = false;\\n    },\\n  };\\n}\\n\\nexport function useActionButtons(\\n  options: UseActionButtonsOptions,\\n): UseActionButtonsResult {\\n  const { actions, onAction, onBeforeAction, confirmTimeout = 3000 } = options;\\n\\n  const [confirmingActionId, setConfirmingActionId] = useState<string | null>(\\n    null,\\n  );\\n  const [executingActionId, setExecutingActionId] = useState<string | null>(\\n    null,\\n  );\\n  const executionLockRef = useRef<ActionExecutionLock>(\\n    createActionExecutionLock(),\\n  );\\n\\n  useEffect(() => {\\n    if (!confirmingActionId) return;\\n    const id = setTimeout(() => setConfirmingActionId(null), confirmTimeout);\\n    return () => clearTimeout(id);\\n  }, [confirmingActionId, confirmTimeout]);\\n\\n  useEffect(() => {\\n    if (!confirmingActionId) return;\\n    const handleKeyDown = (e: KeyboardEvent) => {\\n      if (e.key === \\\"Escape\\\") {\\n        setConfirmingActionId(null);\\n      }\\n    };\\n\\n    window.addEventListener(\\\"keydown\\\", handleKeyDown);\\n    return () => window.removeEventListener(\\\"keydown\\\", handleKeyDown);\\n  }, [confirmingActionId]);\\n\\n  const runAction = useCallback(\\n    async (actionId: string) => {\\n      const action = actions.find((a) => a.id === actionId);\\n      if (!action) return;\\n\\n      const isAnyActionExecuting = executingActionId !== null;\\n      if (action.disabled || action.loading || isAnyActionExecuting) {\\n        return;\\n      }\\n\\n      if (action.confirmLabel && confirmingActionId !== action.id) {\\n        setConfirmingActionId(action.id);\\n        return;\\n      }\\n\\n      if (!executionLockRef.current.tryAcquire()) {\\n        return;\\n      }\\n\\n      if (onBeforeAction) {\\n        const shouldProceed = await onBeforeAction(action.id);\\n        if (!shouldProceed) {\\n          setConfirmingActionId(null);\\n          executionLockRef.current.release();\\n          return;\\n        }\\n      }\\n\\n      try {\\n        setExecutingActionId(action.id);\\n        await onAction(action.id);\\n      } finally {\\n        executionLockRef.current.release();\\n        setExecutingActionId(null);\\n        setConfirmingActionId(null);\\n      }\\n    },\\n    [actions, confirmingActionId, executingActionId, onAction, onBeforeAction],\\n  );\\n\\n  const resolvedActions = useMemo(\\n    () =>\\n      actions.map((action) => {\\n        const isConfirming = confirmingActionId === action.id;\\n        const isThisActionExecuting = executingActionId === action.id;\\n        const isLoading = action.loading || isThisActionExecuting;\\n        const isDisabled =\\n          action.disabled ||\\n          (executingActionId !== null && !isThisActionExecuting);\\n        const currentLabel =\\n          isConfirming && action.confirmLabel\\n            ? action.confirmLabel\\n            : action.label;\\n\\n        return {\\n          ...action,\\n          currentLabel,\\n          isConfirming,\\n          isExecuting: isThisActionExecuting,\\n          isDisabled,\\n          isLoading,\\n        };\\n      }),\\n    [actions, confirmingActionId, executingActionId],\\n  );\\n\\n  return {\\n    actions: resolvedActions,\\n    runAction,\\n    confirmingActionId,\\n    executingActionId,\\n  };\\n}\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/option-list.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"option-list\",\n  \"type\": \"registry:block\",\n  \"title\": \"Option List\",\n  \"description\": \"Single or multi-select choices with confirmation actions.\",\n  \"dependencies\": [\n    \"lucide-react\",\n    \"zod\"\n  ],\n  \"registryDependencies\": [\n    \"button\",\n    \"separator\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/option-list/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/option-list/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn        → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n *   Button    → shadcn/ui Button\\n *   Separator → shadcn/ui Separator\\n */\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport { Button } from \\\"@/components/ui/button\\\";\\nexport { Separator } from \\\"@/components/ui/separator\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/option-list/index.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/option-list/index.tsx\",\n      \"content\": \"export { OptionList } from \\\"./option-list\\\";\\nexport type {\\n  OptionListProps,\\n  OptionListOption,\\n  OptionListSelection,\\n  SerializableOptionList,\\n} from \\\"./schema\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/option-list/option-list.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/option-list/option-list.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport {\\n  useMemo,\\n  useState,\\n  useCallback,\\n  useEffect,\\n  useRef,\\n  Fragment,\\n} from \\\"react\\\";\\nimport type { KeyboardEvent } from \\\"react\\\";\\nimport type {\\n  OptionListProps,\\n  OptionListSelection,\\n  OptionListOption,\\n} from \\\"./schema\\\";\\nimport {\\n  normalizeSelectionForOptions,\\n  parseSelectionToIdSet,\\n} from \\\"./selection\\\";\\nimport { ActionButtons } from \\\"../shared/action-buttons\\\";\\nimport { normalizeActionsConfig } from \\\"../shared/actions-config\\\";\\nimport type { Action } from \\\"../shared/schema\\\";\\nimport { cn, Button, Separator } from \\\"./_adapter\\\";\\nimport { Check } from \\\"lucide-react\\\";\\n\\nfunction convertIdSetToSelection(\\n  selected: Set<string>,\\n  mode: \\\"multi\\\" | \\\"single\\\",\\n): OptionListSelection {\\n  if (mode === \\\"single\\\") {\\n    const [first] = selected;\\n    return first ?? null;\\n  }\\n  return Array.from(selected);\\n}\\n\\nfunction areSetsEqual(a: Set<string>, b: Set<string>) {\\n  if (a.size !== b.size) return false;\\n  for (const val of a) {\\n    if (!b.has(val)) return false;\\n  }\\n  return true;\\n}\\n\\ninterface SelectionIndicatorProps {\\n  mode: \\\"multi\\\" | \\\"single\\\";\\n  isSelected: boolean;\\n  disabled?: boolean;\\n}\\n\\nfunction SelectionIndicator({\\n  mode,\\n  isSelected,\\n  disabled,\\n}: SelectionIndicatorProps) {\\n  const shape = mode === \\\"single\\\" ? \\\"rounded-full\\\" : \\\"rounded\\\";\\n\\n  return (\\n    <div\\n      className={cn(\\n        \\\"flex size-4 shrink-0 items-center justify-center border-2 transition-colors\\\",\\n        shape,\\n        isSelected && \\\"border-primary bg-primary text-primary-foreground\\\",\\n        !isSelected && \\\"border-muted-foreground/50\\\",\\n        disabled && \\\"opacity-50\\\",\\n      )}\\n    >\\n      {mode === \\\"multi\\\" && isSelected && <Check className=\\\"size-3\\\" />}\\n      {mode === \\\"single\\\" && isSelected && (\\n        <span className=\\\"size-2 rounded-full bg-current\\\" />\\n      )}\\n    </div>\\n  );\\n}\\n\\ninterface OptionItemProps {\\n  option: OptionListOption;\\n  isSelected: boolean;\\n  isDisabled: boolean;\\n  selectionMode: \\\"multi\\\" | \\\"single\\\";\\n  isFirst: boolean;\\n  isLast: boolean;\\n  onToggle: () => void;\\n  tabIndex?: number;\\n  onFocus?: () => void;\\n  buttonRef?: (el: HTMLButtonElement | null) => void;\\n}\\n\\nfunction OptionItem({\\n  option,\\n  isSelected,\\n  isDisabled,\\n  selectionMode,\\n  isFirst,\\n  isLast,\\n  onToggle,\\n  tabIndex,\\n  onFocus,\\n  buttonRef,\\n}: OptionItemProps) {\\n  const hasAdjacentOptions = !isFirst && !isLast;\\n\\n  return (\\n    <Button\\n      ref={buttonRef}\\n      data-id={option.id}\\n      variant=\\\"ghost\\\"\\n      size=\\\"lg\\\"\\n      role=\\\"option\\\"\\n      aria-selected={isSelected}\\n      onClick={onToggle}\\n      onFocus={onFocus}\\n      tabIndex={tabIndex}\\n      disabled={isDisabled}\\n      className={cn(\\n        \\\"peer group relative h-auto min-h-[50px] w-full justify-start text-left text-sm font-medium\\\",\\n        \\\"rounded-none border-0 bg-transparent px-0 py-2 text-base shadow-none transition-none hover:bg-transparent! @md/option-list:text-sm\\\",\\n        isFirst && \\\"pb-2.5\\\",\\n        hasAdjacentOptions && \\\"py-2.5\\\",\\n      )}\\n    >\\n      <span\\n        className={cn(\\n          \\\"bg-primary/5 absolute inset-0 -mx-3 -my-0.5 rounded-xl opacity-0 transition-opacity group-hover:opacity-100\\\",\\n        )}\\n      />\\n      <div className=\\\"relative flex items-start gap-3\\\">\\n        <span className=\\\"flex h-6 items-center\\\">\\n          <SelectionIndicator\\n            mode={selectionMode}\\n            isSelected={isSelected}\\n            disabled={option.disabled}\\n          />\\n        </span>\\n        {option.icon && (\\n          <span className=\\\"flex h-6 items-center\\\">{option.icon}</span>\\n        )}\\n        <div className=\\\"flex flex-col text-left\\\">\\n          <span className=\\\"leading-6 text-pretty\\\">{option.label}</span>\\n          {option.description && (\\n            <span className=\\\"text-muted-foreground text-sm font-normal text-pretty\\\">\\n              {option.description}\\n            </span>\\n          )}\\n        </div>\\n      </div>\\n    </Button>\\n  );\\n}\\n\\ninterface OptionListConfirmationProps {\\n  id: string;\\n  options: OptionListOption[];\\n  selectedIds: Set<string>;\\n  className?: string;\\n}\\n\\nfunction OptionListConfirmation({\\n  id,\\n  options,\\n  selectedIds,\\n  className,\\n}: OptionListConfirmationProps) {\\n  const confirmedOptions = options.filter((opt) => selectedIds.has(opt.id));\\n\\n  return (\\n    <div\\n      className={cn(\\n        \\\"@container/option-list flex w-full max-w-md min-w-80 flex-col\\\",\\n        \\\"text-foreground\\\",\\n        \\\"motion-safe:animate-in motion-safe:fade-in motion-safe:blur-in-sm motion-safe:zoom-in-95 motion-safe:duration-300 motion-safe:ease-[cubic-bezier(0.16,1,0.3,1)] motion-safe:fill-mode-both\\\",\\n        className,\\n      )}\\n      data-slot=\\\"option-list\\\"\\n      data-tool-ui-id={id}\\n      data-receipt=\\\"true\\\"\\n      role=\\\"status\\\"\\n      aria-label=\\\"Confirmed selection\\\"\\n    >\\n      <div\\n        className={cn(\\n          \\\"bg-card/60 flex w-full flex-col overflow-hidden rounded-2xl border px-5 py-2.5 shadow-xs\\\",\\n        )}\\n      >\\n        {confirmedOptions.map((option, index) => (\\n          <Fragment key={option.id}>\\n            {index > 0 && (\\n              <Separator className=\\\"my-1.5\\\" orientation=\\\"horizontal\\\" />\\n            )}\\n            <div className=\\\"flex items-start gap-3 py-1\\\">\\n              <span className=\\\"flex h-6 items-center\\\">\\n                <Check className=\\\"text-primary size-4 shrink-0\\\" />\\n              </span>\\n              {option.icon && (\\n                <span className=\\\"flex h-6 items-center\\\">{option.icon}</span>\\n              )}\\n              <div className=\\\"flex flex-col text-left\\\">\\n                <span className=\\\"text-base leading-6 font-medium text-pretty @md/option-list:text-sm\\\">\\n                  {option.label}\\n                </span>\\n                {option.description && (\\n                  <span className=\\\"text-muted-foreground text-sm font-normal text-pretty\\\">\\n                    {option.description}\\n                  </span>\\n                )}\\n              </div>\\n            </div>\\n          </Fragment>\\n        ))}\\n      </div>\\n    </div>\\n  );\\n}\\n\\nexport function OptionList({\\n  id,\\n  options,\\n  selectionMode = \\\"multi\\\",\\n  minSelections = 1,\\n  maxSelections,\\n  value,\\n  defaultValue,\\n  choice,\\n  onChange,\\n  actions,\\n  onAction,\\n  onBeforeAction,\\n  className,\\n}: OptionListProps) {\\n  if (process.env[\\\"NODE_ENV\\\"] !== \\\"production\\\") {\\n    if (value !== undefined && defaultValue !== undefined) {\\n      console.warn(\\n        \\\"[OptionList] Both `value` (controlled) and `defaultValue` (uncontrolled) were provided. `defaultValue` is ignored when `value` is set.\\\",\\n      );\\n    }\\n    if (value !== undefined && !onChange) {\\n      console.warn(\\n        \\\"[OptionList] `value` was provided without `onChange`. This makes OptionList controlled; selection will not update unless the parent updates `value`.\\\",\\n      );\\n    }\\n  }\\n\\n  const effectiveMaxSelections = selectionMode === \\\"single\\\" ? 1 : maxSelections;\\n  const optionIds = useMemo(\\n    () => new Set(options.map((option) => option.id)),\\n    [options],\\n  );\\n\\n  const [uncontrolledSelected, setUncontrolledSelected] = useState<Set<string>>(\\n    () =>\\n      normalizeSelectionForOptions(\\n        parseSelectionToIdSet(\\n          defaultValue,\\n          selectionMode,\\n          effectiveMaxSelections,\\n        ),\\n        optionIds,\\n      ),\\n  );\\n\\n  const selectedIds = useMemo(() => {\\n    const parsed =\\n      value !== undefined\\n        ? parseSelectionToIdSet(value, selectionMode, effectiveMaxSelections)\\n        : uncontrolledSelected;\\n    return normalizeSelectionForOptions(parsed, optionIds);\\n  }, [\\n    value,\\n    uncontrolledSelected,\\n    selectionMode,\\n    effectiveMaxSelections,\\n    optionIds,\\n  ]);\\n\\n  const selectedCount = selectedIds.size;\\n\\n  const optionStates = useMemo(() => {\\n    return options.map((option) => {\\n      const isSelected = selectedIds.has(option.id);\\n      const isSelectionLocked =\\n        selectionMode === \\\"multi\\\" &&\\n        effectiveMaxSelections !== undefined &&\\n        selectedCount >= effectiveMaxSelections &&\\n        !isSelected;\\n      const isDisabled = option.disabled || isSelectionLocked;\\n\\n      return { option, isSelected, isDisabled };\\n    });\\n  }, [\\n    options,\\n    selectedIds,\\n    selectionMode,\\n    effectiveMaxSelections,\\n    selectedCount,\\n  ]);\\n\\n  const optionRefs = useRef<Array<HTMLButtonElement | null>>([]);\\n  const [activeIndex, setActiveIndex] = useState(() => {\\n    const firstSelected = optionStates.findIndex(\\n      (s) => s.isSelected && !s.isDisabled,\\n    );\\n    if (firstSelected >= 0) return firstSelected;\\n    const firstEnabled = optionStates.findIndex((s) => !s.isDisabled);\\n    return firstEnabled >= 0 ? firstEnabled : 0;\\n  });\\n\\n  useEffect(() => {\\n    if (optionStates.length === 0) return;\\n    setActiveIndex((prev) => {\\n      if (\\n        prev < 0 ||\\n        prev >= optionStates.length ||\\n        optionStates[prev].isDisabled\\n      ) {\\n        const firstEnabled = optionStates.findIndex((s) => !s.isDisabled);\\n        return firstEnabled >= 0 ? firstEnabled : 0;\\n      }\\n      return prev;\\n    });\\n  }, [optionStates]);\\n\\n  const updateSelection = useCallback(\\n    (next: Set<string>) => {\\n      const normalizedNext = normalizeSelectionForOptions(\\n        parseSelectionToIdSet(\\n          Array.from(next),\\n          selectionMode,\\n          effectiveMaxSelections,\\n        ),\\n        optionIds,\\n      );\\n\\n      if (value === undefined) {\\n        if (!areSetsEqual(uncontrolledSelected, normalizedNext)) {\\n          setUncontrolledSelected(normalizedNext);\\n        }\\n      }\\n\\n      onChange?.(convertIdSetToSelection(normalizedNext, selectionMode));\\n    },\\n    [\\n      effectiveMaxSelections,\\n      selectionMode,\\n      uncontrolledSelected,\\n      value,\\n      onChange,\\n      optionIds,\\n    ],\\n  );\\n\\n  const toggleSelection = useCallback(\\n    (optionId: string) => {\\n      const next = new Set(selectedIds);\\n      const isSelected = next.has(optionId);\\n\\n      if (selectionMode === \\\"single\\\") {\\n        if (isSelected) {\\n          next.delete(optionId);\\n        } else {\\n          next.clear();\\n          next.add(optionId);\\n        }\\n      } else {\\n        if (isSelected) {\\n          next.delete(optionId);\\n        } else {\\n          if (effectiveMaxSelections && next.size >= effectiveMaxSelections) {\\n            return;\\n          }\\n          next.add(optionId);\\n        }\\n      }\\n\\n      updateSelection(next);\\n    },\\n    [effectiveMaxSelections, selectedIds, selectionMode, updateSelection],\\n  );\\n\\n  const toSelectionState = useCallback(\\n    (selected: Set<string>): OptionListSelection =>\\n      convertIdSetToSelection(selected, selectionMode),\\n    [selectionMode],\\n  );\\n\\n  const handleCancel = useCallback((): OptionListSelection => {\\n    const empty = new Set<string>();\\n    updateSelection(empty);\\n    return toSelectionState(empty);\\n  }, [toSelectionState, updateSelection]);\\n\\n  const customActions = useMemo(\\n    () => normalizeActionsConfig(actions),\\n    [actions],\\n  );\\n\\n  const handleFooterAction = useCallback(\\n    async (actionId: string) => {\\n      let nextState = toSelectionState(selectedIds);\\n\\n      if (actionId === \\\"cancel\\\") {\\n        nextState = handleCancel();\\n      }\\n\\n      await onAction?.(actionId, nextState);\\n    },\\n    [handleCancel, onAction, selectedIds, toSelectionState],\\n  );\\n\\n  const normalizedFooterActions = useMemo(() => {\\n    if (customActions) return customActions;\\n    return {\\n      items: [\\n        { id: \\\"cancel\\\", label: \\\"Clear\\\", variant: \\\"ghost\\\" as const },\\n        { id: \\\"confirm\\\", label: \\\"Confirm\\\", variant: \\\"default\\\" as const },\\n      ],\\n      align: \\\"right\\\" as const,\\n    } satisfies ReturnType<typeof normalizeActionsConfig>;\\n  }, [customActions]);\\n\\n  const isConfirmDisabled =\\n    selectedCount < minSelections || selectedCount === 0;\\n  const hasNothingToClear = selectedCount === 0;\\n\\n  const focusOptionAt = useCallback((index: number) => {\\n    const el = optionRefs.current[index];\\n    if (el) el.focus();\\n    setActiveIndex(index);\\n  }, []);\\n\\n  const findFirstEnabledIndex = useCallback(() => {\\n    const idx = optionStates.findIndex((s) => !s.isDisabled);\\n    return idx >= 0 ? idx : 0;\\n  }, [optionStates]);\\n\\n  const findLastEnabledIndex = useCallback(() => {\\n    for (let i = optionStates.length - 1; i >= 0; i--) {\\n      if (!optionStates[i].isDisabled) return i;\\n    }\\n    return 0;\\n  }, [optionStates]);\\n\\n  const findNextEnabledIndex = useCallback(\\n    (start: number, direction: 1 | -1) => {\\n      const len = optionStates.length;\\n      if (len === 0) return 0;\\n      for (let step = 1; step <= len; step++) {\\n        const idx = (start + direction * step + len) % len;\\n        if (!optionStates[idx].isDisabled) return idx;\\n      }\\n      return start;\\n    },\\n    [optionStates],\\n  );\\n\\n  const handleListboxKeyDown = useCallback(\\n    (e: KeyboardEvent<HTMLDivElement>) => {\\n      if (optionStates.length === 0) return;\\n\\n      const key = e.key;\\n\\n      if (key === \\\"ArrowDown\\\") {\\n        e.preventDefault();\\n        e.stopPropagation();\\n        focusOptionAt(findNextEnabledIndex(activeIndex, 1));\\n        return;\\n      }\\n\\n      if (key === \\\"ArrowUp\\\") {\\n        e.preventDefault();\\n        e.stopPropagation();\\n        focusOptionAt(findNextEnabledIndex(activeIndex, -1));\\n        return;\\n      }\\n\\n      if (key === \\\"Home\\\") {\\n        e.preventDefault();\\n        e.stopPropagation();\\n        focusOptionAt(findFirstEnabledIndex());\\n        return;\\n      }\\n\\n      if (key === \\\"End\\\") {\\n        e.preventDefault();\\n        e.stopPropagation();\\n        focusOptionAt(findLastEnabledIndex());\\n        return;\\n      }\\n\\n      if (key === \\\"Enter\\\" || key === \\\" \\\") {\\n        e.preventDefault();\\n        e.stopPropagation();\\n        const current = optionStates[activeIndex];\\n        if (!current || current.isDisabled) return;\\n        toggleSelection(current.option.id);\\n        return;\\n      }\\n\\n      if (key === \\\"Escape\\\") {\\n        e.preventDefault();\\n        e.stopPropagation();\\n        if (!hasNothingToClear) {\\n          handleCancel();\\n        }\\n      }\\n    },\\n    [\\n      activeIndex,\\n      findFirstEnabledIndex,\\n      findLastEnabledIndex,\\n      findNextEnabledIndex,\\n      focusOptionAt,\\n      handleCancel,\\n      hasNothingToClear,\\n      optionStates,\\n      toggleSelection,\\n    ],\\n  );\\n\\n  const actionsWithDisabledState = useMemo((): Action[] => {\\n    return normalizedFooterActions.items.map((action) => {\\n      const isDisabledByValidation =\\n        (action.id === \\\"confirm\\\" && isConfirmDisabled) ||\\n        (action.id === \\\"cancel\\\" && hasNothingToClear);\\n      return {\\n        ...action,\\n        disabled: action.disabled || isDisabledByValidation,\\n        label:\\n          action.id === \\\"confirm\\\" &&\\n          selectionMode === \\\"multi\\\" &&\\n          selectedCount > 0\\n            ? `${action.label} (${selectedCount})`\\n            : action.label,\\n      };\\n    });\\n  }, [\\n    normalizedFooterActions.items,\\n    isConfirmDisabled,\\n    hasNothingToClear,\\n    selectionMode,\\n    selectedCount,\\n  ]);\\n\\n  const isReceipt = choice !== undefined && choice !== null;\\n  const viewKey = isReceipt ? `receipt-${String(choice)}` : \\\"interactive\\\";\\n\\n  return (\\n    <div key={viewKey} className=\\\"contents\\\">\\n      {isReceipt ? (\\n        <OptionListConfirmation\\n          id={id}\\n          options={options}\\n          selectedIds={normalizeSelectionForOptions(\\n            parseSelectionToIdSet(choice, selectionMode),\\n            optionIds,\\n          )}\\n          className={className}\\n        />\\n      ) : (\\n        <div\\n          className={cn(\\n            \\\"@container/option-list flex w-full max-w-md min-w-80 flex-col gap-3\\\",\\n            \\\"text-foreground\\\",\\n            className,\\n          )}\\n          data-slot=\\\"option-list\\\"\\n          data-tool-ui-id={id}\\n          role=\\\"group\\\"\\n          aria-label=\\\"Option list\\\"\\n        >\\n          <div\\n            className={cn(\\n              \\\"group/list bg-card flex w-full flex-col overflow-hidden rounded-2xl border px-4 py-1.5 shadow-xs\\\",\\n            )}\\n            role=\\\"listbox\\\"\\n            aria-multiselectable={selectionMode === \\\"multi\\\"}\\n            onKeyDown={handleListboxKeyDown}\\n          >\\n            {optionStates.map(({ option, isSelected, isDisabled }, index) => {\\n              return (\\n                <Fragment key={option.id}>\\n                  {index > 0 && (\\n                    <Separator\\n                      className=\\\"transition-opacity [@media(hover:hover)]:[&:has(+_:hover)]:opacity-0 [@media(hover:hover)]:[.peer:hover+&]:opacity-0\\\"\\n                      orientation=\\\"horizontal\\\"\\n                    />\\n                  )}\\n                  <OptionItem\\n                    option={option}\\n                    isSelected={isSelected}\\n                    isDisabled={isDisabled}\\n                    selectionMode={selectionMode}\\n                    isFirst={index === 0}\\n                    isLast={index === optionStates.length - 1}\\n                    tabIndex={index === activeIndex ? 0 : -1}\\n                    onFocus={() => setActiveIndex(index)}\\n                    buttonRef={(el) => {\\n                      optionRefs.current[index] = el;\\n                    }}\\n                    onToggle={() => toggleSelection(option.id)}\\n                  />\\n                </Fragment>\\n              );\\n            })}\\n          </div>\\n\\n          <div className=\\\"@container/actions\\\">\\n            <ActionButtons\\n              actions={actionsWithDisabledState}\\n              align={normalizedFooterActions.align}\\n              confirmTimeout={normalizedFooterActions.confirmTimeout}\\n              onAction={handleFooterAction}\\n              onBeforeAction={\\n                onBeforeAction\\n                  ? (actionId) =>\\n                      onBeforeAction(actionId, toSelectionState(selectedIds))\\n                  : undefined\\n              }\\n            />\\n          </div>\\n        </div>\\n      )}\\n    </div>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/option-list/README.md\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/option-list/README.md\",\n      \"content\": \"# Option List\\n\\nImplementation for the \\\"option-list\\\" Tool UI surface.\\n\\n## Files\\n\\n- public exports: components/tool-ui/option-list/index.tsx\\n- serializable schema + parse helpers: components/tool-ui/option-list/schema.ts\\n\\n## Companion assets\\n\\n- Docs page: app/docs/option-list/content.mdx\\n- Preset payload: lib/presets/option-list.ts\\n\\n## Quick check\\n\\nRun this after edits:\\n\\npnpm test\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/option-list/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/option-list/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { ReactNode } from \\\"react\\\";\\nimport type { ActionsProp } from \\\"../shared/actions-config\\\";\\nimport type { EmbeddedActionsProps } from \\\"../shared/embedded-actions\\\";\\nimport {\\n  ActionSchema,\\n  SerializableActionSchema,\\n  SerializableActionsConfigSchema,\\n  ToolUIIdSchema,\\n  ToolUIReceiptSchema,\\n  ToolUIRoleSchema,\\n} from \\\"../shared/schema\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\n\\nexport const OptionListOptionSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  description: z.string().optional(),\\n  icon: z.custom<ReactNode>().optional(),\\n  disabled: z.boolean().optional(),\\n});\\n\\nexport type OptionListSelection = string[] | string | null;\\n\\nconst OptionListSelectionSchema = z\\n  .union([z.array(z.string()), z.string(), z.null()])\\n  .optional();\\n\\ntype OptionListSchemaInvariantInput = {\\n  options: Array<{ id: string }>;\\n  minSelections?: number;\\n  maxSelections?: number;\\n  value?: OptionListSelection;\\n  defaultValue?: OptionListSelection;\\n  choice?: OptionListSelection;\\n};\\n\\nfunction selectionToIds(selection: OptionListSelection | undefined): string[] {\\n  if (selection == null) return [];\\n  if (typeof selection === \\\"string\\\") return [selection];\\n  return Array.isArray(selection) ? selection : [];\\n}\\n\\nfunction validateOptionListInvariants(\\n  data: OptionListSchemaInvariantInput,\\n  ctx: z.RefinementCtx,\\n) {\\n  if (\\n    data.minSelections !== undefined &&\\n    data.maxSelections !== undefined &&\\n    data.minSelections > data.maxSelections\\n  ) {\\n    ctx.addIssue({\\n      code: z.ZodIssueCode.custom,\\n      path: [\\\"minSelections\\\"],\\n      message: \\\"`minSelections` cannot be greater than `maxSelections`.\\\",\\n    });\\n  }\\n\\n  const optionIds = new Set<string>();\\n  for (let index = 0; index < data.options.length; index++) {\\n    const optionId = data.options[index]?.id;\\n    if (!optionId) continue;\\n\\n    if (optionIds.has(optionId)) {\\n      ctx.addIssue({\\n        code: z.ZodIssueCode.custom,\\n        path: [\\\"options\\\", index, \\\"id\\\"],\\n        message: `Duplicate option id \\\"${optionId}\\\" is not allowed.`,\\n      });\\n    } else {\\n      optionIds.add(optionId);\\n    }\\n  }\\n\\n  const selectionFields: Array<\\n    [\\\"value\\\" | \\\"defaultValue\\\" | \\\"choice\\\", OptionListSelection | undefined]\\n  > = [\\n    [\\\"value\\\", data.value],\\n    [\\\"defaultValue\\\", data.defaultValue],\\n    [\\\"choice\\\", data.choice],\\n  ];\\n\\n  for (const [fieldName, selection] of selectionFields) {\\n    if (selection == null) continue;\\n\\n    const ids = selectionToIds(selection);\\n    ids.forEach((selectionId, index) => {\\n      if (!optionIds.has(selectionId)) {\\n        ctx.addIssue({\\n          code: z.ZodIssueCode.custom,\\n          path:\\n            typeof selection === \\\"string\\\" ? [fieldName] : [fieldName, index],\\n          message: `Selection id \\\"${selectionId}\\\" must exist in options.`,\\n        });\\n      }\\n    });\\n  }\\n}\\n\\nconst OptionListPropsSchemaBase = z.object({\\n  /**\\n   * Unique identifier for this tool UI instance in the conversation.\\n   *\\n   * Used for:\\n   * - Assistant referencing (\\\"the options above\\\")\\n   * - Receipt generation (linking selections to their source)\\n   * - Narration context\\n   *\\n   * Should be stable across re-renders, meaningful, and unique within the conversation.\\n   *\\n   * @example \\\"option-list-deploy-target\\\", \\\"format-selection\\\"\\n   */\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n  options: z.array(OptionListOptionSchema).min(1),\\n  selectionMode: z.enum([\\\"multi\\\", \\\"single\\\"]).optional(),\\n  /**\\n   * Controlled selection value (advanced / runtime only).\\n   *\\n   * For Tool UI tool payloads, prefer `defaultValue` (initial selection) and\\n   * `choice` (receipt state). Controlled `value` is intentionally excluded\\n   * from `SerializableOptionListSchema` to avoid accidental \\\"controlled but\\n   * non-interactive\\\" states when an LLM includes `value` in args.\\n   */\\n  value: OptionListSelectionSchema,\\n  defaultValue: OptionListSelectionSchema,\\n  /**\\n   * When set, renders the component in receipt state showing the user's choice.\\n   *\\n   * In receipt state:\\n   * - Only the chosen option(s) are shown\\n   * - Actions are hidden\\n   * - The component is read-only\\n   *\\n   * Use this with assistant-ui's `addResult` to show the outcome of a decision.\\n   *\\n   * @example\\n   * ```tsx\\n   * // In a toolkit render function:\\n   * if (result) {\\n   *   return <OptionList {...args} choice={result} />;\\n   * }\\n   * ```\\n   */\\n  choice: OptionListSelectionSchema,\\n  actions: z\\n    .union([z.array(ActionSchema), SerializableActionsConfigSchema])\\n    .optional(),\\n  minSelections: z.number().min(0).optional(),\\n  maxSelections: z.number().min(1).optional(),\\n});\\n\\nexport const OptionListPropsSchema = OptionListPropsSchemaBase.superRefine(\\n  validateOptionListInvariants,\\n);\\n\\nexport type OptionListOption = z.infer<typeof OptionListOptionSchema>;\\n\\nexport type OptionListProps = Omit<\\n  z.infer<typeof OptionListPropsSchema>,\\n  \\\"value\\\" | \\\"defaultValue\\\" | \\\"choice\\\" | \\\"actions\\\"\\n> & {\\n  /** @see OptionListPropsSchema.id */\\n  id: string;\\n  value?: OptionListSelection;\\n  defaultValue?: OptionListSelection;\\n  /** @see OptionListPropsSchema.choice */\\n  choice?: OptionListSelection;\\n  onChange?: (value: OptionListSelection) => void;\\n  actions?: ActionsProp;\\n  onAction?: EmbeddedActionsProps<OptionListSelection>[\\\"onAction\\\"];\\n  onBeforeAction?: EmbeddedActionsProps<OptionListSelection>[\\\"onBeforeAction\\\"];\\n  className?: string;\\n};\\n\\nexport const SerializableOptionListSchema = OptionListPropsSchemaBase.omit({\\n  // Exclude controlled selection from tool/LLM payloads.\\n  value: true,\\n})\\n  .extend({\\n    options: z.array(OptionListOptionSchema.omit({ icon: true })),\\n    actions: z\\n      .union([\\n        z.array(SerializableActionSchema),\\n        SerializableActionsConfigSchema,\\n      ])\\n      .optional(),\\n  })\\n  .strict()\\n  .superRefine(validateOptionListInvariants);\\n\\nexport type SerializableOptionList = z.infer<\\n  typeof SerializableOptionListSchema\\n>;\\n\\nconst SerializableOptionListSchemaContract = defineToolUiContract(\\n  \\\"OptionList\\\",\\n  SerializableOptionListSchema,\\n);\\n\\nexport const parseSerializableOptionList: (\\n  input: unknown,\\n) => SerializableOptionList = SerializableOptionListSchemaContract.parse;\\n\\nexport const safeParseSerializableOptionList: (\\n  input: unknown,\\n) => SerializableOptionList | null =\\n  SerializableOptionListSchemaContract.safeParse;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/option-list/selection.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/option-list/selection.ts\",\n      \"content\": \"import type { OptionListSelection } from \\\"./schema\\\";\\n\\nexport function parseSelectionToIdSet(\\n  value: OptionListSelection | undefined,\\n  mode: \\\"multi\\\" | \\\"single\\\",\\n  maxSelections?: number,\\n): Set<string> {\\n  if (mode === \\\"single\\\") {\\n    const single =\\n      typeof value === \\\"string\\\"\\n        ? value\\n        : Array.isArray(value)\\n          ? value[0]\\n          : null;\\n    return single ? new Set([single]) : new Set();\\n  }\\n\\n  const arr =\\n    typeof value === \\\"string\\\" ? [value] : Array.isArray(value) ? value : [];\\n\\n  return new Set(maxSelections ? arr.slice(0, maxSelections) : arr);\\n}\\n\\nexport function normalizeSelectionForOptions(\\n  selection: Set<string>,\\n  optionIds: Set<string>,\\n): Set<string> {\\n  const normalized = new Set<string>();\\n  for (const id of selection) {\\n    if (optionIds.has(id)) {\\n      normalized.add(id);\\n    }\\n  }\\n  return normalized;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/shared/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn     → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n *   Button → shadcn/ui Button\\n */\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport { Button } from \\\"@/components/ui/button\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/action-buttons.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/shared/action-buttons.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport type { Action } from \\\"./schema\\\";\\nimport { cn, Button } from \\\"./_adapter\\\";\\nimport { useActionButtons } from \\\"./use-action-buttons\\\";\\n\\nexport interface ActionButtonsProps {\\n  actions: Action[];\\n  onAction: (actionId: string) => void | Promise<void>;\\n  onBeforeAction?: (actionId: string) => boolean | Promise<boolean>;\\n  confirmTimeout?: number;\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  className?: string;\\n}\\n\\nexport function ActionButtons({\\n  actions,\\n  onAction,\\n  onBeforeAction,\\n  confirmTimeout = 3000,\\n  align = \\\"right\\\",\\n  className,\\n}: ActionButtonsProps) {\\n  const { actions: resolvedActions, runAction } = useActionButtons({\\n    actions,\\n    onAction,\\n    onBeforeAction,\\n    confirmTimeout,\\n  });\\n\\n  return (\\n    <div\\n      className={cn(\\n        \\\"flex flex-col gap-3\\\",\\n        \\\"@sm/actions:flex-row @sm/actions:flex-wrap @sm/actions:items-center @sm/actions:gap-2\\\",\\n        align === \\\"left\\\" && \\\"@sm/actions:justify-start\\\",\\n        align === \\\"center\\\" && \\\"@sm/actions:justify-center\\\",\\n        align === \\\"right\\\" && \\\"@sm/actions:justify-end\\\",\\n        className,\\n      )}\\n    >\\n      {resolvedActions.map((action) => {\\n        const label = action.currentLabel;\\n        const variant = action.variant || \\\"default\\\";\\n\\n        return (\\n          <Button\\n            key={action.id}\\n            variant={variant}\\n            onClick={() => runAction(action.id)}\\n            disabled={action.isDisabled}\\n            className={cn(\\n              \\\"rounded-full px-4!\\\",\\n              \\\"justify-center\\\",\\n              \\\"min-h-11 w-full text-base\\\",\\n              \\\"@sm/actions:min-h-0 @sm/actions:w-auto @sm/actions:px-3 @sm/actions:py-2 @sm/actions:text-sm\\\",\\n              action.isConfirming &&\\n                \\\"ring-destructive ring-2 ring-offset-2 motion-safe:animate-pulse\\\",\\n            )}\\n            aria-label={\\n              action.shortcut ? `${label} (${action.shortcut})` : label\\n            }\\n          >\\n            {action.isLoading && (\\n              <svg\\n                className=\\\"mr-2 h-4 w-4 motion-safe:animate-spin\\\"\\n                xmlns=\\\"http://www.w3.org/2000/svg\\\"\\n                fill=\\\"none\\\"\\n                viewBox=\\\"0 0 24 24\\\"\\n              >\\n                <circle\\n                  className=\\\"opacity-25\\\"\\n                  cx=\\\"12\\\"\\n                  cy=\\\"12\\\"\\n                  r=\\\"10\\\"\\n                  stroke=\\\"currentColor\\\"\\n                  strokeWidth=\\\"4\\\"\\n                />\\n                <path\\n                  className=\\\"opacity-75\\\"\\n                  fill=\\\"currentColor\\\"\\n                  d=\\\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\\\"\\n                />\\n              </svg>\\n            )}\\n            {action.icon && !action.isLoading && (\\n              <span className=\\\"mr-2\\\">{action.icon}</span>\\n            )}\\n            {label}\\n            {action.shortcut && !action.isLoading && (\\n              <kbd className=\\\"border-border bg-muted ml-2.5 hidden rounded-lg border px-2 py-0.5 font-mono text-xs font-medium sm:inline-block\\\">\\n                {action.shortcut}\\n              </kbd>\\n            )}\\n          </Button>\\n        );\\n      })}\\n    </div>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/actions-config.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/actions-config.ts\",\n      \"content\": \"import type { Action, ActionsConfig } from \\\"./schema\\\";\\n\\nexport type ActionsProp = ActionsConfig | Action[];\\n\\nconst NEGATORY_ACTION_IDS = new Set([\\n  \\\"cancel\\\",\\n  \\\"dismiss\\\",\\n  \\\"skip\\\",\\n  \\\"no\\\",\\n  \\\"reset\\\",\\n  \\\"close\\\",\\n  \\\"decline\\\",\\n  \\\"reject\\\",\\n  \\\"back\\\",\\n  \\\"later\\\",\\n  \\\"not-now\\\",\\n  \\\"maybe-later\\\",\\n]);\\n\\nfunction inferVariant(action: Action): Action {\\n  if (action.variant) return action;\\n  if (NEGATORY_ACTION_IDS.has(action.id)) {\\n    return { ...action, variant: \\\"ghost\\\" };\\n  }\\n  return action;\\n}\\n\\nexport function normalizeActionsConfig(\\n  actions?: ActionsProp,\\n): ActionsConfig | null {\\n  if (!actions) return null;\\n\\n  const rawItems = Array.isArray(actions) ? actions : (actions.items ?? []);\\n\\n  if (rawItems.length === 0) {\\n    return null;\\n  }\\n\\n  const items = rawItems.map(inferVariant);\\n\\n  return Array.isArray(actions)\\n    ? { items }\\n    : {\\n        items,\\n        align: actions.align,\\n        confirmTimeout: actions.confirmTimeout,\\n      };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/embedded-actions.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/embedded-actions.ts\",\n      \"content\": \"import type { ActionsProp } from \\\"./actions-config\\\";\\n\\nexport type EmbeddedActionHandler<TState> = (\\n  actionId: string,\\n  state: TState,\\n) => void | Promise<void>;\\n\\nexport type EmbeddedBeforeActionHandler<TState> = (\\n  actionId: string,\\n  state: TState,\\n) => boolean | Promise<boolean>;\\n\\nexport interface EmbeddedActionsProps<TState> {\\n  actions?: ActionsProp;\\n  onAction?: EmbeddedActionHandler<TState>;\\n  onBeforeAction?: EmbeddedBeforeActionHandler<TState>;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { ReactNode } from \\\"react\\\";\\n\\n/**\\n * Tool UI conventions:\\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\\n * - Schema: `SerializableXSchema`\\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\\n * - Root attrs: `data-tool-ui-id` + `data-slot`\\n */\\n\\n/**\\n * Schema for tool UI identity.\\n *\\n * Every tool UI should have a unique identifier that:\\n * - Is stable across re-renders\\n * - Is meaningful (not auto-generated)\\n * - Is unique within the conversation\\n *\\n * Format recommendation: `{component-type}-{semantic-identifier}`\\n * Examples: \\\"data-table-expenses-q3\\\", \\\"option-list-deploy-target\\\"\\n */\\nexport const ToolUIIdSchema = z.string().min(1);\\n\\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\\n\\n/**\\n * Primary role of a Tool UI surface in a chat context.\\n */\\nexport const ToolUIRoleSchema = z.enum([\\n  \\\"information\\\",\\n  \\\"decision\\\",\\n  \\\"control\\\",\\n  \\\"state\\\",\\n  \\\"composite\\\",\\n]);\\n\\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\\n\\nexport const ToolUIReceiptOutcomeSchema = z.enum([\\n  \\\"success\\\",\\n  \\\"partial\\\",\\n  \\\"failed\\\",\\n  \\\"cancelled\\\",\\n]);\\n\\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\\n\\n/**\\n * Optional receipt metadata: a durable summary of an outcome.\\n */\\nexport const ToolUIReceiptSchema = z.object({\\n  outcome: ToolUIReceiptOutcomeSchema,\\n  summary: z.string().min(1),\\n  identifiers: z.record(z.string(), z.string()).optional(),\\n  at: z.string().datetime(),\\n});\\n\\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\\n\\n/**\\n * Base schema for Tool UI payloads (id + optional role/receipt).\\n */\\nexport const ToolUISurfaceSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n});\\n\\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\\n\\nexport const ActionSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  /**\\n   * Canonical narration the assistant can use after this action is taken.\\n   *\\n   * Example: \\\"I exported the table as CSV.\\\" / \\\"I opened the link in a new tab.\\\"\\n   */\\n  sentence: z.string().optional(),\\n  confirmLabel: z.string().optional(),\\n  variant: z\\n    .enum([\\\"default\\\", \\\"destructive\\\", \\\"secondary\\\", \\\"ghost\\\", \\\"outline\\\"])\\n    .optional(),\\n  icon: z.custom<ReactNode>().optional(),\\n  loading: z.boolean().optional(),\\n  disabled: z.boolean().optional(),\\n  shortcut: z.string().optional(),\\n});\\n\\nexport type Action = z.infer<typeof ActionSchema>;\\nexport type LocalAction = Action;\\nexport type DecisionAction = Action;\\n\\nexport const DecisionResultSchema = z.object({\\n  kind: z.literal(\\\"decision\\\"),\\n  version: z.literal(1),\\n  decisionId: z.string().min(1),\\n  actionId: z.string().min(1),\\n  actionLabel: z.string().min(1),\\n  at: z.string().datetime(),\\n  payload: z.record(z.string(), z.unknown()).optional(),\\n});\\n\\nexport type DecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n> = Omit<z.infer<typeof DecisionResultSchema>, \\\"payload\\\"> & {\\n  payload?: TPayload;\\n};\\n\\nexport function createDecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n>(args: {\\n  decisionId: string;\\n  action: { id: string; label: string };\\n  payload?: TPayload;\\n}): DecisionResult<TPayload> {\\n  return {\\n    kind: \\\"decision\\\",\\n    version: 1,\\n    decisionId: args.decisionId,\\n    actionId: args.action.id,\\n    actionLabel: args.action.label,\\n    at: new Date().toISOString(),\\n    payload: args.payload,\\n  };\\n}\\n\\nexport const ActionButtonsPropsSchema = z.object({\\n  actions: z.array(ActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n  className: z.string().optional(),\\n});\\n\\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\\n  actions: z.array(SerializableActionSchema),\\n}).omit({ className: true });\\n\\nexport interface ActionsConfig {\\n  items: Action[];\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  confirmTimeout?: number;\\n}\\n\\nexport const SerializableActionsConfigSchema = z.object({\\n  items: z.array(SerializableActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n});\\n\\nexport type SerializableActionsConfig = z.infer<\\n  typeof SerializableActionsConfigSchema\\n>;\\n\\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/use-action-buttons.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/shared/use-action-buttons.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { useCallback, useEffect, useMemo, useRef, useState } from \\\"react\\\";\\nimport type { Action } from \\\"./schema\\\";\\n\\nexport type UseActionButtonsOptions = {\\n  actions: Action[];\\n  onAction: (actionId: string) => void | Promise<void>;\\n  onBeforeAction?: (actionId: string) => boolean | Promise<boolean>;\\n  confirmTimeout?: number;\\n};\\n\\nexport type UseActionButtonsResult = {\\n  actions: Array<\\n    Action & {\\n      currentLabel: string;\\n      isConfirming: boolean;\\n      isExecuting: boolean;\\n      isDisabled: boolean;\\n      isLoading: boolean;\\n    }\\n  >;\\n  runAction: (actionId: string) => Promise<void>;\\n  confirmingActionId: string | null;\\n  executingActionId: string | null;\\n};\\n\\ntype ActionExecutionLock = {\\n  tryAcquire: () => boolean;\\n  release: () => void;\\n};\\n\\nexport function createActionExecutionLock(): ActionExecutionLock {\\n  let locked = false;\\n\\n  return {\\n    tryAcquire: () => {\\n      if (locked) return false;\\n      locked = true;\\n      return true;\\n    },\\n    release: () => {\\n      locked = false;\\n    },\\n  };\\n}\\n\\nexport function useActionButtons(\\n  options: UseActionButtonsOptions,\\n): UseActionButtonsResult {\\n  const { actions, onAction, onBeforeAction, confirmTimeout = 3000 } = options;\\n\\n  const [confirmingActionId, setConfirmingActionId] = useState<string | null>(\\n    null,\\n  );\\n  const [executingActionId, setExecutingActionId] = useState<string | null>(\\n    null,\\n  );\\n  const executionLockRef = useRef<ActionExecutionLock>(\\n    createActionExecutionLock(),\\n  );\\n\\n  useEffect(() => {\\n    if (!confirmingActionId) return;\\n    const id = setTimeout(() => setConfirmingActionId(null), confirmTimeout);\\n    return () => clearTimeout(id);\\n  }, [confirmingActionId, confirmTimeout]);\\n\\n  useEffect(() => {\\n    if (!confirmingActionId) return;\\n    const handleKeyDown = (e: KeyboardEvent) => {\\n      if (e.key === \\\"Escape\\\") {\\n        setConfirmingActionId(null);\\n      }\\n    };\\n\\n    window.addEventListener(\\\"keydown\\\", handleKeyDown);\\n    return () => window.removeEventListener(\\\"keydown\\\", handleKeyDown);\\n  }, [confirmingActionId]);\\n\\n  const runAction = useCallback(\\n    async (actionId: string) => {\\n      const action = actions.find((a) => a.id === actionId);\\n      if (!action) return;\\n\\n      const isAnyActionExecuting = executingActionId !== null;\\n      if (action.disabled || action.loading || isAnyActionExecuting) {\\n        return;\\n      }\\n\\n      if (action.confirmLabel && confirmingActionId !== action.id) {\\n        setConfirmingActionId(action.id);\\n        return;\\n      }\\n\\n      if (!executionLockRef.current.tryAcquire()) {\\n        return;\\n      }\\n\\n      if (onBeforeAction) {\\n        const shouldProceed = await onBeforeAction(action.id);\\n        if (!shouldProceed) {\\n          setConfirmingActionId(null);\\n          executionLockRef.current.release();\\n          return;\\n        }\\n      }\\n\\n      try {\\n        setExecutingActionId(action.id);\\n        await onAction(action.id);\\n      } finally {\\n        executionLockRef.current.release();\\n        setExecutingActionId(null);\\n        setConfirmingActionId(null);\\n      }\\n    },\\n    [actions, confirmingActionId, executingActionId, onAction, onBeforeAction],\\n  );\\n\\n  const resolvedActions = useMemo(\\n    () =>\\n      actions.map((action) => {\\n        const isConfirming = confirmingActionId === action.id;\\n        const isThisActionExecuting = executingActionId === action.id;\\n        const isLoading = action.loading || isThisActionExecuting;\\n        const isDisabled =\\n          action.disabled ||\\n          (executingActionId !== null && !isThisActionExecuting);\\n        const currentLabel =\\n          isConfirming && action.confirmLabel\\n            ? action.confirmLabel\\n            : action.label;\\n\\n        return {\\n          ...action,\\n          currentLabel,\\n          isConfirming,\\n          isExecuting: isThisActionExecuting,\\n          isDisabled,\\n          isLoading,\\n        };\\n      }),\\n    [actions, confirmingActionId, executingActionId],\\n  );\\n\\n  return {\\n    actions: resolvedActions,\\n    runAction,\\n    confirmingActionId,\\n    executingActionId,\\n  };\\n}\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/order-summary.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"order-summary\",\n  \"type\": \"registry:block\",\n  \"title\": \"Order Summary\",\n  \"description\": \"Itemized purchase confirmation with pricing.\",\n  \"dependencies\": [\n    \"lucide-react\",\n    \"zod\"\n  ],\n  \"registryDependencies\": [\n    \"button\",\n    \"separator\",\n    \"skeleton\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/order-summary/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/order-summary/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn        → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n *   Button    → shadcn/ui Button\\n *   Separator → shadcn/ui Separator\\n *   Skeleton  → shadcn/ui Skeleton\\n */\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport { Button } from \\\"@/components/ui/button\\\";\\nexport { Separator } from \\\"@/components/ui/separator\\\";\\nexport { Skeleton } from \\\"@/components/ui/skeleton\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/order-summary/index.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/order-summary/index.tsx\",\n      \"content\": \"export { OrderSummary } from \\\"./order-summary\\\";\\nexport type {\\n  OrderSummaryDisplayProps,\\n  OrderSummaryReceiptProps,\\n  OrderSummaryCompoundComponent,\\n} from \\\"./order-summary\\\";\\nexport {\\n  type SerializableOrderSummary,\\n  type OrderSummaryProps,\\n  type OrderSummaryVariant,\\n  type OrderItem,\\n  type Pricing,\\n  type OrderDecision,\\n} from \\\"./schema\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/order-summary/order-summary.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/order-summary/order-summary.tsx\",\n      \"content\": \"import { CheckCircle, Package } from \\\"lucide-react\\\";\\nimport type { ReactElement } from \\\"react\\\";\\nimport { cn, Separator } from \\\"./_adapter\\\";\\nimport type {\\n  OrderSummaryProps,\\n  OrderItem,\\n  Pricing,\\n  OrderDecision,\\n  OrderSummaryVariant,\\n} from \\\"./schema\\\";\\n\\nfunction formatCurrency(amount: number, currency: string): string {\\n  try {\\n    return new Intl.NumberFormat(undefined, {\\n      style: \\\"currency\\\",\\n      currency,\\n    }).format(amount);\\n  } catch {\\n    return `${currency} ${amount.toFixed(2)}`;\\n  }\\n}\\n\\nfunction formatQuantity(quantity: number): string {\\n  return quantity === 1 ? \\\"\\\" : `Qty: ${quantity}`;\\n}\\n\\nfunction ItemImage({ src, alt }: { src?: string; alt: string }) {\\n  if (!src) {\\n    return (\\n      <div className=\\\"bg-muted flex h-12 w-12 shrink-0 items-center justify-center rounded-md\\\">\\n        <Package\\n          aria-hidden=\\\"true\\\"\\n          focusable=\\\"false\\\"\\n          className=\\\"text-muted-foreground h-5 w-5\\\"\\n        />\\n      </div>\\n    );\\n  }\\n\\n  return (\\n    <img\\n      src={src}\\n      alt={alt}\\n      width={48}\\n      height={48}\\n      className=\\\"h-12 w-12 shrink-0 rounded-md object-cover\\\"\\n    />\\n  );\\n}\\n\\nfunction OrderItemRow({\\n  item,\\n  currency,\\n}: {\\n  item: OrderItem;\\n  currency: string;\\n}) {\\n  const quantity = item.quantity ?? 1;\\n  const quantityText = formatQuantity(quantity);\\n  const hasDescription = item.description || quantityText;\\n  const lineTotal = item.unitPrice * quantity;\\n\\n  return (\\n    <div className=\\\"flex gap-3\\\">\\n      <ItemImage src={item.imageUrl} alt={item.name} />\\n      <div className=\\\"flex min-w-0 flex-1 items-center justify-between\\\">\\n        <div className=\\\"flex min-w-0 flex-1 flex-col gap-0.5\\\">\\n          <div className=\\\"flex items-center justify-between\\\">\\n            <span className=\\\"truncate text-sm font-medium\\\">{item.name}</span>\\n            <span className=\\\"truncate text-sm tabular-nums\\\">\\n              {formatCurrency(lineTotal, currency)}\\n            </span>\\n          </div>\\n          {hasDescription && (\\n            <div className=\\\"text-muted-foreground truncate text-sm\\\">\\n              {[item.description, quantityText].filter(Boolean).join(\\\" · \\\")}\\n            </div>\\n          )}\\n        </div>\\n      </div>\\n    </div>\\n  );\\n}\\n\\nfunction PricingBreakdown({\\n  pricing,\\n  className,\\n}: {\\n  pricing: Pricing;\\n  className?: string;\\n}) {\\n  const currency = pricing.currency ?? \\\"USD\\\";\\n\\n  return (\\n    <dl className={cn(\\\"flex flex-col gap-2 text-sm\\\", className)}>\\n      <div className=\\\"flex justify-between gap-4\\\">\\n        <dt className=\\\"text-muted-foreground\\\">Subtotal</dt>\\n        <dd className=\\\"tabular-nums\\\">\\n          {formatCurrency(pricing.subtotal, currency)}\\n        </dd>\\n      </div>\\n\\n      {pricing.discount !== undefined && pricing.discount > 0 && (\\n        <div className=\\\"flex justify-between gap-4 text-green-600 dark:text-green-500\\\">\\n          <dt>{pricing.discountLabel || \\\"Discount\\\"}</dt>\\n          <dd className=\\\"tabular-nums\\\">\\n            -{formatCurrency(pricing.discount, currency)}\\n          </dd>\\n        </div>\\n      )}\\n\\n      {pricing.shipping !== undefined && (\\n        <div className=\\\"flex justify-between gap-4\\\">\\n          <dt className=\\\"text-muted-foreground\\\">Shipping</dt>\\n          <dd className=\\\"tabular-nums\\\">\\n            {pricing.shipping === 0\\n              ? \\\"Free\\\"\\n              : formatCurrency(pricing.shipping, currency)}\\n          </dd>\\n        </div>\\n      )}\\n\\n      {pricing.tax !== undefined && (\\n        <div className=\\\"flex justify-between gap-4\\\">\\n          <dt className=\\\"text-muted-foreground\\\">{pricing.taxLabel || \\\"Tax\\\"}</dt>\\n          <dd className=\\\"tabular-nums\\\">\\n            {formatCurrency(pricing.tax, currency)}\\n          </dd>\\n        </div>\\n      )}\\n\\n      <div className=\\\"flex justify-between gap-4\\\">\\n        <dt className=\\\"font-medium\\\">Total</dt>\\n        <dd className=\\\"font-semibold tabular-nums\\\">\\n          {formatCurrency(pricing.total, currency)}\\n        </dd>\\n      </div>\\n    </dl>\\n  );\\n}\\n\\nfunction formatDate(isoString: string): string | undefined {\\n  try {\\n    const date = new Date(isoString);\\n    if (isNaN(date.getTime())) return undefined;\\n    return date.toLocaleDateString(undefined, {\\n      month: \\\"short\\\",\\n      day: \\\"numeric\\\",\\n      year: \\\"numeric\\\",\\n    });\\n  } catch {\\n    return undefined;\\n  }\\n}\\n\\nfunction ReceiptBadge({\\n  orderId,\\n  confirmedAt,\\n}: {\\n  orderId?: string;\\n  confirmedAt?: string;\\n}) {\\n  const formattedDate = confirmedAt ? formatDate(confirmedAt) : undefined;\\n\\n  const parts = [orderId && `#${orderId}`, formattedDate].filter(Boolean);\\n  if (parts.length === 0) return null;\\n\\n  return (\\n    <p className=\\\"text-muted-foreground mt-1 text-sm\\\">{parts.join(\\\" · \\\")}</p>\\n  );\\n}\\n\\nfunction OrderSummaryRoot({\\n  id,\\n  title = \\\"Order Summary\\\",\\n  variant,\\n  items,\\n  pricing,\\n  choice,\\n  className,\\n}: OrderSummaryProps) {\\n  const titleId = `${id}-title`;\\n  const resolvedVariant: OrderSummaryVariant =\\n    variant ?? (choice === undefined ? \\\"summary\\\" : \\\"receipt\\\");\\n  const isReceipt = resolvedVariant === \\\"receipt\\\";\\n  const isMalformedPayload =\\n    !Array.isArray(items) ||\\n    items.length === 0 ||\\n    pricing == null ||\\n    (isReceipt && choice === undefined);\\n\\n  if (isMalformedPayload) {\\n    return (\\n      <article\\n        data-slot=\\\"order-summary\\\"\\n        data-tool-ui-id={id}\\n        aria-labelledby={titleId}\\n        className={cn(\\\"flex max-w-md min-w-80 flex-col gap-3\\\", className)}\\n      >\\n        <div className=\\\"text-card-foreground rounded-lg border bg-card p-4 shadow-sm\\\">\\n          <h2 id={titleId} className=\\\"text-base font-semibold\\\">\\n            {title}\\n          </h2>\\n          <p className=\\\"text-muted-foreground mt-2 text-sm\\\">\\n            Unable to render order summary\\n          </p>\\n        </div>\\n      </article>\\n    );\\n  }\\n\\n  return (\\n    <article\\n      data-slot=\\\"order-summary\\\"\\n      data-tool-ui-id={id}\\n      aria-labelledby={titleId}\\n      className={cn(\\\"flex max-w-md min-w-80 flex-col gap-3\\\", className)}\\n    >\\n      <div\\n        className={cn(\\n          \\\"text-card-foreground rounded-lg border shadow-sm\\\",\\n          isReceipt ? \\\"bg-card/60\\\" : \\\"bg-card\\\",\\n        )}\\n      >\\n        <div className={cn(\\\"space-y-4 p-4\\\", isReceipt && \\\"opacity-95\\\")}>\\n          <div>\\n            <h2\\n              id={titleId}\\n              className=\\\"flex items-center gap-2 text-base font-semibold\\\"\\n            >\\n              {isReceipt && (\\n                <CheckCircle\\n                  aria-hidden=\\\"true\\\"\\n                  focusable=\\\"false\\\"\\n                  className=\\\"h-5 w-5 text-green-600 dark:text-green-500\\\"\\n                />\\n              )}\\n              {title}\\n            </h2>\\n            {isReceipt && choice && (\\n              <ReceiptBadge\\n                orderId={choice.orderId}\\n                confirmedAt={choice.confirmedAt}\\n              />\\n            )}\\n          </div>\\n\\n          <div className=\\\"space-y-3\\\">\\n            {items.map((item) => (\\n              <OrderItemRow\\n                key={item.id}\\n                item={item}\\n                currency={pricing.currency ?? \\\"USD\\\"}\\n              />\\n            ))}\\n          </div>\\n\\n          <Separator />\\n\\n          <PricingBreakdown pricing={pricing} />\\n        </div>\\n      </div>\\n    </article>\\n  );\\n}\\n\\nexport type OrderSummaryDisplayProps = OrderSummaryProps;\\n\\nfunction OrderSummaryDisplay(props: OrderSummaryDisplayProps) {\\n  return <OrderSummaryRoot {...props} variant=\\\"summary\\\" />;\\n}\\n\\nexport interface OrderSummaryReceiptProps extends Omit<\\n  OrderSummaryProps,\\n  \\\"choice\\\"\\n> {\\n  choice: OrderDecision;\\n}\\n\\nfunction OrderSummaryReceipt(props: OrderSummaryReceiptProps) {\\n  return <OrderSummaryRoot {...props} variant=\\\"receipt\\\" />;\\n}\\n\\nexport interface OrderSummaryCompoundComponent {\\n  (props: OrderSummaryProps): ReactElement;\\n  Display: (props: OrderSummaryDisplayProps) => ReactElement;\\n  Receipt: (props: OrderSummaryReceiptProps) => ReactElement;\\n}\\n\\nexport const OrderSummary: OrderSummaryCompoundComponent = Object.assign(\\n  OrderSummaryRoot,\\n  {\\n    Display: OrderSummaryDisplay,\\n    Receipt: OrderSummaryReceipt,\\n  },\\n);\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/order-summary/README.md\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/order-summary/README.md\",\n      \"content\": \"# Order Summary\\n\\nImplementation for the \\\"order-summary\\\" Tool UI surface.\\n\\n## Files\\n\\n- public exports: components/tool-ui/order-summary/index.tsx\\n- serializable schema + parse helpers: components/tool-ui/order-summary/schema.ts\\n\\n## Companion assets\\n\\n- Docs page: app/docs/order-summary/content.mdx\\n- Preset payload: lib/presets/order-summary.ts\\n\\n## Quick check\\n\\nRun this after edits:\\n\\npnpm test\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/order-summary/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/order-summary/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\nimport { ToolUIIdSchema, ToolUIRoleSchema } from \\\"../shared/schema\\\";\\n\\nexport const OrderItemSchema = z.object({\\n  id: z.string(),\\n  name: z.string(),\\n  description: z.string().optional(),\\n  imageUrl: z.string().url().optional(),\\n  quantity: z.number().int().positive().optional(),\\n  unitPrice: z.number(),\\n});\\n\\nexport type OrderItem = z.infer<typeof OrderItemSchema>;\\n\\nconst OrderItemsSchema = z\\n  .array(OrderItemSchema)\\n  .min(1)\\n  .superRefine((items, ctx) => {\\n    const seenIds = new Set<string>();\\n\\n    for (const [index, item] of items.entries()) {\\n      if (seenIds.has(item.id)) {\\n        ctx.addIssue({\\n          code: z.ZodIssueCode.custom,\\n          message: `Duplicate item id: \\\"${item.id}\\\"`,\\n          path: [index, \\\"id\\\"],\\n        });\\n      }\\n\\n      seenIds.add(item.id);\\n    }\\n  });\\n\\nexport const PricingSchema = z.object({\\n  subtotal: z.number(),\\n  tax: z.number().optional(),\\n  taxLabel: z.string().optional(),\\n  shipping: z.number().optional(),\\n  discount: z.number().nonnegative().optional(),\\n  discountLabel: z.string().optional(),\\n  total: z.number(),\\n  currency: z.string().optional(),\\n});\\n\\nexport type Pricing = z.infer<typeof PricingSchema>;\\n\\nexport const OrderSummaryVariantSchema = z.enum([\\\"summary\\\", \\\"receipt\\\"]);\\nexport type OrderSummaryVariant = z.infer<typeof OrderSummaryVariantSchema>;\\n\\nexport const OrderDecisionSchema = z.object({\\n  action: z.literal(\\\"confirm\\\"),\\n  orderId: z.string().optional(),\\n  confirmedAt: z.string().datetime().optional(),\\n});\\n\\nexport type OrderDecision = z.infer<typeof OrderDecisionSchema>;\\n\\nexport const SerializableOrderSummarySchema = z\\n  .object({\\n    id: ToolUIIdSchema,\\n    role: ToolUIRoleSchema.optional(),\\n    title: z.string().optional(),\\n    variant: OrderSummaryVariantSchema.optional(),\\n    items: OrderItemsSchema,\\n    pricing: PricingSchema,\\n    choice: OrderDecisionSchema.optional(),\\n  })\\n  .strict()\\n  .superRefine((value, ctx) => {\\n    if (value.variant === \\\"receipt\\\" && value.choice === undefined) {\\n      ctx.addIssue({\\n        code: z.ZodIssueCode.custom,\\n        message: 'Receipt variant requires \\\"choice\\\".',\\n        path: [\\\"choice\\\"],\\n      });\\n    }\\n\\n    if (value.variant === \\\"summary\\\" && value.choice !== undefined) {\\n      ctx.addIssue({\\n        code: z.ZodIssueCode.custom,\\n        message: 'Summary variant cannot include \\\"choice\\\".',\\n        path: [\\\"choice\\\"],\\n      });\\n    }\\n  });\\n\\nexport type SerializableOrderSummary = z.infer<\\n  typeof SerializableOrderSummarySchema\\n>;\\n\\nconst SerializableOrderSummarySchemaContract = defineToolUiContract(\\n  \\\"OrderSummary\\\",\\n  SerializableOrderSummarySchema,\\n);\\n\\nexport const parseSerializableOrderSummary: (\\n  input: unknown,\\n) => SerializableOrderSummary = SerializableOrderSummarySchemaContract.parse;\\n\\nexport const safeParseSerializableOrderSummary: (\\n  input: unknown,\\n) => SerializableOrderSummary | null =\\n  SerializableOrderSummarySchemaContract.safeParse;\\n\\nexport interface OrderSummaryProps extends SerializableOrderSummary {\\n  className?: string;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { ReactNode } from \\\"react\\\";\\n\\n/**\\n * Tool UI conventions:\\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\\n * - Schema: `SerializableXSchema`\\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\\n * - Root attrs: `data-tool-ui-id` + `data-slot`\\n */\\n\\n/**\\n * Schema for tool UI identity.\\n *\\n * Every tool UI should have a unique identifier that:\\n * - Is stable across re-renders\\n * - Is meaningful (not auto-generated)\\n * - Is unique within the conversation\\n *\\n * Format recommendation: `{component-type}-{semantic-identifier}`\\n * Examples: \\\"data-table-expenses-q3\\\", \\\"option-list-deploy-target\\\"\\n */\\nexport const ToolUIIdSchema = z.string().min(1);\\n\\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\\n\\n/**\\n * Primary role of a Tool UI surface in a chat context.\\n */\\nexport const ToolUIRoleSchema = z.enum([\\n  \\\"information\\\",\\n  \\\"decision\\\",\\n  \\\"control\\\",\\n  \\\"state\\\",\\n  \\\"composite\\\",\\n]);\\n\\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\\n\\nexport const ToolUIReceiptOutcomeSchema = z.enum([\\n  \\\"success\\\",\\n  \\\"partial\\\",\\n  \\\"failed\\\",\\n  \\\"cancelled\\\",\\n]);\\n\\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\\n\\n/**\\n * Optional receipt metadata: a durable summary of an outcome.\\n */\\nexport const ToolUIReceiptSchema = z.object({\\n  outcome: ToolUIReceiptOutcomeSchema,\\n  summary: z.string().min(1),\\n  identifiers: z.record(z.string(), z.string()).optional(),\\n  at: z.string().datetime(),\\n});\\n\\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\\n\\n/**\\n * Base schema for Tool UI payloads (id + optional role/receipt).\\n */\\nexport const ToolUISurfaceSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n});\\n\\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\\n\\nexport const ActionSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  /**\\n   * Canonical narration the assistant can use after this action is taken.\\n   *\\n   * Example: \\\"I exported the table as CSV.\\\" / \\\"I opened the link in a new tab.\\\"\\n   */\\n  sentence: z.string().optional(),\\n  confirmLabel: z.string().optional(),\\n  variant: z\\n    .enum([\\\"default\\\", \\\"destructive\\\", \\\"secondary\\\", \\\"ghost\\\", \\\"outline\\\"])\\n    .optional(),\\n  icon: z.custom<ReactNode>().optional(),\\n  loading: z.boolean().optional(),\\n  disabled: z.boolean().optional(),\\n  shortcut: z.string().optional(),\\n});\\n\\nexport type Action = z.infer<typeof ActionSchema>;\\nexport type LocalAction = Action;\\nexport type DecisionAction = Action;\\n\\nexport const DecisionResultSchema = z.object({\\n  kind: z.literal(\\\"decision\\\"),\\n  version: z.literal(1),\\n  decisionId: z.string().min(1),\\n  actionId: z.string().min(1),\\n  actionLabel: z.string().min(1),\\n  at: z.string().datetime(),\\n  payload: z.record(z.string(), z.unknown()).optional(),\\n});\\n\\nexport type DecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n> = Omit<z.infer<typeof DecisionResultSchema>, \\\"payload\\\"> & {\\n  payload?: TPayload;\\n};\\n\\nexport function createDecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n>(args: {\\n  decisionId: string;\\n  action: { id: string; label: string };\\n  payload?: TPayload;\\n}): DecisionResult<TPayload> {\\n  return {\\n    kind: \\\"decision\\\",\\n    version: 1,\\n    decisionId: args.decisionId,\\n    actionId: args.action.id,\\n    actionLabel: args.action.label,\\n    at: new Date().toISOString(),\\n    payload: args.payload,\\n  };\\n}\\n\\nexport const ActionButtonsPropsSchema = z.object({\\n  actions: z.array(ActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n  className: z.string().optional(),\\n});\\n\\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\\n  actions: z.array(SerializableActionSchema),\\n}).omit({ className: true });\\n\\nexport interface ActionsConfig {\\n  items: Action[];\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  confirmTimeout?: number;\\n}\\n\\nexport const SerializableActionsConfigSchema = z.object({\\n  items: z.array(SerializableActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n});\\n\\nexport type SerializableActionsConfig = z.infer<\\n  typeof SerializableActionsConfigSchema\\n>;\\n\\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/parameter-slider.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"parameter-slider\",\n  \"type\": \"registry:block\",\n  \"title\": \"Parameter Slider\",\n  \"description\": \"Numeric parameter adjustment controls.\",\n  \"dependencies\": [\n    \"@radix-ui/react-slider\",\n    \"zod\"\n  ],\n  \"registryDependencies\": [\n    \"button\",\n    \"separator\",\n    \"slider\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/parameter-slider/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/parameter-slider/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn        → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n *   Button    → shadcn/ui Button\\n *   Separator → shadcn/ui Separator\\n *   Slider    → shadcn/ui Slider\\n */\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport { Button } from \\\"@/components/ui/button\\\";\\nexport { Separator } from \\\"@/components/ui/separator\\\";\\nexport { Slider } from \\\"@/components/ui/slider\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/parameter-slider/index.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/parameter-slider/index.tsx\",\n      \"content\": \"export { ParameterSlider } from \\\"./parameter-slider\\\";\\nexport type {\\n  ParameterSliderProps,\\n  SliderConfig,\\n  SliderValue,\\n  SerializableParameterSlider,\\n} from \\\"./schema\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/parameter-slider/math.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/parameter-slider/math.ts\",\n      \"content\": \"import type { SliderConfig, SliderValue } from \\\"./schema\\\";\\n\\ntype SliderPercentInput = {\\n  value: number;\\n  min: number;\\n  max: number;\\n};\\n\\nfunction clampPercent(value: number): number {\\n  if (!Number.isFinite(value)) return 0;\\n  return Math.max(0, Math.min(100, value));\\n}\\n\\nexport function sliderRangeToPercent({\\n  value,\\n  min,\\n  max,\\n}: SliderPercentInput): number {\\n  const range = max - min;\\n  if (!Number.isFinite(range) || range <= 0) return 0;\\n  return clampPercent(((value - min) / range) * 100);\\n}\\n\\nexport function createSliderValueSnapshot(\\n  sliders: SliderConfig[],\\n): SliderValue[] {\\n  return sliders.map((slider) => ({ id: slider.id, value: slider.value }));\\n}\\n\\nexport function createSliderSignature(sliders: SliderConfig[]): string {\\n  return JSON.stringify(\\n    sliders.map(({ id, min, max, step, value, unit, precision }) => ({\\n      id,\\n      min,\\n      max,\\n      step: step ?? 1,\\n      value,\\n      unit: unit ?? \\\"\\\",\\n      precision: precision ?? null,\\n    })),\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/parameter-slider/parameter-slider.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/parameter-slider/parameter-slider.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport {\\n  useCallback,\\n  useEffect,\\n  useLayoutEffect,\\n  useMemo,\\n  useRef,\\n  useState,\\n} from \\\"react\\\";\\nimport * as SliderPrimitive from \\\"@radix-ui/react-slider\\\";\\nimport type { ParameterSliderProps, SliderConfig, SliderValue } from \\\"./schema\\\";\\nimport { ActionButtons } from \\\"../shared/action-buttons\\\";\\nimport { normalizeActionsConfig } from \\\"../shared/actions-config\\\";\\nimport { useControllableState } from \\\"../shared/use-controllable-state\\\";\\nimport { useSignatureReset } from \\\"../shared/use-signature-reset\\\";\\n\\nimport { cn } from \\\"./_adapter\\\";\\nimport {\\n  createSliderSignature,\\n  createSliderValueSnapshot,\\n  sliderRangeToPercent,\\n} from \\\"./math\\\";\\n\\nfunction formatSignedValue(\\n  value: number,\\n  min: number,\\n  max: number,\\n  precision?: number,\\n  unit?: string,\\n): string {\\n  const crossesZero = min < 0 && max > 0;\\n  const fixed =\\n    precision !== undefined ? value.toFixed(precision) : String(value);\\n  const numericPart = crossesZero && value >= 0 ? `+${fixed}` : fixed;\\n  return unit ? `${numericPart} ${unit}` : numericPart;\\n}\\n\\nfunction getAriaValueText(\\n  value: number,\\n  min: number,\\n  max: number,\\n  unit?: string,\\n): string {\\n  const crossesZero = min < 0 && max > 0;\\n  if (crossesZero) {\\n    if (value > 0) {\\n      return unit ? `plus ${value} ${unit}` : `plus ${value}`;\\n    } else if (value < 0) {\\n      return unit\\n        ? `minus ${Math.abs(value)} ${unit}`\\n        : `minus ${Math.abs(value)}`;\\n    }\\n  }\\n  return unit ? `${value} ${unit}` : String(value);\\n}\\n\\nconst TICK_COUNT = 16;\\nconst TEXT_PADDING_X = 4;\\nconst TEXT_PADDING_X_OUTER = 0; // Less inset on outer-facing side (near edges)\\nconst TEXT_PADDING_Y = 2;\\nconst DETECTION_MARGIN_X = 12;\\nconst DETECTION_MARGIN_X_OUTER = 4; // Small margin at edges for steep falloff - segments fully close at terminal positions\\nconst DETECTION_MARGIN_Y = 12;\\nconst TRACK_HEIGHT = 48;\\nconst TEXT_RELEASE_INSET = 8;\\nconst TRACK_EDGE_INSET = 4; // px from track edge - keeps elements visible at extremes\\nconst THUMB_WIDTH = 12; // w-3\\n// Text vertical offset: raised slightly from center\\n// Positive = raised, negative = lowered\\nconst TEXT_VERTICAL_OFFSET = 0.5;\\n\\nfunction clampPercent(value: number): number {\\n  if (!Number.isFinite(value)) return 0;\\n  return Math.max(0, Math.min(100, value));\\n}\\n\\n// Convert a percentage (0-100) to an inset position string\\n// At 0%: 4px from left edge; at 100%: 4px from right edge\\nfunction toInsetPosition(percent: number): string {\\n  const safePercent = clampPercent(percent);\\n  return `calc(${TRACK_EDGE_INSET}px + (100% - ${TRACK_EDGE_INSET * 2}px) * ${safePercent / 100})`;\\n}\\n\\n// Radix keeps the thumb in bounds by applying a percent-dependent px offset.\\n// Matching this for fill clipping prevents handle/fill drift near extremes.\\nfunction getRadixThumbInBoundsOffsetPx(percent: number): number {\\n  const safePercent = clampPercent(percent);\\n  const halfWidth = THUMB_WIDTH / 2;\\n  return halfWidth - (safePercent * halfWidth) / 50;\\n}\\n\\nfunction toRadixThumbPosition(percent: number): string {\\n  const safePercent = clampPercent(percent);\\n  const offsetPx = getRadixThumbInBoundsOffsetPx(safePercent);\\n  return `calc(${safePercent}% + ${offsetPx}px)`;\\n}\\n\\nfunction signedDistanceToRoundedRect(\\n  px: number,\\n  py: number,\\n  left: number,\\n  right: number,\\n  top: number,\\n  bottom: number,\\n  radiusLeft: number,\\n  radiusRight: number,\\n): number {\\n  const innerLeft = left + radiusLeft;\\n  const innerRight = right - radiusRight;\\n  const innerTop = top + Math.max(radiusLeft, radiusRight);\\n  const innerBottom = bottom - Math.max(radiusLeft, radiusRight);\\n\\n  const inLeftCorner = px < innerLeft;\\n  const inRightCorner = px > innerRight;\\n  const inCornerY = py < innerTop || py > innerBottom;\\n\\n  if ((inLeftCorner || inRightCorner) && inCornerY) {\\n    const radius = inLeftCorner ? radiusLeft : radiusRight;\\n    const cornerX = inLeftCorner ? innerLeft : innerRight;\\n    const cornerY = py < innerTop ? top + radius : bottom - radius;\\n    const distToCornerCenter = Math.hypot(px - cornerX, py - cornerY);\\n    return distToCornerCenter - radius;\\n  }\\n\\n  const dx = Math.max(left - px, px - right, 0);\\n  const dy = Math.max(top - py, py - bottom, 0);\\n\\n  if (dx === 0 && dy === 0) {\\n    return -Math.min(px - left, right - px, py - top, bottom - py);\\n  }\\n\\n  return Math.max(dx, dy);\\n}\\n\\nconst OUTER_EDGE_RADIUS_FACTOR = 0.3; // Reduced radius on outer-facing sides for steeper falloff\\n\\nfunction calculateGap(\\n  thumbCenterX: number,\\n  textRect: { left: number; right: number; height: number; centerY: number },\\n  isLeftAligned: boolean,\\n): number {\\n  const { left, right, height, centerY } = textRect;\\n  // Asymmetric padding/margin: outer-facing side has less padding, more margin\\n  const paddingLeft = isLeftAligned ? TEXT_PADDING_X_OUTER : TEXT_PADDING_X;\\n  const paddingRight = isLeftAligned ? TEXT_PADDING_X : TEXT_PADDING_X_OUTER;\\n  const marginLeft = isLeftAligned\\n    ? DETECTION_MARGIN_X_OUTER\\n    : DETECTION_MARGIN_X;\\n  const marginRight = isLeftAligned\\n    ? DETECTION_MARGIN_X\\n    : DETECTION_MARGIN_X_OUTER;\\n  const paddingY = TEXT_PADDING_Y;\\n  const marginY = DETECTION_MARGIN_Y;\\n  const thumbCenterY = centerY;\\n\\n  // Inner boundary (where max gap occurs)\\n  const innerLeft = left - paddingLeft;\\n  const innerRight = right + paddingRight;\\n  const innerTop = centerY - height / 2 - paddingY;\\n  const innerBottom = centerY + height / 2 + paddingY;\\n  const innerHeight = height + paddingY * 2;\\n  const innerRadius = innerHeight / 2;\\n  // Smaller radius on outer-facing side (left for label, right for value)\\n  const innerRadiusLeft = isLeftAligned\\n    ? innerRadius * OUTER_EDGE_RADIUS_FACTOR\\n    : innerRadius;\\n  const innerRadiusRight = isLeftAligned\\n    ? innerRadius\\n    : innerRadius * OUTER_EDGE_RADIUS_FACTOR;\\n\\n  // Outer boundary (where effect starts) - proportionally larger\\n  const outerLeft = left - paddingLeft - marginLeft;\\n  const outerRight = right + paddingRight + marginRight;\\n  const outerTop = centerY - height / 2 - paddingY - marginY;\\n  const outerBottom = centerY + height / 2 + paddingY + marginY;\\n  const outerHeight = height + paddingY * 2 + marginY * 2;\\n  const outerRadius = outerHeight / 2;\\n  const outerRadiusLeft = isLeftAligned\\n    ? outerRadius * OUTER_EDGE_RADIUS_FACTOR\\n    : outerRadius;\\n  const outerRadiusRight = isLeftAligned\\n    ? outerRadius\\n    : outerRadius * OUTER_EDGE_RADIUS_FACTOR;\\n\\n  const outerDist = signedDistanceToRoundedRect(\\n    thumbCenterX,\\n    thumbCenterY,\\n    outerLeft,\\n    outerRight,\\n    outerTop,\\n    outerBottom,\\n    outerRadiusLeft,\\n    outerRadiusRight,\\n  );\\n\\n  // Outside outer boundary - no gap\\n  if (outerDist > 0) return 0;\\n\\n  const innerDist = signedDistanceToRoundedRect(\\n    thumbCenterX,\\n    thumbCenterY,\\n    innerLeft,\\n    innerRight,\\n    innerTop,\\n    innerBottom,\\n    innerRadiusLeft,\\n    innerRadiusRight,\\n  );\\n\\n  // Inside inner boundary - max gap\\n  const maxGap = height + paddingY * 2;\\n  if (innerDist <= 0) return maxGap;\\n\\n  // Between boundaries - linear interpolation\\n  // outerDist is negative (inside outer), innerDist is positive (outside inner)\\n  const totalDist = Math.abs(outerDist) + innerDist;\\n  const t = Math.abs(outerDist) / totalDist;\\n\\n  return maxGap * t;\\n}\\n\\ninterface SliderRowProps {\\n  config: SliderConfig;\\n  value: number;\\n  onChange: (value: number) => void;\\n  trackClassName?: string;\\n  fillClassName?: string;\\n  handleClassName?: string;\\n}\\n\\nfunction SliderRow({\\n  config,\\n  value,\\n  onChange,\\n  trackClassName,\\n  fillClassName,\\n  handleClassName,\\n}: SliderRowProps) {\\n  const { id, label, min, max, step = 1, unit, precision, disabled } = config;\\n  // Per-slider theming overrides component-level theming\\n  const resolvedTrackClassName = config.trackClassName ?? trackClassName;\\n  const resolvedFillClassName = config.fillClassName ?? fillClassName;\\n  const resolvedHandleClassName = config.handleClassName ?? handleClassName;\\n  const crossesZero = min < 0 && max > 0;\\n  const [isDragging, setIsDragging] = useState(false);\\n  const [isHovered, setIsHovered] = useState(false);\\n\\n  const trackRef = useRef<HTMLSpanElement>(null);\\n  const labelRef = useRef<HTMLSpanElement>(null);\\n  const valueRef = useRef<HTMLSpanElement>(null);\\n\\n  const [dragGap, setDragGap] = useState(0);\\n  const [fullGap, setFullGap] = useState(0);\\n  const [intersectsText, setIntersectsText] = useState(false);\\n  const [layoutVersion, setLayoutVersion] = useState(0);\\n\\n  useEffect(() => {\\n    if (!isDragging) return;\\n    const handlePointerUp = () => setIsDragging(false);\\n    document.addEventListener(\\\"pointerup\\\", handlePointerUp);\\n    return () => document.removeEventListener(\\\"pointerup\\\", handlePointerUp);\\n  }, [isDragging]);\\n\\n  useEffect(() => {\\n    const track = trackRef.current;\\n    const labelEl = labelRef.current;\\n    const valueEl = valueRef.current;\\n    if (!track || !labelEl || !valueEl) return;\\n\\n    const bumpLayoutVersion = () => setLayoutVersion((v) => v + 1);\\n\\n    if (typeof ResizeObserver !== \\\"undefined\\\") {\\n      const observer = new ResizeObserver(() => {\\n        bumpLayoutVersion();\\n      });\\n      observer.observe(track);\\n      observer.observe(labelEl);\\n      observer.observe(valueEl);\\n      return () => observer.disconnect();\\n    }\\n\\n    window.addEventListener(\\\"resize\\\", bumpLayoutVersion);\\n    return () => window.removeEventListener(\\\"resize\\\", bumpLayoutVersion);\\n  }, []);\\n\\n  useLayoutEffect(() => {\\n    const track = trackRef.current;\\n    const labelEl = labelRef.current;\\n    const valueEl = valueRef.current;\\n\\n    if (!track || !labelEl || !valueEl) return;\\n\\n    const trackRect = track.getBoundingClientRect();\\n    const labelRect = labelEl.getBoundingClientRect();\\n    const valueRect = valueEl.getBoundingClientRect();\\n\\n    const trackWidth = trackRect.width;\\n    const valuePercent = sliderRangeToPercent({ value, min, max });\\n    // Use same inset coordinate system as visual elements\\n    const thumbCenterPx =\\n      (trackWidth * clampPercent(valuePercent)) / 100 +\\n      getRadixThumbInBoundsOffsetPx(valuePercent);\\n    const thumbHalfWidth = THUMB_WIDTH / 2;\\n\\n    // Text is raised by TEXT_VERTICAL_OFFSET from center\\n    const trackCenterY = TRACK_HEIGHT / 2 - TEXT_VERTICAL_OFFSET;\\n\\n    const labelGap = calculateGap(\\n      thumbCenterPx,\\n      {\\n        left: labelRect.left - trackRect.left,\\n        right: labelRect.right - trackRect.left,\\n        height: labelRect.height,\\n        centerY: trackCenterY,\\n      },\\n      true,\\n    ); // label is left-aligned\\n\\n    const valueGap = calculateGap(\\n      thumbCenterPx,\\n      {\\n        left: valueRect.left - trackRect.left,\\n        right: valueRect.right - trackRect.left,\\n        height: valueRect.height,\\n        centerY: trackCenterY,\\n      },\\n      false,\\n    ); // value is right-aligned\\n\\n    setDragGap(Math.max(labelGap, valueGap));\\n\\n    // Tight intersection check for release state\\n    // Inset by px-2 (8px) padding to check against actual text, not padded container\\n    const labelLeft = labelRect.left - trackRect.left + TEXT_RELEASE_INSET;\\n    const labelRight = labelRect.right - trackRect.left - TEXT_RELEASE_INSET;\\n    const valueLeft = valueRect.left - trackRect.left + TEXT_RELEASE_INSET;\\n    const valueRight = valueRect.right - trackRect.left - TEXT_RELEASE_INSET;\\n\\n    const thumbLeft = thumbCenterPx - thumbHalfWidth;\\n    const thumbRight = thumbCenterPx + thumbHalfWidth;\\n\\n    const hitsLabel = thumbRight > labelLeft && thumbLeft < labelRight;\\n    const hitsValue = thumbRight > valueLeft && thumbLeft < valueRight;\\n\\n    setIntersectsText(hitsLabel || hitsValue);\\n\\n    // Calculate full separation gap for release state\\n    // Use the max gap of whichever text element(s) the handle intersects\\n    const labelFullGap = labelRect.height + TEXT_PADDING_Y * 2;\\n    const valueFullGap = valueRect.height + TEXT_PADDING_Y * 2;\\n    const releaseGap =\\n      hitsLabel && hitsValue\\n        ? Math.max(labelFullGap, valueFullGap)\\n        : hitsLabel\\n          ? labelFullGap\\n          : hitsValue\\n            ? valueFullGap\\n            : 0;\\n    setFullGap(releaseGap);\\n  }, [value, min, max, layoutVersion]);\\n\\n  // While dragging: use distance-based separation, but never collapse below\\n  // the release split when the thumb still intersects text.\\n  const gap = isDragging\\n    ? Math.max(dragGap, intersectsText ? fullGap : 0)\\n    : intersectsText\\n      ? fullGap\\n      : 0;\\n\\n  const ticks = useMemo(() => {\\n    // Generate equidistant ticks regardless of step value\\n    const majorTickCount = TICK_COUNT;\\n    const result: { percent: number; isCenter: boolean; isSubtick: boolean }[] =\\n      [];\\n\\n    for (let i = 0; i <= majorTickCount; i++) {\\n      const percent = (i / majorTickCount) * 100;\\n      const isCenter = !crossesZero && percent === 50;\\n\\n      // Skip the center tick (50%) for crossesZero sliders\\n      if (crossesZero && percent === 50) continue;\\n\\n      // Add subtick at midpoint before this tick (except for first)\\n      if (i > 0) {\\n        const prevPercent = ((i - 1) / majorTickCount) * 100;\\n        // Don't add subtick if it would be at 50% for crossesZero\\n        const midPercent = (prevPercent + percent) / 2;\\n        if (!(crossesZero && midPercent === 50)) {\\n          result.push({\\n            percent: midPercent,\\n            isCenter: false,\\n            isSubtick: true,\\n          });\\n        }\\n      }\\n\\n      result.push({ percent, isCenter, isSubtick: false });\\n    }\\n\\n    return result;\\n  }, [crossesZero]);\\n\\n  const zeroPercent = crossesZero\\n    ? sliderRangeToPercent({ value: 0, min, max })\\n    : 0;\\n  const valuePercent = sliderRangeToPercent({ value, min, max });\\n\\n  // Fill clip-path uses the same inset coordinate system as the handle.\\n  // This keeps the collapsed stroke aligned with the fill edge near extremes.\\n  const fillClipPath = useMemo(() => {\\n    const toClipFromRightInset = (percent: number) =>\\n      `calc(100% - ${toRadixThumbPosition(percent)})`;\\n    const toClipFromLeftInset = (percent: number) =>\\n      toRadixThumbPosition(percent);\\n    const TERMINAL_EPSILON = 1e-6;\\n    const snapLeftInset = (percent: number) => {\\n      if (percent <= TERMINAL_EPSILON) return \\\"0\\\";\\n      if (percent >= 100 - TERMINAL_EPSILON) return \\\"100%\\\";\\n      return toClipFromLeftInset(percent);\\n    };\\n    const snapRightInset = (percent: number) => {\\n      if (percent <= TERMINAL_EPSILON) return \\\"100%\\\";\\n      if (percent >= 100 - TERMINAL_EPSILON) return \\\"0\\\";\\n      return toClipFromRightInset(percent);\\n    };\\n\\n    if (crossesZero) {\\n      // Keep center anchor stable by always clipping the low/high pair,\\n      // independent of sign branch, then snapping at terminal edges.\\n      const lowPercent = Math.min(valuePercent, zeroPercent);\\n      const highPercent = Math.max(valuePercent, zeroPercent);\\n      return `inset(0 ${snapRightInset(highPercent)} 0 ${snapLeftInset(lowPercent)})`;\\n    }\\n    // Non-crossing: fill starts at left edge; snap right inset at terminals.\\n    return `inset(0 ${snapRightInset(valuePercent)} 0 0)`;\\n  }, [crossesZero, zeroPercent, valuePercent]);\\n\\n  const fillMaskImage = crossesZero\\n    ? \\\"linear-gradient(to right, rgba(0,0,0,0.2) 0%, rgba(0,0,0,0.35) 50%, rgba(0,0,0,0.7) 100%)\\\"\\n    : \\\"linear-gradient(to right, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.7) 100%)\\\";\\n\\n  // Metallic reflection gradient that follows the handle position\\n  // Visible while dragging OR when resting at edges (0%/100%)\\n  const reflectionStyle = useMemo(() => {\\n    const edgeThreshold = 3;\\n    const nearEdge =\\n      valuePercent <= edgeThreshold || valuePercent >= 100 - edgeThreshold;\\n\\n    // Narrower spread when stationary at edges (~35% narrower)\\n    const spreadPercent = nearEdge && !isDragging ? 6.5 : 10;\\n    const handlePos = toRadixThumbPosition(valuePercent);\\n    const start = `clamp(0%, calc(${handlePos} - ${spreadPercent}%), 100%)`;\\n    const end = `clamp(0%, calc(${handlePos} + ${spreadPercent}%), 100%)`;\\n\\n    const gradient = `linear-gradient(to right,\\n      transparent ${start},\\n      white ${handlePos},\\n      transparent ${end})`;\\n\\n    return {\\n      background: gradient,\\n      WebkitMask:\\n        \\\"linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)\\\",\\n      WebkitMaskComposite: \\\"xor\\\",\\n      maskComposite: \\\"exclude\\\",\\n      padding: \\\"1px\\\",\\n    };\\n  }, [valuePercent, isDragging]);\\n\\n  // Opacity scales with handle size: rest → hover → drag\\n  const reflectionOpacity = useMemo(() => {\\n    const edgeThreshold = 3;\\n    const atEdge =\\n      valuePercent <= edgeThreshold || valuePercent >= 100 - edgeThreshold;\\n\\n    if (isDragging || atEdge) {\\n      return 1;\\n    }\\n    if (isHovered) {\\n      return 0.6;\\n    }\\n    return 0;\\n  }, [valuePercent, isDragging, isHovered]);\\n\\n  const handleValueChange = useCallback(\\n    (values: number[]) => {\\n      if (values[0] !== undefined) {\\n        onChange(values[0]);\\n      }\\n    },\\n    [onChange],\\n  );\\n\\n  return (\\n    <div className=\\\"py-2\\\">\\n      <SliderPrimitive.Root\\n        id={id}\\n        className={cn(\\n          \\\"group/slider relative flex w-full touch-none items-center select-none\\\",\\n          \\\"isolate h-12\\\",\\n          isDragging\\n            ? \\\"[&>span]:transition-[left,transform] [&>span]:duration-45 [&>span]:ease-linear\\\"\\n            : \\\"[&>span]:transition-[left,transform] [&>span]:duration-90 [&>span]:ease-[cubic-bezier(0.22,1,0.36,1)]\\\",\\n          \\\"[&>span]:will-change-[left,transform]\\\",\\n          \\\"motion-reduce:[&>span]:transition-none\\\",\\n          disabled && \\\"pointer-events-none opacity-50\\\",\\n        )}\\n        value={[value]}\\n        onValueChange={handleValueChange}\\n        onPointerDown={() => setIsDragging(true)}\\n        onPointerUp={() => setIsDragging(false)}\\n        onPointerEnter={() => setIsHovered(true)}\\n        onPointerLeave={() => setIsHovered(false)}\\n        min={min}\\n        max={max}\\n        step={step}\\n        disabled={disabled}\\n        aria-valuetext={getAriaValueText(value, min, max, unit)}\\n      >\\n        <SliderPrimitive.Track\\n          ref={trackRef}\\n          className={cn(\\n            \\\"squircle relative h-12 w-full grow overflow-hidden rounded-sm\\\",\\n            \\\"ring-border ring-1 ring-inset\\\",\\n            \\\"dark:ring-white/10\\\",\\n            resolvedTrackClassName ?? \\\"bg-muted\\\",\\n          )}\\n        >\\n          <div\\n            className={cn(\\n              \\\"absolute inset-0 will-change-[clip-path]\\\",\\n              isDragging\\n                ? \\\"transition-[clip-path] duration-45 ease-linear\\\"\\n                : \\\"transition-[clip-path] duration-90 ease-[cubic-bezier(0.22,1,0.36,1)]\\\",\\n              \\\"motion-reduce:transition-none\\\",\\n              resolvedFillClassName ?? \\\"bg-primary/30 dark:bg-primary/40\\\",\\n            )}\\n            style={{\\n              maskImage: fillMaskImage,\\n              WebkitMaskImage: fillMaskImage,\\n              clipPath: fillClipPath,\\n            }}\\n          />\\n\\n          {ticks.map((tick, i) => {\\n            const isEdge =\\n              !tick.isSubtick && (tick.percent === 0 || tick.percent === 100);\\n            return (\\n              <span\\n                key={i}\\n                className={cn(\\n                  \\\"pointer-events-none absolute bottom-px w-px\\\",\\n                  tick.isSubtick ? \\\"h-1.5\\\" : \\\"h-2\\\",\\n                  isEdge\\n                    ? \\\"bg-transparent\\\"\\n                    : tick.isSubtick\\n                      ? \\\"bg-foreground/8 dark:bg-white/5\\\"\\n                      : tick.isCenter\\n                        ? \\\"bg-foreground/30 dark:bg-white/25\\\"\\n                        : \\\"bg-foreground/15 dark:bg-white/8\\\",\\n                )}\\n                style={{\\n                  left: toInsetPosition(tick.percent),\\n                  transform: \\\"translateX(-50%)\\\",\\n                }}\\n              />\\n            );\\n          })}\\n        </SliderPrimitive.Track>\\n\\n        {/* Metallic reflection overlay - follows handle, brightness scales with interaction */}\\n        <div\\n          className={cn(\\n            \\\"squircle pointer-events-none absolute inset-0 rounded-sm\\\",\\n            isDragging\\n              ? \\\"transition-[opacity,background] duration-45 ease-linear\\\"\\n              : \\\"transition-[opacity,background] duration-90 ease-[cubic-bezier(0.22,1,0.36,1)]\\\",\\n            \\\"motion-reduce:transition-none\\\",\\n          )}\\n          style={{\\n            ...reflectionStyle,\\n            opacity: reflectionOpacity,\\n            filter: \\\"blur(1px)\\\",\\n            mixBlendMode: \\\"overlay\\\",\\n          }}\\n        />\\n\\n        <SliderPrimitive.Thumb\\n          className={cn(\\n            \\\"group/thumb z-0 block w-3 shrink-0 cursor-grab rounded-sm\\\",\\n            \\\"relative bg-transparent outline-none\\\",\\n            \\\"transition-[height,opacity] duration-150 ease-[var(--cubic-ease-in-out)]\\\",\\n            \\\"focus-visible:outline-ring focus-visible:outline-2 focus-visible:outline-offset-1\\\",\\n            \\\"active:cursor-grabbing\\\",\\n            \\\"disabled:pointer-events-none disabled:opacity-50\\\",\\n            // Height morphs: rest (track height) → hover → active\\n            isDragging ? \\\"h-[56px]\\\" : isHovered ? \\\"h-[54px]\\\" : \\\"h-12\\\",\\n          )}\\n        >\\n          {(() => {\\n            // Calculate morph state\\n            const isActive = isHovered || isDragging;\\n\\n            // Indicator stays centered on the real thumb while CSS transitions\\n            // smooth thumb wrapper and fill movement together.\\n            const fillEdgeOffset = 0;\\n\\n            // Hide rest-state indicator at edges (0% or 100%) - the reflection gradient handles this\\n            const edgeThreshold = 3;\\n            const atEdge =\\n              valuePercent <= edgeThreshold ||\\n              valuePercent >= 100 - edgeThreshold;\\n            const restOpacity = atEdge ? 0 : 0.25;\\n\\n            // Asymmetric segment heights: gap is shifted up to match raised text position\\n            // Top segment is shorter, bottom segment is taller\\n            const topHeight =\\n              isActive && gap > 0\\n                ? `calc(50% - ${gap / 2 + TEXT_VERTICAL_OFFSET}px)`\\n                : \\\"50%\\\";\\n            const bottomHeight =\\n              isActive && gap > 0\\n                ? `calc(50% - ${gap / 2 - TEXT_VERTICAL_OFFSET}px)`\\n                : \\\"50%\\\";\\n\\n            return (\\n              <>\\n                <span\\n                  className={cn(\\n                    \\\"absolute top-0 left-1/2\\\",\\n                    \\\"transition-all duration-100 ease-[var(--cubic-ease-in-out)]\\\",\\n                    isActive\\n                      ? gap > 0\\n                        ? \\\"rounded-full\\\"\\n                        : \\\"rounded-t-full\\\"\\n                      : \\\"rounded-t-sm\\\",\\n                    isDragging ? \\\"w-2\\\" : isActive ? \\\"w-1.5\\\" : \\\"w-px\\\",\\n                    resolvedHandleClassName ?? \\\"bg-primary\\\",\\n                  )}\\n                  style={{\\n                    transform: `translateX(calc(-50% + ${fillEdgeOffset}px))`,\\n                    height: topHeight,\\n                    opacity: isActive ? 1 : restOpacity,\\n                  }}\\n                />\\n                <span\\n                  className={cn(\\n                    \\\"absolute bottom-0 left-1/2\\\",\\n                    \\\"transition-all duration-100 ease-[var(--cubic-ease-in-out)]\\\",\\n                    isActive\\n                      ? gap > 0\\n                        ? \\\"rounded-full\\\"\\n                        : \\\"rounded-b-full\\\"\\n                      : \\\"rounded-b-sm\\\",\\n                    isDragging ? \\\"w-2\\\" : isActive ? \\\"w-1.5\\\" : \\\"w-px\\\",\\n                    resolvedHandleClassName ?? \\\"bg-primary\\\",\\n                  )}\\n                  style={{\\n                    transform: `translateX(calc(-50% + ${fillEdgeOffset}px))`,\\n                    height: bottomHeight,\\n                    opacity: isActive ? 1 : restOpacity,\\n                  }}\\n                />\\n              </>\\n            );\\n          })()}\\n        </SliderPrimitive.Thumb>\\n\\n        <div\\n          className=\\\"pointer-events-none absolute inset-x-3 top-1/2 z-10 flex items-center justify-between\\\"\\n          style={{\\n            transform: `translateY(calc(-50% - ${TEXT_VERTICAL_OFFSET}px))`,\\n          }}\\n        >\\n          <span\\n            ref={labelRef}\\n            className=\\\"text-primary -mt-px rounded-full px-2 py-px text-sm font-normal tracking-wide\\\"\\n          >\\n            {label}\\n          </span>\\n          <span\\n            ref={valueRef}\\n            className=\\\"text-foreground -mt-px -mb-0.5 flex h-6 items-center rounded-full px-2 font-mono text-xs tabular-nums\\\"\\n          >\\n            {formatSignedValue(value, min, max, precision, unit)}\\n          </span>\\n        </div>\\n      </SliderPrimitive.Root>\\n    </div>\\n  );\\n}\\n\\nexport function ParameterSlider({\\n  id,\\n  sliders,\\n  values: controlledValues,\\n  onChange,\\n  actions,\\n  onAction,\\n  onBeforeAction,\\n  className,\\n  trackClassName,\\n  fillClassName,\\n  handleClassName,\\n}: ParameterSliderProps) {\\n  const slidersSignature = useMemo(\\n    () => createSliderSignature(sliders),\\n    [sliders],\\n  );\\n  const sliderSnapshot = useMemo(\\n    () => createSliderValueSnapshot(sliders),\\n    [sliders],\\n  );\\n  const {\\n    value: currentValues,\\n    isControlled,\\n    setValue,\\n    setUncontrolledValue,\\n  } = useControllableState<SliderValue[]>({\\n    value: controlledValues,\\n    defaultValue: sliderSnapshot,\\n    onChange,\\n  });\\n\\n  useSignatureReset(slidersSignature, () => {\\n    if (!isControlled) {\\n      setUncontrolledValue(sliderSnapshot);\\n    }\\n  });\\n\\n  const valueMap = useMemo(() => {\\n    const map = new Map<string, number>();\\n    for (const v of currentValues) {\\n      map.set(v.id, v.value);\\n    }\\n    return map;\\n  }, [currentValues]);\\n\\n  const updateValue = useCallback(\\n    (sliderId: string, newValue: number) => {\\n      setValue((prev) =>\\n        prev.map((v) => (v.id === sliderId ? { ...v, value: newValue } : v)),\\n      );\\n    },\\n    [setValue],\\n  );\\n\\n  const handleReset = useCallback(() => {\\n    setValue(sliderSnapshot);\\n  }, [setValue, sliderSnapshot]);\\n\\n  const handleAction = useCallback(\\n    async (actionId: string) => {\\n      let nextValues = currentValues;\\n      if (actionId === \\\"reset\\\") {\\n        handleReset();\\n        nextValues = sliderSnapshot;\\n      }\\n\\n      await onAction?.(actionId, nextValues);\\n    },\\n    [currentValues, handleReset, onAction, sliderSnapshot],\\n  );\\n\\n  const normalizedActions = useMemo(() => {\\n    const normalized = normalizeActionsConfig(actions);\\n    if (normalized) return normalized;\\n    return {\\n      items: [\\n        { id: \\\"reset\\\", label: \\\"Reset\\\", variant: \\\"ghost\\\" as const },\\n        { id: \\\"apply\\\", label: \\\"Apply\\\", variant: \\\"default\\\" as const },\\n      ],\\n      align: \\\"right\\\" as const,\\n    };\\n  }, [actions]);\\n\\n  return (\\n    <article\\n      className={cn(\\n        \\\"@container/parameter-slider isolate flex w-full max-w-md min-w-80 flex-col gap-3\\\",\\n        \\\"text-foreground\\\",\\n        className,\\n      )}\\n      data-slot=\\\"parameter-slider\\\"\\n      data-tool-ui-id={id}\\n    >\\n      <div\\n        className={cn(\\n          \\\"bg-card flex w-full flex-col overflow-hidden rounded-2xl border px-5 py-3 shadow-xs\\\",\\n        )}\\n      >\\n        {sliders.map((slider) => (\\n          <SliderRow\\n            key={slider.id}\\n            config={slider}\\n            value={valueMap.get(slider.id) ?? slider.value}\\n            onChange={(v) => updateValue(slider.id, v)}\\n            trackClassName={trackClassName}\\n            fillClassName={fillClassName}\\n            handleClassName={handleClassName}\\n          />\\n        ))}\\n      </div>\\n\\n      <div className=\\\"@container/actions\\\">\\n        <ActionButtons\\n          actions={normalizedActions.items}\\n          align={normalizedActions.align}\\n          confirmTimeout={normalizedActions.confirmTimeout}\\n          onAction={handleAction}\\n          onBeforeAction={\\n            onBeforeAction\\n              ? (actionId) => onBeforeAction(actionId, currentValues)\\n              : undefined\\n          }\\n        />\\n      </div>\\n    </article>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/parameter-slider/README.md\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/parameter-slider/README.md\",\n      \"content\": \"# Parameter Slider\\n\\nImplementation for the \\\"parameter-slider\\\" Tool UI surface.\\n\\n## Files\\n\\n- public exports: components/tool-ui/parameter-slider/index.tsx\\n- serializable schema + parse helpers: components/tool-ui/parameter-slider/schema.ts\\n\\n## Companion assets\\n\\n- Docs page: app/docs/parameter-slider/content.mdx\\n- Preset payload: lib/presets/parameter-slider.ts\\n\\n## Quick check\\n\\nRun this after edits:\\n\\npnpm test\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/parameter-slider/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/parameter-slider/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { type ActionsProp } from \\\"../shared/actions-config\\\";\\nimport type { EmbeddedActionsProps } from \\\"../shared/embedded-actions\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\nimport {\\n  SerializableActionSchema,\\n  SerializableActionsConfigSchema,\\n  ToolUIIdSchema,\\n  ToolUIRoleSchema,\\n} from \\\"../shared/schema\\\";\\n\\nexport const SliderConfigSchema = z\\n  .object({\\n    id: z.string().min(1),\\n    label: z.string().min(1),\\n    min: z.number().finite(),\\n    max: z.number().finite(),\\n    step: z.number().finite().positive().optional(),\\n    value: z.number().finite(),\\n    unit: z.string().optional(),\\n    precision: z.number().int().min(0).optional(),\\n    disabled: z.boolean().optional(),\\n    trackClassName: z.string().optional(),\\n    fillClassName: z.string().optional(),\\n    handleClassName: z.string().optional(),\\n  })\\n  .superRefine((slider, ctx) => {\\n    if (slider.max <= slider.min) {\\n      ctx.addIssue({\\n        code: z.ZodIssueCode.custom,\\n        path: [\\\"max\\\"],\\n        message: \\\"max must be greater than min\\\",\\n      });\\n    }\\n\\n    if (slider.value < slider.min || slider.value > slider.max) {\\n      ctx.addIssue({\\n        code: z.ZodIssueCode.custom,\\n        path: [\\\"value\\\"],\\n        message: \\\"value must be between min and max\\\",\\n      });\\n    }\\n  });\\n\\nexport type SliderConfig = z.infer<typeof SliderConfigSchema>;\\n\\nexport const SerializableParameterSliderSchema = z\\n  .object({\\n    id: ToolUIIdSchema,\\n    role: ToolUIRoleSchema.optional(),\\n    sliders: z.array(SliderConfigSchema).min(1),\\n    actions: z\\n      .union([\\n        z.array(SerializableActionSchema),\\n        SerializableActionsConfigSchema,\\n      ])\\n      .optional(),\\n  })\\n  .strict()\\n  .superRefine((payload, ctx) => {\\n    const seenIds = new Map<string, number>();\\n\\n    payload.sliders.forEach((slider, index) => {\\n      const firstSeenAt = seenIds.get(slider.id);\\n      if (firstSeenAt !== undefined) {\\n        ctx.addIssue({\\n          code: z.ZodIssueCode.custom,\\n          path: [\\\"sliders\\\", index, \\\"id\\\"],\\n          message: `duplicate slider id '${slider.id}' (first seen at index ${firstSeenAt})`,\\n        });\\n        return;\\n      }\\n      seenIds.set(slider.id, index);\\n    });\\n  });\\n\\nexport type SerializableParameterSlider = z.infer<\\n  typeof SerializableParameterSliderSchema\\n>;\\n\\nconst SerializableParameterSliderSchemaContract = defineToolUiContract(\\n  \\\"ParameterSlider\\\",\\n  SerializableParameterSliderSchema,\\n);\\n\\nexport const parseSerializableParameterSlider: (\\n  input: unknown,\\n) => SerializableParameterSlider =\\n  SerializableParameterSliderSchemaContract.parse;\\n\\nexport const safeParseSerializableParameterSlider: (\\n  input: unknown,\\n) => SerializableParameterSlider | null =\\n  SerializableParameterSliderSchemaContract.safeParse;\\n\\nexport interface SliderValue {\\n  id: string;\\n  value: number;\\n}\\n\\nexport interface ParameterSliderProps extends Omit<\\n  SerializableParameterSlider,\\n  \\\"actions\\\"\\n> {\\n  className?: string;\\n  values?: SliderValue[];\\n  onChange?: (values: SliderValue[]) => void;\\n  actions?: ActionsProp;\\n  onAction?: EmbeddedActionsProps<SliderValue[]>[\\\"onAction\\\"];\\n  onBeforeAction?: EmbeddedActionsProps<SliderValue[]>[\\\"onBeforeAction\\\"];\\n  trackClassName?: string;\\n  fillClassName?: string;\\n  handleClassName?: string;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/shared/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn     → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n *   Button → shadcn/ui Button\\n */\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport { Button } from \\\"@/components/ui/button\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/action-buttons.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/shared/action-buttons.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport type { Action } from \\\"./schema\\\";\\nimport { cn, Button } from \\\"./_adapter\\\";\\nimport { useActionButtons } from \\\"./use-action-buttons\\\";\\n\\nexport interface ActionButtonsProps {\\n  actions: Action[];\\n  onAction: (actionId: string) => void | Promise<void>;\\n  onBeforeAction?: (actionId: string) => boolean | Promise<boolean>;\\n  confirmTimeout?: number;\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  className?: string;\\n}\\n\\nexport function ActionButtons({\\n  actions,\\n  onAction,\\n  onBeforeAction,\\n  confirmTimeout = 3000,\\n  align = \\\"right\\\",\\n  className,\\n}: ActionButtonsProps) {\\n  const { actions: resolvedActions, runAction } = useActionButtons({\\n    actions,\\n    onAction,\\n    onBeforeAction,\\n    confirmTimeout,\\n  });\\n\\n  return (\\n    <div\\n      className={cn(\\n        \\\"flex flex-col gap-3\\\",\\n        \\\"@sm/actions:flex-row @sm/actions:flex-wrap @sm/actions:items-center @sm/actions:gap-2\\\",\\n        align === \\\"left\\\" && \\\"@sm/actions:justify-start\\\",\\n        align === \\\"center\\\" && \\\"@sm/actions:justify-center\\\",\\n        align === \\\"right\\\" && \\\"@sm/actions:justify-end\\\",\\n        className,\\n      )}\\n    >\\n      {resolvedActions.map((action) => {\\n        const label = action.currentLabel;\\n        const variant = action.variant || \\\"default\\\";\\n\\n        return (\\n          <Button\\n            key={action.id}\\n            variant={variant}\\n            onClick={() => runAction(action.id)}\\n            disabled={action.isDisabled}\\n            className={cn(\\n              \\\"rounded-full px-4!\\\",\\n              \\\"justify-center\\\",\\n              \\\"min-h-11 w-full text-base\\\",\\n              \\\"@sm/actions:min-h-0 @sm/actions:w-auto @sm/actions:px-3 @sm/actions:py-2 @sm/actions:text-sm\\\",\\n              action.isConfirming &&\\n                \\\"ring-destructive ring-2 ring-offset-2 motion-safe:animate-pulse\\\",\\n            )}\\n            aria-label={\\n              action.shortcut ? `${label} (${action.shortcut})` : label\\n            }\\n          >\\n            {action.isLoading && (\\n              <svg\\n                className=\\\"mr-2 h-4 w-4 motion-safe:animate-spin\\\"\\n                xmlns=\\\"http://www.w3.org/2000/svg\\\"\\n                fill=\\\"none\\\"\\n                viewBox=\\\"0 0 24 24\\\"\\n              >\\n                <circle\\n                  className=\\\"opacity-25\\\"\\n                  cx=\\\"12\\\"\\n                  cy=\\\"12\\\"\\n                  r=\\\"10\\\"\\n                  stroke=\\\"currentColor\\\"\\n                  strokeWidth=\\\"4\\\"\\n                />\\n                <path\\n                  className=\\\"opacity-75\\\"\\n                  fill=\\\"currentColor\\\"\\n                  d=\\\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\\\"\\n                />\\n              </svg>\\n            )}\\n            {action.icon && !action.isLoading && (\\n              <span className=\\\"mr-2\\\">{action.icon}</span>\\n            )}\\n            {label}\\n            {action.shortcut && !action.isLoading && (\\n              <kbd className=\\\"border-border bg-muted ml-2.5 hidden rounded-lg border px-2 py-0.5 font-mono text-xs font-medium sm:inline-block\\\">\\n                {action.shortcut}\\n              </kbd>\\n            )}\\n          </Button>\\n        );\\n      })}\\n    </div>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/actions-config.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/actions-config.ts\",\n      \"content\": \"import type { Action, ActionsConfig } from \\\"./schema\\\";\\n\\nexport type ActionsProp = ActionsConfig | Action[];\\n\\nconst NEGATORY_ACTION_IDS = new Set([\\n  \\\"cancel\\\",\\n  \\\"dismiss\\\",\\n  \\\"skip\\\",\\n  \\\"no\\\",\\n  \\\"reset\\\",\\n  \\\"close\\\",\\n  \\\"decline\\\",\\n  \\\"reject\\\",\\n  \\\"back\\\",\\n  \\\"later\\\",\\n  \\\"not-now\\\",\\n  \\\"maybe-later\\\",\\n]);\\n\\nfunction inferVariant(action: Action): Action {\\n  if (action.variant) return action;\\n  if (NEGATORY_ACTION_IDS.has(action.id)) {\\n    return { ...action, variant: \\\"ghost\\\" };\\n  }\\n  return action;\\n}\\n\\nexport function normalizeActionsConfig(\\n  actions?: ActionsProp,\\n): ActionsConfig | null {\\n  if (!actions) return null;\\n\\n  const rawItems = Array.isArray(actions) ? actions : (actions.items ?? []);\\n\\n  if (rawItems.length === 0) {\\n    return null;\\n  }\\n\\n  const items = rawItems.map(inferVariant);\\n\\n  return Array.isArray(actions)\\n    ? { items }\\n    : {\\n        items,\\n        align: actions.align,\\n        confirmTimeout: actions.confirmTimeout,\\n      };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/embedded-actions.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/embedded-actions.ts\",\n      \"content\": \"import type { ActionsProp } from \\\"./actions-config\\\";\\n\\nexport type EmbeddedActionHandler<TState> = (\\n  actionId: string,\\n  state: TState,\\n) => void | Promise<void>;\\n\\nexport type EmbeddedBeforeActionHandler<TState> = (\\n  actionId: string,\\n  state: TState,\\n) => boolean | Promise<boolean>;\\n\\nexport interface EmbeddedActionsProps<TState> {\\n  actions?: ActionsProp;\\n  onAction?: EmbeddedActionHandler<TState>;\\n  onBeforeAction?: EmbeddedBeforeActionHandler<TState>;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { ReactNode } from \\\"react\\\";\\n\\n/**\\n * Tool UI conventions:\\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\\n * - Schema: `SerializableXSchema`\\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\\n * - Root attrs: `data-tool-ui-id` + `data-slot`\\n */\\n\\n/**\\n * Schema for tool UI identity.\\n *\\n * Every tool UI should have a unique identifier that:\\n * - Is stable across re-renders\\n * - Is meaningful (not auto-generated)\\n * - Is unique within the conversation\\n *\\n * Format recommendation: `{component-type}-{semantic-identifier}`\\n * Examples: \\\"data-table-expenses-q3\\\", \\\"option-list-deploy-target\\\"\\n */\\nexport const ToolUIIdSchema = z.string().min(1);\\n\\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\\n\\n/**\\n * Primary role of a Tool UI surface in a chat context.\\n */\\nexport const ToolUIRoleSchema = z.enum([\\n  \\\"information\\\",\\n  \\\"decision\\\",\\n  \\\"control\\\",\\n  \\\"state\\\",\\n  \\\"composite\\\",\\n]);\\n\\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\\n\\nexport const ToolUIReceiptOutcomeSchema = z.enum([\\n  \\\"success\\\",\\n  \\\"partial\\\",\\n  \\\"failed\\\",\\n  \\\"cancelled\\\",\\n]);\\n\\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\\n\\n/**\\n * Optional receipt metadata: a durable summary of an outcome.\\n */\\nexport const ToolUIReceiptSchema = z.object({\\n  outcome: ToolUIReceiptOutcomeSchema,\\n  summary: z.string().min(1),\\n  identifiers: z.record(z.string(), z.string()).optional(),\\n  at: z.string().datetime(),\\n});\\n\\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\\n\\n/**\\n * Base schema for Tool UI payloads (id + optional role/receipt).\\n */\\nexport const ToolUISurfaceSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n});\\n\\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\\n\\nexport const ActionSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  /**\\n   * Canonical narration the assistant can use after this action is taken.\\n   *\\n   * Example: \\\"I exported the table as CSV.\\\" / \\\"I opened the link in a new tab.\\\"\\n   */\\n  sentence: z.string().optional(),\\n  confirmLabel: z.string().optional(),\\n  variant: z\\n    .enum([\\\"default\\\", \\\"destructive\\\", \\\"secondary\\\", \\\"ghost\\\", \\\"outline\\\"])\\n    .optional(),\\n  icon: z.custom<ReactNode>().optional(),\\n  loading: z.boolean().optional(),\\n  disabled: z.boolean().optional(),\\n  shortcut: z.string().optional(),\\n});\\n\\nexport type Action = z.infer<typeof ActionSchema>;\\nexport type LocalAction = Action;\\nexport type DecisionAction = Action;\\n\\nexport const DecisionResultSchema = z.object({\\n  kind: z.literal(\\\"decision\\\"),\\n  version: z.literal(1),\\n  decisionId: z.string().min(1),\\n  actionId: z.string().min(1),\\n  actionLabel: z.string().min(1),\\n  at: z.string().datetime(),\\n  payload: z.record(z.string(), z.unknown()).optional(),\\n});\\n\\nexport type DecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n> = Omit<z.infer<typeof DecisionResultSchema>, \\\"payload\\\"> & {\\n  payload?: TPayload;\\n};\\n\\nexport function createDecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n>(args: {\\n  decisionId: string;\\n  action: { id: string; label: string };\\n  payload?: TPayload;\\n}): DecisionResult<TPayload> {\\n  return {\\n    kind: \\\"decision\\\",\\n    version: 1,\\n    decisionId: args.decisionId,\\n    actionId: args.action.id,\\n    actionLabel: args.action.label,\\n    at: new Date().toISOString(),\\n    payload: args.payload,\\n  };\\n}\\n\\nexport const ActionButtonsPropsSchema = z.object({\\n  actions: z.array(ActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n  className: z.string().optional(),\\n});\\n\\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\\n  actions: z.array(SerializableActionSchema),\\n}).omit({ className: true });\\n\\nexport interface ActionsConfig {\\n  items: Action[];\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  confirmTimeout?: number;\\n}\\n\\nexport const SerializableActionsConfigSchema = z.object({\\n  items: z.array(SerializableActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n});\\n\\nexport type SerializableActionsConfig = z.infer<\\n  typeof SerializableActionsConfigSchema\\n>;\\n\\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/use-action-buttons.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/shared/use-action-buttons.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { useCallback, useEffect, useMemo, useRef, useState } from \\\"react\\\";\\nimport type { Action } from \\\"./schema\\\";\\n\\nexport type UseActionButtonsOptions = {\\n  actions: Action[];\\n  onAction: (actionId: string) => void | Promise<void>;\\n  onBeforeAction?: (actionId: string) => boolean | Promise<boolean>;\\n  confirmTimeout?: number;\\n};\\n\\nexport type UseActionButtonsResult = {\\n  actions: Array<\\n    Action & {\\n      currentLabel: string;\\n      isConfirming: boolean;\\n      isExecuting: boolean;\\n      isDisabled: boolean;\\n      isLoading: boolean;\\n    }\\n  >;\\n  runAction: (actionId: string) => Promise<void>;\\n  confirmingActionId: string | null;\\n  executingActionId: string | null;\\n};\\n\\ntype ActionExecutionLock = {\\n  tryAcquire: () => boolean;\\n  release: () => void;\\n};\\n\\nexport function createActionExecutionLock(): ActionExecutionLock {\\n  let locked = false;\\n\\n  return {\\n    tryAcquire: () => {\\n      if (locked) return false;\\n      locked = true;\\n      return true;\\n    },\\n    release: () => {\\n      locked = false;\\n    },\\n  };\\n}\\n\\nexport function useActionButtons(\\n  options: UseActionButtonsOptions,\\n): UseActionButtonsResult {\\n  const { actions, onAction, onBeforeAction, confirmTimeout = 3000 } = options;\\n\\n  const [confirmingActionId, setConfirmingActionId] = useState<string | null>(\\n    null,\\n  );\\n  const [executingActionId, setExecutingActionId] = useState<string | null>(\\n    null,\\n  );\\n  const executionLockRef = useRef<ActionExecutionLock>(\\n    createActionExecutionLock(),\\n  );\\n\\n  useEffect(() => {\\n    if (!confirmingActionId) return;\\n    const id = setTimeout(() => setConfirmingActionId(null), confirmTimeout);\\n    return () => clearTimeout(id);\\n  }, [confirmingActionId, confirmTimeout]);\\n\\n  useEffect(() => {\\n    if (!confirmingActionId) return;\\n    const handleKeyDown = (e: KeyboardEvent) => {\\n      if (e.key === \\\"Escape\\\") {\\n        setConfirmingActionId(null);\\n      }\\n    };\\n\\n    window.addEventListener(\\\"keydown\\\", handleKeyDown);\\n    return () => window.removeEventListener(\\\"keydown\\\", handleKeyDown);\\n  }, [confirmingActionId]);\\n\\n  const runAction = useCallback(\\n    async (actionId: string) => {\\n      const action = actions.find((a) => a.id === actionId);\\n      if (!action) return;\\n\\n      const isAnyActionExecuting = executingActionId !== null;\\n      if (action.disabled || action.loading || isAnyActionExecuting) {\\n        return;\\n      }\\n\\n      if (action.confirmLabel && confirmingActionId !== action.id) {\\n        setConfirmingActionId(action.id);\\n        return;\\n      }\\n\\n      if (!executionLockRef.current.tryAcquire()) {\\n        return;\\n      }\\n\\n      if (onBeforeAction) {\\n        const shouldProceed = await onBeforeAction(action.id);\\n        if (!shouldProceed) {\\n          setConfirmingActionId(null);\\n          executionLockRef.current.release();\\n          return;\\n        }\\n      }\\n\\n      try {\\n        setExecutingActionId(action.id);\\n        await onAction(action.id);\\n      } finally {\\n        executionLockRef.current.release();\\n        setExecutingActionId(null);\\n        setConfirmingActionId(null);\\n      }\\n    },\\n    [actions, confirmingActionId, executingActionId, onAction, onBeforeAction],\\n  );\\n\\n  const resolvedActions = useMemo(\\n    () =>\\n      actions.map((action) => {\\n        const isConfirming = confirmingActionId === action.id;\\n        const isThisActionExecuting = executingActionId === action.id;\\n        const isLoading = action.loading || isThisActionExecuting;\\n        const isDisabled =\\n          action.disabled ||\\n          (executingActionId !== null && !isThisActionExecuting);\\n        const currentLabel =\\n          isConfirming && action.confirmLabel\\n            ? action.confirmLabel\\n            : action.label;\\n\\n        return {\\n          ...action,\\n          currentLabel,\\n          isConfirming,\\n          isExecuting: isThisActionExecuting,\\n          isDisabled,\\n          isLoading,\\n        };\\n      }),\\n    [actions, confirmingActionId, executingActionId],\\n  );\\n\\n  return {\\n    actions: resolvedActions,\\n    runAction,\\n    confirmingActionId,\\n    executingActionId,\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/use-controllable-state.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/use-controllable-state.ts\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { useCallback, useMemo, useRef, useState } from \\\"react\\\";\\n\\nexport type UseControllableStateOptions<T> = {\\n  value?: T;\\n  defaultValue: T;\\n  onChange?: (next: T) => void;\\n};\\n\\nexport function useControllableState<T>({\\n  value,\\n  defaultValue,\\n  onChange,\\n}: UseControllableStateOptions<T>) {\\n  const [uncontrolled, setUncontrolled] = useState<T>(defaultValue);\\n  const isControlled = value !== undefined;\\n\\n  const currentValue = useMemo(\\n    () => (isControlled ? (value as T) : uncontrolled),\\n    [isControlled, value, uncontrolled],\\n  );\\n  const currentValueRef = useRef(currentValue);\\n  currentValueRef.current = currentValue;\\n\\n  const setValue = useCallback(\\n    (next: T | ((prev: T) => T)) => {\\n      const resolved =\\n        typeof next === \\\"function\\\"\\n          ? (next as (prev: T) => T)(currentValueRef.current)\\n          : next;\\n\\n      currentValueRef.current = resolved;\\n      if (!isControlled) {\\n        setUncontrolled(resolved);\\n      }\\n\\n      onChange?.(resolved);\\n      return resolved;\\n    },\\n    [isControlled, onChange],\\n  );\\n\\n  const setUncontrolledValue = useCallback((next: T) => {\\n    setUncontrolled(next);\\n  }, []);\\n\\n  return {\\n    value: currentValue,\\n    isControlled,\\n    setValue,\\n    setUncontrolledValue,\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/use-signature-reset.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/use-signature-reset.ts\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { useEffect, useRef } from \\\"react\\\";\\n\\nexport function useSignatureReset(\\n  signature: string,\\n  onSignatureChange: () => void,\\n) {\\n  const previousSignature = useRef(signature);\\n\\n  useEffect(() => {\\n    if (previousSignature.current === signature) return;\\n    previousSignature.current = signature;\\n    onSignatureChange();\\n  }, [signature, onSignatureChange]);\\n}\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/plan.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"plan\",\n  \"type\": \"registry:block\",\n  \"title\": \"Plan\",\n  \"description\": \"Display step-by-step task workflows in AI interfaces.\",\n  \"dependencies\": [\n    \"lucide-react\",\n    \"zod\"\n  ],\n  \"registryDependencies\": [\n    \"accordion\",\n    \"card\",\n    \"collapsible\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/plan/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/plan/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn          → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n *   Accordion   → shadcn/ui Accordion\\n *   Card        → shadcn/ui Card\\n *   Collapsible → shadcn/ui Collapsible\\n */\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport {\\n  Accordion,\\n  AccordionItem,\\n  AccordionTrigger,\\n  AccordionContent,\\n} from \\\"@/components/ui/accordion\\\";\\nexport {\\n  Card,\\n  CardHeader,\\n  CardTitle,\\n  CardDescription,\\n  CardContent,\\n  CardFooter,\\n} from \\\"@/components/ui/card\\\";\\nexport {\\n  Collapsible,\\n  CollapsibleTrigger,\\n  CollapsibleContent,\\n} from \\\"@/components/ui/collapsible\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/plan/index.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/plan/index.tsx\",\n      \"content\": \"export { Plan, PlanCompact } from \\\"./plan\\\";\\nexport type {\\n  PlanProps,\\n  PlanTodo,\\n  PlanTodoStatus,\\n  SerializablePlan,\\n} from \\\"./schema\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/plan/plan.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/plan/plan.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport * as React from \\\"react\\\";\\nimport { useMemo, useState, useEffect, useRef, memo } from \\\"react\\\";\\nimport { Loader2, Check, X, MoreHorizontal, ChevronRight } from \\\"lucide-react\\\";\\nimport type { PlanProps, PlanTodo, PlanTodoStatus } from \\\"./schema\\\";\\nimport {\\n  cn,\\n  Card,\\n  CardHeader,\\n  CardTitle,\\n  CardDescription,\\n  CardContent,\\n  Accordion,\\n  AccordionItem,\\n  AccordionTrigger,\\n  AccordionContent,\\n  Collapsible,\\n  CollapsibleTrigger,\\n  CollapsibleContent,\\n} from \\\"./_adapter\\\";\\nimport { calculatePlanProgress, shouldCelebrateProgress } from \\\"./progress\\\";\\n\\nconst INITIAL_VISIBLE_TODO_COUNT = 4;\\n\\nconst TodoIcon = memo(function TodoIcon({\\n  status,\\n}: {\\n  status: PlanTodoStatus;\\n}) {\\n  if (status === \\\"pending\\\") {\\n    return (\\n      <span\\n        className=\\\"border-border bg-card flex size-6 shrink-0 items-center justify-center rounded-full border motion-safe:transition-all motion-safe:duration-200\\\"\\n        aria-hidden=\\\"true\\\"\\n      />\\n    );\\n  }\\n\\n  if (status === \\\"in_progress\\\") {\\n    return (\\n      <span\\n        className=\\\"border-border bg-card flex size-6 shrink-0 items-center justify-center rounded-full border shadow-[0_0_0_4px_hsl(var(--primary)/0.1)] motion-safe:transition-all motion-safe:duration-300\\\"\\n        aria-hidden=\\\"true\\\"\\n      >\\n        <Loader2 className=\\\"text-primary size-5 motion-safe:animate-spin\\\" />\\n      </span>\\n    );\\n  }\\n\\n  if (status === \\\"completed\\\") {\\n    return (\\n      <span\\n        className=\\\"border-primary bg-primary flex size-6 shrink-0 items-center justify-center rounded-full border shadow-sm motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:duration-300 motion-safe:ease-out\\\"\\n        aria-hidden=\\\"true\\\"\\n      >\\n        <Check\\n          className=\\\"text-primary-foreground size-4 motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:delay-75 motion-safe:duration-200 motion-safe:fill-mode-both\\\"\\n          strokeWidth={3}\\n        />\\n      </span>\\n    );\\n  }\\n\\n  if (status === \\\"cancelled\\\") {\\n    return (\\n      <span\\n        className=\\\"border-destructive bg-destructive flex size-6 shrink-0 items-center justify-center rounded-full border shadow-sm motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:duration-300 motion-safe:ease-out dark:border-red-600 dark:bg-red-600\\\"\\n        aria-hidden=\\\"true\\\"\\n      >\\n        <X\\n          className=\\\"size-4 text-white motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:delay-75 motion-safe:duration-200 motion-safe:fill-mode-both\\\"\\n          strokeWidth={3}\\n        />\\n      </span>\\n    );\\n  }\\n\\n  return null;\\n});\\n\\ninterface PlanTodoItemProps {\\n  todo: PlanTodo;\\n  className?: string;\\n  style?: React.CSSProperties;\\n  showConnector?: boolean;\\n}\\n\\nfunction areTodoPropsEqual(\\n  prev: PlanTodoItemProps,\\n  next: PlanTodoItemProps,\\n): boolean {\\n  if (prev.todo.id !== next.todo.id) return false;\\n  if (prev.todo.label !== next.todo.label) return false;\\n  if (prev.todo.status !== next.todo.status) return false;\\n  if (prev.todo.description !== next.todo.description) return false;\\n  if (prev.showConnector !== next.showConnector) return false;\\n  if (prev.className !== next.className) return false;\\n  const prevStyle = prev.style;\\n  const nextStyle = next.style;\\n  if (prevStyle === nextStyle) return true;\\n  if (!prevStyle || !nextStyle) return false;\\n  return (\\n    prevStyle.animationDelay === nextStyle.animationDelay &&\\n    prevStyle.animationFillMode === nextStyle.animationFillMode\\n  );\\n}\\n\\nconst PlanTodoItem = memo(function PlanTodoItem({\\n  todo,\\n  className,\\n  style,\\n  showConnector,\\n}: PlanTodoItemProps) {\\n  const [isOpen, setIsOpen] = React.useState(false);\\n\\n  const labelElement = (\\n    <span\\n      className={cn(\\n        \\\"text-sm leading-6 font-medium break-words\\\",\\n        todo.status === \\\"pending\\\" && \\\"text-muted-foreground\\\",\\n        todo.status === \\\"in_progress\\\" &&\\n          \\\"motion-safe:shimmer shimmer-invert text-foreground\\\",\\n        (todo.status === \\\"completed\\\" || todo.status === \\\"cancelled\\\") &&\\n          \\\"text-muted-foreground\\\",\\n      )}\\n    >\\n      {todo.label}\\n    </span>\\n  );\\n\\n  if (!todo.description) {\\n    return (\\n      <li\\n        className={cn(\\n          \\\"relative -mx-2 flex cursor-default items-start gap-3 rounded-md px-2 py-1.5\\\",\\n          className,\\n        )}\\n        style={style}\\n      >\\n        {showConnector && (\\n          <div\\n            className=\\\"bg-border absolute top-6 left-5 w-px\\\"\\n            style={{\\n              height: \\\"calc(100% + 0.25rem)\\\",\\n            }}\\n            aria-hidden=\\\"true\\\"\\n          />\\n        )}\\n        <div className=\\\"relative z-10\\\">\\n          <TodoIcon status={todo.status} />\\n        </div>\\n        <div className=\\\"min-w-0 flex-1\\\">{labelElement}</div>\\n      </li>\\n    );\\n  }\\n\\n  return (\\n    <li\\n      className={cn(\\n        \\\"relative -mx-2 min-w-0 cursor-default rounded-md\\\",\\n        className,\\n      )}\\n      style={style}\\n    >\\n      {showConnector && (\\n        <div\\n          className=\\\"bg-border absolute top-6 left-5 w-px\\\"\\n          style={{\\n            height: \\\"calc(100% + 0.25rem)\\\",\\n          }}\\n          aria-hidden=\\\"true\\\"\\n        />\\n      )}\\n      <Collapsible asChild open={isOpen} onOpenChange={setIsOpen}>\\n        <div\\n          className=\\\"data-[state=open]:bg-primary/5 min-w-0 rounded-md motion-safe:transition-all motion-safe:duration-200\\\"\\n          style={{\\n            backdropFilter: isOpen ? \\\"blur(2px)\\\" : undefined,\\n          }}\\n        >\\n          <CollapsibleTrigger className=\\\"group/todo flex w-full cursor-default items-start gap-3 px-2 py-1.5 text-left\\\">\\n            <div className=\\\"relative z-10\\\">\\n              <TodoIcon status={todo.status} />\\n            </div>\\n            <span className=\\\"min-w-0 flex-1\\\">{labelElement}</span>\\n            <ChevronRight className=\\\"text-muted-foreground/50 group-hover/todo:text-muted-foreground mt-0.5 size-4 shrink-0 rotate-90 group-data-[state=open]/todo:[transform:rotateY(180deg)] motion-safe:transition-transform motion-safe:duration-300 motion-safe:ease-[cubic-bezier(0.34,1.56,0.64,1)]\\\" />\\n          </CollapsibleTrigger>\\n          <CollapsibleContent\\n            className=\\\"group/content\\\"\\n            data-slot=\\\"collapsible-content\\\"\\n          >\\n            <div className=\\\"min-w-0 motion-safe:group-data-[state=closed]/content:animate-out motion-safe:group-data-[state=closed]/content:fade-out motion-safe:group-data-[state=closed]/content:slide-out-to-top-1 motion-safe:group-data-[state=closed]/content:duration-150 motion-safe:group-data-[state=open]/content:animate-in motion-safe:group-data-[state=open]/content:fade-in motion-safe:group-data-[state=open]/content:slide-in-from-top-1 motion-safe:group-data-[state=open]/content:delay-75 motion-safe:group-data-[state=open]/content:duration-150 motion-safe:group-data-[state=open]/content:fill-mode-both\\\">\\n              <p className=\\\"text-muted-foreground min-w-0 pr-2 pb-1.5 pl-11 text-sm text-pretty break-words\\\">\\n                {todo.description}\\n              </p>\\n            </div>\\n          </CollapsibleContent>\\n        </div>\\n      </Collapsible>\\n    </li>\\n  );\\n}, areTodoPropsEqual);\\n\\ninterface TodoListProps {\\n  todos: PlanTodo[];\\n  newTodoIds: Set<string>;\\n}\\n\\nfunction TodoList({ todos, newTodoIds }: TodoListProps) {\\n  return (\\n    <>\\n      {todos.map((todo, index) => {\\n        const isNew = newTodoIds.has(todo.id);\\n        const staggerDelay = isNew ? index * 50 : 0;\\n\\n        return (\\n          <PlanTodoItem\\n            key={todo.id}\\n            todo={todo}\\n            showConnector={index < todos.length - 1}\\n            className={cn(\\n              isNew &&\\n                \\\"motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-1 motion-safe:duration-300 motion-safe:ease-out\\\",\\n            )}\\n            style={\\n              isNew\\n                ? {\\n                    animationDelay: `${staggerDelay}ms`,\\n                    animationFillMode: \\\"backwards\\\",\\n                  }\\n                : undefined\\n            }\\n          />\\n        );\\n      })}\\n    </>\\n  );\\n}\\n\\ninterface ProgressBarProps {\\n  progress: number;\\n  isCelebrating: boolean;\\n}\\n\\nconst ProgressBar = memo(function ProgressBar({\\n  progress,\\n  isCelebrating,\\n}: ProgressBarProps) {\\n  return (\\n    <div\\n      className=\\\"bg-muted relative mb-3 h-1.5 overflow-hidden rounded-full\\\"\\n      role=\\\"progressbar\\\"\\n      aria-valuemin={0}\\n      aria-valuemax={100}\\n      aria-valuenow={progress}\\n    >\\n      <div\\n        className={cn(\\n          \\\"h-full rounded-full transition-all duration-500\\\",\\n          progress === 100\\n            ? \\\"bg-gradient-to-r from-emerald-600 via-emerald-500 to-emerald-400 motion-safe:animate-in motion-safe:fade-in motion-safe:duration-500 motion-safe:ease-out\\\"\\n            : \\\"bg-primary\\\",\\n        )}\\n        style={{\\n          width: `${progress}%`,\\n          boxShadow:\\n            \\\"inset 0 1px 0 rgba(255,255,255,0.3), 0 1px 2px rgba(0,0,0,0.2)\\\",\\n        }}\\n      />\\n      {isCelebrating && (\\n        <div\\n          className=\\\"pointer-events-none absolute inset-0 rounded-full motion-safe:animate-pulse\\\"\\n          style={{\\n            boxShadow: \\\"0 0 20px rgba(16, 185, 129, 0.6)\\\",\\n          }}\\n        />\\n      )}\\n    </div>\\n  );\\n});\\n\\nfunction PlanRoot({\\n  id,\\n  title,\\n  description,\\n  todos,\\n  maxVisibleTodos = INITIAL_VISIBLE_TODO_COUNT,\\n  className,\\n  compact = false,\\n}: PlanProps & { compact?: boolean }) {\\n  const seenTodoIds = useRef(new Set<string>());\\n  const [newTodoIds, setNewTodoIds] = useState<Set<string>>(new Set());\\n  const [isCelebrating, setIsCelebrating] = useState(false);\\n  const prevProgressRef = useRef(0);\\n\\n  const { visibleTodos, hiddenTodos, completedCount, allComplete, progress } =\\n    useMemo(() => {\\n      const completed = todos.filter((t) => t.status === \\\"completed\\\").length;\\n      return {\\n        visibleTodos: todos.slice(0, maxVisibleTodos),\\n        hiddenTodos: todos.slice(maxVisibleTodos),\\n        completedCount: completed,\\n        allComplete: completed === todos.length,\\n        progress: calculatePlanProgress({\\n          completedCount: completed,\\n          totalCount: todos.length,\\n        }),\\n      };\\n    }, [todos, maxVisibleTodos]);\\n\\n  useEffect(() => {\\n    const newIds = new Set<string>();\\n\\n    todos.forEach((todo) => {\\n      if (!seenTodoIds.current.has(todo.id)) {\\n        newIds.add(todo.id);\\n        seenTodoIds.current.add(todo.id);\\n      }\\n    });\\n\\n    if (newIds.size > 0) {\\n      setNewTodoIds(newIds);\\n\\n      // Clear animation class after entrance completes\\n      const timer = setTimeout(() => {\\n        setNewTodoIds(new Set());\\n      }, 500);\\n\\n      return () => clearTimeout(timer);\\n    }\\n  }, [todos]);\\n\\n  useEffect(() => {\\n    const shouldCelebrate = shouldCelebrateProgress({\\n      previous: prevProgressRef.current,\\n      next: progress,\\n    });\\n    prevProgressRef.current = progress;\\n\\n    if (shouldCelebrate) {\\n      setIsCelebrating(true);\\n      const timer = setTimeout(() => setIsCelebrating(false), 1000);\\n      return () => clearTimeout(timer);\\n    }\\n  }, [progress]);\\n\\n  const todoList = (\\n    <ul className={cn(\\\"min-w-0 space-y-1\\\", compact ? \\\"mt-0\\\" : \\\"mt-4\\\")}>\\n      <TodoList todos={visibleTodos} newTodoIds={newTodoIds} />\\n\\n      {hiddenTodos.length > 0 && (\\n        <li className=\\\"mt-1\\\">\\n          <Accordion type=\\\"single\\\" collapsible>\\n            <AccordionItem value=\\\"more\\\" className=\\\"border-0\\\">\\n              <AccordionTrigger className=\\\"text-muted-foreground hover:text-primary flex cursor-default items-start justify-start gap-2 py-1 text-sm font-normal [&>svg:last-child]:hidden\\\">\\n                <MoreHorizontal className=\\\"text-muted-foreground/70 mt-0.5 size-4 shrink-0\\\" />\\n                <span>{hiddenTodos.length} more</span>\\n              </AccordionTrigger>\\n              <AccordionContent className=\\\"pt-2 pb-0\\\">\\n                <ul className=\\\"-mx-2 space-y-2 px-2\\\">\\n                  <TodoList todos={hiddenTodos} newTodoIds={newTodoIds} />\\n                </ul>\\n              </AccordionContent>\\n            </AccordionItem>\\n          </Accordion>\\n        </li>\\n      )}\\n    </ul>\\n  );\\n\\n  return (\\n    <Card\\n      className={cn(\\\"isolate w-full max-w-xl min-w-80 gap-4 py-4\\\", className)}\\n      data-tool-ui-id={id}\\n      data-slot=\\\"plan\\\"\\n    >\\n      {!compact && (\\n        <CardHeader className=\\\"flex flex-row items-start justify-between gap-4\\\">\\n          <div className=\\\"space-y-1.5\\\">\\n            <CardTitle className=\\\"leading-5 font-medium text-pretty\\\">\\n              {title}\\n            </CardTitle>\\n            {description && <CardDescription>{description}</CardDescription>}\\n          </div>\\n          {allComplete && (\\n            <Check className=\\\"mt-0.5 size-5 shrink-0 text-emerald-500\\\" />\\n          )}\\n        </CardHeader>\\n      )}\\n\\n      <CardContent className=\\\"min-w-0 px-4\\\">\\n        <div\\n          className={cn(\\n            \\\"min-w-0\\\",\\n            !compact && \\\"bg-muted/70 rounded-lg px-6 py-4\\\",\\n          )}\\n        >\\n          {!compact && (\\n            <>\\n              <div className=\\\"text-muted-foreground mb-2 text-sm\\\">\\n                {completedCount} of {todos.length} complete\\n              </div>\\n              <ProgressBar progress={progress} isCelebrating={isCelebrating} />\\n            </>\\n          )}\\n          {todoList}\\n        </div>\\n      </CardContent>\\n    </Card>\\n  );\\n}\\n\\nfunction PlanComponent(props: PlanProps) {\\n  return <PlanRoot key={props.id} {...props} />;\\n}\\n\\nexport function PlanCompact(props: PlanProps) {\\n  return <PlanRoot key={props.id} {...props} compact />;\\n}\\n\\ntype PlanComponentType = typeof PlanComponent & {\\n  Compact: typeof PlanCompact;\\n};\\n\\nexport const Plan = Object.assign(PlanComponent, {\\n  Compact: PlanCompact,\\n}) as PlanComponentType;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/plan/progress.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/plan/progress.ts\",\n      \"content\": \"type ProgressInput = {\\n  completedCount: number;\\n  totalCount: number;\\n};\\n\\ntype CelebrateProgressInput = {\\n  previous: number;\\n  next: number;\\n};\\n\\nfunction clampProgress(value: number): number {\\n  if (!Number.isFinite(value)) return 0;\\n  return Math.max(0, Math.min(100, value));\\n}\\n\\nexport function calculatePlanProgress({\\n  completedCount,\\n  totalCount,\\n}: ProgressInput): number {\\n  if (totalCount <= 0) return 0;\\n  return clampProgress((completedCount / totalCount) * 100);\\n}\\n\\nexport function shouldCelebrateProgress({\\n  previous,\\n  next,\\n}: CelebrateProgressInput): boolean {\\n  return previous < 100 && next === 100;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/plan/README.md\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/plan/README.md\",\n      \"content\": \"# Plan\\n\\nImplementation for the \\\"plan\\\" Tool UI surface.\\n\\n## Files\\n\\n- public exports: components/tool-ui/plan/index.tsx\\n- serializable schema + parse helpers: components/tool-ui/plan/schema.ts\\n\\n## Companion assets\\n\\n- Docs page: app/docs/plan/content.mdx\\n- Preset payload: lib/presets/plan.ts\\n\\n## Quick check\\n\\nRun this after edits:\\n\\npnpm test\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/plan/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/plan/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport {\\n  ToolUIIdSchema,\\n  ToolUIReceiptSchema,\\n  ToolUIRoleSchema,\\n} from \\\"../shared/schema\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\n\\nexport const PlanTodoStatusSchema = z.enum([\\n  \\\"pending\\\",\\n  \\\"in_progress\\\",\\n  \\\"completed\\\",\\n  \\\"cancelled\\\",\\n]);\\n\\nexport const PlanTodoSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  status: PlanTodoStatusSchema,\\n  description: z.string().optional(),\\n});\\n\\nexport type PlanTodoStatus = z.infer<typeof PlanTodoStatusSchema>;\\nexport type PlanTodo = z.infer<typeof PlanTodoSchema>;\\n\\nexport const PlanPropsSchema = z\\n  .object({\\n    id: ToolUIIdSchema,\\n    role: ToolUIRoleSchema.optional(),\\n    receipt: ToolUIReceiptSchema.optional(),\\n    title: z.string().min(1),\\n    description: z.string().optional(),\\n    todos: z.array(PlanTodoSchema).min(1),\\n    maxVisibleTodos: z.number().finite().int().min(1).optional(),\\n  })\\n  .superRefine((value, ctx) => {\\n    const seenTodoIds = new Set<string>();\\n    value.todos.forEach((todo, index) => {\\n      if (seenTodoIds.has(todo.id)) {\\n        ctx.addIssue({\\n          code: \\\"custom\\\",\\n          path: [\\\"todos\\\", index, \\\"id\\\"],\\n          message: `Duplicate todo id \\\"${todo.id}\\\".`,\\n        });\\n        return;\\n      }\\n      seenTodoIds.add(todo.id);\\n    });\\n  });\\n\\nexport type PlanProps = z.infer<typeof PlanPropsSchema> & {\\n  className?: string;\\n};\\n\\nexport const SerializablePlanSchema = PlanPropsSchema;\\n\\nexport type SerializablePlan = z.infer<typeof SerializablePlanSchema>;\\n\\nconst SerializablePlanSchemaContract = defineToolUiContract(\\n  \\\"Plan\\\",\\n  SerializablePlanSchema,\\n);\\n\\nexport const parseSerializablePlan: (input: unknown) => SerializablePlan =\\n  SerializablePlanSchemaContract.parse;\\n\\nexport const safeParseSerializablePlan: (\\n  input: unknown,\\n) => SerializablePlan | null = SerializablePlanSchemaContract.safeParse;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { ReactNode } from \\\"react\\\";\\n\\n/**\\n * Tool UI conventions:\\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\\n * - Schema: `SerializableXSchema`\\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\\n * - Root attrs: `data-tool-ui-id` + `data-slot`\\n */\\n\\n/**\\n * Schema for tool UI identity.\\n *\\n * Every tool UI should have a unique identifier that:\\n * - Is stable across re-renders\\n * - Is meaningful (not auto-generated)\\n * - Is unique within the conversation\\n *\\n * Format recommendation: `{component-type}-{semantic-identifier}`\\n * Examples: \\\"data-table-expenses-q3\\\", \\\"option-list-deploy-target\\\"\\n */\\nexport const ToolUIIdSchema = z.string().min(1);\\n\\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\\n\\n/**\\n * Primary role of a Tool UI surface in a chat context.\\n */\\nexport const ToolUIRoleSchema = z.enum([\\n  \\\"information\\\",\\n  \\\"decision\\\",\\n  \\\"control\\\",\\n  \\\"state\\\",\\n  \\\"composite\\\",\\n]);\\n\\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\\n\\nexport const ToolUIReceiptOutcomeSchema = z.enum([\\n  \\\"success\\\",\\n  \\\"partial\\\",\\n  \\\"failed\\\",\\n  \\\"cancelled\\\",\\n]);\\n\\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\\n\\n/**\\n * Optional receipt metadata: a durable summary of an outcome.\\n */\\nexport const ToolUIReceiptSchema = z.object({\\n  outcome: ToolUIReceiptOutcomeSchema,\\n  summary: z.string().min(1),\\n  identifiers: z.record(z.string(), z.string()).optional(),\\n  at: z.string().datetime(),\\n});\\n\\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\\n\\n/**\\n * Base schema for Tool UI payloads (id + optional role/receipt).\\n */\\nexport const ToolUISurfaceSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n});\\n\\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\\n\\nexport const ActionSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  /**\\n   * Canonical narration the assistant can use after this action is taken.\\n   *\\n   * Example: \\\"I exported the table as CSV.\\\" / \\\"I opened the link in a new tab.\\\"\\n   */\\n  sentence: z.string().optional(),\\n  confirmLabel: z.string().optional(),\\n  variant: z\\n    .enum([\\\"default\\\", \\\"destructive\\\", \\\"secondary\\\", \\\"ghost\\\", \\\"outline\\\"])\\n    .optional(),\\n  icon: z.custom<ReactNode>().optional(),\\n  loading: z.boolean().optional(),\\n  disabled: z.boolean().optional(),\\n  shortcut: z.string().optional(),\\n});\\n\\nexport type Action = z.infer<typeof ActionSchema>;\\nexport type LocalAction = Action;\\nexport type DecisionAction = Action;\\n\\nexport const DecisionResultSchema = z.object({\\n  kind: z.literal(\\\"decision\\\"),\\n  version: z.literal(1),\\n  decisionId: z.string().min(1),\\n  actionId: z.string().min(1),\\n  actionLabel: z.string().min(1),\\n  at: z.string().datetime(),\\n  payload: z.record(z.string(), z.unknown()).optional(),\\n});\\n\\nexport type DecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n> = Omit<z.infer<typeof DecisionResultSchema>, \\\"payload\\\"> & {\\n  payload?: TPayload;\\n};\\n\\nexport function createDecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n>(args: {\\n  decisionId: string;\\n  action: { id: string; label: string };\\n  payload?: TPayload;\\n}): DecisionResult<TPayload> {\\n  return {\\n    kind: \\\"decision\\\",\\n    version: 1,\\n    decisionId: args.decisionId,\\n    actionId: args.action.id,\\n    actionLabel: args.action.label,\\n    at: new Date().toISOString(),\\n    payload: args.payload,\\n  };\\n}\\n\\nexport const ActionButtonsPropsSchema = z.object({\\n  actions: z.array(ActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n  className: z.string().optional(),\\n});\\n\\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\\n  actions: z.array(SerializableActionSchema),\\n}).omit({ className: true });\\n\\nexport interface ActionsConfig {\\n  items: Action[];\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  confirmTimeout?: number;\\n}\\n\\nexport const SerializableActionsConfigSchema = z.object({\\n  items: z.array(SerializableActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n});\\n\\nexport type SerializableActionsConfig = z.infer<\\n  typeof SerializableActionsConfigSchema\\n>;\\n\\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/preferences-panel.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"preferences-panel\",\n  \"type\": \"registry:block\",\n  \"title\": \"Preferences Panel\",\n  \"description\": \"Compact settings panel for user preferences.\",\n  \"dependencies\": [\n    \"lucide-react\",\n    \"zod\"\n  ],\n  \"registryDependencies\": [\n    \"button\",\n    \"label\",\n    \"select\",\n    \"separator\",\n    \"switch\",\n    \"toggle-group\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/preferences-panel/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/preferences-panel/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn           → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n *   Button       → shadcn/ui Button\\n *   Switch       → shadcn/ui Switch\\n *   ToggleGroup  → shadcn/ui ToggleGroup\\n *   Select       → shadcn/ui Select\\n *   Separator    → shadcn/ui Separator\\n *   Label        → shadcn/ui Label\\n */\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport { Button } from \\\"@/components/ui/button\\\";\\nexport { Switch } from \\\"@/components/ui/switch\\\";\\nexport { ToggleGroup, ToggleGroupItem } from \\\"@/components/ui/toggle-group\\\";\\nexport {\\n  Select,\\n  SelectContent,\\n  SelectItem,\\n  SelectTrigger,\\n  SelectValue,\\n} from \\\"@/components/ui/select\\\";\\nexport { Separator } from \\\"@/components/ui/separator\\\";\\nexport { Label } from \\\"@/components/ui/label\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/preferences-panel/index.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/preferences-panel/index.tsx\",\n      \"content\": \"export { PreferencesPanel, PreferencesPanelReceipt } from \\\"./preferences-panel\\\";\\nexport {\\n  type SerializablePreferencesPanel,\\n  type SerializablePreferencesPanelReceipt,\\n  type PreferencesPanelProps,\\n  type PreferencesPanelReceiptProps,\\n  type PreferencesValue,\\n  type PreferenceItem,\\n  type PreferenceSection,\\n} from \\\"./schema\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/preferences-panel/preferences-panel.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/preferences-panel/preferences-panel.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { useCallback, useMemo } from \\\"react\\\";\\nimport type {\\n  PreferencesPanelProps,\\n  PreferencesPanelReceiptProps,\\n  PreferencesValue,\\n  PreferenceItem,\\n  PreferenceSection,\\n} from \\\"./schema\\\";\\nimport { ActionButtons } from \\\"../shared/action-buttons\\\";\\nimport { normalizeActionsConfig } from \\\"../shared/actions-config\\\";\\nimport { type Action } from \\\"../shared/schema\\\";\\nimport { useControllableState } from \\\"../shared/use-controllable-state\\\";\\nimport { useSignatureReset } from \\\"../shared/use-signature-reset\\\";\\n\\nimport {\\n  cn,\\n  Switch,\\n  ToggleGroup,\\n  ToggleGroupItem,\\n  Select,\\n  SelectContent,\\n  SelectItem,\\n  SelectTrigger,\\n  SelectValue,\\n  Separator,\\n  Label,\\n} from \\\"./_adapter\\\";\\nimport { Check, AlertCircle } from \\\"lucide-react\\\";\\nimport { createPreferencesSectionSignature } from \\\"./signature\\\";\\n\\nfunction getInitialValue(item: PreferenceItem): string | boolean {\\n  switch (item.type) {\\n    case \\\"switch\\\":\\n      return item.defaultChecked ?? false;\\n    case \\\"toggle\\\":\\n      return item.defaultValue ?? item.options?.[0]?.value ?? \\\"\\\";\\n    case \\\"select\\\":\\n      return item.defaultSelected ?? item.selectOptions?.[0]?.value ?? \\\"\\\";\\n  }\\n}\\n\\nfunction formatDisplayValue(\\n  item: PreferenceItem,\\n  value: string | boolean,\\n): string {\\n  if (item.type === \\\"switch\\\") {\\n    return typeof value === \\\"boolean\\\" && value ? \\\"On\\\" : \\\"Off\\\";\\n  }\\n\\n  const stringValue = typeof value === \\\"string\\\" ? value : \\\"\\\";\\n  const options = item.type === \\\"toggle\\\" ? item.options : item.selectOptions;\\n  const option = options?.find((opt) => opt.value === stringValue);\\n\\n  return option?.label ?? stringValue;\\n}\\n\\nfunction computeInitialValues(sections: PreferenceSection[]): PreferencesValue {\\n  return sections.reduce<PreferencesValue>((acc, section) => {\\n    section.items.forEach((item) => {\\n      acc[item.id] = getInitialValue(item);\\n    });\\n    return acc;\\n  }, {});\\n}\\n\\ninterface PreferenceControlProps {\\n  item: PreferenceItem;\\n  value: string | boolean;\\n  onChange: (value: string | boolean) => void;\\n  disabled?: boolean;\\n}\\n\\nfunction SwitchControl({\\n  id,\\n  checked,\\n  onChange,\\n  disabled,\\n  label,\\n}: {\\n  id: string;\\n  checked: boolean;\\n  onChange: (value: boolean) => void;\\n  disabled?: boolean;\\n  label: string;\\n}) {\\n  return (\\n    <Switch\\n      id={id}\\n      checked={checked}\\n      onCheckedChange={onChange}\\n      disabled={disabled}\\n      aria-label={label}\\n    />\\n  );\\n}\\n\\nfunction ToggleControl({\\n  value,\\n  options,\\n  onChange,\\n  disabled,\\n  label,\\n}: {\\n  value: string;\\n  options: Array<{ value: string; label: string }>;\\n  onChange: (value: string) => void;\\n  disabled?: boolean;\\n  label: string;\\n}) {\\n  return (\\n    <ToggleGroup\\n      type=\\\"single\\\"\\n      value={value}\\n      onValueChange={(v) => v && onChange(v)}\\n      disabled={disabled}\\n      aria-label={label}\\n      className=\\\"gap-1\\\"\\n    >\\n      {options.map((opt) => (\\n        <ToggleGroupItem\\n          key={opt.value}\\n          value={opt.value}\\n          aria-label={opt.label}\\n          className=\\\"!rounded-full px-3 py-1.5 text-sm\\\"\\n        >\\n          {opt.label}\\n        </ToggleGroupItem>\\n      ))}\\n    </ToggleGroup>\\n  );\\n}\\n\\nfunction SelectControl({\\n  id,\\n  value,\\n  options,\\n  onChange,\\n  disabled,\\n  label,\\n}: {\\n  id: string;\\n  value: string;\\n  options: Array<{ value: string; label: string }>;\\n  onChange: (value: string) => void;\\n  disabled?: boolean;\\n  label: string;\\n}) {\\n  return (\\n    <Select value={value} onValueChange={onChange} disabled={disabled}>\\n      <SelectTrigger id={id} className=\\\"w-[180px]\\\" aria-label={label}>\\n        <SelectValue placeholder=\\\"Select...\\\" />\\n      </SelectTrigger>\\n      <SelectContent>\\n        {options.map((opt) => (\\n          <SelectItem key={opt.value} value={opt.value}>\\n            {opt.label}\\n          </SelectItem>\\n        ))}\\n      </SelectContent>\\n    </Select>\\n  );\\n}\\n\\nfunction PreferenceControl({\\n  item,\\n  value,\\n  onChange,\\n  disabled,\\n}: PreferenceControlProps) {\\n  const id = `preference-${item.id}`;\\n\\n  if (item.type === \\\"switch\\\") {\\n    return (\\n      <SwitchControl\\n        id={id}\\n        checked={typeof value === \\\"boolean\\\" ? value : false}\\n        onChange={onChange}\\n        disabled={disabled}\\n        label={item.label}\\n      />\\n    );\\n  }\\n\\n  const stringValue = typeof value === \\\"string\\\" ? value : \\\"\\\";\\n\\n  if (item.type === \\\"toggle\\\" && item.options) {\\n    return (\\n      <ToggleControl\\n        value={stringValue}\\n        options={item.options}\\n        onChange={onChange}\\n        disabled={disabled}\\n        label={item.label}\\n      />\\n    );\\n  }\\n\\n  if (item.type === \\\"select\\\" && item.selectOptions) {\\n    return (\\n      <SelectControl\\n        id={id}\\n        value={stringValue}\\n        options={item.selectOptions}\\n        onChange={onChange}\\n        disabled={disabled}\\n        label={item.label}\\n      />\\n    );\\n  }\\n\\n  return null;\\n}\\n\\ninterface PreferenceItemRowProps {\\n  item: PreferenceItem;\\n  value: string | boolean;\\n  onChange?: (value: string | boolean) => void;\\n  disabled?: boolean;\\n  isReceipt?: boolean;\\n  error?: string;\\n  showSuccessIndicators?: boolean;\\n  isFirstInSectionWithoutHeading?: boolean;\\n}\\n\\nfunction ItemLabel({\\n  item,\\n  error,\\n  isReceipt,\\n}: {\\n  item: PreferenceItem;\\n  error?: string;\\n  isReceipt: boolean;\\n}) {\\n  const htmlFor = `preference-${item.id}`;\\n\\n  if (isReceipt) {\\n    return (\\n      <>\\n        <span className=\\\"text-sm leading-6 font-medium text-pretty\\\">\\n          {item.label}\\n        </span>\\n        {error ? (\\n          <span className=\\\"text-destructive text-sm font-normal text-pretty\\\">\\n            {error}\\n          </span>\\n        ) : item.description ? (\\n          <span className=\\\"text-muted-foreground text-sm font-normal text-pretty\\\">\\n            {item.description}\\n          </span>\\n        ) : null}\\n      </>\\n    );\\n  }\\n\\n  return (\\n    <>\\n      <Label htmlFor={htmlFor} className=\\\"leading-6 font-medium text-pretty\\\">\\n        {item.label}\\n      </Label>\\n      {item.description && (\\n        <p className=\\\"text-muted-foreground text-sm font-normal text-pretty\\\">\\n          {item.description}\\n        </p>\\n      )}\\n    </>\\n  );\\n}\\n\\nfunction ItemValue({\\n  item,\\n  value,\\n  error,\\n  showSuccessIndicators,\\n}: {\\n  item: PreferenceItem;\\n  value: string | boolean;\\n  error?: string;\\n  showSuccessIndicators: boolean;\\n}) {\\n  const displayValue = formatDisplayValue(item, value);\\n\\n  return (\\n    <div className=\\\"flex shrink-0 items-center gap-2\\\">\\n      <span className=\\\"text-muted-foreground text-sm font-medium\\\">\\n        {displayValue}\\n      </span>\\n      {error ? (\\n        <AlertCircle className=\\\"text-destructive size-3.5\\\" />\\n      ) : showSuccessIndicators ? (\\n        <Check className=\\\"size-3.5 text-emerald-600 dark:text-emerald-500\\\" />\\n      ) : null}\\n    </div>\\n  );\\n}\\n\\nfunction PreferenceItemRow({\\n  item,\\n  value,\\n  onChange,\\n  disabled,\\n  isReceipt = false,\\n  error,\\n  showSuccessIndicators = false,\\n  isFirstInSectionWithoutHeading = false,\\n}: PreferenceItemRowProps) {\\n  const shouldStack = item.type !== \\\"switch\\\" && !isReceipt;\\n\\n  return (\\n    <div\\n      className={cn(\\n        \\\"flex items-start justify-between gap-4\\\",\\n        isFirstInSectionWithoutHeading ? \\\"pt-0 pb-3\\\" : \\\"py-3\\\",\\n        shouldStack &&\\n          \\\"flex-col gap-3 @sm/preferences-panel:flex-row @sm/preferences-panel:gap-4\\\",\\n      )}\\n    >\\n      <div className=\\\"flex flex-col gap-1\\\">\\n        <ItemLabel item={item} error={error} isReceipt={isReceipt} />\\n      </div>\\n\\n      {isReceipt ? (\\n        <ItemValue\\n          item={item}\\n          value={value}\\n          error={error}\\n          showSuccessIndicators={showSuccessIndicators}\\n        />\\n      ) : (\\n        <div className=\\\"flex shrink-0\\\">\\n          <PreferenceControl\\n            item={item}\\n            value={value}\\n            onChange={onChange!}\\n            disabled={disabled}\\n          />\\n        </div>\\n      )}\\n    </div>\\n  );\\n}\\n\\ninterface ItemListProps {\\n  items: PreferenceItem[];\\n  values: PreferencesValue;\\n  onChangeValue?: (itemId: string, value: string | boolean) => void;\\n  disabled?: boolean;\\n  isReceipt?: boolean;\\n  errors?: Record<string, string>;\\n  showSuccessIndicators?: boolean;\\n  hasHeading?: boolean;\\n  hasTitle?: boolean;\\n}\\n\\nfunction ItemList({\\n  items,\\n  values,\\n  onChangeValue,\\n  disabled,\\n  isReceipt,\\n  errors,\\n  showSuccessIndicators,\\n  hasHeading = false,\\n  hasTitle = false,\\n}: ItemListProps) {\\n  const shouldRemoveFirstPadding = !hasHeading && hasTitle;\\n\\n  return (\\n    <div className=\\\"flex flex-col\\\">\\n      {items.map((item, index) => {\\n        const isFirst = index === 0;\\n        const itemValue = values[item.id] ?? getInitialValue(item);\\n        const handleChange = onChangeValue\\n          ? (v: string | boolean) => onChangeValue(item.id, v)\\n          : undefined;\\n\\n        return (\\n          <div key={item.id}>\\n            {!isFirst && <Separator className=\\\"my-1\\\" />}\\n            <PreferenceItemRow\\n              item={item}\\n              value={itemValue}\\n              onChange={handleChange}\\n              disabled={disabled}\\n              isReceipt={isReceipt}\\n              error={errors?.[item.id]}\\n              showSuccessIndicators={showSuccessIndicators}\\n              isFirstInSectionWithoutHeading={\\n                isFirst && shouldRemoveFirstPadding\\n              }\\n            />\\n          </div>\\n        );\\n      })}\\n    </div>\\n  );\\n}\\n\\ninterface PreferencesSectionProps {\\n  section: PreferenceSection;\\n  values: PreferencesValue;\\n  onChangeValue?: (itemId: string, value: string | boolean) => void;\\n  disabled?: boolean;\\n  isReceipt?: boolean;\\n  errors?: Record<string, string>;\\n  hasTitle?: boolean;\\n}\\n\\nfunction PreferencesSection({\\n  section,\\n  values,\\n  onChangeValue,\\n  disabled,\\n  isReceipt = false,\\n  errors,\\n  hasTitle = false,\\n}: PreferencesSectionProps) {\\n  const hasErrors = !!(errors && Object.keys(errors).length > 0);\\n\\n  const content = (\\n    <ItemList\\n      items={section.items}\\n      values={values}\\n      onChangeValue={onChangeValue}\\n      disabled={disabled}\\n      isReceipt={isReceipt}\\n      errors={errors}\\n      showSuccessIndicators={hasErrors}\\n      hasHeading={!!section.heading}\\n      hasTitle={hasTitle}\\n    />\\n  );\\n\\n  if (section.heading) {\\n    return (\\n      <fieldset className=\\\"flex flex-col\\\">\\n        <legend className=\\\"text-muted-foreground pb-1 text-xs tracking-widest uppercase\\\">\\n          {section.heading}\\n        </legend>\\n        {content}\\n      </fieldset>\\n    );\\n  }\\n\\n  return content;\\n}\\n\\ninterface ReceiptHeaderProps {\\n  title: string;\\n  hasErrors: boolean;\\n}\\n\\nfunction ReceiptHeader({ title, hasErrors }: ReceiptHeaderProps) {\\n  return (\\n    <>\\n      <div className=\\\"flex items-center justify-between gap-3 px-5 py-4\\\">\\n        <h2 className=\\\"text-base leading-none font-semibold\\\">{title}</h2>\\n        {hasErrors === true ? (\\n          <span className=\\\"text-destructive flex items-center gap-1.5 text-xs font-medium\\\">\\n            <AlertCircle className=\\\"size-3.5\\\" />\\n            Error\\n          </span>\\n        ) : (\\n          <span className=\\\"flex items-center gap-1.5 text-xs font-medium text-emerald-600 dark:text-emerald-500\\\">\\n            <Check className=\\\"size-3.5\\\" />\\n            Saved\\n          </span>\\n        )}\\n      </div>\\n      <Separator />\\n    </>\\n  );\\n}\\n\\nexport function PreferencesPanelReceipt({\\n  id,\\n  title,\\n  sections,\\n  choice,\\n  error,\\n  className,\\n}: PreferencesPanelReceiptProps) {\\n  const hasErrors = error && Object.keys(error).length > 0;\\n\\n  return (\\n    <article\\n      data-slot=\\\"preferences-panel\\\"\\n      data-tool-ui-id={id}\\n      data-receipt=\\\"true\\\"\\n      role=\\\"status\\\"\\n      aria-label={\\n        hasErrors ? \\\"Preferences with errors\\\" : \\\"Confirmed preferences\\\"\\n      }\\n      className={cn(\\n        \\\"@container/preferences-panel flex w-full max-w-md min-w-80 flex-col\\\",\\n        className,\\n      )}\\n    >\\n      <div className=\\\"bg-card/60 flex w-full flex-col overflow-hidden rounded-2xl border opacity-95 shadow-xs\\\">\\n        {title && <ReceiptHeader title={title} hasErrors={!!hasErrors} />}\\n        <div\\n          className={cn(\\\"flex flex-col gap-4 px-5\\\", title ? \\\"py-6\\\" : \\\"py-2\\\")}\\n        >\\n          {sections.map((section, index) => (\\n            <div key={index}>\\n              <PreferencesSection\\n                section={section}\\n                values={choice}\\n                errors={error}\\n                isReceipt={true}\\n                hasTitle={!!title}\\n              />\\n            </div>\\n          ))}\\n        </div>\\n      </div>\\n    </article>\\n  );\\n}\\n\\nfunction PreferencesPanelRoot({\\n  id,\\n  title,\\n  sections,\\n  value: controlledValue,\\n  onChange,\\n  actions,\\n  onAction,\\n  onBeforeAction,\\n  className,\\n}: PreferencesPanelProps) {\\n  const initialValues = useMemo(\\n    () => computeInitialValues(sections),\\n    [sections],\\n  );\\n  const sectionsSignature = useMemo(\\n    () => createPreferencesSectionSignature(sections),\\n    [sections],\\n  );\\n  const {\\n    value: currentValue,\\n    isControlled,\\n    setValue,\\n    setUncontrolledValue,\\n  } = useControllableState<PreferencesValue>({\\n    value: controlledValue,\\n    defaultValue: initialValues,\\n    onChange,\\n  });\\n\\n  useSignatureReset(sectionsSignature, () => {\\n    if (!isControlled) {\\n      setUncontrolledValue(initialValues);\\n    }\\n  });\\n\\n  const updateValue = useCallback(\\n    (itemId: string, newValue: string | boolean) => {\\n      setValue((prev) => ({ ...prev, [itemId]: newValue }));\\n    },\\n    [setValue],\\n  );\\n\\n  const isDirty = useMemo(() => {\\n    return Object.keys(currentValue).some(\\n      (key) => currentValue[key] !== initialValues[key],\\n    );\\n  }, [currentValue, initialValues]);\\n\\n  const handleCancel = useCallback((): PreferencesValue => {\\n    setValue(initialValues);\\n    return initialValues;\\n  }, [initialValues, setValue]);\\n\\n  const handleAction = useCallback(\\n    async (actionId: string) => {\\n      let nextValue = currentValue;\\n\\n      if (actionId === \\\"cancel\\\") {\\n        nextValue = handleCancel();\\n      }\\n\\n      await onAction?.(actionId, nextValue);\\n    },\\n    [currentValue, handleCancel, onAction],\\n  );\\n\\n  const normalizedActions = useMemo(() => {\\n    const normalized = normalizeActionsConfig(actions);\\n    if (normalized) {\\n      return {\\n        ...normalized,\\n        align: normalized.align ?? (\\\"right\\\" as const),\\n      };\\n    }\\n\\n    const defaultActions: Action[] = [\\n      { id: \\\"cancel\\\", label: \\\"Cancel\\\", variant: \\\"ghost\\\" },\\n      { id: \\\"save\\\", label: \\\"Save Changes\\\", variant: \\\"default\\\" },\\n    ];\\n\\n    return {\\n      items: defaultActions,\\n      align: \\\"right\\\" as const,\\n    };\\n  }, [actions]);\\n\\n  const actionsWithState = useMemo((): Action[] => {\\n    return normalizedActions.items.map((action) => {\\n      const isSaveAction = action.id === \\\"save\\\";\\n      const baseDisabled = \\\"disabled\\\" in action ? action.disabled : false;\\n      const shouldDisable = baseDisabled || (isSaveAction && !isDirty);\\n\\n      return {\\n        ...action,\\n        disabled: shouldDisable,\\n      };\\n    });\\n  }, [normalizedActions.items, isDirty]);\\n\\n  return (\\n    <article\\n      data-slot=\\\"preferences-panel\\\"\\n      data-tool-ui-id={id}\\n      role=\\\"form\\\"\\n      className={cn(\\n        \\\"text-foreground @container/preferences-panel flex w-full max-w-md min-w-80 flex-col gap-3\\\",\\n        className,\\n      )}\\n    >\\n      <div className=\\\"bg-card flex w-full flex-col overflow-hidden rounded-2xl border shadow-xs\\\">\\n        {title && (\\n          <>\\n            <div className=\\\"px-5 py-4\\\">\\n              <h2 className=\\\"text-base leading-none font-semibold\\\">{title}</h2>\\n            </div>\\n            <Separator />\\n          </>\\n        )}\\n        <div\\n          className={cn(\\\"flex flex-col gap-4 px-5\\\", title ? \\\"py-6\\\" : \\\"py-2\\\")}\\n        >\\n          {sections.map((section, sectionIndex) => (\\n            <div key={sectionIndex}>\\n              <PreferencesSection\\n                section={section}\\n                values={currentValue}\\n                onChangeValue={updateValue}\\n                isReceipt={false}\\n                hasTitle={!!title}\\n              />\\n            </div>\\n          ))}\\n        </div>\\n      </div>\\n\\n      <div className=\\\"@container/actions\\\">\\n        <ActionButtons\\n          actions={actionsWithState}\\n          align={normalizedActions.align}\\n          confirmTimeout={normalizedActions.confirmTimeout}\\n          onAction={handleAction}\\n          onBeforeAction={\\n            onBeforeAction\\n              ? (actionId) => onBeforeAction(actionId, currentValue)\\n              : undefined\\n          }\\n        />\\n      </div>\\n    </article>\\n  );\\n}\\n\\ntype PreferencesPanelComponent = typeof PreferencesPanelRoot & {\\n  Receipt: typeof PreferencesPanelReceipt;\\n};\\n\\nexport const PreferencesPanel = Object.assign(PreferencesPanelRoot, {\\n  Receipt: PreferencesPanelReceipt,\\n}) as PreferencesPanelComponent;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/preferences-panel/README.md\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/preferences-panel/README.md\",\n      \"content\": \"# Preferences Panel\\n\\nImplementation for the \\\"preferences-panel\\\" Tool UI surface.\\n\\n## Files\\n\\n- public exports: components/tool-ui/preferences-panel/index.tsx\\n- serializable schema + parse helpers: components/tool-ui/preferences-panel/schema.ts\\n\\n## Companion assets\\n\\n- Docs page: app/docs/preferences-panel/content.mdx\\n- Preset payload: lib/presets/preferences-panel.ts\\n\\n## Quick check\\n\\nRun this after edits:\\n\\npnpm test\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/preferences-panel/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/preferences-panel/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { type ActionsProp } from \\\"../shared/actions-config\\\";\\nimport type { EmbeddedActionsProps } from \\\"../shared/embedded-actions\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\nimport {\\n  SerializableActionSchema,\\n  SerializableActionsConfigSchema,\\n  ToolUIIdSchema,\\n  ToolUIReceiptSchema,\\n  ToolUIRoleSchema,\\n} from \\\"../shared/schema\\\";\\n\\nconst PreferenceItemBaseSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  description: z.string().optional(),\\n});\\n\\nconst PreferenceSwitchSchema = PreferenceItemBaseSchema.extend({\\n  type: z.literal(\\\"switch\\\"),\\n  defaultChecked: z.boolean().optional(),\\n});\\n\\nconst PreferenceToggleSchema = PreferenceItemBaseSchema.extend({\\n  type: z.literal(\\\"toggle\\\"),\\n  options: z\\n    .array(\\n      z.object({\\n        value: z.string().min(1),\\n        label: z.string().min(1),\\n      }),\\n    )\\n    .min(2),\\n  defaultValue: z.string().optional(),\\n});\\n\\nconst PreferenceSelectSchema = PreferenceItemBaseSchema.extend({\\n  type: z.literal(\\\"select\\\"),\\n  selectOptions: z\\n    .array(\\n      z.object({\\n        value: z.string().min(1),\\n        label: z.string().min(1),\\n      }),\\n    )\\n    .min(5),\\n  defaultSelected: z.string().optional(),\\n});\\n\\nconst PreferenceItemSchema = z.discriminatedUnion(\\\"type\\\", [\\n  PreferenceSwitchSchema,\\n  PreferenceToggleSchema,\\n  PreferenceSelectSchema,\\n]);\\n\\nconst PreferenceSectionSchema = z.object({\\n  heading: z.string().min(1).optional(),\\n  items: z.array(PreferenceItemSchema).min(1),\\n});\\n\\nconst PreferencesPanelBaseSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n  title: z.string().min(1).optional(),\\n  sections: z.array(PreferenceSectionSchema).min(1),\\n});\\n\\nexport const SerializablePreferencesPanelSchema =\\n  PreferencesPanelBaseSchema.extend({\\n    actions: z\\n      .union([\\n        z.array(SerializableActionSchema),\\n        SerializableActionsConfigSchema,\\n      ])\\n      .optional(),\\n  }).strict();\\n\\nexport const SerializablePreferencesPanelReceiptSchema =\\n  PreferencesPanelBaseSchema.extend({\\n    choice: z.record(z.string(), z.union([z.string(), z.boolean()])),\\n    error: z.record(z.string(), z.string()).optional(),\\n  }).strict();\\n\\nexport type SerializablePreferencesPanel = z.infer<\\n  typeof SerializablePreferencesPanelSchema\\n>;\\n\\nexport type SerializablePreferencesPanelReceipt = z.infer<\\n  typeof SerializablePreferencesPanelReceiptSchema\\n>;\\n\\nconst SerializablePreferencesPanelSchemaContract = defineToolUiContract(\\n  \\\"PreferencesPanel\\\",\\n  SerializablePreferencesPanelSchema,\\n);\\n\\nconst SerializablePreferencesPanelReceiptSchemaContract = defineToolUiContract(\\n  \\\"PreferencesPanelReceipt\\\",\\n  SerializablePreferencesPanelReceiptSchema,\\n);\\n\\nexport const parseSerializablePreferencesPanel: (\\n  input: unknown,\\n) => SerializablePreferencesPanel =\\n  SerializablePreferencesPanelSchemaContract.parse;\\n\\nexport const safeParseSerializablePreferencesPanel: (\\n  input: unknown,\\n) => SerializablePreferencesPanel | null =\\n  SerializablePreferencesPanelSchemaContract.safeParse;\\n\\nexport const parseSerializablePreferencesPanelReceipt: (\\n  input: unknown,\\n) => SerializablePreferencesPanelReceipt =\\n  SerializablePreferencesPanelReceiptSchemaContract.parse;\\n\\nexport const safeParseSerializablePreferencesPanelReceipt: (\\n  input: unknown,\\n) => SerializablePreferencesPanelReceipt | null =\\n  SerializablePreferencesPanelReceiptSchemaContract.safeParse;\\n\\nexport interface PreferencesValue {\\n  [itemId: string]: string | boolean;\\n}\\n\\nexport interface PreferencesPanelProps extends Omit<\\n  SerializablePreferencesPanel,\\n  \\\"actions\\\"\\n> {\\n  className?: string;\\n  value?: PreferencesValue;\\n  onChange?: (value: PreferencesValue) => void;\\n  actions?: ActionsProp;\\n  onAction?: EmbeddedActionsProps<PreferencesValue>[\\\"onAction\\\"];\\n  onBeforeAction?: EmbeddedActionsProps<PreferencesValue>[\\\"onBeforeAction\\\"];\\n}\\n\\nexport interface PreferencesPanelReceiptProps extends SerializablePreferencesPanelReceipt {\\n  className?: string;\\n}\\n\\nexport type PreferenceItem = z.infer<typeof PreferenceItemSchema>;\\nexport type PreferenceSection = z.infer<typeof PreferenceSectionSchema>;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/preferences-panel/signature.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/preferences-panel/signature.ts\",\n      \"content\": \"import type { PreferenceSection } from \\\"./schema\\\";\\n\\nexport function createPreferencesSectionSignature(\\n  sections: PreferenceSection[],\\n): string {\\n  return JSON.stringify(\\n    sections.map((section) => ({\\n      heading: section.heading ?? \\\"\\\",\\n      items: section.items.map((item) => {\\n        if (item.type === \\\"switch\\\") {\\n          return {\\n            id: item.id,\\n            type: item.type,\\n            defaultChecked: item.defaultChecked ?? false,\\n          };\\n        }\\n\\n        if (item.type === \\\"toggle\\\") {\\n          return {\\n            id: item.id,\\n            type: item.type,\\n            defaultValue: item.defaultValue ?? item.options[0]?.value ?? \\\"\\\",\\n            options: item.options.map((option) => option.value),\\n          };\\n        }\\n\\n        return {\\n          id: item.id,\\n          type: item.type,\\n          defaultSelected:\\n            item.defaultSelected ?? item.selectOptions[0]?.value ?? \\\"\\\",\\n          options: item.selectOptions.map((option) => option.value),\\n        };\\n      }),\\n    })),\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/shared/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn     → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n *   Button → shadcn/ui Button\\n */\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport { Button } from \\\"@/components/ui/button\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/action-buttons.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/shared/action-buttons.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport type { Action } from \\\"./schema\\\";\\nimport { cn, Button } from \\\"./_adapter\\\";\\nimport { useActionButtons } from \\\"./use-action-buttons\\\";\\n\\nexport interface ActionButtonsProps {\\n  actions: Action[];\\n  onAction: (actionId: string) => void | Promise<void>;\\n  onBeforeAction?: (actionId: string) => boolean | Promise<boolean>;\\n  confirmTimeout?: number;\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  className?: string;\\n}\\n\\nexport function ActionButtons({\\n  actions,\\n  onAction,\\n  onBeforeAction,\\n  confirmTimeout = 3000,\\n  align = \\\"right\\\",\\n  className,\\n}: ActionButtonsProps) {\\n  const { actions: resolvedActions, runAction } = useActionButtons({\\n    actions,\\n    onAction,\\n    onBeforeAction,\\n    confirmTimeout,\\n  });\\n\\n  return (\\n    <div\\n      className={cn(\\n        \\\"flex flex-col gap-3\\\",\\n        \\\"@sm/actions:flex-row @sm/actions:flex-wrap @sm/actions:items-center @sm/actions:gap-2\\\",\\n        align === \\\"left\\\" && \\\"@sm/actions:justify-start\\\",\\n        align === \\\"center\\\" && \\\"@sm/actions:justify-center\\\",\\n        align === \\\"right\\\" && \\\"@sm/actions:justify-end\\\",\\n        className,\\n      )}\\n    >\\n      {resolvedActions.map((action) => {\\n        const label = action.currentLabel;\\n        const variant = action.variant || \\\"default\\\";\\n\\n        return (\\n          <Button\\n            key={action.id}\\n            variant={variant}\\n            onClick={() => runAction(action.id)}\\n            disabled={action.isDisabled}\\n            className={cn(\\n              \\\"rounded-full px-4!\\\",\\n              \\\"justify-center\\\",\\n              \\\"min-h-11 w-full text-base\\\",\\n              \\\"@sm/actions:min-h-0 @sm/actions:w-auto @sm/actions:px-3 @sm/actions:py-2 @sm/actions:text-sm\\\",\\n              action.isConfirming &&\\n                \\\"ring-destructive ring-2 ring-offset-2 motion-safe:animate-pulse\\\",\\n            )}\\n            aria-label={\\n              action.shortcut ? `${label} (${action.shortcut})` : label\\n            }\\n          >\\n            {action.isLoading && (\\n              <svg\\n                className=\\\"mr-2 h-4 w-4 motion-safe:animate-spin\\\"\\n                xmlns=\\\"http://www.w3.org/2000/svg\\\"\\n                fill=\\\"none\\\"\\n                viewBox=\\\"0 0 24 24\\\"\\n              >\\n                <circle\\n                  className=\\\"opacity-25\\\"\\n                  cx=\\\"12\\\"\\n                  cy=\\\"12\\\"\\n                  r=\\\"10\\\"\\n                  stroke=\\\"currentColor\\\"\\n                  strokeWidth=\\\"4\\\"\\n                />\\n                <path\\n                  className=\\\"opacity-75\\\"\\n                  fill=\\\"currentColor\\\"\\n                  d=\\\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\\\"\\n                />\\n              </svg>\\n            )}\\n            {action.icon && !action.isLoading && (\\n              <span className=\\\"mr-2\\\">{action.icon}</span>\\n            )}\\n            {label}\\n            {action.shortcut && !action.isLoading && (\\n              <kbd className=\\\"border-border bg-muted ml-2.5 hidden rounded-lg border px-2 py-0.5 font-mono text-xs font-medium sm:inline-block\\\">\\n                {action.shortcut}\\n              </kbd>\\n            )}\\n          </Button>\\n        );\\n      })}\\n    </div>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/actions-config.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/actions-config.ts\",\n      \"content\": \"import type { Action, ActionsConfig } from \\\"./schema\\\";\\n\\nexport type ActionsProp = ActionsConfig | Action[];\\n\\nconst NEGATORY_ACTION_IDS = new Set([\\n  \\\"cancel\\\",\\n  \\\"dismiss\\\",\\n  \\\"skip\\\",\\n  \\\"no\\\",\\n  \\\"reset\\\",\\n  \\\"close\\\",\\n  \\\"decline\\\",\\n  \\\"reject\\\",\\n  \\\"back\\\",\\n  \\\"later\\\",\\n  \\\"not-now\\\",\\n  \\\"maybe-later\\\",\\n]);\\n\\nfunction inferVariant(action: Action): Action {\\n  if (action.variant) return action;\\n  if (NEGATORY_ACTION_IDS.has(action.id)) {\\n    return { ...action, variant: \\\"ghost\\\" };\\n  }\\n  return action;\\n}\\n\\nexport function normalizeActionsConfig(\\n  actions?: ActionsProp,\\n): ActionsConfig | null {\\n  if (!actions) return null;\\n\\n  const rawItems = Array.isArray(actions) ? actions : (actions.items ?? []);\\n\\n  if (rawItems.length === 0) {\\n    return null;\\n  }\\n\\n  const items = rawItems.map(inferVariant);\\n\\n  return Array.isArray(actions)\\n    ? { items }\\n    : {\\n        items,\\n        align: actions.align,\\n        confirmTimeout: actions.confirmTimeout,\\n      };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/embedded-actions.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/embedded-actions.ts\",\n      \"content\": \"import type { ActionsProp } from \\\"./actions-config\\\";\\n\\nexport type EmbeddedActionHandler<TState> = (\\n  actionId: string,\\n  state: TState,\\n) => void | Promise<void>;\\n\\nexport type EmbeddedBeforeActionHandler<TState> = (\\n  actionId: string,\\n  state: TState,\\n) => boolean | Promise<boolean>;\\n\\nexport interface EmbeddedActionsProps<TState> {\\n  actions?: ActionsProp;\\n  onAction?: EmbeddedActionHandler<TState>;\\n  onBeforeAction?: EmbeddedBeforeActionHandler<TState>;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { ReactNode } from \\\"react\\\";\\n\\n/**\\n * Tool UI conventions:\\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\\n * - Schema: `SerializableXSchema`\\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\\n * - Root attrs: `data-tool-ui-id` + `data-slot`\\n */\\n\\n/**\\n * Schema for tool UI identity.\\n *\\n * Every tool UI should have a unique identifier that:\\n * - Is stable across re-renders\\n * - Is meaningful (not auto-generated)\\n * - Is unique within the conversation\\n *\\n * Format recommendation: `{component-type}-{semantic-identifier}`\\n * Examples: \\\"data-table-expenses-q3\\\", \\\"option-list-deploy-target\\\"\\n */\\nexport const ToolUIIdSchema = z.string().min(1);\\n\\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\\n\\n/**\\n * Primary role of a Tool UI surface in a chat context.\\n */\\nexport const ToolUIRoleSchema = z.enum([\\n  \\\"information\\\",\\n  \\\"decision\\\",\\n  \\\"control\\\",\\n  \\\"state\\\",\\n  \\\"composite\\\",\\n]);\\n\\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\\n\\nexport const ToolUIReceiptOutcomeSchema = z.enum([\\n  \\\"success\\\",\\n  \\\"partial\\\",\\n  \\\"failed\\\",\\n  \\\"cancelled\\\",\\n]);\\n\\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\\n\\n/**\\n * Optional receipt metadata: a durable summary of an outcome.\\n */\\nexport const ToolUIReceiptSchema = z.object({\\n  outcome: ToolUIReceiptOutcomeSchema,\\n  summary: z.string().min(1),\\n  identifiers: z.record(z.string(), z.string()).optional(),\\n  at: z.string().datetime(),\\n});\\n\\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\\n\\n/**\\n * Base schema for Tool UI payloads (id + optional role/receipt).\\n */\\nexport const ToolUISurfaceSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n});\\n\\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\\n\\nexport const ActionSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  /**\\n   * Canonical narration the assistant can use after this action is taken.\\n   *\\n   * Example: \\\"I exported the table as CSV.\\\" / \\\"I opened the link in a new tab.\\\"\\n   */\\n  sentence: z.string().optional(),\\n  confirmLabel: z.string().optional(),\\n  variant: z\\n    .enum([\\\"default\\\", \\\"destructive\\\", \\\"secondary\\\", \\\"ghost\\\", \\\"outline\\\"])\\n    .optional(),\\n  icon: z.custom<ReactNode>().optional(),\\n  loading: z.boolean().optional(),\\n  disabled: z.boolean().optional(),\\n  shortcut: z.string().optional(),\\n});\\n\\nexport type Action = z.infer<typeof ActionSchema>;\\nexport type LocalAction = Action;\\nexport type DecisionAction = Action;\\n\\nexport const DecisionResultSchema = z.object({\\n  kind: z.literal(\\\"decision\\\"),\\n  version: z.literal(1),\\n  decisionId: z.string().min(1),\\n  actionId: z.string().min(1),\\n  actionLabel: z.string().min(1),\\n  at: z.string().datetime(),\\n  payload: z.record(z.string(), z.unknown()).optional(),\\n});\\n\\nexport type DecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n> = Omit<z.infer<typeof DecisionResultSchema>, \\\"payload\\\"> & {\\n  payload?: TPayload;\\n};\\n\\nexport function createDecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n>(args: {\\n  decisionId: string;\\n  action: { id: string; label: string };\\n  payload?: TPayload;\\n}): DecisionResult<TPayload> {\\n  return {\\n    kind: \\\"decision\\\",\\n    version: 1,\\n    decisionId: args.decisionId,\\n    actionId: args.action.id,\\n    actionLabel: args.action.label,\\n    at: new Date().toISOString(),\\n    payload: args.payload,\\n  };\\n}\\n\\nexport const ActionButtonsPropsSchema = z.object({\\n  actions: z.array(ActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n  className: z.string().optional(),\\n});\\n\\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\\n  actions: z.array(SerializableActionSchema),\\n}).omit({ className: true });\\n\\nexport interface ActionsConfig {\\n  items: Action[];\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  confirmTimeout?: number;\\n}\\n\\nexport const SerializableActionsConfigSchema = z.object({\\n  items: z.array(SerializableActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n});\\n\\nexport type SerializableActionsConfig = z.infer<\\n  typeof SerializableActionsConfigSchema\\n>;\\n\\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/use-action-buttons.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/shared/use-action-buttons.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { useCallback, useEffect, useMemo, useRef, useState } from \\\"react\\\";\\nimport type { Action } from \\\"./schema\\\";\\n\\nexport type UseActionButtonsOptions = {\\n  actions: Action[];\\n  onAction: (actionId: string) => void | Promise<void>;\\n  onBeforeAction?: (actionId: string) => boolean | Promise<boolean>;\\n  confirmTimeout?: number;\\n};\\n\\nexport type UseActionButtonsResult = {\\n  actions: Array<\\n    Action & {\\n      currentLabel: string;\\n      isConfirming: boolean;\\n      isExecuting: boolean;\\n      isDisabled: boolean;\\n      isLoading: boolean;\\n    }\\n  >;\\n  runAction: (actionId: string) => Promise<void>;\\n  confirmingActionId: string | null;\\n  executingActionId: string | null;\\n};\\n\\ntype ActionExecutionLock = {\\n  tryAcquire: () => boolean;\\n  release: () => void;\\n};\\n\\nexport function createActionExecutionLock(): ActionExecutionLock {\\n  let locked = false;\\n\\n  return {\\n    tryAcquire: () => {\\n      if (locked) return false;\\n      locked = true;\\n      return true;\\n    },\\n    release: () => {\\n      locked = false;\\n    },\\n  };\\n}\\n\\nexport function useActionButtons(\\n  options: UseActionButtonsOptions,\\n): UseActionButtonsResult {\\n  const { actions, onAction, onBeforeAction, confirmTimeout = 3000 } = options;\\n\\n  const [confirmingActionId, setConfirmingActionId] = useState<string | null>(\\n    null,\\n  );\\n  const [executingActionId, setExecutingActionId] = useState<string | null>(\\n    null,\\n  );\\n  const executionLockRef = useRef<ActionExecutionLock>(\\n    createActionExecutionLock(),\\n  );\\n\\n  useEffect(() => {\\n    if (!confirmingActionId) return;\\n    const id = setTimeout(() => setConfirmingActionId(null), confirmTimeout);\\n    return () => clearTimeout(id);\\n  }, [confirmingActionId, confirmTimeout]);\\n\\n  useEffect(() => {\\n    if (!confirmingActionId) return;\\n    const handleKeyDown = (e: KeyboardEvent) => {\\n      if (e.key === \\\"Escape\\\") {\\n        setConfirmingActionId(null);\\n      }\\n    };\\n\\n    window.addEventListener(\\\"keydown\\\", handleKeyDown);\\n    return () => window.removeEventListener(\\\"keydown\\\", handleKeyDown);\\n  }, [confirmingActionId]);\\n\\n  const runAction = useCallback(\\n    async (actionId: string) => {\\n      const action = actions.find((a) => a.id === actionId);\\n      if (!action) return;\\n\\n      const isAnyActionExecuting = executingActionId !== null;\\n      if (action.disabled || action.loading || isAnyActionExecuting) {\\n        return;\\n      }\\n\\n      if (action.confirmLabel && confirmingActionId !== action.id) {\\n        setConfirmingActionId(action.id);\\n        return;\\n      }\\n\\n      if (!executionLockRef.current.tryAcquire()) {\\n        return;\\n      }\\n\\n      if (onBeforeAction) {\\n        const shouldProceed = await onBeforeAction(action.id);\\n        if (!shouldProceed) {\\n          setConfirmingActionId(null);\\n          executionLockRef.current.release();\\n          return;\\n        }\\n      }\\n\\n      try {\\n        setExecutingActionId(action.id);\\n        await onAction(action.id);\\n      } finally {\\n        executionLockRef.current.release();\\n        setExecutingActionId(null);\\n        setConfirmingActionId(null);\\n      }\\n    },\\n    [actions, confirmingActionId, executingActionId, onAction, onBeforeAction],\\n  );\\n\\n  const resolvedActions = useMemo(\\n    () =>\\n      actions.map((action) => {\\n        const isConfirming = confirmingActionId === action.id;\\n        const isThisActionExecuting = executingActionId === action.id;\\n        const isLoading = action.loading || isThisActionExecuting;\\n        const isDisabled =\\n          action.disabled ||\\n          (executingActionId !== null && !isThisActionExecuting);\\n        const currentLabel =\\n          isConfirming && action.confirmLabel\\n            ? action.confirmLabel\\n            : action.label;\\n\\n        return {\\n          ...action,\\n          currentLabel,\\n          isConfirming,\\n          isExecuting: isThisActionExecuting,\\n          isDisabled,\\n          isLoading,\\n        };\\n      }),\\n    [actions, confirmingActionId, executingActionId],\\n  );\\n\\n  return {\\n    actions: resolvedActions,\\n    runAction,\\n    confirmingActionId,\\n    executingActionId,\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/use-controllable-state.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/use-controllable-state.ts\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { useCallback, useMemo, useRef, useState } from \\\"react\\\";\\n\\nexport type UseControllableStateOptions<T> = {\\n  value?: T;\\n  defaultValue: T;\\n  onChange?: (next: T) => void;\\n};\\n\\nexport function useControllableState<T>({\\n  value,\\n  defaultValue,\\n  onChange,\\n}: UseControllableStateOptions<T>) {\\n  const [uncontrolled, setUncontrolled] = useState<T>(defaultValue);\\n  const isControlled = value !== undefined;\\n\\n  const currentValue = useMemo(\\n    () => (isControlled ? (value as T) : uncontrolled),\\n    [isControlled, value, uncontrolled],\\n  );\\n  const currentValueRef = useRef(currentValue);\\n  currentValueRef.current = currentValue;\\n\\n  const setValue = useCallback(\\n    (next: T | ((prev: T) => T)) => {\\n      const resolved =\\n        typeof next === \\\"function\\\"\\n          ? (next as (prev: T) => T)(currentValueRef.current)\\n          : next;\\n\\n      currentValueRef.current = resolved;\\n      if (!isControlled) {\\n        setUncontrolled(resolved);\\n      }\\n\\n      onChange?.(resolved);\\n      return resolved;\\n    },\\n    [isControlled, onChange],\\n  );\\n\\n  const setUncontrolledValue = useCallback((next: T) => {\\n    setUncontrolled(next);\\n  }, []);\\n\\n  return {\\n    value: currentValue,\\n    isControlled,\\n    setValue,\\n    setUncontrolledValue,\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/use-signature-reset.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/use-signature-reset.ts\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { useEffect, useRef } from \\\"react\\\";\\n\\nexport function useSignatureReset(\\n  signature: string,\\n  onSignatureChange: () => void,\\n) {\\n  const previousSignature = useRef(signature);\\n\\n  useEffect(() => {\\n    if (previousSignature.current === signature) return;\\n    previousSignature.current = signature;\\n    onSignatureChange();\\n  }, [signature, onSignatureChange]);\\n}\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/progress-tracker.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"progress-tracker\",\n  \"type\": \"registry:block\",\n  \"title\": \"Progress Tracker\",\n  \"description\": \"Show real-time status feedback for multi-step operations in AI interfaces.\",\n  \"dependencies\": [\n    \"lucide-react\",\n    \"zod\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/progress-tracker/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/progress-tracker/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn        → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n */\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/progress-tracker/index.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/progress-tracker/index.tsx\",\n      \"content\": \"export { ProgressTracker } from \\\"./progress-tracker\\\";\\nexport {\\n  type SerializableProgressTracker,\\n  type ProgressTrackerProps,\\n  type ProgressTrackerChoice,\\n  type ProgressStep,\\n} from \\\"./schema\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/progress-tracker/progress-tracker.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/progress-tracker/progress-tracker.tsx\",\n      \"content\": \"import { cn } from \\\"./_adapter\\\";\\nimport type {\\n  ProgressStep,\\n  ProgressTrackerChoice,\\n  ProgressTrackerProps,\\n} from \\\"./schema\\\";\\nimport { Check, X, Loader2, Timer, AlertCircle } from \\\"lucide-react\\\";\\nimport type { LucideIcon } from \\\"lucide-react\\\";\\n\\nfunction formatElapsedTime(milliseconds: number): string {\\n  const roundedSeconds = Math.round(Math.max(0, milliseconds) / 100) / 10;\\n\\n  if (roundedSeconds < 60) {\\n    return `${roundedSeconds.toFixed(1)}s`;\\n  }\\n\\n  const wholeSeconds = Math.floor(roundedSeconds);\\n  const minutes = Math.floor(wholeSeconds / 60);\\n  const remainingSeconds = wholeSeconds % 60;\\n  return `${minutes}m ${remainingSeconds}s`;\\n}\\n\\nfunction formatElapsedTimeDateTime(milliseconds: number): string {\\n  const roundedSeconds = Math.round(Math.max(0, milliseconds) / 100) / 10;\\n\\n  if (roundedSeconds < 60) {\\n    return `PT${Number(roundedSeconds.toFixed(1))}S`;\\n  }\\n\\n  const wholeSeconds = Math.floor(roundedSeconds);\\n  const hours = Math.floor(wholeSeconds / 3600);\\n  const minutes = Math.floor((wholeSeconds % 3600) / 60);\\n  const seconds = wholeSeconds % 60;\\n\\n  const hourPart = hours > 0 ? `${hours}H` : \\\"\\\";\\n  const minutePart = minutes > 0 ? `${minutes}M` : \\\"\\\";\\n  const secondPart = seconds > 0 ? `${seconds}S` : \\\"\\\";\\n\\n  if (!hourPart && !minutePart && !secondPart) {\\n    return \\\"PT0S\\\";\\n  }\\n\\n  return `PT${hourPart}${minutePart}${secondPart}`;\\n}\\n\\nfunction getCurrentStepId(steps: ProgressStep[]): string | null {\\n  const inProgressStep = steps.find((s) => s.status === \\\"in-progress\\\");\\n  if (inProgressStep) return inProgressStep.id;\\n\\n  const failedStep = steps.find((s) => s.status === \\\"failed\\\");\\n  if (failedStep) return failedStep.id;\\n\\n  const firstPendingStep = steps.find((s) => s.status === \\\"pending\\\");\\n  if (firstPendingStep) return firstPendingStep.id;\\n\\n  return null;\\n}\\n\\nfunction getReceiptState(outcome: ProgressTrackerChoice[\\\"outcome\\\"]): {\\n  toneClassName: string;\\n  icon: LucideIcon;\\n} {\\n  switch (outcome) {\\n    case \\\"success\\\":\\n      return {\\n        toneClassName: \\\"text-emerald-600 dark:text-emerald-500\\\",\\n        icon: Check,\\n      };\\n    case \\\"partial\\\":\\n      return {\\n        toneClassName: \\\"text-amber-600 dark:text-amber-500\\\",\\n        icon: AlertCircle,\\n      };\\n    case \\\"failed\\\":\\n      return {\\n        toneClassName: \\\"text-destructive\\\",\\n        icon: AlertCircle,\\n      };\\n    case \\\"cancelled\\\":\\n      return {\\n        toneClassName: \\\"text-muted-foreground\\\",\\n        icon: X,\\n      };\\n  }\\n}\\n\\ninterface StepIndicatorProps {\\n  status: \\\"pending\\\" | \\\"in-progress\\\" | \\\"completed\\\" | \\\"failed\\\";\\n}\\n\\nfunction StepIndicator({ status }: StepIndicatorProps) {\\n  if (status === \\\"pending\\\") {\\n    return (\\n      <span\\n        className=\\\"bg-card border-border flex size-6 shrink-0 items-center justify-center rounded-full border motion-safe:transition-all motion-safe:duration-200\\\"\\n        aria-hidden=\\\"true\\\"\\n      />\\n    );\\n  }\\n\\n  if (status === \\\"in-progress\\\") {\\n    return (\\n      <span\\n        className=\\\"bg-card border-border flex size-6 shrink-0 items-center justify-center rounded-full border shadow-[0_0_0_4px_hsl(var(--primary)/0.1)] motion-safe:transition-all motion-safe:duration-300\\\"\\n        aria-hidden=\\\"true\\\"\\n      >\\n        <Loader2 className=\\\"text-primary size-5 motion-safe:animate-spin\\\" />\\n      </span>\\n    );\\n  }\\n\\n  if (status === \\\"completed\\\") {\\n    return (\\n      <span\\n        className=\\\"bg-primary text-primary-foreground border-primary flex size-6 shrink-0 items-center justify-center rounded-full border shadow-sm motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:duration-300 motion-safe:ease-out\\\"\\n        aria-hidden=\\\"true\\\"\\n      >\\n        <Check\\n          className=\\\"size-4 motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:delay-75 motion-safe:duration-200 motion-safe:fill-mode-both\\\"\\n          strokeWidth={3}\\n        />\\n      </span>\\n    );\\n  }\\n\\n  if (status === \\\"failed\\\") {\\n    return (\\n      <span\\n        className=\\\"bg-destructive border-destructive flex size-6 shrink-0 items-center justify-center rounded-full border text-white shadow-sm motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:duration-300 motion-safe:ease-out dark:border-red-600 dark:bg-red-600\\\"\\n        aria-hidden=\\\"true\\\"\\n      >\\n        <X\\n          className=\\\"size-4 motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:delay-75 motion-safe:duration-200 motion-safe:fill-mode-both\\\"\\n          strokeWidth={3}\\n        />\\n      </span>\\n    );\\n  }\\n\\n  return null;\\n}\\n\\nfunction ElapsedTimeBadge({ elapsedTime }: { elapsedTime?: number }) {\\n  if (elapsedTime === undefined || elapsedTime <= 0) {\\n    return null;\\n  }\\n\\n  return (\\n    <div className=\\\"text-muted-foreground flex items-center gap-1.5 font-mono text-xs\\\">\\n      <Timer className=\\\"-mt-px size-3.5\\\" />\\n      <time dateTime={formatElapsedTimeDateTime(elapsedTime)}>\\n        {formatElapsedTime(elapsedTime)}\\n      </time>\\n    </div>\\n  );\\n}\\n\\ninterface ProgressTrackerBaseProps {\\n  id: ProgressTrackerProps[\\\"id\\\"];\\n  steps: ProgressTrackerProps[\\\"steps\\\"];\\n  elapsedTime?: ProgressTrackerProps[\\\"elapsedTime\\\"];\\n  className?: ProgressTrackerProps[\\\"className\\\"];\\n}\\n\\nfunction ProgressTrackerReceipt({\\n  id,\\n  steps,\\n  elapsedTime,\\n  className,\\n  choice,\\n}: ProgressTrackerBaseProps & { choice: ProgressTrackerChoice }) {\\n  const receiptState = getReceiptState(choice.outcome);\\n  const ReceiptIcon = receiptState.icon;\\n\\n  return (\\n    <div\\n      className={cn(\\n        \\\"isolate flex w-full max-w-md min-w-80 flex-col\\\",\\n        \\\"text-foreground select-none\\\",\\n        \\\"motion-safe:animate-in motion-safe:fade-in motion-safe:blur-in-sm motion-safe:zoom-in-95 motion-safe:duration-300 motion-safe:ease-[cubic-bezier(0.16,1,0.3,1)] motion-safe:fill-mode-both\\\",\\n        className,\\n      )}\\n      data-slot=\\\"progress-tracker\\\"\\n      data-tool-ui-id={id}\\n      data-receipt=\\\"true\\\"\\n      role=\\\"status\\\"\\n      aria-label={choice.summary}\\n    >\\n      <div className=\\\"bg-card/60 flex w-full flex-col gap-4 rounded-2xl border p-5 shadow-xs\\\">\\n        <div className=\\\"flex items-center justify-between\\\">\\n          <ElapsedTimeBadge elapsedTime={elapsedTime} />\\n          <span\\n            className={cn(\\n              \\\"flex items-center gap-1.5 text-xs font-medium\\\",\\n              receiptState.toneClassName,\\n            )}\\n          >\\n            <ReceiptIcon className=\\\"size-3.5\\\" />\\n            {choice.summary}\\n          </span>\\n        </div>\\n\\n        <ol className=\\\"m-0 flex list-none flex-col gap-2 p-0\\\">\\n          {steps.map((step, index) => (\\n            <li\\n              key={step.id}\\n              className=\\\"relative -mx-2 flex items-start gap-3 rounded-lg px-2 py-1.5\\\"\\n            >\\n              {index < steps.length - 1 && (\\n                <div\\n                  className=\\\"bg-border absolute top-8 left-5 w-px\\\"\\n                  style={{\\n                    height: \\\"calc(100% + 0.5rem)\\\",\\n                  }}\\n                  aria-hidden=\\\"true\\\"\\n                />\\n              )}\\n              <div className=\\\"relative z-10\\\">\\n                <StepIndicator status={step.status} />\\n              </div>\\n              <div className=\\\"flex flex-1 flex-col gap-0.5\\\">\\n                <span className=\\\"text-sm leading-6 font-medium\\\">\\n                  {step.label}\\n                </span>\\n                {step.description && (\\n                  <span className=\\\"text-muted-foreground text-sm\\\">\\n                    {step.description}\\n                  </span>\\n                )}\\n              </div>\\n            </li>\\n          ))}\\n        </ol>\\n      </div>\\n    </div>\\n  );\\n}\\n\\nfunction ProgressTrackerLive({\\n  id,\\n  steps,\\n  elapsedTime,\\n  className,\\n}: ProgressTrackerBaseProps) {\\n  const hasInProgress = steps.some((step) => step.status === \\\"in-progress\\\");\\n  const currentStepId = getCurrentStepId(steps);\\n\\n  return (\\n    <article\\n      className={cn(\\n        \\\"isolate flex w-full max-w-md min-w-80 flex-col gap-3\\\",\\n        \\\"text-foreground select-none\\\",\\n        className,\\n      )}\\n      data-slot=\\\"progress-tracker\\\"\\n      data-tool-ui-id={id}\\n      role=\\\"status\\\"\\n      aria-live=\\\"polite\\\"\\n      aria-busy={hasInProgress}\\n    >\\n      <div className=\\\"bg-card flex w-full flex-col gap-4 rounded-2xl border p-5 shadow-xs\\\">\\n        <ElapsedTimeBadge elapsedTime={elapsedTime} />\\n\\n        <ol className=\\\"m-0 flex list-none flex-col gap-3 p-0\\\">\\n          {steps.map((step, index) => {\\n            const isCurrent = step.id === currentStepId;\\n            const isActive = step.status === \\\"in-progress\\\";\\n            const isFailed = step.status === \\\"failed\\\";\\n            const hasDescription = !!step.description;\\n            const shouldShowDescription = isActive || isFailed;\\n\\n            return (\\n              <li\\n                key={step.id}\\n                className=\\\"relative -mx-2\\\"\\n                aria-current={isCurrent ? \\\"step\\\" : undefined}\\n              >\\n                {index < steps.length - 1 && (\\n                  <div\\n                    className={cn(\\n                      \\\"bg-border absolute top-6 left-5 w-px\\\",\\n                      \\\"motion-safe:transition-all motion-safe:duration-300\\\",\\n                    )}\\n                    style={{\\n                      height: \\\"calc(100% + 0.25rem)\\\",\\n                    }}\\n                    aria-hidden=\\\"true\\\"\\n                  />\\n                )}\\n                <div\\n                  className={cn(\\n                    \\\"relative z-10 flex items-start gap-3 rounded-lg px-2 py-1.5\\\",\\n                    \\\"motion-safe:transition-all motion-safe:duration-300\\\",\\n                    isCurrent && \\\"bg-primary/5\\\",\\n                  )}\\n                  style={{\\n                    backdropFilter: isCurrent ? \\\"blur(2px)\\\" : undefined,\\n                  }}\\n                >\\n                  <div className=\\\"relative z-10\\\">\\n                    <StepIndicator status={step.status} />\\n                  </div>\\n                  <div className=\\\"flex flex-1 flex-col\\\">\\n                    <span\\n                      className={cn(\\n                        \\\"text-sm leading-6 font-medium\\\",\\n                        step.status === \\\"pending\\\" && \\\"text-muted-foreground\\\",\\n                        step.status === \\\"in-progress\\\" &&\\n                          \\\"motion-safe:shimmer shimmer-invert text-foreground\\\",\\n                      )}\\n                    >\\n                      {step.label}\\n                    </span>\\n                    {hasDescription && (\\n                      <div\\n                        className={cn(\\n                          \\\"grid motion-safe:transition-[grid-template-rows,opacity] motion-safe:duration-300 motion-safe:ease-out\\\",\\n                          shouldShowDescription\\n                            ? \\\"grid-rows-[1fr] opacity-100\\\"\\n                            : \\\"grid-rows-[0fr] opacity-0\\\",\\n                        )}\\n                        aria-hidden={!shouldShowDescription}\\n                      >\\n                        <div className=\\\"overflow-hidden\\\">\\n                          <span className=\\\"text-muted-foreground block pt-0.5 text-sm\\\">\\n                            {step.description}\\n                          </span>\\n                        </div>\\n                      </div>\\n                    )}\\n                  </div>\\n                </div>\\n              </li>\\n            );\\n          })}\\n        </ol>\\n      </div>\\n    </article>\\n  );\\n}\\n\\nfunction ProgressTrackerRoot({\\n  id,\\n  steps,\\n  elapsedTime,\\n  className,\\n  choice,\\n}: ProgressTrackerProps) {\\n  const viewKey = choice ? `receipt-${choice.outcome}` : \\\"interactive\\\";\\n\\n  return (\\n    <div key={viewKey} className=\\\"contents\\\">\\n      {choice ? (\\n        <ProgressTrackerReceipt\\n          id={id}\\n          steps={steps}\\n          elapsedTime={elapsedTime}\\n          className={className}\\n          choice={choice}\\n        />\\n      ) : (\\n        <ProgressTrackerLive\\n          id={id}\\n          steps={steps}\\n          elapsedTime={elapsedTime}\\n          className={className}\\n        />\\n      )}\\n    </div>\\n  );\\n}\\n\\ntype ProgressTrackerComponent = typeof ProgressTrackerRoot & {\\n  Live: typeof ProgressTrackerLive;\\n  Receipt: typeof ProgressTrackerReceipt;\\n};\\n\\nexport const ProgressTracker = Object.assign(ProgressTrackerRoot, {\\n  Live: ProgressTrackerLive,\\n  Receipt: ProgressTrackerReceipt,\\n}) as ProgressTrackerComponent;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/progress-tracker/README.md\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/progress-tracker/README.md\",\n      \"content\": \"# Progress Tracker\\n\\nImplementation for the \\\"progress-tracker\\\" Tool UI surface.\\n\\n## Files\\n\\n- public exports: components/tool-ui/progress-tracker/index.tsx\\n- serializable schema + parse helpers: components/tool-ui/progress-tracker/schema.ts\\n\\n## Companion assets\\n\\n- Docs page: app/docs/progress-tracker/content.mdx\\n- Preset payload: lib/presets/progress-tracker.ts\\n\\n## Quick check\\n\\nRun this after edits:\\n\\npnpm test\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/progress-tracker/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/progress-tracker/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport {\\n  ToolUISurfaceSchema,\\n  ToolUIReceiptSchema,\\n  type ToolUIReceipt,\\n} from \\\"../shared/schema\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\n\\n/**\\n * Receipt state for ProgressTracker showing the outcome of a workflow.\\n */\\nexport type ProgressTrackerChoice = ToolUIReceipt;\\n\\nexport const ProgressStepSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  description: z.string().optional(),\\n  status: z.enum([\\\"pending\\\", \\\"in-progress\\\", \\\"completed\\\", \\\"failed\\\"]),\\n});\\n\\nexport type ProgressStep = z.infer<typeof ProgressStepSchema>;\\n\\nconst ProgressStepsSchema = z\\n  .array(ProgressStepSchema)\\n  .min(1)\\n  .superRefine((steps, ctx) => {\\n    const seenIds = new Set<string>();\\n\\n    for (const [index, step] of steps.entries()) {\\n      if (seenIds.has(step.id)) {\\n        ctx.addIssue({\\n          code: z.ZodIssueCode.custom,\\n          message: `Duplicate step id: \\\"${step.id}\\\"`,\\n          path: [index, \\\"id\\\"],\\n        });\\n      }\\n\\n      seenIds.add(step.id);\\n    }\\n  });\\n\\nexport const SerializableProgressTrackerSchema = ToolUISurfaceSchema.omit({\\n  receipt: true,\\n})\\n  .extend({\\n    steps: ProgressStepsSchema,\\n    elapsedTime: z.number().finite().nonnegative().optional(),\\n    /**\\n     * When set, renders the component in receipt state showing the workflow outcome.\\n     */\\n    choice: ToolUIReceiptSchema.optional(),\\n  })\\n  .strict();\\n\\nexport type SerializableProgressTracker = z.infer<\\n  typeof SerializableProgressTrackerSchema\\n>;\\n\\nconst SerializableProgressTrackerSchemaContract = defineToolUiContract(\\n  \\\"ProgressTracker\\\",\\n  SerializableProgressTrackerSchema,\\n);\\n\\nexport const parseSerializableProgressTracker: (\\n  input: unknown,\\n) => SerializableProgressTracker =\\n  SerializableProgressTrackerSchemaContract.parse;\\n\\nexport const safeParseSerializableProgressTracker: (\\n  input: unknown,\\n) => SerializableProgressTracker | null =\\n  SerializableProgressTrackerSchemaContract.safeParse;\\n\\nexport interface ProgressTrackerProps extends SerializableProgressTracker {\\n  className?: string;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { ReactNode } from \\\"react\\\";\\n\\n/**\\n * Tool UI conventions:\\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\\n * - Schema: `SerializableXSchema`\\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\\n * - Root attrs: `data-tool-ui-id` + `data-slot`\\n */\\n\\n/**\\n * Schema for tool UI identity.\\n *\\n * Every tool UI should have a unique identifier that:\\n * - Is stable across re-renders\\n * - Is meaningful (not auto-generated)\\n * - Is unique within the conversation\\n *\\n * Format recommendation: `{component-type}-{semantic-identifier}`\\n * Examples: \\\"data-table-expenses-q3\\\", \\\"option-list-deploy-target\\\"\\n */\\nexport const ToolUIIdSchema = z.string().min(1);\\n\\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\\n\\n/**\\n * Primary role of a Tool UI surface in a chat context.\\n */\\nexport const ToolUIRoleSchema = z.enum([\\n  \\\"information\\\",\\n  \\\"decision\\\",\\n  \\\"control\\\",\\n  \\\"state\\\",\\n  \\\"composite\\\",\\n]);\\n\\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\\n\\nexport const ToolUIReceiptOutcomeSchema = z.enum([\\n  \\\"success\\\",\\n  \\\"partial\\\",\\n  \\\"failed\\\",\\n  \\\"cancelled\\\",\\n]);\\n\\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\\n\\n/**\\n * Optional receipt metadata: a durable summary of an outcome.\\n */\\nexport const ToolUIReceiptSchema = z.object({\\n  outcome: ToolUIReceiptOutcomeSchema,\\n  summary: z.string().min(1),\\n  identifiers: z.record(z.string(), z.string()).optional(),\\n  at: z.string().datetime(),\\n});\\n\\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\\n\\n/**\\n * Base schema for Tool UI payloads (id + optional role/receipt).\\n */\\nexport const ToolUISurfaceSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n});\\n\\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\\n\\nexport const ActionSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  /**\\n   * Canonical narration the assistant can use after this action is taken.\\n   *\\n   * Example: \\\"I exported the table as CSV.\\\" / \\\"I opened the link in a new tab.\\\"\\n   */\\n  sentence: z.string().optional(),\\n  confirmLabel: z.string().optional(),\\n  variant: z\\n    .enum([\\\"default\\\", \\\"destructive\\\", \\\"secondary\\\", \\\"ghost\\\", \\\"outline\\\"])\\n    .optional(),\\n  icon: z.custom<ReactNode>().optional(),\\n  loading: z.boolean().optional(),\\n  disabled: z.boolean().optional(),\\n  shortcut: z.string().optional(),\\n});\\n\\nexport type Action = z.infer<typeof ActionSchema>;\\nexport type LocalAction = Action;\\nexport type DecisionAction = Action;\\n\\nexport const DecisionResultSchema = z.object({\\n  kind: z.literal(\\\"decision\\\"),\\n  version: z.literal(1),\\n  decisionId: z.string().min(1),\\n  actionId: z.string().min(1),\\n  actionLabel: z.string().min(1),\\n  at: z.string().datetime(),\\n  payload: z.record(z.string(), z.unknown()).optional(),\\n});\\n\\nexport type DecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n> = Omit<z.infer<typeof DecisionResultSchema>, \\\"payload\\\"> & {\\n  payload?: TPayload;\\n};\\n\\nexport function createDecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n>(args: {\\n  decisionId: string;\\n  action: { id: string; label: string };\\n  payload?: TPayload;\\n}): DecisionResult<TPayload> {\\n  return {\\n    kind: \\\"decision\\\",\\n    version: 1,\\n    decisionId: args.decisionId,\\n    actionId: args.action.id,\\n    actionLabel: args.action.label,\\n    at: new Date().toISOString(),\\n    payload: args.payload,\\n  };\\n}\\n\\nexport const ActionButtonsPropsSchema = z.object({\\n  actions: z.array(ActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n  className: z.string().optional(),\\n});\\n\\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\\n  actions: z.array(SerializableActionSchema),\\n}).omit({ className: true });\\n\\nexport interface ActionsConfig {\\n  items: Action[];\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  confirmTimeout?: number;\\n}\\n\\nexport const SerializableActionsConfigSchema = z.object({\\n  items: z.array(SerializableActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n});\\n\\nexport type SerializableActionsConfig = z.infer<\\n  typeof SerializableActionsConfigSchema\\n>;\\n\\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/question-flow.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"question-flow\",\n  \"type\": \"registry:block\",\n  \"title\": \"Question Flow\",\n  \"description\": \"Multi-step guided questions with branching.\",\n  \"dependencies\": [\n    \"lucide-react\",\n    \"zod\"\n  ],\n  \"registryDependencies\": [\n    \"button\",\n    \"separator\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/question-flow/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/question-flow/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn        → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n *   Button    → shadcn/ui Button\\n *   Separator → shadcn/ui Separator\\n */\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport { Button } from \\\"@/components/ui/button\\\";\\nexport { Separator } from \\\"@/components/ui/separator\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/question-flow/index.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/question-flow/index.tsx\",\n      \"content\": \"export { QuestionFlow } from \\\"./question-flow\\\";\\nexport {\\n  type SerializableQuestionFlow,\\n  type SerializableProgressiveMode,\\n  type SerializableUpfrontMode,\\n  type SerializableReceiptMode,\\n  type QuestionFlowProps,\\n  type QuestionFlowProgressiveProps,\\n  type QuestionFlowUpfrontProps,\\n  type QuestionFlowReceiptProps,\\n  type QuestionFlowOption,\\n  type QuestionFlowStepDefinition,\\n  type QuestionFlowChoice,\\n  type QuestionFlowSummaryItem,\\n} from \\\"./schema\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/question-flow/question-flow.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/question-flow/question-flow.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport {\\n  useMemo,\\n  useState,\\n  useCallback,\\n  useRef,\\n  useEffect,\\n  Fragment,\\n} from \\\"react\\\";\\nimport type { KeyboardEvent } from \\\"react\\\";\\nimport type {\\n  QuestionFlowProps,\\n  QuestionFlowProgressiveProps,\\n  QuestionFlowUpfrontProps,\\n  QuestionFlowReceiptProps,\\n  QuestionFlowOption,\\n} from \\\"./schema\\\";\\nimport { cn, Button, Separator } from \\\"./_adapter\\\";\\nimport { Check, ChevronLeft } from \\\"lucide-react\\\";\\n\\ninterface SelectionIndicatorProps {\\n  mode: \\\"single\\\" | \\\"multi\\\";\\n  isSelected: boolean;\\n  disabled?: boolean;\\n}\\n\\ninterface ProgressBarProps {\\n  current: number;\\n  total: number;\\n}\\n\\nfunction ProgressBar({ current, total }: ProgressBarProps) {\\n  return (\\n    <div\\n      className=\\\"flex h-1.5 gap-1\\\"\\n      role=\\\"progressbar\\\"\\n      aria-valuenow={current}\\n      aria-valuemin={1}\\n      aria-valuemax={total}\\n    >\\n      {Array.from({ length: total }).map((_, i) => (\\n        <div\\n          key={i}\\n          className=\\\"relative flex-1 overflow-hidden rounded-full bg-muted\\\"\\n        >\\n          <div\\n            className={cn(\\n              \\\"absolute inset-0 origin-left rounded-full bg-primary\\\",\\n              \\\"motion-safe:transition-transform motion-safe:duration-300 motion-safe:ease-[var(--cubic-ease-in-out)]\\\",\\n              i < current ? \\\"scale-x-100\\\" : \\\"scale-x-0\\\",\\n            )}\\n          />\\n        </div>\\n      ))}\\n    </div>\\n  );\\n}\\n\\nfunction SelectionIndicator({\\n  mode,\\n  isSelected,\\n  disabled,\\n}: SelectionIndicatorProps) {\\n  const shape = mode === \\\"single\\\" ? \\\"rounded-full\\\" : \\\"rounded\\\";\\n\\n  return (\\n    <div\\n      className={cn(\\n        \\\"flex size-4 shrink-0 items-center justify-center border-2\\\",\\n        \\\"motion-safe:transition-colors motion-safe:duration-200\\\",\\n        shape,\\n        isSelected && [\\n          \\\"border-primary bg-primary text-primary-foreground\\\",\\n          \\\"motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:duration-300 motion-safe:ease-out\\\",\\n        ],\\n        !isSelected && \\\"border-muted-foreground/50\\\",\\n        disabled && \\\"opacity-50\\\",\\n      )}\\n    >\\n      {mode === \\\"multi\\\" && isSelected && (\\n        <Check\\n          className=\\\"size-3 motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:delay-75 motion-safe:duration-200 motion-safe:fill-mode-both\\\"\\n          strokeWidth={3}\\n        />\\n      )}\\n      {mode === \\\"single\\\" && isSelected && (\\n        <span className=\\\"size-2 rounded-full bg-current motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:duration-300 motion-safe:ease-out\\\" />\\n      )}\\n    </div>\\n  );\\n}\\n\\ninterface OptionItemProps {\\n  option: QuestionFlowOption;\\n  isSelected: boolean;\\n  isDisabled: boolean;\\n  selectionMode: \\\"single\\\" | \\\"multi\\\";\\n  isFirst: boolean;\\n  isLast: boolean;\\n  onToggle: () => void;\\n  tabIndex?: number;\\n  onFocus?: () => void;\\n  buttonRef?: (el: HTMLButtonElement | null) => void;\\n}\\n\\nfunction OptionItem({\\n  option,\\n  isSelected,\\n  isDisabled,\\n  selectionMode,\\n  isFirst,\\n  isLast,\\n  onToggle,\\n  tabIndex,\\n  onFocus,\\n  buttonRef,\\n}: OptionItemProps) {\\n  const hasAdjacentOptions = !isFirst && !isLast;\\n\\n  return (\\n    <Button\\n      ref={buttonRef}\\n      data-id={option.id}\\n      variant=\\\"ghost\\\"\\n      size=\\\"lg\\\"\\n      role=\\\"option\\\"\\n      aria-selected={isSelected}\\n      onClick={onToggle}\\n      onFocus={onFocus}\\n      tabIndex={tabIndex}\\n      disabled={isDisabled}\\n      className={cn(\\n        \\\"peer group relative h-auto min-h-[50px] w-full justify-start text-left text-sm font-medium\\\",\\n        \\\"rounded-none border-0 bg-transparent px-0 py-2 text-base shadow-none transition-none hover:bg-transparent! @md/question-flow:text-sm\\\",\\n        isFirst && \\\"pb-2.5\\\",\\n        hasAdjacentOptions && \\\"py-2.5\\\",\\n      )}\\n    >\\n      <span\\n        className={cn(\\n          \\\"bg-primary/5 absolute inset-0 -mx-3 -my-0.5 rounded-xl opacity-0 transition-opacity group-hover:opacity-100\\\",\\n        )}\\n      />\\n      <div className=\\\"relative flex items-start gap-3\\\">\\n        <span className=\\\"flex h-6 items-center\\\">\\n          <SelectionIndicator\\n            mode={selectionMode}\\n            isSelected={isSelected}\\n            disabled={option.disabled}\\n          />\\n        </span>\\n        {option.icon && (\\n          <span className=\\\"flex h-6 items-center\\\">{option.icon}</span>\\n        )}\\n        <div className=\\\"flex flex-col text-left\\\">\\n          <span className=\\\"leading-6 text-pretty\\\">{option.label}</span>\\n          {option.description && (\\n            <span className=\\\"text-muted-foreground text-sm font-normal text-pretty\\\">\\n              {option.description}\\n            </span>\\n          )}\\n        </div>\\n      </div>\\n    </Button>\\n  );\\n}\\n\\nfunction QuestionFlowReceipt({\\n  id,\\n  choice,\\n  className,\\n}: QuestionFlowReceiptProps) {\\n  return (\\n    <div\\n      className={cn(\\n        \\\"@container/question-flow flex w-full min-w-80 max-w-md flex-col\\\",\\n        \\\"text-foreground\\\",\\n        \\\"motion-safe:animate-in motion-safe:fade-in motion-safe:blur-in-sm motion-safe:zoom-in-95 motion-safe:duration-300 motion-safe:ease-out motion-safe:fill-mode-both\\\",\\n        className,\\n      )}\\n      data-slot=\\\"question-flow\\\"\\n      data-tool-ui-id={id}\\n      data-receipt=\\\"true\\\"\\n      role=\\\"status\\\"\\n      aria-label={choice.title}\\n    >\\n      <div\\n        className={cn(\\n          \\\"bg-card/60 flex w-full flex-col gap-3 rounded-2xl border px-5 py-4 shadow-xs\\\",\\n        )}\\n      >\\n        <div className=\\\"flex items-center justify-between gap-3\\\">\\n          <span className=\\\"text-base font-medium\\\">{choice.title}</span>\\n          <span className=\\\"flex items-center gap-1.5 text-xs font-medium text-emerald-600 dark:text-emerald-500\\\">\\n            <Check className=\\\"size-3.5\\\" />\\n            Complete\\n          </span>\\n        </div>\\n        <div className=\\\"flex flex-col\\\">\\n          {choice.summary.map((item, index) => (\\n            <Fragment key={index}>\\n              {index > 0 && <Separator className=\\\"my-2\\\" />}\\n              <div\\n                className=\\\"flex flex-col gap-0.5 text-sm motion-safe:animate-in motion-safe:fade-in motion-safe:blur-in-sm motion-safe:slide-in-from-bottom-1 motion-safe:duration-300 motion-safe:ease-out motion-safe:fill-mode-both\\\"\\n                style={{ animationDelay: `${150 + index * 75}ms` }}\\n              >\\n                <span className=\\\"text-muted-foreground\\\">{item.label}</span>\\n                <span className=\\\"font-medium\\\">{item.value}</span>\\n              </div>\\n            </Fragment>\\n          ))}\\n        </div>\\n      </div>\\n    </div>\\n  );\\n}\\n\\ninterface StepBodyData {\\n  stepKey: string;\\n  title: string;\\n  description?: string;\\n  options: QuestionFlowOption[];\\n  selectionMode: \\\"single\\\" | \\\"multi\\\";\\n  selectedIds: Set<string>;\\n}\\n\\nexport function getQuestionFlowStepIds(id: string, stepKey: string) {\\n  const safeId = encodeURIComponent(id).replace(/%/g, \\\"_\\\");\\n  const safeStepKey = encodeURIComponent(stepKey).replace(/%/g, \\\"_\\\");\\n  return {\\n    titleId: `${safeId}-${safeStepKey}-title`,\\n    descriptionId: `${safeId}-${safeStepKey}-description`,\\n  };\\n}\\n\\ninterface StepContentProps {\\n  step: number;\\n  totalSteps?: number;\\n  title: string;\\n  description?: string;\\n  options: QuestionFlowOption[];\\n  selectionMode: \\\"single\\\" | \\\"multi\\\";\\n  selectedIds: Set<string>;\\n  onToggle: (optionId: string) => void;\\n  onBack?: () => void;\\n  onNext: () => void;\\n  showBack: boolean;\\n  isLastStep: boolean;\\n  id: string;\\n  className?: string;\\n  stepKey?: string;\\n  exitingStepData?: StepBodyData | null;\\n  transitionDirection?: \\\"forward\\\" | \\\"backward\\\";\\n}\\n\\nfunction StepBodyContent({\\n  stepKey,\\n  title,\\n  description,\\n  options,\\n  selectionMode,\\n  selectedIds,\\n  onToggle,\\n  id,\\n  isExiting,\\n  transitionDirection,\\n}: {\\n  stepKey: string;\\n  title: string;\\n  description?: string;\\n  options: QuestionFlowOption[];\\n  selectionMode: \\\"single\\\" | \\\"multi\\\";\\n  selectedIds: Set<string>;\\n  onToggle?: (optionId: string) => void;\\n  id: string;\\n  isExiting?: boolean;\\n  transitionDirection?: \\\"forward\\\" | \\\"backward\\\";\\n}) {\\n  const optionRefs = useRef<Array<HTMLButtonElement | null>>([]);\\n  const { titleId, descriptionId } = getQuestionFlowStepIds(id, stepKey);\\n\\n  const optionStates = useMemo(() => {\\n    return options.map((option) => {\\n      const isSelected = selectedIds.has(option.id);\\n      const isDisabled = option.disabled ?? false;\\n      return { option, isSelected, isDisabled };\\n    });\\n  }, [options, selectedIds]);\\n\\n  const [activeIndex, setActiveIndex] = useState(() => {\\n    const firstSelected = optionStates.findIndex(\\n      (s) => s.isSelected && !s.isDisabled,\\n    );\\n    if (firstSelected >= 0) return firstSelected;\\n    const firstEnabled = optionStates.findIndex((s) => !s.isDisabled);\\n    return firstEnabled >= 0 ? firstEnabled : 0;\\n  });\\n\\n  const focusOptionAt = useCallback((index: number) => {\\n    const el = optionRefs.current[index];\\n    if (el) el.focus();\\n    setActiveIndex(index);\\n  }, []);\\n\\n  const findNextEnabledIndex = useCallback(\\n    (start: number, direction: 1 | -1) => {\\n      const len = optionStates.length;\\n      if (len === 0) return 0;\\n      for (let s = 1; s <= len; s++) {\\n        const idx = (start + direction * s + len) % len;\\n        if (!optionStates[idx].isDisabled) return idx;\\n      }\\n      return start;\\n    },\\n    [optionStates],\\n  );\\n\\n  const handleKeyDown = useCallback(\\n    (e: KeyboardEvent<HTMLDivElement>) => {\\n      if (optionStates.length === 0 || isExiting) return;\\n\\n      const key = e.key;\\n\\n      if (key === \\\"ArrowDown\\\") {\\n        e.preventDefault();\\n        focusOptionAt(findNextEnabledIndex(activeIndex, 1));\\n        return;\\n      }\\n\\n      if (key === \\\"ArrowUp\\\") {\\n        e.preventDefault();\\n        focusOptionAt(findNextEnabledIndex(activeIndex, -1));\\n        return;\\n      }\\n\\n      if (key === \\\"Home\\\") {\\n        e.preventDefault();\\n        const first = optionStates.findIndex((s) => !s.isDisabled);\\n        focusOptionAt(first >= 0 ? first : 0);\\n        return;\\n      }\\n\\n      if (key === \\\"End\\\") {\\n        e.preventDefault();\\n        for (let i = optionStates.length - 1; i >= 0; i--) {\\n          if (!optionStates[i].isDisabled) {\\n            focusOptionAt(i);\\n            return;\\n          }\\n        }\\n        return;\\n      }\\n\\n      if (key === \\\"Enter\\\" || key === \\\" \\\") {\\n        e.preventDefault();\\n        const current = optionStates[activeIndex];\\n        if (!current || current.isDisabled) return;\\n        onToggle?.(current.option.id);\\n        return;\\n      }\\n    },\\n    [\\n      activeIndex,\\n      findNextEnabledIndex,\\n      focusOptionAt,\\n      isExiting,\\n      onToggle,\\n      optionStates,\\n    ],\\n  );\\n\\n  const isTransitioning = transitionDirection !== undefined;\\n\\n  const enterClass =\\n    transitionDirection === \\\"forward\\\"\\n      ? \\\"motion-safe:slide-in-from-right-4\\\"\\n      : \\\"motion-safe:slide-in-from-left-4\\\";\\n\\n  const exitClass =\\n    transitionDirection === \\\"forward\\\"\\n      ? \\\"motion-safe:slide-out-to-left-4\\\"\\n      : \\\"motion-safe:slide-out-to-right-4\\\";\\n\\n  return (\\n    <div\\n      key={stepKey}\\n      className={cn(\\n        \\\"flex flex-col gap-4\\\",\\n        isExiting && [\\n          \\\"absolute inset-0\\\",\\n          \\\"motion-safe:animate-out motion-safe:fade-out motion-safe:blur-out-sm motion-safe:duration-250 motion-safe:ease-[var(--cubic-ease-in-out)] motion-safe:fill-mode-forwards\\\",\\n          exitClass,\\n        ],\\n        !isExiting &&\\n          isTransitioning && [\\n            \\\"motion-safe:animate-in motion-safe:fade-in motion-safe:blur-in-sm motion-safe:duration-250 motion-safe:ease-[var(--cubic-ease-in-out)] motion-safe:fill-mode-both\\\",\\n            enterClass,\\n          ],\\n      )}\\n      aria-hidden={isExiting}\\n    >\\n      <div className=\\\"flex flex-col gap-1\\\">\\n        <h2 id={titleId} className=\\\"text-lg font-semibold leading-tight\\\">\\n          {title}\\n        </h2>\\n        {description && (\\n          <p id={descriptionId} className=\\\"text-muted-foreground text-sm\\\">\\n            {description}\\n          </p>\\n        )}\\n      </div>\\n\\n      <div\\n        className=\\\"flex flex-col px-1\\\"\\n        role=\\\"listbox\\\"\\n        aria-multiselectable={selectionMode === \\\"multi\\\"}\\n        onKeyDown={isExiting ? undefined : handleKeyDown}\\n      >\\n        {optionStates.map(({ option, isSelected, isDisabled }, index) => (\\n          <Fragment key={option.id}>\\n            {index > 0 && (\\n              <Separator\\n                className=\\\"transition-opacity [@media(hover:hover)]:[&:has(+_:hover)]:opacity-0 [@media(hover:hover)]:[.peer:hover+&]:opacity-0\\\"\\n                orientation=\\\"horizontal\\\"\\n              />\\n            )}\\n            <OptionItem\\n              option={option}\\n              isSelected={isSelected}\\n              isDisabled={isExiting || isDisabled}\\n              selectionMode={selectionMode}\\n              isFirst={index === 0}\\n              isLast={index === optionStates.length - 1}\\n              tabIndex={isExiting ? -1 : index === activeIndex ? 0 : -1}\\n              onFocus={() => !isExiting && setActiveIndex(index)}\\n              buttonRef={(el) => {\\n                optionRefs.current[index] = el;\\n              }}\\n              onToggle={() => !isExiting && onToggle?.(option.id)}\\n            />\\n          </Fragment>\\n        ))}\\n      </div>\\n    </div>\\n  );\\n}\\n\\nfunction StepContent({\\n  step,\\n  totalSteps,\\n  title,\\n  description,\\n  options,\\n  selectionMode,\\n  selectedIds,\\n  onToggle,\\n  onBack,\\n  onNext,\\n  showBack,\\n  isLastStep,\\n  id,\\n  className,\\n  stepKey,\\n  exitingStepData,\\n  transitionDirection = \\\"forward\\\",\\n}: StepContentProps) {\\n  const isTransitioning =\\n    exitingStepData !== null && exitingStepData !== undefined;\\n  const canProceed = selectedIds.size > 0;\\n  const resolvedStepKey = stepKey ?? \\\"current\\\";\\n  const { titleId, descriptionId } = getQuestionFlowStepIds(\\n    id,\\n    resolvedStepKey,\\n  );\\n\\n  const stepLabel = totalSteps\\n    ? `Step ${step} of ${totalSteps}`\\n    : `Step ${step}`;\\n\\n  return (\\n    <div\\n      className={cn(\\n        \\\"@container/question-flow flex w-full min-w-80 max-w-md flex-col gap-3\\\",\\n        \\\"text-foreground\\\",\\n        className,\\n      )}\\n      data-slot=\\\"question-flow\\\"\\n      data-tool-ui-id={id}\\n      role=\\\"form\\\"\\n      aria-labelledby={titleId}\\n      aria-describedby={description ? descriptionId : undefined}\\n    >\\n      <div\\n        className={cn(\\n          \\\"bg-card flex w-full flex-col gap-4 rounded-2xl border p-5 shadow-xs\\\",\\n        )}\\n      >\\n        <div className=\\\"flex flex-col gap-1\\\">\\n          <div className=\\\"flex flex-col gap-2\\\">\\n            <span\\n              className=\\\"text-muted-foreground text-xs font-medium uppercase tracking-wide\\\"\\n              aria-label={stepLabel}\\n            >\\n              {stepLabel}\\n            </span>\\n            {totalSteps && <ProgressBar current={step} total={totalSteps} />}\\n          </div>\\n        </div>\\n\\n        <div className=\\\"relative mt-1\\\">\\n          {exitingStepData && (\\n            <StepBodyContent\\n              key={exitingStepData.stepKey}\\n              stepKey={exitingStepData.stepKey}\\n              title={exitingStepData.title}\\n              description={exitingStepData.description}\\n              options={exitingStepData.options}\\n              selectionMode={exitingStepData.selectionMode}\\n              selectedIds={exitingStepData.selectedIds}\\n              id={id}\\n              isExiting\\n              transitionDirection={transitionDirection}\\n            />\\n          )}\\n          <StepBodyContent\\n            key={resolvedStepKey}\\n            stepKey={resolvedStepKey}\\n            title={title}\\n            description={description}\\n            options={options}\\n            selectionMode={selectionMode}\\n            selectedIds={selectedIds}\\n            onToggle={onToggle}\\n            id={id}\\n            isExiting={false}\\n            transitionDirection={\\n              exitingStepData ? transitionDirection : undefined\\n            }\\n          />\\n        </div>\\n\\n        <div className=\\\"flex items-center justify-between pt-2\\\">\\n          {showBack ? (\\n            <Button\\n              variant=\\\"ghost\\\"\\n              size=\\\"default\\\"\\n              onClick={onBack}\\n              disabled={isTransitioning}\\n              className=\\\"gap-1 rounded-full text-muted-foreground\\\"\\n            >\\n              <ChevronLeft className=\\\"size-4\\\" />\\n              Back\\n            </Button>\\n          ) : (\\n            <div />\\n          )}\\n          <Button\\n            variant=\\\"default\\\"\\n            size=\\\"default\\\"\\n            onClick={onNext}\\n            disabled={!canProceed || isTransitioning}\\n            className=\\\"rounded-full\\\"\\n          >\\n            {isLastStep ? \\\"Complete\\\" : \\\"Next\\\"}\\n          </Button>\\n        </div>\\n      </div>\\n    </div>\\n  );\\n}\\n\\nfunction QuestionFlowProgressive({\\n  id,\\n  step,\\n  title,\\n  description,\\n  options,\\n  selectionMode = \\\"single\\\",\\n  defaultValue,\\n  onSelect,\\n  onBack,\\n  className,\\n}: QuestionFlowProgressiveProps) {\\n  const [selectedIds, setSelectedIds] = useState<Set<string>>(\\n    () => new Set(defaultValue ?? []),\\n  );\\n\\n  const handleToggle = useCallback(\\n    (optionId: string) => {\\n      setSelectedIds((prev) => {\\n        const next = new Set(prev);\\n        if (selectionMode === \\\"single\\\") {\\n          if (next.has(optionId)) {\\n            next.delete(optionId);\\n          } else {\\n            next.clear();\\n            next.add(optionId);\\n          }\\n        } else {\\n          if (next.has(optionId)) {\\n            next.delete(optionId);\\n          } else {\\n            next.add(optionId);\\n          }\\n        }\\n        return next;\\n      });\\n    },\\n    [selectionMode],\\n  );\\n\\n  const handleNext = useCallback(() => {\\n    if (selectedIds.size === 0) return;\\n    const selection = Array.from(selectedIds);\\n    onSelect?.(selection);\\n  }, [onSelect, selectedIds]);\\n\\n  return (\\n    <StepContent\\n      id={id}\\n      step={step}\\n      title={title}\\n      description={description}\\n      options={options}\\n      selectionMode={selectionMode}\\n      selectedIds={selectedIds}\\n      onToggle={handleToggle}\\n      onBack={onBack}\\n      onNext={handleNext}\\n      showBack={step > 1 && onBack !== undefined}\\n      isLastStep={false}\\n      className={className}\\n    />\\n  );\\n}\\n\\nfunction QuestionFlowUpfront({\\n  id,\\n  steps,\\n  onStepChange,\\n  onComplete,\\n  className,\\n}: QuestionFlowUpfrontProps) {\\n  const [currentStepIndex, setCurrentStepIndex] = useState(0);\\n  const [answers, setAnswers] = useState<Record<string, string[]>>({});\\n  const [exitingStepData, setExitingStepData] = useState<StepBodyData | null>(\\n    null,\\n  );\\n  const [transitionDirection, setTransitionDirection] = useState<\\n    \\\"forward\\\" | \\\"backward\\\"\\n  >(\\\"forward\\\");\\n\\n  const currentStep = steps[currentStepIndex];\\n  const isLastStep = currentStepIndex === steps.length - 1;\\n  const totalSteps = steps.length;\\n\\n  useEffect(() => {\\n    if (exitingStepData) {\\n      const timer = setTimeout(() => setExitingStepData(null), 250);\\n      return () => clearTimeout(timer);\\n    }\\n  }, [exitingStepData]);\\n\\n  const currentSelection = useMemo(() => {\\n    const answer = answers[currentStep.id];\\n    return new Set(answer ?? []);\\n  }, [answers, currentStep.id]);\\n\\n  const handleToggle = useCallback(\\n    (optionId: string) => {\\n      const mode = currentStep.selectionMode ?? \\\"single\\\";\\n      setAnswers((prev) => {\\n        const current = prev[currentStep.id] ?? [];\\n        let next: string[];\\n\\n        if (mode === \\\"single\\\") {\\n          next = current.includes(optionId) ? [] : [optionId];\\n        } else {\\n          next = current.includes(optionId)\\n            ? current.filter((id) => id !== optionId)\\n            : [...current, optionId];\\n        }\\n\\n        return { ...prev, [currentStep.id]: next };\\n      });\\n    },\\n    [currentStep.id, currentStep.selectionMode],\\n  );\\n\\n  const handleBack = useCallback(() => {\\n    if (currentStepIndex > 0) {\\n      const currentStepData = steps[currentStepIndex];\\n      const stepOptions: QuestionFlowOption[] = currentStepData.options.map(\\n        (opt) => ({\\n          ...opt,\\n          icon: undefined,\\n        }),\\n      );\\n\\n      setExitingStepData({\\n        stepKey: currentStepData.id,\\n        title: currentStepData.title,\\n        description: currentStepData.description,\\n        options: stepOptions,\\n        selectionMode: currentStepData.selectionMode ?? \\\"single\\\",\\n        selectedIds: new Set(answers[currentStepData.id] ?? []),\\n      });\\n      setTransitionDirection(\\\"backward\\\");\\n      const prevIndex = currentStepIndex - 1;\\n      setCurrentStepIndex(prevIndex);\\n      onStepChange?.(steps[prevIndex].id);\\n    }\\n  }, [answers, currentStepIndex, onStepChange, steps]);\\n\\n  const handleNext = useCallback(() => {\\n    if (currentSelection.size === 0) return;\\n\\n    if (isLastStep) {\\n      onComplete?.(answers);\\n    } else {\\n      const currentStepData = steps[currentStepIndex];\\n      const stepOptions: QuestionFlowOption[] = currentStepData.options.map(\\n        (opt) => ({\\n          ...opt,\\n          icon: undefined,\\n        }),\\n      );\\n\\n      setExitingStepData({\\n        stepKey: currentStepData.id,\\n        title: currentStepData.title,\\n        description: currentStepData.description,\\n        options: stepOptions,\\n        selectionMode: currentStepData.selectionMode ?? \\\"single\\\",\\n        selectedIds: new Set(answers[currentStepData.id] ?? []),\\n      });\\n      setTransitionDirection(\\\"forward\\\");\\n      const nextIndex = currentStepIndex + 1;\\n      setCurrentStepIndex(nextIndex);\\n      onStepChange?.(steps[nextIndex].id);\\n    }\\n  }, [\\n    answers,\\n    currentSelection.size,\\n    currentStepIndex,\\n    isLastStep,\\n    onComplete,\\n    onStepChange,\\n    steps,\\n  ]);\\n\\n  const stepOptions: QuestionFlowOption[] = currentStep.options.map((opt) => ({\\n    ...opt,\\n    icon: undefined,\\n  }));\\n\\n  return (\\n    <StepContent\\n      id={id}\\n      step={currentStepIndex + 1}\\n      totalSteps={totalSteps}\\n      title={currentStep.title}\\n      description={currentStep.description}\\n      options={stepOptions}\\n      selectionMode={currentStep.selectionMode ?? \\\"single\\\"}\\n      selectedIds={currentSelection}\\n      onToggle={handleToggle}\\n      onBack={handleBack}\\n      onNext={handleNext}\\n      showBack={currentStepIndex > 0}\\n      isLastStep={isLastStep}\\n      className={className}\\n      stepKey={currentStep.id}\\n      exitingStepData={exitingStepData}\\n      transitionDirection={transitionDirection}\\n    />\\n  );\\n}\\n\\nexport function QuestionFlow(props: QuestionFlowProps) {\\n  if (\\\"choice\\\" in props && props.choice !== undefined) {\\n    return <QuestionFlowReceipt {...(props as QuestionFlowReceiptProps)} />;\\n  }\\n\\n  if (\\\"steps\\\" in props && props.steps !== undefined) {\\n    return <QuestionFlowUpfront {...(props as QuestionFlowUpfrontProps)} />;\\n  }\\n\\n  return (\\n    <QuestionFlowProgressive {...(props as QuestionFlowProgressiveProps)} />\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/question-flow/README.md\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/question-flow/README.md\",\n      \"content\": \"# Question Flow\\n\\nImplementation for the \\\"question-flow\\\" Tool UI surface.\\n\\n## Files\\n\\n- public exports: components/tool-ui/question-flow/index.tsx\\n- serializable schema + parse helpers: components/tool-ui/question-flow/schema.ts\\n\\n## Companion assets\\n\\n- Docs page: app/docs/question-flow/content.mdx\\n- Preset payload: lib/presets/question-flow.ts\\n\\n## Quick check\\n\\nRun this after edits:\\n\\npnpm test\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/question-flow/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/question-flow/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { ReactNode } from \\\"react\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\nimport { ToolUIIdSchema, ToolUIRoleSchema } from \\\"../shared/schema\\\";\\n\\nexport const QuestionFlowOptionSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  description: z.string().optional(),\\n  icon: z.custom<ReactNode>().optional(),\\n  disabled: z.boolean().optional(),\\n});\\n\\nexport type QuestionFlowOption = z.infer<typeof QuestionFlowOptionSchema>;\\n\\nexport const QuestionFlowStepDefinitionSchema = z.object({\\n  id: z.string().min(1),\\n  title: z.string().min(1),\\n  description: z.string().optional(),\\n  options: z.array(QuestionFlowOptionSchema.omit({ icon: true })).min(1),\\n  selectionMode: z.enum([\\\"single\\\", \\\"multi\\\"]).optional(),\\n});\\n\\nexport type QuestionFlowStepDefinition = z.infer<\\n  typeof QuestionFlowStepDefinitionSchema\\n>;\\n\\nexport const QuestionFlowSummaryItemSchema = z.object({\\n  label: z.string().min(1),\\n  value: z.string().min(1),\\n});\\n\\nexport type QuestionFlowSummaryItem = z.infer<\\n  typeof QuestionFlowSummaryItemSchema\\n>;\\n\\nexport const QuestionFlowChoiceSchema = z.object({\\n  title: z.string().min(1),\\n  summary: z.array(QuestionFlowSummaryItemSchema).min(1),\\n});\\n\\nexport type QuestionFlowChoice = z.infer<typeof QuestionFlowChoiceSchema>;\\n\\nconst BaseSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n});\\n\\nexport const SerializableProgressiveModeSchema = BaseSchema.extend({\\n  step: z.number().min(1),\\n  title: z.string().min(1),\\n  description: z.string().optional(),\\n  options: z.array(QuestionFlowOptionSchema.omit({ icon: true })).min(1),\\n  selectionMode: z.enum([\\\"single\\\", \\\"multi\\\"]).optional(),\\n});\\n\\nexport type SerializableProgressiveMode = z.infer<\\n  typeof SerializableProgressiveModeSchema\\n>;\\n\\nexport const SerializableUpfrontModeSchema = BaseSchema.extend({\\n  steps: z.array(QuestionFlowStepDefinitionSchema).min(1),\\n});\\n\\nexport type SerializableUpfrontMode = z.infer<\\n  typeof SerializableUpfrontModeSchema\\n>;\\n\\nexport const SerializableReceiptModeSchema = BaseSchema.extend({\\n  choice: QuestionFlowChoiceSchema,\\n});\\n\\nexport type SerializableReceiptMode = z.infer<\\n  typeof SerializableReceiptModeSchema\\n>;\\n\\nexport const SerializableQuestionFlowSchema = z.union([\\n  SerializableProgressiveModeSchema,\\n  SerializableUpfrontModeSchema,\\n  SerializableReceiptModeSchema,\\n]);\\n\\nexport type SerializableQuestionFlow = z.infer<\\n  typeof SerializableQuestionFlowSchema\\n>;\\n\\nconst SerializableQuestionFlowSchemaContract = defineToolUiContract(\\n  \\\"QuestionFlow\\\",\\n  SerializableQuestionFlowSchema,\\n);\\n\\nexport const parseSerializableQuestionFlow: (\\n  input: unknown,\\n) => SerializableQuestionFlow = SerializableQuestionFlowSchemaContract.parse;\\n\\nexport const safeParseSerializableQuestionFlow: (\\n  input: unknown,\\n) => SerializableQuestionFlow | null =\\n  SerializableQuestionFlowSchemaContract.safeParse;\\ninterface BaseRuntimeProps {\\n  className?: string;\\n}\\n\\nexport interface QuestionFlowProgressiveProps\\n  extends BaseRuntimeProps, Omit<SerializableProgressiveMode, \\\"options\\\"> {\\n  options: QuestionFlowOption[];\\n  defaultValue?: string[];\\n  onSelect?: (optionIds: string[]) => void | Promise<void>;\\n  onBack?: () => void;\\n  steps?: never;\\n  choice?: never;\\n}\\n\\nexport interface QuestionFlowUpfrontProps\\n  extends BaseRuntimeProps, SerializableUpfrontMode {\\n  onStepChange?: (stepId: string) => void;\\n  onComplete?: (answers: Record<string, string[]>) => void | Promise<void>;\\n  step?: never;\\n  choice?: never;\\n}\\n\\nexport interface QuestionFlowReceiptProps\\n  extends BaseRuntimeProps, SerializableReceiptMode {\\n  step?: never;\\n  steps?: never;\\n}\\n\\nexport type QuestionFlowProps =\\n  | QuestionFlowProgressiveProps\\n  | QuestionFlowUpfrontProps\\n  | QuestionFlowReceiptProps;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { ReactNode } from \\\"react\\\";\\n\\n/**\\n * Tool UI conventions:\\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\\n * - Schema: `SerializableXSchema`\\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\\n * - Root attrs: `data-tool-ui-id` + `data-slot`\\n */\\n\\n/**\\n * Schema for tool UI identity.\\n *\\n * Every tool UI should have a unique identifier that:\\n * - Is stable across re-renders\\n * - Is meaningful (not auto-generated)\\n * - Is unique within the conversation\\n *\\n * Format recommendation: `{component-type}-{semantic-identifier}`\\n * Examples: \\\"data-table-expenses-q3\\\", \\\"option-list-deploy-target\\\"\\n */\\nexport const ToolUIIdSchema = z.string().min(1);\\n\\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\\n\\n/**\\n * Primary role of a Tool UI surface in a chat context.\\n */\\nexport const ToolUIRoleSchema = z.enum([\\n  \\\"information\\\",\\n  \\\"decision\\\",\\n  \\\"control\\\",\\n  \\\"state\\\",\\n  \\\"composite\\\",\\n]);\\n\\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\\n\\nexport const ToolUIReceiptOutcomeSchema = z.enum([\\n  \\\"success\\\",\\n  \\\"partial\\\",\\n  \\\"failed\\\",\\n  \\\"cancelled\\\",\\n]);\\n\\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\\n\\n/**\\n * Optional receipt metadata: a durable summary of an outcome.\\n */\\nexport const ToolUIReceiptSchema = z.object({\\n  outcome: ToolUIReceiptOutcomeSchema,\\n  summary: z.string().min(1),\\n  identifiers: z.record(z.string(), z.string()).optional(),\\n  at: z.string().datetime(),\\n});\\n\\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\\n\\n/**\\n * Base schema for Tool UI payloads (id + optional role/receipt).\\n */\\nexport const ToolUISurfaceSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n});\\n\\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\\n\\nexport const ActionSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  /**\\n   * Canonical narration the assistant can use after this action is taken.\\n   *\\n   * Example: \\\"I exported the table as CSV.\\\" / \\\"I opened the link in a new tab.\\\"\\n   */\\n  sentence: z.string().optional(),\\n  confirmLabel: z.string().optional(),\\n  variant: z\\n    .enum([\\\"default\\\", \\\"destructive\\\", \\\"secondary\\\", \\\"ghost\\\", \\\"outline\\\"])\\n    .optional(),\\n  icon: z.custom<ReactNode>().optional(),\\n  loading: z.boolean().optional(),\\n  disabled: z.boolean().optional(),\\n  shortcut: z.string().optional(),\\n});\\n\\nexport type Action = z.infer<typeof ActionSchema>;\\nexport type LocalAction = Action;\\nexport type DecisionAction = Action;\\n\\nexport const DecisionResultSchema = z.object({\\n  kind: z.literal(\\\"decision\\\"),\\n  version: z.literal(1),\\n  decisionId: z.string().min(1),\\n  actionId: z.string().min(1),\\n  actionLabel: z.string().min(1),\\n  at: z.string().datetime(),\\n  payload: z.record(z.string(), z.unknown()).optional(),\\n});\\n\\nexport type DecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n> = Omit<z.infer<typeof DecisionResultSchema>, \\\"payload\\\"> & {\\n  payload?: TPayload;\\n};\\n\\nexport function createDecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n>(args: {\\n  decisionId: string;\\n  action: { id: string; label: string };\\n  payload?: TPayload;\\n}): DecisionResult<TPayload> {\\n  return {\\n    kind: \\\"decision\\\",\\n    version: 1,\\n    decisionId: args.decisionId,\\n    actionId: args.action.id,\\n    actionLabel: args.action.label,\\n    at: new Date().toISOString(),\\n    payload: args.payload,\\n  };\\n}\\n\\nexport const ActionButtonsPropsSchema = z.object({\\n  actions: z.array(ActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n  className: z.string().optional(),\\n});\\n\\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\\n  actions: z.array(SerializableActionSchema),\\n}).omit({ className: true });\\n\\nexport interface ActionsConfig {\\n  items: Action[];\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  confirmTimeout?: number;\\n}\\n\\nexport const SerializableActionsConfigSchema = z.object({\\n  items: z.array(SerializableActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n});\\n\\nexport type SerializableActionsConfig = z.infer<\\n  typeof SerializableActionsConfigSchema\\n>;\\n\\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/registry.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry.json\",\n  \"name\": \"tool-ui\",\n  \"homepage\": \"https://tool-ui.com\",\n  \"items\": [\n    {\n      \"name\": \"approval-card\",\n      \"type\": \"registry:block\",\n      \"title\": \"Approval Card\",\n      \"description\": \"Binary confirmation for agent actions.\",\n      \"dependencies\": [\n        \"lucide-react\",\n        \"zod\"\n      ],\n      \"registryDependencies\": [\n        \"button\",\n        \"separator\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/approval-card/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/approval-card/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/approval-card/approval-card.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/approval-card/approval-card.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/approval-card/index.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/approval-card/index.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/approval-card/README.md\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/approval-card/README.md\"\n        },\n        {\n          \"path\": \"components/tool-ui/approval-card/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/approval-card/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/shared/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/action-buttons.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/shared/action-buttons.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/use-action-buttons.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/shared/use-action-buttons.tsx\"\n        }\n      ]\n    },\n    {\n      \"name\": \"audio\",\n      \"type\": \"registry:block\",\n      \"title\": \"Audio\",\n      \"description\": \"Audio playback with artwork and metadata.\",\n      \"dependencies\": [\n        \"lucide-react\",\n        \"zod\"\n      ],\n      \"registryDependencies\": [\n        \"button\",\n        \"slider\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/audio/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/audio/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/audio/audio.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/audio/audio.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/audio/context.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/audio/context.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/audio/index.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/audio/index.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/audio/README.md\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/audio/README.md\"\n        },\n        {\n          \"path\": \"components/tool-ui/audio/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/audio/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/schema.ts\"\n        }\n      ]\n    },\n    {\n      \"name\": \"chart\",\n      \"type\": \"registry:block\",\n      \"title\": \"Chart\",\n      \"description\": \"Visualize data with interactive charts.\",\n      \"dependencies\": [\n        \"recharts@2.15.4\",\n        \"zod\"\n      ],\n      \"registryDependencies\": [\n        \"card\",\n        \"chart\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/chart/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/chart/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/chart/chart.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/chart/chart.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/chart/index.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/chart/index.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/chart/README.md\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/chart/README.md\"\n        },\n        {\n          \"path\": \"components/tool-ui/chart/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/chart/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/schema.ts\"\n        }\n      ]\n    },\n    {\n      \"name\": \"citation\",\n      \"type\": \"registry:block\",\n      \"title\": \"Citation\",\n      \"description\": \"Display source references with attribution.\",\n      \"dependencies\": [\n        \"lucide-react\",\n        \"zod\"\n      ],\n      \"registryDependencies\": [\n        \"popover\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/citation/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/citation/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/citation/citation-list.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/citation/citation-list.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/citation/citation.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/citation/citation.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/citation/index.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/citation/index.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/citation/README.md\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/citation/README.md\"\n        },\n        {\n          \"path\": \"components/tool-ui/citation/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/citation/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/aspect-ratio.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/aspect-ratio.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/format-utils.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/format-utils.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/index.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/index.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/overlay-gradient.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/overlay-gradient.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/safe-navigation.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/safe-navigation.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/sanitize-href.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/sanitize-href.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/schema.ts\"\n        }\n      ]\n    },\n    {\n      \"name\": \"code-block\",\n      \"type\": \"registry:block\",\n      \"title\": \"Code Block\",\n      \"description\": \"Display syntax-highlighted code snippets.\",\n      \"dependencies\": [\n        \"lucide-react\",\n        \"shiki\",\n        \"zod\"\n      ],\n      \"registryDependencies\": [\n        \"button\",\n        \"collapsible\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/code-block/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/code-block/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/code-block/code-block.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/code-block/code-block.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/code-block/index.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/code-block/index.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/code-block/README.md\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/code-block/README.md\"\n        },\n        {\n          \"path\": \"components/tool-ui/code-block/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/code-block/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/pierre-dark-theme.js\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/shared/pierre-dark-theme.js\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/pierre-light-theme.js\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/shared/pierre-light-theme.js\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/use-copy-to-clipboard.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/use-copy-to-clipboard.ts\"\n        }\n      ]\n    },\n    {\n      \"name\": \"code-diff\",\n      \"type\": \"registry:block\",\n      \"title\": \"Code Diff\",\n      \"description\": \"Code Diff component for AI interfaces.\",\n      \"dependencies\": [\n        \"@pierre/diffs\",\n        \"lucide-react\",\n        \"zod\"\n      ],\n      \"registryDependencies\": [\n        \"button\",\n        \"collapsible\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/code-diff/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/code-diff/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/code-diff/code-diff.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/code-diff/code-diff.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/code-diff/index.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/code-diff/index.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/code-diff/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/code-diff/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/pierre-dark-theme.js\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/shared/pierre-dark-theme.js\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/pierre-light-theme.js\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/shared/pierre-light-theme.js\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/use-copy-to-clipboard.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/use-copy-to-clipboard.ts\"\n        }\n      ]\n    },\n    {\n      \"name\": \"data-table\",\n      \"type\": \"registry:block\",\n      \"title\": \"Data Table\",\n      \"description\": \"Sortable, responsive data tables for tool call results.\",\n      \"dependencies\": [\n        \"zod\"\n      ],\n      \"registryDependencies\": [\n        \"accordion\",\n        \"badge\",\n        \"button\",\n        \"dropdown-menu\",\n        \"table\",\n        \"tooltip\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/data-table/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/data-table/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/data-table/data-table.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/data-table/data-table.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/data-table/formatters.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/data-table/formatters.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/data-table/index.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/data-table/index.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/data-table/README.md\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/data-table/README.md\"\n        },\n        {\n          \"path\": \"components/tool-ui/data-table/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/data-table/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/data-table/types.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/data-table/types.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/data-table/utilities.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/data-table/utilities.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/aspect-ratio.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/aspect-ratio.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/format-utils.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/format-utils.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/index.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/index.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/overlay-gradient.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/overlay-gradient.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/safe-navigation.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/safe-navigation.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/sanitize-href.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/sanitize-href.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/schema.ts\"\n        }\n      ]\n    },\n    {\n      \"name\": \"geo-map\",\n      \"type\": \"registry:block\",\n      \"title\": \"Geo Map\",\n      \"description\": \"Display geolocated entities and fleet positions.\",\n      \"dependencies\": [\n        \"leaflet\",\n        \"react-leaflet\",\n        \"supercluster\",\n        \"zod\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/geo-map/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/geo-map/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/geo-map/geo-map-engine.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/geo-map/geo-map-engine.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/geo-map/geo-map-icons.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/geo-map/geo-map-icons.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/geo-map/geo-map-overlays.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/geo-map/geo-map-overlays.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/geo-map/geo-map-theme.module.css\",\n          \"type\": \"registry:style\",\n          \"target\": \"components/tool-ui/geo-map/geo-map-theme.module.css\"\n        },\n        {\n          \"path\": \"components/tool-ui/geo-map/geo-map.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/geo-map/geo-map.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/geo-map/index.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/geo-map/index.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/geo-map/README.md\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/geo-map/README.md\"\n        },\n        {\n          \"path\": \"components/tool-ui/geo-map/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/geo-map/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/schema.ts\"\n        }\n      ]\n    },\n    {\n      \"name\": \"image\",\n      \"type\": \"registry:block\",\n      \"title\": \"Image\",\n      \"description\": \"Display images with metadata and attribution.\",\n      \"dependencies\": [\n        \"zod\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/image/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/image/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/image/image.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/image/image.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/image/index.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/image/index.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/image/README.md\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/image/README.md\"\n        },\n        {\n          \"path\": \"components/tool-ui/image/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/image/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/aspect-ratio.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/aspect-ratio.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/format-utils.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/format-utils.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/index.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/index.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/overlay-gradient.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/overlay-gradient.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/safe-navigation.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/safe-navigation.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/sanitize-href.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/sanitize-href.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/schema.ts\"\n        }\n      ]\n    },\n    {\n      \"name\": \"image-gallery\",\n      \"type\": \"registry:block\",\n      \"title\": \"Image Gallery\",\n      \"description\": \"Grid layout for browsing image collections.\",\n      \"dependencies\": [\n        \"lucide-react\",\n        \"zod\"\n      ],\n      \"registryDependencies\": [\n        \"button\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/image-gallery/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/image-gallery/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/image-gallery/context.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/image-gallery/context.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/image-gallery/gallery-grid.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/image-gallery/gallery-grid.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/image-gallery/gallery-lightbox.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/image-gallery/gallery-lightbox.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/image-gallery/image-gallery.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/image-gallery/image-gallery.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/image-gallery/index.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/image-gallery/index.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/image-gallery/README.md\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/image-gallery/README.md\"\n        },\n        {\n          \"path\": \"components/tool-ui/image-gallery/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/image-gallery/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/image-gallery/styles.css\",\n          \"type\": \"registry:style\",\n          \"target\": \"components/tool-ui/image-gallery/styles.css\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/aspect-ratio.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/aspect-ratio.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/format-utils.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/format-utils.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/index.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/index.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/overlay-gradient.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/overlay-gradient.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/safe-navigation.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/safe-navigation.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/sanitize-href.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/sanitize-href.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/schema.ts\"\n        }\n      ]\n    },\n    {\n      \"name\": \"instagram-post\",\n      \"type\": \"registry:block\",\n      \"title\": \"Instagram Post\",\n      \"description\": \"Render Instagram post previews.\",\n      \"dependencies\": [\n        \"lucide-react\",\n        \"zod\"\n      ],\n      \"registryDependencies\": [\n        \"button\",\n        \"tooltip\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/instagram-post/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/instagram-post/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/instagram-post/index.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/instagram-post/index.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/instagram-post/instagram-post.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/instagram-post/instagram-post.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/instagram-post/README.md\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/instagram-post/README.md\"\n        },\n        {\n          \"path\": \"components/tool-ui/instagram-post/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/instagram-post/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/utils.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/utils.ts\"\n        }\n      ]\n    },\n    {\n      \"name\": \"item-carousel\",\n      \"type\": \"registry:block\",\n      \"title\": \"Item Carousel\",\n      \"description\": \"Horizontal carousel for browsing collections.\",\n      \"dependencies\": [\n        \"lucide-react\",\n        \"zod\"\n      ],\n      \"registryDependencies\": [\n        \"button\",\n        \"card\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/item-carousel/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/item-carousel/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/item-carousel/index.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/item-carousel/index.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/item-carousel/item-card.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/item-carousel/item-card.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/item-carousel/item-carousel.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/item-carousel/item-carousel.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/item-carousel/README.md\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/item-carousel/README.md\"\n        },\n        {\n          \"path\": \"components/tool-ui/item-carousel/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/item-carousel/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/utils.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/utils.ts\"\n        }\n      ]\n    },\n    {\n      \"name\": \"link-preview\",\n      \"type\": \"registry:block\",\n      \"title\": \"Link Preview\",\n      \"description\": \"Rich link previews with OG data.\",\n      \"dependencies\": [\n        \"lucide-react\",\n        \"zod\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/link-preview/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/link-preview/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/link-preview/index.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/link-preview/index.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/link-preview/link-preview.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/link-preview/link-preview.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/link-preview/README.md\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/link-preview/README.md\"\n        },\n        {\n          \"path\": \"components/tool-ui/link-preview/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/link-preview/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/aspect-ratio.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/aspect-ratio.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/format-utils.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/format-utils.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/index.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/index.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/overlay-gradient.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/overlay-gradient.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/safe-navigation.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/safe-navigation.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/sanitize-href.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/sanitize-href.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/schema.ts\"\n        }\n      ]\n    },\n    {\n      \"name\": \"linkedin-post\",\n      \"type\": \"registry:block\",\n      \"title\": \"Linkedin Post\",\n      \"description\": \"Render LinkedIn post previews.\",\n      \"dependencies\": [\n        \"lucide-react\",\n        \"zod\"\n      ],\n      \"registryDependencies\": [\n        \"button\",\n        \"tooltip\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/linkedin-post/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/linkedin-post/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/linkedin-post/index.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/linkedin-post/index.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/linkedin-post/linkedin-post.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/linkedin-post/linkedin-post.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/linkedin-post/README.md\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/linkedin-post/README.md\"\n        },\n        {\n          \"path\": \"components/tool-ui/linkedin-post/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/linkedin-post/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/aspect-ratio.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/aspect-ratio.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/format-utils.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/format-utils.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/index.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/index.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/overlay-gradient.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/overlay-gradient.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/safe-navigation.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/safe-navigation.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/sanitize-href.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/sanitize-href.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/utils.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/utils.ts\"\n        }\n      ]\n    },\n    {\n      \"name\": \"message-draft\",\n      \"type\": \"registry:block\",\n      \"title\": \"Message Draft\",\n      \"description\": \"Review and confirm drafted messages before sending.\",\n      \"dependencies\": [\n        \"lucide-react\",\n        \"zod\"\n      ],\n      \"registryDependencies\": [\n        \"button\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/message-draft/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/message-draft/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/message-draft/index.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/message-draft/index.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/message-draft/message-draft.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/message-draft/message-draft.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/message-draft/README.md\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/message-draft/README.md\"\n        },\n        {\n          \"path\": \"components/tool-ui/message-draft/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/message-draft/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/shared/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/action-buttons.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/shared/action-buttons.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/use-action-buttons.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/shared/use-action-buttons.tsx\"\n        }\n      ]\n    },\n    {\n      \"name\": \"option-list\",\n      \"type\": \"registry:block\",\n      \"title\": \"Option List\",\n      \"description\": \"Single or multi-select choices with confirmation actions.\",\n      \"dependencies\": [\n        \"lucide-react\",\n        \"zod\"\n      ],\n      \"registryDependencies\": [\n        \"button\",\n        \"separator\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/option-list/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/option-list/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/option-list/index.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/option-list/index.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/option-list/option-list.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/option-list/option-list.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/option-list/README.md\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/option-list/README.md\"\n        },\n        {\n          \"path\": \"components/tool-ui/option-list/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/option-list/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/option-list/selection.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/option-list/selection.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/shared/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/action-buttons.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/shared/action-buttons.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/actions-config.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/actions-config.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/embedded-actions.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/embedded-actions.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/use-action-buttons.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/shared/use-action-buttons.tsx\"\n        }\n      ]\n    },\n    {\n      \"name\": \"order-summary\",\n      \"type\": \"registry:block\",\n      \"title\": \"Order Summary\",\n      \"description\": \"Itemized purchase confirmation with pricing.\",\n      \"dependencies\": [\n        \"lucide-react\",\n        \"zod\"\n      ],\n      \"registryDependencies\": [\n        \"button\",\n        \"separator\",\n        \"skeleton\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/order-summary/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/order-summary/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/order-summary/index.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/order-summary/index.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/order-summary/order-summary.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/order-summary/order-summary.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/order-summary/README.md\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/order-summary/README.md\"\n        },\n        {\n          \"path\": \"components/tool-ui/order-summary/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/order-summary/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/schema.ts\"\n        }\n      ]\n    },\n    {\n      \"name\": \"parameter-slider\",\n      \"type\": \"registry:block\",\n      \"title\": \"Parameter Slider\",\n      \"description\": \"Numeric parameter adjustment controls.\",\n      \"dependencies\": [\n        \"@radix-ui/react-slider\",\n        \"zod\"\n      ],\n      \"registryDependencies\": [\n        \"button\",\n        \"separator\",\n        \"slider\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/parameter-slider/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/parameter-slider/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/parameter-slider/index.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/parameter-slider/index.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/parameter-slider/math.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/parameter-slider/math.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/parameter-slider/parameter-slider.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/parameter-slider/parameter-slider.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/parameter-slider/README.md\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/parameter-slider/README.md\"\n        },\n        {\n          \"path\": \"components/tool-ui/parameter-slider/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/parameter-slider/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/shared/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/action-buttons.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/shared/action-buttons.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/actions-config.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/actions-config.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/embedded-actions.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/embedded-actions.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/use-action-buttons.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/shared/use-action-buttons.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/use-controllable-state.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/use-controllable-state.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/use-signature-reset.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/use-signature-reset.ts\"\n        }\n      ]\n    },\n    {\n      \"name\": \"plan\",\n      \"type\": \"registry:block\",\n      \"title\": \"Plan\",\n      \"description\": \"Display step-by-step task workflows in AI interfaces.\",\n      \"dependencies\": [\n        \"lucide-react\",\n        \"zod\"\n      ],\n      \"registryDependencies\": [\n        \"accordion\",\n        \"card\",\n        \"collapsible\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/plan/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/plan/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/plan/index.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/plan/index.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/plan/plan.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/plan/plan.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/plan/progress.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/plan/progress.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/plan/README.md\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/plan/README.md\"\n        },\n        {\n          \"path\": \"components/tool-ui/plan/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/plan/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/schema.ts\"\n        }\n      ]\n    },\n    {\n      \"name\": \"preferences-panel\",\n      \"type\": \"registry:block\",\n      \"title\": \"Preferences Panel\",\n      \"description\": \"Compact settings panel for user preferences.\",\n      \"dependencies\": [\n        \"lucide-react\",\n        \"zod\"\n      ],\n      \"registryDependencies\": [\n        \"button\",\n        \"label\",\n        \"select\",\n        \"separator\",\n        \"switch\",\n        \"toggle-group\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/preferences-panel/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/preferences-panel/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/preferences-panel/index.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/preferences-panel/index.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/preferences-panel/preferences-panel.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/preferences-panel/preferences-panel.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/preferences-panel/README.md\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/preferences-panel/README.md\"\n        },\n        {\n          \"path\": \"components/tool-ui/preferences-panel/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/preferences-panel/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/preferences-panel/signature.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/preferences-panel/signature.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/shared/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/action-buttons.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/shared/action-buttons.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/actions-config.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/actions-config.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/embedded-actions.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/embedded-actions.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/use-action-buttons.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/shared/use-action-buttons.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/use-controllable-state.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/use-controllable-state.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/use-signature-reset.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/use-signature-reset.ts\"\n        }\n      ]\n    },\n    {\n      \"name\": \"progress-tracker\",\n      \"type\": \"registry:block\",\n      \"title\": \"Progress Tracker\",\n      \"description\": \"Show real-time status feedback for multi-step operations in AI interfaces.\",\n      \"dependencies\": [\n        \"lucide-react\",\n        \"zod\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/progress-tracker/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/progress-tracker/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/progress-tracker/index.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/progress-tracker/index.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/progress-tracker/progress-tracker.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/progress-tracker/progress-tracker.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/progress-tracker/README.md\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/progress-tracker/README.md\"\n        },\n        {\n          \"path\": \"components/tool-ui/progress-tracker/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/progress-tracker/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/schema.ts\"\n        }\n      ]\n    },\n    {\n      \"name\": \"question-flow\",\n      \"type\": \"registry:block\",\n      \"title\": \"Question Flow\",\n      \"description\": \"Multi-step guided questions with branching.\",\n      \"dependencies\": [\n        \"lucide-react\",\n        \"zod\"\n      ],\n      \"registryDependencies\": [\n        \"button\",\n        \"separator\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/question-flow/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/question-flow/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/question-flow/index.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/question-flow/index.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/question-flow/question-flow.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/question-flow/question-flow.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/question-flow/README.md\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/question-flow/README.md\"\n        },\n        {\n          \"path\": \"components/tool-ui/question-flow/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/question-flow/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/schema.ts\"\n        }\n      ]\n    },\n    {\n      \"name\": \"stats-display\",\n      \"type\": \"registry:block\",\n      \"title\": \"Stats Display\",\n      \"description\": \"Display key metrics in compact cards.\",\n      \"dependencies\": [\n        \"zod\"\n      ],\n      \"registryDependencies\": [\n        \"card\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/stats-display/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/stats-display/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/stats-display/index.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/stats-display/index.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/stats-display/README.md\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/stats-display/README.md\"\n        },\n        {\n          \"path\": \"components/tool-ui/stats-display/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/stats-display/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/stats-display/sparkline.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/stats-display/sparkline.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/stats-display/stats-display.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/stats-display/stats-display.tsx\"\n        }\n      ]\n    },\n    {\n      \"name\": \"terminal\",\n      \"type\": \"registry:block\",\n      \"title\": \"Terminal\",\n      \"description\": \"Show command-line output and logs.\",\n      \"dependencies\": [\n        \"ansi-to-react\",\n        \"lucide-react\",\n        \"zod\"\n      ],\n      \"registryDependencies\": [\n        \"button\",\n        \"collapsible\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/use-copy-to-clipboard.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/use-copy-to-clipboard.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/terminal/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/terminal/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/terminal/index.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/terminal/index.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/terminal/README.md\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/terminal/README.md\"\n        },\n        {\n          \"path\": \"components/tool-ui/terminal/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/terminal/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/terminal/terminal.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/terminal/terminal.tsx\"\n        }\n      ]\n    },\n    {\n      \"name\": \"video\",\n      \"type\": \"registry:block\",\n      \"title\": \"Video\",\n      \"description\": \"Video playback with controls and poster.\",\n      \"dependencies\": [\n        \"lucide-react\",\n        \"zod\"\n      ],\n      \"registryDependencies\": [\n        \"button\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/aspect-ratio.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/aspect-ratio.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/format-utils.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/format-utils.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/index.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/index.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/overlay-gradient.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/overlay-gradient.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/safe-navigation.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/safe-navigation.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/sanitize-href.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/sanitize-href.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/video/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/video/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/video/context.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/video/context.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/video/index.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/video/index.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/video/README.md\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/video/README.md\"\n        },\n        {\n          \"path\": \"components/tool-ui/video/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/video/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/video/video-helpers.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/video/video-helpers.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/video/video.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/video/video.tsx\"\n        }\n      ]\n    },\n    {\n      \"name\": \"weather-widget\",\n      \"type\": \"registry:block\",\n      \"title\": \"Weather Widget\",\n      \"description\": \"Display weather conditions and forecasts.\",\n      \"dependencies\": [\n        \"lucide-react\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/weather-widget/generated/weather-runtime-core.generated.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/weather-widget/generated/weather-runtime-core.generated.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/weather-widget/runtime.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/weather-widget/runtime.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/weather-widget/schema-runtime.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/weather-widget/schema-runtime.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/weather-widget/weather-data-overlay.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/weather-widget/weather-data-overlay.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/weather-widget/weather-widget-container.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/weather-widget/weather-widget-container.tsx\"\n        }\n      ]\n    },\n    {\n      \"name\": \"x-post\",\n      \"type\": \"registry:block\",\n      \"title\": \"X Post\",\n      \"description\": \"Render X (Twitter) post previews.\",\n      \"dependencies\": [\n        \"lucide-react\",\n        \"zod\"\n      ],\n      \"registryDependencies\": [\n        \"button\",\n        \"tooltip\"\n      ],\n      \"files\": [\n        {\n          \"path\": \"components/tool-ui/shared/contract.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/contract.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/aspect-ratio.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/aspect-ratio.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/format-utils.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/format-utils.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/index.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/index.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/overlay-gradient.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/overlay-gradient.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/safe-navigation.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/safe-navigation.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/media/sanitize-href.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/media/sanitize-href.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/parse.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/parse.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/shared/utils.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/shared/utils.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/x-post/_adapter.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/x-post/_adapter.tsx\"\n        },\n        {\n          \"path\": \"components/tool-ui/x-post/index.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/x-post/index.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/x-post/README.md\",\n          \"type\": \"registry:file\",\n          \"target\": \"components/tool-ui/x-post/README.md\"\n        },\n        {\n          \"path\": \"components/tool-ui/x-post/schema.ts\",\n          \"type\": \"registry:lib\",\n          \"target\": \"components/tool-ui/x-post/schema.ts\"\n        },\n        {\n          \"path\": \"components/tool-ui/x-post/x-post.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/tool-ui/x-post/x-post.tsx\"\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/stats-display.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"stats-display\",\n  \"type\": \"registry:block\",\n  \"title\": \"Stats Display\",\n  \"description\": \"Display key metrics in compact cards.\",\n  \"dependencies\": [\n    \"zod\"\n  ],\n  \"registryDependencies\": [\n    \"card\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { ReactNode } from \\\"react\\\";\\n\\n/**\\n * Tool UI conventions:\\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\\n * - Schema: `SerializableXSchema`\\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\\n * - Root attrs: `data-tool-ui-id` + `data-slot`\\n */\\n\\n/**\\n * Schema for tool UI identity.\\n *\\n * Every tool UI should have a unique identifier that:\\n * - Is stable across re-renders\\n * - Is meaningful (not auto-generated)\\n * - Is unique within the conversation\\n *\\n * Format recommendation: `{component-type}-{semantic-identifier}`\\n * Examples: \\\"data-table-expenses-q3\\\", \\\"option-list-deploy-target\\\"\\n */\\nexport const ToolUIIdSchema = z.string().min(1);\\n\\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\\n\\n/**\\n * Primary role of a Tool UI surface in a chat context.\\n */\\nexport const ToolUIRoleSchema = z.enum([\\n  \\\"information\\\",\\n  \\\"decision\\\",\\n  \\\"control\\\",\\n  \\\"state\\\",\\n  \\\"composite\\\",\\n]);\\n\\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\\n\\nexport const ToolUIReceiptOutcomeSchema = z.enum([\\n  \\\"success\\\",\\n  \\\"partial\\\",\\n  \\\"failed\\\",\\n  \\\"cancelled\\\",\\n]);\\n\\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\\n\\n/**\\n * Optional receipt metadata: a durable summary of an outcome.\\n */\\nexport const ToolUIReceiptSchema = z.object({\\n  outcome: ToolUIReceiptOutcomeSchema,\\n  summary: z.string().min(1),\\n  identifiers: z.record(z.string(), z.string()).optional(),\\n  at: z.string().datetime(),\\n});\\n\\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\\n\\n/**\\n * Base schema for Tool UI payloads (id + optional role/receipt).\\n */\\nexport const ToolUISurfaceSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n});\\n\\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\\n\\nexport const ActionSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  /**\\n   * Canonical narration the assistant can use after this action is taken.\\n   *\\n   * Example: \\\"I exported the table as CSV.\\\" / \\\"I opened the link in a new tab.\\\"\\n   */\\n  sentence: z.string().optional(),\\n  confirmLabel: z.string().optional(),\\n  variant: z\\n    .enum([\\\"default\\\", \\\"destructive\\\", \\\"secondary\\\", \\\"ghost\\\", \\\"outline\\\"])\\n    .optional(),\\n  icon: z.custom<ReactNode>().optional(),\\n  loading: z.boolean().optional(),\\n  disabled: z.boolean().optional(),\\n  shortcut: z.string().optional(),\\n});\\n\\nexport type Action = z.infer<typeof ActionSchema>;\\nexport type LocalAction = Action;\\nexport type DecisionAction = Action;\\n\\nexport const DecisionResultSchema = z.object({\\n  kind: z.literal(\\\"decision\\\"),\\n  version: z.literal(1),\\n  decisionId: z.string().min(1),\\n  actionId: z.string().min(1),\\n  actionLabel: z.string().min(1),\\n  at: z.string().datetime(),\\n  payload: z.record(z.string(), z.unknown()).optional(),\\n});\\n\\nexport type DecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n> = Omit<z.infer<typeof DecisionResultSchema>, \\\"payload\\\"> & {\\n  payload?: TPayload;\\n};\\n\\nexport function createDecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n>(args: {\\n  decisionId: string;\\n  action: { id: string; label: string };\\n  payload?: TPayload;\\n}): DecisionResult<TPayload> {\\n  return {\\n    kind: \\\"decision\\\",\\n    version: 1,\\n    decisionId: args.decisionId,\\n    actionId: args.action.id,\\n    actionLabel: args.action.label,\\n    at: new Date().toISOString(),\\n    payload: args.payload,\\n  };\\n}\\n\\nexport const ActionButtonsPropsSchema = z.object({\\n  actions: z.array(ActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n  className: z.string().optional(),\\n});\\n\\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\\n  actions: z.array(SerializableActionSchema),\\n}).omit({ className: true });\\n\\nexport interface ActionsConfig {\\n  items: Action[];\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  confirmTimeout?: number;\\n}\\n\\nexport const SerializableActionsConfigSchema = z.object({\\n  items: z.array(SerializableActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n});\\n\\nexport type SerializableActionsConfig = z.infer<\\n  typeof SerializableActionsConfigSchema\\n>;\\n\\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/stats-display/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/stats-display/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn   → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n *   Card → shadcn/ui Card\\n */\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport {\\n  Card,\\n  CardHeader,\\n  CardTitle,\\n  CardDescription,\\n  CardContent,\\n} from \\\"@/components/ui/card\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/stats-display/index.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/stats-display/index.tsx\",\n      \"content\": \"export { StatsDisplay } from \\\"./stats-display\\\";\\nexport { Sparkline, type SparklineProps } from \\\"./sparkline\\\";\\nexport {\\n  type SerializableStatsDisplay,\\n  type StatsDisplayProps,\\n  type StatFormat,\\n  type StatDiff,\\n  type StatSparkline,\\n  type StatItem,\\n} from \\\"./schema\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/stats-display/README.md\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/stats-display/README.md\",\n      \"content\": \"# Stats Display\\n\\nImplementation for the \\\"stats-display\\\" Tool UI surface.\\n\\n## Files\\n\\n- public exports: components/tool-ui/stats-display/index.tsx\\n- serializable schema + parse helpers: components/tool-ui/stats-display/schema.ts\\n\\n## Companion assets\\n\\n- Docs page: app/docs/stats-display/content.mdx\\n- Preset payload: lib/presets/stats-display.ts\\n\\n## Quick check\\n\\nRun this after edits:\\n\\npnpm test\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/stats-display/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/stats-display/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\nimport { ToolUIIdSchema, ToolUIRoleSchema } from \\\"../shared/schema\\\";\\n\\nconst TextFormatSchema = z.object({\\n  kind: z.literal(\\\"text\\\"),\\n});\\n\\nconst NumberFormatSchema = z.object({\\n  kind: z.literal(\\\"number\\\"),\\n  decimals: z.number().int().min(0).optional(),\\n  compact: z.boolean().optional(),\\n});\\n\\nconst CurrencyFormatSchema = z.object({\\n  kind: z.literal(\\\"currency\\\"),\\n  currency: z.string().min(1),\\n  decimals: z.number().int().min(0).optional(),\\n});\\n\\nconst PercentFormatSchema = z.object({\\n  kind: z.literal(\\\"percent\\\"),\\n  decimals: z.number().int().min(0).optional(),\\n  basis: z.enum([\\\"fraction\\\", \\\"unit\\\"]).optional(),\\n});\\n\\nexport const StatFormatSchema = z.discriminatedUnion(\\\"kind\\\", [\\n  TextFormatSchema,\\n  NumberFormatSchema,\\n  CurrencyFormatSchema,\\n  PercentFormatSchema,\\n]);\\n\\nexport type StatFormat = z.infer<typeof StatFormatSchema>;\\n\\nexport const StatDiffSchema = z.object({\\n  value: z.number(),\\n  decimals: z.number().int().min(0).optional(),\\n  upIsPositive: z.boolean().optional(),\\n  label: z.string().optional(),\\n});\\n\\nexport type StatDiff = z.infer<typeof StatDiffSchema>;\\n\\nexport const StatSparklineSchema = z.object({\\n  data: z.array(z.number()).min(2),\\n  color: z.string().optional(),\\n});\\n\\nexport type StatSparkline = z.infer<typeof StatSparklineSchema>;\\n\\nexport const StatItemSchema = z.object({\\n  key: z.string().min(1),\\n  label: z.string().min(1),\\n  value: z.union([z.string(), z.number()]),\\n  format: StatFormatSchema.optional(),\\n  diff: StatDiffSchema.optional(),\\n  sparkline: StatSparklineSchema.optional(),\\n});\\n\\nexport type StatItem = z.infer<typeof StatItemSchema>;\\n\\nexport const SerializableStatsDisplaySchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  title: z.string().optional(),\\n  description: z.string().optional(),\\n  stats: z.array(StatItemSchema).min(1),\\n});\\n\\nexport type SerializableStatsDisplay = z.infer<\\n  typeof SerializableStatsDisplaySchema\\n>;\\n\\nconst SerializableStatsDisplaySchemaContract = defineToolUiContract(\\n  \\\"StatsDisplay\\\",\\n  SerializableStatsDisplaySchema,\\n);\\n\\nexport const parseSerializableStatsDisplay: (\\n  input: unknown,\\n) => SerializableStatsDisplay = SerializableStatsDisplaySchemaContract.parse;\\n\\nexport const safeParseSerializableStatsDisplay: (\\n  input: unknown,\\n) => SerializableStatsDisplay | null =\\n  SerializableStatsDisplaySchemaContract.safeParse;\\nexport interface StatsDisplayProps extends SerializableStatsDisplay {\\n  className?: string;\\n  locale?: string;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/stats-display/sparkline.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/stats-display/sparkline.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport type { CSSProperties } from \\\"react\\\";\\nimport { useId } from \\\"react\\\";\\nimport { cn } from \\\"./_adapter\\\";\\n\\nexport interface SparklineProps {\\n  data: number[];\\n  color?: string;\\n  width?: number;\\n  height?: number;\\n  className?: string;\\n  style?: CSSProperties;\\n  showFill?: boolean;\\n  fillOpacity?: number;\\n}\\n\\nexport function Sparkline({\\n  data,\\n  color = \\\"currentColor\\\",\\n  width = 64,\\n  height = 24,\\n  className,\\n  style,\\n  showFill = false,\\n  fillOpacity = 0.09,\\n}: SparklineProps) {\\n  const gradientId = useId();\\n\\n  if (data.length < 2) {\\n    return null;\\n  }\\n\\n  const minVal = Math.min(...data);\\n  const maxVal = Math.max(...data);\\n  const range = maxVal - minVal || 1;\\n\\n  const padding = 0;\\n  const usableWidth = width;\\n  const usableHeight = height;\\n\\n  const linePoints = data.map((value, index) => {\\n    const x = padding + (index / (data.length - 1)) * usableWidth;\\n    const y =\\n      padding + usableHeight - ((value - minVal) / range) * usableHeight;\\n    return { x, y };\\n  });\\n\\n  const linePointsString = linePoints.map((p) => `${p.x},${p.y}`).join(\\\" \\\");\\n\\n  const areaPointsString = [\\n    `${padding},${height}`,\\n    ...linePoints.map((p) => `${p.x},${p.y}`),\\n    `${width - padding},${height}`,\\n  ].join(\\\" \\\");\\n\\n  const animationDelay = style?.animationDelay ?? \\\"0ms\\\";\\n  const baseAnimationDelay =\\n    typeof animationDelay === \\\"number\\\" ? `${animationDelay}ms` : animationDelay;\\n  const secondaryAnimationDelay = `calc(${baseAnimationDelay} + 100ms)`;\\n\\n  return (\\n    <svg\\n      viewBox={`0 0 ${width} ${height}`}\\n      aria-hidden=\\\"true\\\"\\n      className={cn(\\\"h-full w-full shrink-0\\\", className)}\\n      style={style}\\n      preserveAspectRatio=\\\"none\\\"\\n    >\\n      {showFill && (\\n        <>\\n          <defs>\\n            <linearGradient id={gradientId} x1=\\\"0\\\" y1=\\\"0\\\" x2=\\\"0\\\" y2=\\\"1\\\">\\n              <stop offset=\\\"0%\\\" stopColor={color} stopOpacity={fillOpacity} />\\n              <stop offset=\\\"100%\\\" stopColor={color} stopOpacity={0} />\\n            </linearGradient>\\n          </defs>\\n          <polygon\\n            points={areaPointsString}\\n            fill={`url(#${gradientId})`}\\n            className=\\\"animate-in fade-in duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] fill-mode-both\\\"\\n            style={{ animationDelay }}\\n          />\\n        </>\\n      )}\\n      <polyline\\n        points={linePointsString}\\n        fill=\\\"none\\\"\\n        stroke={color}\\n        strokeWidth={1}\\n        strokeOpacity={0.15}\\n        strokeLinecap=\\\"round\\\"\\n        strokeLinejoin=\\\"round\\\"\\n        vectorEffect=\\\"non-scaling-stroke\\\"\\n      />\\n      <polyline\\n        points={linePointsString}\\n        fill=\\\"none\\\"\\n        stroke={color}\\n        strokeWidth={0.75}\\n        strokeLinecap=\\\"round\\\"\\n        strokeLinejoin=\\\"round\\\"\\n        vectorEffect=\\\"non-scaling-stroke\\\"\\n        pathLength={1}\\n        strokeDasharray=\\\"0.36 0.64\\\"\\n        strokeDashoffset={0}\\n        strokeOpacity={0.2}\\n        className=\\\"opacity-0 motion-safe:animate-in motion-safe:fade-in motion-safe:duration-700 motion-safe:ease-out motion-safe:fill-mode-both\\\"\\n        style={{ animationDelay: baseAnimationDelay }}\\n      />\\n      <polyline\\n        points={linePointsString}\\n        fill=\\\"none\\\"\\n        stroke={color}\\n        strokeWidth={0.75}\\n        strokeLinecap=\\\"round\\\"\\n        strokeLinejoin=\\\"round\\\"\\n        vectorEffect=\\\"non-scaling-stroke\\\"\\n        pathLength={1}\\n        strokeDasharray=\\\"0.24 0.76\\\"\\n        strokeDashoffset={0}\\n        strokeOpacity={0.65}\\n        className=\\\"opacity-0 motion-safe:animate-in motion-safe:fade-in motion-safe:duration-500 motion-safe:ease-out motion-safe:fill-mode-both\\\"\\n        style={{ animationDelay: secondaryAnimationDelay }}\\n      />\\n    </svg>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/stats-display/stats-display.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/stats-display/stats-display.tsx\",\n      \"content\": \"\\\"use client\\\";\\nimport {\\n  cn,\\n  Card,\\n  CardHeader,\\n  CardTitle,\\n  CardDescription,\\n  CardContent,\\n} from \\\"./_adapter\\\";\\nimport type {\\n  StatsDisplayProps,\\n  StatItem,\\n  StatFormat,\\n  StatDiff,\\n} from \\\"./schema\\\";\\nimport { Sparkline } from \\\"./sparkline\\\";\\n\\ninterface FormattedValueProps {\\n  value: string | number;\\n  format?: StatFormat;\\n  locale?: string;\\n}\\n\\nfunction FormattedValue({ value, format, locale }: FormattedValueProps) {\\n  if (typeof value === \\\"string\\\" || !format) {\\n    return <span className=\\\"font-light tabular-nums\\\">{String(value)}</span>;\\n  }\\n\\n  switch (format.kind) {\\n    case \\\"number\\\": {\\n      const decimals = format.decimals ?? 0;\\n      if (format.compact) {\\n        const parts = new Intl.NumberFormat(locale, {\\n          minimumFractionDigits: decimals,\\n          maximumFractionDigits: decimals,\\n          notation: \\\"compact\\\",\\n        }).formatToParts(value);\\n        const fullNumber = new Intl.NumberFormat(locale).format(value);\\n        return (\\n          <span className=\\\"font-light tabular-nums\\\" aria-label={fullNumber}>\\n            {parts.map((part, i) =>\\n              part.type === \\\"compact\\\" ? (\\n                <span\\n                  key={i}\\n                  className=\\\"ml-0.5 text-[0.65em] opacity-80\\\"\\n                  aria-hidden=\\\"true\\\"\\n                >\\n                  {part.value}\\n                </span>\\n              ) : (\\n                <span key={i}>{part.value}</span>\\n              ),\\n            )}\\n          </span>\\n        );\\n      }\\n      const formatted = new Intl.NumberFormat(locale, {\\n        minimumFractionDigits: decimals,\\n        maximumFractionDigits: decimals,\\n      }).format(value);\\n      return <span className=\\\"font-light tabular-nums\\\">{formatted}</span>;\\n    }\\n    case \\\"currency\\\": {\\n      const currency = format.currency;\\n      const decimals = format.decimals ?? 2;\\n      const formatted = new Intl.NumberFormat(locale, {\\n        style: \\\"currency\\\",\\n        currency,\\n        minimumFractionDigits: decimals,\\n        maximumFractionDigits: decimals,\\n      }).format(value);\\n      const spokenValue = new Intl.NumberFormat(locale, {\\n        style: \\\"currency\\\",\\n        currency,\\n        currencyDisplay: \\\"name\\\",\\n        minimumFractionDigits: decimals,\\n        maximumFractionDigits: decimals,\\n      }).format(value);\\n      return (\\n        <span className=\\\"font-light tabular-nums\\\" aria-label={spokenValue}>\\n          {formatted}\\n        </span>\\n      );\\n    }\\n    case \\\"percent\\\": {\\n      const decimals = format.decimals ?? 2;\\n      const basis = format.basis ?? \\\"fraction\\\";\\n      const numeric = basis === \\\"fraction\\\" ? value * 100 : value;\\n      const formatted = numeric.toFixed(decimals);\\n      return (\\n        <span\\n          className=\\\"font-light tabular-nums\\\"\\n          aria-label={`${formatted} percent`}\\n        >\\n          {formatted}\\n          <span className=\\\"ml-0.5 text-[0.65em] opacity-80\\\" aria-hidden=\\\"true\\\">\\n            %\\n          </span>\\n        </span>\\n      );\\n    }\\n    case \\\"text\\\":\\n    default:\\n      return <span className=\\\"font-light tabular-nums\\\">{String(value)}</span>;\\n  }\\n}\\n\\ninterface DeltaValueProps {\\n  diff: StatDiff;\\n}\\n\\nfunction DeltaValue({ diff }: DeltaValueProps) {\\n  const { value, decimals = 1, upIsPositive = true, label } = diff;\\n\\n  const isPositive = value > 0;\\n  const isNegative = value < 0;\\n\\n  const isGood = upIsPositive ? isPositive : isNegative;\\n  const isBad = upIsPositive ? isNegative : isPositive;\\n\\n  const colorClass = isGood\\n    ? \\\"text-green-600 dark:text-green-400\\\"\\n    : isBad\\n      ? \\\"text-red-600 dark:text-red-500\\\"\\n      : \\\"text-muted-foreground\\\";\\n\\n  const bgClass = isGood\\n    ? \\\"bg-green-500/10 dark:bg-green-600/15\\\"\\n    : isBad\\n      ? \\\"bg-red-500/10 dark:bg-red-500/15\\\"\\n      : \\\"bg-muted\\\";\\n\\n  const formatted = Math.abs(value).toFixed(decimals);\\n  const sign = isNegative ? \\\"−\\\" : \\\"+\\\";\\n  const display = `${sign}${formatted}%`;\\n\\n  return (\\n    <span\\n      className={cn(\\n        \\\"inline-flex items-center gap-1 rounded-full px-1.5 py-0.5 text-xs  tabular-nums\\\",\\n        colorClass,\\n        bgClass,\\n      )}\\n    >\\n      {!upIsPositive && (\\n        <span className=\\\"text-[0.9em]\\\">{isGood ? \\\"↓\\\" : \\\"↑\\\"}</span>\\n      )}\\n      {display}\\n      {label && (\\n        <span className=\\\"text-muted-foreground font-normal\\\">{label}</span>\\n      )}\\n    </span>\\n  );\\n}\\n\\ninterface StatCardProps {\\n  stat: StatItem;\\n  locale?: string;\\n  isSingle?: boolean;\\n  index?: number;\\n}\\n\\nfunction StatCard({\\n  stat,\\n  locale,\\n  isSingle = false,\\n  index = 0,\\n}: StatCardProps) {\\n  const sparklineColor = stat.sparkline?.color ?? \\\"var(--muted-foreground)\\\";\\n  const hasSparkline = Boolean(stat.sparkline);\\n  const baseDelay = index * 175;\\n\\n  return (\\n    <div\\n      className={cn(\\n        \\\"relative flex min-h-28 flex-col gap-1 px-6\\\",\\n        isSingle ? \\\"justify-center\\\" : \\\"justify-end\\\",\\n      )}\\n    >\\n      {hasSparkline && (\\n        <Sparkline\\n          data={stat.sparkline!.data}\\n          color={sparklineColor}\\n          showFill\\n          fillOpacity={0.09}\\n          className=\\\"pointer-events-none absolute inset-x-0 top-2 bottom-2 animate-in fade-in slide-in-from-bottom-12 duration-500 ease-[cubic-bezier(0.16,1,0.3,1)] fill-mode-both\\\"\\n          style={{ animationDelay: `${baseDelay}ms` }}\\n        />\\n      )}\\n      <span\\n        className=\\\"text-muted-foreground relative text-xs font-normal tracking-wider uppercase opacity-90 animate-in fade-in slide-in-from-bottom-1 duration-500 ease-[cubic-bezier(0.16,1,0.3,1)] fill-mode-both\\\"\\n        style={{ animationDelay: `${baseDelay + 75}ms` }}\\n      >\\n        {stat.label}\\n      </span>\\n      <div\\n        className=\\\"relative flex items-baseline gap-2 pb-2 animate-in fade-in slide-in-from-bottom-2 duration-500 ease-[cubic-bezier(0.16,1,0.3,1)] fill-mode-both\\\"\\n        style={{ animationDelay: `${baseDelay + 150}ms` }}\\n      >\\n        <span\\n          className={cn(\\n            \\\"font-light tracking-normal\\\",\\n            isSingle ? \\\"text-5xl\\\" : \\\"text-3xl\\\",\\n          )}\\n        >\\n          <FormattedValue\\n            value={stat.value}\\n            format={stat.format}\\n            locale={locale}\\n          />\\n        </span>\\n        {stat.diff && <DeltaValue diff={stat.diff} />}\\n      </div>\\n    </div>\\n  );\\n}\\n\\nexport function StatsDisplay({\\n  id,\\n  title,\\n  description,\\n  stats,\\n  className,\\n  locale: localeProp,\\n}: StatsDisplayProps) {\\n  const locale =\\n    localeProp ??\\n    (typeof navigator !== \\\"undefined\\\" ? navigator.language : undefined);\\n  const hasHeader = Boolean(title || description);\\n  const isSingle = stats.length === 1;\\n\\n  return (\\n    <article\\n      data-slot=\\\"stats-display\\\"\\n      data-tool-ui-id={id}\\n      className={cn(\\n        \\\"w-full min-w-80 max-w-xl\\\",\\n        isSingle && \\\"max-w-sm\\\",\\n        className,\\n      )}\\n    >\\n      <Card className={cn(\\\"overflow-clip !pb-0 !pt-2\\\", hasHeader && \\\"!gap-0\\\")}>\\n        {hasHeader && (\\n          <CardHeader className=\\\"border-b border-border !pt-3 !pb-4\\\">\\n            {title && <CardTitle className=\\\"text-pretty\\\">{title}</CardTitle>}\\n            {description && (\\n              <CardDescription className=\\\"text-pretty\\\">\\n                {description}\\n              </CardDescription>\\n            )}\\n          </CardHeader>\\n        )}\\n        <CardContent className=\\\"@container overflow-hidden p-0\\\">\\n          <div\\n            className=\\\"grid @[440px]:-ml-px @[440px]:-mt-px\\\"\\n            style={{\\n              gridTemplateColumns: \\\"repeat(auto-fit, minmax(220px, 1fr))\\\",\\n            }}\\n          >\\n            {stats.map((stat, index) => (\\n              <div\\n                key={stat.key}\\n                className={cn(\\n                  \\\"overflow-clip py-3 first:pt-0 @[440px]:py-3 @[440px]:first:pt-3 @[440px]:border-l @[440px]:border-t @[440px]:border-border\\\",\\n                  index > 0 && \\\"border-border border-t\\\",\\n                )}\\n              >\\n                <StatCard\\n                  stat={stat}\\n                  locale={locale}\\n                  isSingle={isSingle}\\n                  index={index}\\n                />\\n              </div>\\n            ))}\\n          </div>\\n        </CardContent>\\n      </Card>\\n    </article>\\n  );\\n}\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/terminal.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"terminal\",\n  \"type\": \"registry:block\",\n  \"title\": \"Terminal\",\n  \"description\": \"Show command-line output and logs.\",\n  \"dependencies\": [\n    \"ansi-to-react\",\n    \"lucide-react\",\n    \"zod\"\n  ],\n  \"registryDependencies\": [\n    \"button\",\n    \"collapsible\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { ReactNode } from \\\"react\\\";\\n\\n/**\\n * Tool UI conventions:\\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\\n * - Schema: `SerializableXSchema`\\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\\n * - Root attrs: `data-tool-ui-id` + `data-slot`\\n */\\n\\n/**\\n * Schema for tool UI identity.\\n *\\n * Every tool UI should have a unique identifier that:\\n * - Is stable across re-renders\\n * - Is meaningful (not auto-generated)\\n * - Is unique within the conversation\\n *\\n * Format recommendation: `{component-type}-{semantic-identifier}`\\n * Examples: \\\"data-table-expenses-q3\\\", \\\"option-list-deploy-target\\\"\\n */\\nexport const ToolUIIdSchema = z.string().min(1);\\n\\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\\n\\n/**\\n * Primary role of a Tool UI surface in a chat context.\\n */\\nexport const ToolUIRoleSchema = z.enum([\\n  \\\"information\\\",\\n  \\\"decision\\\",\\n  \\\"control\\\",\\n  \\\"state\\\",\\n  \\\"composite\\\",\\n]);\\n\\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\\n\\nexport const ToolUIReceiptOutcomeSchema = z.enum([\\n  \\\"success\\\",\\n  \\\"partial\\\",\\n  \\\"failed\\\",\\n  \\\"cancelled\\\",\\n]);\\n\\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\\n\\n/**\\n * Optional receipt metadata: a durable summary of an outcome.\\n */\\nexport const ToolUIReceiptSchema = z.object({\\n  outcome: ToolUIReceiptOutcomeSchema,\\n  summary: z.string().min(1),\\n  identifiers: z.record(z.string(), z.string()).optional(),\\n  at: z.string().datetime(),\\n});\\n\\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\\n\\n/**\\n * Base schema for Tool UI payloads (id + optional role/receipt).\\n */\\nexport const ToolUISurfaceSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n});\\n\\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\\n\\nexport const ActionSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  /**\\n   * Canonical narration the assistant can use after this action is taken.\\n   *\\n   * Example: \\\"I exported the table as CSV.\\\" / \\\"I opened the link in a new tab.\\\"\\n   */\\n  sentence: z.string().optional(),\\n  confirmLabel: z.string().optional(),\\n  variant: z\\n    .enum([\\\"default\\\", \\\"destructive\\\", \\\"secondary\\\", \\\"ghost\\\", \\\"outline\\\"])\\n    .optional(),\\n  icon: z.custom<ReactNode>().optional(),\\n  loading: z.boolean().optional(),\\n  disabled: z.boolean().optional(),\\n  shortcut: z.string().optional(),\\n});\\n\\nexport type Action = z.infer<typeof ActionSchema>;\\nexport type LocalAction = Action;\\nexport type DecisionAction = Action;\\n\\nexport const DecisionResultSchema = z.object({\\n  kind: z.literal(\\\"decision\\\"),\\n  version: z.literal(1),\\n  decisionId: z.string().min(1),\\n  actionId: z.string().min(1),\\n  actionLabel: z.string().min(1),\\n  at: z.string().datetime(),\\n  payload: z.record(z.string(), z.unknown()).optional(),\\n});\\n\\nexport type DecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n> = Omit<z.infer<typeof DecisionResultSchema>, \\\"payload\\\"> & {\\n  payload?: TPayload;\\n};\\n\\nexport function createDecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n>(args: {\\n  decisionId: string;\\n  action: { id: string; label: string };\\n  payload?: TPayload;\\n}): DecisionResult<TPayload> {\\n  return {\\n    kind: \\\"decision\\\",\\n    version: 1,\\n    decisionId: args.decisionId,\\n    actionId: args.action.id,\\n    actionLabel: args.action.label,\\n    at: new Date().toISOString(),\\n    payload: args.payload,\\n  };\\n}\\n\\nexport const ActionButtonsPropsSchema = z.object({\\n  actions: z.array(ActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n  className: z.string().optional(),\\n});\\n\\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\\n  actions: z.array(SerializableActionSchema),\\n}).omit({ className: true });\\n\\nexport interface ActionsConfig {\\n  items: Action[];\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  confirmTimeout?: number;\\n}\\n\\nexport const SerializableActionsConfigSchema = z.object({\\n  items: z.array(SerializableActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n});\\n\\nexport type SerializableActionsConfig = z.infer<\\n  typeof SerializableActionsConfigSchema\\n>;\\n\\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/use-copy-to-clipboard.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/use-copy-to-clipboard.ts\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { useCallback, useEffect, useState } from \\\"react\\\";\\n\\nfunction fallbackCopyToClipboard(text: string): boolean {\\n  const textArea = document.createElement(\\\"textarea\\\");\\n  try {\\n    textArea.value = text;\\n    textArea.setAttribute(\\\"readonly\\\", \\\"\\\");\\n    textArea.style.position = \\\"fixed\\\";\\n    textArea.style.top = \\\"-9999px\\\";\\n    textArea.style.left = \\\"-9999px\\\";\\n    document.body.appendChild(textArea);\\n    textArea.select();\\n    return document.execCommand(\\\"copy\\\");\\n  } catch {\\n    return false;\\n  } finally {\\n    if (textArea.parentNode) {\\n      textArea.parentNode.removeChild(textArea);\\n    }\\n  }\\n}\\n\\nexport function useCopyToClipboard(options?: { resetAfterMs?: number }): {\\n  copiedId: string | null;\\n  copy: (text: string, id?: string) => Promise<boolean>;\\n} {\\n  const resetAfterMs = options?.resetAfterMs ?? 2000;\\n  const [copiedId, setCopiedId] = useState<string | null>(null);\\n\\n  const copy = useCallback(async (text: string, id: string = \\\"default\\\") => {\\n    let ok = false;\\n    try {\\n      if (navigator.clipboard?.writeText) {\\n        await navigator.clipboard.writeText(text);\\n        ok = true;\\n      } else {\\n        ok = fallbackCopyToClipboard(text);\\n      }\\n    } catch {\\n      ok = fallbackCopyToClipboard(text);\\n    }\\n\\n    if (ok) {\\n      setCopiedId(id);\\n    }\\n\\n    return ok;\\n  }, []);\\n\\n  useEffect(() => {\\n    if (!copiedId) return;\\n    const timeout = setTimeout(() => setCopiedId(null), resetAfterMs);\\n    return () => clearTimeout(timeout);\\n  }, [copiedId, resetAfterMs]);\\n\\n  return { copiedId, copy };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/terminal/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/terminal/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn          → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n *   Button      → shadcn/ui Button\\n *   Collapsible → shadcn/ui Collapsible\\n */\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport { Button } from \\\"@/components/ui/button\\\";\\nexport { Collapsible, CollapsibleTrigger } from \\\"@/components/ui/collapsible\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/terminal/index.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/terminal/index.tsx\",\n      \"content\": \"export { Terminal } from \\\"./terminal\\\";\\nexport type { TerminalProps, SerializableTerminal } from \\\"./schema\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/terminal/README.md\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/terminal/README.md\",\n      \"content\": \"# Terminal\\n\\nImplementation for the \\\"terminal\\\" Tool UI surface.\\n\\n## Files\\n\\n- public exports: components/tool-ui/terminal/index.tsx\\n- serializable schema + parse helpers: components/tool-ui/terminal/schema.ts\\n\\n## Companion assets\\n\\n- Docs page: app/docs/terminal/content.mdx\\n- Preset payload: lib/presets/terminal.ts\\n\\n## Quick check\\n\\nRun this after edits:\\n\\npnpm test\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/terminal/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/terminal/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\nimport {\\n  ToolUIIdSchema,\\n  ToolUIReceiptSchema,\\n  ToolUIRoleSchema,\\n} from \\\"../shared/schema\\\";\\n\\nexport const TerminalPropsSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n  command: z.string(),\\n  stdout: z.string().optional(),\\n  stderr: z.string().optional(),\\n  exitCode: z.number().int().min(0),\\n  durationMs: z.number().optional(),\\n  cwd: z.string().optional(),\\n  truncated: z.boolean().optional(),\\n  maxCollapsedLines: z.number().min(1).optional(),\\n  className: z.string().optional(),\\n});\\n\\nexport type TerminalProps = z.infer<typeof TerminalPropsSchema>;\\n\\nexport const SerializableTerminalSchema = TerminalPropsSchema.omit({\\n  className: true,\\n});\\n\\nexport type SerializableTerminal = z.infer<typeof SerializableTerminalSchema>;\\n\\nconst SerializableTerminalSchemaContract = defineToolUiContract(\\n  \\\"Terminal\\\",\\n  SerializableTerminalSchema,\\n);\\n\\nexport const parseSerializableTerminal: (\\n  input: unknown,\\n) => SerializableTerminal = SerializableTerminalSchemaContract.parse;\\n\\nexport const safeParseSerializableTerminal: (\\n  input: unknown,\\n) => SerializableTerminal | null = SerializableTerminalSchemaContract.safeParse;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/terminal/terminal.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/terminal/terminal.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { useState, useCallback } from \\\"react\\\";\\nimport Ansi from \\\"ansi-to-react\\\";\\nimport {\\n  Copy,\\n  Check,\\n  ChevronDown,\\n  ChevronUp,\\n  Terminal as TerminalIcon,\\n} from \\\"lucide-react\\\";\\nimport type { TerminalProps } from \\\"./schema\\\";\\nimport { useCopyToClipboard } from \\\"../shared/use-copy-to-clipboard\\\";\\n\\nimport { Button, Collapsible, CollapsibleTrigger } from \\\"./_adapter\\\";\\nimport { cn } from \\\"./_adapter\\\";\\n\\nconst COPY_ID = \\\"terminal-output\\\";\\n\\ntype TerminalControlledProps = {\\n  expanded?: boolean;\\n  defaultExpanded?: boolean;\\n  onExpandedChange?: (expanded: boolean) => void;\\n};\\n\\ntype TerminalRootProps = TerminalProps & TerminalControlledProps;\\n\\ntype TerminalHeaderProps = Pick<\\n  TerminalProps,\\n  \\\"command\\\" | \\\"cwd\\\" | \\\"exitCode\\\"\\n> & {\\n  formattedDuration: string | null;\\n  hasOutput: boolean;\\n  copiedId: string | null;\\n  onCopy: () => void;\\n};\\n\\ntype TerminalOutputProps = Pick<\\n  TerminalProps,\\n  \\\"stdout\\\" | \\\"stderr\\\" | \\\"truncated\\\"\\n> & {\\n  isCollapsed: boolean;\\n  shouldCollapse: boolean;\\n  lineCount: number;\\n  onToggleCollapse: () => void;\\n};\\n\\nfunction formatDuration(durationMs?: number): string | null {\\n  if (durationMs == null) return null;\\n  if (durationMs < 1000) return `${Math.round(durationMs)}ms`;\\n  return `${(durationMs / 1000).toFixed(1)}s`;\\n}\\n\\nfunction countOutputLines(output: string): number {\\n  const trimmedTrailingNewlines = output.replace(/\\\\n+$/, \\\"\\\");\\n  if (!trimmedTrailingNewlines) return 0;\\n  return trimmedTrailingNewlines.split(\\\"\\\\n\\\").length;\\n}\\n\\nfunction TerminalHeader({\\n  command,\\n  cwd,\\n  exitCode,\\n  formattedDuration,\\n  hasOutput,\\n  copiedId,\\n  onCopy,\\n}: TerminalHeaderProps) {\\n  return (\\n    <div className=\\\"bg-card flex items-center justify-between border-b px-4 py-2\\\">\\n      <div className=\\\"flex items-center gap-2 overflow-hidden\\\">\\n        <TerminalIcon className=\\\"text-muted-foreground h-4 w-4 shrink-0\\\" />\\n        <code className=\\\"text-foreground truncate font-mono text-xs\\\">\\n          {cwd && <span className=\\\"text-muted-foreground\\\">{cwd}$ </span>}\\n          {command}\\n        </code>\\n      </div>\\n      <div className=\\\"flex items-center gap-3\\\">\\n        {formattedDuration && (\\n          <span className=\\\"text-muted-foreground font-mono text-sm tabular-nums\\\">\\n            {formattedDuration}\\n          </span>\\n        )}\\n        <span\\n          className={cn(\\n            \\\"font-mono text-sm tabular-nums\\\",\\n            exitCode === 0\\n              ? \\\"text-muted-foreground\\\"\\n              : \\\"text-red-600 dark:text-red-400\\\",\\n          )}\\n        >\\n          {exitCode}\\n        </span>\\n        <Button\\n          variant=\\\"ghost\\\"\\n          size=\\\"sm\\\"\\n          onClick={onCopy}\\n          disabled={!hasOutput}\\n          className=\\\"h-7 w-7 p-0\\\"\\n          aria-label={\\n            !hasOutput\\n              ? \\\"No output to copy\\\"\\n              : copiedId === COPY_ID\\n                ? \\\"Copied\\\"\\n                : \\\"Copy output\\\"\\n          }\\n        >\\n          {hasOutput && copiedId === COPY_ID ? (\\n            <Check className=\\\"h-4 w-4 text-green-700 dark:text-green-400\\\" />\\n          ) : (\\n            <Copy className=\\\"text-muted-foreground h-4 w-4\\\" />\\n          )}\\n        </Button>\\n      </div>\\n    </div>\\n  );\\n}\\n\\nfunction TerminalOutput({\\n  stdout,\\n  stderr,\\n  truncated,\\n  isCollapsed,\\n  shouldCollapse,\\n  lineCount,\\n  onToggleCollapse,\\n}: TerminalOutputProps) {\\n  return (\\n    <Collapsible open={!isCollapsed}>\\n      <div\\n        className={cn(\\n          \\\"relative font-mono text-sm\\\",\\n          isCollapsed && \\\"max-h-[200px] overflow-hidden\\\",\\n        )}\\n      >\\n        <div className=\\\"overflow-x-auto p-4\\\">\\n          {stdout && (\\n            <div className=\\\"text-foreground whitespace-pre\\\">\\n              <Ansi>{stdout}</Ansi>\\n            </div>\\n          )}\\n          {stderr && (\\n            <div className=\\\"mt-2 whitespace-pre text-red-500 dark:text-red-400\\\">\\n              <Ansi>{stderr}</Ansi>\\n            </div>\\n          )}\\n          {truncated && (\\n            <div className=\\\"text-muted-foreground mt-2 text-xs italic\\\">\\n              Output truncated...\\n            </div>\\n          )}\\n        </div>\\n\\n        {isCollapsed && (\\n          <div className=\\\"from-card absolute inset-x-0 bottom-0 h-16 bg-gradient-to-t to-transparent\\\" />\\n        )}\\n      </div>\\n\\n      {shouldCollapse && (\\n        <CollapsibleTrigger asChild>\\n          <Button\\n            variant=\\\"ghost\\\"\\n            onClick={onToggleCollapse}\\n            className=\\\"text-muted-foreground w-full rounded-none border-t font-normal\\\"\\n          >\\n            {isCollapsed ? (\\n              <>\\n                <ChevronDown className=\\\"mr-1 size-4\\\" />\\n                Show all {lineCount} lines\\n              </>\\n            ) : (\\n              <>\\n                <ChevronUp className=\\\"mr-1 size-4\\\" />\\n                Collapse\\n              </>\\n            )}\\n          </Button>\\n        </CollapsibleTrigger>\\n      )}\\n    </Collapsible>\\n  );\\n}\\n\\nfunction TerminalEmpty() {\\n  return (\\n    <div className=\\\"text-muted-foreground px-4 py-3 font-mono text-sm italic\\\">\\n      No output\\n    </div>\\n  );\\n}\\n\\nfunction TerminalRoot({\\n  id,\\n  command,\\n  stdout,\\n  stderr,\\n  exitCode,\\n  durationMs,\\n  cwd,\\n  truncated,\\n  maxCollapsedLines,\\n  className,\\n  expanded,\\n  defaultExpanded = false,\\n  onExpandedChange,\\n}: TerminalRootProps) {\\n  const [uncontrolledExpanded, setUncontrolledExpanded] =\\n    useState(defaultExpanded);\\n  const { copiedId, copy } = useCopyToClipboard();\\n\\n  const isExpanded = expanded ?? uncontrolledExpanded;\\n  const hasOutput = Boolean(stdout || stderr);\\n  const fullOutput = [stdout, stderr].filter(Boolean).join(\\\"\\\\n\\\");\\n  const formattedDuration = formatDuration(durationMs);\\n  const lineCount = countOutputLines(fullOutput);\\n  const shouldCollapse =\\n    maxCollapsedLines !== undefined && lineCount > maxCollapsedLines;\\n  const isCollapsed = shouldCollapse && !isExpanded;\\n\\n  const setExpanded = useCallback(\\n    (nextExpanded: boolean) => {\\n      if (expanded === undefined) {\\n        setUncontrolledExpanded(nextExpanded);\\n      }\\n      onExpandedChange?.(nextExpanded);\\n    },\\n    [expanded, onExpandedChange],\\n  );\\n\\n  const handleCopy = useCallback(() => {\\n    if (!hasOutput) return;\\n    copy(fullOutput, COPY_ID);\\n  }, [hasOutput, fullOutput, copy]);\\n\\n  return (\\n    <div\\n      className={cn(\\n        \\\"@container flex w-full min-w-80 flex-col gap-3\\\",\\n        className,\\n      )}\\n      data-tool-ui-id={id}\\n      data-slot=\\\"terminal\\\"\\n    >\\n      <div className=\\\"border-border bg-card overflow-hidden rounded-lg border shadow-xs\\\">\\n        <TerminalHeader\\n          command={command}\\n          cwd={cwd}\\n          exitCode={exitCode}\\n          formattedDuration={formattedDuration}\\n          hasOutput={hasOutput}\\n          copiedId={copiedId}\\n          onCopy={handleCopy}\\n        />\\n\\n        {hasOutput && (\\n          <TerminalOutput\\n            stdout={stdout}\\n            stderr={stderr}\\n            truncated={truncated}\\n            isCollapsed={isCollapsed}\\n            shouldCollapse={shouldCollapse}\\n            lineCount={lineCount}\\n            onToggleCollapse={() => setExpanded(!isExpanded)}\\n          />\\n        )}\\n\\n        {!hasOutput && <TerminalEmpty />}\\n      </div>\\n    </div>\\n  );\\n}\\n\\ntype TerminalComponent = typeof TerminalRoot & {\\n  Header: typeof TerminalHeader;\\n  Output: typeof TerminalOutput;\\n  Empty: typeof TerminalEmpty;\\n};\\n\\nexport const Terminal = Object.assign(TerminalRoot, {\\n  Header: TerminalHeader,\\n  Output: TerminalOutput,\\n  Empty: TerminalEmpty,\\n}) as TerminalComponent;\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/video.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"video\",\n  \"type\": \"registry:block\",\n  \"title\": \"Video\",\n  \"description\": \"Video playback with controls and poster.\",\n  \"dependencies\": [\n    \"lucide-react\",\n    \"zod\"\n  ],\n  \"registryDependencies\": [\n    \"button\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/aspect-ratio.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/aspect-ratio.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nexport const AspectRatioSchema = z\\n  .enum([\\\"auto\\\", \\\"1:1\\\", \\\"4:3\\\", \\\"16:9\\\", \\\"9:16\\\"])\\n  .default(\\\"auto\\\");\\n\\nexport type AspectRatio = z.infer<typeof AspectRatioSchema>;\\n\\nexport const MediaFitSchema = z.enum([\\\"cover\\\", \\\"contain\\\"]).default(\\\"cover\\\");\\n\\nexport type MediaFit = z.infer<typeof MediaFitSchema>;\\n\\nexport const RATIO_CLASS_MAP: Record<AspectRatio, string> = {\\n  auto: \\\"\\\",\\n  \\\"1:1\\\": \\\"aspect-square\\\",\\n  \\\"4:3\\\": \\\"aspect-[4/3]\\\",\\n  \\\"16:9\\\": \\\"aspect-video\\\",\\n  \\\"9:16\\\": \\\"aspect-[9/16]\\\",\\n};\\n\\nexport function getRatioClass(ratio: AspectRatio): string {\\n  return RATIO_CLASS_MAP[ratio];\\n}\\n\\nexport function getFitClass(fit: MediaFit): string {\\n  return fit === \\\"cover\\\" ? \\\"object-cover\\\" : \\\"object-contain\\\";\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/format-utils.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/format-utils.ts\",\n      \"content\": \"/**\\n * Format duration in milliseconds to human-readable string.\\n * @example formatDuration(128000) => \\\"2:08\\\"\\n * @example formatDuration(3661000) => \\\"1:01:01\\\"\\n */\\nexport function formatDuration(durationMs: number): string {\\n  const totalSeconds = Math.round(durationMs / 1000);\\n  const hours = Math.floor(totalSeconds / 3600);\\n  const minutes = Math.floor((totalSeconds % 3600) / 60);\\n  const seconds = totalSeconds % 60;\\n\\n  if (hours > 0) {\\n    return `${hours}:${minutes.toString().padStart(2, \\\"0\\\")}:${seconds\\n      .toString()\\n      .padStart(2, \\\"0\\\")}`;\\n  }\\n  return `${minutes}:${seconds.toString().padStart(2, \\\"0\\\")}`;\\n}\\n\\n/**\\n * Format file size in bytes to human-readable string.\\n * @example formatFileSize(1024) => \\\"1 KB\\\"\\n * @example formatFileSize(1536000) => \\\"1.5 MB\\\"\\n */\\nexport function formatFileSize(bytes: number): string {\\n  if (bytes < 1024) return `${bytes} B`;\\n  const units = [\\\"KB\\\", \\\"MB\\\", \\\"GB\\\"];\\n  let size = bytes / 1024;\\n  let unit = 0;\\n  while (size >= 1024 && unit < units.length - 1) {\\n    size /= 1024;\\n    unit += 1;\\n  }\\n  return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unit]}`;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/index.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/index.ts\",\n      \"content\": \"export {\\n  AspectRatioSchema,\\n  MediaFitSchema,\\n  RATIO_CLASS_MAP,\\n  getRatioClass,\\n  getFitClass,\\n  type AspectRatio,\\n  type MediaFit,\\n} from \\\"./aspect-ratio\\\";\\n\\nexport { OVERLAY_GRADIENT } from \\\"./overlay-gradient\\\";\\n\\nexport { formatDuration, formatFileSize } from \\\"./format-utils\\\";\\n\\nexport { sanitizeHref } from \\\"./sanitize-href\\\";\\nexport {\\n  resolveSafeNavigationHref,\\n  openSafeNavigationHref,\\n} from \\\"./safe-navigation\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/overlay-gradient.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/overlay-gradient.ts\",\n      \"content\": \"/**\\n * Eased gradient for hover overlays on media elements.\\n * Creates a smooth fade from opaque black at top to transparent.\\n *\\n * @see https://larsenwork.com/easing-gradients/\\n */\\nexport const OVERLAY_GRADIENT = `linear-gradient(\\n  to bottom,\\n  hsl(0, 0%, 0%) 0%,\\n  hsla(0, 0%, 0%, 0.987) 8.3%,\\n  hsla(0, 0%, 0%, 0.951) 16.6%,\\n  hsla(0, 0%, 0%, 0.896) 24.6%,\\n  hsla(0, 0%, 0%, 0.825) 32.5%,\\n  hsla(0, 0%, 0%, 0.741) 40.1%,\\n  hsla(0, 0%, 0%, 0.648) 47.6%,\\n  hsla(0, 0%, 0%, 0.55) 54.8%,\\n  hsla(0, 0%, 0%, 0.45) 61.7%,\\n  hsla(0, 0%, 0%, 0.352) 68.3%,\\n  hsla(0, 0%, 0%, 0.259) 74.5%,\\n  hsla(0, 0%, 0%, 0.175) 80.4%,\\n  hsla(0, 0%, 0%, 0.104) 86%,\\n  hsla(0, 0%, 0%, 0.049) 91.1%,\\n  hsla(0, 0%, 0%, 0.013) 95.8%,\\n  hsla(0, 0%, 0%, 0) 100%\\n)` as const;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/safe-navigation.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/safe-navigation.ts\",\n      \"content\": \"import { sanitizeHref } from \\\"./sanitize-href\\\";\\n\\nexport function resolveSafeNavigationHref(\\n  ...candidates: Array<string | null | undefined>\\n): string | undefined {\\n  for (const candidate of candidates) {\\n    const safeHref = sanitizeHref(candidate ?? undefined);\\n    if (safeHref) {\\n      return safeHref;\\n    }\\n  }\\n\\n  return undefined;\\n}\\n\\nexport function openSafeNavigationHref(href: string | undefined): boolean {\\n  if (!href || typeof window === \\\"undefined\\\") {\\n    return false;\\n  }\\n\\n  window.open(href, \\\"_blank\\\", \\\"noopener,noreferrer\\\");\\n  return true;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/sanitize-href.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/sanitize-href.ts\",\n      \"content\": \"/**\\n * Sanitize a URL to ensure it's safe for use in href attributes.\\n * Allows:\\n * - Absolute http(s) URLs\\n * - Relative URLs (/path, ./path, ../path, ?query, #hash)\\n *\\n * @returns The sanitized URL string, or undefined if invalid/unsafe\\n */\\nexport function sanitizeHref(href?: string): string | undefined {\\n  if (!href) return undefined;\\n  const candidate = href.trim();\\n  if (!candidate) return undefined;\\n\\n  if (\\n    candidate.startsWith(\\\"/\\\") ||\\n    candidate.startsWith(\\\"./\\\") ||\\n    candidate.startsWith(\\\"../\\\") ||\\n    candidate.startsWith(\\\"?\\\") ||\\n    candidate.startsWith(\\\"#\\\")\\n  ) {\\n    if (candidate.startsWith(\\\"//\\\")) return undefined;\\n    // eslint-disable-next-line no-control-regex -- intentionally matching control characters\\n    if (/[\\\\u0000-\\\\u001F\\\\u007F]/.test(candidate)) return undefined;\\n    return candidate;\\n  }\\n\\n  try {\\n    const url = new URL(candidate);\\n    if (url.protocol === \\\"http:\\\" || url.protocol === \\\"https:\\\") {\\n      return url.toString();\\n    }\\n  } catch {\\n    return undefined;\\n  }\\n  return undefined;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport type { ReactNode } from \\\"react\\\";\\n\\n/**\\n * Tool UI conventions:\\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\\n * - Schema: `SerializableXSchema`\\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\\n * - Root attrs: `data-tool-ui-id` + `data-slot`\\n */\\n\\n/**\\n * Schema for tool UI identity.\\n *\\n * Every tool UI should have a unique identifier that:\\n * - Is stable across re-renders\\n * - Is meaningful (not auto-generated)\\n * - Is unique within the conversation\\n *\\n * Format recommendation: `{component-type}-{semantic-identifier}`\\n * Examples: \\\"data-table-expenses-q3\\\", \\\"option-list-deploy-target\\\"\\n */\\nexport const ToolUIIdSchema = z.string().min(1);\\n\\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\\n\\n/**\\n * Primary role of a Tool UI surface in a chat context.\\n */\\nexport const ToolUIRoleSchema = z.enum([\\n  \\\"information\\\",\\n  \\\"decision\\\",\\n  \\\"control\\\",\\n  \\\"state\\\",\\n  \\\"composite\\\",\\n]);\\n\\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\\n\\nexport const ToolUIReceiptOutcomeSchema = z.enum([\\n  \\\"success\\\",\\n  \\\"partial\\\",\\n  \\\"failed\\\",\\n  \\\"cancelled\\\",\\n]);\\n\\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\\n\\n/**\\n * Optional receipt metadata: a durable summary of an outcome.\\n */\\nexport const ToolUIReceiptSchema = z.object({\\n  outcome: ToolUIReceiptOutcomeSchema,\\n  summary: z.string().min(1),\\n  identifiers: z.record(z.string(), z.string()).optional(),\\n  at: z.string().datetime(),\\n});\\n\\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\\n\\n/**\\n * Base schema for Tool UI payloads (id + optional role/receipt).\\n */\\nexport const ToolUISurfaceSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n});\\n\\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\\n\\nexport const ActionSchema = z.object({\\n  id: z.string().min(1),\\n  label: z.string().min(1),\\n  /**\\n   * Canonical narration the assistant can use after this action is taken.\\n   *\\n   * Example: \\\"I exported the table as CSV.\\\" / \\\"I opened the link in a new tab.\\\"\\n   */\\n  sentence: z.string().optional(),\\n  confirmLabel: z.string().optional(),\\n  variant: z\\n    .enum([\\\"default\\\", \\\"destructive\\\", \\\"secondary\\\", \\\"ghost\\\", \\\"outline\\\"])\\n    .optional(),\\n  icon: z.custom<ReactNode>().optional(),\\n  loading: z.boolean().optional(),\\n  disabled: z.boolean().optional(),\\n  shortcut: z.string().optional(),\\n});\\n\\nexport type Action = z.infer<typeof ActionSchema>;\\nexport type LocalAction = Action;\\nexport type DecisionAction = Action;\\n\\nexport const DecisionResultSchema = z.object({\\n  kind: z.literal(\\\"decision\\\"),\\n  version: z.literal(1),\\n  decisionId: z.string().min(1),\\n  actionId: z.string().min(1),\\n  actionLabel: z.string().min(1),\\n  at: z.string().datetime(),\\n  payload: z.record(z.string(), z.unknown()).optional(),\\n});\\n\\nexport type DecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n> = Omit<z.infer<typeof DecisionResultSchema>, \\\"payload\\\"> & {\\n  payload?: TPayload;\\n};\\n\\nexport function createDecisionResult<\\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\\n>(args: {\\n  decisionId: string;\\n  action: { id: string; label: string };\\n  payload?: TPayload;\\n}): DecisionResult<TPayload> {\\n  return {\\n    kind: \\\"decision\\\",\\n    version: 1,\\n    decisionId: args.decisionId,\\n    actionId: args.action.id,\\n    actionLabel: args.action.label,\\n    at: new Date().toISOString(),\\n    payload: args.payload,\\n  };\\n}\\n\\nexport const ActionButtonsPropsSchema = z.object({\\n  actions: z.array(ActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n  className: z.string().optional(),\\n});\\n\\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\\n  actions: z.array(SerializableActionSchema),\\n}).omit({ className: true });\\n\\nexport interface ActionsConfig {\\n  items: Action[];\\n  align?: \\\"left\\\" | \\\"center\\\" | \\\"right\\\";\\n  confirmTimeout?: number;\\n}\\n\\nexport const SerializableActionsConfigSchema = z.object({\\n  items: z.array(SerializableActionSchema).min(1),\\n  align: z.enum([\\\"left\\\", \\\"center\\\", \\\"right\\\"]).optional(),\\n  confirmTimeout: z.number().positive().optional(),\\n});\\n\\nexport type SerializableActionsConfig = z.infer<\\n  typeof SerializableActionsConfigSchema\\n>;\\n\\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/video/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/video/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n */\\n\\\"use client\\\";\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport { Button } from \\\"@/components/ui/button\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/video/context.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/video/context.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport * as React from \\\"react\\\";\\n\\nexport interface VideoPlaybackState {\\n  playing: boolean;\\n  muted: boolean;\\n}\\n\\nexport interface VideoContextValue {\\n  state: VideoPlaybackState;\\n  setState: (patch: Partial<VideoPlaybackState>) => void;\\n  videoElement: HTMLVideoElement | null;\\n  setVideoElement: (node: HTMLVideoElement | null) => void;\\n}\\n\\nconst VideoContext = React.createContext<VideoContextValue | null>(null);\\n\\nexport function useVideo() {\\n  const ctx = React.use(VideoContext);\\n  if (!ctx) {\\n    throw new Error(\\\"useVideo must be used within a <VideoProvider />\\\");\\n  }\\n  return ctx;\\n}\\n\\nexport interface VideoProviderProps {\\n  children: React.ReactNode;\\n  defaultState?: Partial<VideoPlaybackState>;\\n}\\n\\nexport function VideoProvider({ children, defaultState }: VideoProviderProps) {\\n  const [state, setStateInternal] = React.useState<VideoPlaybackState>({\\n    playing: defaultState?.playing ?? false,\\n    muted: defaultState?.muted ?? true,\\n  });\\n\\n  const [videoElement, setVideoElement] =\\n    React.useState<HTMLVideoElement | null>(null);\\n\\n  const setState = React.useCallback((patch: Partial<VideoPlaybackState>) => {\\n    setStateInternal((prev) => ({ ...prev, ...patch }));\\n  }, []);\\n\\n  const value = React.useMemo(\\n    () => ({ state, setState, videoElement, setVideoElement }),\\n    [state, setState, videoElement],\\n  );\\n\\n  return (\\n    <VideoContext.Provider value={value}>{children}</VideoContext.Provider>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/video/index.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/video/index.ts\",\n      \"content\": \"export { Video } from \\\"./video\\\";\\nexport type { VideoProps } from \\\"./video\\\";\\nexport type { SerializableVideo, Source } from \\\"./schema\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/video/README.md\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/video/README.md\",\n      \"content\": \"# Video\\n\\nImplementation for the \\\"video\\\" Tool UI surface.\\n\\n## Files\\n\\n- public exports: components/tool-ui/video/index.ts\\n- serializable schema + parse helpers: components/tool-ui/video/schema.ts\\n\\n## Companion assets\\n\\n- Docs page: app/docs/video/content.mdx\\n- Preset payload: lib/presets/video.ts\\n\\n## Quick check\\n\\nRun this after edits:\\n\\npnpm test\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/video/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/video/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\nimport {\\n  ToolUIIdSchema,\\n  ToolUIReceiptSchema,\\n  ToolUIRoleSchema,\\n} from \\\"../shared/schema\\\";\\n\\nimport { AspectRatioSchema, MediaFitSchema } from \\\"../shared/media\\\";\\n\\nexport const SourceSchema = z.object({\\n  label: z.string(),\\n  iconUrl: z.url().optional(),\\n  url: z.url().optional(),\\n});\\n\\nexport type Source = z.infer<typeof SourceSchema>;\\n\\nexport const SerializableVideoSchema = z.object({\\n  id: ToolUIIdSchema,\\n  role: ToolUIRoleSchema.optional(),\\n  receipt: ToolUIReceiptSchema.optional(),\\n  assetId: z.string(),\\n  src: z.url(),\\n  poster: z.url().optional(),\\n  title: z.string().optional(),\\n  description: z.string().optional(),\\n  href: z.url().optional(),\\n  domain: z.string().optional(),\\n  durationMs: z.number().int().positive().optional(),\\n  ratio: AspectRatioSchema.optional(),\\n  fit: MediaFitSchema.optional(),\\n  createdAt: z.string().datetime().optional(),\\n  locale: z.string().optional(),\\n  source: SourceSchema.optional(),\\n});\\n\\nexport type SerializableVideo = z.infer<typeof SerializableVideoSchema>;\\n\\nconst SerializableVideoSchemaContract = defineToolUiContract(\\n  \\\"Video\\\",\\n  SerializableVideoSchema,\\n);\\n\\nexport const parseSerializableVideo: (input: unknown) => SerializableVideo =\\n  SerializableVideoSchemaContract.parse;\\n\\nexport const safeParseSerializableVideo: (\\n  input: unknown,\\n) => SerializableVideo | null = SerializableVideoSchemaContract.safeParse;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/video/video-helpers.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/video/video-helpers.ts\",\n      \"content\": \"import type { SerializableVideo } from \\\"./schema\\\";\\nimport { resolveSafeNavigationHref, sanitizeHref } from \\\"../shared/media\\\";\\n\\nexport type VideoMediaEvent = \\\"mute\\\" | \\\"unmute\\\";\\n\\nexport interface ResolvedVideoNavigation {\\n  sanitizedHref: string | undefined;\\n  sanitizedSourceUrl: string | undefined;\\n  primaryHref: string | undefined;\\n}\\n\\nexport function getMuteMediaEvent(\\n  previousMuted: boolean,\\n  nextMuted: boolean,\\n): VideoMediaEvent | null {\\n  if (previousMuted === nextMuted) {\\n    return null;\\n  }\\n\\n  return nextMuted ? \\\"mute\\\" : \\\"unmute\\\";\\n}\\n\\nexport function resolveVideoNavigation(\\n  rawHref: string | undefined,\\n  rawSourceUrl: string | undefined,\\n): ResolvedVideoNavigation {\\n  const sanitizedHref = sanitizeHref(rawHref);\\n  const sanitizedSourceUrl = sanitizeHref(rawSourceUrl);\\n\\n  return {\\n    sanitizedHref,\\n    sanitizedSourceUrl,\\n    primaryHref: resolveSafeNavigationHref(sanitizedHref, sanitizedSourceUrl),\\n  };\\n}\\n\\nexport function normalizeVideoDataForCallback(\\n  video: SerializableVideo,\\n  normalized: {\\n    ratio: NonNullable<SerializableVideo[\\\"ratio\\\"]>;\\n    fit: NonNullable<SerializableVideo[\\\"fit\\\"]>;\\n    locale: string;\\n    sanitizedHref: string | undefined;\\n    sanitizedSourceUrl: string | undefined;\\n  },\\n): SerializableVideo {\\n  return {\\n    ...video,\\n    ratio: normalized.ratio,\\n    fit: normalized.fit,\\n    href: normalized.sanitizedHref,\\n    source: video.source\\n      ? { ...video.source, url: normalized.sanitizedSourceUrl }\\n      : undefined,\\n    locale: normalized.locale,\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/video/video.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/video/video.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport * as React from \\\"react\\\";\\nimport { ExternalLink, Play } from \\\"lucide-react\\\";\\nimport { cn, Button } from \\\"./_adapter\\\";\\n\\nimport {\\n  formatDuration,\\n  getFitClass,\\n  openSafeNavigationHref,\\n  OVERLAY_GRADIENT,\\n  RATIO_CLASS_MAP,\\n} from \\\"../shared/media\\\";\\nimport { VideoProvider, useVideo } from \\\"./context\\\";\\nimport type { SerializableVideo } from \\\"./schema\\\";\\nimport {\\n  getMuteMediaEvent,\\n  normalizeVideoDataForCallback,\\n  resolveVideoNavigation,\\n} from \\\"./video-helpers\\\";\\n\\nconst FALLBACK_LOCALE = \\\"en-US\\\";\\n\\nexport interface VideoProps extends SerializableVideo {\\n  className?: string;\\n  // Keep behavior flags intentionally minimal; prefer explicit variants over more booleans.\\n  autoPlay?: boolean;\\n  defaultMuted?: boolean;\\n  onNavigate?: (href: string, video: SerializableVideo) => void;\\n  onMediaEvent?: (type: \\\"play\\\" | \\\"pause\\\" | \\\"mute\\\" | \\\"unmute\\\") => void;\\n}\\n\\nfunction VideoRoot(props: VideoProps) {\\n  const { defaultMuted = true, ...rest } = props;\\n\\n  return (\\n    <VideoProvider defaultState={{ muted: defaultMuted }}>\\n      <VideoInner {...rest} />\\n    </VideoProvider>\\n  );\\n}\\n\\nfunction VideoInner(props: Omit<VideoProps, \\\"defaultMuted\\\">) {\\n  const {\\n    className,\\n    autoPlay = true,\\n    onNavigate,\\n    onMediaEvent,\\n    ...serializable\\n  } = props;\\n\\n  const {\\n    id,\\n    src,\\n    poster,\\n    title,\\n    description,\\n    href: rawHref,\\n    domain,\\n    durationMs,\\n    ratio = \\\"16:9\\\",\\n    fit = \\\"cover\\\",\\n    createdAt,\\n    source,\\n    locale: providedLocale,\\n  } = serializable;\\n\\n  const locale = providedLocale ?? FALLBACK_LOCALE;\\n  const { sanitizedHref, sanitizedSourceUrl, primaryHref } =\\n    resolveVideoNavigation(rawHref, source?.url);\\n\\n  const videoData: SerializableVideo = normalizeVideoDataForCallback(\\n    serializable,\\n    {\\n      ratio,\\n      fit,\\n      locale,\\n      sanitizedHref,\\n      sanitizedSourceUrl,\\n    },\\n  );\\n\\n  const { state, setState, setVideoElement } = useVideo();\\n  const videoRef = React.useRef<HTMLVideoElement | null>(null);\\n  const previousMutedRef = React.useRef(state.muted);\\n\\n  React.useEffect(() => {\\n    setVideoElement(videoRef.current);\\n    return () => setVideoElement(null);\\n  }, [setVideoElement]);\\n\\n  React.useEffect(() => {\\n    const video = videoRef.current;\\n    if (!video) return;\\n    if (video.muted !== state.muted) {\\n      video.muted = state.muted;\\n    }\\n  }, [state.muted]);\\n\\n  React.useEffect(() => {\\n    const video = videoRef.current;\\n    if (!video) return;\\n    if (state.playing && video.paused) {\\n      void video.play().catch(() => undefined);\\n    } else if (!state.playing && !video.paused) {\\n      video.pause();\\n    }\\n  }, [state.playing]);\\n\\n  const navigate = (targetHref: string) => {\\n    if (onNavigate) {\\n      onNavigate(targetHref, videoData);\\n    } else {\\n      openSafeNavigationHref(targetHref);\\n    }\\n  };\\n\\n  const handleWatch = (event: React.MouseEvent<HTMLButtonElement>) => {\\n    event.preventDefault();\\n    event.stopPropagation();\\n    const video = videoRef.current;\\n    if (!video) return;\\n    if (video.paused) {\\n      void video.play().catch(() => undefined);\\n    } else {\\n      video.pause();\\n    }\\n  };\\n\\n  const handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => {\\n    event.preventDefault();\\n    event.stopPropagation();\\n    if (!primaryHref) return;\\n    navigate(primaryHref);\\n  };\\n\\n  const sourceLabel = source?.label;\\n  const metadataDomain = domain && domain !== sourceLabel ? domain : undefined;\\n  const hasMetadata = Boolean(\\n    description || sourceLabel || metadataDomain || durationMs || createdAt,\\n  );\\n  const hasOverlay = Boolean(title || primaryHref);\\n\\n  return (\\n    <article\\n      className={cn(\\\"relative w-full min-w-80 max-w-md\\\", className)}\\n      lang={locale}\\n      data-tool-ui-id={id}\\n      data-slot=\\\"video\\\"\\n    >\\n      <div\\n        className={cn(\\n          \\\"group @container relative isolate flex w-full min-w-0 flex-col overflow-hidden rounded-xl\\\",\\n          \\\"border border-border bg-card text-sm shadow-xs\\\",\\n        )}\\n      >\\n        <div\\n          className={cn(\\n            \\\"group relative w-full overflow-hidden bg-black\\\",\\n            ratio !== \\\"auto\\\" ? RATIO_CLASS_MAP[ratio] : \\\"aspect-video\\\",\\n          )}\\n        >\\n          <video\\n            ref={videoRef}\\n            className={cn(\\n              \\\"relative z-10 h-full w-full transition-transform duration-200 group-hover:scale-[1.01]\\\",\\n              getFitClass(fit),\\n              ratio !== \\\"auto\\\" && \\\"absolute inset-0 h-full w-full\\\",\\n            )}\\n            src={src}\\n            poster={poster}\\n            controls\\n            playsInline\\n            autoPlay={autoPlay}\\n            preload=\\\"metadata\\\"\\n            muted={state.muted}\\n            onPlay={() => {\\n              setState({ playing: true });\\n              onMediaEvent?.(\\\"play\\\");\\n            }}\\n            onPause={() => {\\n              setState({ playing: false });\\n              onMediaEvent?.(\\\"pause\\\");\\n            }}\\n            onVolumeChange={(event) => {\\n              const target = event.currentTarget;\\n              setState({ muted: target.muted });\\n              const mediaEvent = getMuteMediaEvent(\\n                previousMutedRef.current,\\n                target.muted,\\n              );\\n              previousMutedRef.current = target.muted;\\n              if (mediaEvent) {\\n                onMediaEvent?.(mediaEvent);\\n              }\\n            }}\\n          />\\n          {hasOverlay && (\\n            <>\\n              <div\\n                className=\\\"pointer-events-none absolute inset-x-0 top-0 z-20 h-32 opacity-0 transition-opacity duration-200 group-hover:opacity-100 group-focus-within:opacity-100\\\"\\n                style={{ backgroundImage: OVERLAY_GRADIENT }}\\n              />\\n              <div className=\\\"absolute inset-x-0 top-0 z-30 flex items-start justify-between gap-2 px-5 pt-4 opacity-0 transition-opacity duration-200 group-hover:opacity-100 group-focus-within:opacity-100\\\">\\n                {title ? (\\n                  <div className=\\\"line-clamp-2 max-w-[70%] font-semibold text-white drop-shadow-sm\\\">\\n                    {title}\\n                  </div>\\n                ) : (\\n                  <span className=\\\"sr-only\\\">Video controls</span>\\n                )}\\n                <div className=\\\"flex items-center gap-2\\\">\\n                  {primaryHref && (\\n                    <Button\\n                      variant=\\\"secondary\\\"\\n                      size=\\\"sm\\\"\\n                      onClick={handleOpen}\\n                      className=\\\"bg-black/55 text-white hover:bg-black/70\\\"\\n                    >\\n                      <ExternalLink\\n                        className=\\\"mr-1 h-4 w-4\\\"\\n                        aria-hidden=\\\"true\\\"\\n                      />\\n                      Open\\n                    </Button>\\n                  )}\\n                  <Button\\n                    variant=\\\"default\\\"\\n                    size=\\\"sm\\\"\\n                    onClick={handleWatch}\\n                    className=\\\"shadow-sm\\\"\\n                  >\\n                    <Play className=\\\"mr-1 h-4 w-4\\\" aria-hidden=\\\"true\\\" />\\n                    Watch\\n                  </Button>\\n                </div>\\n              </div>\\n            </>\\n          )}\\n        </div>\\n\\n        {hasMetadata && (\\n          <div className=\\\"flex flex-col gap-1.5 px-4 py-3\\\">\\n            {description && (\\n              <p className=\\\"text-foreground line-clamp-2 text-sm leading-snug\\\">\\n                {description}\\n              </p>\\n            )}\\n            <div className=\\\"text-muted-foreground flex flex-wrap items-center gap-x-3 gap-y-1 text-xs\\\">\\n              {sourceLabel && <span>{sourceLabel}</span>}\\n              {metadataDomain && <span>{metadataDomain}</span>}\\n              {typeof durationMs === \\\"number\\\" && (\\n                <span>{formatDuration(durationMs)}</span>\\n              )}\\n              {createdAt && (\\n                <time dateTime={createdAt}>\\n                  {formatCreatedAt(createdAt, locale)}\\n                </time>\\n              )}\\n            </div>\\n          </div>\\n        )}\\n      </div>\\n    </article>\\n  );\\n}\\n\\nfunction formatCreatedAt(createdAt: string, locale: string): string {\\n  const date = new Date(createdAt);\\n  if (Number.isNaN(date.getTime())) {\\n    return createdAt;\\n  }\\n\\n  return new Intl.DateTimeFormat(locale, { dateStyle: \\\"medium\\\" }).format(date);\\n}\\n\\ntype VideoComponent = typeof VideoRoot & {\\n  Root: typeof VideoRoot;\\n};\\n\\nexport const Video = Object.assign(VideoRoot, {\\n  Root: VideoRoot,\\n}) as VideoComponent;\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/weather-widget.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"weather-widget\",\n  \"type\": \"registry:block\",\n  \"title\": \"Weather Widget\",\n  \"description\": \"Display weather conditions and forecasts.\",\n  \"dependencies\": [\n    \"lucide-react\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/weather-widget/generated/weather-runtime-core.generated.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/weather-widget/generated/weather-runtime-core.generated.ts\",\n      \"content\": \"// @ts-nocheck\\n// AUTO-GENERATED by `pnpm weather:compile`.\\n// Source: lib/weather-authoring/weather-widget/weather-runtime-core.ts\\n// DO NOT EDIT MANUALLY.\\n\\nimport{useMemo as ye,useState as ft,useEffect as dt}from\\\"react\\\";var Ze=8,j=new Set;function he(e){return j.has(e)?!0:j.size>=Ze?!1:(j.add(e),!0)}function ae(e){j.delete(e)}function ge(e,t){return t&&(ae(e),null)}import{useCallback as N,useEffect as Ke,useRef as v}from\\\"react\\\";var O=`#version 300 es\\nin vec4 a_position;out vec2 v_uv;void main(){gl_Position=a_position;v_uv=a_position.xy*0.5+0.5;}`,se=`#version 300 es\\nprecision highp float;in vec2 v_uv;out vec4 fragColor;uniform float u_time;uniform vec2 u_resolution;uniform float u_timeOfDay;uniform float u_moonPhase;uniform float u_starDensity;uniform vec2 u_celestialPos;uniform float u_sunSize;uniform float u_moonSize;uniform float u_sunGlowIntensity;uniform float u_sunGlowSize;uniform float u_sunRayCount;uniform float u_sunRayLength;uniform float u_sunRayIntensity;uniform float u_sunRayShimmer;uniform float u_sunRayShimmerSpeed;uniform float u_moonGlowIntensity;uniform float u_moonGlowSize;uniform float u_skyBrightness;uniform float u_skySaturation;uniform float u_skyContrast;uniform sampler2D u_moonTexture;uniform bool u_hasMoonTexture;\\n#define PI 3.14159265359\\n#define GODRAY_MAX_SAMPLES 32\\n#define TAU 6.28318530718\\nfloat hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453123);}vec2 hash2(vec2 p){p=vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3)));return fract(sin(p)*43758.5453);}float noise(vec2 p){vec2 i=floor(p);vec2 f=fract(p);f=f*f*(3.0-2.0*f);float a=hash(i);float b=hash(i+vec2(1.0,0.0));float c=hash(i+vec2(0.0,1.0));float d=hash(i+vec2(1.0,1.0));return mix(mix(a,b,f.x),mix(c,d,f.x),f.y);}float fbm(vec2 p,int octaves){float value=0.0;float amplitude=0.5;float frequency=1.0;for(int i=0;i<6;i++){if(i>=octaves)break;value+=amplitude*noise(p*frequency);frequency*=2.0;amplitude*=0.5;}return value;}float getSunY(float timeOfDay,float baseY){float belowHorizon=-0.25;float riseProgress=smoothstep(0.18,0.32,timeOfDay);float setProgress=smoothstep(0.68,0.82,timeOfDay);float visible=riseProgress*(1.0-setProgress);return mix(belowHorizon,baseY,visible);}float getMoonY(float timeOfDay,float baseY){float belowHorizon=-0.25;float risingEvening=smoothstep(0.74,0.88,timeOfDay);float settingMorning=1.0-smoothstep(0.12,0.26,timeOfDay);float visible=max(risingEvening,settingMorning);return mix(belowHorizon,baseY,visible);}float getHorizonFade(float y){return smoothstep(-0.2,0.0,y);}vec3 getSkyColor(vec2 uv,float timeOfDay){vec3 dayTop=vec3(0.4,0.6,0.9);vec3 dayHorizon=vec3(0.7,0.8,0.95);vec3 sunsetTop=vec3(0.2,0.2,0.4);vec3 sunsetHorizon=vec3(0.9,0.5,0.2);vec3 nightTop=vec3(0.02,0.02,0.05);vec3 nightHorizon=vec3(0.05,0.05,0.1);float dayAmount=smoothstep(0.25,0.4,timeOfDay)*smoothstep(0.75,0.6,timeOfDay);float sunsetAmount=max(smoothstep(0.2,0.3,timeOfDay)*smoothstep(0.4,0.3,timeOfDay),smoothstep(0.6,0.7,timeOfDay)*smoothstep(0.8,0.7,timeOfDay));float nightAmount=max(0.0,1.0-dayAmount-sunsetAmount);float gradientFactor=pow(1.0-uv.y,1.0);vec3 topColor=dayTop*dayAmount+sunsetTop*sunsetAmount+nightTop*nightAmount;vec3 horizonColor=dayHorizon*dayAmount+sunsetHorizon*sunsetAmount+nightHorizon*nightAmount;vec3 avgColor=(topColor+horizonColor)*0.5;topColor=mix(avgColor,topColor,u_skyContrast);horizonColor=mix(avgColor,horizonColor,u_skyContrast);vec3 color=mix(topColor,horizonColor,gradientFactor);color*=u_skyBrightness;float gray=dot(color,vec3(0.299,0.587,0.114));if(u_skySaturation<=1.0){color=mix(vec3(gray),color,u_skySaturation);}else{float boost=u_skySaturation-1.0;color=color+(color-vec3(gray))*boost;}return clamp(color,0.0,1.0);}float drawStars(vec2 uv,float density,float time){float stars=0.0;for(int layer=0;layer<3;layer++){float layerScale=100.0+float(layer)*50.0;vec2 gridUV=uv*layerScale;vec2 gridID=floor(gridUV);vec2 gridFract=fract(gridUV);vec2 starPos=hash2(gridID+float(layer)*100.0);float dist=length(gridFract-starPos);float starPresent=step(1.0-density*0.3,hash(gridID*(float(layer)+1.0)));float starSize=0.02+hash(gridID.yx)*0.03;float twinkle=sin(time*(2.0+hash(gridID)*3.0)+hash(gridID.yx)*TAU)*0.3+0.7;float star=smoothstep(starSize,0.0,dist)*starPresent*twinkle;star*=1.0-float(layer)*0.3;stars+=star;}return stars;}vec3 drawSun(vec2 uv,vec2 sunPos,float size){vec2 aspect=vec2(u_resolution.x/u_resolution.y,1.0);vec2 diff=(uv-sunPos)*aspect;float dist=length(diff);float angle=atan(diff.y,diff.x);float disc=1.0-smoothstep(size*0.9,size,dist);vec3 sunCore=vec3(1.0,1.0,0.95);vec3 sunEdge=vec3(1.0,0.9,0.4);float edgeFactor=clamp(dist/size,0.0,1.0);vec3 sunColor=mix(sunCore,sunEdge,edgeFactor);float limbDarkening=1.0-pow(clamp(dist/size,0.0,1.0),2.0)*0.3;sunColor*=limbDarkening;float glowSize=max(0.1,u_sunGlowSize);float scaledDist=dist/glowSize;float glow1=exp(-scaledDist*8.0)*0.5;float glow2=exp(-scaledDist*3.0)*0.3;float glow3=exp(-scaledDist*1.5)*0.15;vec3 glowColor=vec3(1.0,0.8,0.4);float glowTotal=(glow1+glow2+glow3)*u_sunGlowIntensity;vec3 result=sunColor*disc*2.0;result+=glowColor*glowTotal;float ringCenter=size*1.15;float ringWidth=max(size*0.35,0.001);float ringMask=smoothstep(size*0.85,size*1.05,dist);ringMask*=1.0-smoothstep(size*5.0,size*9.0,dist);float chromaShift=size*(0.012+u_sunRayIntensity*0.06);chromaShift*=smoothstep(size*0.9,size*2.4,dist);float ringR=exp(-pow((dist-chromaShift-ringCenter)/ringWidth,2.0));float ringG=exp(-pow((dist-ringCenter)/ringWidth,2.0));float ringB=exp(-pow((dist+chromaShift-ringCenter)/ringWidth,2.0));float ringT=clamp((dist-size)/(size*2.2),0.0,1.0);vec3 ringSpectral=0.55+0.45*cos(TAU*(ringT+vec3(0.0,0.33,0.67)));ringSpectral=clamp(ringSpectral,0.0,1.0);vec3 ringColor=mix(vec3(1.0),ringSpectral,0.45);float ringIntensity=(ringR+ringG+ringB)/3.0;ringIntensity*=ringMask*u_sunGlowIntensity*0.025;result+=ringColor*ringIntensity;if(u_sunRayCount>0.0&&u_sunRayIntensity>0.0){if(dist<size*3.6){float motion=clamp(u_sunRayShimmer,0.0,5.0);float t=u_time*max(0.0,u_sunRayShimmerSpeed);float rayPhase=angle*u_sunRayCount;float rayIndex=floor(rayPhase/PI+0.5);float raySeed=hash(vec2(rayIndex,19.17));float major=pow(abs(cos(rayPhase)),10.0);float minor=pow(abs(cos(rayPhase*2.0+raySeed*2.3)),22.0)*0.18;float rayShape=max(major,minor);float breathe=1.0+(noise(vec2(t*0.05,raySeed*7.0))-0.5)*(0.08*motion);float shimmer=1.0+(noise(vec2(dist*12.0-t*0.25,raySeed*23.0))-0.5)*(0.12*motion);float micro=1.0+(noise(vec2(t*0.6,rayPhase*0.8))-0.5)*(0.06*motion);float rayNoise=0.72+0.28*noise(vec2(rayPhase*0.35,t*0.12+raySeed*10.0));float rayPattern=rayShape*rayNoise;float rayStart=smoothstep(size*0.75,size*1.25,dist);float rayEnd=smoothstep(size*(3.0*breathe),size*1.5,dist);float rayLengthVar=0.75+raySeed*0.55;float maxRayDist=max(0.001,u_sunRayLength*0.15);float rayFalloff=exp(-dist*dist/(maxRayDist*maxRayDist*rayLengthVar*breathe));float rays=rayPattern*rayFalloff*rayStart*rayEnd*u_sunRayIntensity;rays*=shimmer*micro;float prismMask=smoothstep(size*1.05,size*2.6,dist);float rayChroma=size*(0.01+u_sunRayIntensity*0.05)*prismMask;float distR=max(0.0,dist-rayChroma);float distB=dist+rayChroma;float falloffR=exp(-distR*distR/(maxRayDist*maxRayDist*rayLengthVar*breathe));float falloffG=rayFalloff;float falloffB=exp(-distB*distB/(maxRayDist*maxRayDist*rayLengthVar*breathe));vec3 rayRGB=vec3(falloffR,falloffG,falloffB)*rayPattern*rayStart*rayEnd;float rayAvg=(rayRGB.r+rayRGB.g+rayRGB.b)/3.0;vec3 rayChromaColor=rayRGB/max(rayAvg,1e-4);float rayT=clamp((dist-size)/(size*2.6),0.0,1.0);vec3 raySpectral=0.55+0.45*cos(TAU*(rayT+vec3(0.0,0.33,0.67)));raySpectral=clamp(raySpectral,0.0,1.0);raySpectral=mix(vec3(1.0),raySpectral,0.28);vec3 rayWarm=vec3(1.0,0.92,0.7);float prismMix=clamp(0.08+u_sunRayIntensity*0.6,0.0,0.45)*prismMask;vec3 rayColor=mix(rayWarm,rayChromaColor,prismMix);rayColor=mix(rayColor,raySpectral,prismMix*0.65);result+=rayColor*rays;}}return result;}vec3 getSphereNormal(vec2 discUV){float r2=dot(discUV,discUV);if(r2>1.0)return vec3(0.0);float z=sqrt(1.0-r2);return normalize(vec3(discUV.x,discUV.y,z));}vec2 sphereToEquirectangular(vec3 normal){float longitude=atan(normal.x,normal.z);float u=longitude/TAU+0.5;float latitude=asin(clamp(normal.y,-1.0,1.0));float v=latitude/PI+0.5;return vec2(u,v);}vec3 getMoonSurfaceColor(vec3 normal,vec2 discUV){if(u_hasMoonTexture){vec2 texUV=sphereToEquirectangular(normal);return texture(u_moonTexture,texUV).rgb;}float brightness=0.7+fbm(discUV*5.0,3)*0.3;return vec3(brightness*0.85,brightness*0.83,brightness*0.8);}vec4 drawMoon(vec2 uv,vec2 moonPos,float size,float phase){vec2 aspect=vec2(u_resolution.x/u_resolution.y,1.0);vec2 diff=(uv-moonPos)*aspect;float dist=length(diff);vec2 discUV=diff/size;float discDist=length(discUV);float disc=1.0-smoothstep(0.95,1.0,discDist);float glowSize=max(0.1,u_moonGlowSize);float glowIntensity=u_moonGlowIntensity;if(disc<0.001){float scaledDist=dist/glowSize;float glow1=exp(-scaledDist*6.0)*0.15;float glow2=exp(-scaledDist*2.0)*0.06;vec3 glowColor=vec3(0.8,0.85,0.95);float phaseAngle=phase*TAU;vec3 sunDir=vec3(sin(phaseAngle),0.0,-cos(phaseAngle));float glowPhase=max(0.2,dot(normalize(vec3(discUV,0.5)),sunDir)*0.5+0.5);return vec4(glowColor*(glow1+glow2)*glowPhase*glowIntensity,0.0);}vec3 normal=getSphereNormal(discUV);float phaseAngle=phase*TAU;vec3 sunDir=vec3(sin(phaseAngle),0.0,-cos(phaseAngle));float NdotL=dot(normal,sunDir);float terminator=smoothstep(-0.02,0.08,NdotL);vec3 baseColor=getMoonSurfaceColor(normal,discUV);vec3 ambient=baseColor*0.03;vec3 lit=baseColor*terminator;vec3 moonSurface=ambient+lit;float limbDarkening=1.0-pow(discDist,3.0)*0.15;moonSurface*=limbDarkening;float rimLight=pow(1.0-abs(NdotL),4.0)*terminator*0.1;moonSurface+=vec3(1.0,0.98,0.95)*rimLight;float scaledDist=dist/glowSize;float glow1=exp(-scaledDist*6.0)*0.12;float glow2=exp(-scaledDist*2.0)*0.06;vec3 glowColor=vec3(0.8,0.85,0.95);float litAmount=max(0.1,terminator);vec3 glow=glowColor*(glow1+glow2)*litAmount*glowIntensity;return vec4(moonSurface*disc+glow,disc);}void main(){vec2 uv=v_uv;vec3 color=getSkyColor(uv,u_timeOfDay);float sunY=getSunY(u_timeOfDay,u_celestialPos.y);float moonY=getMoonY(u_timeOfDay,u_celestialPos.y);vec2 sunPos=vec2(u_celestialPos.x,sunY);vec2 moonPos=vec2(u_celestialPos.x,moonY);float moonFade=getHorizonFade(moonY);if(moonFade>0.01){float stars=drawStars(uv,u_starDensity,u_time);color+=vec3(stars)*moonFade;}float sunFade=getHorizonFade(sunY);if(sunFade>0.01){vec3 sun=drawSun(uv,sunPos,u_sunSize);color+=sun*sunFade;}if(moonFade>0.01){vec4 moon=drawMoon(uv,moonPos,u_moonSize,u_moonPhase);float alpha=moon.a*moonFade;color=mix(color,moon.rgb,alpha)+moon.rgb*(1.0-moon.a)*moonFade;}fragColor=vec4(color,0.0);}`,oe=`#version 300 es\\nprecision highp float;in vec2 v_uv;out vec4 fragColor;uniform float u_time;uniform vec2 u_resolution;uniform sampler2D u_sceneTexture;uniform float u_timeOfDay;uniform float u_coverage;uniform float u_density;uniform float u_softness;uniform float u_windSpeed;uniform float u_windAngle;uniform float u_turbulence;uniform float u_lightIntensity;uniform float u_ambientDarkness;uniform int u_numLayers;uniform float u_cloudScale;uniform vec2 u_celestialPos;uniform float u_celestialSize;uniform float u_celestialBrightness;uniform float u_backlightIntensity;\\n#define PI 3.14159265359\\nfloat hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453123);}float noise(vec2 p){vec2 i=floor(p);vec2 f=fract(p);f=f*f*(3.0-2.0*f);float a=hash(i);float b=hash(i+vec2(1.0,0.0));float c=hash(i+vec2(0.0,1.0));float d=hash(i+vec2(1.0,1.0));return mix(mix(a,b,f.x),mix(c,d,f.x),f.y);}float fbm(vec2 p,int octaves){float value=0.0;float amplitude=0.5;for(int i=0;i<8;i++){if(i>=octaves)break;value+=amplitude*noise(p);p*=2.0;amplitude*=0.5;}return value;}float cloudLayer(vec2 uv,float time,float layerSeed,float speed,float turbAmount){vec2 wind=vec2(cos(u_windAngle),sin(u_windAngle))*speed*time;float layerScale=(1.8+hash(vec2(layerSeed,0.0))*1.2)*u_cloudScale;float layerRotation=hash(vec2(layerSeed,1.0))*0.5-0.25;vec2 layerOffset=vec2(hash(vec2(layerSeed,2.0))*100.0,hash(vec2(layerSeed,3.0))*100.0);float c=cos(layerRotation);float s=sin(layerRotation);vec2 rotatedUV=vec2(uv.x*c-uv.y*s,uv.x*s+uv.y*c);vec2 p=rotatedUV*layerScale+wind+layerOffset;float turbSeed=layerSeed*50.0;vec2 turbOffset=vec2(fbm(p*0.5+time*0.1+turbSeed,4),fbm(p*0.5+turbSeed+100.0+time*0.1,4))*turbAmount;float n=fbm(p+turbOffset,6);return n;}vec3 cloudLighting(float density,float heightInCloud,float sunAlt,float warmth,float nightFactor,vec2 uv){float daylight=smoothstep(-0.12,0.1,sunAlt);vec3 dayLitColor=vec3(1.0,0.98,0.96);vec3 sunsetLitColor=vec3(1.0,0.7,0.45);vec3 nightLitColor=vec3(0.12,0.14,0.2);vec3 litColor=mix(dayLitColor,sunsetLitColor,warmth);litColor=mix(litColor,nightLitColor,nightFactor);vec3 dayShadowColor=vec3(0.45,0.5,0.6);vec3 sunsetShadowColor=vec3(0.35,0.25,0.3);vec3 nightShadowColor=vec3(0.03,0.04,0.07);vec3 shadowColor=mix(dayShadowColor,sunsetShadowColor,warmth);shadowColor=mix(shadowColor,nightShadowColor,nightFactor);shadowColor*=(1.0-u_ambientDarkness*0.3);float topLight=heightInCloud*max(0.0,sunAlt);float sideLight=(1.0-abs(heightInCloud-0.5)*2.0)*(1.0-sunAlt*0.5);float bottomLight=(1.0-heightInCloud)*warmth*0.5;float ambientLight=mix(0.03,0.2,daylight);float lightAmount=(topLight*0.5+sideLight*0.3+bottomLight)*daylight+ambientLight;lightAmount=clamp(lightAmount*u_lightIntensity,0.0,1.0);vec3 cloudColor=mix(shadowColor,litColor,lightAmount);float rimLight=pow(density,0.5)*(1.0-density)*4.0;vec3 rimColor=mix(vec3(1.0,1.0,0.95),vec3(1.0,0.8,0.5),warmth);rimColor=mix(rimColor,vec3(0.15,0.18,0.25),nightFactor);float rimStrength=mix(0.1,0.3,daylight);cloudColor+=rimColor*rimLight*rimStrength*u_lightIntensity;float hotSpot=pow(max(0.0,lightAmount-0.6)*2.5,2.0)*warmth*daylight;cloudColor+=vec3(1.0,0.5,0.2)*hotSpot*0.4;float aspect=u_resolution.x/u_resolution.y;vec2 celestialUV=u_celestialPos;vec2 diff=(uv-celestialUV)*vec2(aspect,1.0);float distToCelestial=length(diff);float transmission=pow(1.0-density,2.0);float glowRadius=u_celestialSize*6.0;float proximityGlow=exp(-distToCelestial*distToCelestial/(glowRadius*glowRadius));float backlight=proximityGlow*transmission*u_celestialBrightness;float edgeDist=u_celestialSize*3.0;float nearCelestial=smoothstep(edgeDist*2.5,edgeDist*0.3,distToCelestial);float edgeFactor=density*(1.0-density)*4.0;float silverLining=nearCelestial*edgeFactor*u_celestialBrightness;vec3 backlightColor=mix(vec3(1.0,0.9,0.7),vec3(0.7,0.75,0.9),nightFactor);cloudColor+=backlightColor*(backlight*0.5+silverLining*0.8)*u_backlightIntensity;return cloudColor;}void main(){vec2 uv=v_uv;vec4 scene=texture(u_sceneTexture,uv);float sunAlt=u_timeOfDay<0.5?u_timeOfDay*2.0:2.0-u_timeOfDay*2.0;sunAlt=sunAlt*2.0-1.0;float warmth=1.0-smoothstep(0.0,0.4,sunAlt);warmth=warmth*warmth;float nightFactor=1.0-smoothstep(-0.12,0.02,sunAlt);float daylight=smoothstep(-0.12,0.1,sunAlt);vec3 color=scene.rgb;float accumulatedAlpha=0.0;for(int i=u_numLayers-1;i>=0;i--){float layerIdx=float(i);float layerDepth=layerIdx/max(1.0,float(u_numLayers)-1.0);float layerSeed=layerIdx*7.31+13.0;float layerSpeed=u_windSpeed*(0.6+hash(vec2(layerSeed,10.0))*0.8);float layerTurb=u_turbulence*(0.7+hash(vec2(layerSeed,11.0))*0.6);float cloud=cloudLayer(uv,u_time,layerSeed,layerSpeed,layerTurb);float threshold=1.0-u_coverage;cloud=smoothstep(threshold,threshold+u_softness,cloud);float heightInCloud=uv.y*0.6+cloud*0.4;vec3 cloudColor=cloudLighting(cloud,heightInCloud,sunAlt,warmth,nightFactor,uv);vec3 hazeColor=mix(vec3(0.05,0.06,0.1),vec3(0.6,0.7,0.85),daylight);float hazeAmount=layerDepth*layerDepth*0.5;cloudColor=mix(cloudColor,hazeColor,hazeAmount);float contrastReduction=1.0-layerDepth*0.3;cloudColor=mix(vec3(0.5),cloudColor,contrastReduction);float alpha=cloud*u_density*(0.6+(1.0-layerDepth)*0.4);color=mix(color,cloudColor,alpha*(1.0-accumulatedAlpha));accumulatedAlpha=accumulatedAlpha+alpha*(1.0-accumulatedAlpha);}fragColor=vec4(color,accumulatedAlpha);}`,ie=`#version 300 es\\nprecision highp float;in vec2 v_uv;out vec4 fragColor;uniform float u_time;uniform vec2 u_resolution;uniform sampler2D u_sceneTexture;uniform float u_glassIntensity;uniform float u_glassZoom;uniform float u_fallingIntensity;uniform float u_fallingSpeed;uniform float u_fallingAngle;uniform float u_fallingStreakLength;uniform int u_fallingLayers;uniform float u_refractionStrength;\\n#define S(a, b, t) smoothstep(a, b, t)\\nvec3 N13(float p){vec3 p3=fract(vec3(p)*vec3(0.1031,0.11369,0.13787));p3+=dot(p3,p3.yzx+19.19);return fract(vec3((p3.x+p3.y)*p3.z,(p3.x+p3.z)*p3.y,(p3.y+p3.z)*p3.x));}float N(float t){return fract(sin(t*12345.564)*7658.76);}float Saw(float b,float t){return S(0.0,b,t)*S(1.0,b,t);}vec2 DropLayer(vec2 uv,float t){vec2 UV=uv;uv.y+=t*0.75;vec2 aspect=vec2(6.0,1.0);vec2 grid=aspect*2.0;vec2 id=floor(uv*grid);float colShift=N(id.x);uv.y+=colShift;id=floor(uv*grid);vec3 n=N13(id.x*35.2+id.y*2376.1);vec2 st=fract(uv*grid)-vec2(0.5,0.0);float x=n.x-0.5;float y=UV.y*20.0;float wiggle=sin(y+sin(y));x+=wiggle*(0.5-abs(x))*(n.z-0.5);x*=0.7;float ti=fract(t+n.z);y=(Saw(0.85,ti)-0.5)*0.9+0.5;vec2 p=vec2(x,y);float d=length((st-p)*aspect.yx);float mainDrop=S(0.4,0.0,d);float r=sqrt(S(1.0,y,st.y));float cd=abs(st.x-x);float trail=S(0.23*r,0.15*r*r,cd);float trailFront=S(-0.02,0.02,st.y-y);trail*=trailFront*r*r;float y2=fract(UV.y*10.0)+(st.y-0.5);float dd=length(st-vec2(x,y2));float droplets=S(0.3,0.0,dd);float m=mainDrop+droplets*r*trailFront;return vec2(m,trail);}float StaticDrops(vec2 uv,float t){uv*=40.0;vec2 id=floor(uv);uv=fract(uv)-0.5;vec3 n=N13(id.x*107.45+id.y*3543.654);vec2 p=(n.xy-0.5)*0.7;float d=length(uv-p);float fade=Saw(0.025,fract(t+n.z));float c=S(0.3,0.0,d)*fract(n.z*10.0)*fade;return c;}vec2 Drops(vec2 uv,float t,float l0,float l1,float l2){float s=StaticDrops(uv,t)*l0;vec2 m1=DropLayer(uv,t)*l1;vec2 m2=DropLayer(uv*1.85,t)*l2;float c=s+m1.x+m2.x;c=S(0.3,1.0,c);return vec2(c,max(m1.y*l0,m2.y*l1));}float hash12(vec2 p){vec3 p3=fract(vec3(p.xyx)*0.1031);p3+=dot(p3,p3.yzx+33.33);return fract((p3.x+p3.y)*p3.z);}vec2 FallingRainLayer(vec2 uv,float t,float speed,float angle,float streakLen,float scale,float density){vec2 offset=vec2(0.0);vec2 p=uv;p.x+=p.y*angle;p*=scale;p.y+=t*speed;vec2 id=floor(p);vec2 gv=fract(p)-0.5;for(int y=-1;y<=1;y++){for(int x=-1;x<=1;x++){vec2 offs=vec2(float(x),float(y));vec2 cellId=id+offs;float n1=hash12(cellId);if(n1>density)continue;vec2 n2=vec2(hash12(cellId*17.23),hash12(cellId*31.17));vec2 dropPos=offs+n2-0.5;vec2 localUV=gv-dropPos;float streakW=0.025+n1*0.02;float streakH=streakLen*(0.4+hash12(cellId*7.13)*0.6);float t_pos=(localUV.y+streakH)/(2.0*streakH);t_pos=clamp(t_pos,0.0,1.0);if(abs(localUV.y)>streakH*1.2)continue;float taper=mix(1.3,0.4,t_pos*t_pos);float width=streakW*taper;float core=S(width,width*0.2,abs(localUV.x));float vertFade=S(0.0,0.1,t_pos)*S(1.0,0.85,t_pos);float streak=core*vertFade;if(streak>0.001){offset.x+=localUV.x*streak*0.5;offset.y+=(n1-0.5)*streak*0.1;}}}return offset;}vec2 FallingRain(vec2 uv,float t){vec2 totalOffset=vec2(0.0);if(u_fallingIntensity<0.01)return totalOffset;float speed=u_fallingSpeed*5.0;float streakLen=u_fallingStreakLength*0.3;for(int i=0;i<6;i++){if(i>=u_fallingLayers)break;float layerIdx=float(i);float depth=layerIdx/float(max(u_fallingLayers-1,1));float layerScale=mix(6.0,30.0,depth);float layerSpeed=speed*mix(2.0,0.5,depth);float layerDensity=u_fallingIntensity*mix(0.8,0.3,depth);float layerStrength=mix(1.0,0.15,depth);float layerStreakLen=streakLen*mix(1.5,0.4,depth);float layerAngle=u_fallingAngle*mix(1.0,0.6,depth);vec2 layerOffset=vec2(sin(layerIdx*73.156)*3.0,cos(layerIdx*37.842)*3.0);vec2 layer=FallingRainLayer(uv+layerOffset,t+layerIdx*0.13,layerSpeed,layerAngle,layerStreakLen,layerScale,layerDensity);totalOffset+=layer*layerStrength;}return totalOffset*0.4;}void main(){vec2 uv=(gl_FragCoord.xy-0.5*u_resolution.xy)/u_resolution.y;vec2 UV=v_uv;uv*=u_glassZoom;float t=u_time*0.2;float rainAmount=u_glassIntensity;float staticDrops=S(-0.5,1.0,rainAmount)*2.0;float layer1=S(0.25,0.75,rainAmount);float layer2=S(0.0,0.5,rainAmount);vec2 c=Drops(uv,t,staticDrops,layer1,layer2);vec2 e=vec2(0.001,0.0);float cx=Drops(uv+e,t,staticDrops,layer1,layer2).x;float cy=Drops(uv+e.yx,t,staticDrops,layer1,layer2).x;vec2 glassNormal=vec2(cx-c.x,cy-c.x);vec2 fallingRainOffset=FallingRain(uv,u_time);vec2 totalRefraction=(glassNormal+fallingRainOffset)*u_refractionStrength;vec2 refractedUV=UV+totalRefraction;refractedUV=clamp(refractedUV,0.0,1.0);vec4 scene=texture(u_sceneTexture,refractedUV);vec3 color=scene.rgb;float rainMagnitude=length(fallingRainOffset);if(rainMagnitude>0.001){float brightness=dot(scene.rgb,vec3(0.299,0.587,0.114));float specular=rainMagnitude*15.0*(0.1+brightness*0.9);color+=vec3(0.8,0.85,0.95)*specular*0.3;}color+=vec3(0.1,0.12,0.15)*c.x*0.5;fragColor=vec4(color,scene.a);}`,re=`#version 300 es\\nprecision highp float;in vec2 v_uv;out vec4 fragColor;uniform float u_time;uniform vec2 u_resolution;uniform sampler2D u_sceneTexture;uniform bool u_enabled;uniform float u_flashIntensity;uniform float u_branchDensity;uniform float u_sceneIllumination;uniform float u_lastFlashTime;uniform float u_strikeSeed;\\n#define MAX_SEGMENTS 32\\n#define MAX_BRANCHES 16\\n#define PI 3.14159265359\\nfloat easeOutSine(float t){return sin(t*PI*0.5);}float easeInSine(float t){return 1.0-cos(t*PI*0.5);}float easeInOutSine(float t){return-(cos(PI*t)-1.0)*0.5;}float easeOutQuad(float t){return 1.0-(1.0-t)*(1.0-t);}float easeOutCubic(float t){float inv=1.0-t;return 1.0-inv*inv*inv;}float hash11(float p){p=fract(p*0.1031);p*=p+33.33;p*=p+p;return fract(p);}float hash12(vec2 p){vec3 p3=fract(vec3(p.xyx)*0.1031);p3+=dot(p3,p3.yzx+33.33);return fract((p3.x+p3.y)*p3.z);}vec2 hash22(vec2 p){vec3 p3=fract(vec3(p.xyx)*vec3(0.1031,0.1030,0.0973));p3+=dot(p3,p3.yzx+33.33);return fract((p3.xx+p3.yz)*p3.zy);}float noise(vec2 p){vec2 i=floor(p);vec2 f=fract(p);f=f*f*(3.0-2.0*f);float a=hash12(i);float b=hash12(i+vec2(1.0,0.0));float c=hash12(i+vec2(0.0,1.0));float d=hash12(i+vec2(1.0,1.0));return mix(mix(a,b,f.x),mix(c,d,f.x),f.y);}float distToSegment(vec2 p,vec2 a,vec2 b){vec2 pa=p-a;vec2 ba=b-a;float h=clamp(dot(pa,ba)/dot(ba,ba),0.0,1.0);return length(pa-ba*h);}vec2 displacedPoint(vec2 start,vec2 end,float t,float seed,float displacementAmt){vec2 basePoint=mix(start,end,t);vec2 dir=end-start;vec2 perp=normalize(vec2(-dir.y,dir.x));float envelope=sin(t*PI);float n1=noise(vec2(t*8.0,seed*100.0))*2.0-1.0;float n2=noise(vec2(t*16.0,seed*100.0+50.0))*2.0-1.0;float n3=noise(vec2(t*32.0,seed*100.0+100.0))*2.0-1.0;float displacement=(n1*0.6+n2*0.3+n3*0.1)*envelope*displacementAmt;float targetBias=1.0-t*0.3;displacement*=targetBias;return basePoint+perp*displacement*length(dir);}float mainBoltDistance(vec2 uv,vec2 start,vec2 end,float seed,float displacementAmt){float minDist=999.0;vec2 prevPoint=start;for(int i=1;i<=MAX_SEGMENTS;i++){float t=float(i)/float(MAX_SEGMENTS);vec2 currPoint=displacedPoint(start,end,t,seed,displacementAmt);float d=distToSegment(uv,prevPoint,currPoint);minDist=min(minDist,d);prevPoint=currPoint;}return minDist;}float branchDistance(vec2 uv,vec2 branchStart,vec2 branchDir,float branchLen,float seed,float displacementAmt){vec2 branchEnd=branchStart+branchDir*branchLen;float minDist=999.0;vec2 prevPoint=branchStart;for(int i=1;i<=12;i++){float t=float(i)/12.0;vec2 currPoint=displacedPoint(branchStart,branchEnd,t,seed,displacementAmt*0.7);float d=distToSegment(uv,prevPoint,currPoint);minDist=min(minDist,d);prevPoint=currPoint;}return minDist;}vec2 branchesDistance(vec2 uv,vec2 start,vec2 end,float seed,float displacementAmt,float density){float minDist=999.0;float brightness=0.0;vec2 mainDir=normalize(end-start);float mainLen=length(end-start);for(int i=0;i<MAX_BRANCHES;i++){float idx=float(i);float branchT=0.15+hash11(seed+idx*7.31)*0.7;float branchProb=(1.0-branchT)*density;if(hash11(seed+idx*3.17)>branchProb)continue;vec2 branchStart=displacedPoint(start,end,branchT,seed,displacementAmt);float angleOffset=(hash11(seed+idx*11.13)*2.0-1.0)*0.6;float side=hash11(seed+idx*5.71)>0.5?1.0:-1.0;float angle=atan(mainDir.y,mainDir.x)+side*(0.3+abs(angleOffset)*0.5);vec2 branchDir=vec2(cos(angle),sin(angle));float branchLen=mainLen*(0.15+hash11(seed+idx*13.37)*0.25);float d=branchDistance(uv,branchStart,branchDir,branchLen,seed+idx*100.0,displacementAmt);if(d<minDist){minDist=d;brightness=0.5-branchT*0.2;}if(density>0.3&&hash11(seed+idx*17.19)<density*0.5){float subT=0.3+hash11(seed+idx*19.23)*0.4;vec2 subStart=branchStart+branchDir*branchLen*subT;float subAngle=angle+(hash11(seed+idx*23.29)*2.0-1.0)*0.5;vec2 subDir=vec2(cos(subAngle),sin(subAngle));float subLen=branchLen*0.4;float subD=branchDistance(uv,subStart,subDir,subLen,seed+idx*200.0,displacementAmt*0.5);if(subD<minDist){minDist=subD;brightness=0.25;}}}return vec2(minDist,brightness);}vec3 lightningGlow(float dist,float brightness,float intensity,float thickness){float scaledDist=dist/max(thickness,0.1);float core=smoothstep(0.003,0.0,scaledDist)*brightness;float innerGlow=exp(-scaledDist*150.0)*brightness;float outerGlow=exp(-dist*dist*3000.0)*brightness*thickness;vec3 coreColor=vec3(1.0,1.0,1.0);vec3 innerColor=vec3(0.7,0.8,1.0);vec3 outerColor=vec3(0.5,0.5,0.9);vec3 color=coreColor*core*2.0;color+=innerColor*innerGlow*0.8;color+=outerColor*outerGlow*0.5;return color*intensity;}float flashEnvelope(float timeSinceStrike,float duration){if(timeSinceStrike<0.0||timeSinceStrike>duration)return 0.0;float t=timeSinceStrike/duration;float attackT=clamp(t/0.03,0.0,1.0);float attack=easeOutCubic(attackT);float sustainT=clamp((t-0.05)/0.65,0.0,1.0);float sustain=1.0-easeInOutSine(sustainT);float decay=exp(-t*2.0);decay=mix(decay,easeOutSine(1.0-t),0.3);float endT=clamp((t-0.75)/0.25,0.0,1.0);float endFade=1.0-easeInSine(endT);return attack*max(sustain,decay*0.4)*endFade;}float restrikeEnvelope(float timeSinceStrike,float duration,float seed){float env=flashEnvelope(timeSinceStrike,duration*0.7);if(hash11(seed*7.7)>0.7){float restrike1=flashEnvelope(timeSinceStrike-duration*0.5,duration*0.3);env=max(env,restrike1*0.6);}if(hash11(seed*11.3)>0.85){float restrike2=flashEnvelope(timeSinceStrike-duration*0.75,duration*0.2);env=max(env,restrike2*0.4);}return env;}void main(){vec4 scene=texture(u_sceneTexture,v_uv);if(!u_enabled){fragColor=scene;return;}vec2 uv=v_uv;float aspect=u_resolution.x/u_resolution.y;uv.x*=aspect;float timeSinceStrike=u_time-u_lastFlashTime;float durationSec=0.8;float flash=restrikeEnvelope(timeSinceStrike,durationSec,u_strikeSeed);float afterimageDuration=durationSec*1.5;float afterimageT=clamp(timeSinceStrike/afterimageDuration,0.0,1.0);float afterimage=timeSinceStrike<0.0?0.0:(1.0-easeInSine(afterimageT));vec3 color=scene.rgb;if(flash>0.01||afterimage>0.01){vec2 strikeHash=hash22(vec2(u_strikeSeed*123.456,u_strikeSeed*789.012));vec2 boltStart=vec2((0.3+strikeHash.x*0.4)*aspect,1.05);vec2 boltEnd=vec2(boltStart.x+(strikeHash.x-0.5)*0.4,-0.05);float straightDist=distToSegment(uv,boltStart,boltEnd);float sourceGlow=exp(-length(uv-boltStart)*3.0);color+=vec3(0.4,0.45,0.6)*sourceGlow*afterimage*0.3;float distLimit=0.18+u_branchDensity*0.25+u_flashIntensity*0.05;float feather=0.08;float region=1.0-smoothstep(distLimit-feather,distLimit,straightDist);if(region<=0.0005){fragColor=vec4(color,scene.a);return;}float displacementAmt=0.15;float mainDist=mainBoltDistance(uv,boltStart,boltEnd,u_strikeSeed,displacementAmt);vec2 branchResult=branchesDistance(uv,boltStart,boltEnd,u_strikeSeed,displacementAmt,u_branchDensity);float branchDist=branchResult.x;float branchBrightness=branchResult.y;float mainThickness=mix(0.2,1.0,easeOutSine(sqrt(max(flash,0.0))));vec3 afterglowColor=vec3(0.5,0.45,0.7);vec3 mainCore=lightningGlow(mainDist,easeOutQuad(max(flash,0.0)),u_flashIntensity,mainThickness);float mainAfterglowDist=mainDist*0.6;float mainAfterglowStrength=exp(-mainAfterglowDist*50.0)*afterimage*0.5;vec3 mainAfterglow=afterglowColor*mainAfterglowStrength;float branchThickness=mix(0.15,1.0,easeOutSine(max(flash,0.0)));vec3 branchCore=lightningGlow(branchDist,branchBrightness*easeOutQuad(max(flash,0.0)),u_flashIntensity,branchThickness);float branchAfterglowDist=branchDist*0.7;float branchAfterglowStrength=exp(-branchAfterglowDist*80.0)*branchBrightness*afterimage*0.4;vec3 branchAfterglow=afterglowColor*branchAfterglowStrength;color+=(mainCore+branchCore)*max(flash,0.0)*region;color+=(mainAfterglow+branchAfterglow)*afterimage*region;}fragColor=vec4(color,scene.a);}`,le=`#version 300 es\\nprecision highp float;in vec2 v_uv;out vec4 fragColor;uniform float u_time;uniform vec2 u_resolution;uniform sampler2D u_sceneTexture;uniform float u_intensity;uniform int u_layers;uniform float u_fallSpeed;uniform float u_windSpeed;uniform float u_windAngle;uniform float u_turbulence;uniform float u_drift;uniform float u_flutter;uniform float u_windShear;uniform float u_flakeSize;uniform float u_sizeVariation;uniform float u_opacity;uniform float u_glowAmount;uniform float u_sparkle;\\n#define PI 3.14159265359\\n#define MAX_LAYERS 6\\nfloat hash12(vec2 p){vec3 p3=fract(vec3(p.xyx)*0.1031);p3+=dot(p3,p3.yzx+33.33);return fract((p3.x+p3.y)*p3.z);}vec2 hash22(vec2 p){vec3 p3=fract(vec3(p.xyx)*vec3(0.1031,0.1030,0.0973));p3+=dot(p3,p3.yzx+33.33);return fract((p3.xx+p3.yz)*p3.zy);}float noise(vec2 p){vec2 i=floor(p);vec2 f=fract(p);f=f*f*(3.0-2.0*f);float a=hash12(i);float b=hash12(i+vec2(1.0,0.0));float c=hash12(i+vec2(0.0,1.0));float d=hash12(i+vec2(1.0,1.0));return mix(mix(a,b,f.x),mix(c,d,f.x),f.y);}vec2 rotate2D(vec2 p,float angle){float c=cos(angle);float s=sin(angle);return vec2(p.x*c-p.y*s,p.x*s+p.y*c);}float snowflakeShape(vec2 uv,float size,float seed,float rotation){vec2 rotatedUV=rotate2D(uv,rotation);float dist=length(rotatedUV);float circle=smoothstep(size,size*0.3,dist);float angle=atan(rotatedUV.y,rotatedUV.x);float hexPattern=0.5+0.5*cos(angle*6.0);hexPattern=pow(hexPattern,2.0);float crystalAmount=smoothstep(0.02,0.05,size)*0.3;float shape=mix(circle,circle*(0.7+hexPattern*0.3),crystalAmount);float glow=exp(-dist*dist/(size*size*3.0))*u_glowAmount;return shape+glow*0.4;}vec2 getWind(float layerDepth){vec2 baseWind=vec2(cos(u_windAngle),0.0)*u_windSpeed;float windResponse=mix(0.3,1.0,1.0-layerDepth);float shearResponse=1.0+u_windShear*(1.0-layerDepth)*0.35;return baseWind*windResponse*shearResponse;}float sparkle(vec2 cellId,float time,float seed){float sparklePhase=hash12(cellId+vec2(seed*100.0,0.0))*100.0;float sparkleFreq=2.0+hash12(cellId+vec2(0.0,seed*100.0))*3.0;float sparkleWave=sin(time*sparkleFreq+sparklePhase);float sparkleIntensity=pow(max(0.0,sparkleWave),16.0);float sparkleProbability=hash12(cellId+vec2(floor(time*0.5),0.0));sparkleIntensity*=step(0.85,sparkleProbability);return sparkleIntensity*u_sparkle;}vec3 snowLayer(vec2 uv,float time,float layerIndex,float totalLayers){float depth=layerIndex/max(1.0,totalLayers-1.0);float layerScale=mix(8.0,40.0,depth);float layerSpeed=u_fallSpeed*mix(1.2,0.4,depth);float layerDensity=u_intensity*mix(1.0,0.5,depth);float layerFlakeSize=u_flakeSize*mix(1.5,0.3,depth);float layerOpacity=u_opacity*mix(1.0,0.4,depth);vec2 layerOffset=vec2(sin(layerIndex*73.156)*10.0,cos(layerIndex*37.842)*10.0);vec2 p=(uv+layerOffset)*layerScale;p.y+=time*layerSpeed*2.0;vec2 baseWind=getWind(depth);p.x+=time*baseWind.x*0.3;vec2 id=floor(p);vec2 gv=fract(p)-0.5;float snow=0.0;float sparkleAccum=0.0;for(int y=-1;y<=1;y++){for(int x=-1;x<=1;x++){vec2 offs=vec2(float(x),float(y));vec2 cellId=id+offs;float h1=hash12(cellId);vec2 h2=hash22(cellId);float h3=hash12(cellId+vec2(127.0,311.0));float h4=hash12(cellId+vec2(271.0,183.0));if(h1>layerDensity)continue;float sizeVar=1.0+(h3-0.5)*u_sizeVariation;float size=layerFlakeSize*sizeVar*0.04;vec2 flakePos=h2*0.8-0.4;float flutterPhase=h3*PI*2.0;float flutterAmp=u_flutter*0.15*(1.0-depth);flakePos.x+=sin(time*3.0+flutterPhase)*flutterAmp;flakePos.y+=cos(time*2.5+flutterPhase*1.3)*flutterAmp*0.5;float driftPhase=h4*PI*2.0+layerIndex*1.7;flakePos.x+=sin(time*0.55+driftPhase)*u_drift*0.18;float turbFreq=0.6+u_turbulence*1.4;vec2 turb=vec2(noise(cellId*0.17+time*turbFreq),noise(cellId.yx*0.17+time*turbFreq+17.0))-0.5;flakePos+=turb*(u_turbulence*0.22)*(1.0-depth);vec2 localUV=gv-offs-flakePos;float rotationSpeed=(1.5-sizeVar*0.5)*(0.5+h4*1.0);float rotationPhase=h4*PI*2.0;float rotation=time*rotationSpeed+rotationPhase;float flake=snowflakeShape(localUV,size,h1,rotation);float flakeSparkle=sparkle(cellId,time,h1)*flake;sparkleAccum+=flakeSparkle;snow+=flake*layerOpacity;}}return vec3(snow,sparkleAccum,depth);}void main(){vec4 scene=texture(u_sceneTexture,v_uv);vec2 uv=v_uv;float aspect=u_resolution.x/u_resolution.y;uv.x*=aspect;float snow=0.0;float totalSparkle=0.0;for(int i=u_layers-1;i>=0;i--){vec3 layerResult=snowLayer(uv,u_time,float(i),float(u_layers));snow+=layerResult.x;totalSparkle+=layerResult.y;}snow=clamp(snow,0.0,1.0);totalSparkle=clamp(totalSparkle,0.0,1.0);vec3 snowColor=vec3(0.75,0.78,0.85);vec3 sparkleColor=vec3(0.9,0.92,1.0);vec3 color=scene.rgb+snowColor*snow+sparkleColor*totalSparkle;fragColor=vec4(color,scene.a);}`,ue=`#version 300 es\\nprecision highp float;in vec2 v_uv;out vec4 fragColor;uniform sampler2D u_sceneTexture;uniform float u_time;uniform vec2 u_resolution;uniform float u_timeOfDay;uniform vec2 u_sunPos;uniform float u_sunVisible;uniform float u_lastFlashTime;uniform float u_strikeSeed;uniform float u_lightningSceneIllumination;uniform bool u_postEnabled;uniform float u_haze;uniform float u_hazeHorizon;uniform float u_hazeDesaturation;uniform float u_hazeContrast;uniform float u_bloomIntensity;uniform float u_bloomThreshold;uniform float u_bloomKnee;uniform float u_bloomRadius;uniform float u_bloomTapScale;uniform float u_exposureIntensity;uniform float u_exposureDesaturation;uniform float u_exposureRecovery;uniform float u_godRayIntensity;uniform float u_godRayDecay;uniform float u_godRayDensity;uniform float u_godRayWeight;uniform int u_godRaySamples;\\n#define PI 3.14159265359\\n#define GODRAY_MAX_SAMPLES 32\\nfloat saturate(float x){return clamp(x,0.0,1.0);}float luminance(vec3 c){return dot(c,vec3(0.299,0.587,0.114));}float easeOutSine(float t){return sin(t*PI*0.5);}float easeInSine(float t){return 1.0-cos(t*PI*0.5);}float easeInOutSine(float t){return-(cos(PI*t)-1.0)*0.5;}float easeOutQuad(float t){return 1.0-(1.0-t)*(1.0-t);}float easeOutCubic(float t){float inv=1.0-t;return 1.0-inv*inv*inv;}float hash11(float p){p=fract(p*0.1031);p*=p+33.33;p*=p+p;return fract(p);}float flashEnvelope(float timeSinceStrike,float duration){if(timeSinceStrike<0.0||timeSinceStrike>duration)return 0.0;float t=timeSinceStrike/duration;float attackT=clamp(t/0.03,0.0,1.0);float attack=easeOutCubic(attackT);float sustainT=clamp((t-0.05)/0.65,0.0,1.0);float sustain=1.0-easeInOutSine(sustainT);float decay=exp(-t*2.0);decay=mix(decay,easeOutSine(1.0-t),0.3);float endT=clamp((t-0.75)/0.25,0.0,1.0);float endFade=1.0-easeInSine(endT);return attack*max(sustain,decay*0.4)*endFade;}float restrikeEnvelope(float timeSinceStrike,float duration,float seed){float env=flashEnvelope(timeSinceStrike,duration*0.7);if(hash11(seed*7.7)>0.7){float restrike1=flashEnvelope(timeSinceStrike-duration*0.5,duration*0.3);env=max(env,restrike1*0.6);}if(hash11(seed*11.3)>0.85){float restrike2=flashEnvelope(timeSinceStrike-duration*0.75,duration*0.2);env=max(env,restrike2*0.4);}return env;}float getSunAltitudeFromTimeOfDay(float timeOfDay){float sunAlt=timeOfDay<0.5?timeOfDay*2.0:2.0-timeOfDay*2.0;return sunAlt*2.0-1.0;}vec3 applyHaze(vec3 color,vec2 uv){float haze=saturate(u_haze);if(haze<=0.0001)return color;float horizon=pow(1.0-uv.y,1.8);float hazeWeight=haze*mix(1.0,horizon,saturate(u_hazeHorizon));float sunAlt=getSunAltitudeFromTimeOfDay(u_timeOfDay);float daylight=smoothstep(-0.12,0.1,sunAlt);vec3 hazeDay=vec3(0.60,0.70,0.85);vec3 hazeNight=vec3(0.06,0.07,0.10);vec3 hazeColor=mix(hazeNight,hazeDay,daylight);color=mix(color,hazeColor,hazeWeight*0.55);float contrast=saturate(1.0-hazeWeight*saturate(u_hazeContrast));color=mix(vec3(0.5),color,contrast);float gray=luminance(color);float sat=saturate(1.0-hazeWeight*saturate(u_hazeDesaturation));color=mix(vec3(gray),color,sat);return color;}vec3 bloomTap(vec2 uv){vec3 c=texture(u_sceneTexture,clamp(uv,0.0,1.0)).rgb;float l=luminance(c);float knee=max(0.0001,u_bloomKnee);float m=smoothstep(u_bloomThreshold,u_bloomThreshold+knee,l);return c*m;}vec3 computeBloom(vec2 uv){float intensity=u_bloomIntensity;if(intensity<=0.0001)return vec3(0.0);vec2 texel=1.0/max(u_resolution,vec2(1.0));float radiusPx=max(0.0,u_bloomRadius)*(u_resolution.y*0.02);radiusPx*=max(0.25,u_bloomTapScale);vec2 d=texel*radiusPx;vec3 sum=vec3(0.0);sum+=bloomTap(uv)*0.20;sum+=bloomTap(uv+vec2(d.x,0.0))*0.12;sum+=bloomTap(uv+vec2(-d.x,0.0))*0.12;sum+=bloomTap(uv+vec2(0.0,d.y))*0.12;sum+=bloomTap(uv+vec2(0.0,-d.y))*0.12;sum+=bloomTap(uv+vec2(d.x,d.y))*0.08;sum+=bloomTap(uv+vec2(-d.x,d.y))*0.08;sum+=bloomTap(uv+vec2(d.x,-d.y))*0.08;sum+=bloomTap(uv+vec2(-d.x,-d.y))*0.08;return sum*intensity;}vec3 applyExposureResponse(vec3 color,float flashStrength){if(flashStrength<=0.0001)return color;float t=saturate(flashStrength);float gain=1.0+flashStrength*2.2;vec3 lifted=color*gain;vec3 tonemapped=1.0-exp(-lifted);vec3 outColor=mix(color,tonemapped,t);float gray=luminance(outColor);float desat=saturate(u_exposureDesaturation)*t;outColor=mix(outColor,vec3(gray),desat);outColor=mix(outColor,vec3(1.0),t*0.06);return outColor;}vec3 computeGodRays(vec2 uv){if(u_godRayIntensity<=0.0001)return vec3(0.0);if(u_godRaySamples<=0)return vec3(0.0);if(u_sunVisible<=0.001)return vec3(0.0);float sunAlt=getSunAltitudeFromTimeOfDay(u_timeOfDay);float daylight=smoothstep(-0.12,0.1,sunAlt);float lowSun=1.0-smoothstep(0.25,0.75,max(0.0,sunAlt));vec2 sunUV=clamp(u_sunPos,vec2(-0.25),vec2(1.25));vec2 delta=(uv-sunUV)*(u_godRayDensity/float(u_godRaySamples));vec2 coord=uv;float illuminationDecay=1.0;float accum=0.0;for(int i=0;i<GODRAY_MAX_SAMPLES;i++){if(i>=u_godRaySamples)break;coord-=delta;vec4 s=texture(u_sceneTexture,clamp(coord,0.0,1.0));float transmittance=1.0-saturate(s.a);float sampleLum=luminance(s.rgb);float brightMask=saturate((sampleLum-0.85)/0.15);brightMask*=brightMask;float raySample=transmittance*brightMask;accum+=raySample*illuminationDecay*u_godRayWeight;illuminationDecay*=u_godRayDecay;}vec3 rayColor=mix(vec3(0.7,0.72,0.8),vec3(1.0,0.92,0.75),daylight);float intensity=u_godRayIntensity*saturate(u_sunVisible)*daylight*lowSun;return rayColor*accum*intensity;}void main(){vec4 scene=texture(u_sceneTexture,v_uv);vec3 color=scene.rgb;if(!u_postEnabled){fragColor=vec4(color,1.0);return;}color+=computeGodRays(v_uv);color+=computeBloom(v_uv);float flashStrength=0.0;if(u_exposureIntensity>0.0001){float timeSinceStrike=u_time-u_lastFlashTime;float durationSec=0.8;float f=restrikeEnvelope(timeSinceStrike,durationSec,u_strikeSeed);float afterimageDuration=durationSec*1.5;float afterT=clamp((timeSinceStrike*max(0.05,u_exposureRecovery))/afterimageDuration,0.0,1.0);float afterimage=timeSinceStrike<0.0?0.0:(1.0-easeInSine(afterT));float sceneFlash=f*max(0.0,u_lightningSceneIllumination);color+=vec3(0.3,0.32,0.4)*sceneFlash;flashStrength=f*u_exposureIntensity;flashStrength=max(flashStrength,afterimage*u_exposureIntensity*0.12);color=applyExposureResponse(color,flashStrength);}color=applyHaze(color,v_uv);fragColor=vec4(clamp(color,0.0,1.0),1.0);}`;function pe(e,t,a){if(e.isContextLost())return null;let n=e.createShader(t);if(!n)return null;if(e.shaderSource(n,a),e.compileShader(n),!e.getShaderParameter(n,e.COMPILE_STATUS)){let s=e.getShaderInfoLog(n);if(!e.isContextLost()){let c=t===e.VERTEX_SHADER?\\\"vertex\\\":t===e.FRAGMENT_SHADER?\\\"fragment\\\":String(t);console.error(`Shader compile error (${c}):`,s??\\\"(no info log)\\\")}return e.deleteShader(n),null}return n}function B(e,t,a){if(e.isContextLost())return null;let n=pe(e,e.VERTEX_SHADER,t),s=pe(e,e.FRAGMENT_SHADER,a);if(!n||!s)return n&&e.deleteShader(n),s&&e.deleteShader(s),null;let c=e.createProgram();if(!c)return e.deleteShader(n),e.deleteShader(s),null;if(e.attachShader(c,n),e.attachShader(c,s),e.linkProgram(c),!e.getProgramParameter(c,e.LINK_STATUS)){let l=e.getProgramInfoLog(c);return e.isContextLost()||console.error(\\\"Program link error:\\\",l??\\\"(no info log)\\\"),e.deleteProgram(c),e.deleteShader(n),e.deleteShader(s),null}return e.detachShader(c,n),e.detachShader(c,s),e.deleteShader(n),e.deleteShader(s),c}function ce(e,t,a){let n=e.createTexture();if(!n)return null;e.bindTexture(e.TEXTURE_2D,n),e.texImage2D(e.TEXTURE_2D,0,e.RGBA,t,a,0,e.RGBA,e.UNSIGNED_BYTE,null),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE);let s=e.createFramebuffer();if(!s)return e.deleteTexture(n),null;e.bindFramebuffer(e.FRAMEBUFFER,s),e.framebufferTexture2D(e.FRAMEBUFFER,e.COLOR_ATTACHMENT0,e.TEXTURE_2D,n,0);let c=e.checkFramebufferStatus(e.FRAMEBUFFER);return c!==e.FRAMEBUFFER_COMPLETE?(e.isContextLost()||console.error(\\\"Framebuffer incomplete:\\\",c),e.deleteFramebuffer(s),e.deleteTexture(n),e.bindFramebuffer(e.FRAMEBUFFER,null),e.bindTexture(e.TEXTURE_2D,null),null):(e.bindFramebuffer(e.FRAMEBUFFER,null),e.bindTexture(e.TEXTURE_2D,null),{fbo:s,texture:n,width:t,height:a})}function fe(e,t,a,n){t.width===a&&t.height===n||(e.bindTexture(e.TEXTURE_2D,t.texture),e.texImage2D(e.TEXTURE_2D,0,e.RGBA,a,n,0,e.RGBA,e.UNSIGNED_BYTE,null),e.bindTexture(e.TEXTURE_2D,null),t.width=a,t.height=n)}function J(e,t,a){let n=Math.max(0,Math.min(1,(a-e)/(t-e)));return n*n*(3-2*n)}function Se(e){let t=e.timeOfDay,a=e.celestialY,n=-.25,s=J(.18,.32,t),c=J(.68,.82,t),l=s*(1-c),o=n+(a-n)*l,r=J(.74,.88,t),i=1-J(.12,.26,t),h=Math.max(r,i),f=n+(a-n)*h,p=l>h,b=p?o:f,R=p?e.sunSize:e.moonSize,C=p?Math.min(1,e.sunGlowIntensity*.3)*l:Math.min(.5,e.moonGlowIntensity*.15)*h;return{sunY:o,moonY:f,sunVisible:l,moonVisible:h,activeY:b,activeSize:R,activeBrightness:C}}function Z(e,t,a,n,s){e.bindFramebuffer(e.FRAMEBUFFER,a.fbo),e.viewport(0,0,n,s),e.useProgram(t)}function K(e,t,a,n){e.activeTexture(e.TEXTURE0),e.bindTexture(e.TEXTURE_2D,a),e.uniform1i(n(t,\\\"u_sceneTexture\\\"),0)}function ve(e,t,a,n){e.bindFramebuffer(e.FRAMEBUFFER,t.fbo),e.viewport(0,0,a,n),e.clearColor(0,0,0,0),e.clear(e.COLOR_BUFFER_BIT)}function be({gl:e,program:t,target:a,displayWidth:n,displayHeight:s,time:c,params:l,moonTexture:o,moonTextureLoaded:r,getUniformLocation:i}){Z(e,t,a,n,s),e.uniform1f(i(t,\\\"u_time\\\"),c),e.uniform2f(i(t,\\\"u_resolution\\\"),n,s),e.uniform1f(i(t,\\\"u_timeOfDay\\\"),l.timeOfDay),e.uniform1f(i(t,\\\"u_moonPhase\\\"),l.moonPhase),e.uniform1f(i(t,\\\"u_starDensity\\\"),l.starDensity),e.uniform2f(i(t,\\\"u_celestialPos\\\"),l.celestialX,l.celestialY),e.uniform1f(i(t,\\\"u_sunSize\\\"),l.sunSize),e.uniform1f(i(t,\\\"u_moonSize\\\"),l.moonSize),e.uniform1f(i(t,\\\"u_sunGlowIntensity\\\"),l.sunGlowIntensity),e.uniform1f(i(t,\\\"u_sunGlowSize\\\"),l.sunGlowSize),e.uniform1f(i(t,\\\"u_sunRayCount\\\"),l.sunRayCount),e.uniform1f(i(t,\\\"u_sunRayLength\\\"),l.sunRayLength),e.uniform1f(i(t,\\\"u_sunRayIntensity\\\"),l.sunRayIntensity),e.uniform1f(i(t,\\\"u_sunRayShimmer\\\"),l.sunRayShimmer),e.uniform1f(i(t,\\\"u_sunRayShimmerSpeed\\\"),l.sunRayShimmerSpeed),e.uniform1f(i(t,\\\"u_moonGlowIntensity\\\"),l.moonGlowIntensity),e.uniform1f(i(t,\\\"u_moonGlowSize\\\"),l.moonGlowSize),e.uniform1f(i(t,\\\"u_skyBrightness\\\"),l.skyBrightness),e.uniform1f(i(t,\\\"u_skySaturation\\\"),l.skySaturation),e.uniform1f(i(t,\\\"u_skyContrast\\\"),l.skyContrast),e.activeTexture(e.TEXTURE0),e.bindTexture(e.TEXTURE_2D,o),e.uniform1i(i(t,\\\"u_moonTexture\\\"),0),e.uniform1i(i(t,\\\"u_hasMoonTexture\\\"),r?1:0),e.drawArrays(e.TRIANGLES,0,6)}function we({gl:e,program:t,target:a,sceneTexture:n,displayWidth:s,displayHeight:c,time:l,params:o,celestial:r,getUniformLocation:i}){Z(e,t,a,s,c),K(e,t,n,i),e.uniform1f(i(t,\\\"u_time\\\"),l),e.uniform2f(i(t,\\\"u_resolution\\\"),s,c),e.uniform1f(i(t,\\\"u_timeOfDay\\\"),r.timeOfDay),e.uniform1f(i(t,\\\"u_coverage\\\"),o.coverage),e.uniform1f(i(t,\\\"u_density\\\"),o.density),e.uniform1f(i(t,\\\"u_softness\\\"),o.softness),e.uniform1f(i(t,\\\"u_windSpeed\\\"),o.windSpeed),e.uniform1f(i(t,\\\"u_windAngle\\\"),o.windAngle),e.uniform1f(i(t,\\\"u_turbulence\\\"),o.turbulence),e.uniform1f(i(t,\\\"u_lightIntensity\\\"),o.lightIntensity),e.uniform1f(i(t,\\\"u_ambientDarkness\\\"),o.ambientDarkness),e.uniform1i(i(t,\\\"u_numLayers\\\"),o.numLayers),e.uniform1f(i(t,\\\"u_cloudScale\\\"),o.cloudScale);let h=Se(r);e.uniform2f(i(t,\\\"u_celestialPos\\\"),r.celestialX,h.activeY),e.uniform1f(i(t,\\\"u_celestialSize\\\"),h.activeSize),e.uniform1f(i(t,\\\"u_celestialBrightness\\\"),h.activeBrightness),e.uniform1f(i(t,\\\"u_backlightIntensity\\\"),o.backlightIntensity),e.drawArrays(e.TRIANGLES,0,6)}function xe({gl:e,program:t,target:a,sceneTexture:n,displayWidth:s,displayHeight:c,time:l,params:o,interactions:r,getUniformLocation:i}){Z(e,t,a,s,c),K(e,t,n,i),e.uniform1f(i(t,\\\"u_time\\\"),l),e.uniform2f(i(t,\\\"u_resolution\\\"),s,c),e.uniform1f(i(t,\\\"u_glassIntensity\\\"),o.glassIntensity),e.uniform1f(i(t,\\\"u_glassZoom\\\"),o.glassZoom),e.uniform1f(i(t,\\\"u_fallingIntensity\\\"),o.fallingIntensity),e.uniform1f(i(t,\\\"u_fallingSpeed\\\"),o.fallingSpeed),e.uniform1f(i(t,\\\"u_fallingAngle\\\"),o.fallingAngle),e.uniform1f(i(t,\\\"u_fallingStreakLength\\\"),o.fallingStreakLength),e.uniform1i(i(t,\\\"u_fallingLayers\\\"),o.fallingLayers),e.uniform1f(i(t,\\\"u_refractionStrength\\\"),r.rainRefractionStrength),e.drawArrays(e.TRIANGLES,0,6)}function Ie(e,t,a,n,s){if(!e.lightning||!a||!t.enabled)return!1;let l=.8*1.5,o=n-s;return o>=0&&o<=l}function Re({gl:e,program:t,target:a,sceneTexture:n,displayWidth:s,displayHeight:c,time:l,params:o,interactions:r,lastFlashTime:i,strikeSeed:h,getUniformLocation:f}){Z(e,t,a,s,c),K(e,t,n,f),e.uniform1f(f(t,\\\"u_time\\\"),l),e.uniform2f(f(t,\\\"u_resolution\\\"),s,c),e.uniform1i(f(t,\\\"u_enabled\\\"),o.enabled?1:0),e.uniform1f(f(t,\\\"u_flashIntensity\\\"),o.flashIntensity),e.uniform1f(f(t,\\\"u_branchDensity\\\"),o.branchDensity),e.uniform1f(f(t,\\\"u_sceneIllumination\\\"),r.lightningSceneIllumination),e.uniform1f(f(t,\\\"u_lastFlashTime\\\"),i),e.uniform1f(f(t,\\\"u_strikeSeed\\\"),h),e.drawArrays(e.TRIANGLES,0,6)}function ke({gl:e,program:t,target:a,sceneTexture:n,displayWidth:s,displayHeight:c,time:l,params:o,getUniformLocation:r}){Z(e,t,a,s,c),K(e,t,n,r),e.uniform1f(r(t,\\\"u_time\\\"),l),e.uniform2f(r(t,\\\"u_resolution\\\"),s,c),e.uniform1f(r(t,\\\"u_intensity\\\"),o.intensity),e.uniform1i(r(t,\\\"u_layers\\\"),o.layers),e.uniform1f(r(t,\\\"u_fallSpeed\\\"),o.fallSpeed),e.uniform1f(r(t,\\\"u_windSpeed\\\"),o.windSpeed),e.uniform1f(r(t,\\\"u_windAngle\\\"),o.windAngle),e.uniform1f(r(t,\\\"u_turbulence\\\"),o.turbulence),e.uniform1f(r(t,\\\"u_drift\\\"),o.drift),e.uniform1f(r(t,\\\"u_flutter\\\"),o.flutter),e.uniform1f(r(t,\\\"u_windShear\\\"),o.windShear),e.uniform1f(r(t,\\\"u_flakeSize\\\"),o.flakeSize),e.uniform1f(r(t,\\\"u_sizeVariation\\\"),o.sizeVariation),e.uniform1f(r(t,\\\"u_opacity\\\"),o.opacity),e.uniform1f(r(t,\\\"u_glowAmount\\\"),o.glowAmount),e.uniform1f(r(t,\\\"u_sparkle\\\"),o.sparkle),e.drawArrays(e.TRIANGLES,0,6)}function _e({gl:e,program:t,sceneTexture:a,displayWidth:n,displayHeight:s,time:c,celestial:l,interactions:o,post:r,lastFlashTime:i,strikeSeed:h,getUniformLocation:f}){e.bindFramebuffer(e.FRAMEBUFFER,null),e.viewport(0,0,n,s),e.useProgram(t),K(e,t,a,f),e.uniform1f(f(t,\\\"u_time\\\"),c),e.uniform2f(f(t,\\\"u_resolution\\\"),n,s),e.uniform1f(f(t,\\\"u_timeOfDay\\\"),l.timeOfDay);let p=Se(l);e.uniform2f(f(t,\\\"u_sunPos\\\"),l.celestialX,p.sunY),e.uniform1f(f(t,\\\"u_sunVisible\\\"),p.sunVisible),e.uniform1f(f(t,\\\"u_lastFlashTime\\\"),i),e.uniform1f(f(t,\\\"u_strikeSeed\\\"),h),e.uniform1f(f(t,\\\"u_lightningSceneIllumination\\\"),o.lightningSceneIllumination),e.uniform1i(f(t,\\\"u_postEnabled\\\"),r.enabled?1:0),e.uniform1f(f(t,\\\"u_haze\\\"),r.haze),e.uniform1f(f(t,\\\"u_hazeHorizon\\\"),r.hazeHorizon),e.uniform1f(f(t,\\\"u_hazeDesaturation\\\"),r.hazeDesaturation),e.uniform1f(f(t,\\\"u_hazeContrast\\\"),r.hazeContrast),e.uniform1f(f(t,\\\"u_bloomIntensity\\\"),r.bloomIntensity),e.uniform1f(f(t,\\\"u_bloomThreshold\\\"),r.bloomThreshold),e.uniform1f(f(t,\\\"u_bloomKnee\\\"),r.bloomKnee),e.uniform1f(f(t,\\\"u_bloomRadius\\\"),r.bloomRadius),e.uniform1f(f(t,\\\"u_bloomTapScale\\\"),r.bloomTapScale),e.uniform1f(f(t,\\\"u_exposureIntensity\\\"),r.exposureIntensity),e.uniform1f(f(t,\\\"u_exposureDesaturation\\\"),r.exposureDesaturation),e.uniform1f(f(t,\\\"u_exposureRecovery\\\"),r.exposureRecovery),e.uniform1f(f(t,\\\"u_godRayIntensity\\\"),r.godRayIntensity),e.uniform1f(f(t,\\\"u_godRayDecay\\\"),r.godRayDecay),e.uniform1f(f(t,\\\"u_godRayDensity\\\"),r.godRayDensity),e.uniform1f(f(t,\\\"u_godRayWeight\\\"),r.godRayWeight),e.uniform1i(f(t,\\\"u_godRaySamples\\\"),Math.max(0,Math.min(32,Math.floor(r.godRaySamples)))),e.drawArrays(e.TRIANGLES,0,6)}function Ce(e){let t=v(null),a=v(null),n=v(0),s=v(0),c=v(-100),l=v(0),o=v(0),r=v(null),i=v(!1),h=v(null),f=v(new WeakMap),p=v(!1),b=v(!1),R=v(!1),C=v(!1),A=v(null),k=v(e);k.current=e;let w=v({celestial:null,cloud:null,rain:null,lightning:null,snow:null,composite:null}),T=v({a:null,b:null}),Q=N((d,u,y)=>{let g=f.current.get(u);g||(g=new Map,f.current.set(u,g));let m=g.get(y);if(m!==void 0)return m;let E=d.getUniformLocation(u,y);return g.set(y,E),E},[]),U=N(()=>{n.current&&(cancelAnimationFrame(n.current),n.current=0),b.current=!1},[]),$=N(d=>{d&&A.current&&ae(d),A.current=null},[]),G=N(()=>{U();let d=a.current,u=R.current;if(d&&!u){for(let y of Object.values(w.current))y&&d.deleteProgram(y);for(let y of[T.current.a,T.current.b])y&&(d.deleteFramebuffer(y.fbo),d.deleteTexture(y.texture));r.current&&d.deleteTexture(r.current),h.current&&d.deleteBuffer(h.current)}w.current={celestial:null,cloud:null,rain:null,lightning:null,snow:null,composite:null},T.current={a:null,b:null},r.current=null,i.current=!1,h.current=null,a.current=null,f.current=new WeakMap},[U]),z=N(({canvas:d,contextLost:u=!1,markInitFailed:y=!0,warnMessage:g,errorMessage:m})=>(u&&(R.current=!0),y&&(C.current=!0),m&&console.error(m),G(),A.current=ge(d,A.current),!1),[G]),V=N(()=>{if(C.current)return!1;let d=t.current;if(!d||A.current===!1)return!1;if(A.current===null){if(!he(d))return A.current=!1,!1;A.current=!0}G(),R.current=!1;let u=d.getContext(\\\"webgl2\\\");if(!u)return z({canvas:d,warnMessage:\\\"[WeatherEffectsCanvas] WebGL2 not supported; rendering without effects.\\\"});if(a.current=u,u.isContextLost())return z({canvas:d,contextLost:!0,markInitFailed:!1});if(w.current.celestial=B(u,O,se),w.current.cloud=B(u,O,oe),w.current.rain=B(u,O,ie),w.current.lightning=B(u,O,re),w.current.snow=B(u,O,le),w.current.composite=B(u,O,ue),!w.current.celestial||!w.current.composite)return u.isContextLost()?z({canvas:d,contextLost:!0,markInitFailed:!1}):z({canvas:d,errorMessage:\\\"Failed to create required WebGL programs\\\"});let y=k.current.dpr??window.devicePixelRatio,g=Math.max(1,Math.floor(d.clientWidth*y)),m=Math.max(1,Math.floor(d.clientHeight*y)),E=ce(u,g,m),x=ce(u,g,m);if(!E||!x)return u.isContextLost()?z({canvas:d,contextLost:!0,markInitFailed:!1}):z({canvas:d,errorMessage:\\\"Failed to create WebGL framebuffers\\\"});T.current.a=E,T.current.b=x;let I=u.createTexture();if(I){u.bindTexture(u.TEXTURE_2D,I),u.texImage2D(u.TEXTURE_2D,0,u.RGBA,1,1,0,u.RGBA,u.UNSIGNED_BYTE,new Uint8Array([128,128,128,255])),r.current=I;let _=new Image;_.crossOrigin=\\\"anonymous\\\",_.onload=()=>{let S=a.current;!S||r.current!==I||(S.bindTexture(u.TEXTURE_2D,I),S.texImage2D(u.TEXTURE_2D,0,u.RGBA,u.RGBA,u.UNSIGNED_BYTE,_),S.generateMipmap(u.TEXTURE_2D),S.texParameteri(u.TEXTURE_2D,u.TEXTURE_MIN_FILTER,u.LINEAR_MIPMAP_LINEAR),S.texParameteri(u.TEXTURE_2D,u.TEXTURE_MAG_FILTER,u.LINEAR),S.texParameteri(u.TEXTURE_2D,u.TEXTURE_WRAP_S,u.REPEAT),S.texParameteri(u.TEXTURE_2D,u.TEXTURE_WRAP_T,u.CLAMP_TO_EDGE),i.current=!0)},_.src=\\\"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAZABkAAD/4QB0RXhpZgAATU0AKgAAAAgABAEaAAUAAAABAAAAPgEbAAUAAAABAAAARgEoAAMAAAABAAIAAIdpAAQAAAABAAAATgAAAAAAAABkAAAAAQAAAGQAAAABAAKgAgAEAAAAAQAAAQCgAwAEAAAAAQAAAIAAAAAA/+0AOFBob3Rvc2hvcCAzLjAAOEJJTQQEAAAAAAAAOEJJTQQlAAAAAAAQ1B2M2Y8AsgTpgAmY7PhCfv/iAdhJQ0NfUFJPRklMRQABAQAAAcgAAAAABDAAAG1udHJSR0IgWFlaIAfgAAEAAQAAAAAAAGFjc3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD21gABAAAAANMtAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACWRlc2MAAADwAAAAJHJYWVoAAAEUAAAAFGdYWVoAAAEoAAAAFGJYWVoAAAE8AAAAFHd0cHQAAAFQAAAAFHJUUkMAAAFkAAAAKGdUUkMAAAFkAAAAKGJUUkMAAAFkAAAAKGNwcnQAAAGMAAAAPG1sdWMAAAAAAAAAAQAAAAxlblVTAAAACAAAABwAcwBSAEcAQlhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z1hZWiAAAAAAAAD21gABAAAAANMtcGFyYQAAAAAABAAAAAJmZgAA8qcAAA1ZAAAT0AAAClsAAAAAAAAAAG1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANv/AABEIAIABAAMBIgACEQEDEQH/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgv/xAC1EAACAQMDAgQDBQUEBAAAAX0BAgMABBEFEiExQQYTUWEHInEUMoGRoQgjQrHBFVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4eLj5OXm5+jp6vHy8/T19vf4+fr/xAAfAQADAQEBAQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/xAC1EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2wBDAAQEBAQEBAgEBAgLCAgICw8LCwsLDxIPDw8PDxIWEhISEhISFhYWFhYWFhYbGxsbGxsfHx8fHyMjIyMjIyMjIyP/2wBDAQUGBgkICQ8ICA8kGRQZJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCT/3QAEABD/2gAMAwEAAhEDEQA/APUAR3P5U8DPQ0n3uQKeAw5FYliiNj3pwh4yzCoiX7Gm7uxagCYhAetRniqxJJ45p4YdDQBIMnvilBb1pgJ6dqdszyOlAFlYpHHy81IInHUiqPzqecinjcxyQRQBa8l88HNJ5cg6ipI4ZWXPSl3SoMNk0AQ+U/emmNuxFPM6Z5U0/wA1W+6KAKxjYdaQKKvqV9BUyrAwGBzQBleTIRkYpRbSHuK0JWROBWe05zgUAOFu3cikMYHSoDO3bn6UwSs3DUATlcU3kHHFChugH51MsUj8KCfwoAj4xx1pOvWrS2kx6rQbSXnAP60AVghPIp2188Gpfs7A98UG3f7wz+NAERVwOWpu2SpAj5wacsj9h0oAi2MOtN2NntVmSaXaRswfXFVt8x42n8qAAr9KafbFO3L0kzmjeoOAM0Af/9D0uO9QBmjQyY564AqIXY5LIc+3IFWodPiEW6MhmIBIGR9QTVCVJN3lj5cHt/jWJRaghNy+6OTb9aq3kD28nlAkN6nimTRz2wUl1cHn5f5GoxLO5II4Y5DHmgZAGkHyknPX2pjSMgJBJzxyf5VbuJDGm4ckjnA4qGOFXBZjtXg4Pf6UAEUyrCGLnce2efyrRtbkOSBIFIHAP+NYrQyBmfB2/wANRIWi5b6daAN99Rtkz5hJPb0z70q6jA4wT+vFc3qE9tFhQfmJxuzjtnoe1ee6pqUUlztjcqV+UlTwfwoA9zOpbEwuBjuDn9Kz7jWAU2lsf7XpXkkPiC4SIRKwIAxyOeKqx3OrXc4Q/MOvBxkfiaAPSJ9ZJAVZATzkAiqj69cQgqULAjP4VnJZafHH9pvFVQBlhnge9Z8+saaUJDLtjHGBj6cHGaANA+JrpmBB2jH3c1cTxdNAyqqvk1yNtr0HnFRFGwK/K5+8D9elaOmanFJKEvnDHOfmG0EHoKLhY6seITcfvHJLemaZ/bUXkbQdpB5JNY91aQxkzQZKljwP5Cqy2SyI27dvz07fnQB0sOrQbsSTAY7j0rZsJLW4kKozEeleYPZbnxnJJwAPX3NdbpdutrEpDEnGAc8Z9qAPTUW3t0Xcxk3c4bjA+g71BLrIh5jUbM9gOnvXI3N/cQ2+6XJRxtGDyCDzii2uo5h8nzRKAeTkn2OadxWO0bxJb2sP2mcqI+hyas2viXQ73DW4yTxzwT+Ga87kWOUk3MYaPsjdP/11q28Vnbxg2sKgj06gfjRcLHpatbT8ggDHI71TuEtFiLK5LDqFx+FZVhcRso80lTnHByatzSxTzboQAeFPr+vamIx5XQAlWzWTcXSxE4IJ9fSte7tn80xuwBHGF5H5isebSXj/AHj9OceuKQxsd85+Zzk/7XQ0pvvXI5xwOBWbdCVwWVcAHjFVIpCWHmH5h17/AMqQzVeeR8spIHbpViG9hEbSsc44xms6TYj+WGzkfT+dO+wB4PNJ6YOOetAH/9H22KHZaiSFxuYYYcDHv6Vz9xAqS7d2Se471oXFnd7P3QyU/hB6g+5qa1s3dTNcKVbGADx071mO5mQWzs4D/dJq/eQ2kMPykMcYB9PpUltbpubPC46+/wCNUnUStszz+dIDGWZoWKj5QDlTVFrqaGUySKAW9eQfWti7ljKruUEg44OPzrPmBlRtqfMvIB6kUDHS6uv2UQyDLHPSsySNjGDnBByMdqSZhJEAwG/kEfy6U+G1d1YSbgMcfWkM5vUvMSMzTHOe9eXX1zFJIRbpsPRlznJ65r0DxTBcRweUgDc8k+h44/xrhBZQ2sf2iIOmM4Y4OT64PY0wuVYpHBEhOAD1qzZz+XcJcZ+63P0rGMjht2Tye/NdFpdhLfLI0S/LGpYge3WmI2bzWY/sjhTu3DZgn1rjJzNjaTleuK0JkSFgsmNvU468VVdfNO7Bwfun1x2oQBaPtwD92t7TtksnznocHFYUMa79r/KM9evauh0y1i2efGwd88j+lFgudxY3YfdDPxsBLZ4OAOtU28Q2d0Gt4ZGXBO3IwMeorm9YuHgtRcOGiMnylSOSK5SJwwyD8q8/hSGeg2uo4Z1bBI5bHOP6VtaTJ9tUtjDDkKDjI9a4Dw9fzLqJgwpjmHOeoFewxraWkYKlWUDsaVh3I9pknXePlA+6eaS5sUt/3bgjnzMDIGfSrEl7al/MhBJwODx2rON7aXsmTJlgCzAHoBQI2YLq3dApQ7vXt+RrQ6qp+UZ/CqFs0cwUKBnsRj9a2AtvAN0pDMWxjofp1xxTEXtLhF1ItsDyT8pJA596276xW2uxCrbnHX0zXnE15MzFogUO7a2e47kcc1q2F7cSLiVsunGWHJA757+lMRruLqKYhjtPas+7mnkcFGwcdK1o7qF0CTHb7nsfaufcP5nJJGePekMf5kiALJkj26USpYzAErgkcsOOlF9f3un6dJcRpuYIQF77Tzx7VxvhvUdUvb2Q6ivyHoG4x9PwoA7FbLfHvhG4KMinRLO58vBDYziiCd4bp1hG4HkAdvatm1VnidpwFLcAg96BH//S9nBkmugkZJXGD6Efp36VNJcO2+DcUCAFR7+mD+VUZrz7NJsixkd/X/8AXSAT6hOyD+6OeMisxmQ15dPJ5eB68Dt6cU6KVtv7vOV5NPmWSM7Y1xzyfXtV6G1jk+WNAu4cEUgMyC3NxIF3AHr83Hv3rTBiOOieh9ar3lrIk/lqCSVxn0PpWcs0Sv5Mm5iQev8ACaYFq+tooxvhZc4wcDFY8MkvmSLjIxycn9a33sxOFO8A8Ak1RvrGK3la3hkBGeWXJBHrSAw79Le5BBRfugN15z3rxnXrG4hlYfNtRsKPRT6ete3ReRG4acZTv71W1Gzt7/DMioG4B68DpzTGfNl3LLJIZpPyq5aX0vEaEgH1q7r9lDpl/JCuJADjHTGe9Yi7GXOMUxHThGubYzDl1Hp1rZ0/TjeWoctlVOMAfrmq2janaQ2TRPGCSD9citLTLlIDhSQDzjjb9KAGyaDJEMTNzjINULWbT9NuvMckSE4X06dutbWtarqLSxmYgj7ob2H0rmdPJ+2Pc6igZSTsIHA9OtAC3t6mtXAiuXVepBY8cfTvVC6t4rSx88ybfMJ+Qr6dMEdf6V6Lb+FrHVZkulUKTnnH5Vz/AIy0naisBhYzg47/AEoQHG2GpiyuBcgbgBjPQfUCu38N+JEuLuSC4ZSjYbDYG78a86is5biVYIh14Vfc/wCNaun2JtbkrcJjZy28cD1/GhgeseMhHDZxXFhu8thk/j1Bwe1eZ2p1O9vVgsc7znp04557YrpbbxXBb/6DeBnt+gYgcZ9fatX7dp/2fzdMZUP3V4wCTwPQ0hnQ2Ms1lp0cgGZMDIHbHBwKz5PFnk6g0F0D5XB4HzZ9MngGp9MmaHC3khdm5yB/X1PpWRrt3plrOC+5gDgjgjB5z9aQHUwX8GpOZLQ5U/d3DnNaFnJIDsJI55z0NYGkCK7aO4twscbAfh/9eutFsrW7zZ4Q8YHXnr6UAbr2iBNzZ5HTtz3qqYWiCn5Suc+vFdPpsLXumB5Tlhzk9SD2/CsSdWyykBlOeO/FMRmSXUflEOo3dm9vp0xVMxwyRqUO0k42gdvUn0q0kUUgLdMc4PpTPLAcY4J6n+lIZuQadGWVkP3uTxW3HY2bRM3mMhRTheoY+/pWNZyAJ5cj8DOw/wC0fX2q59sEsojcBM8Hb0FAj//T9MljFxOWUnr0rUms5reDejbRgYIrl7TWFtRtuCSMjgcnkVpnWXubVg5wcDYKzGXJJS8Cjk+/uPWoftQhbeCRx271jLqZiUgttLnketLPeW38QOOOaQzTn1Tz3z06ZH9cViTM3nBu5PBHvUTgTMrRMNvTJq3bwyCX1dBnPtQBuWtvcSwhpBlAMA1JLalE+ZcAdT61LZ3UkMJGAwXnBrktQ8ZXcF99kuEMYEgCLn74PQ0xE1+iMwQnHGQM9RWDefbrW3l/swh5iAAHJ2gd8e9bktpK7B9nJ/eLg/dyeR+lVvInfcsQGRznpSGebeKtF8y1S4jx5u0NIVyBuxz17ZrzSOFxFhlPXIbsfXmvo66sruK1ZZQG3/eBGQD7Zrh20ZJZRjaka9AeAO5NMDy2FdqhuQAfz9a6J9kFkJ1cbnbbtX+Zq1qd7pAjFpEN5V8nAIHPcGn2OmJAd14BINoZUJ7n+lMRPHLEdOaVPnKHJB5NdhajTLmxV1jxGQOOvP4+teevJJaBrf7pbKNtx/Ou48MwSSWX2iVsLjGBngDvz60hnWWKwiBkDhdoyPX6VpwWcE8Lm4QvwdgxwT3rHhEKbCdzZ6gDFaMU8kRBf7q9Ae9IRwg0s2es/bZYhD1K44A7ZqbW9Nku7DzYCJFwCcHkd+faux8Tqbyy860+VgD5nYfQVyWkrc2kElvPEQXA8vceGx1pjPLxZPJceWy8jr+Fej6XoE0NsLomMq5wqE5I96S7t45WWCKHG/AEmOnrmtL7JeWDxxNjGOcc5PfnpQBNBA6rnaOP/wBXSsa70CSW+WR4w6NjPbn/AOtXQ2SNKf3nBBAB9B61uRfZow0bvufjHHGPrSAy4bL7LAIyPkUbgAf84q7bSSTwGNGwPTPpVxoUvSBbnnONvrzWK8kUKvCQd/QYOKBnbeFdVjgne3u5PlxxUuqX6PcGWIDA44/nXLWtmRbiXcNzEj6Cn/a4prx7BCTLGhdhtONvAzn8aCS698SohwDQlyQyhyAqtzj0rOwPNV5Bt46j26U590UaTngvklRzgCgZu3UEd9D9pQFIw3y9j7UtoclGd84ODn+tY8eog4gQ/wD1h64qW9vfKh8lGXaCNrD7x9d2ffpQB//Uha/dIWBAyT0znp3zV2K+kazMjt90Y5NZv2FEtWEj5IXo3f2zVGFUi0zygDlGBwPQdqxLNwTSuVEfIGCc9f1rsLLy5Ysy4feDkDsa8wtphPdNeKkiHABBPy4HsD1rrNMvpYpQxPHUgdPSgLHUTpDbRGa2B2njp+dUzdTyMXLcHimy3akAAgw/1qE7G3FOMU7isdHY3cSR4Y81mmO1vrz7RdRxOEO4HYdynt1rHD7ejcVet7luYlOCeo9aAsdDPOhi+QbeMcd658S+XICg5zxnmpy0u0Kx6fyqB49i5P3X5/GgDVvtcmuYFjkCr5Y7cZ96yLuyiubYyROA2CHzg57dDUcwitLaSQ/vFIBBI5GOwrLjl+0Q+bKdqt1+XBH9aLgecz+GZZrgIMbT0OOvrWgtrCZhBC/+rIG5sndkcqPpXdWGnwGPEbkqTkZzkc9BV2CxjSbcqxnOexB+vemBwl34J1Dz87iVcgjHcevNei6D4PnsbMiQtIMjJHYe9dXp1qmxVPzZbAB46/yrfaVdrwvhTnBA5HFAjjJ7OCGT7Rt3ArgL0/Ks5bd5yZbgFUToP5V09zECCydF5+bp+VYUiRsgLuCWOOD0/CkMZPOrIscyjaBwo9feqIhBIuDjBBwOuP8ACppEiSTy13Oeh9PwxVmK1ZwBGeB2HX3oA0rPSLe9tTNwAuPTOe2KgvNIELLbSJgkZyey1HJeSafBhMu2eM9qyrnUZbqTzG3MSMdc9P5UAVLi3IudkAypOMD2qSeS2VBJEQsijkHnpwao3GoLbjefkJ4AYZzn0rlroySL50y4ZT8rA+2frzSGdHBqSyqZLZ+Aeo7Gqduly0rNIDM5J24PT/Gudtne5jLgksOuM8//AF66PTLnyJ8qDg4AByeT9aANuGO5ZNzuYyP4SO/XirUd08cTc5Vhg49RS3UxYBs8sOMfrWcy74QAASQRnOKLisawlikO8yhUx8zN2qnBdwXLC3RtwJ+929KbZaZLqdrJCnzooO72zwahisILC2KoQ204XGcj8+tMDPutStbHfFcEb87R1yQfTAqOS4XcVdQQB1ycAnvVG/kW4nG7JIx7ep6entTJ4VmiVUchjywBxx7d6LhY/9VJ3W6Kxr8pUHPPBx9axpV2wF3AGTjg8f5NdZNpuYQMAM/Uf/qpTpURsyFXJB9OKxNDiYUjEu1Mn6Vpxl1GR0HcHrWimkuCPLQ4P4VchsGg+eVDx0+tAxYrxGRQwGe4HetmNYg6yEjkZHqPY1lLZu7iRc1rJbuq7SCR60CJEiR8Lnn36CmSAq2yMKeeo6gd8Vowo6jYq5A7mphaRSOxA4XqcUCKEgkb5iSe2Cc1KAiwHglqufZj0HOcEZPI/GtKKCIoRjB75FAjkY1Z3IPyjvxmszUYDCpbBwfQYye1egLp0BIIY/SpGtI5P3Zj3Y78dKdgucJZRkKsnpjj+lblvHJNLnGB6ntWtPZx42ABQP7oH86kt7ZPLMeMf1+tAF6w2hSjEsg5LY6n/CrbTxIHUKDgcc/zqxZ2yC3KuQc44HpVW/hIYLAQFP5/jimSZ93LE8S9yRyMn8qwDbxM5ES7efWt5opEGTz+HH5VHLCUX5EAU9D9e9IZkIrW7HDYJ6fWrf2iW2BYsAWHBxStavjenT3FAtGkYB+MdD1oGYk4LzBrjcU7kHr9KqXLrDAPs8W45+8Tz+Vdf/ZJfAYg56CobizmiBVMEdsdqQHlGsTTrcp5m4qvzAkYU8cj35pdt9JceaMCJgOMEH1xzXpt1ZW8tuqyxrI6Hdn0/WuXvbS7kkCkM69if4fp3/KgZlCyRmWVcEnOVHA2/j/SrcVvFMcxAgKCBznH9DV+PQpZ4d0n3R8uC2fzB5/GrltpU9rJlJDIuO/IoAq2tvKUZ5I9wX1zxx7d6ikec/Jjan3vX2rr9NmntcsEUqx5Mg3D8qTVdMkZVuRt2HjKj+X/ANegLnM7H8tQjAcHp79qzGSRnKzg8fwgnH/666NbcxKfKRj6Fhz+dUJYLi6ZowG3nkn1P1oA5+eKBZRcSbBIRt+U7jj39DWeqk3ZdxuzyMjP61rS6GYiZpIzlxyDnj1x/wDrq1FZukJSBQnselAz/9b0o2jN8xaoZ7cLgE9eeK1fJc8bc054JD/BWJZjrbnpxUnljO09KvtbyHHy/pSGGYcBaAKWyJRwPyFMx6Zq+YG/uH8qPJJ42H8qAKwQuMck1KEkU8jH1FTCGRfuqfyNKY58520AMXOeTj6UpJXlSc0FJP7lO2SY+7QBNHdNHx14xzQ8xcj+QqHy5M5CU0pJ0xQA7gnBJNAWTPXApojkqQIR97NAFhfMUD5qP3hPbBquQxOQDT18wHjNAEwtmPJOakMLnHPSoC7Be9O+0MBhQ35UAPMORnPIqJkYnnOKDNIeMH8qZh2GDmgBCWH3CRVciVyScmrGxsYCZ/OmhJgfu4oArmEvxnn3pHtXPIq7sk/u0bZP4hmgDIktmAwCTTxEQowea0jHnquKhMGTwpIoAriNjyPzqU71Xb94elPMD54WgRygcjNAEId8bPuimBFJzzmrDRyN/CfwpFidf4TQA9RvXYfzqm9qoyODV8L/ALJpfKJOQMUAf//Z\\\"}let P=new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]),L=u.createBuffer();if(!L)return u.isContextLost()?z({canvas:d,contextLost:!0,markInitFailed:!1}):z({canvas:d,errorMessage:\\\"Failed to create WebGL buffer\\\"});h.current=L,u.bindBuffer(u.ARRAY_BUFFER,L),u.bufferData(u.ARRAY_BUFFER,P,u.STATIC_DRAW);for(let _ of Object.values(w.current)){if(!_)continue;let S=u.getAttribLocation(_,\\\"a_position\\\");S>=0&&(u.enableVertexAttribArray(S),u.vertexAttribPointer(S,2,u.FLOAT,!1,0,0))}return s.current=performance.now(),!0},[G,z]),F=N(()=>{let d=a.current,u=t.current,y=w.current,g=T.current,m=k.current;if(R.current||!p.current){b.current=!1,n.current=0;return}if(!d||!u||!g.a||!g.b){b.current=!1;return}let E=m.dpr??window.devicePixelRatio,x=Math.max(1,Math.floor(u.clientWidth*E)),I=Math.max(1,Math.floor(u.clientHeight*E));(u.width!==x||u.height!==I)&&(u.width=x,u.height=I,fe(d,g.a,x,I),fe(d,g.b,x,I));let P=(performance.now()-s.current)/1e3,L=(ne,$e)=>Q(d,ne,$e);m.layers.lightning&&m.lightning.enabled&&m.lightning.autoMode&&P>=l.current&&(c.current=P,o.current=Math.random(),l.current=P+m.lightning.autoInterval*(.5+Math.random()));let _=g.a,S=g.b,X=()=>{let ne=_;_=S,S=ne};m.layers.celestial&&y.celestial?(be({gl:d,program:y.celestial,target:S,displayWidth:x,displayHeight:I,time:P,params:m.celestial,moonTexture:r.current,moonTextureLoaded:i.current,getUniformLocation:L}),X()):(ve(d,S,x,I),X()),m.layers.clouds&&y.cloud&&(we({gl:d,program:y.cloud,target:S,sceneTexture:_.texture,displayWidth:x,displayHeight:I,time:P,params:m.cloud,celestial:m.celestial,getUniformLocation:L}),X()),m.layers.rain&&y.rain&&(xe({gl:d,program:y.rain,target:S,sceneTexture:_.texture,displayWidth:x,displayHeight:I,time:P,params:m.rain,interactions:m.interactions,getUniformLocation:L}),X()),Ie(m.layers,m.lightning,y.lightning,P,c.current)&&y.lightning&&(Re({gl:d,program:y.lightning,target:S,sceneTexture:_.texture,displayWidth:x,displayHeight:I,time:P,params:m.lightning,interactions:m.interactions,lastFlashTime:c.current,strikeSeed:o.current,getUniformLocation:L}),X()),m.layers.snow&&y.snow&&(ke({gl:d,program:y.snow,target:S,sceneTexture:_.texture,displayWidth:x,displayHeight:I,time:P,params:m.snow,getUniformLocation:L}),X()),y.composite&&_e({gl:d,program:y.composite,sceneTexture:_.texture,displayWidth:x,displayHeight:I,time:P,celestial:m.celestial,interactions:m.interactions,post:m.post,lastFlashTime:c.current,strikeSeed:o.current,getUniformLocation:L}),p.current&&!R.current?(b.current=!0,n.current=requestAnimationFrame(F)):(b.current=!1,n.current=0)},[Q]);return Ke(()=>{let d=t.current;if(!d)return;let u=m=>{m.preventDefault(),R.current=!0,G()},y=()=>{R.current=!1,C.current=!1,V()&&p.current&&(b.current=!0,F())};d.addEventListener(\\\"webglcontextlost\\\",u,{passive:!1}),d.addEventListener(\\\"webglcontextrestored\\\",y);let g=typeof IntersectionObserver<\\\"u\\\"?new IntersectionObserver(m=>{let x=!!m[0]?.isIntersecting;if(p.current=x,!x){U(),G(),$(d);return}!b.current&&!R.current&&(a.current&&T.current.a&&T.current.b||V())&&(b.current=!0,F())},{threshold:0}):null;return g?g.observe(d):p.current=!0,!g&&V()&&p.current&&(b.current=!0,F()),()=>{g?.disconnect(),d.removeEventListener(\\\"webglcontextlost\\\",u),d.removeEventListener(\\\"webglcontextrestored\\\",y),G(),$(d)}},[G,V,$,F,U]),t}var De={celestial:!0,clouds:!0,rain:!1,lightning:!1,snow:!1},Te={timeOfDay:.5,moonPhase:.5,starDensity:.5,celestialX:.74,celestialY:.78,sunSize:.14,moonSize:.17,sunGlowIntensity:3.05,sunGlowSize:.3,sunRayCount:6,sunRayLength:3,sunRayIntensity:.1,sunRayShimmer:1,sunRayShimmerSpeed:1,moonGlowIntensity:3.45,moonGlowSize:.94,skyBrightness:1,skySaturation:1,skyContrast:1},Pe={coverage:.5,density:.7,softness:.5,cloudScale:1,windSpeed:.3,windAngle:0,turbulence:.3,lightIntensity:1,ambientDarkness:.2,backlightIntensity:.5,numLayers:3},ze={glassIntensity:.5,glassZoom:1,fallingIntensity:.6,fallingSpeed:2,fallingAngle:.1,fallingStreakLength:1,fallingLayers:4},Ee={enabled:!1,autoMode:!0,autoInterval:8,flashIntensity:1,branchDensity:.5},Ae={intensity:.5,layers:4,fallSpeed:.6,windSpeed:.3,windAngle:.2,turbulence:.3,drift:.5,flutter:.5,windShear:.5,flakeSize:1,sizeVariation:.5,opacity:.5,glowAmount:.25,sparkle:.25},Ge={rainRefractionStrength:1,lightningSceneIllumination:.6},Le={enabled:!0,haze:0,hazeHorizon:.8,hazeDesaturation:.35,hazeContrast:.6,bloomIntensity:0,bloomThreshold:.82,bloomKnee:.35,bloomRadius:1.2,bloomTapScale:1,exposureIntensity:0,exposureDesaturation:.25,exposureRecovery:1,godRayIntensity:0,godRayDecay:.965,godRayDensity:.9,godRayWeight:.35,godRaySamples:16};function M(e,t){if(!t)return{...e};let a={...e};for(let n of Object.keys(t)){let s=t[n];s!==void 0&&(a[n]=s)}return a}function We(e){return{layers:M(De,e.layers),celestial:M(Te,e.celestial),cloud:M(Pe,e.cloud),rain:M(ze,e.rain),lightning:M(Ee,e.lightning),snow:M(Ae,e.snow),interactions:M(Ge,e.interactions),post:M(Le,e.post),dpr:e.dpr}}import{jsx as qe}from\\\"react/jsx-runtime\\\";function Fe({className:e,dpr:t,layers:a,celestial:n,cloud:s,rain:c,lightning:l,snow:o,interactions:r,post:i}){let h=Ce(We({dpr:t,layers:a,celestial:n,cloud:s,rain:c,lightning:l,snow:o,interactions:r,post:i}));return qe(\\\"canvas\\\",{ref:h,className:e,style:{width:\\\"100%\\\",height:\\\"100%\\\"}})}var de={clear:{dawn:{celestial:{celestialX:.69,skyBrightness:1,starDensity:.36,sunGlowIntensity:3.47,sunGlowSize:.49,sunRayCount:4,sunRayShimmer:5,sunRayShimmerSpeed:5},glass:{blur:4.5,brightness:.95}},dusk:{celestial:{celestialX:.69,skyBrightness:1,starDensity:.36,sunGlowIntensity:3.84,sunGlowSize:.43,sunRayCount:4,sunRayIntensity:.24,sunRayLength:1.37},glass:{brightness:.9}},midnight:{celestial:{celestialY:.71,moonGlowIntensity:3.23,moonPhase:.51,skyBrightness:.44,skyContrast:1,skySaturation:2,starDensity:1.72,sunGlowIntensity:1.59}},noon:{celestial:{celestialY:.71,moonPhase:.2421,skyBrightness:.8,skySaturation:1.9,starDensity:.14,sunGlowIntensity:1.72,sunGlowSize:.69,sunRayCount:4,sunRayIntensity:.19,sunRayShimmer:5,sunRayShimmerSpeed:5},glass:{brightness:1}}},cloudy:{dawn:{celestial:{celestialX:.69,celestialY:.74,skyBrightness:1,skyContrast:1.1,skySaturation:.88,starDensity:.36,sunGlowIntensity:2.4,sunGlowSize:.38,sunRayShimmer:5,sunRayShimmerSpeed:5},cloud:{ambientDarkness:1,backlightIntensity:1.07,cloudScale:1.73,coverage:.81,density:1.1,lightIntensity:.67,numLayers:1,softness:.42,windSpeed:.04},glass:{blur:3,brightness:.95},post:{bloomIntensity:.99,bloomKnee:.58,bloomRadius:5.5,bloomTapScale:1.39,bloomThreshold:.78,exposureIntensity:1.14,exposureRecovery:1.95,godRayDecay:.857,godRayDensity:.79,godRayIntensity:1.08,godRaySamples:121,godRayWeight:.42}},dusk:{celestial:{celestialX:.69,celestialY:.74,skyContrast:.99,skySaturation:1.38,starDensity:.11,sunGlowIntensity:3.21,sunGlowSize:.47,sunRayShimmer:5,sunRayShimmerSpeed:5},cloud:{ambientDarkness:.54,backlightIntensity:.78,cloudScale:1.73,coverage:.81,density:.9,lightIntensity:.78,numLayers:1,softness:.42,windSpeed:.04},glass:{brightness:.9},post:{bloomIntensity:1.11,bloomKnee:.58,bloomRadius:5.5,bloomTapScale:1.39,bloomThreshold:.78,exposureIntensity:1.14,exposureRecovery:1.95,godRayDecay:.857,godRayDensity:.79,godRayIntensity:1.08,godRaySamples:121,godRayWeight:.42}},midnight:{celestial:{celestialX:.74,celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,starDensity:1.72,sunRayShimmer:5,sunRayShimmerSpeed:5},cloud:{cloudScale:1.73,coverage:.81,density:1.02,numLayers:1,softness:.42,windSpeed:.04},post:{bloomIntensity:1.11,bloomKnee:.58,bloomRadius:5.5,bloomTapScale:1.39,bloomThreshold:.78,exposureIntensity:1.14,exposureRecovery:1.95,godRayDecay:.857,godRayDensity:.79,godRayIntensity:1.08,godRaySamples:121,godRayWeight:.42}},noon:{celestial:{celestialX:.74,celestialY:.71,moonPhase:.2421,skyBrightness:.84,skyContrast:1.31,skySaturation:1.44,starDensity:.14,sunGlowIntensity:1.35,sunGlowSize:.61,sunRayCount:4,sunRayIntensity:.22,sunRayShimmer:5,sunRayShimmerSpeed:5},cloud:{ambientDarkness:0,backlightIntensity:.45,cloudScale:1.73,coverage:.81,density:1.06,lightIntensity:.44,numLayers:1,softness:.42,windSpeed:.04},glass:{brightness:1},post:{bloomIntensity:.42,bloomKnee:.58,bloomRadius:5.5,bloomTapScale:1.39,bloomThreshold:.76,exposureIntensity:1.14,exposureRecovery:1.95,godRayDecay:.884,godRayDensity:.79,godRayIntensity:1.33,godRaySamples:121,godRayWeight:.42}}},drizzle:{dawn:{celestial:{celestialX:.69,celestialY:.74,moonGlowIntensity:3.55,moonGlowSize:.97,skyBrightness:.96,skyContrast:.84,skySaturation:.94,starDensity:1.72},cloud:{ambientDarkness:.7,backlightIntensity:.82,cloudScale:1.21,coverage:.61,density:.98,lightIntensity:0,numLayers:1,softness:.17,turbulence:.16,windSpeed:.02},glass:{brightness:.95},rain:{fallingAngle:.02,fallingIntensity:.13,fallingSpeed:2.24,fallingStreakLength:.32,glassIntensity:.29,glassZoom:.65}},dusk:{celestial:{celestialX:.69,celestialY:.74,moonGlowIntensity:3.55,moonGlowSize:.97,skyBrightness:.77,skyContrast:.84,skySaturation:.85,starDensity:1.72,sunGlowIntensity:2.73,sunRayLength:1.45},cloud:{ambientDarkness:.7,backlightIntensity:.82,cloudScale:1.21,coverage:.61,density:.98,lightIntensity:0,numLayers:1,softness:.17,turbulence:.16,windSpeed:.02},glass:{brightness:.9},rain:{fallingAngle:.02,fallingIntensity:.13,fallingSpeed:2.24,fallingStreakLength:.32,glassIntensity:.29,glassZoom:.65}},midnight:{celestial:{celestialX:.74,celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,skyBrightness:.77,skyContrast:.84,skySaturation:.85,starDensity:1.72,sunGlowIntensity:2.73,sunRayLength:1.45},cloud:{ambientDarkness:.7,backlightIntensity:.82,cloudScale:1.21,coverage:.61,density:.98,lightIntensity:0,numLayers:1,softness:.17,turbulence:.16,windSpeed:.02},rain:{fallingAngle:.02,fallingIntensity:.13,fallingSpeed:2.24,fallingStreakLength:.32,glassIntensity:.2,glassZoom:.5}},noon:{celestial:{celestialX:.74,celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,skyBrightness:.77,skyContrast:.84,skySaturation:.85,starDensity:1.72,sunGlowIntensity:2.73,sunRayLength:1.45},cloud:{ambientDarkness:.7,backlightIntensity:.82,cloudScale:1.21,coverage:.61,density:.98,lightIntensity:0,numLayers:1,softness:.17,turbulence:.16,windSpeed:.02},glass:{brightness:1},rain:{fallingAngle:.02,fallingIntensity:.13,fallingSpeed:2.24,fallingStreakLength:.32,glassIntensity:.29,glassZoom:.65}}},fog:{dawn:{celestial:{celestialY:.71,skyBrightness:1.05,skyContrast:.41,skySaturation:1.25,starDensity:.11,sunGlowIntensity:2.42,sunGlowSize:.38,sunRayIntensity:.05,sunRayLength:1.59},cloud:{ambientDarkness:1,backlightIntensity:.54,cloudScale:.64,density:1.5,numLayers:6,softness:.77,turbulence:.67,windSpeed:.02},glass:{brightness:.95}},dusk:{celestial:{celestialY:.71,skyBrightness:1.02,skyContrast:.62,skySaturation:.88,starDensity:.11,sunGlowIntensity:2.42,sunGlowSize:.38,sunRayIntensity:.05,sunRayLength:1.59},cloud:{ambientDarkness:0,backlightIntensity:.54,cloudScale:.93,numLayers:6,softness:.5,turbulence:1,windSpeed:.03},glass:{brightness:.9}},midnight:{celestial:{celestialY:.71,moonGlowIntensity:2.74,moonGlowSize:1.43,moonPhase:.5,skyBrightness:1.02,skyContrast:.62,skySaturation:.88,starDensity:.11,sunGlowIntensity:2.42,sunGlowSize:.38,sunRayIntensity:.05,sunRayLength:1.59},cloud:{ambientDarkness:0,backlightIntensity:1.4,cloudScale:.91,density:1.27,lightIntensity:2,numLayers:6,softness:.63,turbulence:.86,windSpeed:.03}},noon:{celestial:{celestialY:.71,skyBrightness:.76,skyContrast:.47,skySaturation:.6,starDensity:.11,sunGlowIntensity:.92,sunGlowSize:.84,sunRayCount:0,sunRayIntensity:0,sunRayLength:0},cloud:{ambientDarkness:1,backlightIntensity:.72,cloudScale:.83,density:1.5,numLayers:6,softness:.84,turbulence:.86,windSpeed:.03},glass:{brightness:1}}},hail:{dawn:{celestial:{celestialX:.69,celestialY:.74,skyBrightness:.42,skySaturation:.74,starDensity:.36,sunGlowIntensity:5,sunGlowSize:.36,sunRayShimmer:5,sunRayShimmerSpeed:5},cloud:{ambientDarkness:.75,backlightIntensity:.64,density:.83,lightIntensity:.55,numLayers:1,turbulence:.55,windAngle:-3.14,windSpeed:.14},glass:{brightness:.95},interactions:{rainRefractionStrength:.96},post:{bloomIntensity:.13,bloomThreshold:.64,godRayIntensity:.56,haze:.11,hazeHorizon:1},rain:{fallingIntensity:.42,fallingLayers:1,fallingSpeed:3,fallingStreakLength:.56,glassIntensity:1,glassZoom:.69},snow:{drift:.63,fallSpeed:8,flakeSize:.77,flutter:.79,glowAmount:.29,intensity:.81,layers:8,opacity:1,sizeVariation:.48,sparkle:.44,turbulence:.91,windAngle:2.23,windShear:.78,windSpeed:.99}},dusk:{celestial:{celestialX:.69,celestialY:.74,skyBrightness:.47,skyContrast:1.39,skySaturation:1.42,starDensity:.11,sunGlowIntensity:3.21,sunGlowSize:.51,sunRayShimmer:5,sunRayShimmerSpeed:5},cloud:{ambientDarkness:.75,backlightIntensity:.64,density:.83,lightIntensity:.79,numLayers:1,turbulence:.55,windAngle:-3.14,windSpeed:.14},glass:{brightness:.9},interactions:{rainRefractionStrength:.96},post:{bloomIntensity:.13,bloomThreshold:.64,godRayIntensity:.56,haze:.15,hazeHorizon:1},rain:{fallingIntensity:.42,fallingLayers:1,fallingSpeed:3,fallingStreakLength:.56,glassIntensity:.72,glassZoom:.69},snow:{drift:.63,fallSpeed:8,flakeSize:.77,flutter:.79,glowAmount:.71,intensity:.47,layers:8,opacity:1,sizeVariation:.77,sparkle:.1,turbulence:.91,windAngle:2.23,windShear:.78,windSpeed:1}},midnight:{celestial:{celestialX:.74,celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,skyBrightness:.42,skySaturation:.74,starDensity:1.72,sunRayShimmer:5,sunRayShimmerSpeed:5},cloud:{ambientDarkness:.75,backlightIntensity:.64,density:.83,lightIntensity:1.21,numLayers:1,turbulence:.55,windAngle:-3.14,windSpeed:.14},interactions:{rainRefractionStrength:.96},post:{bloomIntensity:.13,bloomThreshold:.64,godRayIntensity:.56,haze:.14,hazeHorizon:1},rain:{fallingIntensity:.42,fallingLayers:1,fallingSpeed:3,fallingStreakLength:.56,glassIntensity:.72,glassZoom:.69},snow:{drift:.63,fallSpeed:8,flakeSize:.77,flutter:.79,glowAmount:.71,intensity:.47,layers:8,opacity:1,sizeVariation:.85,sparkle:.1,turbulence:.87,windAngle:2.23,windShear:.78,windSpeed:1.11}},noon:{celestial:{celestialX:.74,celestialY:.71,moonPhase:.2421,skyBrightness:.56,skyContrast:1.03,skySaturation:.62,starDensity:.14,sunGlowIntensity:2.5,sunGlowSize:.54,sunRayCount:3,sunRayShimmer:5,sunRayShimmerSpeed:5},cloud:{ambientDarkness:1,backlightIntensity:.21,coverage:.74,density:.95,lightIntensity:.6,numLayers:1,softness:.36,turbulence:.55,windAngle:-3.14,windSpeed:.14},glass:{brightness:1},interactions:{rainRefractionStrength:.96},post:{bloomIntensity:.13,bloomThreshold:.64,godRayIntensity:.56,haze:.23,hazeHorizon:1},rain:{fallingIntensity:.42,fallingLayers:1,fallingSpeed:3,fallingStreakLength:.56,glassIntensity:.72,glassZoom:.69},snow:{drift:.63,fallSpeed:8,flakeSize:.77,flutter:.79,glowAmount:.71,intensity:.8,layers:8,opacity:1,sizeVariation:.77,sparkle:.1,turbulence:.91,windAngle:2.23,windShear:.78,windSpeed:.89}}},\\\"heavy-rain\\\":{dawn:{celestial:{celestialX:.69,celestialY:.74,moonGlowIntensity:3.55,moonGlowSize:.97,moonPhase:.3776,skyBrightness:.87,skySaturation:.56,starDensity:1.72,sunGlowIntensity:3.75,sunGlowSize:.43},cloud:{ambientDarkness:1,backlightIntensity:1.35,cloudScale:1.52,coverage:.53,density:1.17,lightIntensity:.06,numLayers:1,softness:.25,turbulence:.45,windSpeed:.05},glass:{brightness:.95},interactions:{rainRefractionStrength:1},post:{bloomIntensity:.36,godRayDecay:.838,godRayDensity:.56,godRayIntensity:1.11,godRaySamples:35,godRayWeight:.66,haze:.14,hazeHorizon:1},rain:{fallingAngle:.3,fallingLayers:6,fallingSpeed:3,fallingStreakLength:2,glassIntensity:.98,glassZoom:.58}},dusk:{celestial:{celestialX:.69,celestialY:.74,moonGlowIntensity:3.55,moonGlowSize:.97,moonPhase:.4115,moonSize:.17,skyBrightness:.96,skyContrast:.99,skySaturation:.72,starDensity:1.72,sunGlowIntensity:4.15,sunGlowSize:.35,sunRayCount:6,sunRayIntensity:.1,sunRayLength:3,sunSize:.14},cloud:{ambientDarkness:1,backlightIntensity:1.8,cloudScale:2.02,coverage:.72,density:.94,lightIntensity:0,numLayers:1,softness:.17,turbulence:.51,windAngle:0,windSpeed:.05},glass:{brightness:.9},interactions:{rainRefractionStrength:1},post:{bloomIntensity:.28,godRayWeight:.55,haze:.21,hazeHorizon:1},rain:{fallingAngle:.3,fallingIntensity:1,fallingLayers:6,fallingSpeed:3,fallingStreakLength:2,glassIntensity:.98,glassZoom:.58}},midnight:{celestial:{celestialX:.74,celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,moonPhase:.3776,moonSize:.17,skyBrightness:1.11,skyContrast:1,skySaturation:.78,starDensity:1.72,sunGlowIntensity:3.05,sunGlowSize:.3,sunRayCount:6,sunRayIntensity:.1,sunRayLength:3,sunSize:.14},cloud:{ambientDarkness:1,backlightIntensity:.81,cloudScale:2.16,coverage:.72,density:1.5,lightIntensity:0,numLayers:1,softness:.37,turbulence:.55,windAngle:0,windSpeed:.05},glass:{blur:1,brightness:1.15},interactions:{rainRefractionStrength:1},rain:{fallingAngle:.3,fallingIntensity:1,fallingLayers:6,fallingSpeed:3,fallingStreakLength:2,glassIntensity:.98,glassZoom:.58}},noon:{celestial:{celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,moonPhase:.3776,skyBrightness:.81,skyContrast:.54,skySaturation:.36,starDensity:1.68,sunGlowIntensity:3.7,sunGlowSize:.22,sunRayCount:0,sunRayIntensity:0,sunRayLength:0},cloud:{ambientDarkness:1,backlightIntensity:.59,cloudScale:1.8,coverage:.58,density:.87,lightIntensity:0,numLayers:1,softness:.2,turbulence:.24,windSpeed:.05},glass:{brightness:1},interactions:{rainRefractionStrength:1},post:{godRayIntensity:1.24,godRaySamples:0,godRayWeight:.6},rain:{fallingAngle:.3,fallingSpeed:3,glassIntensity:.98,glassZoom:.58}}},overcast:{dawn:{celestial:{celestialY:.71,moonGlowIntensity:1.91,moonGlowSize:.32,moonPhase:.5,skyBrightness:1.36,skyContrast:.73,skySaturation:1.6,starDensity:.11,sunGlowIntensity:5.1,sunGlowSize:.27,sunRayCount:6,sunRayIntensity:.05,sunRayLength:.6},cloud:{ambientDarkness:0,backlightIntensity:1.41,cloudScale:1.9,density:.3,lightIntensity:.75,numLayers:8,softness:.77,turbulence:.66,windSpeed:.04},glass:{brightness:.95},post:{bloomThreshold:.81,godRayIntensity:.32,godRayWeight:.3,haze:.15,hazeContrast:0,hazeHorizon:1}},dusk:{celestial:{celestialY:.71,moonGlowIntensity:1.91,moonGlowSize:.32,moonPhase:.5,skyBrightness:1.12,skyContrast:.83,skySaturation:2,starDensity:.11,sunGlowIntensity:4.15,sunGlowSize:.28,sunRayCount:6,sunRayIntensity:.05,sunRayLength:.6},cloud:{ambientDarkness:0,backlightIntensity:1.41,cloudScale:1.9,density:.3,lightIntensity:.78,numLayers:8,softness:.77,turbulence:.66,windSpeed:.04},glass:{brightness:.9},post:{bloomThreshold:.81,godRayIntensity:.32,godRayWeight:.3,haze:.15,hazeContrast:0,hazeHorizon:1}},midnight:{celestial:{celestialY:.71,moonGlowIntensity:2.75,moonGlowSize:1.54,moonPhase:.5,skyBrightness:1.58,skyContrast:.84,skySaturation:1.15,starDensity:.11,sunGlowIntensity:2.42,sunGlowSize:.38,sunRayCount:6,sunRayIntensity:.05,sunRayLength:1.59},cloud:{ambientDarkness:0,backlightIntensity:1.41,cloudScale:1.9,coverage:.9,density:1.16,lightIntensity:0,numLayers:8,softness:.87,turbulence:.66,windSpeed:.04},post:{bloomThreshold:.81,godRayIntensity:.02,godRayWeight:.15,haze:.35,hazeContrast:0,hazeHorizon:.96}},noon:{celestial:{celestialY:.71,moonGlowIntensity:1.91,moonGlowSize:.32,moonPhase:.5,skyBrightness:1.16,skyContrast:.53,skySaturation:.88,starDensity:.11,sunGlowIntensity:5.2,sunGlowSize:.38,sunRayCount:6,sunRayIntensity:.05,sunRayLength:1.59},cloud:{ambientDarkness:0,backlightIntensity:.9,cloudScale:1.9,coverage:.95,density:.3,lightIntensity:.08,numLayers:8,softness:.77,turbulence:.66,windSpeed:.04},glass:{brightness:1},post:{bloomThreshold:.81,godRayIntensity:.12,godRayWeight:.2,haze:.15,hazeContrast:0,hazeHorizon:1}}},\\\"partly-cloudy\\\":{dawn:{celestial:{celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,moonPhase:.5,starDensity:1.72,sunGlowIntensity:3.89,sunGlowSize:.38,sunRayCount:4,sunRayIntensity:.17},cloud:{ambientDarkness:.42,coverage:.37,density:1.24,lightIntensity:1.42,softness:.28,windSpeed:.01},glass:{blur:3,brightness:.95}},dusk:{celestial:{celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,moonPhase:.5,starDensity:1.72,sunGlowIntensity:3.89,sunGlowSize:.38,sunRayCount:4,sunRayIntensity:.17},cloud:{ambientDarkness:.42,cloudScale:1.68,coverage:.37,density:1.24,lightIntensity:1.42,softness:.26,windSpeed:.01},glass:{brightness:.9}},midnight:{celestial:{celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,moonPhase:.5,starDensity:1.72},cloud:{ambientDarkness:.42,backlightIntensity:1.23,cloudScale:1.11,coverage:.37,density:1.24,lightIntensity:1.42,numLayers:5,softness:.21,turbulence:.18,windSpeed:.01}},noon:{celestial:{celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,moonPhase:.5,starDensity:1.72},cloud:{ambientDarkness:.42,backlightIntensity:0,cloudScale:1.86,coverage:.48,density:1.71,lightIntensity:.67,numLayers:2,softness:.41,windSpeed:.01},glass:{brightness:1}}},rain:{dawn:{celestial:{celestialX:.69,celestialY:.74,moonGlowIntensity:3.55,moonGlowSize:.97,skyBrightness:.96,skyContrast:.84,skySaturation:.94,starDensity:1.72},cloud:{ambientDarkness:.7,backlightIntensity:.82,cloudScale:1.13,coverage:.72,density:.94,lightIntensity:0,numLayers:1,softness:.17,turbulence:.34,windSpeed:.05},glass:{brightness:.95},interactions:{rainRefractionStrength:.76},rain:{fallingAngle:0,fallingIntensity:.84,fallingLayers:4,fallingSpeed:3,fallingStreakLength:.54,glassIntensity:.38,glassZoom:.5}},dusk:{celestial:{celestialX:.69,celestialY:.74,moonGlowIntensity:3.55,moonGlowSize:.97,moonPhase:.4115,moonSize:.17,skyBrightness:.96,skyContrast:.84,skySaturation:.94,starDensity:1.72,sunGlowIntensity:3.05,sunGlowSize:.3,sunRayCount:6,sunRayIntensity:.1,sunRayLength:3,sunSize:.14},cloud:{ambientDarkness:1,backlightIntensity:.82,cloudScale:1.13,coverage:.72,density:.94,lightIntensity:0,numLayers:1,softness:.17,turbulence:.34,windAngle:0,windSpeed:.05},glass:{brightness:.9},interactions:{rainRefractionStrength:.76},lightning:{enabled:!0},rain:{fallingAngle:0,fallingIntensity:.84,fallingLayers:5,fallingSpeed:3,fallingStreakLength:.54,glassIntensity:.38,glassZoom:.5}},midnight:{celestial:{celestialX:.74,celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,moonPhase:.3776,moonSize:.17,skyBrightness:1.11,skyContrast:1,skySaturation:.78,starDensity:1.72,sunGlowIntensity:3.05,sunGlowSize:.3,sunRayCount:6,sunRayIntensity:.1,sunRayLength:3,sunSize:.14},cloud:{ambientDarkness:1,backlightIntensity:.77,cloudScale:1.13,coverage:.72,density:1,lightIntensity:0,numLayers:1,softness:.23,turbulence:.34,windAngle:0,windSpeed:.05},interactions:{rainRefractionStrength:.76},rain:{fallingAngle:0,fallingIntensity:.84,fallingLayers:5,fallingSpeed:3,fallingStreakLength:.54,glassIntensity:.38,glassZoom:.5}},noon:{celestial:{celestialX:.74,celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,moonPhase:.3776,moonSize:.17,skyBrightness:1.12,skyContrast:.49,skySaturation:.59,starDensity:1.72,sunGlowIntensity:3.05,sunGlowSize:.22,sunRayCount:0,sunRayIntensity:0,sunRayLength:0,sunSize:.14},cloud:{ambientDarkness:1,backlightIntensity:.59,cloudScale:1.13,coverage:.79,density:1.08,lightIntensity:0,numLayers:1,softness:.23,turbulence:.34,windAngle:0,windSpeed:.05},glass:{brightness:1},interactions:{rainRefractionStrength:.76},lightning:{enabled:!0},rain:{fallingAngle:0,fallingIntensity:.84,fallingLayers:5,fallingSpeed:3,fallingStreakLength:.54,glassIntensity:.38,glassZoom:.5}}},sleet:{dawn:{celestial:{celestialX:.69,celestialY:.74,skyBrightness:.82,starDensity:.36,sunGlowIntensity:5},cloud:{ambientDarkness:1,backlightIntensity:1.27,turbulence:.56,windSpeed:.03},glass:{brightness:.95},rain:{fallingAngle:.31,fallingIntensity:.22,fallingSpeed:2.07,fallingStreakLength:2,glassIntensity:.41,glassZoom:.85},snow:{drift:1,fallSpeed:8,flakeSize:.85,flutter:.98,glowAmount:.09,intensity:.34,layers:8,opacity:.78,sizeVariation:1,sparkle:.48,turbulence:.92,windAngle:1.76,windShear:1,windSpeed:1.8}},dusk:{celestial:{celestialX:.69,celestialY:.74,starDensity:.36,sunGlowIntensity:5},cloud:{turbulence:.56,windSpeed:.03},glass:{brightness:.9},lightning:{enabled:!0},rain:{fallingAngle:.31,fallingIntensity:.22,fallingSpeed:2.07,fallingStreakLength:2,glassIntensity:.41,glassZoom:.85},snow:{drift:1,fallSpeed:8,flakeSize:.85,flutter:.98,glowAmount:.09,intensity:.34,layers:8,opacity:.78,sizeVariation:1,sparkle:.48,windAngle:1.76,windShear:1,windSpeed:1.8}},midnight:{celestial:{celestialX:.69,celestialY:.74,starDensity:.36,sunGlowIntensity:5},cloud:{turbulence:.56,windSpeed:.03},lightning:{enabled:!0},rain:{fallingAngle:.31,fallingIntensity:.22,fallingSpeed:2.07,fallingStreakLength:2,glassIntensity:.41,glassZoom:.85},snow:{drift:1,fallSpeed:8,flakeSize:.85,flutter:.98,glowAmount:.09,intensity:.34,layers:8,opacity:.78,sizeVariation:1,sparkle:.48,windAngle:1.76,windShear:1,windSpeed:1.8}},noon:{celestial:{celestialX:.69,celestialY:.74,starDensity:.36,sunGlowIntensity:5},cloud:{turbulence:.56,windSpeed:.03},glass:{brightness:1},lightning:{enabled:!0},rain:{fallingAngle:.31,fallingIntensity:.22,fallingSpeed:2.07,fallingStreakLength:2,glassIntensity:.41,glassZoom:.85},snow:{drift:1,fallSpeed:8,flakeSize:.85,flutter:.98,glowAmount:.09,intensity:.34,layers:8,opacity:.78,sizeVariation:1,sparkle:.48,turbulence:1,windAngle:1.76,windShear:1,windSpeed:1.8}}},snow:{dawn:{celestial:{celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,skyBrightness:.6,skyContrast:0,skySaturation:1.5,starDensity:1.72},cloud:{ambientDarkness:.94,backlightIntensity:1.14,density:1.19,lightIntensity:1.73,turbulence:.3,windSpeed:.04},glass:{brightness:.95,chromaticAberration:2,depth:8,strength:10},post:{godRayIntensity:.4,haze:.03,hazeContrast:.05,hazeDesaturation:0,hazeHorizon:1},snow:{drift:1,fallSpeed:1.5,flakeSize:1.05,flutter:1,glowAmount:.44,intensity:.53,layers:8,opacity:.4,sizeVariation:.63,sparkle:.97,turbulence:.59,windAngle:1.95,windShear:.8,windSpeed:2}},dusk:{celestial:{celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,skyBrightness:1.01,skyContrast:.86,skySaturation:2,starDensity:1.72,sunGlowIntensity:4.9},cloud:{ambientDarkness:.9,backlightIntensity:1.53,cloudScale:1.82,coverage:.62,density:1.19,lightIntensity:.55,softness:.31,turbulence:.3,windSpeed:.04},glass:{blur:2,chromaticAberration:2,depth:8,strength:10},post:{godRayIntensity:.4,haze:.03,hazeContrast:.05,hazeDesaturation:0,hazeHorizon:1},snow:{drift:1,fallSpeed:1.5,flakeSize:1.05,flutter:1,glowAmount:.88,intensity:.96,layers:8,opacity:.34,sizeVariation:.63,sparkle:.97,turbulence:.59,windAngle:1.95,windShear:.8,windSpeed:2}},midnight:{celestial:{celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,moonPhase:.5,skyBrightness:1.07,skyContrast:.59,skySaturation:1.57,starDensity:1.72},cloud:{ambientDarkness:.04,cloudScale:1.15,coverage:.5,density:1.19,lightIntensity:1.29,softness:.14,turbulence:.3,windAngle:-.08,windSpeed:.04},glass:{blur:1,chromaticAberration:2,depth:8,strength:10},post:{godRayIntensity:.4,haze:.03,hazeContrast:.05,hazeDesaturation:0,hazeHorizon:1},snow:{drift:1,fallSpeed:1.5,flakeSize:1.05,flutter:1,glowAmount:.88,intensity:.53,layers:8,opacity:.34,sizeVariation:.63,sparkle:.97,turbulence:.59,windAngle:1.95,windShear:.8,windSpeed:1.44}},noon:{celestial:{celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,skyBrightness:.35,skyContrast:.69,skySaturation:.73,starDensity:1.72,sunGlowIntensity:3.45,sunGlowSize:.29,sunRayCount:0},cloud:{ambientDarkness:1,backlightIntensity:1.01,density:1.19,lightIntensity:.57,turbulence:.3,windSpeed:.04},glass:{blur:1,brightness:1.15,chromaticAberration:2,depth:8,saturation:1.4,strength:10},post:{godRayIntensity:.4,haze:.03,hazeContrast:.05,hazeDesaturation:0,hazeHorizon:1},snow:{drift:1,fallSpeed:1.5,flakeSize:1.28,flutter:1,glowAmount:.47,intensity:.53,layers:8,opacity:.34,sizeVariation:.63,sparkle:1,turbulence:.59,windAngle:1.95,windShear:.8,windSpeed:2}}},thunderstorm:{dawn:{celestial:{celestialX:.69,celestialY:.74,moonGlowIntensity:0,moonGlowSize:0,skyBrightness:.78,skyContrast:1.03,skySaturation:.94,starDensity:1.72,sunGlowIntensity:5.1,sunRayCount:5,sunRayIntensity:.13,sunRayLength:1.7,sunRayShimmer:5,sunRayShimmerSpeed:5},cloud:{backlightIntensity:.55,cloudScale:1.56,coverage:.71,density:.97,lightIntensity:0,numLayers:1,softness:.28,turbulence:.36,windSpeed:.04},glass:{blur:1,brightness:.9,chromaticAberration:2,strength:45},interactions:{lightningSceneIllumination:.83,rainRefractionStrength:2},lightning:{autoInterval:8,branchDensity:.83,enabled:!0,flashIntensity:1.57},post:{bloomIntensity:.39,exposureIntensity:.95,exposureRecovery:4.5,godRayDecay:.894,godRayDensity:.91,godRayIntensity:.26,godRaySamples:39,godRayWeight:.36,haze:.07,hazeContrast:0,hazeHorizon:1},rain:{fallingAngle:.02,fallingLayers:12,fallingSpeed:3,fallingStreakLength:1.46,glassIntensity:1.71,glassZoom:.51}},dusk:{celestial:{celestialX:.69,celestialY:.74,moonGlowIntensity:0,moonGlowSize:0,skyBrightness:.91,skyContrast:1.03,skySaturation:1.19,starDensity:1.72,sunGlowIntensity:5.05,sunRayShimmer:5,sunRayShimmerSpeed:5},cloud:{ambientDarkness:1,backlightIntensity:.55,cloudScale:1.56,coverage:.71,density:.97,lightIntensity:0,numLayers:1,softness:.28,turbulence:.36,windSpeed:.04},glass:{blur:1,brightness:.9,chromaticAberration:2,strength:45},interactions:{lightningSceneIllumination:.83,rainRefractionStrength:2},lightning:{branchDensity:.83,enabled:!0,flashIntensity:1.57},post:{bloomIntensity:.44,exposureIntensity:1.1,exposureRecovery:4.5,haze:.07,hazeContrast:0,hazeHorizon:1},rain:{fallingAngle:.02,fallingLayers:12,fallingSpeed:3,fallingStreakLength:1.46,glassIntensity:1.32,glassZoom:.51}},midnight:{celestial:{celestialY:.71,moonGlowIntensity:4.55,moonGlowSize:1.25,moonPhase:.5,skyBrightness:.78,skyContrast:1.03,skySaturation:.74,starDensity:1.72,sunRayShimmer:5,sunRayShimmerSpeed:5},cloud:{ambientDarkness:.88,backlightIntensity:.55,cloudScale:1.56,coverage:.71,density:.97,lightIntensity:0,numLayers:1,softness:.28,turbulence:.36,windSpeed:.04},glass:{blur:1,brightness:.9,chromaticAberration:2,strength:45},interactions:{lightningSceneIllumination:.83,rainRefractionStrength:2},lightning:{autoInterval:8.5,branchDensity:.89,enabled:!0,flashIntensity:1.57},post:{bloomIntensity:.44,exposureIntensity:1.1,exposureRecovery:4.5,haze:.07,hazeContrast:0,hazeHorizon:1},rain:{fallingAngle:.02,fallingLayers:12,fallingSpeed:3,fallingStreakLength:1.46,glassIntensity:1.32,glassZoom:.51}},noon:{celestial:{celestialY:.71,moonGlowIntensity:0,moonGlowSize:0,skyBrightness:.74,skyContrast:1.07,skySaturation:.55,starDensity:1.72,sunGlowIntensity:2.7,sunGlowSize:.51,sunRayCount:0,sunRayIntensity:0,sunRayLength:0,sunRayShimmer:5,sunRayShimmerSpeed:5},cloud:{ambientDarkness:1,backlightIntensity:.55,cloudScale:1.56,coverage:.71,density:.97,lightIntensity:0,numLayers:1,softness:.28,turbulence:.36,windSpeed:.04},glass:{blur:1,brightness:.9,chromaticAberration:2,strength:45},interactions:{lightningSceneIllumination:.83,rainRefractionStrength:2},lightning:{enabled:!0,flashIntensity:1.57},post:{bloomIntensity:.44,exposureIntensity:1.1,exposureRecovery:4.5,haze:.07,hazeContrast:0,hazeHorizon:1},rain:{fallingAngle:.02,fallingLayers:12,fallingSpeed:3,fallingStreakLength:1.46,glassIntensity:1.32,glassZoom:.51}}},windy:{dawn:{celestial:{celestialX:.69,celestialY:.74,skyBrightness:1,starDensity:.36,sunGlowIntensity:5},cloud:{cloudScale:.85,coverage:.4,windSpeed:.17},glass:{brightness:.95}},dusk:{celestial:{celestialX:.69,celestialY:.74,starDensity:.11,sunGlowIntensity:3.21,sunGlowSize:.51},cloud:{cloudScale:.85,coverage:.4,windSpeed:.17},glass:{brightness:.9}},midnight:{celestial:{celestialX:.74,celestialY:.71,moonGlowIntensity:3.55,moonGlowSize:.97,starDensity:1.72},cloud:{cloudScale:.85,coverage:.4,windSpeed:.17}},noon:{celestial:{celestialX:.74,celestialY:.71,moonPhase:.2421,starDensity:.14,sunGlowIntensity:1.59,sunGlowSize:.55},cloud:{cloudScale:.85,coverage:.4,windSpeed:.17},glass:{brightness:1}}}};function H(e){if(!e)return .5;let t=new Date(e),a=t.getUTCHours(),n=t.getUTCMinutes();return(a+n/60)/24}function Qe(e){if(!e)return .5;let t=new Date(e);t.setUTCHours(0,0,0,0);let a=new Date(\\\"2000-01-06T00:00:00Z\\\"),n=(t.getTime()-a.getTime())/(1e3*60*60*24),s=29.530588853;return(n%s+s)%s/s}function je(e){return e<0}var Je={clear:1,\\\"partly-cloudy\\\":.9,cloudy:.8,overcast:.65,fog:.7,drizzle:.7,rain:.6,\\\"heavy-rain\\\":.45,thunderstorm:.3,snow:.8,sleet:.65,hail:.5,windy:.9};function et(e,t){return e<.35?\\\"dark\\\":e>.45?\\\"light\\\":t??\\\"dark\\\"}function Oe(e){let t=e*24;return t<6?-1+t/6:t<12?(t-6)/6:t<18?1-(t-12)/6:-(t-18)/6}function tt(e,t=\\\"clear\\\"){let a=Oe(e),n;a<0?n=.05+(1+a)*.1:n=.15+a*.85;let s=Je[t],c=n*s;return Math.max(0,Math.min(1,c))}function nt(e=0){return e<=10?e/10*.3:e<=25?.3+(e-10)/15*.4:.7+Math.min((e-25)/25,.3)}function at(e){switch(e){case\\\"light\\\":return .3;case\\\"moderate\\\":return .6;case\\\"heavy\\\":return 1;default:return 0}}function st(e=10){return e>=10?0:e>=5?(10-e)/5*.3:.3+(5-e)/5*.7}function te(e){return Math.max(0,Math.min(1,e))}function ee(e,t,a){let n=te((a-e)/(t-e));return n*n*(3-2*n)}var D={x:.74,y:.78,sunSize:.14,moonSize:.17,starDensity:2,sunGlowIntensity:3.05,sunGlowSize:.3,sunRayCount:6,sunRayLength:3,sunRayIntensity:.1,moonGlowIntensity:3.45,moonGlowSize:.94},ot={clear:D,\\\"partly-cloudy\\\":D,cloudy:D,overcast:D,fog:D,drizzle:D,rain:D,\\\"heavy-rain\\\":D,thunderstorm:D,snow:D,sleet:D,hail:D,windy:D},it={clear:{cloud:{coverage:.1,speed:.3,darkness:0,turbulence:.2}},\\\"partly-cloudy\\\":{cloud:{coverage:.4,speed:.4,darkness:.1,turbulence:.3}},cloudy:{cloud:{coverage:.7,speed:.4,darkness:.2,turbulence:.3}},overcast:{cloud:{coverage:.95,speed:.3,darkness:.35,turbulence:.25}},fog:{cloud:{coverage:.6,speed:.15,darkness:.15,turbulence:.1}},drizzle:{cloud:{coverage:.75,speed:.35,darkness:.3,turbulence:.3},rain:{intensity:.25,glassDrops:!0,fallingRain:!0,angle:3}},rain:{cloud:{coverage:.85,speed:.5,darkness:.4,turbulence:.4},rain:{intensity:.6,glassDrops:!0,fallingRain:!0,angle:5}},\\\"heavy-rain\\\":{cloud:{coverage:.95,speed:.6,darkness:.55,turbulence:.5},rain:{intensity:1,glassDrops:!0,fallingRain:!0,angle:8}},thunderstorm:{cloud:{coverage:1,speed:.7,darkness:.7,turbulence:.6},rain:{intensity:1,glassDrops:!0,fallingRain:!0,angle:15},lightning:{enabled:!0,autoTrigger:!0,intervalMin:4,intervalMax:12}},snow:{cloud:{coverage:.7,speed:.25,darkness:.2,turbulence:.2},snow:{intensity:.7,windDrift:.3}},sleet:{cloud:{coverage:.8,speed:.4,darkness:.35,turbulence:.35},rain:{intensity:.5,glassDrops:!0,fallingRain:!0,angle:10},snow:{intensity:.3,windDrift:.4}},hail:{cloud:{coverage:.9,speed:.6,darkness:.5,turbulence:.5},rain:{intensity:.7,glassDrops:!0,fallingRain:!0,angle:5},snow:{intensity:.3,windDrift:.4}},windy:{cloud:{coverage:.5,speed:1,darkness:.1,turbulence:.6}}};function Me(e){let{conditionCode:t,windSpeed:a,precipitationLevel:n,visibility:s,timestamp:c,timeOfDay:l}=e,o=it[t],r=l??H(c),i=Oe(r),h=nt(a),f=at(n),p=st(s),b=je(i),R={sunAltitude:i,haze:Math.max(p,(o.cloud?.darkness??0)*.3),starVisibility:b&&!o.cloud?1:b?1-(o.cloud?.coverage??0):0},C={atmosphere:R};if(o.cloud&&(C.cloud={...o.cloud,speed:o.cloud.speed*(1+h*.5),turbulence:o.cloud.turbulence*(1+h*.3)}),o.rain){let E=f>0?f:o.rain.intensity;C.rain={...o.rain,intensity:E,angle:o.rain.angle+h*10}}o.lightning&&(C.lightning={...o.lightning}),o.snow&&(C.snow={...o.snow,windDrift:o.snow.windDrift+h*.3});let A=Qe(c),k=ot[t];C.celestial={timeOfDay:r,moonPhase:A,starDensity:b?k.starDensity:0,celestialX:k.x,celestialY:k.y,sunSize:k.sunSize,moonSize:k.moonSize,sunGlowIntensity:k.sunGlowIntensity,sunGlowSize:k.sunGlowSize,sunRayCount:k.sunRayCount,sunRayLength:k.sunRayLength,sunRayIntensity:k.sunRayIntensity,moonGlowIntensity:k.moonGlowIntensity,moonGlowSize:k.moonGlowSize};let w=te(R.haze),T=C.cloud?.coverage??0,Q=T>.001,$=te(.04+(t===\\\"fog\\\"?.18:t===\\\"thunderstorm\\\"?.12:t===\\\"heavy-rain\\\"?.1:t===\\\"overcast\\\"?.08:t===\\\"cloudy\\\"||t===\\\"partly-cloudy\\\"?.06:.04)+w*.22),G=1.1+w*1.2,z=C.lightning?.enabled?.85:0,V=ee(-.05,.08,i),F=1-ee(.18,.7,Math.max(0,i)),d=ee(.25,.85,T),u=1-ee(.97,1,T),y=.35+w*.65,g=Q?te(V*F*d*u*y*.6):0,m={enabled:!0,haze:w,bloomIntensity:$,bloomRadius:G,exposureIntensity:z,godRayIntensity:g};return C.post=m,C}function rt(e){let t=Me(e),a=e.timeOfDay??t.celestial?.timeOfDay??H(e.timestamp),n=t.cloud!==void 0,s=t.rain!==void 0,c=t.lightning!==void 0,l=t.snow!==void 0,o=t.lightning?.intervalMin??4,r=t.lightning?.intervalMax??12;return{layers:{celestial:!0,clouds:n,rain:s,lightning:c,snow:l},celestial:{timeOfDay:a,moonPhase:t.celestial?.moonPhase??.5,starDensity:t.celestial?.starDensity??.5,celestialX:t.celestial?.celestialX??.5,celestialY:t.celestial?.celestialY??.72,sunSize:t.celestial?.sunSize??.06,moonSize:t.celestial?.moonSize??.05,sunGlowIntensity:t.celestial?.sunGlowIntensity??1,sunGlowSize:t.celestial?.sunGlowSize??.3,sunRayCount:t.celestial?.sunRayCount??12,sunRayLength:t.celestial?.sunRayLength??.5,sunRayIntensity:t.celestial?.sunRayIntensity??.4,sunRayShimmer:1,sunRayShimmerSpeed:1,moonGlowIntensity:t.celestial?.moonGlowIntensity??1,moonGlowSize:t.celestial?.moonGlowSize??.2,skyBrightness:1,skySaturation:1,skyContrast:1},cloud:{cloudScale:1.5,coverage:t.cloud?.coverage??.5,density:.7,softness:.3,windSpeed:t.cloud?.speed??.5,windAngle:0,turbulence:t.cloud?.turbulence??.5,lightIntensity:1,ambientDarkness:t.cloud?.darkness??.3,backlightIntensity:.5,numLayers:3},rain:{glassIntensity:s?(t.rain?.intensity??.5)*.7:0,zoom:1,fallingIntensity:s?t.rain?.intensity??.6:0,fallingSpeed:1,fallingAngle:s?(t.rain?.angle??5)*.02:.1,fallingStreakLength:.8,fallingLayers:3,fallingRefraction:.3},lightning:{autoMode:c?t.lightning?.autoTrigger??!0:!1,autoInterval:c?(o+r)/2:8,glowIntensity:.8,branchDensity:.6,sceneIllumination:.6},snow:{intensity:l?t.snow?.intensity??.7:0,layers:4,fallSpeed:.5,windSpeed:l?t.snow?.windDrift??.3:.3,windAngle:0,turbulence:.3,drift:l?t.snow?.windDrift??.3:.3,flutter:.5,windShear:.2,flakeSize:1,sizeVariation:.5,opacity:.8,glowAmount:.3,sparkle:.2},post:{enabled:!0,haze:0,hazeHorizon:.5,hazeDesaturation:.3,hazeContrast:.2,bloomIntensity:0,bloomThreshold:.8,bloomKnee:.5,bloomRadius:8,bloomTapScale:1,exposureIntensity:0,exposureDesaturation:.3,exposureRecovery:2,godRayIntensity:0,godRayDecay:.96,godRayDensity:.5,godRayWeight:.3,godRaySamples:60}}}function lt(e){return{layers:e.layers,celestial:{timeOfDay:e.celestial.timeOfDay,moonPhase:e.celestial.moonPhase,starDensity:e.celestial.starDensity,celestialX:e.celestial.celestialX,celestialY:e.celestial.celestialY,sunSize:e.celestial.sunSize,moonSize:e.celestial.moonSize,sunGlowIntensity:e.celestial.sunGlowIntensity,sunGlowSize:e.celestial.sunGlowSize,sunRayCount:e.celestial.sunRayCount,sunRayLength:e.celestial.sunRayLength,sunRayIntensity:e.celestial.sunRayIntensity,sunRayShimmer:e.celestial.sunRayShimmer,sunRayShimmerSpeed:e.celestial.sunRayShimmerSpeed,moonGlowIntensity:e.celestial.moonGlowIntensity,moonGlowSize:e.celestial.moonGlowSize,skyBrightness:e.celestial.skyBrightness,skySaturation:e.celestial.skySaturation,skyContrast:e.celestial.skyContrast},cloud:{coverage:e.cloud.coverage,density:e.cloud.density,softness:e.cloud.softness,cloudScale:e.cloud.cloudScale,windSpeed:e.cloud.windSpeed,windAngle:e.cloud.windAngle,turbulence:e.cloud.turbulence,lightIntensity:e.cloud.lightIntensity,ambientDarkness:e.cloud.ambientDarkness,backlightIntensity:e.cloud.backlightIntensity,numLayers:e.cloud.numLayers},rain:{glassIntensity:e.rain.glassIntensity,glassZoom:e.rain.zoom,fallingIntensity:e.rain.fallingIntensity,fallingSpeed:e.rain.fallingSpeed,fallingAngle:e.rain.fallingAngle,fallingStreakLength:e.rain.fallingStreakLength,fallingLayers:e.rain.fallingLayers},lightning:{enabled:e.layers.lightning,autoMode:e.lightning.autoMode,autoInterval:e.lightning.autoInterval,flashIntensity:e.lightning.glowIntensity,branchDensity:e.lightning.branchDensity},snow:{intensity:e.snow.intensity,layers:e.snow.layers,fallSpeed:e.snow.fallSpeed,windSpeed:e.snow.windSpeed,windAngle:e.snow.windAngle,turbulence:e.snow.turbulence,drift:e.snow.drift,flutter:e.snow.flutter,windShear:e.snow.windShear,flakeSize:e.snow.flakeSize,sizeVariation:e.snow.sizeVariation,opacity:e.snow.opacity,glowAmount:e.snow.glowAmount,sparkle:e.snow.sparkle},interactions:{rainRefractionStrength:e.rain.fallingRefraction,lightningSceneIllumination:e.lightning.sceneIllumination},post:e.post}}function Be(e){return lt(rt(e))}var me={dawn:.25,noon:.5,dusk:.75,midnight:0},ut=[\\\"dawn\\\",\\\"noon\\\",\\\"dusk\\\",\\\"midnight\\\"];function q(e){let t=(e%1+1)%1,a=\\\"noon\\\",n=1/0;for(let s of ut){let c=me[s],l=Math.abs(t-c);l>.5&&(l=1-l);let o=Math.abs(l-n)<=Number.EPSILON;(l<n||o&&s===\\\"midnight\\\")&&(n=l,a=s)}return a}function W(e,t){if(!(!e&&!t))return{...e,...t}}function Ne(e,t){return{...e,layers:W(e.layers,t.layers),celestial:W(e.celestial,t.celestial),cloud:W(e.cloud,t.cloud),rain:W(e.rain,t.rain),lightning:W(e.lightning,t.lightning),snow:W(e.snow,t.snow),glass:W(e.glass,t.glass),interactions:W(e.interactions,t.interactions),post:W(e.post,t.post)}}function Ye(e){let t=Be(e),a=e.timeOfDay;typeof a==\\\"number\\\"&&t.celestial&&(t.celestial.timeOfDay=a);let n=e.tunedPresets?.[e.conditionCode],s=a??t.celestial?.timeOfDay;if(!n||s===void 0)return t;let c=q(s);return Ne(t,n[c])}function ct(){if(typeof window>\\\"u\\\")return\\\"high\\\";let e=window.devicePixelRatio||1,t=window.innerWidth*window.innerHeight*e*e,a=typeof navigator<\\\"u\\\"?navigator.hardwareConcurrency:void 0,n=typeof navigator<\\\"u\\\"?navigator.deviceMemory:void 0,s=window.innerWidth<768;return typeof n==\\\"number\\\"&&n<=4||typeof a==\\\"number\\\"&&a<=4||t>25e5?s?\\\"low\\\":\\\"medium\\\":s?\\\"medium\\\":\\\"high\\\"}function Ve(e){return e===\\\"low\\\"||e===\\\"medium\\\"||e===\\\"high\\\"?e:ct()}function Xe(e){if(typeof window>\\\"u\\\")return;let t=window.devicePixelRatio||1;return Math.max(1,Math.min(t,e===\\\"low\\\"?1:e===\\\"medium\\\"?1.5:2))}import{jsx as He}from\\\"react/jsx-runtime\\\";var mt=de;function yt({conditionCode:e,windSpeed:t,precipitationLevel:a,visibility:n,timestamp:s,timeOfDay:c,settings:l,className:o}){let[r,i]=ft(!1),h=l?.enabled!==!1,f=l?.reducedMotion??!1,p=ye(()=>Ve(l?.quality??\\\"auto\\\"),[l?.quality]);dt(()=>{i(!0)},[]);let b=ye(()=>Xe(p),[p]),R=ye(()=>!h||f?null:Ye({conditionCode:e,windSpeed:t,precipitationLevel:a,visibility:n,timestamp:s,timeOfDay:c,tunedPresets:mt}),[h,f,e,t,a,n,s,c]);return!r||!h||f||!R?null:He(\\\"div\\\",{className:o,style:{position:\\\"absolute\\\",inset:0,overflow:\\\"hidden\\\",pointerEvents:\\\"none\\\",borderRadius:\\\"inherit\\\"},\\\"aria-hidden\\\":\\\"true\\\",children:He(Fe,{className:\\\"absolute inset-0\\\",dpr:b,...R})})}function ht({glassStyles:e,blurAmount:t}){if(!!e.backdropFilter)return e;let n=`blur(${t}px)`;return{backdropFilter:n,WebkitBackdropFilter:n}}import{useEffect as gt,useMemo as pt,useState as St}from\\\"react\\\";var Y={depth:12,radius:12,strength:40,chromaticAberration:8,blur:2,brightness:1.05,saturation:1.2};function vt({width:e,height:t,radius:a,depth:n}){let s=Math.ceil(a/t*15),c=Math.ceil(a/e*15),l=Math.max(0,e-2*n),o=Math.max(0,t-2*n),r=`<svg height=\\\"${t}\\\" width=\\\"${e}\\\" viewBox=\\\"0 0 ${e} ${t}\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n    <style>.mix { mix-blend-mode: screen; }</style>\\n    <defs>\\n      <linearGradient id=\\\"Y\\\" x1=\\\"0\\\" x2=\\\"0\\\" y1=\\\"${s}%\\\" y2=\\\"${100-s}%\\\">\\n        <stop offset=\\\"0%\\\" stop-color=\\\"#0F0\\\" />\\n        <stop offset=\\\"100%\\\" stop-color=\\\"#000\\\" />\\n      </linearGradient>\\n      <linearGradient id=\\\"X\\\" x1=\\\"${c}%\\\" x2=\\\"${100-c}%\\\" y1=\\\"0\\\" y2=\\\"0\\\">\\n        <stop offset=\\\"0%\\\" stop-color=\\\"#F00\\\" />\\n        <stop offset=\\\"100%\\\" stop-color=\\\"#000\\\" />\\n      </linearGradient>\\n    </defs>\\n    <rect x=\\\"0\\\" y=\\\"0\\\" height=\\\"${t}\\\" width=\\\"${e}\\\" fill=\\\"#808080\\\" />\\n    <g filter=\\\"blur(2px)\\\">\\n      <rect x=\\\"0\\\" y=\\\"0\\\" height=\\\"${t}\\\" width=\\\"${e}\\\" fill=\\\"#000080\\\" />\\n      <rect x=\\\"0\\\" y=\\\"0\\\" height=\\\"${t}\\\" width=\\\"${e}\\\" fill=\\\"url(#Y)\\\" class=\\\"mix\\\" />\\n      <rect x=\\\"0\\\" y=\\\"0\\\" height=\\\"${t}\\\" width=\\\"${e}\\\" fill=\\\"url(#X)\\\" class=\\\"mix\\\" />\\n      <rect x=\\\"${n}\\\" y=\\\"${n}\\\" height=\\\"${o}\\\" width=\\\"${l}\\\" fill=\\\"#808080\\\" rx=\\\"${a}\\\" ry=\\\"${a}\\\" filter=\\\"blur(${n}px)\\\" />\\n    </g>\\n  </svg>`;return\\\"data:image/svg+xml;utf8,\\\"+encodeURIComponent(r)}function bt({width:e,height:t,radius:a,depth:n,strength:s,chromaticAberration:c}){let l=vt({width:e,height:t,radius:a,depth:n}),o=`<feImage x=\\\"0\\\" y=\\\"0\\\" height=\\\"${t}\\\" width=\\\"${e}\\\" href=\\\"${l}\\\" result=\\\"displacementMap\\\" />`,r;if(c===0)r=`\\n      ${o}\\n      <feDisplacementMap in=\\\"SourceGraphic\\\" in2=\\\"displacementMap\\\" scale=\\\"${s}\\\" xChannelSelector=\\\"R\\\" yChannelSelector=\\\"G\\\" />\\n    `;else{let h=s+c*2,f=s+c;r=`\\n      ${o}\\n      <feDisplacementMap in=\\\"SourceGraphic\\\" in2=\\\"displacementMap\\\" scale=\\\"${h}\\\" xChannelSelector=\\\"R\\\" yChannelSelector=\\\"G\\\" />\\n      <feColorMatrix type=\\\"matrix\\\" values=\\\"1 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1 0\\\" result=\\\"displacedR\\\" />\\n      <feDisplacementMap in=\\\"SourceGraphic\\\" in2=\\\"displacementMap\\\" scale=\\\"${f}\\\" xChannelSelector=\\\"R\\\" yChannelSelector=\\\"G\\\" />\\n      <feColorMatrix type=\\\"matrix\\\" values=\\\"0 0 0 0 0  0 1 0 0 0  0 0 0 0 0  0 0 0 1 0\\\" result=\\\"displacedG\\\" />\\n      <feDisplacementMap in=\\\"SourceGraphic\\\" in2=\\\"displacementMap\\\" scale=\\\"${s}\\\" xChannelSelector=\\\"R\\\" yChannelSelector=\\\"G\\\" />\\n      <feColorMatrix type=\\\"matrix\\\" values=\\\"0 0 0 0 0  0 0 0 0 0  0 0 1 0 0  0 0 0 1 0\\\" result=\\\"displacedB\\\" />\\n      <feBlend in=\\\"displacedR\\\" in2=\\\"displacedG\\\" mode=\\\"screen\\\"/>\\n      <feBlend in2=\\\"displacedB\\\" mode=\\\"screen\\\"/>\\n    `}let i=`<svg height=\\\"${t}\\\" width=\\\"${e}\\\" viewBox=\\\"0 0 ${e} ${t}\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n    <defs>\\n      <filter id=\\\"displace\\\" color-interpolation-filters=\\\"sRGB\\\">${r}</filter>\\n    </defs>\\n  </svg>`;return\\\"data:image/svg+xml;utf8,\\\"+encodeURIComponent(i)+\\\"#displace\\\"}function wt({filterUrl:e,blur:t,brightness:a,saturation:n}){return`blur(${t/2}px) url('${e}') blur(${t}px) brightness(${a}) saturate(${n})`}function xt(){let[e,t]=St(!0);return gt(()=>{let a=CSS.supports(\\\"backdrop-filter\\\",\\\"blur(1px)\\\")||CSS.supports(\\\"-webkit-backdrop-filter\\\",\\\"blur(1px)\\\");t(a)},[]),e}function It({width:e,height:t,depth:a=Y.depth,radius:n=Y.radius,strength:s=Y.strength,chromaticAberration:c=Y.chromaticAberration,blur:l=Y.blur,brightness:o=Y.brightness,saturation:r=Y.saturation,enabled:i=!0}){let h=xt();return pt(()=>{if(!i||!h||e<=0||t<=0)return{};let f=bt({width:e,height:t,radius:n,depth:a,strength:s,chromaticAberration:c}),p=wt({filterUrl:f,blur:l,brightness:o,saturation:r});return{backdropFilter:p,WebkitBackdropFilter:p}},[e,t,a,n,s,c,l,o,r,i,h])}function Ue(e){return(e%1+1)%1}function Rt(e){let t=Ue(e),a=q(t);return me[a]}function kt(e){return((Math.floor(e)%12+12)%12+.5)/12}function _t(e){let{time:t,updatedAt:a}=e;return typeof t?.timeBucket==\\\"number\\\"?{timeOfDay:kt(t.timeBucket),source:\\\"timeBucket\\\"}:typeof t?.localTimeOfDay==\\\"number\\\"?{timeOfDay:Ue(t.localTimeOfDay),source:\\\"localTimeOfDay\\\"}:typeof a==\\\"string\\\"?{timeOfDay:H(a),source:\\\"updatedAt\\\"}:{timeOfDay:.5,source:\\\"defaultNoon\\\"}}export{yt as EffectCompositorRuntime,de as TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES,q as getNearestCheckpoint,tt as getSceneBrightnessFromTimeOfDay,H as getTimeOfDay,et as getWeatherTheme,ht as resolveGlassBackdropFilterStyles,_t as resolveWeatherTime,Rt as snapTimeOfDayToNearestCheckpoint,It as useGlassStyles};\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/weather-widget/runtime.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/weather-widget/runtime.ts\",\n      \"content\": \"/**\\n * Start here when wiring the Weather Widget in your app.\\n *\\n * Import `WeatherWidget` and the runtime types from this file for production use.\\n * Reach for `index.tsx` only if you're actively working on authoring/debug internals.\\n */\\nexport { WeatherWidget } from \\\"./weather-widget-container\\\";\\nexport type {\\n  WeatherWidgetPayload,\\n  WeatherWidgetRuntimeProps as WeatherWidgetProps,\\n  WeatherWidgetCurrent,\\n  WeatherWidgetTime,\\n  WeatherWidgetLocation,\\n  WeatherConditionCode,\\n  ForecastDay,\\n  TemperatureUnit,\\n  PrecipitationLevel,\\n} from \\\"./schema-runtime\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/weather-widget/schema-runtime.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/weather-widget/schema-runtime.ts\",\n      \"content\": \"export type WeatherConditionCode =\\n  | \\\"clear\\\"\\n  | \\\"partly-cloudy\\\"\\n  | \\\"cloudy\\\"\\n  | \\\"overcast\\\"\\n  | \\\"fog\\\"\\n  | \\\"drizzle\\\"\\n  | \\\"rain\\\"\\n  | \\\"heavy-rain\\\"\\n  | \\\"thunderstorm\\\"\\n  | \\\"snow\\\"\\n  | \\\"sleet\\\"\\n  | \\\"hail\\\"\\n  | \\\"windy\\\";\\n\\nexport type TemperatureUnit = \\\"celsius\\\" | \\\"fahrenheit\\\";\\n\\nexport type PrecipitationLevel = \\\"none\\\" | \\\"light\\\" | \\\"moderate\\\" | \\\"heavy\\\";\\n\\nexport type TimeBucket = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11;\\n\\nexport interface ForecastDay {\\n  label: string;\\n  conditionCode: WeatherConditionCode;\\n  tempMin: number;\\n  tempMax: number;\\n}\\n\\nexport interface WeatherWidgetCurrent {\\n  conditionCode: WeatherConditionCode;\\n  temperature: number;\\n  tempMin: number;\\n  tempMax: number;\\n  windSpeed?: number;\\n  precipitationLevel?: PrecipitationLevel;\\n  visibility?: number;\\n}\\n\\nexport interface WeatherWidgetTime {\\n  timeBucket?: TimeBucket;\\n  localTimeOfDay?: number;\\n}\\n\\nexport interface WeatherWidgetLocation {\\n  name: string;\\n}\\n\\nexport interface WeatherWidgetPayload {\\n  version: \\\"3.1\\\";\\n  id: string;\\n  location: WeatherWidgetLocation;\\n  units: {\\n    temperature: TemperatureUnit;\\n  };\\n  current: WeatherWidgetCurrent;\\n  forecast: ForecastDay[];\\n  time: WeatherWidgetTime;\\n  updatedAt?: string;\\n}\\n\\nexport type EffectQuality = \\\"low\\\" | \\\"medium\\\" | \\\"high\\\" | \\\"auto\\\";\\n\\nexport interface EffectSettings {\\n  enabled?: boolean;\\n  quality?: EffectQuality;\\n  reducedMotion?: boolean;\\n}\\n\\nexport interface WeatherWidgetRuntimeProps extends WeatherWidgetPayload {\\n  className?: string;\\n  effects?: EffectSettings;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/weather-widget/weather-data-overlay.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/weather-widget/weather-data-overlay.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { useEffect, useRef, useState, useCallback } from \\\"react\\\";\\nimport {\\n  Sun,\\n  Cloud,\\n  CloudSun,\\n  CloudFog,\\n  CloudDrizzle,\\n  CloudRain,\\n  CloudLightning,\\n  Snowflake,\\n  CloudHail,\\n  Wind,\\n  type LucideIcon,\\n} from \\\"lucide-react\\\";\\nimport { cn } from \\\"@/lib/utils\\\";\\nimport type {\\n  ForecastDay,\\n  TemperatureUnit,\\n  WeatherConditionCode,\\n} from \\\"./schema-runtime\\\";\\nimport {\\n  getSceneBrightnessFromTimeOfDay,\\n  getTimeOfDay,\\n  getWeatherTheme,\\n} from \\\"./generated/weather-runtime-core.generated\\\";\\nimport {\\n  resolveGlassBackdropFilterStyles,\\n  useGlassStyles,\\n} from \\\"./generated/weather-runtime-core.generated\\\";\\n\\ntype WeatherTheme = \\\"light\\\" | \\\"dark\\\";\\n\\nfunction getPeakIntensity(timeOfDay: number): number {\\n  const noonDistance = Math.abs(timeOfDay - 0.5);\\n  const midnightDistance = Math.min(timeOfDay, 1 - timeOfDay);\\n  const minDistance = Math.min(noonDistance, midnightDistance);\\n  return Math.max(0, 1 - minDistance * 4);\\n}\\n\\nfunction sineEasedGradient(\\n  x: number,\\n  y: number,\\n  radius: number,\\n  peakOpacity: number,\\n  steps = 8,\\n): string {\\n  const stops: string[] = [];\\n  for (let i = 0; i <= steps; i++) {\\n    const t = i / steps;\\n    const eased = Math.sin((t * Math.PI) / 2);\\n    const opacity = peakOpacity * (1 - eased);\\n    const position = t * 100;\\n    stops.push(\\n      `rgba(255,255,255,${opacity.toFixed(4)}) ${position.toFixed(1)}%`,\\n    );\\n  }\\n  return `radial-gradient(circle ${radius}px at ${x}px ${y}px, ${stops.join(\\\", \\\")})`;\\n}\\n\\nconst conditionIcons: Record<WeatherConditionCode, LucideIcon> = {\\n  clear: Sun,\\n  \\\"partly-cloudy\\\": CloudSun,\\n  cloudy: Cloud,\\n  overcast: Cloud,\\n  fog: CloudFog,\\n  drizzle: CloudDrizzle,\\n  rain: CloudRain,\\n  \\\"heavy-rain\\\": CloudRain,\\n  thunderstorm: CloudLightning,\\n  snow: Snowflake,\\n  sleet: CloudHail,\\n  hail: CloudHail,\\n  windy: Wind,\\n};\\n\\nexport interface GlassEffectParams {\\n  enabled?: boolean;\\n  depth?: number;\\n  strength?: number;\\n  chromaticAberration?: number;\\n  blur?: number;\\n  brightness?: number;\\n  saturation?: number;\\n}\\n\\nexport interface WeatherDataOverlayProps {\\n  location: string;\\n  conditionCode: WeatherConditionCode;\\n  temperature: number;\\n  tempHigh: number;\\n  tempLow: number;\\n  forecast?: ForecastDay[];\\n  unit?: TemperatureUnit;\\n  theme?: WeatherTheme;\\n  /**\\n   * Provide either `timeOfDay` (0-1) or a `timestamp` ISO string.\\n   * If neither is provided, defaults to noon (0.5).\\n   */\\n  timeOfDay?: number;\\n  timestamp?: string | undefined;\\n  className?: string;\\n  reducedMotion?: boolean;\\n  /**\\n   * Glass refraction effect parameters for the forecast card.\\n   * When enabled, applies SVG displacement filter for realistic glass distortion.\\n   */\\n  glassParams?: GlassEffectParams | undefined;\\n}\\n\\ninterface GlowState {\\n  x: number;\\n  y: number;\\n  intensity: number;\\n}\\n\\nexport function observeCardDimensions(\\n  element: HTMLDivElement | null,\\n  onResize: () => void,\\n): () => void {\\n  if (!element || typeof ResizeObserver !== \\\"function\\\") {\\n    return () => {};\\n  }\\n\\n  const observer = new ResizeObserver(onResize);\\n  observer.observe(element);\\n  return () => observer.disconnect();\\n}\\n\\nexport function WeatherDataOverlay({\\n  location,\\n  conditionCode,\\n  temperature,\\n  tempHigh,\\n  tempLow,\\n  forecast = [],\\n  unit = \\\"fahrenheit\\\",\\n  theme: themeProp,\\n  timeOfDay: timeOfDayProp,\\n  timestamp,\\n  className,\\n  reducedMotion = false,\\n  glassParams,\\n}: WeatherDataOverlayProps) {\\n  const timeOfDay =\\n    typeof timeOfDayProp === \\\"number\\\"\\n      ? timeOfDayProp\\n      : typeof timestamp === \\\"string\\\"\\n        ? getTimeOfDay(timestamp)\\n        : 0.5;\\n\\n  const [glowState, setGlowState] = useState<GlowState>({\\n    x: 0,\\n    y: 0,\\n    intensity: 0,\\n  });\\n  const [cardDimensions, setCardDimensions] = useState({ width: 0, height: 0 });\\n  const cardRef = useRef<HTMLDivElement>(null);\\n  const containerRef = useRef<HTMLDivElement>(null);\\n  const pendingGlowStateRef = useRef<GlowState | null>(null);\\n  const pendingGlowFrameRef = useRef<number | null>(null);\\n\\n  // Glass effect styles applied directly to forecast container.\\n  // Enabled by default - falls back to simple blur if SVG filter unsupported.\\n  const glassEnabled = glassParams?.enabled !== false;\\n  const glassStyles = useGlassStyles({\\n    width: cardDimensions.width,\\n    height: cardDimensions.height,\\n    depth: glassParams?.depth ?? 3,\\n    radius: 12,\\n    strength: glassParams?.strength ?? 75,\\n    chromaticAberration: glassParams?.chromaticAberration ?? 6,\\n    blur: glassParams?.blur ?? 1.5,\\n    brightness: glassParams?.brightness ?? 0.8,\\n    saturation: glassParams?.saturation ?? 1.3,\\n    enabled: glassEnabled,\\n  });\\n\\n  // Track forecast card dimensions for glass effect\\n  const updateCardDimensions = useCallback(() => {\\n    if (cardRef.current) {\\n      const rect = cardRef.current.getBoundingClientRect();\\n      setCardDimensions({\\n        width: Math.round(rect.width),\\n        height: Math.round(rect.height),\\n      });\\n    }\\n  }, []);\\n  const hasForecastStrip = forecast.length > 0;\\n\\n  useEffect(() => {\\n    updateCardDimensions();\\n    return observeCardDimensions(cardRef.current, updateCardDimensions);\\n  }, [hasForecastStrip, updateCardDimensions]);\\n\\n  const theme =\\n    themeProp ??\\n    getWeatherTheme(\\n      getSceneBrightnessFromTimeOfDay(timeOfDay, conditionCode),\\n      undefined,\\n    );\\n\\n  const commitGlowState = useCallback((nextState: GlowState) => {\\n    setGlowState((prevState) => {\\n      if (\\n        prevState.x === nextState.x &&\\n        prevState.y === nextState.y &&\\n        prevState.intensity === nextState.intensity\\n      ) {\\n        return prevState;\\n      }\\n\\n      return nextState;\\n    });\\n  }, []);\\n\\n  const cancelPendingGlowFrame = useCallback(() => {\\n    pendingGlowStateRef.current = null;\\n\\n    if (\\n      pendingGlowFrameRef.current !== null &&\\n      typeof window !== \\\"undefined\\\" &&\\n      typeof window.cancelAnimationFrame === \\\"function\\\"\\n    ) {\\n      window.cancelAnimationFrame(pendingGlowFrameRef.current);\\n    }\\n\\n    pendingGlowFrameRef.current = null;\\n  }, []);\\n\\n  const scheduleGlowState = useCallback(\\n    (nextState: GlowState) => {\\n      pendingGlowStateRef.current = nextState;\\n\\n      if (pendingGlowFrameRef.current !== null) {\\n        return;\\n      }\\n\\n      if (\\n        typeof window === \\\"undefined\\\" ||\\n        typeof window.requestAnimationFrame !== \\\"function\\\"\\n      ) {\\n        pendingGlowStateRef.current = null;\\n        commitGlowState(nextState);\\n        return;\\n      }\\n\\n      pendingGlowFrameRef.current = window.requestAnimationFrame(() => {\\n        pendingGlowFrameRef.current = null;\\n        const pendingState = pendingGlowStateRef.current;\\n        pendingGlowStateRef.current = null;\\n\\n        if (pendingState) {\\n          commitGlowState(pendingState);\\n        }\\n      });\\n    },\\n    [commitGlowState],\\n  );\\n\\n  const clearGlowIntensity = useCallback(() => {\\n    cancelPendingGlowFrame();\\n    setGlowState((prevState) => {\\n      if (prevState.intensity === 0) {\\n        return prevState;\\n      }\\n\\n      return { ...prevState, intensity: 0 };\\n    });\\n  }, [cancelPendingGlowFrame]);\\n\\n  useEffect(() => {\\n    if (reducedMotion) {\\n      clearGlowIntensity();\\n      return;\\n    }\\n\\n    const container = containerRef.current;\\n    if (!container) return;\\n\\n    const handleMouseMove = (e: MouseEvent) => {\\n      if (!cardRef.current) return;\\n      const cardRect = cardRef.current.getBoundingClientRect();\\n\\n      const clampedX = Math.max(\\n        cardRect.left,\\n        Math.min(e.clientX, cardRect.right),\\n      );\\n      const clampedY = Math.max(\\n        cardRect.top,\\n        Math.min(e.clientY, cardRect.bottom),\\n      );\\n\\n      const distanceX =\\n        e.clientX < cardRect.left\\n          ? cardRect.left - e.clientX\\n          : e.clientX > cardRect.right\\n            ? e.clientX - cardRect.right\\n            : 0;\\n      const distanceY =\\n        e.clientY < cardRect.top\\n          ? cardRect.top - e.clientY\\n          : e.clientY > cardRect.bottom\\n            ? e.clientY - cardRect.bottom\\n            : 0;\\n      const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);\\n\\n      const maxDistance = 150;\\n      const intensity = Math.max(0, 1 - distance / maxDistance);\\n\\n      scheduleGlowState({\\n        x: clampedX - cardRect.left,\\n        y: clampedY - cardRect.top,\\n        intensity,\\n      });\\n    };\\n\\n    const handleMouseLeave = () => {\\n      clearGlowIntensity();\\n    };\\n\\n    container.addEventListener(\\\"mousemove\\\", handleMouseMove);\\n    container.addEventListener(\\\"mouseleave\\\", handleMouseLeave);\\n\\n    return () => {\\n      container.removeEventListener(\\\"mousemove\\\", handleMouseMove);\\n      container.removeEventListener(\\\"mouseleave\\\", handleMouseLeave);\\n      cancelPendingGlowFrame();\\n    };\\n  }, [\\n    reducedMotion,\\n    clearGlowIntensity,\\n    scheduleGlowState,\\n    cancelPendingGlowFrame,\\n  ]);\\n\\n  const roundedTemperature = Math.round(temperature);\\n  const unitSymbol = unit === \\\"celsius\\\" ? \\\"C\\\" : \\\"F\\\";\\n  const spokenUnit = unit === \\\"celsius\\\" ? \\\"Celsius\\\" : \\\"Fahrenheit\\\";\\n  const peakIntensity = getPeakIntensity(timeOfDay);\\n\\n  const isDark = theme === \\\"dark\\\";\\n  const textPrimary = isDark ? \\\"text-white\\\" : \\\"text-black\\\";\\n  const textPrimarySoft = isDark ? \\\"text-white/90\\\" : \\\"text-black/85\\\";\\n  const textSecondary = isDark ? \\\"text-white/80\\\" : \\\"text-black/80\\\";\\n  const textSubtle = isDark ? \\\"text-white/40\\\" : \\\"text-black/40\\\";\\n\\n  const baseBgOpacity = isDark ? 0.04 : 0.04;\\n  const bgOpacity = baseBgOpacity * (1 - peakIntensity * 0.7);\\n  const midnightDistance = Math.min(timeOfDay, 1 - timeOfDay);\\n  const baseBlur = isDark ? 2 + midnightDistance * 38 : 24;\\n  const blurAmount = isDark\\n    ? baseBlur\\n    : baseBlur - peakIntensity * (baseBlur - 8);\\n\\n  // Dawn intensity peaks around timeOfDay 0.2-0.3 (morning transition)\\n  const isDawn = timeOfDay > 0.1 && timeOfDay < 0.4;\\n  const dawnIntensity = isDawn ? 1 - Math.abs(timeOfDay - 0.25) * 4 : 0;\\n  const forecastTextShadow =\\n    dawnIntensity > 0\\n      ? `0 0.5px 1px rgba(0,0,0,${(dawnIntensity * 0.4).toFixed(2)})`\\n      : undefined;\\n\\n  const shadowStyle = isDark\\n    ? \\\"0 1px 8px rgba(0,0,0,0.3)\\\"\\n    : \\\"0 1px 8px rgba(255,255,255,0.3)\\\";\\n\\n  // Fluid type scales with the widget container size. (Requires container-type:size.)\\n  const locationFontSize = \\\"clamp(13px, 7.5cqmin, 17px)\\\";\\n  const temperatureFontSize = \\\"clamp(48px, 32cqmin, 72px)\\\";\\n  const degreeFontSize = \\\"clamp(18px, 12cqmin, 28px)\\\";\\n  const hiLoFontSize = \\\"clamp(11px, 6.5cqmin, 15px)\\\";\\n  const forecastFontFamily =\\n    '\\\"SF Pro Text\\\", Inter, \\\"Noto Sans\\\", system-ui, sans-serif';\\n\\n  return (\\n    <div\\n      ref={containerRef}\\n      className={cn(\\n        \\\"pointer-events-auto absolute inset-0 z-10 flex select-none flex-col\\\",\\n        className,\\n      )}\\n    >\\n      {/* Current weather (more inset than forecast strip) */}\\n      <div className=\\\"px-6 pt-6\\\">\\n        <div className=\\\"flex flex-col items-start\\\">\\n          <h2\\n            className={cn(\\n              \\\"font-medium leading-[1.08] tracking-tight\\\",\\n              textSecondary,\\n            )}\\n            style={{\\n              fontSize: locationFontSize,\\n              fontFamily: forecastFontFamily,\\n              textShadow: shadowStyle,\\n            }}\\n          >\\n            {location}\\n          </h2>\\n\\n          <div className=\\\"-mt-0.5 flex items-start gap-1\\\">\\n            <span\\n              className={cn(\\n                \\\"font-[250] tabular-nums leading-[1.02] tracking-[-0.015em]\\\",\\n                textPrimarySoft,\\n              )}\\n              style={{\\n                fontSize: temperatureFontSize,\\n                fontFamily: forecastFontFamily,\\n                fontFeatureSettings: '\\\"tnum\\\" 1, \\\"case\\\" 1',\\n                textShadow: isDark\\n                  ? \\\"0 2px 20px rgba(0,0,0,0.25)\\\"\\n                  : \\\"0 2px 20px rgba(255,255,255,0.3)\\\",\\n              }}\\n              aria-hidden=\\\"true\\\"\\n            >\\n              {roundedTemperature}\\n            </span>\\n            <span\\n              className={cn(\\\"mt-2 font-[250] tabular-nums\\\", textSecondary)}\\n              style={{\\n                fontSize: degreeFontSize,\\n                fontFamily: forecastFontFamily,\\n                fontFeatureSettings: '\\\"tnum\\\" 1, \\\"case\\\" 1',\\n              }}\\n              aria-hidden=\\\"true\\\"\\n            >\\n              °{unitSymbol}\\n            </span>\\n            <span className=\\\"sr-only\\\">\\n              {roundedTemperature} degrees {spokenUnit}\\n            </span>\\n          </div>\\n\\n          <div\\n            className=\\\"mt-0.5 flex items-center gap-3\\\"\\n            style={{\\n              fontFamily: forecastFontFamily,\\n              fontFeatureSettings: '\\\"tnum\\\" 1, \\\"case\\\" 1',\\n            }}\\n          >\\n            <span\\n              className=\\\"font-medium tabular-nums\\\"\\n              style={{ fontSize: hiLoFontSize }}\\n            >\\n              <span className={textSubtle}>H </span>\\n              <span className={textPrimary}>{Math.round(tempHigh)}°</span>\\n            </span>\\n            <span\\n              className=\\\"font-medium tabular-nums\\\"\\n              style={{ fontSize: hiLoFontSize }}\\n            >\\n              <span className={textSubtle}>L </span>\\n              <span className={textPrimary}>{Math.round(tempLow)}°</span>\\n            </span>\\n          </div>\\n        </div>\\n      </div>\\n\\n      {/* Spacer */}\\n      <div className=\\\"flex-1\\\" />\\n\\n      {/* Forecast strip - hidden at small container sizes (less inset than header) */}\\n      {forecast.length > 0 && (\\n        <div className=\\\"px-3 pb-3\\\">\\n          {/* Show the strip earlier, but progressively reduce content as height shrinks. */}\\n          <div ref={cardRef} className=\\\"weather-forecast-strip relative hidden\\\">\\n            {/* Edge shine - outside overflow-hidden so it aligns with border */}\\n            <div\\n              className=\\\"pointer-events-none absolute inset-0 z-10 rounded-xl transition-opacity duration-300 ease-out\\\"\\n              style={{\\n                opacity: glowState.intensity,\\n                background: sineEasedGradient(\\n                  glowState.x,\\n                  glowState.y,\\n                  100,\\n                  isDark ? 0.6 : 1,\\n                ),\\n                mask: \\\"linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)\\\",\\n                maskComposite: \\\"exclude\\\",\\n                WebkitMaskComposite: \\\"xor\\\",\\n                padding: \\\"0.5px\\\",\\n              }}\\n            />\\n            <div\\n              className=\\\"relative overflow-hidden rounded-xl px-3 py-2.5\\\"\\n              style={{\\n                backgroundColor: `rgba(255, 255, 255, ${bgOpacity})`,\\n                ...resolveGlassBackdropFilterStyles({\\n                  glassStyles,\\n                  blurAmount,\\n                }),\\n              }}\\n            >\\n              {/* Inner glow */}\\n              <div\\n                className=\\\"pointer-events-none absolute inset-0 mix-blend-color-dodge transition-opacity duration-300 ease-out\\\"\\n                style={{\\n                  opacity: glowState.intensity,\\n                  background: sineEasedGradient(\\n                    glowState.x,\\n                    glowState.y,\\n                    120,\\n                    isDark ? 0.06 : 0.15,\\n                  ),\\n                }}\\n              />\\n              <div className=\\\"relative flex items-center justify-between\\\">\\n                {forecast.slice(0, 5).map((day, index) => {\\n                  const DayIcon = conditionIcons[day.conditionCode];\\n                  return (\\n                    <div\\n                      key={`${day.label}-${index}`}\\n                      className=\\\"flex flex-1 flex-col items-center gap-0.5\\\"\\n                      style={{\\n                        fontFamily: forecastFontFamily,\\n                        fontFeatureSettings: '\\\"tnum\\\" 1, \\\"case\\\" 1',\\n                        textShadow: forecastTextShadow,\\n                      }}\\n                    >\\n                      <span\\n                        className={cn(\\n                          \\\"text-[10px] uppercase tracking-[0.08em]\\\",\\n                          index === 0 ? \\\"font-semibold\\\" : \\\"font-medium\\\",\\n                          textPrimary,\\n                        )}\\n                      >\\n                        {day.label}\\n                      </span>\\n                      <DayIcon\\n                        className={cn(\\n                          \\\"my-0.5 size-5\\\",\\n                          textPrimary,\\n                          index === 0 ? \\\"opacity-100\\\" : \\\"opacity-70\\\",\\n                          // At shorter containers (but still showing the strip),\\n                          // omit the icon to preserve legibility.\\n                          \\\"weather-forecast-icon hidden\\\",\\n                        )}\\n                        strokeWidth={1.5}\\n                        aria-hidden=\\\"true\\\"\\n                      />\\n                      <div className=\\\"flex flex-col items-center gap-0.5\\\">\\n                        <span\\n                          className={cn(\\n                            \\\"text-[15px] tabular-nums leading-[1.2] tracking-[-0.01em]\\\",\\n                            index === 0 ? \\\"font-semibold\\\" : \\\"font-medium\\\",\\n                            textPrimary,\\n                          )}\\n                        >\\n                          {Math.round(day.tempMax)}°\\n                        </span>\\n                        <span\\n                          className={cn(\\n                            \\\"font-normal text-[12px] tabular-nums leading-[1.3]\\\",\\n                            textPrimary,\\n                          )}\\n                        >\\n                          {Math.round(day.tempMin)}°\\n                        </span>\\n                      </div>\\n                    </div>\\n                  );\\n                })}\\n              </div>\\n            </div>\\n          </div>\\n        </div>\\n      )}\\n\\n      {/* Height-based container queries (requires container-type:size on the weather container). */}\\n      <style>{`\\n        @container weather (min-height: 245px) {\\n          .weather-forecast-strip {\\n            display: block !important;\\n          }\\n        }\\n        @container weather (min-height: 280px) {\\n          .weather-forecast-icon {\\n            display: block !important;\\n          }\\n        }\\n      `}</style>\\n    </div>\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/weather-widget/weather-widget-container.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/weather-widget/weather-widget-container.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { useEffect, useState } from \\\"react\\\";\\n\\nimport { cn } from \\\"@/lib/utils\\\";\\nimport {\\n  EffectCompositorRuntime,\\n  getNearestCheckpoint,\\n  getSceneBrightnessFromTimeOfDay,\\n  getWeatherTheme,\\n  resolveWeatherTime,\\n  snapTimeOfDayToNearestCheckpoint,\\n  TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES,\\n} from \\\"./generated/weather-runtime-core.generated\\\";\\nimport type { WeatherWidgetRuntimeProps } from \\\"./schema-runtime\\\";\\nimport { WeatherDataOverlay } from \\\"./weather-data-overlay\\\";\\n\\ntype TimeCheckpoint = \\\"dawn\\\" | \\\"noon\\\" | \\\"dusk\\\" | \\\"midnight\\\";\\n\\nexport function WeatherWidget({\\n  version: _version,\\n  id,\\n  location,\\n  units,\\n  current,\\n  forecast,\\n  time,\\n  updatedAt,\\n  className,\\n  effects,\\n}: WeatherWidgetRuntimeProps) {\\n  const [prefersReducedMotion, setPrefersReducedMotion] = useState(() => {\\n    if (typeof window === \\\"undefined\\\") {\\n      return false;\\n    }\\n\\n    return (\\n      window.matchMedia?.(\\\"(prefers-reduced-motion: reduce)\\\")?.matches ?? false\\n    );\\n  });\\n\\n  useEffect(() => {\\n    if (\\n      typeof window === \\\"undefined\\\" ||\\n      typeof window.matchMedia !== \\\"function\\\"\\n    ) {\\n      return;\\n    }\\n\\n    const mediaQueryList = window.matchMedia(\\n      \\\"(prefers-reduced-motion: reduce)\\\",\\n    );\\n    setPrefersReducedMotion(mediaQueryList.matches);\\n\\n    const handleMotionPreferenceChange = (event: MediaQueryListEvent) => {\\n      setPrefersReducedMotion(event.matches);\\n    };\\n\\n    if (typeof mediaQueryList.addEventListener === \\\"function\\\") {\\n      mediaQueryList.addEventListener(\\\"change\\\", handleMotionPreferenceChange);\\n      return () => {\\n        mediaQueryList.removeEventListener(\\n          \\\"change\\\",\\n          handleMotionPreferenceChange,\\n        );\\n      };\\n    }\\n\\n    mediaQueryList.addListener(handleMotionPreferenceChange);\\n    return () => {\\n      mediaQueryList.removeListener(handleMotionPreferenceChange);\\n    };\\n  }, []);\\n\\n  const reducedMotion = effects?.reducedMotion ?? prefersReducedMotion;\\n  const effectsEnabled = effects?.enabled !== false && !reducedMotion;\\n\\n  const resolvedTime = resolveWeatherTime({\\n    time,\\n    updatedAt,\\n  });\\n  const timeOfDay = snapTimeOfDayToNearestCheckpoint(resolvedTime.timeOfDay);\\n  const tunedOverrides =\\n    TUNED_WEATHER_EFFECTS_CHECKPOINT_OVERRIDES[current.conditionCode];\\n  const checkpoint = getNearestCheckpoint(timeOfDay) as TimeCheckpoint;\\n  const checkpointOverrides = tunedOverrides?.[checkpoint];\\n  const glassParams =\\n    checkpointOverrides && \\\"glass\\\" in checkpointOverrides\\n      ? checkpointOverrides.glass\\n      : undefined;\\n  const brightness = getSceneBrightnessFromTimeOfDay(\\n    timeOfDay,\\n    current.conditionCode,\\n  );\\n  const weatherTheme = getWeatherTheme(brightness, undefined);\\n  const isWeatherDark = weatherTheme === \\\"dark\\\";\\n  const backgroundClass = isWeatherDark\\n    ? \\\"bg-gradient-to-b from-zinc-950 via-zinc-900/70 to-zinc-950\\\"\\n    : \\\"bg-gradient-to-b from-sky-50 via-sky-100/70 to-white\\\";\\n\\n  return (\\n    <article\\n      data-slot=\\\"weather-widget\\\"\\n      data-tool-ui-id={id}\\n      className={cn(\\\"isolate w-full max-w-md\\\", className)}\\n    >\\n      <div\\n        data-slot=\\\"card\\\"\\n        className={cn(\\n          \\\"@container/weather relative aspect-[4/3] overflow-clip rounded-2xl border-0 p-0 shadow-none [container-type:size]\\\",\\n          backgroundClass,\\n        )}\\n      >\\n        {effectsEnabled && (\\n          <EffectCompositorRuntime\\n            className=\\\"absolute inset-0\\\"\\n            conditionCode={current.conditionCode}\\n            windSpeed={current.windSpeed}\\n            precipitationLevel={current.precipitationLevel}\\n            visibility={current.visibility}\\n            timestamp={updatedAt}\\n            timeOfDay={timeOfDay}\\n            settings={effects}\\n          />\\n        )}\\n\\n        <WeatherDataOverlay\\n          location={location.name}\\n          conditionCode={current.conditionCode}\\n          temperature={current.temperature}\\n          tempHigh={current.tempMax}\\n          tempLow={current.tempMin}\\n          forecast={forecast}\\n          unit={units.temperature}\\n          theme={weatherTheme}\\n          timeOfDay={timeOfDay}\\n          timestamp={updatedAt}\\n          glassParams={glassParams}\\n          reducedMotion={reducedMotion}\\n        />\\n      </div>\\n    </article>\\n  );\\n}\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/public/r/x-post.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"x-post\",\n  \"type\": \"registry:block\",\n  \"title\": \"X Post\",\n  \"description\": \"Render X (Twitter) post previews.\",\n  \"dependencies\": [\n    \"lucide-react\",\n    \"zod\"\n  ],\n  \"registryDependencies\": [\n    \"button\",\n    \"tooltip\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/tool-ui/shared/contract.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/contract.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { parseWithSchema, safeParseWithSchema } from \\\"./parse\\\";\\n\\nexport interface ToolUiContract<T> {\\n  schema: z.ZodType<T>;\\n  parse: (input: unknown) => T;\\n  safeParse: (input: unknown) => T | null;\\n}\\n\\nexport function defineToolUiContract<T>(\\n  componentName: string,\\n  schema: z.ZodType<T>,\\n): ToolUiContract<T> {\\n  return {\\n    schema,\\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\\n  };\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/aspect-ratio.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/aspect-ratio.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nexport const AspectRatioSchema = z\\n  .enum([\\\"auto\\\", \\\"1:1\\\", \\\"4:3\\\", \\\"16:9\\\", \\\"9:16\\\"])\\n  .default(\\\"auto\\\");\\n\\nexport type AspectRatio = z.infer<typeof AspectRatioSchema>;\\n\\nexport const MediaFitSchema = z.enum([\\\"cover\\\", \\\"contain\\\"]).default(\\\"cover\\\");\\n\\nexport type MediaFit = z.infer<typeof MediaFitSchema>;\\n\\nexport const RATIO_CLASS_MAP: Record<AspectRatio, string> = {\\n  auto: \\\"\\\",\\n  \\\"1:1\\\": \\\"aspect-square\\\",\\n  \\\"4:3\\\": \\\"aspect-[4/3]\\\",\\n  \\\"16:9\\\": \\\"aspect-video\\\",\\n  \\\"9:16\\\": \\\"aspect-[9/16]\\\",\\n};\\n\\nexport function getRatioClass(ratio: AspectRatio): string {\\n  return RATIO_CLASS_MAP[ratio];\\n}\\n\\nexport function getFitClass(fit: MediaFit): string {\\n  return fit === \\\"cover\\\" ? \\\"object-cover\\\" : \\\"object-contain\\\";\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/format-utils.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/format-utils.ts\",\n      \"content\": \"/**\\n * Format duration in milliseconds to human-readable string.\\n * @example formatDuration(128000) => \\\"2:08\\\"\\n * @example formatDuration(3661000) => \\\"1:01:01\\\"\\n */\\nexport function formatDuration(durationMs: number): string {\\n  const totalSeconds = Math.round(durationMs / 1000);\\n  const hours = Math.floor(totalSeconds / 3600);\\n  const minutes = Math.floor((totalSeconds % 3600) / 60);\\n  const seconds = totalSeconds % 60;\\n\\n  if (hours > 0) {\\n    return `${hours}:${minutes.toString().padStart(2, \\\"0\\\")}:${seconds\\n      .toString()\\n      .padStart(2, \\\"0\\\")}`;\\n  }\\n  return `${minutes}:${seconds.toString().padStart(2, \\\"0\\\")}`;\\n}\\n\\n/**\\n * Format file size in bytes to human-readable string.\\n * @example formatFileSize(1024) => \\\"1 KB\\\"\\n * @example formatFileSize(1536000) => \\\"1.5 MB\\\"\\n */\\nexport function formatFileSize(bytes: number): string {\\n  if (bytes < 1024) return `${bytes} B`;\\n  const units = [\\\"KB\\\", \\\"MB\\\", \\\"GB\\\"];\\n  let size = bytes / 1024;\\n  let unit = 0;\\n  while (size >= 1024 && unit < units.length - 1) {\\n    size /= 1024;\\n    unit += 1;\\n  }\\n  return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unit]}`;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/index.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/index.ts\",\n      \"content\": \"export {\\n  AspectRatioSchema,\\n  MediaFitSchema,\\n  RATIO_CLASS_MAP,\\n  getRatioClass,\\n  getFitClass,\\n  type AspectRatio,\\n  type MediaFit,\\n} from \\\"./aspect-ratio\\\";\\n\\nexport { OVERLAY_GRADIENT } from \\\"./overlay-gradient\\\";\\n\\nexport { formatDuration, formatFileSize } from \\\"./format-utils\\\";\\n\\nexport { sanitizeHref } from \\\"./sanitize-href\\\";\\nexport {\\n  resolveSafeNavigationHref,\\n  openSafeNavigationHref,\\n} from \\\"./safe-navigation\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/overlay-gradient.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/overlay-gradient.ts\",\n      \"content\": \"/**\\n * Eased gradient for hover overlays on media elements.\\n * Creates a smooth fade from opaque black at top to transparent.\\n *\\n * @see https://larsenwork.com/easing-gradients/\\n */\\nexport const OVERLAY_GRADIENT = `linear-gradient(\\n  to bottom,\\n  hsl(0, 0%, 0%) 0%,\\n  hsla(0, 0%, 0%, 0.987) 8.3%,\\n  hsla(0, 0%, 0%, 0.951) 16.6%,\\n  hsla(0, 0%, 0%, 0.896) 24.6%,\\n  hsla(0, 0%, 0%, 0.825) 32.5%,\\n  hsla(0, 0%, 0%, 0.741) 40.1%,\\n  hsla(0, 0%, 0%, 0.648) 47.6%,\\n  hsla(0, 0%, 0%, 0.55) 54.8%,\\n  hsla(0, 0%, 0%, 0.45) 61.7%,\\n  hsla(0, 0%, 0%, 0.352) 68.3%,\\n  hsla(0, 0%, 0%, 0.259) 74.5%,\\n  hsla(0, 0%, 0%, 0.175) 80.4%,\\n  hsla(0, 0%, 0%, 0.104) 86%,\\n  hsla(0, 0%, 0%, 0.049) 91.1%,\\n  hsla(0, 0%, 0%, 0.013) 95.8%,\\n  hsla(0, 0%, 0%, 0) 100%\\n)` as const;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/safe-navigation.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/safe-navigation.ts\",\n      \"content\": \"import { sanitizeHref } from \\\"./sanitize-href\\\";\\n\\nexport function resolveSafeNavigationHref(\\n  ...candidates: Array<string | null | undefined>\\n): string | undefined {\\n  for (const candidate of candidates) {\\n    const safeHref = sanitizeHref(candidate ?? undefined);\\n    if (safeHref) {\\n      return safeHref;\\n    }\\n  }\\n\\n  return undefined;\\n}\\n\\nexport function openSafeNavigationHref(href: string | undefined): boolean {\\n  if (!href || typeof window === \\\"undefined\\\") {\\n    return false;\\n  }\\n\\n  window.open(href, \\\"_blank\\\", \\\"noopener,noreferrer\\\");\\n  return true;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/media/sanitize-href.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/media/sanitize-href.ts\",\n      \"content\": \"/**\\n * Sanitize a URL to ensure it's safe for use in href attributes.\\n * Allows:\\n * - Absolute http(s) URLs\\n * - Relative URLs (/path, ./path, ../path, ?query, #hash)\\n *\\n * @returns The sanitized URL string, or undefined if invalid/unsafe\\n */\\nexport function sanitizeHref(href?: string): string | undefined {\\n  if (!href) return undefined;\\n  const candidate = href.trim();\\n  if (!candidate) return undefined;\\n\\n  if (\\n    candidate.startsWith(\\\"/\\\") ||\\n    candidate.startsWith(\\\"./\\\") ||\\n    candidate.startsWith(\\\"../\\\") ||\\n    candidate.startsWith(\\\"?\\\") ||\\n    candidate.startsWith(\\\"#\\\")\\n  ) {\\n    if (candidate.startsWith(\\\"//\\\")) return undefined;\\n    // eslint-disable-next-line no-control-regex -- intentionally matching control characters\\n    if (/[\\\\u0000-\\\\u001F\\\\u007F]/.test(candidate)) return undefined;\\n    return candidate;\\n  }\\n\\n  try {\\n    const url = new URL(candidate);\\n    if (url.protocol === \\\"http:\\\" || url.protocol === \\\"https:\\\") {\\n      return url.toString();\\n    }\\n  } catch {\\n    return undefined;\\n  }\\n  return undefined;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/parse.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/parse.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\n\\nfunction formatZodPath(path: Array<string | number | symbol>): string {\\n  if (path.length === 0) return \\\"root\\\";\\n  return path\\n    .map((segment) =>\\n      typeof segment === \\\"number\\\" ? `[${segment}]` : String(segment),\\n    )\\n    .join(\\\".\\\");\\n}\\n\\n/**\\n * Format Zod errors into a compact `path: message` string.\\n */\\nexport function formatZodError(error: z.ZodError): string {\\n  const parts = error.issues.map((issue) => {\\n    const path = formatZodPath(issue.path);\\n    return `${path}: ${issue.message}`;\\n  });\\n\\n  return Array.from(new Set(parts)).join(\\\"; \\\");\\n}\\n\\n/**\\n * Parse unknown input and throw a readable error.\\n */\\nexport function parseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n  name: string,\\n): T {\\n  const res = schema.safeParse(input);\\n  if (!res.success) {\\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\\n  }\\n  return res.data;\\n}\\n\\n/**\\n * Parse unknown input, returning `null` instead of throwing on failure.\\n *\\n * Use this in assistant-ui `render` functions where `args` stream in\\n * incrementally and may be incomplete until the tool call finishes.\\n */\\nexport function safeParseWithSchema<T>(\\n  schema: z.ZodType<T>,\\n  input: unknown,\\n): T | null {\\n  const res = schema.safeParse(input);\\n  return res.success ? res.data : null;\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/shared/utils.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/shared/utils.ts\",\n      \"content\": \"export function formatRelativeTime(iso: string): string {\\n  const seconds = Math.round((Date.now() - new Date(iso).getTime()) / 1000);\\n  if (seconds < 60) return `${seconds}s`;\\n  if (seconds < 3600) return `${Math.round(seconds / 60)}m`;\\n  if (seconds < 86400) return `${Math.round(seconds / 3600)}h`;\\n  if (seconds < 604800) return `${Math.round(seconds / 86400)}d`;\\n  return `${Math.round(seconds / 604800)}w`;\\n}\\n\\nexport function formatCount(count: number): string {\\n  if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;\\n  if (count >= 1_000) return `${(count / 1_000).toFixed(1)}K`;\\n  return String(count);\\n}\\n\\nexport function getDomain(url: string): string {\\n  try {\\n    return new URL(url).hostname.replace(/^www\\\\./, \\\"\\\");\\n  } catch {\\n    return \\\"\\\";\\n  }\\n}\\n\\nexport function prefersReducedMotion(): boolean {\\n  return (\\n    typeof window !== \\\"undefined\\\" &&\\n    window.matchMedia?.(\\\"(prefers-reduced-motion: reduce)\\\").matches\\n  );\\n}\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/x-post/_adapter.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/x-post/_adapter.tsx\",\n      \"content\": \"/**\\n * Adapter: UI and utility re-exports for copy-standalone portability.\\n *\\n * When copying this component to another project, update these imports\\n * to match your project's paths:\\n *\\n *   cn      → Your Tailwind merge utility (e.g., \\\"@/lib/utils\\\", \\\"~/lib/cn\\\")\\n *   Button  → shadcn/ui Button\\n *   Tooltip → shadcn/ui Tooltip\\n */\\n\\nexport { cn } from \\\"@/lib/utils\\\";\\nexport { Button } from \\\"@/components/ui/button\\\";\\nexport {\\n  Tooltip,\\n  TooltipContent,\\n  TooltipProvider,\\n  TooltipTrigger,\\n} from \\\"@/components/ui/tooltip\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/x-post/index.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/x-post/index.ts\",\n      \"content\": \"export { XPost } from \\\"./x-post\\\";\\nexport type { XPostProps } from \\\"./x-post\\\";\\nexport type {\\n  XPostData,\\n  XPostAuthor,\\n  XPostMedia,\\n  XPostLinkPreview,\\n  XPostStats,\\n} from \\\"./schema\\\";\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/x-post/README.md\",\n      \"type\": \"registry:file\",\n      \"target\": \"components/tool-ui/x-post/README.md\",\n      \"content\": \"# X Post\\n\\nImplementation for the \\\"x-post\\\" Tool UI surface.\\n\\n## Files\\n\\n- public exports: components/tool-ui/x-post/index.ts\\n- serializable schema + parse helpers: components/tool-ui/x-post/schema.ts\\n\\n## Companion assets\\n\\n- Docs page: app/docs/social-post/content.mdx\\n- Preset payload: lib/presets/x-post.ts\\n\\n## Quick check\\n\\nRun this after edits:\\n\\npnpm test\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/x-post/schema.ts\",\n      \"type\": \"registry:lib\",\n      \"target\": \"components/tool-ui/x-post/schema.ts\",\n      \"content\": \"import { z } from \\\"zod\\\";\\nimport { defineToolUiContract } from \\\"../shared/contract\\\";\\n\\nexport const XPostAuthorSchema = z.object({\\n  name: z.string(),\\n  handle: z.string(),\\n  avatarUrl: z.url(),\\n  verified: z.boolean().optional(),\\n});\\n\\nexport const XPostMediaSchema = z.object({\\n  type: z.enum([\\\"image\\\", \\\"video\\\"]),\\n  url: z.url(),\\n  alt: z.string(),\\n  aspectRatio: z.enum([\\\"1:1\\\", \\\"4:3\\\", \\\"16:9\\\", \\\"9:16\\\"]).optional(),\\n});\\n\\nexport const XPostLinkPreviewSchema = z.object({\\n  url: z.url(),\\n  title: z.string().optional(),\\n  description: z.string().optional(),\\n  imageUrl: z.url().optional(),\\n  domain: z.string().optional(),\\n});\\n\\nexport const XPostStatsSchema = z.object({\\n  likes: z.number().optional(),\\n  isLiked: z.boolean().optional(),\\n  isReposted: z.boolean().optional(),\\n  isBookmarked: z.boolean().optional(),\\n});\\n\\nexport interface XPostData {\\n  id: string;\\n  author: z.infer<typeof XPostAuthorSchema>;\\n  text?: string;\\n  media?: z.infer<typeof XPostMediaSchema>;\\n  linkPreview?: z.infer<typeof XPostLinkPreviewSchema>;\\n  quotedPost?: XPostData;\\n  stats?: z.infer<typeof XPostStatsSchema>;\\n  createdAt?: string;\\n}\\n\\nexport const SerializableXPostSchema: z.ZodType<XPostData> = z.object({\\n  id: z.string(),\\n  author: XPostAuthorSchema,\\n  text: z.string().optional(),\\n  media: XPostMediaSchema.optional(),\\n  linkPreview: XPostLinkPreviewSchema.optional(),\\n  quotedPost: z.lazy(() => SerializableXPostSchema).optional(),\\n  stats: XPostStatsSchema.optional(),\\n  createdAt: z.string().optional(),\\n});\\nexport type XPostAuthor = z.infer<typeof XPostAuthorSchema>;\\nexport type XPostMedia = z.infer<typeof XPostMediaSchema>;\\nexport type XPostLinkPreview = z.infer<typeof XPostLinkPreviewSchema>;\\nexport type XPostStats = z.infer<typeof XPostStatsSchema>;\\n\\nconst SerializableXPostSchemaContract = defineToolUiContract(\\n  \\\"XPost\\\",\\n  SerializableXPostSchema,\\n);\\n\\nexport const parseSerializableXPost: (input: unknown) => XPostData =\\n  SerializableXPostSchemaContract.parse;\\n\\nexport const safeParseSerializableXPost: (input: unknown) => XPostData | null =\\n  SerializableXPostSchemaContract.safeParse;\\n\"\n    },\n    {\n      \"path\": \"components/tool-ui/x-post/x-post.tsx\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/tool-ui/x-post/x-post.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport * as React from \\\"react\\\";\\nimport { Heart, Share } from \\\"lucide-react\\\";\\nimport {\\n  cn,\\n  Button,\\n  Tooltip,\\n  TooltipContent,\\n  TooltipProvider,\\n  TooltipTrigger,\\n} from \\\"./_adapter\\\";\\nimport { formatCount, formatRelativeTime, getDomain } from \\\"../shared/utils\\\";\\n\\nimport { resolveSafeNavigationHref } from \\\"../shared/media\\\";\\nimport type { XPostData, XPostMedia, XPostLinkPreview } from \\\"./schema\\\";\\n\\nexport interface XPostProps {\\n  post: XPostData;\\n  className?: string;\\n  onAction?: (action: string, post: XPostData) => void;\\n}\\n\\nfunction Avatar({ src, alt }: { src: string; alt: string }) {\\n  return (\\n    <img\\n      src={src}\\n      alt={alt}\\n      width={40}\\n      height={40}\\n      className=\\\"size-10 shrink-0 rounded-full object-cover\\\"\\n    />\\n  );\\n}\\n\\nfunction XLogo({ className }: { className?: string }) {\\n  return (\\n    <svg\\n      viewBox=\\\"0 0 300 271\\\"\\n      className={className}\\n      role=\\\"img\\\"\\n      aria-label=\\\"X (formerly Twitter) logo\\\"\\n    >\\n      <path\\n        fill=\\\"currentColor\\\"\\n        d=\\\"m236 0h46l-101 115 118 156h-92.6l-72.5-94.8-83 94.8h-46l107-123-113-148h94.9l65.5 86.6zm-16.1 244h25.5l-165-218h-27.4z\\\"\\n      />\\n    </svg>\\n  );\\n}\\n\\nfunction VerifiedBadge({ className }: { className?: string }) {\\n  return (\\n    <svg\\n      viewBox=\\\"0 0 24 24\\\"\\n      className={className}\\n      role=\\\"img\\\"\\n      aria-label=\\\"Verified account\\\"\\n    >\\n      <path\\n        fill=\\\"currentColor\\\"\\n        d=\\\"M22.5 12.5c0-1.58-.875-2.95-2.148-3.6.154-.435.238-.905.238-1.4 0-2.21-1.71-3.998-3.818-3.998-.47 0-.92.084-1.336.25C14.818 2.415 13.51 1.5 12 1.5s-2.816.917-3.437 2.25c-.415-.165-.866-.25-1.336-.25-2.11 0-3.818 1.79-3.818 4 0 .495.083.965.238 1.4-1.272.65-2.147 2.018-2.147 3.6 0 1.495.782 2.798 1.942 3.486-.02.17-.032.34-.032.514 0 2.21 1.708 4 3.818 4 .47 0 .92-.086 1.335-.25.62 1.334 1.926 2.25 3.437 2.25 1.512 0 2.818-.916 3.437-2.25.415.163.865.248 1.336.248 2.11 0 3.818-1.79 3.818-4 0-.174-.012-.344-.033-.513 1.158-.687 1.943-1.99 1.943-3.484zm-6.616-3.334l-4.334 6.5c-.145.217-.382.334-.625.334-.143 0-.288-.04-.416-.126l-.115-.094-2.415-2.415c-.293-.293-.293-.768 0-1.06s.768-.294 1.06 0l1.77 1.767 3.825-5.74c.23-.345.696-.436 1.04-.207.346.23.44.696.21 1.04z\\\"\\n      />\\n    </svg>\\n  );\\n}\\n\\nfunction AuthorInfo({\\n  name,\\n  handle,\\n  verified,\\n  createdAt,\\n}: {\\n  name: string;\\n  handle: string;\\n  verified?: boolean;\\n  createdAt?: string;\\n}) {\\n  return (\\n    <div className=\\\"flex min-w-0 items-center gap-1\\\">\\n      <span className=\\\"truncate font-semibold\\\">{name}</span>\\n      {verified && (\\n        <VerifiedBadge className=\\\"size-[18px] shrink-0 text-blue-500\\\" />\\n      )}\\n      <span className=\\\"text-muted-foreground truncate\\\">@{handle}</span>\\n      {createdAt && (\\n        <>\\n          <span className=\\\"text-muted-foreground\\\">·</span>\\n          <span className=\\\"text-muted-foreground\\\">\\n            {formatRelativeTime(createdAt)}\\n          </span>\\n        </>\\n      )}\\n    </div>\\n  );\\n}\\n\\nfunction PostBody({ text }: { text?: string }) {\\n  if (!text) return null;\\n  return (\\n    <p className=\\\"text-[15px] leading-normal text-pretty wrap-break-word whitespace-pre-wrap\\\">\\n      {text}\\n    </p>\\n  );\\n}\\n\\nfunction PostMedia({\\n  media,\\n  onOpen,\\n}: {\\n  media: XPostMedia;\\n  onOpen?: () => void;\\n}) {\\n  const aspectRatio =\\n    media.aspectRatio === \\\"1:1\\\"\\n      ? \\\"1\\\"\\n      : media.aspectRatio === \\\"4:3\\\"\\n        ? \\\"4/3\\\"\\n        : \\\"16/9\\\";\\n\\n  return (\\n    <button\\n      type=\\\"button\\\"\\n      className=\\\"bg-muted mt-2 w-full overflow-hidden rounded-xl\\\"\\n      style={{ aspectRatio }}\\n      onClick={() => onOpen?.()}\\n    >\\n      {media.type === \\\"image\\\" ? (\\n        <img\\n          src={media.url}\\n          alt={media.alt}\\n          className=\\\"size-full object-cover\\\"\\n          loading=\\\"lazy\\\"\\n        />\\n      ) : (\\n        <video\\n          src={media.url}\\n          controls\\n          playsInline\\n          className=\\\"size-full object-contain\\\"\\n        />\\n      )}\\n    </button>\\n  );\\n}\\n\\nfunction PostLinkPreview({ preview }: { preview: XPostLinkPreview }) {\\n  const href = resolveSafeNavigationHref(preview.url);\\n  const domain = preview.domain ?? getDomain(preview.url);\\n  const content = (\\n    <>\\n      {preview.imageUrl && (\\n        <img\\n          src={preview.imageUrl}\\n          alt=\\\"\\\"\\n          className=\\\"h-48 w-full object-cover\\\"\\n          loading=\\\"lazy\\\"\\n        />\\n      )}\\n      <div className=\\\"p-3\\\">\\n        {domain && (\\n          <div className=\\\"text-muted-foreground text-xs\\\">{domain}</div>\\n        )}\\n        {preview.title && (\\n          <div className=\\\"font-medium text-pretty\\\">{preview.title}</div>\\n        )}\\n        {preview.description && (\\n          <div className=\\\"text-muted-foreground line-clamp-2 text-sm text-pretty\\\">\\n            {preview.description}\\n          </div>\\n        )}\\n      </div>\\n    </>\\n  );\\n\\n  if (!href) {\\n    return (\\n      <div className=\\\"mt-2 block overflow-hidden rounded-xl border\\\">\\n        {content}\\n      </div>\\n    );\\n  }\\n\\n  return (\\n    <a\\n      href={href}\\n      target=\\\"_blank\\\"\\n      rel=\\\"noopener noreferrer\\\"\\n      className=\\\"hover:bg-muted/50 mt-2 block overflow-hidden rounded-xl border transition-colors\\\"\\n    >\\n      {content}\\n    </a>\\n  );\\n}\\n\\nfunction QuotedPostCard({ post }: { post: XPostData }) {\\n  return (\\n    <div className=\\\"hover:bg-muted/30 mt-2 rounded-xl border p-3 transition-colors\\\">\\n      <div className=\\\"flex min-w-0 items-center gap-1\\\">\\n        <img\\n          src={post.author.avatarUrl}\\n          alt={`${post.author.name} avatar`}\\n          width={16}\\n          height={16}\\n          className=\\\"size-4 rounded-full object-cover\\\"\\n        />\\n        <span className=\\\"truncate font-semibold\\\">{post.author.name}</span>\\n        {post.author.verified && (\\n          <VerifiedBadge className=\\\"size-3.5 shrink-0 text-blue-500\\\" />\\n        )}\\n        <span className=\\\"text-muted-foreground truncate\\\">\\n          @{post.author.handle}\\n        </span>\\n        {post.createdAt && (\\n          <>\\n            <span className=\\\"text-muted-foreground shrink-0\\\">·</span>\\n            <span className=\\\"text-muted-foreground shrink-0\\\">\\n              {formatRelativeTime(post.createdAt)}\\n            </span>\\n          </>\\n        )}\\n      </div>\\n      {post.text && <p className=\\\"mt-1.5\\\">{post.text}</p>}\\n      {post.media && (\\n        <img\\n          src={post.media.url}\\n          alt={post.media.alt}\\n          className=\\\"mt-2 rounded-lg\\\"\\n        />\\n      )}\\n    </div>\\n  );\\n}\\n\\nfunction ActionButton({\\n  icon: Icon,\\n  label,\\n  count,\\n  active,\\n  hoverColor,\\n  activeColor,\\n  onClick,\\n}: {\\n  icon: React.ComponentType<{ className?: string }>;\\n  label: string;\\n  count?: number;\\n  active?: boolean;\\n  hoverColor: string;\\n  activeColor?: string;\\n  onClick: () => void;\\n}) {\\n  return (\\n    <Tooltip>\\n      <TooltipTrigger asChild>\\n        <Button\\n          variant=\\\"ghost\\\"\\n          size=\\\"sm\\\"\\n          onClick={(e) => {\\n            e.stopPropagation();\\n            onClick();\\n          }}\\n          className={cn(\\n            \\\"h-auto gap-1.5 px-2 py-1\\\",\\n            hoverColor,\\n            active && activeColor,\\n          )}\\n          aria-label={label}\\n        >\\n          <Icon className=\\\"size-4\\\" />\\n          {count !== undefined && (\\n            <span className=\\\"text-sm\\\">{formatCount(count)}</span>\\n          )}\\n        </Button>\\n      </TooltipTrigger>\\n      <TooltipContent>{label}</TooltipContent>\\n    </Tooltip>\\n  );\\n}\\n\\nfunction PostActions({\\n  stats,\\n  onAction,\\n}: {\\n  stats?: XPostData[\\\"stats\\\"];\\n  onAction: (action: string) => void;\\n}) {\\n  return (\\n    <TooltipProvider delayDuration={300}>\\n      <div className=\\\"mt-3 flex items-center gap-4\\\">\\n        <ActionButton\\n          icon={Heart}\\n          label=\\\"Like\\\"\\n          count={stats?.likes}\\n          active={stats?.isLiked}\\n          hoverColor=\\\"hover:text-pink-500 hover:bg-pink-500/10\\\"\\n          activeColor=\\\"text-pink-500 fill-pink-500\\\"\\n          onClick={() => onAction(\\\"like\\\")}\\n        />\\n        <ActionButton\\n          icon={Share}\\n          label=\\\"Share\\\"\\n          hoverColor=\\\"hover:text-blue-500 hover:bg-blue-500/10\\\"\\n          onClick={() => onAction(\\\"share\\\")}\\n        />\\n      </div>\\n    </TooltipProvider>\\n  );\\n}\\n\\nexport function XPost({ post, className, onAction }: XPostProps) {\\n  return (\\n    <div\\n      className={cn(\\\"flex max-w-xl flex-col gap-3\\\", className)}\\n      data-tool-ui-id={post.id}\\n      data-slot=\\\"x-post\\\"\\n    >\\n      <article className=\\\"bg-card rounded-xl border p-3 shadow-sm\\\">\\n        <div className=\\\"flex gap-3\\\">\\n          <Avatar\\n            src={post.author.avatarUrl}\\n            alt={`${post.author.name} avatar`}\\n          />\\n          <div className=\\\"min-w-0 flex-1\\\">\\n            <div className=\\\"flex items-start justify-between gap-2\\\">\\n              <AuthorInfo\\n                name={post.author.name}\\n                handle={post.author.handle}\\n                verified={post.author.verified}\\n                createdAt={post.createdAt}\\n              />\\n              <XLogo className=\\\"text-muted-foreground/40 size-4\\\" />\\n            </div>\\n            <PostBody text={post.text} />\\n            {post.media && <PostMedia media={post.media} />}\\n            {post.quotedPost && <QuotedPostCard post={post.quotedPost} />}\\n            {post.linkPreview && !post.quotedPost && (\\n              <PostLinkPreview preview={post.linkPreview} />\\n            )}\\n            <PostActions\\n              stats={post.stats}\\n              onAction={(action) => onAction?.(action, post)}\\n            />\\n          </div>\\n        </div>\\n      </article>\\n    </div>\\n  );\\n}\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/www/scripts/build-tool-ui-registry.ts",
    "content": "import path from \"path\";\nimport { fileURLToPath } from \"url\";\nimport { writeToolUiRegistryArtifacts } from \"../lib/registry/tool-ui-registry\";\n\nasync function main(): Promise<void> {\n  const scriptDir = path.dirname(fileURLToPath(import.meta.url));\n  const projectRoot = path.resolve(scriptDir, \"..\");\n  const artifacts = await writeToolUiRegistryArtifacts(projectRoot);\n\n  console.log(\n    `Built registry with ${artifacts.items.length} items at public/r/*.json`,\n  );\n}\n\nmain().catch((error: unknown) => {\n  console.error(\"Failed to build Tool UI shadcn registry artifacts.\");\n  console.error(error);\n  process.exitCode = 1;\n});\n"
  },
  {
    "path": "apps/www/scripts/check-changelog.ts",
    "content": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { validateChangelogStructure } from \"../lib/changelog/changelog\";\n\nfunction main(): void {\n  const scriptDir = path.dirname(fileURLToPath(import.meta.url));\n  const projectRoot = path.resolve(scriptDir, \"..\");\n  const changelogPath = path.join(\n    projectRoot,\n    \"app/docs/changelog/content.mdx\",\n  );\n\n  if (!fs.existsSync(changelogPath)) {\n    throw new Error(\n      'Missing changelog file at \"app/docs/changelog/content.mdx\". Run \"pnpm changelog:generate\" first.',\n    );\n  }\n\n  const content = fs.readFileSync(changelogPath, \"utf8\");\n  const validation = validateChangelogStructure(content);\n\n  if (!validation.ok) {\n    throw new Error(\n      [\n        \"Changelog structure validation failed:\",\n        ...validation.errors.map((error) => `- ${error}`),\n      ].join(\"\\n\"),\n    );\n  }\n\n  console.log(\"Changelog structure check passed.\");\n}\n\ntry {\n  main();\n} catch (error) {\n  console.error(error);\n  process.exitCode = 1;\n}\n"
  },
  {
    "path": "apps/www/scripts/compile-weather-runtime.ts",
    "content": "import { watch } from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport {\n  getStaleWeatherRuntimeArtifacts,\n  WEATHER_AUTHORING_WATCH_DIRS,\n  writeWeatherRuntimeArtifacts,\n} from \"../lib/weather-codegen/compile-weather-runtime\";\n\ninterface ScriptOptions {\n  check: boolean;\n  watchMode: boolean;\n}\n\nfunction parseOptions(args: string[]): ScriptOptions {\n  return {\n    check: args.includes(\"--check\"),\n    watchMode: args.includes(\"--watch\"),\n  };\n}\n\nfunction resolveProjectRoot(): string {\n  const scriptDir = path.dirname(fileURLToPath(import.meta.url));\n  return path.resolve(scriptDir, \"..\");\n}\n\nasync function runCompile(projectRoot: string): Promise<void> {\n  const { written, unchanged } =\n    await writeWeatherRuntimeArtifacts(projectRoot);\n\n  if (written.length === 0) {\n    console.log(\"Weather artifacts are already up to date.\");\n    return;\n  }\n\n  console.log(\"Updated weather runtime artifacts:\");\n  for (const file of written) {\n    console.log(`- ${file}`);\n  }\n\n  if (unchanged.length > 0) {\n    console.log(`Unchanged artifacts: ${unchanged.length}`);\n  }\n}\n\nasync function runCheck(projectRoot: string): Promise<void> {\n  const stale = await getStaleWeatherRuntimeArtifacts(projectRoot);\n\n  if (stale.length === 0) {\n    console.log(\"Weather runtime artifacts are up to date.\");\n    return;\n  }\n\n  console.error(\"Weather runtime artifacts are stale:\");\n  for (const file of stale) {\n    console.error(`- ${file}`);\n  }\n  console.error(\"\\nFix with: pnpm weather:compile\");\n  process.exitCode = 1;\n}\n\nfunction watchAuthoringSources(projectRoot: string): void {\n  console.log(\"Watching weather authoring sources for changes...\");\n\n  let queued = false;\n  let timer: NodeJS.Timeout | null = null;\n\n  const rebuild = () => {\n    if (queued) {\n      return;\n    }\n\n    queued = true;\n    if (timer) {\n      clearTimeout(timer);\n    }\n\n    timer = setTimeout(async () => {\n      queued = false;\n      try {\n        await runCompile(projectRoot);\n      } catch (error) {\n        console.error(\"Weather runtime compile failed:\");\n        console.error(error);\n      }\n    }, 100);\n  };\n\n  for (const dir of WEATHER_AUTHORING_WATCH_DIRS) {\n    const absoluteDir = path.join(projectRoot, dir);\n\n    watch(absoluteDir, { persistent: true }, (_eventType, fileName) => {\n      const changed =\n        typeof fileName === \"string\" ? fileName : \"(unknown file)\";\n      console.log(`Detected change in ${dir}/${changed}`);\n      rebuild();\n    });\n  }\n}\n\nasync function main(): Promise<void> {\n  const options = parseOptions(process.argv.slice(2));\n  const projectRoot = resolveProjectRoot();\n\n  if (options.check && options.watchMode) {\n    throw new Error(\"Use either --check or --watch, not both.\");\n  }\n\n  if (options.check) {\n    await runCheck(projectRoot);\n    return;\n  }\n\n  await runCompile(projectRoot);\n\n  if (options.watchMode) {\n    watchAuthoringSources(projectRoot);\n  }\n}\n\nvoid main().catch((error) => {\n  console.error(error);\n  process.exitCode = 1;\n});\n"
  },
  {
    "path": "apps/www/scripts/generate-changelog.ts",
    "content": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { execFileSync } from \"node:child_process\";\nimport { fileURLToPath } from \"node:url\";\nimport {\n  ensureChangelogFileExists,\n  readLatestReleaseDate,\n  readLatestReleaseGeneratedToRef,\n  renderReleaseSection,\n  upsertReleaseSection,\n  validateChangelogStructure,\n} from \"../lib/changelog/changelog\";\nimport {\n  collectReleaseGitContext,\n  formatCommitSummary,\n} from \"../lib/changelog/git\";\nimport { inferReleaseNotes } from \"../lib/changelog/inference\";\n\nfunction resolveArgValue(argv: string[], prefix: string): string | undefined {\n  const arg = argv.find((value) => value.startsWith(prefix));\n  if (!arg) {\n    return undefined;\n  }\n\n  const value = arg.slice(prefix.length).trim();\n  return value.length > 0 ? value : undefined;\n}\n\nfunction resolveReleaseDate(argv: string[]): string {\n  const date = resolveArgValue(argv, \"--date=\") ?? process.env.CHANGELOG_DATE;\n  if (!date) {\n    return new Date().toISOString().slice(0, 10);\n  }\n\n  if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(date)) {\n    throw new Error(\n      `Invalid date \"${date}\". Use YYYY-MM-DD (example: --date=2026-02-12).`,\n    );\n  }\n\n  return date;\n}\n\nfunction summarizeChangelogStyle(content: string): string {\n  const lines = content\n    .split(\"\\n\")\n    .map((line) => line.trimEnd())\n    .filter((line) => !line.startsWith(\"import \"));\n  return lines.slice(0, 120).join(\"\\n\");\n}\n\nfunction resolveStableCommitHash(projectRoot: string, ref: string): string {\n  try {\n    return execFileSync(\n      \"git\",\n      [\"-C\", projectRoot, \"rev-parse\", \"--verify\", `${ref}^{commit}`],\n      {\n        encoding: \"utf8\",\n        stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      },\n    ).trim();\n  } catch {\n    throw new Error(\n      `Invalid git ref \"${ref}\". Provide a valid commit-ish for --to (example: --to=HEAD).`,\n    );\n  }\n}\n\nfunction resolveLatestEntryCommitRef(\n  projectRoot: string,\n  changelogRelativePath: string,\n  entryDate: string,\n): string | null {\n  try {\n    const entryCommit = execFileSync(\n      \"git\",\n      [\n        \"-C\",\n        projectRoot,\n        \"log\",\n        \"-n\",\n        \"1\",\n        \"--pretty=format:%H\",\n        \"-G\",\n        `^[-+]?##[[:space:]]+${entryDate}$`,\n        \"--\",\n        changelogRelativePath,\n      ],\n      {\n        encoding: \"utf8\",\n        stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      },\n    ).trim();\n    return entryCommit.length > 0 ? entryCommit : null;\n  } catch {\n    return null;\n  }\n}\n\nasync function main(): Promise<void> {\n  const scriptDir = path.dirname(fileURLToPath(import.meta.url));\n  const projectRoot = path.resolve(scriptDir, \"..\");\n  const changelogRelativePath = \"app/docs/changelog/content.mdx\";\n  const changelogPath = path.join(projectRoot, changelogRelativePath);\n  const argv = process.argv.slice(2);\n  const releaseDate = resolveReleaseDate(argv);\n  const explicitFromRef = resolveArgValue(argv, \"--from=\");\n  const requestedToRef = resolveArgValue(argv, \"--to=\") ?? \"HEAD\";\n  const forceTagBaseline = argv.includes(\"--from-tag\");\n  const fromChangelog = !forceTagBaseline;\n\n  ensureChangelogFileExists(changelogPath);\n\n  const currentContent = fs.readFileSync(changelogPath, \"utf8\");\n  const resolvedToCommitHash = resolveStableCommitHash(\n    projectRoot,\n    requestedToRef,\n  );\n  const markerFromRef = fromChangelog\n    ? readLatestReleaseGeneratedToRef(currentContent)\n    : null;\n  const latestReleaseDate = fromChangelog\n    ? readLatestReleaseDate(currentContent)\n    : null;\n  const latestEntryCommitRef =\n    fromChangelog && latestReleaseDate\n      ? resolveLatestEntryCommitRef(\n          projectRoot,\n          changelogRelativePath,\n          latestReleaseDate,\n        )\n      : null;\n  const fromRef =\n    explicitFromRef ?? markerFromRef ?? latestEntryCommitRef ?? undefined;\n  const fromDate =\n    fromChangelog && !fromRef && latestReleaseDate\n      ? latestReleaseDate\n      : undefined;\n  const fromChangelogPath =\n    fromChangelog && !fromRef && !fromDate ? changelogRelativePath : undefined;\n  const baselineSource = explicitFromRef\n    ? \"explicit --from\"\n    : markerFromRef\n      ? \"latest changelog marker\"\n      : latestEntryCommitRef\n        ? \"latest changelog entry commit\"\n        : fromDate\n          ? \"latest changelog release date\"\n          : fromChangelogPath\n            ? \"last changelog file commit\"\n            : \"latest git tag\";\n  const gitContext = collectReleaseGitContext(projectRoot, {\n    fromRef,\n    fromDate,\n    toRef: resolvedToCommitHash,\n    fromChangelogPath,\n  });\n  const commitSummary = formatCommitSummary(gitContext);\n\n  const notes = await inferReleaseNotes({\n    releaseDate,\n    commitSummary,\n    changedFiles: gitContext.changedFiles,\n    changelogTemplateContext: summarizeChangelogStyle(currentContent),\n  });\n\n  const section = renderReleaseSection({\n    date: releaseDate,\n    notes,\n    generatedToRef: resolvedToCommitHash,\n  });\n\n  const nextContent = upsertReleaseSection({\n    content: currentContent,\n    date: releaseDate,\n    section,\n  });\n\n  const validation = validateChangelogStructure(nextContent);\n  if (!validation.ok) {\n    throw new Error(\n      [\n        \"Generated changelog failed structural validation:\",\n        ...validation.errors.map((error) => `- ${error}`),\n      ].join(\"\\n\"),\n    );\n  }\n\n  fs.writeFileSync(changelogPath, nextContent, \"utf8\");\n\n  console.log(\"Generated changelog section.\");\n  console.log(`- date: ${releaseDate}`);\n  console.log(`- baseline: ${baselineSource}`);\n  console.log(`- range: ${gitContext.range}`);\n  console.log(`- lastTag: ${gitContext.lastTag ?? \"none\"}`);\n  console.log(`- changelog: ${path.relative(projectRoot, changelogPath)}`);\n}\n\nmain().catch((error: unknown) => {\n  console.error(\"Failed to generate changelog.\");\n  console.error(error);\n  process.exitCode = 1;\n});\n"
  },
  {
    "path": "apps/www/scripts/install-git-hooks.ts",
    "content": "import { spawnSync } from \"node:child_process\";\nimport { pathToFileURL } from \"node:url\";\n\ntype RunCommand = (command: string, args: string[]) => number | null;\n\nfunction run(command: string, args: string[]): number | null {\n  const result = spawnSync(command, args, { stdio: \"ignore\" });\n  if (result.error) {\n    return null;\n  }\n  return result.status;\n}\n\nexport function configureGitHooks(\n  runCommand: RunCommand = run,\n): \"configured\" | \"failed\" | \"skipped\" {\n  const insideWorkTree = runCommand(\"git\", [\n    \"rev-parse\",\n    \"--is-inside-work-tree\",\n  ]);\n  if (insideWorkTree !== 0) {\n    return \"skipped\";\n  }\n\n  const configured = runCommand(\"git\", [\n    \"config\",\n    \"--local\",\n    \"core.hooksPath\",\n    \".githooks\",\n  ]);\n  if (configured !== 0) {\n    return \"failed\";\n  }\n\n  return \"configured\";\n}\n\nfunction main(): void {\n  const status = configureGitHooks();\n  if (status === \"configured\") {\n    console.log('Configured git hooks path to \".githooks\".');\n  }\n}\n\nconst entryScript = process.argv[1];\nif (entryScript && import.meta.url === pathToFileURL(entryScript).href) {\n  main();\n}\n"
  },
  {
    "path": "apps/www/scripts/new-tool-ui-component.ts",
    "content": "import { promises as fs } from \"fs\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\n\nfunction toPascalCase(input: string): string {\n  return input\n    .split(\"-\")\n    .filter(Boolean)\n    .map((segment) => segment[0].toUpperCase() + segment.slice(1))\n    .join(\"\");\n}\n\nfunction assertSlug(value: string): void {\n  if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) {\n    throw new Error(\n      `Invalid component slug \"${value}\". Use kebab-case (example: task-receipt).`,\n    );\n  }\n}\n\nasync function writeFileSafe(filePath: string, content: string): Promise<void> {\n  await fs.mkdir(path.dirname(filePath), { recursive: true });\n  await fs.writeFile(filePath, content, \"utf8\");\n}\n\nasync function ensureMissing(paths: string[]): Promise<void> {\n  for (const targetPath of paths) {\n    try {\n      await fs.stat(targetPath);\n      throw new Error(`Refusing to overwrite existing path: ${targetPath}`);\n    } catch (error) {\n      if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") {\n        throw error;\n      }\n    }\n  }\n}\n\nasync function main(): Promise<void> {\n  const [, , slugArg] = process.argv;\n  if (!slugArg) {\n    throw new Error(\"Usage: pnpm component:new <kebab-case-component-name>\");\n  }\n\n  assertSlug(slugArg);\n  const slug = slugArg.trim();\n  const componentName = toPascalCase(slug);\n  const fileStem = slug;\n\n  const scriptDir = path.dirname(fileURLToPath(import.meta.url));\n  const projectRoot = path.resolve(scriptDir, \"..\");\n\n  const componentDir = path.join(projectRoot, \"components\", \"tool-ui\", slug);\n  const docsDir = path.join(projectRoot, \"app\", \"docs\", slug);\n  const presetFile = path.join(projectRoot, \"lib\", \"presets\", `${slug}.ts`);\n\n  await ensureMissing([componentDir, docsDir, presetFile]);\n\n  const schemaFile = path.join(componentDir, \"schema.ts\");\n  const adapterFile = path.join(componentDir, \"_adapter.tsx\");\n  const componentFile = path.join(componentDir, `${fileStem}.tsx`);\n  const indexFile = path.join(componentDir, \"index.tsx\");\n  const readmeFile = path.join(componentDir, \"README.md\");\n  const docsFile = path.join(docsDir, \"content.mdx\");\n\n  await writeFileSafe(\n    schemaFile,\n    `import { z } from \"zod\";\nimport { ToolUIIdSchema, ToolUIRoleSchema } from \"../shared/schema\";\nimport { defineToolUiContract } from \"../shared/contract\";\n\nexport const Serializable${componentName}Schema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n});\n\nexport type Serializable${componentName} = z.infer<\n  typeof Serializable${componentName}Schema\n>;\n\nconst Serializable${componentName}SchemaContract = defineToolUiContract(\n  \"${componentName}\",\n  Serializable${componentName}Schema,\n);\n\nexport const parseSerializable${componentName}: (\n  input: unknown,\n) => Serializable${componentName} = Serializable${componentName}SchemaContract.parse;\n\nexport const safeParseSerializable${componentName}: (\n  input: unknown,\n) => Serializable${componentName} | null =\n  Serializable${componentName}SchemaContract.safeParse;\n\nexport interface ${componentName}Props extends Serializable${componentName} {\n  className?: string;\n}\n`,\n  );\n\n  await writeFileSafe(\n    adapterFile,\n    `export { cn } from \"@/lib/ui/cn\";\nexport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\n`,\n  );\n\n  await writeFileSafe(\n    componentFile,\n    `\"use client\";\n\nimport { cn, Card, CardContent, CardHeader, CardTitle } from \"./_adapter\";\nimport type { ${componentName}Props } from \"./schema\";\n\nexport function ${componentName}({ id, className }: ${componentName}Props) {\n  return (\n    <Card\n      className={cn(\"w-full max-w-md\", className)}\n      data-slot=\"${slug}\"\n      data-tool-ui-id={id}\n    >\n      <CardHeader>\n        <CardTitle>${componentName}</CardTitle>\n      </CardHeader>\n      <CardContent>\n        <p className=\"text-muted-foreground text-sm\">\n          Replace this placeholder with your final UI.\n        </p>\n      </CardContent>\n    </Card>\n  );\n}\n`,\n  );\n\n  await writeFileSafe(\n    indexFile,\n    `export { ${componentName} } from \"./${fileStem}\";\nexport {\n  Serializable${componentName}Schema,\n  parseSerializable${componentName},\n  safeParseSerializable${componentName},\n} from \"./schema\";\nexport type { Serializable${componentName}, ${componentName}Props } from \"./schema\";\n`,\n  );\n\n  await writeFileSafe(\n    readmeFile,\n    `# ${componentName}\n\nSource for the \\`${componentName}\\` Tool UI component.\n\n## Key files\n\n- \\`./schema.ts\\` - serializable contract + parse helpers\n- \\`./${fileStem}.tsx\\` - component implementation\n- \\`./index.tsx\\` - public exports\n\n## Companion docs\n\n- Docs page: \\`/app/docs/${slug}/content.mdx\\`\n- Preset seed: \\`/lib/presets/${slug}.ts\\`\n`,\n  );\n\n  await writeFileSafe(\n    docsFile,\n    `import { DocsHeader } from \"../_components/docs-header\";\n\n<DocsHeader\n  title=\"${componentName}\"\n  description=\"Describe what this tool UI surface does.\"\n  mdxPath=\"app/docs/${slug}/content.mdx\"\n/>\n\n## Overview\n\nDocument this component's intent, schema contract, and usage examples.\n`,\n  );\n\n  await writeFileSafe(\n    presetFile,\n    `export const ${componentName}Preset = {\n  id: \"${slug}-example\",\n};\n`,\n  );\n\n  console.log(`Created component scaffold for \"${slug}\"`);\n  console.log(`- components/tool-ui/${slug}`);\n  console.log(`- app/docs/${slug}/content.mdx`);\n  console.log(`- lib/presets/${slug}.ts`);\n  console.log(\n    \"Next: wire category metadata in lib/docs/component-registry.ts.\",\n  );\n}\n\nmain().catch((error: unknown) => {\n  console.error(\"Failed to scaffold Tool UI component.\");\n  console.error(error);\n  process.exitCode = 1;\n});\n"
  },
  {
    "path": "apps/www/scripts/precommit-registry-sync.ts",
    "content": "import { spawnSync } from \"node:child_process\";\n\nconst WATCHED_PREFIXES = [\n  \"apps/www/components/tool-ui/\",\n  \"apps/www/lib/registry/\",\n];\nconst WATCHED_FILES = new Set([\"apps/www/scripts/build-tool-ui-registry.ts\"]);\n\nfunction runCapture(command: string, args: string[]): string {\n  const result = spawnSync(command, args, { encoding: \"utf8\" });\n  if (result.status !== 0) {\n    throw new Error(`Command failed: ${command} ${args.join(\" \")}`);\n  }\n  return result.stdout.trim();\n}\n\nfunction run(command: string, args: string[]): void {\n  const result = spawnSync(command, args, { stdio: \"inherit\" });\n  if (result.status !== 0) {\n    throw new Error(`Command failed: ${command} ${args.join(\" \")}`);\n  }\n}\n\nfunction shouldSyncRegistry(stagedFiles: string[]): boolean {\n  return stagedFiles.some(\n    (file) =>\n      WATCHED_FILES.has(file) ||\n      WATCHED_PREFIXES.some((prefix) => file.startsWith(prefix)),\n  );\n}\n\nfunction main(): void {\n  const insideWorkTree = spawnSync(\"git\", [\n    \"rev-parse\",\n    \"--is-inside-work-tree\",\n  ]);\n  if (insideWorkTree.status !== 0) {\n    return;\n  }\n\n  const stagedOutput = runCapture(\"git\", [\n    \"diff\",\n    \"--cached\",\n    \"--name-only\",\n    \"--diff-filter=ACMR\",\n  ]);\n  const stagedFiles = stagedOutput\n    .split(\"\\n\")\n    .map((file) => file.trim())\n    .filter((file) => file.length > 0);\n\n  if (!shouldSyncRegistry(stagedFiles)) {\n    return;\n  }\n\n  console.log(\n    \"Detected Tool UI source changes; regenerating registry artifacts.\",\n  );\n  run(\"pnpm\", [\"registry:build\"]);\n  run(\"git\", [\"add\", \"public/r\"]);\n}\n\ntry {\n  main();\n} catch (error) {\n  console.error(error);\n  process.exitCode = 1;\n}\n"
  },
  {
    "path": "apps/www/scripts/registry-check.ts",
    "content": "import { spawnSync } from \"node:child_process\";\n\nfunction run(command: string, args: string[]): void {\n  const result = spawnSync(command, args, { stdio: \"inherit\" });\n  if (result.status !== 0) {\n    throw new Error(`Command failed: ${command} ${args.join(\" \")}`);\n  }\n}\n\nfunction runCapture(command: string, args: string[]): string {\n  const result = spawnSync(command, args, { encoding: \"utf8\" });\n  if (result.status !== 0) {\n    throw new Error(`Command failed: ${command} ${args.join(\" \")}`);\n  }\n  return result.stdout.trim();\n}\n\nfunction main(): void {\n  run(\"pnpm\", [\"registry:build\"]);\n\n  const diffCheck = spawnSync(\"git\", [\"diff\", \"--quiet\", \"--\", \"public/r\"]);\n  if (diffCheck.status === 0) {\n    return;\n  }\n\n  const changedFiles = runCapture(\"git\", [\n    \"diff\",\n    \"--name-only\",\n    \"--\",\n    \"public/r\",\n  ]);\n\n  console.error(\"\\nRegistry artifacts are out of date.\");\n  if (changedFiles.length > 0) {\n    console.error(\"Changed files:\");\n    for (const file of changedFiles.split(\"\\n\")) {\n      console.error(`- ${file}`);\n    }\n  }\n  console.error(\n    '\\nFix with: pnpm registry:build && git add public/r && git commit -m \"chore: refresh registry artifacts\"',\n  );\n\n  process.exitCode = 1;\n}\n\ntry {\n  main();\n} catch (error) {\n  console.error(error);\n  process.exitCode = 1;\n}\n"
  },
  {
    "path": "apps/www/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES6\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"strictNullChecks\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"incremental\": true,\n    \"paths\": {\n      \"@/*\": [\"./*\"],\n      \"@/components/*\": [\"./components/*\"],\n      \"@/lib/*\": [\"./lib/*\"],\n      \"@/app/*\": [\"./app/*\"]\n    },\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ]\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\",\n    \".next/dev/types/**/*.ts\"\n  ],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "apps/www/vitest.config.ts",
    "content": "import { defineConfig } from \"vitest/config\";\nimport path from \"path\";\n\nexport default defineConfig({\n  resolve: {\n    alias: {\n      \"@\": path.resolve(__dirname, \"./\"),\n    },\n  },\n  test: {\n    environment: \"node\",\n    include: [\"lib/playground/**/*.test.ts\", \"lib/tests/**/*.test.ts\"],\n    setupFiles: [\"lib/tests/setup/console-guard.ts\"],\n    passWithNoTests: true,\n    globals: true,\n    coverage: {\n      reporter: [\"text\", \"json-summary\"],\n    },\n  },\n});\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"tool-ui\",\n  \"version\": \"0.2.0\",\n  \"private\": true,\n  \"description\": \"Open source component library for tool call widgets\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"prepare\": \"tsx apps/www/scripts/install-git-hooks.ts\",\n    \"dev\": \"pnpm --filter tool-ui-www dev\",\n    \"build\": \"pnpm --filter tool-ui-www build\",\n    \"start\": \"pnpm --filter tool-ui-www start\",\n    \"check\": \"pnpm --filter tool-ui-www check\",\n    \"lint\": \"pnpm --filter tool-ui-www lint\",\n    \"lint:fix\": \"pnpm --filter tool-ui-www lint:fix\",\n    \"lint:oxlint\": \"pnpm --filter tool-ui-www lint:oxlint\",\n    \"lint:eslint\": \"pnpm --filter tool-ui-www lint:eslint\",\n    \"lint:format\": \"pnpm --filter tool-ui-www lint:format\",\n    \"typecheck\": \"pnpm --filter tool-ui-www typecheck\",\n    \"test\": \"pnpm --filter tool-ui-www test\",\n    \"test:watch\": \"pnpm --filter tool-ui-www test:watch\",\n    \"format\": \"pnpm --filter tool-ui-www format\",\n    \"format:check\": \"pnpm --filter tool-ui-www format:check\",\n    \"hooks:pre-commit\": \"pnpm --filter tool-ui-www hooks:pre-commit\",\n    \"hooks:pre-push\": \"pnpm --filter tool-ui-www hooks:pre-push\",\n    \"registry:build\": \"pnpm --filter tool-ui-www registry:build\",\n    \"registry:check\": \"pnpm --filter tool-ui-www registry:check\",\n    \"changelog:check\": \"pnpm --filter tool-ui-www changelog:check\",\n    \"changelog:generate\": \"pnpm --filter tool-ui-www changelog:generate\",\n    \"component:new\": \"pnpm --filter tool-ui-www component:new\",\n    \"weather:check\": \"pnpm --filter tool-ui-www weather:check\",\n    \"verify:ci\": \"pnpm --filter tool-ui-www run check && pnpm --filter tool-ui-www run test && pnpm --filter tool-ui-www run changelog:check && pnpm --filter tool-ui-www run registry:check && pnpm --filter tool-ui-www run weather:check\",\n    \"ci:version\": \"changeset version && pnpm install --no-frozen-lockfile\",\n    \"ci:publish\": \"pnpm --filter './packages/*' build && changeset publish\"\n  },\n  \"devDependencies\": {\n    \"@changesets/cli\": \"^2.29.8\",\n    \"lint-staged\": \"^16.4.0\",\n    \"tsx\": \"^4.21.0\"\n  },\n  \"engines\": {\n    \"node\": \">=20\",\n    \"pnpm\": \">=8.0.0\"\n  },\n  \"lint-staged\": {\n    \"apps/www/**/*.{ts,tsx,js,jsx,mjs,cjs,css,json,jsonc}\": \"npx oxfmt --no-error-on-unmatched-pattern\"\n  },\n  \"packageManager\": \"pnpm@10.11.0\"\n}\n"
  },
  {
    "path": "packages/agent/package.json",
    "content": "{\n  \"name\": \"tool-agent\",\n  \"version\": \"0.1.2\",\n  \"description\": \"CLI for setting up Tool UI coding agent integrations\",\n  \"bin\": {\n    \"tool-agent\": \"./dist/cli.mjs\"\n  },\n  \"files\": [\n    \"dist\",\n    \"plugin\"\n  ],\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"tsup\"\n  },\n  \"dependencies\": {\n    \"@clack/prompts\": \"^0.10.0\"\n  },\n  \"devDependencies\": {\n    \"tsup\": \"^8.5.0\",\n    \"typescript\": \"^5.9.3\"\n  },\n  \"engines\": {\n    \"node\": \">=20\"\n  }\n}\n"
  },
  {
    "path": "packages/agent/plugin/.claude-plugin/plugin.json",
    "content": "{\n  \"name\": \"tool-ui\",\n  \"description\": \"Develop assistant-ui tool UIs using Tool UI components\",\n  \"version\": \"0.1.1\",\n  \"author\": {\n    \"name\": \"tool-ui\"\n  }\n}\n"
  },
  {
    "path": "packages/agent/plugin/skills/tool-ui/SKILL.md",
    "content": "---\nname: tool-ui\ndescription: Find, install, configure, and integrate Tool UI components in React apps using shadcn registry entries, compatibility checks, scaffolded runtime wiring, toolkit setup with assistant-ui, and troubleshooting workflows. Use when developers ask to add one or more Tool UI components, choose components for a use case, verify compatibility, wire a toolkit in a codebase, or integrate Tool UI payloads into assistant-ui or an existing chat/runtime stack.\n---\n\n# Tool UI\n\nUse this skill to move from request to working Tool UI integration quickly.\n\nPrefer assistant-ui when the project has no existing chat UI/runtime. Treat assistant-ui as optional when the app already has a working runtime.\n\n## Step 1: Compatibility and Doctor\n\nRead `components.json` in the user's project and verify:\n\n- `components.json` exists.\n\n## Step 2: Install Components\n\n### Install command from project root\n\n**Preferred (AI-assisted integration):**\n\n```bash\nnpx tool-agent \"integrate the <component> component\"\n```\n\nUse component-specific prompts from the catalog below (e.g. `integrate the plan component for step-by-step task workflows with status tracking`).\n\n**Alternative (direct registry):**\n\n```bash\nnpx shadcn@latest add @tool-ui/<component-id>\n```\n\nMultiple components:\n\n```bash\nnpx tool-agent \"integrate the plan, progress-tracker, and approval-card components for planning flows\"\n```\n\nOr via shadcn:\n\n```bash\nnpx shadcn@latest add @tool-ui/plan @tool-ui/progress-tracker @tool-ui/approval-card\n```\n\n### Complete component catalog\n\nAll 25 Tool UI components with tool-agent prompts and shadcn install commands:\n\n**Progress**\n\n| Component          | Description                                         | tool-agent prompt                                                                                 | shadcn                                            |\n| ------------------ | --------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------- |\n| `plan`             | Step-by-step task workflows with status tracking    | `integrate the plan component for step-by-step task workflows with status tracking`               | `npx shadcn@latest add @tool-ui/plan`             |\n| `progress-tracker` | Real-time status feedback for multi-step operations | `integrate the progress tracker component for real-time status feedback on multi-step operations` | `npx shadcn@latest add @tool-ui/progress-tracker` |\n\n**Input**\n\n| Component           | Description                                 | tool-agent prompt                                                                      | shadcn                                             |\n| ------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------- |\n| `option-list`       | Let users select from multiple choices      | `integrate the option list component to let users select from multiple choices`        | `npx shadcn@latest add @tool-ui/option-list`       |\n| `parameter-slider`  | Numeric parameter adjustment controls       | `integrate the parameter slider component for numeric parameter adjustment controls`   | `npx shadcn@latest add @tool-ui/parameter-slider`  |\n| `preferences-panel` | Compact settings panel for user preferences | `integrate the preferences panel component for compact user settings`                  | `npx shadcn@latest add @tool-ui/preferences-panel` |\n| `question-flow`     | Multi-step guided questions with branching  | `integrate the question flow component for multi-step guided questions with branching` | `npx shadcn@latest add @tool-ui/question-flow`     |\n\n**Display**\n\n| Component        | Description                                   | tool-agent prompt                                                                          | shadcn                                          |\n| ---------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------ | ----------------------------------------------- |\n| `citation`       | Display source references with attribution    | `integrate the citation component to display source references with attribution`           | `npx shadcn@latest add @tool-ui/citation`       |\n| `item-carousel`  | Horizontal carousel for browsing collections  | `integrate the item carousel component for horizontal browsing of collections`             | `npx shadcn@latest add @tool-ui/item-carousel`  |\n| `link-preview`   | Rich link previews with Open Graph data       | `integrate the link preview component for rich link previews with Open Graph data`         | `npx shadcn@latest add @tool-ui/link-preview`   |\n| `stats-display`  | Key metrics and KPIs in a visual grid         | `integrate the stats display component for key metrics and KPIs in a visual grid`          | `npx shadcn@latest add @tool-ui/stats-display`  |\n| `terminal`       | Show command-line output and logs             | `integrate the terminal component to show command-line output and logs`                    | `npx shadcn@latest add @tool-ui/terminal`       |\n| `weather-widget` | Weather display with forecasts and conditions | `integrate the weather widget component for weather display with forecasts and conditions` | `npx shadcn@latest add @tool-ui/weather-widget` |\n\n**Artifacts**\n\n| Component        | Description                                                                | tool-agent prompt                                                                         | shadcn                                          |\n| ---------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------- |\n| `chart`          | Visualize data with interactive charts (needs `recharts`)                  | `integrate the chart component to visualize data with interactive charts`                 | `npx shadcn@latest add @tool-ui/chart`          |\n| `code-block`     | Display syntax-highlighted code snippets                                   | `integrate the code block component for syntax-highlighted code snippets`                 | `npx shadcn@latest add @tool-ui/code-block`     |\n| `code-diff`      | Compare code changes with syntax-highlighted diffs (needs `@pierre/diffs`) | `integrate the code diff component to compare code changes with syntax-highlighted diffs` | `npx shadcn@latest add @tool-ui/code-diff`      |\n| `data-table`     | Present structured data in sortable tables                                 | `integrate the data table component to present structured data in sortable tables`        | `npx shadcn@latest add @tool-ui/data-table`     |\n| `message-draft`  | Review and approve messages before sending                                 | `integrate the message draft component to review and approve messages before sending`     | `npx shadcn@latest add @tool-ui/message-draft`  |\n| `instagram-post` | Render Instagram post previews                                             | `integrate the instagram post component to render Instagram post previews`                | `npx shadcn@latest add @tool-ui/instagram-post` |\n| `linkedin-post`  | Render LinkedIn post previews                                              | `integrate the linkedin post component to render LinkedIn post previews`                  | `npx shadcn@latest add @tool-ui/linkedin-post`  |\n| `x-post`         | Render X post previews                                                     | `integrate the x post component to render X/Twitter post previews`                        | `npx shadcn@latest add @tool-ui/x-post`         |\n\n**Confirmation**\n\n| Component       | Description                             | tool-agent prompt                                                                  | shadcn                                         |\n| --------------- | --------------------------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------- |\n| `approval-card` | Binary confirmation for agent actions   | `integrate the approval card component for binary confirmation of agent actions`   | `npx shadcn@latest add @tool-ui/approval-card` |\n| `order-summary` | Display purchases with itemized pricing | `integrate the order summary component to display purchases with itemized pricing` | `npx shadcn@latest add @tool-ui/order-summary` |\n\n**Media**\n\n| Component       | Description                                  | tool-agent prompt                                                               | shadcn                                         |\n| --------------- | -------------------------------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------- |\n| `audio`         | Audio playback with artwork and metadata     | `integrate the audio component for audio playback with artwork and metadata`    | `npx shadcn@latest add @tool-ui/audio`         |\n| `image`         | Display images with metadata and attribution | `integrate the image component to display images with metadata and attribution` | `npx shadcn@latest add @tool-ui/image`         |\n| `image-gallery` | Masonry grid with fullscreen lightbox viewer | `integrate the image gallery component with masonry grid and lightbox viewer`   | `npx shadcn@latest add @tool-ui/image-gallery` |\n| `video`         | Video playback with controls and poster      | `integrate the video component for video playback with controls and poster`     | `npx shadcn@latest add @tool-ui/video`         |\n\n**Display (Geo Map)**\n\n| Component | Description                                     | tool-agent prompt                                                                    | shadcn                                   |\n| --------- | ----------------------------------------------- | ------------------------------------------------------------------------------------ | ---------------------------------------- |\n| `geo-map` | Display geolocated entities and fleet positions | `integrate the geo map component to display geolocated entities and fleet positions` | `npx shadcn@latest add @tool-ui/geo-map` |\n\n### Example installs by use case\n\n**tool-agent (recommended):**\n\n```bash\nnpx tool-agent \"integrate the plan, progress-tracker, and approval-card components for planning flows\"\nnpx tool-agent \"integrate citation, link-preview, code-block, and code-diff for research output\"\nnpx tool-agent \"integrate data-table, chart, and stats-display for data visualization\"\nnpx tool-agent \"integrate image, image-gallery, video, and audio for media display\"\n```\n\n**shadcn (direct):**\n\n```bash\nnpx shadcn@latest add @tool-ui/plan @tool-ui/progress-tracker @tool-ui/approval-card\nnpx shadcn@latest add @tool-ui/citation @tool-ui/link-preview @tool-ui/code-block @tool-ui/code-diff\nnpx shadcn@latest add @tool-ui/data-table @tool-ui/chart @tool-ui/stats-display\n# npm i recharts  # peer for chart\nnpx shadcn@latest add @tool-ui/image @tool-ui/image-gallery @tool-ui/video @tool-ui/audio\n```\n\n### Toolkit setup in a codebase\n\nAfter installing components, wire them into assistant-ui via a `Toolkit`. This section covers the full setup: provider, runtime, toolkit file, and ID handling.\n\n#### 1. Provider and runtime\n\nCreate an assistant wrapper that provides runtime, transport, and tools:\n\n```tsx\n\"use client\";\n\nimport { lastAssistantMessageIsCompleteWithToolCalls } from \"ai\";\nimport { AssistantRuntimeProvider, Tools, useAui } from \"@assistant-ui/react\";\nimport {\n  AssistantChatTransport,\n  useChatRuntime,\n} from \"@assistant-ui/react-ai-sdk\";\nimport { Thread } from \"@/components/assistant-ui/thread\";\nimport { toolkit } from \"@/components/toolkit\";\n\nexport const Assistant = () => {\n  const runtime = useChatRuntime({\n    transport: new AssistantChatTransport({ api: \"/api/chat\" }),\n    sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,\n  });\n  const aui = useAui({ tools: Tools({ toolkit }) });\n\n  return (\n    <AssistantRuntimeProvider runtime={runtime} aui={aui}>\n      <div className=\"h-dvh\">\n        <Thread />\n      </div>\n    </AssistantRuntimeProvider>\n  );\n};\n```\n\nKey points:\n\n- `useChatRuntime` + `AssistantChatTransport`: connects to your chat API.\n- `Tools({ toolkit })`: forwards tool definitions and renderers to the model.\n- `sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls`: auto-continues after tool calls (optional but common for tool-heavy flows).\n\n#### 2. Toolkit file structure\n\nCreate a single `toolkit.ts` (or `toolkit.tsx`) that exports a `Toolkit` object. Each key is a tool name; each value has `type`, `description`, `parameters`, and `render`.\n\n**Tool descriptions** — Always include a `description` on every tool. Describe _when_ to call the tool and what role it plays, not the visible content. The Tool UI component already renders the payload (options, plan, chart, etc.) to the user; a description that repeats that content is redundant. Prefer model-oriented guidance (e.g. \"Present a plan for the user to follow\" or \"Let the user pick one option\") over content echo (e.g. \"Shows a list of options with labels and descriptions\").\n\n## -**Frontend vs backend tools**\n\n| -   |                    | Frontend                                                      | Backend                                                                 |\n| --- | ------------------ | ------------------------------------------------------------- | ----------------------------------------------------------------------- |\n| -   | **Implementation** | Runs in the browser; user interaction commits via `addResult` | Tool implementation lives on the server; model returns the result       |\n| -   | **`execute`**      | Required — runs the tool UI flow client-side                  | Not needed                                                              |\n| -   | **`parameters`**   | Required (schema for model args)                              | does not use, uses inputSchema instead if backend llm is done via aisdk |\n| -   | **`render`**       | Required (UI for args, status, result, `addResult`)           | Required (UI for `result`)                                              |\n\n**Backend tools** (model returns result; no user input):\n\n```tsx\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { Plan } from \"@/components/tool-ui/plan\";\nimport { safeParseSerializablePlan } from \"@/components/tool-ui/plan/schema\";\n\nexport const toolkit: Toolkit = {\n  showPlan: {\n    type: \"backend\",\n    render: ({ result }) => {\n      const parsed = safeParseSerializablePlan(result);\n      if (!parsed) return null;\n      return <Plan {...parsed} />;\n    },\n  },\n};\n```\n\n**Frontend tools** (model sends args; user interaction commits via `addResult`):\n\n```tsx\nimport { type Toolkit } from \"@assistant-ui/react\";\nimport { OptionList } from \"@/components/tool-ui/option-list\";\nimport {\n  SerializableOptionListSchema,\n  safeParseSerializableOptionList,\n} from \"@/components/tool-ui/option-list/schema\";\n\nconst optionListTool: Toolkit[string] = {\n  description: \"Render selectable options with confirm and clear actions.\",\n  parameters: SerializableOptionListSchema,\n  render: ({ args, toolCallId, result, addResult }) => {\n    const parsed = safeParseSerializableOptionList({\n      ...args,\n      id: args?.id ?? `option-list-${toolCallId}`,\n    });\n    if (!parsed) return null;\n\n    if (result) {\n      return <OptionList {...parsed} choice={result} />;\n    }\n    return (\n      <OptionList\n        {...parsed}\n        onAction={async (actionId, selection) => {\n          if (actionId === \"confirm\" || actionId === \"cancel\") {\n            await addResult?.(selection);\n          }\n        }}\n      />\n    );\n  },\n};\n\nexport const toolkit: Toolkit = {\n  option_list: optionListTool,\n  approval_card: {\n    /* ... */\n  },\n};\n```\n\n#### 3. API route (AI SDK)\n\nWhen the chat API uses the AI SDK (`streamText`), define backend tools with `tool()` from `ai`:\n\n- Use **`inputSchema`**\n- Backend tools use **`execute`** on the server; the result is streamed and rendered via the toolkit `render` function\n\n```ts\nimport { streamText, tool, convertToModelMessages } from \"ai\";\nimport { openai } from \"@ai-sdk/openai\";\nimport { z } from \"zod\";\n\n// With frontend tools: ...frontendTools(clientTools) — clientTools come from the request body via AssistantChatTransport\nconst result = streamText({\n  model: openai(\"gpt-4o\"),\n  messages: await convertToModelMessages(messages),\n  tools: {\n    get_weather: tool({\n      description:\n        \"Get the current weather and forecast for a location. Returns data to display in a weather widget.\",\n      inputSchema: z.object({\n        location: z.string().describe(\"City name, e.g. 'San Francisco'\"),\n        units: z\n          .enum([\"celsius\", \"fahrenheit\"])\n          .default(\"fahrenheit\")\n          .describe(\"Temperature unit\"),\n      }),\n      execute: async ({ location, units }) => {\n        // Fetch weather data, return shape matching your widget schema\n        return { location, units /* ... */ };\n      },\n    }),\n  },\n});\n```\n\n#### 4. Action-centric vs compound components\n\n| Pattern            | Components                                                          | Usage                                                                                                              |\n| ------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |\n| **Action-centric** | `OptionList`, `ParameterSlider`, `PreferencesPanel`, `ApprovalCard` | Wire `onAction` or `onConfirm`/`onCancel` directly; no `ToolUI` wrapper. Pass `choice={result}` for receipt state. |\n| **Compound**       | `OrderSummary`, `DataTable`, etc.                                   | Wrap in `ToolUI` + `ToolUI.Surface` + `ToolUI.Actions`; use `DecisionActions` or `LocalActions`.                   |\n\nAction-centric example (OptionList):\n\n```tsx\nreturn (\n  <OptionList\n    {...parsed}\n    onAction={async (actionId, selection) => {\n      if (actionId === \"confirm\" || actionId === \"cancel\") {\n        await addResult?.(selection);\n      }\n    }}\n  />\n);\n```\n\nCompound example (OrderSummary with DecisionActions):\n\n```tsx\nreturn (\n  <ToolUI id={parsed.id}>\n    <ToolUI.Surface>\n      <OrderSummary {...parsed} />\n    </ToolUI.Surface>\n    <ToolUI.Actions>\n      <ToolUI.DecisionActions\n        actions={[\n          { id: \"cancel\", label: \"Cancel\", variant: \"outline\" },\n          { id: \"confirm\", label: \"Purchase\" },\n        ]}\n        onAction={(action) =>\n          createDecisionResult({ decisionId: parsed.id, action })\n        }\n        onCommit={(decision) => addResult?.(decision)}\n      />\n    </ToolUI.Actions>\n  </ToolUI>\n);\n```\n\nApprovalCard uses embedded actions; wire `onConfirm`/`onCancel` directly:\n\n```tsx\nreturn (\n  <ApprovalCard\n    {...parsed}\n    choice={\n      result === \"approved\" || result === \"denied\" ? result : parsed.choice\n    }\n    onConfirm={async () => addResult?.(\"approved\")}\n    onCancel={async () => addResult?.(\"denied\")}\n  />\n);\n```\n\n## Action Model\n\nTool UI uses two action surfaces, rendered as compound siblings outside the display component:\n\n- `ToolUI.LocalActions`: non-consequential side effects (export, copy, open link). Handlers must not call `addResult(...)`.\n- `ToolUI.DecisionActions`: consequential choices that produce a `DecisionResult` envelope via `createDecisionResult(...)`. The commit callback calls `addResult(...)`.\n\nCompound wrapper pattern for display components with actions:\n\n```tsx\n<ToolUI id={surfaceId}>\n  <ToolUI.Surface>\n    <DataTable {...props} />\n  </ToolUI.Surface>\n  <ToolUI.Actions>\n    <ToolUI.LocalActions\n      actions={[{ id: \"export-csv\", label: \"Export CSV\" }]}\n      onAction={(actionId) => {\n        /* side effects only */\n      }}\n    />\n  </ToolUI.Actions>\n</ToolUI>\n```\n\nThree components are action-centric exceptions — they keep embedded action props instead of sibling surfaces. All three share a unified interface:\n\n- `actions`: action buttons rendered by the component.\n- `onAction(actionId, state)`: runs after the action and receives post-action state.\n- `onBeforeAction(actionId, state)`: guard evaluated before an action runs.\n\n| Component          | State type passed to handlers |\n| ------------------ | ----------------------------- |\n| `OptionList`       | `OptionListSelection`         |\n| `ParameterSlider`  | `SliderValue[]`               |\n| `PreferencesPanel` | `PreferencesValue`            |\n\nComponents using the compound pattern: `CodeBlock`, `CodeDiff`, `Terminal`, `ProgressTracker`.\n\nContext is shared via `createContext` + `use()` (React 19). Subcomponents throw if used outside their Root.\n\n## Receipt and Choice Convention\n\nComponents with outcomes use a `choice` prop to render confirmed/completed state:\n\n| Component         | `choice` type            | Values / shape                                         |\n| ----------------- | ------------------------ | ------------------------------------------------------ |\n| `ApprovalCard`    | `\"approved\" \\| \"denied\"` | String literal                                         |\n| `OptionList`      | `string \\| string[]`     | Selected option ID(s)                                  |\n| `OrderSummary`    | `OrderDecision`          | `{ action: \"confirm\", orderId?, confirmedAt? }`        |\n| `ProgressTracker` | `ToolUIReceipt`          | `{ outcome, summary, identifiers?, at }` (shared type) |\n\nWhen `choice` is present, the component renders in receipt mode — read-only, no actions.\n\n## Operational Rules\n\n- Install the smallest set of components that solves the request.\n- Every tool must have a `description`; write it for the model (when to call, what role) — avoid repeating content the component will render.\n\nNotes:\nFrontend tools need an execute function\nBackend tools have the tool implementation on the server side.\nBackend tool don't need either\nIgnore the generated files\nAfter setup:\n\n- Ensure the required package dependencies are installed so the first experience of running after the changes is magical\n- Notify the user if env variables are not set that should be for a successful run of the feature that was just implemented, most likely mainly variables required by the api chat endpoint.\n"
  },
  {
    "path": "packages/agent/src/cli.ts",
    "content": "import * as p from \"@clack/prompts\";\nimport { execFileSync } from \"node:child_process\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\ninterface AgentConfig {\n  agent: string;\n  pluginPath: string;\n}\n\nconst CONFIG_DIR = \".tool-ui\";\nconst CONFIG_FILE = \"agent.json\";\n\nfunction getConfigPath(): string {\n  return join(process.cwd(), CONFIG_DIR, CONFIG_FILE);\n}\n\nfunction readConfig(): AgentConfig | null {\n  const configPath = getConfigPath();\n  if (!existsSync(configPath)) return null;\n  try {\n    return JSON.parse(readFileSync(configPath, \"utf-8\")) as AgentConfig;\n  } catch {\n    return null;\n  }\n}\n\nfunction writeConfig(config: AgentConfig): void {\n  const dir = join(process.cwd(), CONFIG_DIR);\n  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n  writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + \"\\n\");\n}\n\nfunction getPluginPath(): string {\n  // In dist/, plugin is at ../plugin relative to the cli.mjs file\n  // In dev (src/), plugin is at ../plugin relative to the src/ dir\n  const candidates = [\n    resolve(__dirname, \"..\", \"plugin\"),\n    resolve(__dirname, \"plugin\"),\n  ];\n  for (const candidate of candidates) {\n    if (existsSync(candidate)) return candidate;\n  }\n  return candidates[0]!;\n}\n\nasync function runSetup(): Promise<AgentConfig> {\n  p.intro(\"Tool UI Agent Setup\");\n\n  const agent = await p.select({\n    message: \"Which coding agent do you use?\",\n    options: [\n      {\n        value: \"claude-code\",\n        label: \"Claude Code\",\n        hint: \"Installs a Claude Code plugin with Tool UI skills\",\n      },\n    ],\n  });\n\n  if (p.isCancel(agent)) {\n    p.cancel(\"Setup cancelled.\");\n    process.exit(0);\n  }\n\n  const pluginPath = getPluginPath();\n  const config: AgentConfig = { agent: agent as string, pluginPath };\n\n  writeConfig(config);\n\n  p.outro(\"Setup complete! Run `tool-agent setup` to reconfigure.\");\n\n  return config;\n}\n\nasync function main(): Promise<void> {\n  const args = process.argv.slice(2);\n\n  if (args[0] === \"setup\") {\n    await runSetup();\n    return;\n  }\n\n  let config = readConfig();\n  if (!config) {\n    config = await runSetup();\n  }\n\n  if (args.length === 0) {\n    console.error(\"Usage: tool-agent <prompt>\");\n    console.error(\"       tool-agent setup\");\n    process.exit(1);\n  }\n\n  const dry = args.includes(\"--dry\");\n  const promptArgs = args.filter((a) => a !== \"--dry\");\n  const prompt = promptArgs.join(\" \");\n\n  if (config.agent === \"claude-code\") {\n    const claudeArgs = [\"--plugin-dir\", config.pluginPath];\n    if (prompt) claudeArgs.unshift(`/tool-ui ${prompt}`);\n    if (dry) {\n      const cmd = [\"claude\", ...claudeArgs];\n      const escaped = cmd.map((a) => (a.includes(\" \") ? `\"${a}\"` : a));\n      console.log(escaped.join(\" \"));\n      return;\n    }\n\n    execFileSync(\"claude\", claudeArgs, { stdio: \"inherit\" });\n    console.log(\"\\nThanks for using tool-agent!\");\n  }\n}\n\nmain().catch((err) => {\n  console.error(err);\n  process.exit(1);\n});\n"
  },
  {
    "path": "packages/agent/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"esModuleInterop\": true,\n    \"strict\": true,\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\",\n    \"declaration\": true,\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/agent/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: [\"src/cli.ts\"],\n  format: [\"esm\"],\n  outDir: \"dist\",\n  outExtension: () => ({ js: \".mjs\" }),\n  banner: { js: \"#!/usr/bin/env node\" },\n  clean: true,\n});\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - \"apps/*\"\n  - \"packages/*\"\n\nignoredBuiltDependencies:\n  - esbuild\n  - unrs-resolver\n\nonlyBuiltDependencies:\n  - sharp\n"
  },
  {
    "path": "skills-lock.json",
    "content": "{\n  \"version\": 1,\n  \"skills\": {\n    \"tool-ui\": {\n      \"source\": \"assistant-ui/tool-ui\",\n      \"sourceType\": \"github\",\n      \"computedHash\": \"f8f3e057c61e35900008bd03c9c65df2377f62a3cf7b9574063d61ecf71fad69\"\n    }\n  }\n}\n"
  }
]