[
  {
    "path": ".changeset/README.md",
    "content": "# Changesets\n\nHello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works\nwith multi-package repos, or single-package repos to help you version and publish your code. You can\nfind the full documentation for it [in our repository](https://github.com/changesets/changesets)\n\nWe have a quick list of common questions to get you started engaging with this project in\n[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)\n"
  },
  {
    "path": ".changeset/config.json",
    "content": "{\n  \"$schema\": \"https://unpkg.com/@changesets/config@3.0.3/schema.json\",\n  \"changelog\": \"@changesets/cli/changelog\",\n  \"commit\": false,\n  \"fixed\": [],\n  \"linked\": [\n    [\n      \"react-grab\",\n      \"grab\",\n      \"@react-grab/claude-code\",\n      \"@react-grab/cursor\",\n      \"@react-grab/opencode\",\n      \"@react-grab/codex\",\n      \"@react-grab/gemini\",\n      \"@react-grab/amp\",\n      \"@react-grab/droid\",\n      \"@react-grab/copilot\",\n      \"@react-grab/cli\",\n      \"@react-grab/utils\",\n      \"@react-grab/relay\"\n    ]\n  ],\n  \"access\": \"public\",\n  \"baseBranch\": \"main\",\n  \"updateInternalDependencies\": \"patch\",\n  \"ignore\": [\n    \"@react-grab/website\",\n    \"@react-grab/web-extension\",\n    \"@react-grab/gym\",\n    \"@react-grab/design-system\"\n  ]\n}\n"
  },
  {
    "path": ".github/workflows/code-quality.yml",
    "content": "name: Code Quality\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install\n\n      - name: Lint\n        run: pnpm lint\n\n  typecheck:\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install\n\n      - name: Typecheck\n        run: pnpm typecheck\n"
  },
  {
    "path": ".github/workflows/publish-any-commit.yml",
    "content": "name: Publish Any Commit\non: [push, pull_request]\n\npermissions: {}\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - run: corepack enable\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: \"pnpm\"\n\n      - name: Install dependencies\n        run: pnpm install\n\n      - name: Build\n        run: pnpm build\n\n      - run: |\n          pnpm dlx pkg-pr-new publish \\\n            ./packages/react-grab \\\n            ./packages/cli \\\n            ./packages/grab \\\n            ./packages/relay \\\n            ./packages/utils \\\n            ./packages/provider-amp \\\n            ./packages/provider-claude-code \\\n            ./packages/provider-codex \\\n            ./packages/provider-cursor \\\n            ./packages/provider-copilot \\\n            ./packages/provider-droid \\\n            ./packages/provider-gemini \\\n            ./packages/provider-opencode\n"
  },
  {
    "path": ".github/workflows/pullfrog.yml",
    "content": "# PULLFROG ACTION — DO NOT EDIT EXCEPT WHERE INDICATED\nname: Pullfrog\nrun-name: ${{ inputs.name || github.workflow }}\non:\n  workflow_dispatch:\n    inputs:\n      prompt:\n        type: string\n        description: Agent prompt\n      name:\n        type: string\n        description: Run name\n\npermissions:\n  id-token: write\n  contents: write\n  pull-requests: write\n  issues: write\n  actions: read\n  checks: read\n\njobs:\n  pullfrog:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 1\n      - name: Run agent\n        uses: pullfrog/pullfrog@v0\n        with:\n          prompt: ${{ inputs.prompt }}\n        env:\n          # add any additional keys your agent(s) need\n          # optionally, comment out any you won't use\n          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\n          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\n          GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}\n          GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}\n          CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}\n          MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}\n          GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}\n          DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}\n          OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}\n"
  },
  {
    "path": ".github/workflows/test-build.yml",
    "content": "name: Test Build\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  test-build:\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install\n\n      - name: Build\n        run: pnpm build\n"
  },
  {
    "path": ".github/workflows/test-cli.yml",
    "content": "name: Test CLI\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  test-cli:\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install\n\n      - name: Build\n        run: pnpm build\n\n      - name: Run CLI tests\n        run: pnpm --filter @react-grab/cli test\n"
  },
  {
    "path": ".github/workflows/test-e2e.yml",
    "content": "name: Test E2E\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  test-e2e:\n    runs-on: ubuntu-latest\n    timeout-minutes: 30\n    strategy:\n      fail-fast: false\n      matrix:\n        shardIndex: [1, 2, 3, 4]\n        shardTotal: [4]\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install\n\n      - name: Install Playwright browsers\n        run: pnpm --filter react-grab exec playwright install chromium --with-deps\n\n      - name: Build\n        run: pnpm build\n\n      - name: Run E2E tests\n        run: pnpm --filter react-grab exec playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n.DS_Store\n.env\n.turbo\ndist\n.next\n**/*.tgz\ncoverage\nreact-grab-extension.zip\ntsup.config.bundled_*.mjs\npackages/website/public/script.js\ntest-results\nplaywright-report\n.vercel\n.cursor/debug.log\n.cursor\nmeta.json"
  },
  {
    "path": ".oxfmtrc.json",
    "content": "{\n  \"$schema\": \"./node_modules/oxfmt/configuration_schema.json\",\n  \"tabWidth\": 2,\n  \"singleQuote\": false,\n  \"printWidth\": 80,\n  \"ignorePatterns\": [\n    \".next\",\n    \"node_modules\",\n    \"dist\",\n    \"build\",\n    \".turbo\",\n    \"pnpm-lock.yaml\"\n  ]\n}\n"
  },
  {
    "path": "AGENTS.md",
    "content": "## General Rules\n\n- MUST: Use @antfu/ni. Use `ni` to install, `nr SCRIPT_NAME` to run. `nun` to uninstall.\n- MUST: Use TypeScript interfaces over types.\n- MUST: Keep all types in the global scope.\n- MUST: Use arrow functions over function declarations\n- MUST: Never comment unless absolutely necessary.\n  - If the code is a hack (like a setTimeout or potentially confusing code), it must be prefixed with // HACK: reason for hack\n  - Do not delete descriptive comments >3 lines without confirming with the user\n- MUST: Use kebab-case for files\n- MUST: Use descriptive names for variables (avoid shorthands, or 1-2 character names).\n  - Example: for .map(), you can use `innerX` instead of `x`\n  - Example: instead of `moved` use `didPositionChange`\n- MUST: Frequently re-evaluate and refactor variable names to be more accurate and descriptive.\n- MUST: Do not type cast (\"as\") unless absolutely necessary\n- MUST: Remove unused code and don't repeat yourself.\n- MUST: Always search the codebase, think of many solutions, then implement the most _elegant_ solution.\n- MUST: Put all magic numbers in `constants.ts` using `SCREAMING_SNAKE_CASE` with unit suffixes (`_MS`, `_PX`).\n- MUST: Put small, focused utility functions in `utils/` with one utility per file.\n- MUST: Use Boolean over !!.\n\n## SolidJS Rules\n\n### Mental Model\n\n- MUST: Treat components as setup functions that run ONCE, not render functions.\n- MUST: Place reactive work in primitives (`createMemo`, `createEffect`, `<Show>`, `<For>`), not component body.\n- MUST: Access signals only inside reactive contexts (JSX expressions, effects, memos).\n\n### Reactivity\n\n- MUST: Call signals as functions: `count()` not `count`.\n- MUST: Use functional updates when new state depends on old: `setCount((prev) => prev + 1)`.\n- MUST: Keep signals atomic (one per value) — one big state object loses granularity.\n- MUST: Use derived functions `() => count() * 2` for cheap/infrequent derivations.\n- MUST: Use `createMemo(() => ...)` for expensive/frequent derivations — caches result.\n- MUST: Use `createEffect` for side effects only (DOM, localStorage, subscriptions).\n- MUST: Call `onCleanup(() => ...)` inside effects for subscriptions/intervals/listeners.\n- MUST: Use path syntax for store updates: `setStore(\"users\", 0, \"name\", \"Jane\")`.\n- MUST: Wrap store props in arrow for `on()`: `on(() => store.value, fn)` not `on(store.value, fn)`.\n- SHOULD: Use `{ equals: false }` for trigger signals that always notify.\n- SHOULD: Use `batch(() => { ... })` when updating multiple signals outside event handlers.\n- SHOULD: Use `on(dep, fn)` for explicit effect dependencies.\n- SHOULD: Use `untrack(() => value())` to read without subscribing.\n- SHOULD: Use `createStore({ ... })` for nested objects with fine-grained reactivity.\n- SHOULD: Use `produce(draft => { ... })` for complex store mutations.\n- NEVER: Derive state via `createEffect(() => setX(y()))` — use memo or derived function.\n- NEVER: Place side effects inside `createMemo` — causes infinite loops/crashes.\n\n### Props\n\n- MUST: Access props via `props.title`, not destructuring.\n- SHOULD: Wrap in getter if needed: `const title = () => props.title`.\n- SHOULD: Use `splitProps(props, [\"keys\"])` to separate local from pass-through props.\n- SHOULD: Use `mergeProps(defaults, props)` for default values.\n- SHOULD: Use `children(() => props.children)` only when transforming, otherwise `{props.children}`.\n- NEVER: Destructure props `({ title })` — breaks reactivity.\n\n### Control Flow\n\n- MUST: Use `<For each={items()}>` for object arrays — item is value, index is signal.\n- MUST: Use `<Index each={items()}>` for primitives/inputs — item is signal, index is number.\n- MUST: Use `<Suspense fallback={...}>` for async, not `<Show when={!loading}>`.\n- MUST: Access resource states via `data()`, `data.loading`, `data.error`, `data.latest`.\n- SHOULD: Use `<Show when={cond()} fallback={...}>` for conditionals.\n- SHOULD: Use `<Show when={val}>` callback for type narrowing: `{(v) => <div>{v().name}</div>}`.\n- SHOULD: Use `<Switch>/<Match>` for multiple conditions.\n- SHOULD: Use `createResource(source, fetcher)` for reactive async data.\n- SHOULD: Use `<ErrorBoundary fallback={(err, reset) => ...}>` for render errors.\n- NEVER: Use `.map()` in JSX — use `<For>` or `<Index>`.\n- NEVER: Rely on ErrorBoundary for event handler or setTimeout errors — use try/catch.\n\n### JSX & DOM\n\n- MUST: Use `class` not `className`.\n- MUST: Combine static `class=\"btn\"` with reactive `classList={{ active: isActive() }}`.\n- MUST: Use `onClick` for delegated events; `on:click` for native (element-level).\n- MUST: Condition inside handler since events are not reactive: `onClick={() => props.onClick?.()}`.\n- MUST: Read refs in `onMount` or effects — refs connect after render.\n- MUST: Call `onCleanup` inside directives for cleanup.\n- SHOULD: Use `on:click` for `stopPropagation`, capture, passive, or custom events.\n- SHOULD: Use `style={{ color: color(), \"--css-var\": value() }}` for inline styles.\n- SHOULD: Type refs as `let el: HTMLElement | undefined` with guard.\n- SHOULD: Use `use:directiveName={accessor}` for reusable DOM behaviors.\n- NEVER: Mix reactive `class={x()}` with `classList`.\n\n## Testing\n\nRun dev `packages/cli` with:\n\n```bash\nnpm_command=exec node packages/cli/dist/cli.js\n```\n\nRun checks always before committing with:\n\n```bash\npnpm test # runs e2e tests\npnpm lint\npnpm typecheck # runs type checking\npnpm format\n```\n\n## Development instructions\n\nThis is a pnpm + Turborepo monorepo (19 packages under `packages/`). No external services (databases, Docker, etc.) are required.\n\n### Build before test\n\n`pnpm build` must complete before `pnpm test` or `pnpm lint` — Turborepo `dependsOn` enforces this, but be aware that `pnpm test` will rebuild if the build cache is cold. After modifying source files, always rebuild before running tests.\n\n### Approved build scripts\n\nThe root `package.json` has `pnpm.onlyBuiltDependencies` configured for `@parcel/watcher`, `esbuild`, `sharp`, `spawn-sync`, and `unrs-resolver`. Without this, `pnpm install` silently skips their native builds and downstream packages may fail.\n\n### Playwright\n\nE2E tests (`pnpm test` at root) run Playwright against the `e2e-playground` Vite dev server on port 5175 (auto-started by the Playwright config). Chromium must be installed: `npx --prefix packages/react-grab playwright install chromium --with-deps`.\n\n### Key commands reference\n\nSee root `package.json` scripts and `CONTRIBUTING.md` for the full list. Quick reference:\n\n- **Install**: `ni` (or `pnpm install`)\n- **Build**: `nr build` (or `pnpm build`)\n- **Dev watch**: `nr dev` (or `pnpm dev`) — watches core packages\n- **Test**: `pnpm test` — runs Playwright E2E + Vitest CLI tests\n- **Lint**: `pnpm lint` — oxlint on react-grab package\n- **Typecheck**: `pnpm typecheck` — tsc on react-grab package\n- **Format**: `pnpm format` — oxfmt\n- **CLI dev**: `npm_command=exec node packages/cli/dist/cli.js`\n- **E2E playground**: `pnpm --filter @react-grab/e2e-playground dev` (port 5175)\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to React Grab\n\nThanks for your interest in contributing to React Grab! This document provides guidelines and instructions for contributing.\n\n## Getting Started\n\n### Prerequisites\n\n- Node.js >= 18\n- pnpm >= 8\n\n### Setup\n\n1. Fork and clone the repository:\n\n```bash\ngit clone https://github.com/YOUR_USERNAME/react-grab.git\ncd react-grab\n```\n\n2. Install dependencies using [@antfu/ni](https://github.com/antfu/ni):\n\n```bash\nni\n```\n\n3. Build all packages:\n\n```bash\nnr build\n```\n\n4. Start development mode:\n\n```bash\nnr dev\n```\n\n## Project Structure\n\n```\npackages/\n├── react-grab/          # Core library\n├── grab/                # Bundled package (library + CLI, published as `grab`)\n├── cli/                 # CLI implementation (@react-grab/cli)\n├── provider-cursor/     # Cursor agent integration\n├── provider-claude-code/  # Claude Code integration\n├── provider-opencode/   # OpenCode integration\n├── provider-codex/      # OpenAI Codex integration\n├── provider-gemini/     # Google Gemini CLI integration\n├── provider-amp/        # Amp SDK integration\n├── provider-droid/      # Droid integration\n├── provider-copilot/    # Copilot integration\n├── website/             # Documentation site (react-grab.com)\n├── e2e-playground/      # E2E test target app\n├── gym/                 # Agent testing playground\n└── web-extension/       # Browser extension\n```\n\n## Development Workflow\n\n### Running the Gym\n\nTest agent provider integrations in the gym:\n\n```bash\npnpm --filter @react-grab/gym dev:claude   # Claude Code\npnpm --filter @react-grab/gym dev:cursor   # Cursor\npnpm --filter @react-grab/gym dev:opencode # OpenCode\npnpm --filter @react-grab/gym dev:codex    # Codex\npnpm --filter @react-grab/gym dev:gemini   # Gemini\npnpm --filter @react-grab/gym dev:amp      # Amp\npnpm --filter @react-grab/gym dev:droid    # Droid\npnpm --filter @react-grab/gym dev:copilot  # Copilot\n```\n\nThe gym runs at `http://localhost:5174` and lets you test react-grab's agent provider API with multiple backends.\n\n### Running Tests\n\n```bash\n# Run CLI tests\npnpm --filter @react-grab/cli test\n```\n\n### Linting & Formatting\n\n```bash\nnr lint        # Check for lint errors\nnr lint:fix    # Fix lint errors\nnr format      # Format code with oxfmt\n```\n\n## Code Style\n\n- **Use TypeScript interfaces** over types\n- **Use arrow functions** over function declarations\n- **Use kebab-case** for file names\n- **Use descriptive variable names** — avoid shorthands or 1-2 character names\n  - Example: `innerElement` instead of `el`\n  - Example: `didPositionChange` instead of `moved`\n- **Avoid type casting** (`as`) unless absolutely necessary\n- **Keep interfaces/types** at the global scope\n- **Remove unused code** and follow DRY principles\n- **Avoid comments** unless absolutely necessary\n  - If a hack is required, prefix with `// HACK: reason for hack`\n\n## Submitting Changes\n\n### Creating a Pull Request\n\n1. Create a new branch:\n\n```bash\ngit checkout -b feat/your-feature-name\n```\n\n2. Make your changes and commit with a descriptive message:\n\n```bash\ngit commit -m \"feat: add new feature\"\n```\n\n3. Push to your fork and open a pull request\n\n### Commit Convention\n\nWe use conventional commits:\n\n- `feat:` — New feature\n- `fix:` — Bug fix\n- `docs:` — Documentation changes\n- `chore:` — Maintenance tasks\n- `refactor:` — Code refactoring\n- `test:` — Test additions or changes\n\n### Adding a Changeset\n\nFor changes that affect published packages, add a changeset:\n\n```bash\nnr changeset\n```\n\nFollow the prompts to describe your changes. This helps maintain accurate changelogs.\n\n## Reporting Issues\n\nFound a bug? Have a feature request? [Open an issue](https://github.com/aidenybai/react-grab/issues) with:\n\n- Clear description of the problem or request\n- Steps to reproduce (for bugs)\n- Expected vs actual behavior\n- Environment details (OS, browser, Node version)\n\n## Community\n\n- Join our [Discord](https://discord.com/invite/G7zxfUzkm7) to discuss ideas and get help\n- Check existing [issues](https://github.com/aidenybai/react-grab/issues) before opening new ones\n\n## License\n\nBy contributing, you agree that your contributions will be licensed under the MIT License.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Aiden Bai\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": "# <img src=\"https://github.com/aidenybai/react-grab/blob/main/.github/public/logo.png?raw=true\" width=\"60\" align=\"center\" /> React Grab\n\n[![size](https://img.shields.io/bundlephobia/minzip/react-grab?label=gzip&style=flat&colorA=000000&colorB=000000)](https://bundlephobia.com/package/react-grab)\n[![version](https://img.shields.io/npm/v/react-grab?style=flat&colorA=000000&colorB=000000)](https://npmjs.com/package/react-grab)\n[![downloads](https://img.shields.io/npm/dt/react-grab.svg?style=flat&colorA=000000&colorB=000000)](https://npmjs.com/package/react-grab)\n\nSelect context for coding agents directly from your website\n\nHow? Point at any element and press **⌘C** (Mac) or **Ctrl+C** (Windows/Linux) to copy the file name, React component, and HTML source code.\n\nIt makes tools like Cursor, Claude Code, Copilot run up to [**3× faster**](https://react-grab.com/blog/intro) and more accurate.\n\n### [Try out a demo! →](https://react-grab.com)\n\n![React Grab Demo](https://github.com/aidenybai/react-grab/blob/main/packages/website/public/demo.gif?raw=true)\n\n## Install\n\nRun this command at your project root (where `next.config.ts` or `vite.config.ts` is located):\n\n```bash\nnpx -y grab@latest init\n```\n\n## Connect to MCP\n\n```bash\nnpx -y grab@latest add mcp\n```\n\n## Usage\n\nOnce installed, hover over any UI element in your browser and press:\n\n- **⌘C** (Cmd+C) on Mac\n- **Ctrl+C** on Windows/Linux\n\nThis copies the element's context (file name, React component, and HTML source code) to your clipboard ready to paste into your coding agent. For example:\n\n```js\n<a class=\"ml-auto inline-block text-sm\" href=\"#\">\n  Forgot your password?\n</a>\nin LoginForm at components/login-form.tsx:46:19\n```\n\n## Manual Installation\n\nIf you're using a React framework or build tool, view instructions below:\n\n#### Next.js (App router)\n\nAdd this inside of your `app/layout.tsx`:\n\n```jsx\nimport Script from \"next/script\";\n\nexport default function RootLayout({ children }) {\n  return (\n    <html>\n      <head>\n        {process.env.NODE_ENV === \"development\" && (\n          <Script\n            src=\"//unpkg.com/react-grab/dist/index.global.js\"\n            crossOrigin=\"anonymous\"\n            strategy=\"beforeInteractive\"\n          />\n        )}\n      </head>\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\n#### Next.js (Pages router)\n\nAdd this into your `pages/_document.tsx`:\n\n```jsx\nimport { Html, Head, Main, NextScript } from \"next/document\";\n\nexport default function Document() {\n  return (\n    <Html lang=\"en\">\n      <Head>\n        {process.env.NODE_ENV === \"development\" && (\n          <Script\n            src=\"//unpkg.com/react-grab/dist/index.global.js\"\n            crossOrigin=\"anonymous\"\n            strategy=\"beforeInteractive\"\n          />\n        )}\n      </Head>\n      <body>\n        <Main />\n        <NextScript />\n      </body>\n    </Html>\n  );\n}\n```\n\n#### Vite\n\nAdd this at the top of your main entry file (e.g., `src/main.tsx`):\n\n```tsx\nif (import.meta.env.DEV) {\n  import(\"react-grab\");\n}\n```\n\n#### Webpack\n\nFirst, install React Grab:\n\n```bash\nnpm install react-grab\n```\n\nThen add this at the top of your main entry file (e.g., `src/index.tsx` or `src/main.tsx`):\n\n```tsx\nif (process.env.NODE_ENV === \"development\") {\n  import(\"react-grab\");\n}\n```\n\n## Plugins\n\nUse plugins to extend React Grab's built-in UI with context menu actions, toolbar menu items, lifecycle hooks, and theme overrides. Plugins run within React Grab.\n\nRegister a plugin using the `registerPlugin` and `unregisterPlugin` exports:\n\n```js\nimport { registerPlugin } from \"react-grab\";\n\nregisterPlugin({\n  name: \"my-plugin\",\n  hooks: {\n    onElementSelect: (element) => {\n      console.log(\"Selected:\", element.tagName);\n    },\n  },\n});\n```\n\nIn React, register inside a `useEffect`:\n\n```jsx\nimport { registerPlugin, unregisterPlugin } from \"react-grab\";\n\nuseEffect(() => {\n  registerPlugin({\n    name: \"my-plugin\",\n    actions: [\n      {\n        id: \"my-action\",\n        label: \"My Action\",\n        shortcut: \"M\",\n        onAction: (context) => {\n          console.log(\"Action on:\", context.element);\n          context.hideContextMenu();\n        },\n      },\n    ],\n  });\n\n  return () => unregisterPlugin(\"my-plugin\");\n}, []);\n```\n\nActions use a `target` field to control where they appear. Omit `target` (or set `\"context-menu\"`) for the right-click menu, or set `\"toolbar\"` for the toolbar dropdown:\n\n```js\nactions: [\n  {\n    id: \"inspect\",\n    label: \"Inspect\",\n    shortcut: \"I\",\n    onAction: (ctx) => console.dir(ctx.element),\n  },\n  {\n    id: \"toggle-freeze\",\n    label: \"Freeze\",\n    target: \"toolbar\",\n    isActive: () => isFrozen,\n    onAction: () => toggleFreeze(),\n  },\n];\n```\n\nSee [`packages/react-grab/src/types.ts`](https://github.com/aidenybai/react-grab/blob/main/packages/react-grab/src/types.ts) for the full `Plugin`, `PluginHooks`, and `PluginConfig` interfaces.\n\n## Resources & Contributing Back\n\nWant to try it out? Check out [our demo](https://react-grab.com).\n\nLooking to contribute back? Check out the [Contributing Guide](https://github.com/aidenybai/react-grab/blob/main/CONTRIBUTING.md).\n\nWant to talk to the community? Hop in our [Discord](https://discord.com/invite/G7zxfUzkm7) and share your ideas and what you've built with React Grab.\n\nFind a bug? Head over to our [issue tracker](https://github.com/aidenybai/react-grab/issues) and we'll do our best to help. We love pull requests, too!\n\nWe expect all contributors to abide by the terms of our [Code of Conduct](https://github.com/aidenybai/react-grab/blob/main/.github/CODE_OF_CONDUCT.md).\n\n[**→ Start contributing on GitHub**](https://github.com/aidenybai/react-grab/blob/main/CONTRIBUTING.md)\n\n### License\n\nReact Grab is MIT-licensed open-source software.\n\n_Thank you to [Andrew Luetgers](https://github.com/andrewluetgers) for donating the `grab` npm package name._\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"private\": true,\n  \"workspaces\": [\n    \"packages/*\"\n  ],\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"cp README.md packages/react-grab/README.md && turbo run build --filter=react-grab --filter=@react-grab/cursor --filter=@react-grab/claude-code --filter=@react-grab/opencode --filter=@react-grab/codex --filter=@react-grab/gemini --filter=@react-grab/amp --filter=@react-grab/droid --filter=@react-grab/copilot --filter=@react-grab/cli --filter=@react-grab/utils --filter=@react-grab/shadcn-registry && pnpm --filter grab build\",\n    \"dev\": \"turbo dev --filter=react-grab --filter=@react-grab/cursor --filter=@react-grab/claude-code --filter=@react-grab/opencode --filter=@react-grab/codex --filter=@react-grab/gemini --filter=@react-grab/amp --filter=@react-grab/droid --filter=@react-grab/copilot --filter=@react-grab/cli --filter=@react-grab/utils\",\n    \"test\": \"turbo run test --filter=react-grab --filter=@react-grab/cli\",\n    \"typecheck\": \"pnpm --filter react-grab typecheck\",\n    \"lint\": \"pnpm --filter react-grab lint\",\n    \"lint:fix\": \"pnpm --filter react-grab lint:fix\",\n    \"format\": \"oxfmt\",\n    \"extension:dev\": \"pnpm --filter web-extension dev\",\n    \"extension:build\": \"pnpm --filter web-extension build\",\n    \"changeset\": \"changeset\",\n    \"version\": \"changeset version\",\n    \"release\": \"pnpm build && changeset publish\"\n  },\n  \"devDependencies\": {\n    \"@changesets/cli\": \"^2.27.10\",\n    \"oxfmt\": \"^0.27.0\",\n    \"turbo\": \"^2.6.3\",\n    \"typescript\": \"^5.9.3\"\n  },\n  \"engines\": {\n    \"node\": \">=18\",\n    \"pnpm\": \">=8\"\n  },\n  \"packageManager\": \"pnpm@10.24.0\",\n  \"pnpm\": {\n    \"onlyBuiltDependencies\": [\n      \"@parcel/watcher\",\n      \"esbuild\",\n      \"sharp\",\n      \"spawn-sync\",\n      \"unrs-resolver\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/cli/CHANGELOG.md",
    "content": "# @react-grab/cli\n\n## 0.1.28\n\n### Patch Changes\n\n- fix\n\n## 0.1.27\n\n### Patch Changes\n\n- fix: install instructions\n\n## 0.1.26\n\n### Patch Changes\n\n- fix: minor tweaks\n\n## 0.1.25\n\n### Patch Changes\n\n- fix: primtiives\n\n## 0.1.24\n\n### Patch Changes\n\n- primitives\n\n## 0.1.23\n\n### Patch Changes\n\n- fix: npx command\n\n## 0.1.22\n\n### Patch Changes\n\n- fix: freezing\n\n## 0.1.21\n\n### Patch Changes\n\n- fix: up and down selection\n\n## 0.1.20\n\n### Patch Changes\n\n- fix: selection performacne\n\n## 0.1.19\n\n### Patch Changes\n\n- fix: gsap\n\n## 0.1.18\n\n### Patch Changes\n\n- fix: minor issues\n\n## 0.1.17\n\n### Patch Changes\n\n- fix: mcp\n\n## 0.1.16\n\n### Patch Changes\n\n- fix: environment detection\n\n## 0.1.15\n\n### Patch Changes\n\n- fix: animations and ux\n\n## 0.1.14\n\n### Patch Changes\n\n- fix: improve recent UX\n\n## 0.1.13\n\n### Patch Changes\n\n- fix MCP client injection\n\n## 0.1.12\n\n### Patch Changes\n\n- feat: MCP\n\n## 0.1.11\n\n### Patch Changes\n\n- fix: claude code provider\n\n## 0.1.10\n\n### Patch Changes\n\n- feat: cdn in cli\n\n## 0.1.9\n\n### Patch Changes\n\n- fix: startServer not exported in providers\n\n## 0.1.8\n\n### Patch Changes\n\n- fix: providers not detaching\n\n## 0.1.7\n\n### Patch Changes\n\n- fix: react freezing safety\n\n## 0.1.6\n\n### Patch Changes\n\n- fix: compat with React Scan\n\n## 0.1.5\n\n### Patch Changes\n\n- fix: fullscreen inset the root\n\n## 0.1.4\n\n### Patch Changes\n\n- fix: improve cli edge cases\n\n## 0.1.3\n\n### Patch Changes\n\n- fix: @react-grab/utils not publishing\n\n## 0.1.2\n\n### Patch Changes\n\n- fix: packages not being published\n\n## 0.1.1\n\n### Patch Changes\n\n- fix: clicking element after keyboard navigation\n\n## 0.1.0\n\n### Minor Changes\n\n- 81adb50: feat: browser\n\n### Patch Changes\n\n- 81adb50: fix: shell script\n- fb2b037: fix: cli\n- a3d5a94: fix: cli global install\n- 81adb50: feat: react support\n- 81adb50: fix: a11y\n- a5e7a6a: fix: optimize loading speed of cli\n- 90af3f6: fix: CLI hanging\n- 81adb50: fix: shell script\n- 78efee2: fix: cli\n- 074e593: fix: cli\n- 5cd3709: fix: decouple browser out from react-grab\n- 54c4867: ui improvements\n\n## 0.1.0-beta.13\n\n### Patch Changes\n\n- ui improvements\n\n## 0.1.0-beta.12\n\n### Patch Changes\n\n- fix: decouple browser out from react-grab\n\n## 0.1.0-beta.11\n\n### Patch Changes\n\n- fix: cli global install\n- Updated dependencies\n  - @react-grab/browser@0.1.0-beta.11\n\n## 0.1.0-beta.10\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - @react-grab/browser@0.1.0-beta.10\n\n## 0.1.0-beta.9\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - @react-grab/browser@0.1.0-beta.9\n\n## 0.1.0-beta.8\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - @react-grab/browser@0.1.0-beta.8\n\n## 0.1.0-beta.7\n\n### Patch Changes\n\n- fix: CLI hanging\n- Updated dependencies\n  - @react-grab/browser@0.1.0-beta.7\n\n## 0.1.0-beta.6\n\n### Patch Changes\n\n- fix: optimize loading speed of cli\n- Updated dependencies\n  - @react-grab/browser@0.1.0-beta.6\n\n## 0.1.0-beta.5\n\n### Patch Changes\n\n- fix: a11y\n- Updated dependencies\n  - @react-grab/browser@0.1.0-beta.5\n\n## 0.1.0-beta.4\n\n### Patch Changes\n\n- feat: react support\n- Updated dependencies\n  - @react-grab/browser@0.1.0-beta.4\n\n## 0.1.0-beta.2\n\n### Patch Changes\n\n- fix: shell script\n- Updated dependencies\n  - @react-grab/browser@0.1.0-beta.2\n\n## 0.1.0-beta.1\n\n### Patch Changes\n\n- fix: shell script\n- Updated dependencies\n  - @react-grab/browser@0.1.0-beta.1\n\n## 0.1.0-beta.0\n\n### Minor Changes\n\n- feat: browser\n\n### Patch Changes\n\n- Updated dependencies\n  - @react-grab/browser@0.1.0-beta.0\n\n## 0.0.98\n\n### Patch Changes\n\n- feat: new state architecture and context menu\n\n## 0.0.97\n\n### Patch Changes\n\n- fix: sourcemap error\n\n## 0.0.96\n\n### Patch Changes\n\n- fix: fiber access timeout handling\n\n## 0.0.95\n\n### Patch Changes\n\n- fix: selecting buttons with disabled states\n\n## 0.0.94\n\n### Patch Changes\n\n- fix: browser crashing on selection bug\n\n## 0.0.93\n\n### Patch Changes\n\n- fix: copying not working\n\n## 0.0.92\n\n### Patch Changes\n\n- refactor: use state machines instead of signals\n\n## 0.0.91\n\n### Patch Changes\n\n- feat: dock\n\n## 0.0.90\n\n### Patch Changes\n\n- fix: check visual edit endpoint\n\n## 0.0.89\n\n### Patch Changes\n\n- fix: many bugfixes\n\n## 0.0.88\n\n### Patch Changes\n\n- fix: deprecation errors\n\n## 0.0.87\n\n### Patch Changes\n\n- feat: visual edits\n\n## 0.0.86\n\n### Patch Changes\n\n- fix: editing\n\n## 0.0.85\n\n### Patch Changes\n\n- fix: check versions on each provider\n\n## 0.0.84\n\n### Patch Changes\n\n- fix: migrate from cross-spawn to execa to fix deprecation issue\n\n## 0.0.83\n\n### Patch Changes\n\n- feat: timings during agent processing\n\n## 0.0.82\n\n### Patch Changes\n\n- fix: agent support\n\n## 0.0.81\n\n### Patch Changes\n\n- feat: codex and gemini support\n\n## 0.0.80\n\n### Patch Changes\n\n- fix: replies and undo\n\n## 0.0.79\n\n### Patch Changes\n\n- fix: claude code exit issue\n\n## 0.0.78\n\n### Patch Changes\n\n- fix: cancel animation\n\n## 0.0.77\n\n### Patch Changes\n\n- fix: new cli proxying\n\n## 0.0.76\n\n### Patch Changes\n\n- feat: allow CLI under react-grab namespace\n\n## 0.0.75\n\n### Patch Changes\n\n- fix: issue with Illegal Invocation on next.js pages\n\n## 0.0.74\n\n### Patch Changes\n\n- fix: updateOptions\n\n## 0.0.73\n\n### Patch Changes\n\n- fix: improve cli\n\n## 0.0.72\n\n### Patch Changes\n\n- fix: shimmer effect\n\n## 0.0.71\n\n### Patch Changes\n\n- fix: ux nits\n\n## 0.0.70\n\n### Patch Changes\n\n- fix: react-grab cli flow when agents is used\n\n## 0.0.69\n\n### Patch Changes\n\n- fix: CLI on script tag\n\n## 0.0.68\n\n### Patch Changes\n\n- feat: opencode and cli installer\n"
  },
  {
    "path": "packages/cli/README.md",
    "content": "# @react-grab/cli\n\nInteractive CLI to install React Grab in your project.\n\n## Usage\n\n```bash\nnpx grab\n```\n\n### Interactive Mode (default)\n\nRunning without options starts the interactive wizard:\n\n```bash\nnpx grab\n```\n\n### Non-Interactive Mode\n\nPass options to skip prompts:\n\n```bash\n# Auto-detect everything and install without prompts\nnpx grab -y\n\n# Specify framework\nnpx grab -f next -r app -y\n\n# Use specific package manager\nnpx grab -p pnpm -y\n```\n\n## Options\n\n| Option              | Alias | Description                                   | Choices                      |\n| ------------------- | ----- | --------------------------------------------- | ---------------------------- |\n| `--framework`       | `-f`  | Framework to configure                        | `next`, `vite`, `webpack`    |\n| `--package-manager` | `-p`  | Package manager to use                        | `npm`, `yarn`, `pnpm`, `bun` |\n| `--router`          | `-r`  | Next.js router type                           | `app`, `pages`               |\n| `--yes`             | `-y`  | Skip all confirmation prompts                 | -                            |\n| `--skip-install`    | -     | Skip package installation (only modify files) | -                            |\n| `--help`            | `-h`  | Show help                                     | -                            |\n| `--version`         | `-v`  | Show version                                  | -                            |\n\n## Examples\n\n```bash\n# Interactive setup\nnpx grab\n\n# Quick install with auto-detection\nnpx grab -y\n\n# Next.js App Router\nnpx grab -f next -r app -y\n\n# Vite with pnpm\nnpx grab -f vite -p pnpm -y\n\n# Only modify files (skip npm install)\nnpx grab --skip-install -y\n```\n\n## Supported Frameworks\n\n| Framework              | File Modified                     |\n| ---------------------- | --------------------------------- |\n| Next.js (App Router)   | `app/layout.tsx`                  |\n| Next.js (Pages Router) | `pages/_document.tsx`             |\n| Vite                   | `index.html`                      |\n| Webpack                | `src/index.tsx` or `src/main.tsx` |\n\n## Manual Installation\n\nIf the CLI doesn't work for your setup, visit the docs:\n\nhttps://react-grab.com/docs\n"
  },
  {
    "path": "packages/cli/package.json",
    "content": "{\n  \"name\": \"@react-grab/cli\",\n  \"version\": \"0.1.28\",\n  \"bin\": {\n    \"react-grab\": \"./dist/cli.js\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/cli.d.ts\",\n      \"import\": \"./dist/cli.js\",\n      \"require\": \"./dist/cli.cjs\"\n    }\n  },\n  \"scripts\": {\n    \"dev\": \"tsup --watch\",\n    \"build\": \"rm -rf dist && NODE_ENV=production tsup\",\n    \"test\": \"vitest run\",\n    \"test:watch\": \"vitest\"\n  },\n  \"dependencies\": {\n    \"@antfu/ni\": \"^0.23.0\",\n    \"commander\": \"^14.0.0\",\n    \"ignore\": \"^7.0.5\",\n    \"jsonc-parser\": \"^3.3.1\",\n    \"ora\": \"^8.2.0\",\n    \"picocolors\": \"^1.1.1\",\n    \"prompts\": \"^2.4.2\",\n    \"smol-toml\": \"^1.6.0\"\n  },\n  \"devDependencies\": {\n    \"@types/prompts\": \"^2.4.9\",\n    \"tsup\": \"^8.4.0\",\n    \"vitest\": \"^3.2.4\"\n  }\n}\n"
  },
  {
    "path": "packages/cli/src/cli.ts",
    "content": "import { Command } from \"commander\";\nimport { add } from \"./commands/add.js\";\nimport { configure } from \"./commands/configure.js\";\nimport { init } from \"./commands/init.js\";\nimport { remove } from \"./commands/remove.js\";\n\nconst VERSION = process.env.VERSION ?? \"0.0.1\";\nconst VERSION_API_URL = \"https://www.react-grab.com/api/version\";\n\nprocess.on(\"SIGINT\", () => process.exit(0));\nprocess.on(\"SIGTERM\", () => process.exit(0));\n\ntry {\n  fetch(`${VERSION_API_URL}?source=cli&t=${Date.now()}`).catch(() => {});\n} catch {}\n\nconst program = new Command()\n  .name(\"grab\")\n  .description(\"add React Grab to your project\")\n  .version(VERSION, \"-v, --version\", \"display the version number\");\n\nprogram.addCommand(init);\nprogram.addCommand(add);\nprogram.addCommand(remove);\nprogram.addCommand(configure);\n\nconst main = async () => {\n  await program.parseAsync();\n};\n\nmain();\n"
  },
  {
    "path": "packages/cli/src/commands/add.ts",
    "content": "import { Command } from \"commander\";\nimport pc from \"picocolors\";\nimport { detectNonInteractive } from \"../utils/is-non-interactive.js\";\nimport { prompts } from \"../utils/prompts.js\";\nimport { detectProject } from \"../utils/detect.js\";\nimport { printDiff } from \"../utils/diff.js\";\nimport { handleError } from \"../utils/handle-error.js\";\nimport { highlighter } from \"../utils/highlighter.js\";\nimport {\n  getPackagesToInstall,\n  getPackagesToUninstall,\n  installPackages,\n  uninstallPackages,\n} from \"../utils/install.js\";\nimport {\n  installMcpServers,\n  promptConnectionMode,\n  promptMcpInstall,\n} from \"../utils/install-mcp.js\";\nimport { logger } from \"../utils/logger.js\";\nimport { spinner } from \"../utils/spinner.js\";\nimport {\n  AGENTS,\n  AGENT_NAMES,\n  type Agent,\n  type AgentIntegration,\n  getAgentDisplayName,\n} from \"../utils/templates.js\";\nimport {\n  applyPackageJsonTransform,\n  applyTransform,\n  previewAgentRemoval,\n  previewPackageJsonAgentRemoval,\n  previewPackageJsonTransform,\n  previewTransform,\n} from \"../utils/transform.js\";\n\nconst VERSION = process.env.VERSION ?? \"0.0.1\";\n\nconst formatInstalledAgentNames = (agents: string[]): string =>\n  agents.map((agent) => AGENT_NAMES[agent as Agent] || agent).join(\", \");\n\nexport const add = new Command()\n  .name(\"add\")\n  .alias(\"install\")\n  .description(\"connect React Grab to your agent\")\n  .argument(\"[agent]\", `agent to connect (${AGENTS.join(\", \")}, mcp)`)\n  .option(\"-y, --yes\", \"skip confirmation prompts\", false)\n  .option(\n    \"-c, --cwd <cwd>\",\n    \"working directory (defaults to current directory)\",\n    process.cwd(),\n  )\n  .action(async (agentArg, opts) => {\n    console.log(\n      `${pc.magenta(\"✿\")} ${pc.bold(\"React Grab\")} ${pc.gray(VERSION)}`,\n    );\n    console.log();\n\n    try {\n      const cwd = opts.cwd;\n      const isNonInteractive = detectNonInteractive(opts.yes);\n\n      const preflightSpinner = spinner(\"Preflight checks.\").start();\n\n      const projectInfo = await detectProject(cwd);\n\n      if (!projectInfo.hasReactGrab) {\n        preflightSpinner.fail(\"React Grab is not installed.\");\n        logger.break();\n        logger.error(\n          `Run ${highlighter.info(\"react-grab init\")} first to install React Grab.`,\n        );\n        logger.break();\n        process.exit(1);\n      }\n\n      preflightSpinner.succeed();\n\n      const availableAgents = AGENTS.filter(\n        (agent) => !projectInfo.installedAgents.includes(agent),\n      );\n\n      if (availableAgents.length === 0 && isNonInteractive && !agentArg) {\n        logger.break();\n        logger.success(\"All legacy agents are already installed.\");\n        logger.log(\"Run without -y to add MCP.\");\n        logger.break();\n        process.exit(0);\n      }\n\n      let agentIntegration: AgentIntegration;\n      let agentsToRemove: Agent[] = [];\n\n      if (agentArg === \"mcp\") {\n        if (isNonInteractive) {\n          const results = installMcpServers();\n          const hasSuccess = results.some((result) => result.success);\n          if (!hasSuccess) {\n            logger.break();\n            logger.error(\"Failed to install MCP server.\");\n            logger.break();\n            process.exit(1);\n          }\n        } else {\n          const didInstall = await promptMcpInstall();\n          if (!didInstall) {\n            logger.break();\n            process.exit(0);\n          }\n        }\n        logger.break();\n        logger.log(\n          `${highlighter.success(\"Success!\")} MCP server has been configured.`,\n        );\n        logger.log(\"Restart your agents to activate.\");\n        logger.break();\n        agentIntegration = \"mcp\";\n        projectInfo.installedAgents = [...projectInfo.installedAgents, \"mcp\"];\n      } else if (agentArg) {\n        if (!AGENTS.includes(agentArg as (typeof AGENTS)[number])) {\n          logger.break();\n          logger.error(`Invalid agent: ${agentArg}`);\n          logger.error(`Available agents: ${AGENTS.join(\", \")}, mcp`);\n          logger.break();\n          process.exit(1);\n        }\n\n        const validAgent = agentArg as Agent;\n\n        if (projectInfo.installedAgents.includes(validAgent)) {\n          logger.break();\n          logger.warn(`${AGENT_NAMES[validAgent]} is already installed.`);\n          logger.break();\n          process.exit(0);\n        }\n\n        agentIntegration = validAgent;\n\n        if (projectInfo.installedAgents.length > 0 && !isNonInteractive) {\n          const installedNames = formatInstalledAgentNames(\n            projectInfo.installedAgents,\n          );\n\n          logger.break();\n          logger.warn(`${installedNames} is already installed.`);\n\n          const { action } = await prompts({\n            type: \"select\",\n            name: \"action\",\n            message: \"How would you like to proceed?\",\n            choices: [\n              {\n                title: `Replace with ${getAgentDisplayName(agentIntegration)}`,\n                value: \"replace\",\n              },\n              {\n                title: `Add ${getAgentDisplayName(agentIntegration)} alongside existing`,\n                value: \"add\",\n              },\n              { title: \"Cancel\", value: \"cancel\" },\n            ],\n          });\n\n          if (!action || action === \"cancel\") {\n            logger.break();\n            logger.log(\"Changes cancelled.\");\n            logger.break();\n            process.exit(0);\n          }\n\n          if (action === \"replace\") {\n            agentsToRemove = [...projectInfo.installedAgents] as Agent[];\n          }\n        }\n      } else if (!isNonInteractive) {\n        if (projectInfo.installedAgents.length > 0) {\n          const installedNames = formatInstalledAgentNames(\n            projectInfo.installedAgents,\n          );\n          logger.warn(`Currently installed: ${installedNames}`);\n          logger.break();\n        }\n\n        const connectionMode = await promptConnectionMode();\n\n        if (connectionMode === undefined) {\n          logger.break();\n          process.exit(1);\n        }\n\n        if (connectionMode === \"mcp\") {\n          const didInstall = await promptMcpInstall();\n          if (!didInstall) {\n            logger.break();\n            process.exit(0);\n          }\n          logger.break();\n          logger.log(\n            `${highlighter.success(\"Success!\")} MCP server has been configured.`,\n          );\n          logger.log(\"Restart your agents to activate.\");\n          logger.break();\n          agentIntegration = \"mcp\";\n          projectInfo.installedAgents = [...projectInfo.installedAgents, \"mcp\"];\n        } else {\n          const { agent } = await prompts({\n            type: \"select\",\n            name: \"agent\",\n            message: `Which ${highlighter.info(\"agent\")} would you like to connect?`,\n            choices: [\n              ...availableAgents.map((availableAgent) => ({\n                title: AGENT_NAMES[availableAgent],\n                value: availableAgent,\n              })),\n              { title: \"Skip\", value: \"skip\" },\n            ],\n          });\n\n          if (!agent || agent === \"skip\") {\n            logger.break();\n            process.exit(0);\n          }\n\n          agentIntegration = agent as AgentIntegration;\n\n          if (projectInfo.installedAgents.length > 0) {\n            const installedNames = formatInstalledAgentNames(\n              projectInfo.installedAgents,\n            );\n\n            const { action } = await prompts({\n              type: \"select\",\n              name: \"action\",\n              message: \"How would you like to proceed?\",\n              choices: [\n                {\n                  title: `Replace ${installedNames} with ${getAgentDisplayName(agentIntegration)}`,\n                  value: \"replace\",\n                },\n                {\n                  title: `Add ${getAgentDisplayName(agentIntegration)} alongside existing`,\n                  value: \"add\",\n                },\n                { title: \"Cancel\", value: \"cancel\" },\n              ],\n            });\n\n            if (!action || action === \"cancel\") {\n              logger.break();\n              logger.log(\"Changes cancelled.\");\n              logger.break();\n              process.exit(0);\n            }\n\n            if (action === \"replace\") {\n              agentsToRemove = [...projectInfo.installedAgents] as Agent[];\n            }\n          }\n        }\n      } else {\n        logger.break();\n        logger.error(\"Please specify an agent to connect.\");\n        logger.error(\"Available agents: \" + availableAgents.join(\", \"));\n        logger.break();\n        process.exit(1);\n      }\n\n      if (agentsToRemove.length > 0) {\n        for (const agentToRemove of agentsToRemove) {\n          const removalResult = previewAgentRemoval(\n            projectInfo.projectRoot,\n            projectInfo.framework,\n            projectInfo.nextRouterType,\n            agentToRemove,\n          );\n\n          const removalPackageJsonResult = previewPackageJsonAgentRemoval(\n            projectInfo.projectRoot,\n            agentToRemove,\n          );\n\n          const packagesToRemove = getPackagesToUninstall(agentToRemove);\n\n          if (packagesToRemove.length > 0) {\n            const uninstallSpinner = spinner(\n              `Removing ${packagesToRemove.join(\", \")}.`,\n            ).start();\n\n            try {\n              uninstallPackages(\n                packagesToRemove,\n                projectInfo.packageManager,\n                projectInfo.projectRoot,\n              );\n              uninstallSpinner.succeed();\n            } catch (error) {\n              uninstallSpinner.fail();\n              handleError(error);\n            }\n          }\n\n          if (\n            removalResult.success &&\n            !removalResult.noChanges &&\n            removalResult.newContent\n          ) {\n            const removeWriteSpinner = spinner(\n              `Removing ${AGENT_NAMES[agentToRemove]} from ${removalResult.filePath}.`,\n            ).start();\n            const writeResult = applyTransform(removalResult);\n            if (!writeResult.success) {\n              removeWriteSpinner.fail();\n              logger.break();\n              logger.error(writeResult.error || \"Failed to write file.\");\n              logger.break();\n              process.exit(1);\n            }\n            removeWriteSpinner.succeed();\n          }\n\n          if (\n            removalPackageJsonResult.success &&\n            !removalPackageJsonResult.noChanges &&\n            removalPackageJsonResult.newContent\n          ) {\n            const removePackageJsonSpinner = spinner(\n              `Removing ${AGENT_NAMES[agentToRemove]} from ${removalPackageJsonResult.filePath}.`,\n            ).start();\n            const packageJsonWriteResult = applyPackageJsonTransform(\n              removalPackageJsonResult,\n            );\n            if (!packageJsonWriteResult.success) {\n              removePackageJsonSpinner.fail();\n              logger.break();\n              logger.error(\n                packageJsonWriteResult.error || \"Failed to write file.\",\n              );\n              logger.break();\n              process.exit(1);\n            }\n            removePackageJsonSpinner.succeed();\n          }\n        }\n\n        projectInfo.installedAgents = projectInfo.installedAgents.filter(\n          (installedAgent) => !agentsToRemove.includes(installedAgent as Agent),\n        );\n      }\n\n      const addingSpinner = spinner(\n        `Adding ${getAgentDisplayName(agentIntegration)}.`,\n      ).start();\n      addingSpinner.succeed();\n\n      const result = previewTransform(\n        projectInfo.projectRoot,\n        projectInfo.framework,\n        projectInfo.nextRouterType,\n        agentIntegration,\n        true,\n      );\n\n      const packageJsonResult = previewPackageJsonTransform(\n        projectInfo.projectRoot,\n        agentIntegration,\n        projectInfo.installedAgents,\n        projectInfo.packageManager,\n      );\n\n      if (!result.success) {\n        logger.break();\n        logger.error(result.message);\n        logger.break();\n        process.exit(1);\n      }\n\n      const hasLayoutChanges =\n        !result.noChanges && result.originalContent && result.newContent;\n      const hasPackageJsonChanges =\n        packageJsonResult.success &&\n        !packageJsonResult.noChanges &&\n        packageJsonResult.originalContent &&\n        packageJsonResult.newContent;\n\n      if (hasLayoutChanges || hasPackageJsonChanges) {\n        logger.break();\n\n        if (hasLayoutChanges) {\n          printDiff(\n            result.filePath,\n            result.originalContent!,\n            result.newContent!,\n          );\n        }\n\n        if (hasPackageJsonChanges) {\n          if (hasLayoutChanges) {\n            logger.break();\n          }\n          printDiff(\n            packageJsonResult.filePath,\n            packageJsonResult.originalContent!,\n            packageJsonResult.newContent!,\n          );\n        }\n\n        if (!isNonInteractive && agentsToRemove.length === 0) {\n          logger.break();\n          const { proceed } = await prompts({\n            type: \"confirm\",\n            name: \"proceed\",\n            message: \"Apply these changes?\",\n            initial: true,\n          });\n\n          if (!proceed) {\n            logger.break();\n            logger.log(\"Changes cancelled.\");\n            logger.break();\n            process.exit(0);\n          }\n        }\n      }\n\n      const packages = getPackagesToInstall(agentIntegration, false);\n\n      if (packages.length > 0) {\n        const installSpinner = spinner(\n          `Installing ${packages.join(\", \")}.`,\n        ).start();\n\n        try {\n          installPackages(\n            packages,\n            projectInfo.packageManager,\n            projectInfo.projectRoot,\n          );\n          installSpinner.succeed();\n        } catch (error) {\n          installSpinner.fail();\n          handleError(error);\n        }\n      }\n\n      if (hasLayoutChanges) {\n        const writeSpinner = spinner(\n          `Applying changes to ${result.filePath}.`,\n        ).start();\n        const writeResult = applyTransform(result);\n        if (!writeResult.success) {\n          writeSpinner.fail();\n          logger.break();\n          logger.error(writeResult.error || \"Failed to write file.\");\n          logger.break();\n          process.exit(1);\n        }\n        writeSpinner.succeed();\n      }\n\n      if (hasPackageJsonChanges) {\n        const packageJsonSpinner = spinner(\n          `Applying changes to ${packageJsonResult.filePath}.`,\n        ).start();\n        const packageJsonWriteResult =\n          applyPackageJsonTransform(packageJsonResult);\n        if (!packageJsonWriteResult.success) {\n          packageJsonSpinner.fail();\n          logger.break();\n          logger.error(packageJsonWriteResult.error || \"Failed to write file.\");\n          logger.break();\n          process.exit(1);\n        }\n        packageJsonSpinner.succeed();\n      }\n\n      logger.break();\n      logger.log(\n        `${highlighter.success(\"Success!\")} ${getAgentDisplayName(agentIntegration)} has been added.`,\n      );\n      if (packageJsonResult.warning) {\n        logger.warn(packageJsonResult.warning);\n      } else {\n        logger.log(\"Make sure to start the agent server before using it.\");\n      }\n      logger.break();\n    } catch (error) {\n      handleError(error);\n    }\n  });\n"
  },
  {
    "path": "packages/cli/src/commands/configure.ts",
    "content": "import { Command } from \"commander\";\nimport pc from \"picocolors\";\nimport { prompts } from \"../utils/prompts.js\";\nimport { detectProject } from \"../utils/detect.js\";\nimport { printDiff } from \"../utils/diff.js\";\nimport { handleError } from \"../utils/handle-error.js\";\nimport { highlighter } from \"../utils/highlighter.js\";\nimport { logger } from \"../utils/logger.js\";\nimport { spinner } from \"../utils/spinner.js\";\nimport {\n  applyOptionsTransform,\n  applyTransform,\n  previewCdnTransform,\n  previewOptionsTransform,\n  type ReactGrabOptions,\n} from \"../utils/transform.js\";\nimport {\n  MAX_SUGGESTIONS_COUNT,\n  MAX_KEY_HOLD_DURATION_MS,\n  MAX_CONTEXT_LINES,\n} from \"../utils/constants.js\";\n\nconst VERSION = process.env.VERSION ?? \"0.0.1\";\n\ninterface ConfigOption {\n  id: string;\n  title: string;\n  description: string;\n}\n\nconst isMac = process.platform === \"darwin\";\nconst META_LABEL = isMac ? \"Cmd\" : \"Win\";\nconst ALT_LABEL = isMac ? \"Option\" : \"Alt\";\n\nconst MODIFIER_ALIASES: Record<string, string> = {\n  cmd: \"meta\",\n  command: \"meta\",\n  win: \"meta\",\n  windows: \"meta\",\n  meta: \"meta\",\n  ctrl: \"ctrl\",\n  control: \"ctrl\",\n  shift: \"shift\",\n  alt: \"alt\",\n  option: \"alt\",\n  opt: \"alt\",\n};\n\nconst MODIFIERS = [\"meta\", \"ctrl\", \"shift\", \"alt\"] as const;\n\nconst BASE_KEYS: Array<{ key: string; aliases: string[] }> = [\n  { key: \" \", aliases: [\"space\", \"spacebar\"] },\n  { key: \"Enter\", aliases: [\"enter\", \"return\"] },\n  { key: \"Escape\", aliases: [\"escape\", \"esc\"] },\n  { key: \"Tab\", aliases: [\"tab\"] },\n  { key: \"Backspace\", aliases: [\"backspace\", \"back\"] },\n  { key: \"Delete\", aliases: [\"delete\", \"del\"] },\n  { key: \"Insert\", aliases: [\"insert\", \"ins\"] },\n  { key: \"Home\", aliases: [\"home\"] },\n  { key: \"End\", aliases: [\"end\"] },\n  { key: \"PageUp\", aliases: [\"pageup\", \"pgup\"] },\n  { key: \"PageDown\", aliases: [\"pagedown\", \"pgdn\", \"pgdown\"] },\n  { key: \"ArrowUp\", aliases: [\"arrowup\", \"up\"] },\n  { key: \"ArrowDown\", aliases: [\"arrowdown\", \"down\"] },\n  { key: \"ArrowLeft\", aliases: [\"arrowleft\", \"left\"] },\n  { key: \"ArrowRight\", aliases: [\"arrowright\", \"right\"] },\n  ...Array.from({ length: 12 }, (_, i) => ({\n    key: `F${i + 1}`,\n    aliases: [`f${i + 1}`],\n  })),\n  ...Array.from({ length: 26 }, (_, i) => {\n    const letter = String.fromCharCode(97 + i);\n    return { key: letter, aliases: [letter] };\n  }),\n  ...Array.from({ length: 10 }, (_, i) => ({\n    key: String(i),\n    aliases: [String(i)],\n  })),\n  { key: \"`\", aliases: [\"backtick\", \"grave\", \"`\"] },\n  { key: \"-\", aliases: [\"minus\", \"dash\", \"-\"] },\n  { key: \"=\", aliases: [\"equals\", \"equal\", \"=\"] },\n  { key: \"[\", aliases: [\"leftbracket\", \"lbracket\", \"[\"] },\n  { key: \"]\", aliases: [\"rightbracket\", \"rbracket\", \"]\"] },\n  { key: \"\\\\\", aliases: [\"backslash\", \"\\\\\"] },\n  { key: \";\", aliases: [\"semicolon\", \";\"] },\n  { key: \"'\", aliases: [\"quote\", \"apostrophe\", \"'\"] },\n  { key: \",\", aliases: [\"comma\", \",\"] },\n  { key: \".\", aliases: [\"period\", \"dot\", \".\"] },\n  { key: \"/\", aliases: [\"slash\", \"forwardslash\", \"/\"] },\n];\n\ninterface KeyCombo {\n  key: string;\n  metaKey?: boolean;\n  ctrlKey?: boolean;\n  shiftKey?: boolean;\n  altKey?: boolean;\n}\n\ninterface KeyChoice {\n  title: string;\n  value: KeyCombo;\n}\n\nconst formatCombo = (combo: KeyCombo): string => {\n  const parts: string[] = [];\n  if (combo.metaKey) parts.push(META_LABEL);\n  if (combo.ctrlKey) parts.push(\"Ctrl\");\n  if (combo.shiftKey) parts.push(\"Shift\");\n  if (combo.altKey) parts.push(ALT_LABEL);\n  const keyDisplay =\n    combo.key === \" \"\n      ? \"Space\"\n      : combo.key.length === 1\n        ? combo.key.toUpperCase()\n        : combo.key;\n  parts.push(keyDisplay);\n  return parts.join(\"+\");\n};\n\nconst parseInput = (\n  input: string,\n): { modifiers: Set<string>; partial: string } => {\n  const normalized = input.toLowerCase().replace(/\\s+/g, \"\");\n  const parts = normalized.split(/[+\\-]/);\n  const modifiers = new Set<string>();\n  let partial = \"\";\n\n  for (const part of parts) {\n    if (!part) continue;\n    const modifierKey = MODIFIER_ALIASES[part];\n    if (modifierKey) {\n      modifiers.add(modifierKey);\n    } else {\n      partial = part;\n    }\n  }\n\n  return { modifiers, partial };\n};\n\nconst POPULAR_KEYS = [\"g\", \"k\", \"e\", \"d\", \"b\", \" \", \"Escape\", \"Enter\"];\n\nconst generateSuggestions = (input: string): KeyChoice[] => {\n  const { modifiers, partial } = parseInput(input);\n  const suggestions: KeyChoice[] = [];\n\n  if (!partial && modifiers.size === 0 && !input) {\n    for (const mod of MODIFIERS) {\n      const label =\n        mod === \"meta\"\n          ? META_LABEL\n          : mod === \"alt\"\n            ? ALT_LABEL\n            : mod.charAt(0).toUpperCase() + mod.slice(1);\n      for (const popularKey of POPULAR_KEYS) {\n        const keyDisplay =\n          popularKey === \" \"\n            ? \"Space\"\n            : popularKey.length === 1\n              ? popularKey.toUpperCase()\n              : popularKey;\n        suggestions.push({\n          title: `${label}+${keyDisplay}`,\n          value: {\n            key: popularKey,\n            ...(mod === \"meta\" ? { metaKey: true } : {}),\n            ...(mod === \"ctrl\" ? { ctrlKey: true } : {}),\n            ...(mod === \"shift\" ? { shiftKey: true } : {}),\n            ...(mod === \"alt\" ? { altKey: true } : {}),\n          },\n        });\n      }\n    }\n    for (const baseKey of BASE_KEYS) {\n      suggestions.push({\n        title:\n          baseKey.key === \" \"\n            ? \"Space\"\n            : baseKey.key.length === 1\n              ? baseKey.key.toUpperCase()\n              : baseKey.key,\n        value: { key: baseKey.key },\n      });\n    }\n    return suggestions;\n  }\n\n  const buildCombo = (\n    key: string,\n    mods: Set<string>,\n    extraMod?: string,\n  ): KeyCombo => ({\n    key,\n    ...(mods.has(\"meta\") || extraMod === \"meta\" ? { metaKey: true } : {}),\n    ...(mods.has(\"ctrl\") || extraMod === \"ctrl\" ? { ctrlKey: true } : {}),\n    ...(mods.has(\"shift\") || extraMod === \"shift\" ? { shiftKey: true } : {}),\n    ...(mods.has(\"alt\") || extraMod === \"alt\" ? { altKey: true } : {}),\n  });\n\n  for (const baseKey of BASE_KEYS) {\n    const matches = partial\n      ? baseKey.aliases.some((alias) => alias.startsWith(partial))\n      : true;\n    if (matches) {\n      const combo = buildCombo(baseKey.key, modifiers);\n      suggestions.push({\n        title: formatCombo(combo),\n        value: combo,\n      });\n    }\n  }\n\n  if (!partial) {\n    const unusedMods = MODIFIERS.filter((m) => !modifiers.has(m));\n    for (const mod of unusedMods) {\n      for (const popularKey of POPULAR_KEYS) {\n        const combo = buildCombo(popularKey, modifiers, mod);\n        suggestions.push({\n          title: formatCombo(combo),\n          value: combo,\n        });\n      }\n    }\n  }\n\n  return suggestions.slice(0, MAX_SUGGESTIONS_COUNT);\n};\n\nconst CONFIG_OPTIONS: ConfigOption[] = [\n  {\n    id: \"activationKey\",\n    title: \"Activation Key\",\n    description: \"The key used to activate React Grab (e.g., g, k, space)\",\n  },\n  {\n    id: \"activationMode\",\n    title: \"Activation Mode\",\n    description: \"Toggle (press to activate/deactivate) or Hold (hold key)\",\n  },\n  {\n    id: \"keyHoldDuration\",\n    title: \"Key Hold Duration\",\n    description: \"Milliseconds to hold the key before activation (hold mode)\",\n  },\n  {\n    id: \"allowActivationInsideInput\",\n    title: \"Allow Activation Inside Input\",\n    description: \"Whether to allow activation when focused on input fields\",\n  },\n  {\n    id: \"maxContextLines\",\n    title: \"Max Context Lines\",\n    description: \"Number of surrounding code lines to include in context\",\n  },\n];\n\nconst formatActivationKeyDisplay = (\n  activationKey: ReactGrabOptions[\"activationKey\"],\n): string => {\n  if (!activationKey) return \"Default (Option/Alt)\";\n  return activationKey\n    .split(\"+\")\n    .map((part) => {\n      const lower = part.toLowerCase();\n      if (lower === \"meta\") return process.platform === \"darwin\" ? \"⌘\" : \"Win\";\n      if (lower === \"alt\") return process.platform === \"darwin\" ? \"⌥\" : \"Alt\";\n      if (lower === \"ctrl\") return \"Ctrl\";\n      if (lower === \"shift\") return \"Shift\";\n      if (lower === \"space\" || lower === \" \") return \"Space\";\n      return part.toUpperCase();\n    })\n    .join(\" + \");\n};\n\nconst comboToString = (combo: KeyCombo): string => {\n  const parts: string[] = [];\n  if (combo.metaKey) parts.push(\"Meta\");\n  if (combo.ctrlKey) parts.push(\"Ctrl\");\n  if (combo.shiftKey) parts.push(\"Shift\");\n  if (combo.altKey) parts.push(\"Alt\");\n  if (combo.key) {\n    const keyDisplay = combo.key === \" \" ? \"Space\" : combo.key;\n    parts.push(keyDisplay);\n  }\n  return parts.join(\"+\");\n};\n\nexport const configure = new Command()\n  .name(\"configure\")\n  .alias(\"config\")\n  .description(\"configure React Grab options\")\n  .option(\"-y, --yes\", \"skip confirmation prompts\", false)\n  .option(\n    \"-k, --key <key>\",\n    \"activation key (e.g., Meta+K, Ctrl+Shift+G, Space)\",\n  )\n  .option(\"-m, --mode <mode>\", \"activation mode (toggle, hold)\")\n  .option(\n    \"--hold-duration <ms>\",\n    \"key hold duration in milliseconds (for hold mode)\",\n  )\n  .option(\n    \"--allow-input <boolean>\",\n    \"allow activation inside input fields (true/false)\",\n  )\n  .option(\"--context-lines <lines>\", \"max context lines to include\")\n  .option(\n    \"--cdn <domain>\",\n    \"CDN domain (e.g., unpkg.com, custom.react-grab.com)\",\n  )\n  .option(\n    \"-c, --cwd <cwd>\",\n    \"working directory (defaults to current directory)\",\n    process.cwd(),\n  )\n  .action(async (opts) => {\n    console.log(\n      `${pc.magenta(\"✿\")} ${pc.bold(\"React Grab\")} ${pc.gray(VERSION)}`,\n    );\n    console.log();\n\n    try {\n      const cwd = opts.cwd;\n\n      const preflightSpinner = spinner(\"Preflight checks.\").start();\n\n      const projectInfo = await detectProject(cwd);\n\n      if (!projectInfo.hasReactGrab) {\n        preflightSpinner.fail(\"React Grab is not installed.\");\n        logger.break();\n        logger.error(\n          `Run ${highlighter.info(\"react-grab init\")} first to install React Grab.`,\n        );\n        logger.break();\n        process.exit(1);\n      }\n\n      preflightSpinner.succeed();\n\n      if (opts.cdn) {\n        const result = previewCdnTransform(\n          projectInfo.projectRoot,\n          projectInfo.framework,\n          projectInfo.nextRouterType,\n          opts.cdn,\n        );\n\n        if (!result.success) {\n          logger.break();\n          logger.error(result.message);\n          logger.break();\n          process.exit(1);\n        }\n\n        if (result.noChanges) {\n          logger.break();\n          logger.log(\"No changes needed.\");\n          logger.break();\n          process.exit(0);\n        }\n\n        logger.break();\n        printDiff(result.filePath, result.originalContent!, result.newContent!);\n\n        if (!opts.yes) {\n          logger.break();\n          const { proceed } = await prompts({\n            type: \"confirm\",\n            name: \"proceed\",\n            message: \"Apply these changes?\",\n            initial: true,\n          });\n\n          if (!proceed) {\n            logger.break();\n            logger.log(\"Changes cancelled.\");\n            logger.break();\n            process.exit(0);\n          }\n        }\n\n        const writeSpinner = spinner(\n          `Applying changes to ${result.filePath}.`,\n        ).start();\n        const writeResult = applyTransform(result);\n        if (!writeResult.success) {\n          writeSpinner.fail();\n          logger.break();\n          logger.error(writeResult.error || \"Failed to write file.\");\n          logger.break();\n          process.exit(1);\n        }\n        writeSpinner.succeed();\n\n        logger.break();\n        logger.log(`${highlighter.success(\"Success!\")} CDN updated.`);\n        logger.break();\n        return;\n      }\n\n      const hasFlags =\n        opts.key ||\n        opts.mode ||\n        opts.holdDuration ||\n        opts.allowInput ||\n        opts.contextLines;\n\n      logger.break();\n      logger.log(`Configure ${highlighter.info(\"React Grab\")} options:`);\n      logger.break();\n\n      const collectedOptions: ReactGrabOptions = {};\n\n      if (hasFlags) {\n        if (opts.key) {\n          collectedOptions.activationKey = opts.key;\n          logger.log(\n            `  Activation key: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`,\n          );\n        }\n\n        if (opts.mode) {\n          if (opts.mode !== \"toggle\" && opts.mode !== \"hold\") {\n            logger.error(`Invalid mode: ${opts.mode}. Use \"toggle\" or \"hold\".`);\n            logger.break();\n            process.exit(1);\n          }\n          collectedOptions.activationMode = opts.mode;\n          logger.log(`  Activation mode: ${highlighter.info(opts.mode)}`);\n        }\n\n        if (opts.holdDuration) {\n          const duration = parseInt(opts.holdDuration, 10);\n          if (\n            isNaN(duration) ||\n            duration < 0 ||\n            duration > MAX_KEY_HOLD_DURATION_MS\n          ) {\n            logger.error(\n              `Invalid hold duration. Must be 0-${MAX_KEY_HOLD_DURATION_MS}ms.`,\n            );\n            logger.break();\n            process.exit(1);\n          }\n          collectedOptions.keyHoldDuration = duration;\n          logger.log(\n            `  Key hold duration: ${highlighter.info(`${duration}ms`)}`,\n          );\n        }\n\n        if (opts.allowInput !== undefined) {\n          const allowInput =\n            opts.allowInput === \"true\" || opts.allowInput === true;\n          collectedOptions.allowActivationInsideInput = allowInput;\n          logger.log(\n            `  Allow activation inside input: ${highlighter.info(String(allowInput))}`,\n          );\n        }\n\n        if (opts.contextLines) {\n          const lines = parseInt(opts.contextLines, 10);\n          if (isNaN(lines) || lines < 0 || lines > MAX_CONTEXT_LINES) {\n            logger.error(\n              `Invalid context lines. Must be 0-${MAX_CONTEXT_LINES}.`,\n            );\n            logger.break();\n            process.exit(1);\n          }\n          collectedOptions.maxContextLines = lines;\n          logger.log(`  Max context lines: ${highlighter.info(String(lines))}`);\n        }\n      } else {\n        const { selectedOption } = await prompts({\n          type: \"autocomplete\",\n          name: \"selectedOption\",\n          message: \"Search for an option to configure:\",\n          choices: CONFIG_OPTIONS.map((option) => ({\n            title: option.title,\n            value: option.id,\n            description: option.description,\n          })),\n          suggest: (input, choices) =>\n            Promise.resolve(\n              choices.filter(\n                (choice) =>\n                  choice.title.toLowerCase().includes(input.toLowerCase()) ||\n                  (choice.description\n                    ?.toLowerCase()\n                    .includes(input.toLowerCase()) ??\n                    false),\n              ),\n            ),\n        });\n\n        if (selectedOption === undefined) {\n          logger.break();\n          process.exit(1);\n        }\n\n        if (selectedOption === \"activationKey\") {\n          const { selectedCombo } = await prompts({\n            type: \"autocomplete\",\n            name: \"selectedCombo\",\n            message: \"Type key combination (e.g. ctrl+shift+g):\",\n            choices: generateSuggestions(\"\"),\n            suggest: (input) => Promise.resolve(generateSuggestions(input)),\n          });\n\n          if (selectedCombo === undefined) {\n            logger.break();\n            process.exit(1);\n          }\n\n          collectedOptions.activationKey = comboToString(selectedCombo);\n\n          logger.log(\n            `  Activation key: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`,\n          );\n        }\n\n        if (selectedOption === \"activationMode\") {\n          const { activationMode } = await prompts({\n            type: \"select\",\n            name: \"activationMode\",\n            message: `Select ${highlighter.info(\"activation mode\")}:`,\n            choices: [\n              {\n                title: \"Toggle (press to activate/deactivate)\",\n                value: \"toggle\",\n              },\n              { title: \"Hold (hold key to keep active)\", value: \"hold\" },\n            ],\n            initial: 0,\n          });\n\n          if (activationMode === undefined) {\n            logger.break();\n            process.exit(1);\n          }\n\n          collectedOptions.activationMode = activationMode;\n        }\n\n        if (selectedOption === \"keyHoldDuration\") {\n          const { keyHoldDuration } = await prompts({\n            type: \"number\",\n            name: \"keyHoldDuration\",\n            message: `Enter ${highlighter.info(\"key hold duration\")} in milliseconds:`,\n            initial: 150,\n            min: 0,\n            max: 2000,\n          });\n\n          if (keyHoldDuration === undefined) {\n            logger.break();\n            process.exit(1);\n          }\n\n          collectedOptions.keyHoldDuration = keyHoldDuration;\n        }\n\n        if (selectedOption === \"allowActivationInsideInput\") {\n          const { allowActivationInsideInput } = await prompts({\n            type: \"confirm\",\n            name: \"allowActivationInsideInput\",\n            message: `Allow activation ${highlighter.info(\"inside input fields\")}?`,\n            initial: true,\n          });\n\n          if (allowActivationInsideInput === undefined) {\n            logger.break();\n            process.exit(1);\n          }\n\n          collectedOptions.allowActivationInsideInput =\n            allowActivationInsideInput;\n        }\n\n        if (selectedOption === \"maxContextLines\") {\n          const { maxContextLines } = await prompts({\n            type: \"number\",\n            name: \"maxContextLines\",\n            message: `Enter ${highlighter.info(\"max context lines\")} to include:`,\n            initial: 3,\n            min: 0,\n            max: 50,\n          });\n\n          if (maxContextLines === undefined) {\n            logger.break();\n            process.exit(1);\n          }\n\n          collectedOptions.maxContextLines = maxContextLines;\n        }\n      }\n\n      const result = previewOptionsTransform(\n        projectInfo.projectRoot,\n        projectInfo.framework,\n        projectInfo.nextRouterType,\n        collectedOptions,\n      );\n\n      if (!result.success) {\n        logger.break();\n        logger.warn(result.message);\n        logger.break();\n\n        const configJson = JSON.stringify(collectedOptions);\n        logger.log(\n          `Add this to your ${highlighter.info(\"init()\")} call or ${highlighter.info(\"data-options\")} attribute:`,\n        );\n        logger.break();\n        console.log(`  ${pc.cyan(configJson)}`);\n        logger.break();\n        process.exit(1);\n      }\n\n      const hasChanges =\n        !result.noChanges && result.originalContent && result.newContent;\n\n      if (hasChanges) {\n        logger.break();\n        printDiff(result.filePath, result.originalContent!, result.newContent!);\n\n        if (!opts.yes) {\n          logger.break();\n          const { proceed } = await prompts({\n            type: \"confirm\",\n            name: \"proceed\",\n            message: \"Apply these changes?\",\n            initial: true,\n          });\n\n          if (!proceed) {\n            logger.break();\n            logger.log(\"Changes cancelled.\");\n            logger.break();\n            process.exit(0);\n          }\n        }\n\n        const writeSpinner = spinner(\n          `Applying changes to ${result.filePath}.`,\n        ).start();\n        const writeResult = applyOptionsTransform(result);\n        if (!writeResult.success) {\n          writeSpinner.fail();\n          logger.break();\n          logger.error(writeResult.error || \"Failed to write file.\");\n          logger.break();\n          process.exit(1);\n        }\n        writeSpinner.succeed();\n      } else {\n        logger.break();\n        logger.log(\"No changes needed.\");\n      }\n\n      logger.break();\n      logger.log(\n        `${highlighter.success(\"Success!\")} React Grab options have been configured.`,\n      );\n      logger.break();\n    } catch (error) {\n      handleError(error);\n    }\n  });\n"
  },
  {
    "path": "packages/cli/src/commands/init.ts",
    "content": "import { existsSync } from \"node:fs\";\nimport { relative, resolve } from \"node:path\";\nimport { Command } from \"commander\";\nimport pc from \"picocolors\";\nimport { detectNonInteractive } from \"../utils/is-non-interactive.js\";\nimport { prompts } from \"../utils/prompts.js\";\nimport {\n  applyPackageJsonWithFeedback,\n  applyTransformWithFeedback,\n  formatInstalledAgentNames,\n  installPackagesWithFeedback,\n  uninstallPackagesWithFeedback,\n} from \"../utils/cli-helpers.js\";\nimport {\n  promptConnectionMode,\n  promptMcpInstall,\n} from \"../utils/install-mcp.js\";\nimport {\n  detectProject,\n  findReactProjects,\n  type Framework,\n  type PackageManager,\n  type UnsupportedFramework,\n  type WorkspaceProject,\n} from \"../utils/detect.js\";\nimport { printDiff } from \"../utils/diff.js\";\nimport { handleError } from \"../utils/handle-error.js\";\nimport { highlighter } from \"../utils/highlighter.js\";\nimport {\n  getPackagesToInstall,\n  getPackagesToUninstall,\n} from \"../utils/install.js\";\nimport { logger } from \"../utils/logger.js\";\nimport { spinner } from \"../utils/spinner.js\";\nimport {\n  AGENTS,\n  type AgentIntegration,\n  getAgentDisplayName,\n} from \"../utils/templates.js\";\nimport {\n  previewAgentRemoval,\n  previewOptionsTransform,\n  previewPackageJsonAgentRemoval,\n  previewPackageJsonTransform,\n  previewTransform,\n  type ReactGrabOptions,\n} from \"../utils/transform.js\";\n\nconst VERSION = process.env.VERSION ?? \"0.0.1\";\nconst REPORT_URL = \"https://react-grab.com/api/report-cli\";\nconst DOCS_URL = \"https://github.com/aidenybai/react-grab\";\n\ninterface ReportConfig {\n  framework: string;\n  packageManager: string;\n  router?: string;\n  agent?: string;\n  isMonorepo: boolean;\n}\n\nconst reportToCli = (\n  type: \"error\" | \"completed\",\n  config?: ReportConfig,\n  error?: Error,\n): void => {\n  fetch(REPORT_URL, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({\n      type,\n      version: VERSION,\n      config,\n      error: error ? { message: error.message, stack: error.stack } : undefined,\n      timestamp: new Date().toISOString(),\n    }),\n  }).catch(() => {});\n};\n\nconst FRAMEWORK_NAMES: Record<Framework, string> = {\n  next: \"Next.js\",\n  vite: \"Vite\",\n  tanstack: \"TanStack Start\",\n  webpack: \"Webpack\",\n  unknown: \"Unknown\",\n};\n\nconst PACKAGE_MANAGER_NAMES: Record<PackageManager, string> = {\n  npm: \"npm\",\n  yarn: \"Yarn\",\n  pnpm: \"pnpm\",\n  bun: \"Bun\",\n};\n\nconst UNSUPPORTED_FRAMEWORK_NAMES: Record<\n  NonNullable<UnsupportedFramework>,\n  string\n> = {\n  remix: \"Remix\",\n  astro: \"Astro\",\n  sveltekit: \"SvelteKit\",\n  gatsby: \"Gatsby\",\n};\n\nconst getAgentName = getAgentDisplayName;\n\nconst sortProjectsByFramework = (\n  projects: WorkspaceProject[],\n): WorkspaceProject[] =>\n  [...projects].sort((projectA, projectB) => {\n    if (projectA.framework === \"unknown\" && projectB.framework !== \"unknown\")\n      return 1;\n    if (projectA.framework !== \"unknown\" && projectB.framework === \"unknown\")\n      return -1;\n    return 0;\n  });\n\nconst printSubprojects = (\n  searchRoot: string,\n  sortedProjects: WorkspaceProject[],\n): void => {\n  logger.break();\n  logger.log(\"Found the following projects:\");\n  logger.break();\n  for (const project of sortedProjects) {\n    const frameworkLabel =\n      project.framework !== \"unknown\"\n        ? ` ${highlighter.dim(`(${FRAMEWORK_NAMES[project.framework]})`)}`\n        : \"\";\n    const relativePath = relative(searchRoot, project.path);\n    logger.log(\n      `  ${highlighter.info(project.name)}${frameworkLabel} ${highlighter.dim(relativePath)}`,\n    );\n  }\n  logger.break();\n  logger.log(\n    `Re-run with ${highlighter.info(\"-c <path>\")} to specify a project:`,\n  );\n  logger.break();\n  logger.log(\n    `  ${highlighter.dim(\"$\")} npx -y grab@latest init -c ${relative(searchRoot, sortedProjects[0].path)}`,\n  );\n  logger.break();\n};\n\nconst formatActivationKeyDisplay = (\n  activationKey: ReactGrabOptions[\"activationKey\"],\n): string => {\n  if (!activationKey) return \"Default (Option/Alt)\";\n  return activationKey\n    .split(\"+\")\n    .map((part) => {\n      const lower = part.toLowerCase();\n      if (lower === \"meta\") return process.platform === \"darwin\" ? \"⌘\" : \"Win\";\n      if (lower === \"alt\") return process.platform === \"darwin\" ? \"⌥\" : \"Alt\";\n      if (lower === \"ctrl\") return \"Ctrl\";\n      if (lower === \"shift\") return \"Shift\";\n      if (lower === \"space\" || lower === \" \") return \"Space\";\n      return part.toUpperCase();\n    })\n    .join(\" + \");\n};\n\nexport const init = new Command()\n  .name(\"init\")\n  .description(\"initialize React Grab in your project\")\n  .option(\"-y, --yes\", \"skip confirmation prompts\", false)\n  .option(\"-f, --force\", \"force overwrite existing config\", false)\n  .option(\n    \"-a, --agent <agent>\",\n    `connect to your agent (${AGENTS.join(\", \")}, mcp)`,\n  )\n  .option(\n    \"-k, --key <key>\",\n    \"activation key (e.g., Meta+K, Ctrl+Shift+G, Space)\",\n  )\n  .option(\"--skip-install\", \"skip package installation\", false)\n  .option(\"--pkg <pkg>\", \"custom package URL for CLI (e.g., grab)\")\n  .option(\n    \"-c, --cwd <cwd>\",\n    \"working directory (defaults to current directory)\",\n    process.cwd(),\n  )\n  .action(async (opts) => {\n    console.log(\n      `${pc.magenta(\"✿\")} ${pc.bold(\"React Grab\")} ${pc.gray(VERSION)}`,\n    );\n    console.log();\n\n    try {\n      const cwd = resolve(opts.cwd);\n      const isNonInteractive = detectNonInteractive(opts.yes);\n\n      if (!existsSync(cwd)) {\n        logger.break();\n        logger.error(`Directory does not exist: ${highlighter.info(cwd)}`);\n        logger.break();\n        process.exit(1);\n      }\n\n      const preflightSpinner = spinner(\"Preflight checks.\").start();\n\n      const projectInfo = await detectProject(cwd);\n\n      const removeAgents = async (\n        agentsToRemove: string[],\n        skipInstall: boolean = false,\n      ) => {\n        for (const agentToRemove of agentsToRemove) {\n          const removalResult = previewAgentRemoval(\n            projectInfo.projectRoot,\n            projectInfo.framework,\n            projectInfo.nextRouterType,\n            agentToRemove,\n          );\n          const removalPackageJsonResult = previewPackageJsonAgentRemoval(\n            projectInfo.projectRoot,\n            agentToRemove,\n          );\n\n          if (!skipInstall) {\n            uninstallPackagesWithFeedback(\n              getPackagesToUninstall(agentToRemove),\n              projectInfo.packageManager,\n              projectInfo.projectRoot,\n            );\n          }\n\n          if (\n            removalResult.success &&\n            !removalResult.noChanges &&\n            removalResult.newContent\n          ) {\n            applyTransformWithFeedback(\n              removalResult,\n              `Removing ${getAgentName(agentToRemove)} from ${removalResult.filePath}.`,\n            );\n          }\n\n          if (\n            removalPackageJsonResult.success &&\n            !removalPackageJsonResult.noChanges &&\n            removalPackageJsonResult.newContent\n          ) {\n            applyPackageJsonWithFeedback(\n              removalPackageJsonResult,\n              `Removing ${getAgentName(agentToRemove)} from ${removalPackageJsonResult.filePath}.`,\n            );\n          }\n        }\n      };\n\n      if (projectInfo.hasReactGrab && !opts.force) {\n        preflightSpinner.succeed();\n\n        if (isNonInteractive) {\n          logger.break();\n          logger.warn(\"React Grab is already installed.\");\n          logger.log(\n            `Use ${highlighter.info(\"--force\")} to reconfigure, or remove ${highlighter.info(\"--yes\")} for interactive mode.`,\n          );\n          logger.break();\n          process.exit(0);\n        }\n\n        logger.break();\n        logger.success(\"React Grab is already installed.\");\n        logger.break();\n\n        if (projectInfo.installedAgents.length > 0) {\n          logger.log(\n            `Currently installed agents: ${highlighter.info(formatInstalledAgentNames(projectInfo.installedAgents))}`,\n          );\n          logger.break();\n        }\n\n        const { wantCustomizeOptions } = await prompts({\n          type: \"confirm\",\n          name: \"wantCustomizeOptions\",\n          message: `Would you like to customize ${highlighter.info(\"options\")}?`,\n          initial: false,\n        });\n\n        if (wantCustomizeOptions === undefined) {\n          logger.break();\n          process.exit(1);\n        }\n\n        if (wantCustomizeOptions || opts.key) {\n          logger.break();\n          logger.log(`Configure ${highlighter.info(\"React Grab\")} options:`);\n          logger.break();\n\n          const collectedOptions: ReactGrabOptions = {};\n\n          if (opts.key) {\n            collectedOptions.activationKey = opts.key;\n            logger.log(\n              `  Activation key: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`,\n            );\n          } else {\n            const { wantActivationKey } = await prompts({\n              type: \"confirm\",\n              name: \"wantActivationKey\",\n              message: `Configure ${highlighter.info(\"activation key\")}?`,\n              initial: false,\n            });\n\n            if (wantActivationKey === undefined) {\n              logger.break();\n              process.exit(1);\n            }\n\n            if (wantActivationKey) {\n              const { key } = await prompts({\n                type: \"text\",\n                name: \"key\",\n                message: \"Enter the activation key (e.g., g, k, space):\",\n                initial: \"\",\n              });\n\n              if (key === undefined) {\n                logger.break();\n                process.exit(1);\n              }\n\n              collectedOptions.activationKey = key\n                ? key.toLowerCase()\n                : undefined;\n\n              logger.log(\n                `  Activation key: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`,\n              );\n            }\n          }\n\n          const { activationMode } = await prompts({\n            type: \"select\",\n            name: \"activationMode\",\n            message: `Select ${highlighter.info(\"activation mode\")}:`,\n            choices: [\n              {\n                title: \"Toggle (press to activate/deactivate)\",\n                value: \"toggle\",\n              },\n              { title: \"Hold (hold key to keep active)\", value: \"hold\" },\n            ],\n            initial: 0,\n          });\n\n          if (activationMode === undefined) {\n            logger.break();\n            process.exit(1);\n          }\n\n          collectedOptions.activationMode = activationMode;\n\n          if (activationMode === \"hold\") {\n            const { keyHoldDuration } = await prompts({\n              type: \"number\",\n              name: \"keyHoldDuration\",\n              message: `Enter ${highlighter.info(\"key hold duration\")} in milliseconds:`,\n              initial: 150,\n              min: 0,\n              max: 2000,\n            });\n\n            if (keyHoldDuration === undefined) {\n              logger.break();\n              process.exit(1);\n            }\n\n            collectedOptions.keyHoldDuration = keyHoldDuration;\n          }\n\n          const { allowActivationInsideInput } = await prompts({\n            type: \"confirm\",\n            name: \"allowActivationInsideInput\",\n            message: `Allow activation ${highlighter.info(\"inside input fields\")}?`,\n            initial: true,\n          });\n\n          if (allowActivationInsideInput === undefined) {\n            logger.break();\n            process.exit(1);\n          }\n\n          collectedOptions.allowActivationInsideInput =\n            allowActivationInsideInput;\n\n          const { maxContextLines } = await prompts({\n            type: \"number\",\n            name: \"maxContextLines\",\n            message: `Enter ${highlighter.info(\"max context lines\")} to include:`,\n            initial: 3,\n            min: 0,\n            max: 50,\n          });\n\n          if (maxContextLines === undefined) {\n            logger.break();\n            process.exit(1);\n          }\n\n          collectedOptions.maxContextLines = maxContextLines;\n\n          const optionsResult = previewOptionsTransform(\n            projectInfo.projectRoot,\n            projectInfo.framework,\n            projectInfo.nextRouterType,\n            collectedOptions,\n          );\n\n          if (!optionsResult.success) {\n            logger.break();\n            logger.error(optionsResult.message);\n            logger.break();\n            process.exit(1);\n          }\n\n          const hasOptionsChanges =\n            !optionsResult.noChanges &&\n            optionsResult.originalContent &&\n            optionsResult.newContent;\n\n          if (hasOptionsChanges) {\n            logger.break();\n            printDiff(\n              optionsResult.filePath,\n              optionsResult.originalContent!,\n              optionsResult.newContent!,\n            );\n\n            logger.break();\n            const { proceed } = await prompts({\n              type: \"confirm\",\n              name: \"proceed\",\n              message: \"Apply these changes?\",\n              initial: true,\n            });\n\n            if (!proceed) {\n              logger.break();\n              logger.log(\"Options configuration cancelled.\");\n            } else {\n              applyTransformWithFeedback(optionsResult);\n\n              logger.break();\n              logger.success(\"React Grab options have been configured.\");\n            }\n          } else {\n            logger.break();\n            logger.log(\"No option changes needed.\");\n          }\n        }\n\n        const availableAgents = AGENTS.filter(\n          (agent) => !projectInfo.installedAgents.includes(agent),\n        );\n\n        logger.break();\n        const { wantAddAgent } = await prompts({\n          type: \"confirm\",\n          name: \"wantAddAgent\",\n          message: `Would you like to ${highlighter.info(\"connect it to your agent\")}?`,\n          initial: false,\n        });\n\n        if (wantAddAgent === undefined) {\n          logger.break();\n          process.exit(1);\n        }\n\n        if (wantAddAgent) {\n          const connectionMode = await promptConnectionMode();\n\n          if (connectionMode === undefined) {\n            logger.break();\n            process.exit(1);\n          }\n\n          let agentIntegration: AgentIntegration;\n\n          if (connectionMode === \"mcp\") {\n            const didInstall = await promptMcpInstall();\n            if (!didInstall) {\n              logger.break();\n              process.exit(0);\n            }\n            logger.break();\n            logger.success(\"MCP server has been configured.\");\n            logger.log(\"Restart your agents to activate.\");\n          } else {\n            const { agent } = await prompts({\n              type: \"select\",\n              name: \"agent\",\n              message: `Which ${highlighter.info(\"agent\")} would you like to connect?`,\n              choices: [\n                ...availableAgents.map((innerAgent) => ({\n                  title: getAgentName(innerAgent),\n                  value: innerAgent,\n                })),\n                { title: \"Skip\", value: \"skip\" },\n              ],\n            });\n\n            if (agent === undefined || agent === \"skip\") {\n              logger.break();\n              process.exit(0);\n            }\n\n            agentIntegration = agent as AgentIntegration;\n            let agentsToRemove: string[] = [];\n\n            if (projectInfo.installedAgents.length > 0) {\n              const installedNames = formatInstalledAgentNames(\n                projectInfo.installedAgents,\n              );\n\n              const { action } = await prompts({\n                type: \"select\",\n                name: \"action\",\n                message: \"How would you like to proceed?\",\n                choices: [\n                  {\n                    title: `Replace ${installedNames} with ${getAgentName(agentIntegration)}`,\n                    value: \"replace\",\n                  },\n                  {\n                    title: `Add ${getAgentName(agentIntegration)} alongside existing`,\n                    value: \"add\",\n                  },\n                  { title: \"Cancel\", value: \"cancel\" },\n                ],\n              });\n\n              if (!action || action === \"cancel\") {\n                logger.break();\n                logger.log(\"Agent addition cancelled.\");\n              } else {\n                if (action === \"replace\") {\n                  agentsToRemove = [...projectInfo.installedAgents];\n                }\n\n                if (agentsToRemove.length > 0) {\n                  await removeAgents(agentsToRemove);\n                  projectInfo.installedAgents =\n                    projectInfo.installedAgents.filter(\n                      (innerAgent) => !agentsToRemove.includes(innerAgent),\n                    );\n                }\n\n                const result = previewTransform(\n                  projectInfo.projectRoot,\n                  projectInfo.framework,\n                  projectInfo.nextRouterType,\n                  agentIntegration,\n                  true,\n                );\n\n                const packageJsonResult = previewPackageJsonTransform(\n                  projectInfo.projectRoot,\n                  agentIntegration,\n                  projectInfo.installedAgents,\n                  projectInfo.packageManager,\n                );\n\n                if (!result.success) {\n                  logger.break();\n                  logger.error(result.message);\n                  logger.break();\n                  process.exit(1);\n                }\n\n                const hasLayoutChanges =\n                  !result.noChanges &&\n                  result.originalContent &&\n                  result.newContent;\n                const hasPackageJsonChanges =\n                  packageJsonResult.success &&\n                  !packageJsonResult.noChanges &&\n                  packageJsonResult.originalContent &&\n                  packageJsonResult.newContent;\n\n                if (hasLayoutChanges || hasPackageJsonChanges) {\n                  logger.break();\n\n                  if (hasLayoutChanges) {\n                    printDiff(\n                      result.filePath,\n                      result.originalContent!,\n                      result.newContent!,\n                    );\n                  }\n\n                  if (hasPackageJsonChanges) {\n                    if (hasLayoutChanges) {\n                      logger.break();\n                    }\n                    printDiff(\n                      packageJsonResult.filePath,\n                      packageJsonResult.originalContent!,\n                      packageJsonResult.newContent!,\n                    );\n                  }\n\n                  if (agentsToRemove.length === 0) {\n                    logger.break();\n                    const { proceed } = await prompts({\n                      type: \"confirm\",\n                      name: \"proceed\",\n                      message: \"Apply these changes?\",\n                      initial: true,\n                    });\n\n                    if (!proceed) {\n                      logger.break();\n                      logger.log(\"Agent addition cancelled.\");\n                    } else {\n                      installPackagesWithFeedback(\n                        getPackagesToInstall(agentIntegration, false),\n                        projectInfo.packageManager,\n                        projectInfo.projectRoot,\n                      );\n\n                      if (hasLayoutChanges) {\n                        applyTransformWithFeedback(result);\n                      }\n\n                      if (hasPackageJsonChanges) {\n                        applyPackageJsonWithFeedback(packageJsonResult);\n                      }\n\n                      logger.break();\n                      logger.success(\n                        `${getAgentName(agentIntegration)} has been added.`,\n                      );\n                    }\n                  } else {\n                    installPackagesWithFeedback(\n                      getPackagesToInstall(agentIntegration, false),\n                      projectInfo.packageManager,\n                      projectInfo.projectRoot,\n                    );\n\n                    if (hasLayoutChanges) {\n                      applyTransformWithFeedback(result);\n                    }\n\n                    if (hasPackageJsonChanges) {\n                      applyPackageJsonWithFeedback(packageJsonResult);\n                    }\n\n                    logger.break();\n                    logger.success(\n                      `${getAgentName(agentIntegration)} has been added.`,\n                    );\n                  }\n                }\n              }\n            } else {\n              const result = previewTransform(\n                projectInfo.projectRoot,\n                projectInfo.framework,\n                projectInfo.nextRouterType,\n                agentIntegration,\n                true,\n              );\n\n              const packageJsonResult = previewPackageJsonTransform(\n                projectInfo.projectRoot,\n                agentIntegration,\n                projectInfo.installedAgents,\n                projectInfo.packageManager,\n              );\n\n              if (!result.success) {\n                logger.break();\n                logger.error(result.message);\n                logger.break();\n                process.exit(1);\n              }\n\n              const hasLayoutChanges =\n                !result.noChanges &&\n                result.originalContent &&\n                result.newContent;\n              const hasPackageJsonChanges =\n                packageJsonResult.success &&\n                !packageJsonResult.noChanges &&\n                packageJsonResult.originalContent &&\n                packageJsonResult.newContent;\n\n              if (hasLayoutChanges || hasPackageJsonChanges) {\n                logger.break();\n\n                if (hasLayoutChanges) {\n                  printDiff(\n                    result.filePath,\n                    result.originalContent!,\n                    result.newContent!,\n                  );\n                }\n\n                if (hasPackageJsonChanges) {\n                  if (hasLayoutChanges) {\n                    logger.break();\n                  }\n                  printDiff(\n                    packageJsonResult.filePath,\n                    packageJsonResult.originalContent!,\n                    packageJsonResult.newContent!,\n                  );\n                }\n\n                logger.break();\n                const { proceed } = await prompts({\n                  type: \"confirm\",\n                  name: \"proceed\",\n                  message: \"Apply these changes?\",\n                  initial: true,\n                });\n\n                if (!proceed) {\n                  logger.break();\n                  logger.log(\"Agent addition cancelled.\");\n                } else {\n                  installPackagesWithFeedback(\n                    getPackagesToInstall(agentIntegration, false),\n                    projectInfo.packageManager,\n                    projectInfo.projectRoot,\n                  );\n\n                  if (hasLayoutChanges) {\n                    applyTransformWithFeedback(result);\n                  }\n\n                  if (hasPackageJsonChanges) {\n                    applyPackageJsonWithFeedback(packageJsonResult);\n                  }\n\n                  logger.break();\n                  logger.success(\n                    `${getAgentName(agentIntegration)} has been added.`,\n                  );\n                }\n              }\n            }\n          }\n        }\n\n        logger.break();\n        process.exit(0);\n      }\n\n      preflightSpinner.succeed();\n\n      const frameworkSpinner = spinner(\"Verifying framework.\").start();\n\n      if (projectInfo.unsupportedFramework) {\n        const frameworkName =\n          UNSUPPORTED_FRAMEWORK_NAMES[projectInfo.unsupportedFramework];\n        frameworkSpinner.fail(`Found ${highlighter.info(frameworkName)}.`);\n        logger.break();\n        logger.log(`${frameworkName} is not yet supported by automatic setup.`);\n        logger.log(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`);\n        logger.break();\n        process.exit(1);\n      }\n\n      if (projectInfo.framework === \"unknown\") {\n        let searchRoot = cwd;\n        let reactProjects = findReactProjects(searchRoot);\n        if (reactProjects.length === 0 && cwd !== process.cwd()) {\n          searchRoot = process.cwd();\n          reactProjects = findReactProjects(searchRoot);\n        }\n\n        if (reactProjects.length > 0) {\n          frameworkSpinner.info(\n            `Verifying framework. Found ${reactProjects.length} project${reactProjects.length === 1 ? \"\" : \"s\"}.`,\n          );\n\n          const sortedProjects = sortProjectsByFramework(reactProjects);\n\n          if (isNonInteractive) {\n            printSubprojects(searchRoot, sortedProjects);\n            process.exit(1);\n          }\n\n          logger.break();\n          const { selectedProject } = await prompts({\n            type: \"select\",\n            name: \"selectedProject\",\n            message: \"Select a project to install React Grab:\",\n            choices: [\n              ...sortedProjects.map((project) => {\n                const frameworkLabel =\n                  project.framework !== \"unknown\"\n                    ? ` ${highlighter.dim(`(${FRAMEWORK_NAMES[project.framework]})`)}`\n                    : \"\";\n                return {\n                  title: `${project.name}${frameworkLabel}`,\n                  value: project.path,\n                };\n              }),\n              { title: \"Skip\", value: \"skip\" },\n            ],\n          });\n\n          if (!selectedProject || selectedProject === \"skip\") {\n            logger.break();\n            process.exit(0);\n          }\n\n          process.chdir(selectedProject);\n          const newProjectInfo = await detectProject(selectedProject);\n          Object.assign(projectInfo, newProjectInfo);\n\n          const newFrameworkSpinner = spinner(\"Verifying framework.\").start();\n          newFrameworkSpinner.succeed(\n            `Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[newProjectInfo.framework])}.`,\n          );\n        } else {\n          frameworkSpinner.fail(\"Could not detect a supported framework.\");\n          logger.break();\n          logger.log(\n            \"React Grab supports Next.js, Vite, TanStack Start, and Webpack projects.\",\n          );\n          logger.log(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`);\n          logger.break();\n          process.exit(1);\n        }\n      } else {\n        frameworkSpinner.succeed(\n          `Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[projectInfo.framework])}.`,\n        );\n      }\n\n      if (projectInfo.framework === \"next\") {\n        const routerSpinner = spinner(\"Detecting router type.\").start();\n        routerSpinner.succeed(\n          `Detecting router type. Found ${highlighter.info(projectInfo.nextRouterType === \"app\" ? \"App Router\" : \"Pages Router\")}.`,\n        );\n      }\n\n      const packageManagerSpinner = spinner(\n        \"Detecting package manager.\",\n      ).start();\n      packageManagerSpinner.succeed(\n        `Detecting package manager. Found ${highlighter.info(PACKAGE_MANAGER_NAMES[projectInfo.packageManager])}.`,\n      );\n\n      const finalFramework = projectInfo.framework;\n      const finalPackageManager = projectInfo.packageManager;\n      const finalNextRouterType = projectInfo.nextRouterType;\n      let agentIntegration: AgentIntegration =\n        (opts.agent as AgentIntegration) || \"none\";\n      const agentsToRemove: string[] = [];\n\n      if (!isNonInteractive && !opts.agent) {\n        logger.break();\n        const { wantAddAgent } = await prompts({\n          type: \"confirm\",\n          name: \"wantAddAgent\",\n          message: `Would you like to ${highlighter.info(\"connect it to your agent\")}?`,\n          initial: false,\n        });\n\n        if (wantAddAgent === undefined) {\n          logger.break();\n          process.exit(1);\n        }\n\n        if (wantAddAgent) {\n          const connectionMode = await promptConnectionMode();\n\n          if (connectionMode === undefined) {\n            logger.break();\n            process.exit(1);\n          }\n\n          if (connectionMode === \"mcp\") {\n            const didInstall = await promptMcpInstall();\n            if (!didInstall) {\n              logger.break();\n              process.exit(0);\n            }\n            logger.break();\n            logger.success(\"MCP server has been configured.\");\n            logger.log(\"Continuing with React Grab installation...\");\n            logger.break();\n            agentIntegration = \"mcp\";\n          } else {\n            const { agent } = await prompts({\n              type: \"select\",\n              name: \"agent\",\n              message: `Which ${highlighter.info(\"agent\")} would you like to connect?`,\n              choices: [\n                ...AGENTS.map((innerAgent) => ({\n                  title: getAgentName(innerAgent),\n                  value: innerAgent,\n                })),\n                { title: \"Skip\", value: \"skip\" },\n              ],\n            });\n\n            if (agent === undefined) {\n              logger.break();\n              process.exit(1);\n            }\n\n            if (agent !== \"skip\") {\n              agentIntegration = agent as AgentIntegration;\n            }\n          }\n        }\n      }\n\n      const result = previewTransform(\n        projectInfo.projectRoot,\n        finalFramework,\n        finalNextRouterType,\n        agentIntegration,\n        false,\n        opts.force,\n      );\n\n      const packageJsonResult = previewPackageJsonTransform(\n        projectInfo.projectRoot,\n        agentIntegration,\n        projectInfo.installedAgents,\n        finalPackageManager,\n      );\n\n      if (!result.success) {\n        logger.break();\n        logger.error(result.message);\n        logger.error(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`);\n        logger.break();\n        process.exit(1);\n      }\n\n      const hasLayoutChanges =\n        !result.noChanges && result.originalContent && result.newContent;\n      const hasPackageJsonChanges =\n        packageJsonResult.success &&\n        !packageJsonResult.noChanges &&\n        packageJsonResult.originalContent &&\n        packageJsonResult.newContent;\n\n      if (hasLayoutChanges || hasPackageJsonChanges) {\n        logger.break();\n\n        if (hasLayoutChanges) {\n          printDiff(\n            result.filePath,\n            result.originalContent!,\n            result.newContent!,\n          );\n        }\n\n        if (hasPackageJsonChanges) {\n          if (hasLayoutChanges) {\n            logger.break();\n          }\n          printDiff(\n            packageJsonResult.filePath,\n            packageJsonResult.originalContent!,\n            packageJsonResult.newContent!,\n          );\n        }\n\n        logger.break();\n        logger.warn(\"Auto-detection may not be 100% accurate.\");\n        logger.warn(\"Please verify the changes before committing.\");\n\n        if (!isNonInteractive) {\n          logger.break();\n          const { proceed } = await prompts({\n            type: \"confirm\",\n            name: \"proceed\",\n            message: \"Apply these changes?\",\n            initial: true,\n          });\n\n          if (!proceed) {\n            logger.break();\n            logger.log(\"Changes cancelled.\");\n            logger.break();\n            process.exit(0);\n          }\n        }\n      }\n\n      if (agentsToRemove.length > 0) {\n        await removeAgents(agentsToRemove, opts.skipInstall);\n        projectInfo.installedAgents = projectInfo.installedAgents.filter(\n          (agent) => !agentsToRemove.includes(agent),\n        );\n      }\n\n      const shouldInstallReactGrab = !projectInfo.hasReactGrab;\n      const shouldInstallAgent =\n        agentIntegration !== \"none\" &&\n        !projectInfo.installedAgents.includes(agentIntegration);\n\n      if (!opts.skipInstall && (shouldInstallReactGrab || shouldInstallAgent)) {\n        installPackagesWithFeedback(\n          getPackagesToInstall(agentIntegration, shouldInstallReactGrab),\n          finalPackageManager,\n          projectInfo.projectRoot,\n        );\n      }\n\n      if (hasLayoutChanges) {\n        applyTransformWithFeedback(result);\n      }\n\n      if (hasPackageJsonChanges) {\n        applyPackageJsonWithFeedback(packageJsonResult);\n      }\n\n      logger.break();\n      logger.log(\n        `${highlighter.success(\"Success!\")} React Grab has been installed.`,\n      );\n      if (packageJsonResult.warning) {\n        logger.break();\n        logger.warn(packageJsonResult.warning);\n        logger.break();\n      } else {\n        logger.log(\"You may now start your development server.\");\n      }\n      logger.break();\n\n      reportToCli(\"completed\", {\n        framework: finalFramework,\n        packageManager: finalPackageManager,\n        router: finalNextRouterType,\n        agent: agentIntegration !== \"none\" ? agentIntegration : undefined,\n        isMonorepo: projectInfo.isMonorepo,\n      });\n    } catch (error) {\n      handleError(error);\n      reportToCli(\"error\", undefined, error as Error);\n    }\n  });\n"
  },
  {
    "path": "packages/cli/src/commands/remove.ts",
    "content": "import { Command } from \"commander\";\nimport pc from \"picocolors\";\nimport { detectNonInteractive } from \"../utils/is-non-interactive.js\";\nimport { prompts } from \"../utils/prompts.js\";\nimport { detectProject } from \"../utils/detect.js\";\nimport { printDiff } from \"../utils/diff.js\";\nimport { handleError } from \"../utils/handle-error.js\";\nimport { highlighter } from \"../utils/highlighter.js\";\nimport { getPackagesToUninstall, uninstallPackages } from \"../utils/install.js\";\nimport { logger } from \"../utils/logger.js\";\nimport { spinner } from \"../utils/spinner.js\";\nimport { AGENTS, getAgentDisplayName } from \"../utils/templates.js\";\nimport {\n  applyPackageJsonTransform,\n  applyTransform,\n  previewAgentRemoval,\n  previewPackageJsonAgentRemoval,\n} from \"../utils/transform.js\";\n\nconst VERSION = process.env.VERSION ?? \"0.0.1\";\n\nexport const remove = new Command()\n  .name(\"remove\")\n  .description(\"disconnect React Grab from your agent\")\n  .argument(\"[agent]\", `agent to disconnect (${AGENTS.join(\", \")}, mcp)`)\n  .option(\"-y, --yes\", \"skip confirmation prompts\", false)\n  .option(\n    \"-c, --cwd <cwd>\",\n    \"working directory (defaults to current directory)\",\n    process.cwd(),\n  )\n  .action(async (agentArg, opts) => {\n    console.log(\n      `${pc.magenta(\"✿\")} ${pc.bold(\"React Grab\")} ${pc.gray(VERSION)}`,\n    );\n    console.log();\n\n    try {\n      const cwd = opts.cwd;\n      const isNonInteractive = detectNonInteractive(opts.yes);\n\n      const preflightSpinner = spinner(\"Preflight checks.\").start();\n\n      const projectInfo = await detectProject(cwd);\n\n      if (!projectInfo.hasReactGrab) {\n        preflightSpinner.fail(\"React Grab is not installed.\");\n        logger.break();\n        logger.error(\n          `Run ${highlighter.info(\"react-grab init\")} first to install React Grab.`,\n        );\n        logger.break();\n        process.exit(1);\n      }\n\n      if (projectInfo.installedAgents.length === 0) {\n        preflightSpinner.succeed();\n        logger.break();\n        logger.warn(\"No agent connections are installed.\");\n        logger.break();\n        process.exit(0);\n      }\n\n      preflightSpinner.succeed();\n\n      let agentToRemove: string;\n\n      if (agentArg) {\n        if (!projectInfo.installedAgents.includes(agentArg)) {\n          logger.break();\n          logger.error(`Agent ${highlighter.info(agentArg)} is not installed.`);\n          logger.log(\n            `Installed agents: ${projectInfo.installedAgents.map(getAgentDisplayName).join(\", \")}`,\n          );\n          logger.break();\n          process.exit(1);\n        }\n        agentToRemove = agentArg;\n      } else if (!isNonInteractive) {\n        logger.break();\n        const { agent } = await prompts({\n          type: \"select\",\n          name: \"agent\",\n          message: `Which ${highlighter.info(\"agent\")} would you like to disconnect?`,\n          choices: projectInfo.installedAgents.map((innerAgent) => ({\n            title: getAgentDisplayName(innerAgent),\n            value: innerAgent,\n          })),\n        });\n\n        if (!agent) {\n          logger.break();\n          process.exit(1);\n        }\n\n        agentToRemove = agent;\n      } else {\n        logger.break();\n        logger.error(\"Please specify an agent to disconnect.\");\n        logger.error(\n          \"Installed agents: \" + projectInfo.installedAgents.join(\", \"),\n        );\n        logger.break();\n        process.exit(1);\n      }\n\n      const removingSpinner = spinner(\n        `Preparing to remove ${getAgentDisplayName(agentToRemove)}.`,\n      ).start();\n      removingSpinner.succeed();\n\n      const result = previewAgentRemoval(\n        projectInfo.projectRoot,\n        projectInfo.framework,\n        projectInfo.nextRouterType,\n        agentToRemove,\n      );\n\n      const packageJsonResult = previewPackageJsonAgentRemoval(\n        projectInfo.projectRoot,\n        agentToRemove,\n      );\n\n      const hasLayoutChanges =\n        result.success &&\n        !result.noChanges &&\n        result.originalContent &&\n        result.newContent;\n      const hasPackageJsonChanges =\n        packageJsonResult.success &&\n        !packageJsonResult.noChanges &&\n        packageJsonResult.originalContent &&\n        packageJsonResult.newContent;\n\n      if (hasLayoutChanges || hasPackageJsonChanges) {\n        logger.break();\n\n        if (hasLayoutChanges) {\n          printDiff(\n            result.filePath,\n            result.originalContent!,\n            result.newContent!,\n          );\n        }\n\n        if (hasPackageJsonChanges) {\n          if (hasLayoutChanges) {\n            logger.break();\n          }\n          printDiff(\n            packageJsonResult.filePath,\n            packageJsonResult.originalContent!,\n            packageJsonResult.newContent!,\n          );\n        }\n\n        if (!isNonInteractive) {\n          logger.break();\n          const { proceed } = await prompts({\n            type: \"confirm\",\n            name: \"proceed\",\n            message: \"Apply these changes?\",\n            initial: true,\n          });\n\n          if (!proceed) {\n            logger.break();\n            logger.log(\"Changes cancelled.\");\n            logger.break();\n            process.exit(0);\n          }\n        }\n      }\n\n      const packages = getPackagesToUninstall(agentToRemove);\n\n      if (packages.length > 0) {\n        const uninstallSpinner = spinner(\n          `Removing ${packages.join(\", \")}.`,\n        ).start();\n\n        try {\n          uninstallPackages(\n            packages,\n            projectInfo.packageManager,\n            projectInfo.projectRoot,\n          );\n          uninstallSpinner.succeed();\n        } catch (error) {\n          uninstallSpinner.fail();\n          handleError(error);\n        }\n      }\n\n      if (hasLayoutChanges) {\n        const writeSpinner = spinner(\n          `Applying changes to ${result.filePath}.`,\n        ).start();\n        const writeResult = applyTransform(result);\n        if (!writeResult.success) {\n          writeSpinner.fail();\n          logger.break();\n          logger.error(writeResult.error || \"Failed to write file.\");\n          logger.break();\n          process.exit(1);\n        }\n        writeSpinner.succeed();\n      }\n\n      if (hasPackageJsonChanges) {\n        const packageJsonSpinner = spinner(\n          `Applying changes to ${packageJsonResult.filePath}.`,\n        ).start();\n        const packageJsonWriteResult =\n          applyPackageJsonTransform(packageJsonResult);\n        if (!packageJsonWriteResult.success) {\n          packageJsonSpinner.fail();\n          logger.break();\n          logger.error(packageJsonWriteResult.error || \"Failed to write file.\");\n          logger.break();\n          process.exit(1);\n        }\n        packageJsonSpinner.succeed();\n      }\n\n      logger.break();\n      logger.log(\n        `${highlighter.success(\"Success!\")} ${getAgentDisplayName(agentToRemove)} has been removed.`,\n      );\n      logger.break();\n    } catch (error) {\n      handleError(error);\n    }\n  });\n"
  },
  {
    "path": "packages/cli/src/utils/cli-helpers.ts",
    "content": "import type { PackageManager } from \"./detect.js\";\nimport type {\n  PackageJsonTransformResult,\n  TransformResult,\n} from \"./transform.js\";\nimport { applyPackageJsonTransform, applyTransform } from \"./transform.js\";\nimport { handleError } from \"./handle-error.js\";\nimport { installPackages, uninstallPackages } from \"./install.js\";\nimport { logger } from \"./logger.js\";\nimport { spinner } from \"./spinner.js\";\nimport { getAgentDisplayName } from \"./templates.js\";\n\nexport const formatInstalledAgentNames = (agents: string[]): string =>\n  agents.map(getAgentDisplayName).join(\", \");\n\nexport const applyTransformWithFeedback = (\n  result: TransformResult,\n  message?: string,\n): void => {\n  const writeSpinner = spinner(\n    message ?? `Applying changes to ${result.filePath}.`,\n  ).start();\n  const writeResult = applyTransform(result);\n  if (!writeResult.success) {\n    writeSpinner.fail();\n    logger.break();\n    logger.error(writeResult.error || \"Failed to write file.\");\n    logger.break();\n    process.exit(1);\n  }\n  writeSpinner.succeed();\n};\n\nexport const applyPackageJsonWithFeedback = (\n  result: PackageJsonTransformResult,\n  message?: string,\n): void => {\n  const writeSpinner = spinner(\n    message ?? `Applying changes to ${result.filePath}.`,\n  ).start();\n  const writeResult = applyPackageJsonTransform(result);\n  if (!writeResult.success) {\n    writeSpinner.fail();\n    logger.break();\n    logger.error(writeResult.error || \"Failed to write file.\");\n    logger.break();\n    process.exit(1);\n  }\n  writeSpinner.succeed();\n};\n\nexport const installPackagesWithFeedback = (\n  packages: string[],\n  packageManager: PackageManager,\n  projectRoot: string,\n): void => {\n  if (packages.length === 0) return;\n  const installSpinner = spinner(`Installing ${packages.join(\", \")}.`).start();\n  try {\n    installPackages(packages, packageManager, projectRoot);\n    installSpinner.succeed();\n  } catch (error) {\n    installSpinner.fail();\n    handleError(error);\n  }\n};\n\nexport const uninstallPackagesWithFeedback = (\n  packages: string[],\n  packageManager: PackageManager,\n  projectRoot: string,\n): void => {\n  if (packages.length === 0) return;\n  const uninstallSpinner = spinner(`Removing ${packages.join(\", \")}.`).start();\n  try {\n    uninstallPackages(packages, packageManager, projectRoot);\n    uninstallSpinner.succeed();\n  } catch (error) {\n    uninstallSpinner.fail();\n    handleError(error);\n  }\n};\n"
  },
  {
    "path": "packages/cli/src/utils/constants.ts",
    "content": "export const MAX_SUGGESTIONS_COUNT = 30;\nexport const MAX_KEY_HOLD_DURATION_MS = 2000;\nexport const MAX_CONTEXT_LINES = 50;\n"
  },
  {
    "path": "packages/cli/src/utils/detect.ts",
    "content": "import { execSync } from \"node:child_process\";\nimport { existsSync, readdirSync, readFileSync } from \"node:fs\";\nimport { basename, dirname, join } from \"node:path\";\nimport { detect } from \"@antfu/ni\";\nimport ignore from \"ignore\";\n\nexport type PackageManager = \"npm\" | \"yarn\" | \"pnpm\" | \"bun\";\nexport type Framework = \"next\" | \"vite\" | \"tanstack\" | \"webpack\" | \"unknown\";\nexport type NextRouterType = \"app\" | \"pages\" | \"unknown\";\nexport type UnsupportedFramework =\n  | \"remix\"\n  | \"astro\"\n  | \"sveltekit\"\n  | \"gatsby\"\n  | null;\n\nexport interface ProjectInfo {\n  packageManager: PackageManager;\n  framework: Framework;\n  nextRouterType: NextRouterType;\n  isMonorepo: boolean;\n  projectRoot: string;\n  hasReactGrab: boolean;\n  installedAgents: string[];\n  unsupportedFramework: UnsupportedFramework;\n}\n\nconst VALID_PACKAGE_MANAGERS: ReadonlySet<string> = new Set([\n  \"npm\",\n  \"yarn\",\n  \"pnpm\",\n  \"bun\",\n]);\n\nexport const detectPackageManager = async (\n  projectRoot: string,\n): Promise<PackageManager> => {\n  const detected = await detect({ cwd: projectRoot });\n  if (detected) {\n    // @antfu/ni returns versioned agents like \"pnpm@6\" or \"yarn@berry\"\n    const managerName = detected.split(\"@\")[0];\n    if (VALID_PACKAGE_MANAGERS.has(managerName)) {\n      return managerName as PackageManager;\n    }\n  }\n  return \"npm\";\n};\n\nexport const detectFramework = (projectRoot: string): Framework => {\n  const packageJsonPath = join(projectRoot, \"package.json\");\n\n  if (!existsSync(packageJsonPath)) {\n    return \"unknown\";\n  }\n\n  try {\n    const packageJson = JSON.parse(readFileSync(packageJsonPath, \"utf-8\"));\n    const allDependencies = {\n      ...packageJson.dependencies,\n      ...packageJson.devDependencies,\n    };\n\n    if (allDependencies[\"next\"]) {\n      return \"next\";\n    }\n\n    if (allDependencies[\"@tanstack/react-start\"]) {\n      return \"tanstack\";\n    }\n\n    if (allDependencies[\"vite\"]) {\n      return \"vite\";\n    }\n\n    if (allDependencies[\"webpack\"]) {\n      return \"webpack\";\n    }\n\n    return \"unknown\";\n  } catch {\n    return \"unknown\";\n  }\n};\n\nexport const detectNextRouterType = (projectRoot: string): NextRouterType => {\n  const hasAppDir = existsSync(join(projectRoot, \"app\"));\n  const hasSrcAppDir = existsSync(join(projectRoot, \"src\", \"app\"));\n  const hasPagesDir = existsSync(join(projectRoot, \"pages\"));\n  const hasSrcPagesDir = existsSync(join(projectRoot, \"src\", \"pages\"));\n\n  if (hasAppDir || hasSrcAppDir) {\n    return \"app\";\n  }\n\n  if (hasPagesDir || hasSrcPagesDir) {\n    return \"pages\";\n  }\n\n  return \"unknown\";\n};\n\nexport const detectMonorepo = (projectRoot: string): boolean => {\n  if (existsSync(join(projectRoot, \"pnpm-workspace.yaml\"))) {\n    return true;\n  }\n\n  if (existsSync(join(projectRoot, \"lerna.json\"))) {\n    return true;\n  }\n\n  const packageJsonPath = join(projectRoot, \"package.json\");\n  if (existsSync(packageJsonPath)) {\n    try {\n      const packageJson = JSON.parse(readFileSync(packageJsonPath, \"utf-8\"));\n      if (packageJson.workspaces) {\n        return true;\n      }\n    } catch {\n      return false;\n    }\n  }\n\n  return false;\n};\n\nexport interface WorkspaceProject {\n  name: string;\n  path: string;\n  framework: Framework;\n  hasReact: boolean;\n}\n\nconst getWorkspacePatterns = (projectRoot: string): string[] => {\n  const patterns: string[] = [];\n\n  const pnpmWorkspacePath = join(projectRoot, \"pnpm-workspace.yaml\");\n  if (existsSync(pnpmWorkspacePath)) {\n    const content = readFileSync(pnpmWorkspacePath, \"utf-8\");\n    const lines = content.split(\"\\n\");\n    let inPackages = false;\n\n    for (const line of lines) {\n      if (line.match(/^packages:\\s*$/)) {\n        inPackages = true;\n        continue;\n      }\n      if (inPackages) {\n        if (line.match(/^[a-zA-Z]/) || line.trim() === \"\") {\n          if (line.match(/^[a-zA-Z]/)) inPackages = false;\n          continue;\n        }\n        const match = line.match(/^\\s*-\\s*['\"]?([^'\"#\\n]+?)['\"]?\\s*$/);\n        if (match) {\n          patterns.push(match[1].trim());\n        }\n      }\n    }\n  }\n\n  const lernaJsonPath = join(projectRoot, \"lerna.json\");\n  if (existsSync(lernaJsonPath)) {\n    try {\n      const lernaJson = JSON.parse(readFileSync(lernaJsonPath, \"utf-8\"));\n      if (Array.isArray(lernaJson.packages)) {\n        patterns.push(...lernaJson.packages);\n      }\n    } catch {}\n  }\n\n  const packageJsonPath = join(projectRoot, \"package.json\");\n  if (existsSync(packageJsonPath)) {\n    try {\n      const packageJson = JSON.parse(readFileSync(packageJsonPath, \"utf-8\"));\n      if (Array.isArray(packageJson.workspaces)) {\n        patterns.push(...packageJson.workspaces);\n      } else if (packageJson.workspaces?.packages) {\n        patterns.push(...packageJson.workspaces.packages);\n      }\n    } catch {}\n  }\n\n  return [...new Set(patterns)];\n};\n\nconst expandWorkspacePattern = (\n  projectRoot: string,\n  pattern: string,\n): string[] => {\n  const isGlob = pattern.endsWith(\"/*\");\n  const cleanPattern = pattern.replace(/\\/\\*$/, \"\");\n  const basePath = join(projectRoot, cleanPattern);\n\n  if (!existsSync(basePath)) return [];\n\n  if (!isGlob) {\n    const hasPackageJson = existsSync(join(basePath, \"package.json\"));\n    return hasPackageJson ? [basePath] : [];\n  }\n\n  const results: string[] = [];\n  try {\n    const entries = readdirSync(basePath, { withFileTypes: true });\n    for (const entry of entries) {\n      if (!entry.isDirectory()) continue;\n      const packageJsonPath = join(basePath, entry.name, \"package.json\");\n      if (existsSync(packageJsonPath)) {\n        results.push(join(basePath, entry.name));\n      }\n    }\n  } catch {\n    return results;\n  }\n  return results;\n};\n\nconst hasReactDependency = (projectPath: string): boolean => {\n  const packageJsonPath = join(projectPath, \"package.json\");\n  if (!existsSync(packageJsonPath)) return false;\n\n  try {\n    const packageJson = JSON.parse(readFileSync(packageJsonPath, \"utf-8\"));\n    const allDeps = {\n      ...packageJson.dependencies,\n      ...packageJson.devDependencies,\n    };\n    return Boolean(allDeps[\"react\"] || allDeps[\"react-dom\"]);\n  } catch {\n    return false;\n  }\n};\n\nconst buildReactProject = (projectPath: string): WorkspaceProject | null => {\n  const framework = detectFramework(projectPath);\n  const hasReact = hasReactDependency(projectPath);\n  if (!hasReact && framework === \"unknown\") return null;\n\n  let name = basename(projectPath);\n  const packageJsonPath = join(projectPath, \"package.json\");\n  try {\n    const packageJson = JSON.parse(readFileSync(packageJsonPath, \"utf-8\"));\n    name = packageJson.name || name;\n  } catch {}\n\n  return { name, path: projectPath, framework, hasReact };\n};\n\nconst findWorkspaceProjects = (projectRoot: string): WorkspaceProject[] => {\n  const patterns = getWorkspacePatterns(projectRoot);\n  const projects: WorkspaceProject[] = [];\n\n  for (const pattern of patterns) {\n    for (const projectPath of expandWorkspacePattern(projectRoot, pattern)) {\n      const project = buildReactProject(projectPath);\n      if (project) projects.push(project);\n    }\n  }\n\n  return projects;\n};\n\nconst ALWAYS_IGNORED_DIRECTORIES = [\n  \"node_modules\",\n  \".git\",\n  \".next\",\n  \".cache\",\n  \".turbo\",\n  \"dist\",\n  \"build\",\n  \"coverage\",\n  \"test-results\",\n];\n\nconst loadGitignore = (projectRoot: string): ReturnType<typeof ignore> => {\n  const ignorer = ignore().add(ALWAYS_IGNORED_DIRECTORIES);\n  const gitignorePath = join(projectRoot, \".gitignore\");\n  if (existsSync(gitignorePath)) {\n    try {\n      ignorer.add(readFileSync(gitignorePath, \"utf-8\"));\n    } catch {}\n  }\n  return ignorer;\n};\n\nconst scanDirectoryForProjects = (\n  rootDirectory: string,\n  ignorer: ReturnType<typeof ignore>,\n  maxDepth: number,\n  currentDepth: number = 0,\n): WorkspaceProject[] => {\n  if (currentDepth >= maxDepth) return [];\n  if (!existsSync(rootDirectory)) return [];\n\n  const projects: WorkspaceProject[] = [];\n\n  try {\n    const entries = readdirSync(rootDirectory, { withFileTypes: true });\n    for (const entry of entries) {\n      if (!entry.isDirectory()) continue;\n      if (ignorer.ignores(entry.name)) continue;\n\n      const entryPath = join(rootDirectory, entry.name);\n      const hasPackageJson = existsSync(join(entryPath, \"package.json\"));\n\n      if (hasPackageJson) {\n        const project = buildReactProject(entryPath);\n        if (project) {\n          projects.push(project);\n          continue;\n        }\n      }\n\n      projects.push(\n        ...scanDirectoryForProjects(\n          entryPath,\n          ignorer,\n          maxDepth,\n          currentDepth + 1,\n        ),\n      );\n    }\n  } catch {\n    return projects;\n  }\n\n  return projects;\n};\n\nconst MAX_SCAN_DEPTH = 2;\n\nexport const findReactProjects = (projectRoot: string): WorkspaceProject[] => {\n  if (detectMonorepo(projectRoot)) {\n    const workspaceProjects = findWorkspaceProjects(projectRoot);\n    if (workspaceProjects.length > 0) {\n      return workspaceProjects;\n    }\n  }\n\n  const ignorer = loadGitignore(projectRoot);\n  const scannedProjects = scanDirectoryForProjects(\n    projectRoot,\n    ignorer,\n    MAX_SCAN_DEPTH,\n  );\n  if (scannedProjects.length > 0) {\n    return scannedProjects;\n  }\n\n  let currentDirectory = dirname(projectRoot);\n  while (currentDirectory !== dirname(currentDirectory)) {\n    const parentProject = buildReactProject(currentDirectory);\n    if (parentProject) {\n      return [parentProject];\n    }\n    currentDirectory = dirname(currentDirectory);\n  }\n\n  return [];\n};\n\nconst hasReactGrabInFile = (filePath: string): boolean => {\n  if (!existsSync(filePath)) return false;\n  try {\n    const content = readFileSync(filePath, \"utf-8\");\n    const fuzzyPatterns = [\n      /[\"'`][^\"'`]*react-grab/,\n      /react-grab[^\"'`]*[\"'`]/,\n      /<[^>]*react-grab/i,\n      /import[^;]*react-grab/i,\n      /require[^)]*react-grab/i,\n      /from\\s+[^;]*react-grab/i,\n      /src[^>]*react-grab/i,\n    ];\n    return fuzzyPatterns.some((pattern) => pattern.test(content));\n  } catch {\n    return false;\n  }\n};\n\nexport const detectReactGrab = (projectRoot: string): boolean => {\n  const packageJsonPath = join(projectRoot, \"package.json\");\n\n  if (existsSync(packageJsonPath)) {\n    try {\n      const packageJson = JSON.parse(readFileSync(packageJsonPath, \"utf-8\"));\n      const allDependencies = {\n        ...packageJson.dependencies,\n        ...packageJson.devDependencies,\n      };\n      if (allDependencies[\"react-grab\"]) {\n        return true;\n      }\n    } catch {}\n  }\n\n  const filesToCheck = [\n    join(projectRoot, \"app\", \"layout.tsx\"),\n    join(projectRoot, \"app\", \"layout.jsx\"),\n    join(projectRoot, \"src\", \"app\", \"layout.tsx\"),\n    join(projectRoot, \"src\", \"app\", \"layout.jsx\"),\n    join(projectRoot, \"pages\", \"_document.tsx\"),\n    join(projectRoot, \"pages\", \"_document.jsx\"),\n    join(projectRoot, \"instrumentation-client.ts\"),\n    join(projectRoot, \"instrumentation-client.js\"),\n    join(projectRoot, \"src\", \"instrumentation-client.ts\"),\n    join(projectRoot, \"src\", \"instrumentation-client.js\"),\n    join(projectRoot, \"index.html\"),\n    join(projectRoot, \"public\", \"index.html\"),\n    join(projectRoot, \"src\", \"index.tsx\"),\n    join(projectRoot, \"src\", \"index.ts\"),\n    join(projectRoot, \"src\", \"main.tsx\"),\n    join(projectRoot, \"src\", \"main.ts\"),\n    join(projectRoot, \"src\", \"routes\", \"__root.tsx\"),\n    join(projectRoot, \"src\", \"routes\", \"__root.jsx\"),\n    join(projectRoot, \"app\", \"routes\", \"__root.tsx\"),\n    join(projectRoot, \"app\", \"routes\", \"__root.jsx\"),\n  ];\n\n  return filesToCheck.some(hasReactGrabInFile);\n};\n\nconst AGENT_PACKAGES = [\n  \"@react-grab/claude-code\",\n  \"@react-grab/cursor\",\n  \"@react-grab/opencode\",\n  \"@react-grab/codex\",\n  \"@react-grab/gemini\",\n  \"@react-grab/amp\",\n  \"@react-grab/droid\",\n  \"@react-grab/copilot\",\n  \"@react-grab/mcp\",\n];\n\nexport const detectUnsupportedFramework = (\n  projectRoot: string,\n): UnsupportedFramework => {\n  const packageJsonPath = join(projectRoot, \"package.json\");\n\n  if (!existsSync(packageJsonPath)) {\n    return null;\n  }\n\n  try {\n    const packageJson = JSON.parse(readFileSync(packageJsonPath, \"utf-8\"));\n    const allDependencies = {\n      ...packageJson.dependencies,\n      ...packageJson.devDependencies,\n    };\n\n    if (allDependencies[\"@remix-run/react\"] || allDependencies[\"remix\"]) {\n      return \"remix\";\n    }\n\n    if (allDependencies[\"astro\"]) {\n      return \"astro\";\n    }\n\n    if (allDependencies[\"@sveltejs/kit\"]) {\n      return \"sveltekit\";\n    }\n\n    if (allDependencies[\"gatsby\"]) {\n      return \"gatsby\";\n    }\n\n    return null;\n  } catch {\n    return null;\n  }\n};\n\nexport const detectInstalledAgents = (projectRoot: string): string[] => {\n  const packageJsonPath = join(projectRoot, \"package.json\");\n\n  if (!existsSync(packageJsonPath)) {\n    return [];\n  }\n\n  try {\n    const packageJson = JSON.parse(readFileSync(packageJsonPath, \"utf-8\"));\n    const allDependencies = {\n      ...packageJson.dependencies,\n      ...packageJson.devDependencies,\n    };\n\n    return AGENT_PACKAGES.filter((agent) =>\n      Boolean(allDependencies[agent]),\n    ).map((agent) => agent.replace(\"@react-grab/\", \"\"));\n  } catch {\n    return [];\n  }\n};\n\nexport type AgentCLI =\n  | \"claude\"\n  | \"cursor-agent\"\n  | \"opencode\"\n  | \"codex\"\n  | \"gemini\"\n  | \"amp\"\n  | \"copilot\"\n  | \"droid\";\n\nconst AGENT_CLI_COMMANDS: AgentCLI[] = [\n  \"claude\",\n  \"cursor-agent\",\n  \"opencode\",\n  \"codex\",\n  \"gemini\",\n  \"amp\",\n  \"copilot\",\n  \"droid\",\n];\n\nconst isCommandAvailable = (command: string): boolean => {\n  try {\n    execSync(`which ${command}`, { stdio: \"ignore\" });\n    return true;\n  } catch {\n    return false;\n  }\n};\n\nexport const detectAvailableAgentCLIs = (): AgentCLI[] => {\n  return AGENT_CLI_COMMANDS.filter(isCommandAvailable);\n};\n\nexport const detectProject = async (\n  projectRoot: string = process.cwd(),\n): Promise<ProjectInfo> => {\n  const framework = detectFramework(projectRoot);\n  const packageManager = await detectPackageManager(projectRoot);\n\n  return {\n    packageManager,\n    framework,\n    nextRouterType:\n      framework === \"next\" ? detectNextRouterType(projectRoot) : \"unknown\",\n    isMonorepo: detectMonorepo(projectRoot),\n    projectRoot,\n    hasReactGrab: detectReactGrab(projectRoot),\n    installedAgents: detectInstalledAgents(projectRoot),\n    unsupportedFramework: detectUnsupportedFramework(projectRoot),\n  };\n};\n"
  },
  {
    "path": "packages/cli/src/utils/diff.ts",
    "content": "interface DiffLine {\n  type: \"added\" | \"removed\" | \"unchanged\";\n  content: string;\n  lineNumber?: number;\n}\n\nconst RED = \"\\x1b[31m\";\nconst GREEN = \"\\x1b[32m\";\nconst GRAY = \"\\x1b[90m\";\nconst RESET = \"\\x1b[0m\";\nconst BOLD = \"\\x1b[1m\";\n\nexport const generateDiff = (\n  originalContent: string,\n  newContent: string,\n): DiffLine[] => {\n  const originalLines = originalContent.split(\"\\n\");\n  const newLines = newContent.split(\"\\n\");\n  const diff: DiffLine[] = [];\n\n  const maxLength = Math.max(originalLines.length, newLines.length);\n\n  let originalIndex = 0;\n  let newIndex = 0;\n\n  while (originalIndex < originalLines.length || newIndex < newLines.length) {\n    const originalLine = originalLines[originalIndex];\n    const newLine = newLines[newIndex];\n\n    if (originalLine === newLine) {\n      diff.push({\n        type: \"unchanged\",\n        content: originalLine,\n        lineNumber: newIndex + 1,\n      });\n      originalIndex++;\n      newIndex++;\n    } else if (originalLine === undefined) {\n      diff.push({ type: \"added\", content: newLine, lineNumber: newIndex + 1 });\n      newIndex++;\n    } else if (newLine === undefined) {\n      diff.push({ type: \"removed\", content: originalLine });\n      originalIndex++;\n    } else {\n      const originalInNew = newLines.indexOf(originalLine, newIndex);\n      const newInOriginal = originalLines.indexOf(newLine, originalIndex);\n\n      if (\n        originalInNew !== -1 &&\n        (newInOriginal === -1 ||\n          originalInNew - newIndex < newInOriginal - originalIndex)\n      ) {\n        while (newIndex < originalInNew) {\n          diff.push({\n            type: \"added\",\n            content: newLines[newIndex],\n            lineNumber: newIndex + 1,\n          });\n          newIndex++;\n        }\n      } else if (newInOriginal !== -1) {\n        while (originalIndex < newInOriginal) {\n          diff.push({ type: \"removed\", content: originalLines[originalIndex] });\n          originalIndex++;\n        }\n      } else {\n        diff.push({ type: \"removed\", content: originalLine });\n        diff.push({\n          type: \"added\",\n          content: newLine,\n          lineNumber: newIndex + 1,\n        });\n        originalIndex++;\n        newIndex++;\n      }\n    }\n  }\n\n  return diff;\n};\n\nexport const formatDiff = (\n  diff: DiffLine[],\n  contextLines: number = 3,\n): string => {\n  const lines: string[] = [];\n  let lastPrintedIndex = -1;\n  let hasChanges = false;\n\n  const changedIndices = diff\n    .map((line, index) => (line.type !== \"unchanged\" ? index : -1))\n    .filter((index) => index !== -1);\n\n  if (changedIndices.length === 0) {\n    return `${GRAY}No changes${RESET}`;\n  }\n\n  for (const changedIndex of changedIndices) {\n    const startContext = Math.max(0, changedIndex - contextLines);\n    const endContext = Math.min(diff.length - 1, changedIndex + contextLines);\n\n    if (startContext > lastPrintedIndex + 1 && lastPrintedIndex !== -1) {\n      lines.push(`${GRAY}  ...${RESET}`);\n    }\n\n    for (\n      let lineIndex = Math.max(startContext, lastPrintedIndex + 1);\n      lineIndex <= endContext;\n      lineIndex++\n    ) {\n      const diffLine = diff[lineIndex];\n\n      if (diffLine.type === \"added\") {\n        lines.push(`${GREEN}+ ${diffLine.content}${RESET}`);\n        hasChanges = true;\n      } else if (diffLine.type === \"removed\") {\n        lines.push(`${RED}- ${diffLine.content}${RESET}`);\n        hasChanges = true;\n      } else {\n        lines.push(`${GRAY}  ${diffLine.content}${RESET}`);\n      }\n\n      lastPrintedIndex = lineIndex;\n    }\n  }\n\n  return hasChanges ? lines.join(\"\\n\") : `${GRAY}No changes${RESET}`;\n};\n\nexport const printDiff = (\n  filePath: string,\n  originalContent: string,\n  newContent: string,\n): void => {\n  console.log(`\\n${BOLD}File: ${filePath}${RESET}`);\n  console.log(\"─\".repeat(60));\n\n  const diff = generateDiff(originalContent, newContent);\n  console.log(formatDiff(diff));\n\n  console.log(\"─\".repeat(60));\n};\n"
  },
  {
    "path": "packages/cli/src/utils/handle-error.ts",
    "content": "import { logger } from \"./logger.js\";\n\nexport const handleError = (error: unknown) => {\n  logger.break();\n  logger.error(\n    \"Something went wrong. Please check the error below for more details.\",\n  );\n  logger.error(\"If the problem persists, please open an issue on GitHub.\");\n  logger.error(\"\");\n  if (error instanceof Error) {\n    logger.error(error.message);\n  }\n  logger.break();\n  process.exit(1);\n};\n"
  },
  {
    "path": "packages/cli/src/utils/highlighter.ts",
    "content": "import pc from \"picocolors\";\n\nexport const highlighter = {\n  error: pc.red,\n  warn: pc.yellow,\n  info: pc.cyan,\n  success: pc.green,\n  dim: pc.dim,\n};\n"
  },
  {
    "path": "packages/cli/src/utils/install-mcp.ts",
    "content": "import fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport process from \"node:process\";\nimport * as jsonc from \"jsonc-parser\";\nimport * as TOML from \"smol-toml\";\nimport { highlighter } from \"./highlighter.js\";\nimport { logger } from \"./logger.js\";\nimport { prompts } from \"./prompts.js\";\nimport { spinner } from \"./spinner.js\";\n\nconst SERVER_NAME = \"react-grab-mcp\";\nconst PACKAGE_NAME = \"@react-grab/mcp\";\n\nexport interface ClientDefinition {\n  name: string;\n  configPath: string;\n  configKey: string;\n  format: \"json\" | \"toml\";\n  serverConfig: Record<string, unknown>;\n}\n\ninterface InstallResult {\n  client: string;\n  configPath: string;\n  success: boolean;\n  error?: string;\n}\n\nconst getXdgConfigHome = (): string =>\n  process.env.XDG_CONFIG_HOME || path.join(os.homedir(), \".config\");\n\nconst getBaseDir = (): string => {\n  const homeDir = os.homedir();\n  if (process.platform === \"win32\") {\n    return process.env.APPDATA || path.join(homeDir, \"AppData\", \"Roaming\");\n  }\n  if (process.platform === \"darwin\") {\n    return path.join(homeDir, \"Library\", \"Application Support\");\n  }\n  return getXdgConfigHome();\n};\n\nconst getZedConfigPath = (): string => {\n  if (process.platform === \"win32\") {\n    return path.join(getBaseDir(), \"Zed\", \"settings.json\");\n  }\n  return path.join(os.homedir(), \".config\", \"zed\", \"settings.json\");\n};\n\nexport const getOpenCodeConfigPath = (): string => {\n  const configDir = path.join(getXdgConfigHome(), \"opencode\");\n  const jsoncPath = path.join(configDir, \"opencode.jsonc\");\n  const jsonPath = path.join(configDir, \"opencode.json\");\n\n  if (fs.existsSync(jsoncPath)) return jsoncPath;\n  if (fs.existsSync(jsonPath)) return jsonPath;\n  return jsoncPath;\n};\n\nconst getClients = (): ClientDefinition[] => {\n  const homeDir = os.homedir();\n  const baseDir = getBaseDir();\n\n  const stdioConfig = {\n    command: \"npx\",\n    args: [\"-y\", PACKAGE_NAME, \"--stdio\"],\n  };\n\n  return [\n    {\n      name: \"Claude Code\",\n      configPath: path.join(homeDir, \".claude.json\"),\n      configKey: \"mcpServers\",\n      format: \"json\",\n      serverConfig: stdioConfig,\n    },\n    {\n      name: \"Codex\",\n      configPath: path.join(\n        process.env.CODEX_HOME || path.join(homeDir, \".codex\"),\n        \"config.toml\",\n      ),\n      configKey: \"mcp_servers\",\n      format: \"toml\",\n      serverConfig: stdioConfig,\n    },\n    {\n      name: \"Cursor\",\n      configPath: path.join(homeDir, \".cursor\", \"mcp.json\"),\n      configKey: \"mcpServers\",\n      format: \"json\",\n      serverConfig: stdioConfig,\n    },\n    {\n      name: \"OpenCode\",\n      configPath: getOpenCodeConfigPath(),\n      configKey: \"mcp\",\n      format: \"json\",\n      serverConfig: {\n        type: \"local\",\n        command: [\"npx\", \"-y\", PACKAGE_NAME, \"--stdio\"],\n      },\n    },\n    {\n      name: \"VS Code\",\n      configPath: path.join(baseDir, \"Code\", \"User\", \"mcp.json\"),\n      configKey: \"servers\",\n      format: \"json\",\n      serverConfig: { type: \"stdio\", ...stdioConfig },\n    },\n    {\n      name: \"Amp\",\n      configPath: path.join(homeDir, \".config\", \"amp\", \"settings.json\"),\n      configKey: \"amp.mcpServers\",\n      format: \"json\",\n      serverConfig: stdioConfig,\n    },\n    {\n      name: \"Droid\",\n      configPath: path.join(homeDir, \".factory\", \"mcp.json\"),\n      configKey: \"mcpServers\",\n      format: \"json\",\n      serverConfig: { type: \"stdio\", ...stdioConfig },\n    },\n    {\n      name: \"Windsurf\",\n      configPath: path.join(homeDir, \".codeium\", \"windsurf\", \"mcp_config.json\"),\n      configKey: \"mcpServers\",\n      format: \"json\",\n      serverConfig: stdioConfig,\n    },\n    {\n      name: \"Zed\",\n      configPath: getZedConfigPath(),\n      configKey: \"context_servers\",\n      format: \"json\",\n      serverConfig: { source: \"custom\", ...stdioConfig, env: {} },\n    },\n  ];\n};\n\nconst ensureDirectory = (filePath: string): void => {\n  const directory = path.dirname(filePath);\n  if (!fs.existsSync(directory)) {\n    fs.mkdirSync(directory, { recursive: true });\n  }\n};\n\nconst JSONC_FORMAT_OPTIONS: jsonc.FormattingOptions = {\n  tabSize: 2,\n  insertSpaces: true,\n};\n\nexport const upsertIntoJsonc = (\n  filePath: string,\n  content: string,\n  configKey: string,\n  serverName: string,\n  serverConfig: Record<string, unknown>,\n): void => {\n  const edits = jsonc.modify(content, [configKey, serverName], serverConfig, {\n    formattingOptions: JSONC_FORMAT_OPTIONS,\n  });\n  fs.writeFileSync(filePath, jsonc.applyEdits(content, edits));\n};\n\nexport const installJsonClient = (client: ClientDefinition): void => {\n  ensureDirectory(client.configPath);\n\n  const content = fs.existsSync(client.configPath)\n    ? fs.readFileSync(client.configPath, \"utf8\")\n    : \"{}\";\n\n  upsertIntoJsonc(\n    client.configPath,\n    content,\n    client.configKey,\n    SERVER_NAME,\n    client.serverConfig,\n  );\n};\n\nexport const installTomlClient = (client: ClientDefinition): void => {\n  ensureDirectory(client.configPath);\n\n  const existingConfig: Record<string, unknown> = fs.existsSync(\n    client.configPath,\n  )\n    ? TOML.parse(fs.readFileSync(client.configPath, \"utf8\"))\n    : {};\n\n  const serverSection = (existingConfig[client.configKey] ?? {}) as Record<\n    string,\n    unknown\n  >;\n  serverSection[SERVER_NAME] = client.serverConfig;\n  existingConfig[client.configKey] = serverSection;\n\n  fs.writeFileSync(client.configPath, TOML.stringify(existingConfig));\n};\n\nexport const getMcpClientNames = (): string[] =>\n  getClients().map((client) => client.name);\n\nexport const installMcpServers = (\n  selectedClients?: string[],\n): InstallResult[] => {\n  const allClients = getClients();\n  const clients = selectedClients\n    ? allClients.filter((client) => selectedClients.includes(client.name))\n    : allClients;\n  const results: InstallResult[] = [];\n\n  const installSpinner = spinner(\"Installing MCP server.\").start();\n\n  for (const client of clients) {\n    try {\n      if (client.format === \"toml\") {\n        installTomlClient(client);\n      } else {\n        installJsonClient(client);\n      }\n      results.push({\n        client: client.name,\n        configPath: client.configPath,\n        success: true,\n      });\n    } catch (error) {\n      const message = error instanceof Error ? error.message : String(error);\n      results.push({\n        client: client.name,\n        configPath: client.configPath,\n        success: false,\n        error: message,\n      });\n    }\n  }\n\n  const successCount = results.filter((result) => result.success).length;\n\n  if (successCount < results.length) {\n    installSpinner.warn(\n      `Installed to ${successCount}/${results.length} agents.`,\n    );\n  } else {\n    installSpinner.succeed(`Installed to ${successCount} agents.`);\n  }\n\n  for (const result of results) {\n    if (result.success) {\n      logger.log(\n        `  ${highlighter.success(\"\\u2713\")} ${result.client} ${highlighter.dim(\"\\u2192\")} ${highlighter.dim(result.configPath)}`,\n      );\n    } else {\n      logger.log(\n        `  ${highlighter.error(\"\\u2717\")} ${result.client} ${highlighter.dim(\"\\u2192\")} ${result.error}`,\n      );\n    }\n  }\n\n  return results;\n};\n\nexport const promptConnectionMode = async (): Promise<\n  \"mcp\" | \"legacy\" | undefined\n> => {\n  const { connectionMode } = await prompts({\n    type: \"select\",\n    name: \"connectionMode\",\n    message: \"How would you like to connect?\",\n    choices: [\n      {\n        title: `MCP ${highlighter.dim(\"(recommended)\")}`,\n        description: \"Installs to all supported agents at once\",\n        value: \"mcp\",\n      },\n      {\n        title: \"Legacy\",\n        description: \"Install a per-project agent package\",\n        value: \"legacy\",\n      },\n    ],\n  });\n\n  return connectionMode as \"mcp\" | \"legacy\" | undefined;\n};\n\nexport const promptMcpInstall = async (): Promise<boolean> => {\n  const clientNames = getMcpClientNames();\n  const { selectedAgents } = await prompts({\n    type: \"multiselect\",\n    name: \"selectedAgents\",\n    message: \"Select agents to install MCP server for:\",\n    choices: clientNames.map((name) => ({\n      title: name,\n      value: name,\n      selected: true,\n    })),\n  });\n\n  if (selectedAgents === undefined || selectedAgents.length === 0) {\n    return false;\n  }\n\n  logger.break();\n  const results = installMcpServers(selectedAgents);\n  const hasSuccess = results.some((result) => result.success);\n  return hasSuccess;\n};\n"
  },
  {
    "path": "packages/cli/src/utils/install.ts",
    "content": "import { execSync } from \"node:child_process\";\nimport type { PackageManager } from \"./detect.js\";\nimport type { AgentIntegration } from \"./templates.js\";\n\nconst INSTALL_COMMANDS: Record<PackageManager, string> = {\n  npm: \"npm install\",\n  yarn: \"yarn add\",\n  pnpm: \"pnpm add\",\n  bun: \"bun add\",\n};\n\nconst UNINSTALL_COMMANDS: Record<PackageManager, string> = {\n  npm: \"npm uninstall\",\n  yarn: \"yarn remove\",\n  pnpm: \"pnpm remove\",\n  bun: \"bun remove\",\n};\n\nexport const installPackages = (\n  packages: string[],\n  packageManager: PackageManager,\n  projectRoot: string,\n  isDev: boolean = true,\n): void => {\n  if (packages.length === 0) {\n    return;\n  }\n\n  const command = INSTALL_COMMANDS[packageManager];\n  const devFlag = isDev ? \" -D\" : \"\";\n  const fullCommand = `${command}${devFlag} ${packages.join(\" \")}`;\n\n  console.log(`Running: ${fullCommand}\\n`);\n\n  execSync(fullCommand, {\n    cwd: projectRoot,\n    stdio: \"inherit\",\n    env: { ...process.env, REACT_GRAB_INIT: \"1\" },\n  });\n};\n\nexport const getPackagesToInstall = (\n  agent: AgentIntegration,\n  includeReactGrab: boolean = true,\n): string[] => {\n  const packages: string[] = [];\n\n  if (includeReactGrab) {\n    packages.push(\"react-grab\");\n  }\n\n  if (agent !== \"none\") {\n    packages.push(`@react-grab/${agent}`);\n  }\n\n  return packages;\n};\n\nexport const uninstallPackages = (\n  packages: string[],\n  packageManager: PackageManager,\n  projectRoot: string,\n): void => {\n  if (packages.length === 0) {\n    return;\n  }\n\n  const command = UNINSTALL_COMMANDS[packageManager];\n  const fullCommand = `${command} ${packages.join(\" \")}`;\n\n  console.log(`Running: ${fullCommand}\\n`);\n\n  execSync(fullCommand, {\n    cwd: projectRoot,\n    stdio: \"inherit\",\n  });\n};\n\nexport const getPackagesToUninstall = (agent: string): string[] => {\n  return [`@react-grab/${agent}`];\n};\n"
  },
  {
    "path": "packages/cli/src/utils/is-non-interactive.ts",
    "content": "const AGENT_ENVIRONMENT_VARIABLES = [\n  \"CI\",\n  \"CLAUDECODE\",\n  \"CURSOR_AGENT\",\n  \"CODEX_CI\",\n  \"OPENCODE\",\n  \"AMP_HOME\",\n  \"AMI\",\n] as const;\n\nconst isEnvironmentVariableSet = (variable: string): boolean =>\n  Boolean(process.env[variable]);\n\nexport const detectNonInteractive = (yesFlag: boolean): boolean =>\n  yesFlag ||\n  AGENT_ENVIRONMENT_VARIABLES.some(isEnvironmentVariableSet) ||\n  !process.stdin.isTTY;\n"
  },
  {
    "path": "packages/cli/src/utils/logger.ts",
    "content": "import { highlighter } from \"./highlighter.js\";\n\nexport const logger = {\n  error(...args: unknown[]) {\n    console.log(highlighter.error(args.join(\" \")));\n  },\n  warn(...args: unknown[]) {\n    console.log(highlighter.warn(args.join(\" \")));\n  },\n  info(...args: unknown[]) {\n    console.log(highlighter.info(args.join(\" \")));\n  },\n  success(...args: unknown[]) {\n    console.log(highlighter.success(args.join(\" \")));\n  },\n  dim(...args: unknown[]) {\n    console.log(highlighter.dim(args.join(\" \")));\n  },\n  log(...args: unknown[]) {\n    console.log(args.join(\" \"));\n  },\n  break() {\n    console.log(\"\");\n  },\n};\n"
  },
  {
    "path": "packages/cli/src/utils/prompts.ts",
    "content": "import basePrompts, { type PromptObject, type Answers } from \"prompts\";\nimport { logger } from \"./logger.js\";\n\nconst onCancel = () => {\n  logger.break();\n  logger.log(\"Cancelled.\");\n  logger.break();\n  process.exit(0);\n};\n\nexport const prompts = <T extends string = string>(\n  questions: PromptObject<T> | PromptObject<T>[],\n): Promise<Answers<T>> => {\n  return basePrompts(questions, { onCancel });\n};\n"
  },
  {
    "path": "packages/cli/src/utils/spinner.ts",
    "content": "import ora from \"ora\";\n\ninterface SpinnerOptions {\n  silent?: boolean;\n}\n\nexport const spinner = (text: string, options?: SpinnerOptions) =>\n  ora({ text, isSilent: options?.silent });\n"
  },
  {
    "path": "packages/cli/src/utils/templates.ts",
    "content": "export const AGENTS = [\n  \"claude-code\",\n  \"cursor\",\n  \"opencode\",\n  \"codex\",\n  \"gemini\",\n  \"amp\",\n  \"droid\",\n  \"copilot\",\n] as const;\n\nexport type Agent = (typeof AGENTS)[number];\n\nexport type AgentIntegration = Agent | \"mcp\" | \"none\";\n\nexport const AGENT_NAMES: Record<Agent, string> = {\n  \"claude-code\": \"Claude Code\",\n  cursor: \"Cursor\",\n  opencode: \"OpenCode\",\n  codex: \"Codex\",\n  gemini: \"Gemini\",\n  amp: \"Amp\",\n  droid: \"Droid\",\n  copilot: \"Copilot\",\n};\n\nexport const getAgentDisplayName = (agent: string): string => {\n  if (agent === \"mcp\") return \"MCP\";\n  if (agent in AGENT_NAMES) {\n    return AGENT_NAMES[agent as Agent];\n  }\n  return agent;\n};\n\nexport const NEXT_APP_ROUTER_SCRIPT = `{process.env.NODE_ENV === \"development\" && (\n          <Script\n            src=\"//unpkg.com/react-grab/dist/index.global.js\"\n            crossOrigin=\"anonymous\"\n            strategy=\"beforeInteractive\"\n          />\n        )}`;\n\nexport const NEXT_APP_ROUTER_SCRIPT_WITH_AGENT = (\n  agent: AgentIntegration,\n): string => {\n  if (agent === \"none\") return NEXT_APP_ROUTER_SCRIPT;\n\n  return `{process.env.NODE_ENV === \"development\" && (\n          <Script\n            src=\"//unpkg.com/react-grab/dist/index.global.js\"\n            crossOrigin=\"anonymous\"\n            strategy=\"beforeInteractive\"\n          />\n        )}\n        {process.env.NODE_ENV === \"development\" && (\n          <Script\n            src=\"//unpkg.com/@react-grab/${agent}/dist/client.global.js\"\n            strategy=\"lazyOnload\"\n          />\n        )}`;\n};\n\nexport const NEXT_PAGES_ROUTER_SCRIPT = `{process.env.NODE_ENV === \"development\" && (\n          <Script\n            src=\"//unpkg.com/react-grab/dist/index.global.js\"\n            crossOrigin=\"anonymous\"\n            strategy=\"beforeInteractive\"\n          />\n        )}`;\n\nexport const NEXT_PAGES_ROUTER_SCRIPT_WITH_AGENT = (\n  agent: AgentIntegration,\n): string => {\n  if (agent === \"none\") return NEXT_PAGES_ROUTER_SCRIPT;\n\n  return `{process.env.NODE_ENV === \"development\" && (\n          <Script\n            src=\"//unpkg.com/react-grab/dist/index.global.js\"\n            crossOrigin=\"anonymous\"\n            strategy=\"beforeInteractive\"\n          />\n        )}\n        {process.env.NODE_ENV === \"development\" && (\n          <Script\n            src=\"//unpkg.com/@react-grab/${agent}/dist/client.global.js\"\n            strategy=\"lazyOnload\"\n          />\n        )}`;\n};\n\nexport const VITE_IMPORT = `if (import.meta.env.DEV) {\n  import(\"react-grab\");\n}`;\n\nexport const VITE_IMPORT_WITH_AGENT = (agent: AgentIntegration): string => {\n  if (agent === \"none\") return VITE_IMPORT;\n\n  return `if (import.meta.env.DEV) {\n  import(\"react-grab\");\n  import(\"@react-grab/${agent}/client\");\n}`;\n};\n\nexport const WEBPACK_IMPORT = `if (process.env.NODE_ENV === \"development\") {\n  import(\"react-grab\");\n}`;\n\nexport const WEBPACK_IMPORT_WITH_AGENT = (agent: AgentIntegration): string => {\n  if (agent === \"none\") return WEBPACK_IMPORT;\n\n  return `if (process.env.NODE_ENV === \"development\") {\n  import(\"react-grab\");\n  import(\"@react-grab/${agent}/client\");\n}`;\n};\n\nexport const TANSTACK_EFFECT = `useEffect(() => {\n    if (import.meta.env.DEV) {\n      void import(\"react-grab\");\n    }\n  }, []);`;\n\nexport const TANSTACK_EFFECT_WITH_AGENT = (agent: AgentIntegration): string => {\n  if (agent === \"none\") return TANSTACK_EFFECT;\n\n  return `useEffect(() => {\n    if (import.meta.env.DEV) {\n      void import(\"react-grab\");\n      void import(\"@react-grab/${agent}/client\");\n    }\n  }, []);`;\n};\n\nexport const SCRIPT_IMPORT = 'import Script from \"next/script\";';\n"
  },
  {
    "path": "packages/cli/src/utils/transform.ts",
    "content": "import {\n  accessSync,\n  constants,\n  existsSync,\n  readFileSync,\n  writeFileSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { Framework, NextRouterType, PackageManager } from \"./detect.js\";\nimport {\n  NEXT_APP_ROUTER_SCRIPT_WITH_AGENT,\n  NEXT_PAGES_ROUTER_SCRIPT_WITH_AGENT,\n  SCRIPT_IMPORT,\n  TANSTACK_EFFECT_WITH_AGENT,\n  VITE_IMPORT_WITH_AGENT,\n  WEBPACK_IMPORT_WITH_AGENT,\n  type AgentIntegration,\n} from \"./templates.js\";\n\nexport interface TransformResult {\n  success: boolean;\n  filePath: string;\n  message: string;\n  originalContent?: string;\n  newContent?: string;\n  noChanges?: boolean;\n}\n\nexport interface ReactGrabOptions {\n  activationKey?: string;\n  activationMode?: \"toggle\" | \"hold\";\n  keyHoldDuration?: number;\n  allowActivationInsideInput?: boolean;\n  maxContextLines?: number;\n}\n\nexport interface PackageJsonTransformResult {\n  success: boolean;\n  filePath: string;\n  message: string;\n  originalContent?: string;\n  newContent?: string;\n  noChanges?: boolean;\n  warning?: string;\n}\n\nconst hasReactGrabCode = (content: string): boolean => {\n  const fuzzyPatterns = [\n    /[\"'`][^\"'`]*react-grab/,\n    /react-grab[^\"'`]*[\"'`]/,\n    /<[^>]*react-grab/i,\n    /import[^;]*react-grab/i,\n    /require[^)]*react-grab/i,\n    /from\\s+[^;]*react-grab/i,\n    /src[^>]*react-grab/i,\n    /href[^>]*react-grab/i,\n  ];\n  return fuzzyPatterns.some((pattern) => pattern.test(content));\n};\n\nconst findLayoutFile = (projectRoot: string): string | null => {\n  const possiblePaths = [\n    join(projectRoot, \"app\", \"layout.tsx\"),\n    join(projectRoot, \"app\", \"layout.jsx\"),\n    join(projectRoot, \"src\", \"app\", \"layout.tsx\"),\n    join(projectRoot, \"src\", \"app\", \"layout.jsx\"),\n  ];\n\n  for (const filePath of possiblePaths) {\n    if (existsSync(filePath)) {\n      return filePath;\n    }\n  }\n\n  return null;\n};\n\nconst findInstrumentationFile = (projectRoot: string): string | null => {\n  const possiblePaths = [\n    join(projectRoot, \"instrumentation-client.ts\"),\n    join(projectRoot, \"instrumentation-client.js\"),\n    join(projectRoot, \"src\", \"instrumentation-client.ts\"),\n    join(projectRoot, \"src\", \"instrumentation-client.js\"),\n  ];\n\n  for (const filePath of possiblePaths) {\n    if (existsSync(filePath)) {\n      return filePath;\n    }\n  }\n\n  return null;\n};\n\nconst hasReactGrabInInstrumentation = (projectRoot: string): boolean => {\n  const instrumentationPath = findInstrumentationFile(projectRoot);\n  if (!instrumentationPath) return false;\n\n  const content = readFileSync(instrumentationPath, \"utf-8\");\n  return hasReactGrabCode(content);\n};\n\nconst findDocumentFile = (projectRoot: string): string | null => {\n  const possiblePaths = [\n    join(projectRoot, \"pages\", \"_document.tsx\"),\n    join(projectRoot, \"pages\", \"_document.jsx\"),\n    join(projectRoot, \"src\", \"pages\", \"_document.tsx\"),\n    join(projectRoot, \"src\", \"pages\", \"_document.jsx\"),\n  ];\n\n  for (const filePath of possiblePaths) {\n    if (existsSync(filePath)) {\n      return filePath;\n    }\n  }\n\n  return null;\n};\n\nconst findIndexHtml = (projectRoot: string): string | null => {\n  const possiblePaths = [\n    join(projectRoot, \"index.html\"),\n    join(projectRoot, \"public\", \"index.html\"),\n  ];\n\n  for (const filePath of possiblePaths) {\n    if (existsSync(filePath)) {\n      return filePath;\n    }\n  }\n\n  return null;\n};\n\nconst findEntryFile = (projectRoot: string): string | null => {\n  const possiblePaths = [\n    join(projectRoot, \"src\", \"index.tsx\"),\n    join(projectRoot, \"src\", \"index.jsx\"),\n    join(projectRoot, \"src\", \"index.ts\"),\n    join(projectRoot, \"src\", \"index.js\"),\n    join(projectRoot, \"src\", \"main.tsx\"),\n    join(projectRoot, \"src\", \"main.jsx\"),\n    join(projectRoot, \"src\", \"main.ts\"),\n    join(projectRoot, \"src\", \"main.js\"),\n  ];\n\n  for (const filePath of possiblePaths) {\n    if (existsSync(filePath)) {\n      return filePath;\n    }\n  }\n\n  return null;\n};\n\nconst findTanStackRootFile = (projectRoot: string): string | null => {\n  const possiblePaths = [\n    join(projectRoot, \"src\", \"routes\", \"__root.tsx\"),\n    join(projectRoot, \"src\", \"routes\", \"__root.jsx\"),\n    join(projectRoot, \"app\", \"routes\", \"__root.tsx\"),\n    join(projectRoot, \"app\", \"routes\", \"__root.jsx\"),\n  ];\n\n  for (const filePath of possiblePaths) {\n    if (existsSync(filePath)) {\n      return filePath;\n    }\n  }\n\n  return null;\n};\n\nconst addAgentToExistingNextApp = (\n  originalContent: string,\n  agent: AgentIntegration,\n  filePath: string,\n): TransformResult => {\n  if (agent === \"none\") {\n    return {\n      success: true,\n      filePath,\n      message: \"React Grab is already configured\",\n      noChanges: true,\n    };\n  }\n\n  const agentPackage = `@react-grab/${agent}`;\n  if (originalContent.includes(agentPackage)) {\n    return {\n      success: true,\n      filePath,\n      message: `Agent ${agent} is already configured`,\n      noChanges: true,\n    };\n  }\n\n  const agentScript = `{process.env.NODE_ENV === \"development\" && (\n          <Script\n            src=\"//unpkg.com/${agentPackage}/dist/client.global.js\"\n            strategy=\"lazyOnload\"\n          />\n        )}`;\n\n  const reactGrabBlockMatch = originalContent.match(\n    /\\{process\\.env\\.NODE_ENV\\s*===\\s*[\"']development[\"']\\s*&&\\s*\\(\\s*<Script[^>]*react-grab[^>]*\\/>\\s*\\)\\}/is,\n  );\n\n  if (reactGrabBlockMatch) {\n    const newContent = originalContent.replace(\n      reactGrabBlockMatch[0],\n      `${reactGrabBlockMatch[0]}\\n        ${agentScript}`,\n    );\n    return {\n      success: true,\n      filePath,\n      message: `Add ${agent} agent`,\n      originalContent,\n      newContent,\n    };\n  }\n\n  const bareScriptMatch = originalContent.match(\n    /<Script[^>]*react-grab[^>]*\\/>/i,\n  );\n\n  if (bareScriptMatch) {\n    const newContent = originalContent.replace(\n      bareScriptMatch[0],\n      `${bareScriptMatch[0]}\\n        <Script src=\"//unpkg.com/${agentPackage}/dist/client.global.js\" strategy=\"lazyOnload\" />`,\n    );\n    return {\n      success: true,\n      filePath,\n      message: `Add ${agent} agent`,\n      originalContent,\n      newContent,\n    };\n  }\n\n  return {\n    success: false,\n    filePath,\n    message: \"Could not find React Grab script to add agent after\",\n  };\n};\n\nconst addAgentToExistingImport = (\n  originalContent: string,\n  agent: AgentIntegration,\n  filePath: string,\n): TransformResult => {\n  if (agent === \"none\") {\n    return {\n      success: true,\n      filePath,\n      message: \"React Grab is already configured\",\n      noChanges: true,\n    };\n  }\n\n  const agentPackage = `@react-grab/${agent}`;\n  if (originalContent.includes(agentPackage)) {\n    return {\n      success: true,\n      filePath,\n      message: `Agent ${agent} is already configured`,\n      noChanges: true,\n    };\n  }\n\n  const agentImport = `import(\"${agentPackage}/client\");`;\n  const reactGrabImportMatch = originalContent.match(\n    /import\\s*\\(\\s*[\"']react-grab[\"']\\s*\\);?/,\n  );\n\n  if (reactGrabImportMatch) {\n    const matchedText = reactGrabImportMatch[0];\n    const hasSemicolon = matchedText.endsWith(\";\");\n    const newContent = originalContent.replace(\n      matchedText,\n      `${hasSemicolon ? matchedText.slice(0, -1) : matchedText};\\n  ${agentImport}`,\n    );\n    return {\n      success: true,\n      filePath,\n      message: `Add ${agent} agent`,\n      originalContent,\n      newContent,\n    };\n  }\n\n  return {\n    success: false,\n    filePath,\n    message: \"Could not find React Grab import to add agent after\",\n  };\n};\n\nconst addAgentToExistingTanStack = (\n  originalContent: string,\n  agent: AgentIntegration,\n  filePath: string,\n): TransformResult => {\n  if (agent === \"none\") {\n    return {\n      success: true,\n      filePath,\n      message: \"React Grab is already configured\",\n      noChanges: true,\n    };\n  }\n\n  const agentPackage = `@react-grab/${agent}`;\n  if (originalContent.includes(agentPackage)) {\n    return {\n      success: true,\n      filePath,\n      message: `Agent ${agent} is already configured`,\n      noChanges: true,\n    };\n  }\n\n  const agentImport = `void import(\"${agentPackage}/client\");`;\n  const reactGrabImportMatch = originalContent.match(\n    /void\\s+import\\s*\\(\\s*[\"']react-grab[\"']\\s*\\);?/,\n  );\n\n  if (reactGrabImportMatch) {\n    const matchedText = reactGrabImportMatch[0];\n    const hasSemicolon = matchedText.endsWith(\";\");\n    const newContent = originalContent.replace(\n      matchedText,\n      `${hasSemicolon ? matchedText.slice(0, -1) : matchedText};\\n      ${agentImport}`,\n    );\n    return {\n      success: true,\n      filePath,\n      message: `Add ${agent} agent`,\n      originalContent,\n      newContent,\n    };\n  }\n\n  return {\n    success: false,\n    filePath,\n    message: \"Could not find React Grab import to add agent after\",\n  };\n};\n\nconst transformNextAppRouter = (\n  projectRoot: string,\n  agent: AgentIntegration,\n  reactGrabAlreadyConfigured: boolean,\n  force: boolean = false,\n): TransformResult => {\n  const layoutPath = findLayoutFile(projectRoot);\n\n  if (!layoutPath) {\n    return {\n      success: false,\n      filePath: \"\",\n      message: \"Could not find app/layout.tsx or app/layout.jsx\",\n    };\n  }\n\n  const originalContent = readFileSync(layoutPath, \"utf-8\");\n  let newContent = originalContent;\n  const hasReactGrabInFile = hasReactGrabCode(originalContent);\n  const hasReactGrabInInstrumentationFile =\n    hasReactGrabInInstrumentation(projectRoot);\n\n  if (!force && hasReactGrabInFile && reactGrabAlreadyConfigured) {\n    return addAgentToExistingNextApp(originalContent, agent, layoutPath);\n  }\n\n  if (!force && (hasReactGrabInFile || hasReactGrabInInstrumentationFile)) {\n    return {\n      success: true,\n      filePath: layoutPath,\n      message:\n        \"React Grab is already installed\" +\n        (hasReactGrabInInstrumentationFile\n          ? \" in instrumentation-client\"\n          : \" in this file\"),\n      noChanges: true,\n    };\n  }\n\n  if (!newContent.includes('import Script from \"next/script\"')) {\n    const importMatch = newContent.match(/^import .+ from ['\"].+['\"];?\\s*$/m);\n    if (importMatch) {\n      newContent = newContent.replace(\n        importMatch[0],\n        `${importMatch[0]}\\n${SCRIPT_IMPORT}`,\n      );\n    } else {\n      newContent = `${SCRIPT_IMPORT}\\n\\n${newContent}`;\n    }\n  }\n\n  const scriptBlock = NEXT_APP_ROUTER_SCRIPT_WITH_AGENT(agent);\n\n  const headMatch = newContent.match(/<head[^>]*>/);\n  if (headMatch) {\n    newContent = newContent.replace(\n      headMatch[0],\n      `${headMatch[0]}\\n        ${scriptBlock}`,\n    );\n  } else {\n    const htmlMatch = newContent.match(/<html[^>]*>/);\n    if (htmlMatch) {\n      newContent = newContent.replace(\n        htmlMatch[0],\n        `${htmlMatch[0]}\\n      <head>\\n        ${scriptBlock}\\n      </head>`,\n      );\n    }\n  }\n\n  return {\n    success: true,\n    filePath: layoutPath,\n    message:\n      \"Add React Grab\" + (agent !== \"none\" ? ` with ${agent} agent` : \"\"),\n    originalContent,\n    newContent,\n  };\n};\n\nconst transformNextPagesRouter = (\n  projectRoot: string,\n  agent: AgentIntegration,\n  reactGrabAlreadyConfigured: boolean,\n  force: boolean = false,\n): TransformResult => {\n  const documentPath = findDocumentFile(projectRoot);\n\n  if (!documentPath) {\n    return {\n      success: false,\n      filePath: \"\",\n      message:\n        \"Could not find pages/_document.tsx or pages/_document.jsx.\\n\\n\" +\n        \"To set up React Grab with Pages Router, create pages/_document.tsx with:\\n\\n\" +\n        '  import { Html, Head, Main, NextScript } from \"next/document\";\\n' +\n        '  import Script from \"next/script\";\\n\\n' +\n        \"  export default function Document() {\\n\" +\n        \"    return (\\n\" +\n        \"      <Html>\\n\" +\n        \"        <Head>\\n\" +\n        '          {process.env.NODE_ENV === \"development\" && (\\n' +\n        '            <Script src=\"//unpkg.com/react-grab/dist/index.global.js\" strategy=\"beforeInteractive\" />\\n' +\n        \"          )}\\n\" +\n        \"        </Head>\\n\" +\n        \"        <body>\\n\" +\n        \"          <Main />\\n\" +\n        \"          <NextScript />\\n\" +\n        \"        </body>\\n\" +\n        \"      </Html>\\n\" +\n        \"    );\\n\" +\n        \"  }\",\n    };\n  }\n\n  const originalContent = readFileSync(documentPath, \"utf-8\");\n  let newContent = originalContent;\n  const hasReactGrabInFile = hasReactGrabCode(originalContent);\n  const hasReactGrabInInstrumentationFile =\n    hasReactGrabInInstrumentation(projectRoot);\n\n  if (!force && hasReactGrabInFile && reactGrabAlreadyConfigured) {\n    return addAgentToExistingNextApp(originalContent, agent, documentPath);\n  }\n\n  if (!force && (hasReactGrabInFile || hasReactGrabInInstrumentationFile)) {\n    return {\n      success: true,\n      filePath: documentPath,\n      message:\n        \"React Grab is already installed\" +\n        (hasReactGrabInInstrumentationFile\n          ? \" in instrumentation-client\"\n          : \" in this file\"),\n      noChanges: true,\n    };\n  }\n\n  if (!newContent.includes('import Script from \"next/script\"')) {\n    const importMatch = newContent.match(/^import .+ from ['\"].+['\"];?\\s*$/m);\n    if (importMatch) {\n      newContent = newContent.replace(\n        importMatch[0],\n        `${importMatch[0]}\\n${SCRIPT_IMPORT}`,\n      );\n    }\n  }\n\n  const scriptBlock = NEXT_PAGES_ROUTER_SCRIPT_WITH_AGENT(agent);\n\n  const headMatch = newContent.match(/<Head[^>]*>/);\n  if (headMatch) {\n    newContent = newContent.replace(\n      headMatch[0],\n      `${headMatch[0]}\\n        ${scriptBlock}`,\n    );\n  }\n\n  return {\n    success: true,\n    filePath: documentPath,\n    message:\n      \"Add React Grab\" + (agent !== \"none\" ? ` with ${agent} agent` : \"\"),\n    originalContent,\n    newContent,\n  };\n};\n\nconst checkExistingInstallation = (\n  filePath: string,\n  agent: AgentIntegration,\n  reactGrabAlreadyConfigured: boolean,\n): TransformResult | null => {\n  const content = readFileSync(filePath, \"utf-8\");\n  if (!hasReactGrabCode(content)) return null;\n\n  if (reactGrabAlreadyConfigured) {\n    return addAgentToExistingImport(content, agent, filePath);\n  }\n  return {\n    success: true,\n    filePath,\n    message: \"React Grab is already installed in this file\",\n    noChanges: true,\n  };\n};\n\nconst transformVite = (\n  projectRoot: string,\n  agent: AgentIntegration,\n  reactGrabAlreadyConfigured: boolean,\n  force: boolean = false,\n): TransformResult => {\n  const entryPath = findEntryFile(projectRoot);\n\n  if (!force) {\n    const indexPath = findIndexHtml(projectRoot);\n    if (indexPath) {\n      const existingResult = checkExistingInstallation(\n        indexPath,\n        agent,\n        reactGrabAlreadyConfigured,\n      );\n      if (existingResult) return existingResult;\n    }\n  }\n\n  if (!entryPath) {\n    return {\n      success: false,\n      filePath: \"\",\n      message: \"Could not find entry file (src/index.tsx, src/main.tsx, etc.)\",\n    };\n  }\n\n  if (!force) {\n    const existingResult = checkExistingInstallation(\n      entryPath,\n      agent,\n      reactGrabAlreadyConfigured,\n    );\n    if (existingResult) return existingResult;\n  }\n\n  const originalContent = readFileSync(entryPath, \"utf-8\");\n  const importBlock = VITE_IMPORT_WITH_AGENT(agent);\n  const newContent = `${importBlock}\\n\\n${originalContent}`;\n\n  return {\n    success: true,\n    filePath: entryPath,\n    message:\n      \"Add React Grab\" + (agent !== \"none\" ? ` with ${agent} agent` : \"\"),\n    originalContent,\n    newContent,\n  };\n};\n\nconst transformWebpack = (\n  projectRoot: string,\n  agent: AgentIntegration,\n  reactGrabAlreadyConfigured: boolean,\n  force: boolean = false,\n): TransformResult => {\n  const entryPath = findEntryFile(projectRoot);\n\n  if (!entryPath) {\n    return {\n      success: false,\n      filePath: \"\",\n      message: \"Could not find entry file (src/index.tsx, src/main.tsx, etc.)\",\n    };\n  }\n\n  if (!force) {\n    const existingResult = checkExistingInstallation(\n      entryPath,\n      agent,\n      reactGrabAlreadyConfigured,\n    );\n    if (existingResult) return existingResult;\n  }\n\n  const originalContent = readFileSync(entryPath, \"utf-8\");\n  const importBlock = WEBPACK_IMPORT_WITH_AGENT(agent);\n  const newContent = `${importBlock}\\n\\n${originalContent}`;\n\n  return {\n    success: true,\n    filePath: entryPath,\n    message:\n      \"Add React Grab\" + (agent !== \"none\" ? ` with ${agent} agent` : \"\"),\n    originalContent,\n    newContent,\n  };\n};\n\nconst transformTanStack = (\n  projectRoot: string,\n  agent: AgentIntegration,\n  reactGrabAlreadyConfigured: boolean,\n  force: boolean = false,\n): TransformResult => {\n  const rootPath = findTanStackRootFile(projectRoot);\n\n  if (!rootPath) {\n    return {\n      success: false,\n      filePath: \"\",\n      message:\n        \"Could not find src/routes/__root.tsx or app/routes/__root.tsx.\\n\\n\" +\n        \"To set up React Grab with TanStack Start, add this to your root route component:\\n\\n\" +\n        '  import { useEffect } from \"react\";\\n\\n' +\n        \"  useEffect(() => {\\n\" +\n        \"    if (import.meta.env.DEV) {\\n\" +\n        '      void import(\"react-grab\");\\n' +\n        \"    }\\n\" +\n        \"  }, []);\",\n    };\n  }\n\n  const originalContent = readFileSync(rootPath, \"utf-8\");\n  let newContent = originalContent;\n  const hasReactGrabInFile = hasReactGrabCode(originalContent);\n\n  if (!force && hasReactGrabInFile && reactGrabAlreadyConfigured) {\n    return addAgentToExistingTanStack(originalContent, agent, rootPath);\n  }\n\n  if (!force && hasReactGrabInFile) {\n    return {\n      success: true,\n      filePath: rootPath,\n      message: \"React Grab is already installed in this file\",\n      noChanges: true,\n    };\n  }\n\n  const hasUseEffectImport =\n    /import\\s+\\{[^}]*useEffect[^}]*\\}\\s+from\\s+[\"']react[\"']/.test(newContent);\n  if (!hasUseEffectImport) {\n    const reactImportMatch = newContent.match(\n      /import\\s+\\{([^}]*)\\}\\s+from\\s+[\"']react[\"'];?/,\n    );\n    if (reactImportMatch) {\n      const existingImports = reactImportMatch[1];\n      newContent = newContent.replace(\n        reactImportMatch[0],\n        `import { ${existingImports.trim()}, useEffect } from \"react\";`,\n      );\n    } else {\n      const firstImportMatch = newContent.match(\n        /^import .+ from ['\"].+['\"];?\\s*$/m,\n      );\n      if (firstImportMatch) {\n        newContent = newContent.replace(\n          firstImportMatch[0],\n          `import { useEffect } from \"react\";\\n${firstImportMatch[0]}`,\n        );\n      } else {\n        newContent = `import { useEffect } from \"react\";\\n\\n${newContent}`;\n      }\n    }\n  }\n\n  const effectBlock = TANSTACK_EFFECT_WITH_AGENT(agent);\n\n  const componentMatch = newContent.match(/function\\s+(\\w+)\\s*\\([^)]*\\)\\s*\\{/);\n\n  if (componentMatch) {\n    const insertPosition = componentMatch.index! + componentMatch[0].length;\n    newContent =\n      newContent.slice(0, insertPosition) +\n      `\\n  ${effectBlock}\\n` +\n      newContent.slice(insertPosition);\n  } else {\n    return {\n      success: false,\n      filePath: rootPath,\n      message: \"Could not find a component function in the root file\",\n    };\n  }\n\n  return {\n    success: true,\n    filePath: rootPath,\n    message:\n      \"Add React Grab\" + (agent !== \"none\" ? ` with ${agent} agent` : \"\"),\n    originalContent,\n    newContent,\n  };\n};\n\nexport const previewTransform = (\n  projectRoot: string,\n  framework: Framework,\n  nextRouterType: NextRouterType,\n  agent: AgentIntegration,\n  reactGrabAlreadyConfigured: boolean = false,\n  force: boolean = false,\n): TransformResult => {\n  const resolvedAgent: AgentIntegration = agent === \"mcp\" ? \"none\" : agent;\n\n  switch (framework) {\n    case \"next\":\n      if (nextRouterType === \"app\") {\n        return transformNextAppRouter(\n          projectRoot,\n          resolvedAgent,\n          reactGrabAlreadyConfigured,\n          force,\n        );\n      }\n      return transformNextPagesRouter(\n        projectRoot,\n        resolvedAgent,\n        reactGrabAlreadyConfigured,\n        force,\n      );\n\n    case \"vite\":\n      return transformVite(\n        projectRoot,\n        resolvedAgent,\n        reactGrabAlreadyConfigured,\n        force,\n      );\n\n    case \"tanstack\":\n      return transformTanStack(\n        projectRoot,\n        resolvedAgent,\n        reactGrabAlreadyConfigured,\n        force,\n      );\n\n    case \"webpack\":\n      return transformWebpack(\n        projectRoot,\n        resolvedAgent,\n        reactGrabAlreadyConfigured,\n        force,\n      );\n\n    default:\n      return {\n        success: false,\n        filePath: \"\",\n        message: `Unknown framework: ${framework}. Please add React Grab manually.`,\n      };\n  }\n};\n\nconst canWriteToFile = (filePath: string): boolean => {\n  try {\n    accessSync(filePath, constants.W_OK);\n    return true;\n  } catch {\n    return false;\n  }\n};\n\nexport const applyTransform = (\n  result: TransformResult,\n): { success: boolean; error?: string } => {\n  if (result.success && result.newContent && result.filePath) {\n    if (!canWriteToFile(result.filePath)) {\n      return {\n        success: false,\n        error: `Cannot write to ${result.filePath}. Check file permissions.`,\n      };\n    }\n\n    try {\n      writeFileSync(result.filePath, result.newContent);\n      return { success: true };\n    } catch (error) {\n      return {\n        success: false,\n        error: `Failed to write to ${result.filePath}: ${error instanceof Error ? error.message : \"Unknown error\"}`,\n      };\n    }\n  }\n  return { success: true };\n};\n\nexport const transformProject = (\n  projectRoot: string,\n  framework: Framework,\n  nextRouterType: NextRouterType,\n  agent: AgentIntegration,\n  reactGrabAlreadyConfigured: boolean = false,\n): TransformResult & { writeError?: string } => {\n  const result = previewTransform(\n    projectRoot,\n    framework,\n    nextRouterType,\n    agent,\n    reactGrabAlreadyConfigured,\n  );\n  const writeResult = applyTransform(result);\n  if (!writeResult.success) {\n    return { ...result, success: false, writeError: writeResult.error };\n  }\n  return result;\n};\n\nconst getPackageExecutor = (packageManager: PackageManager): string => {\n  switch (packageManager) {\n    case \"bun\":\n      return \"bunx\";\n    case \"pnpm\":\n      return \"pnpm dlx\";\n    case \"yarn\":\n      return \"npx\";\n    case \"npm\":\n    default:\n      return \"npx\";\n  }\n};\n\nconst AGENT_PACKAGES: Record<string, string> = {\n  \"claude-code\": \"@react-grab/claude-code@latest\",\n  cursor: \"@react-grab/cursor@latest\",\n  opencode: \"@react-grab/opencode@latest\",\n  codex: \"@react-grab/codex@latest\",\n  gemini: \"@react-grab/gemini@latest\",\n  amp: \"@react-grab/amp@latest\",\n  droid: \"@react-grab/droid@latest\",\n  copilot: \"@react-grab/copilot@latest\",\n};\n\nexport const getAgentPrefix = (\n  agent: string,\n  packageManager: PackageManager,\n): string | null => {\n  const agentPackage = AGENT_PACKAGES[agent];\n  if (!agentPackage) return null;\n  const executor = getPackageExecutor(packageManager);\n  return `${executor} ${agentPackage} &&`;\n};\n\nconst getAllAgentPrefixVariants = (agent: string): string[] => {\n  const agentPackage = AGENT_PACKAGES[agent];\n  if (!agentPackage) return [];\n  return [\n    `npx ${agentPackage} &&`,\n    `bunx ${agentPackage} &&`,\n    `pnpm dlx ${agentPackage} &&`,\n    `yarn dlx ${agentPackage} &&`,\n  ];\n};\n\nexport const previewPackageJsonTransform = (\n  projectRoot: string,\n  agent: AgentIntegration,\n  installedAgents: string[],\n  packageManager: PackageManager = \"npm\",\n): PackageJsonTransformResult => {\n  if (agent === \"none\") {\n    return {\n      success: true,\n      filePath: \"\",\n      message: \"No agent selected, skipping package.json modification\",\n      noChanges: true,\n    };\n  }\n\n  if (agent === \"mcp\") {\n    return {\n      success: true,\n      filePath: \"\",\n      message: \"MCP does not use package.json dev script\",\n      noChanges: true,\n    };\n  }\n\n  const packageJsonPath = join(projectRoot, \"package.json\");\n\n  if (!existsSync(packageJsonPath)) {\n    return {\n      success: false,\n      filePath: \"\",\n      message: \"Could not find package.json\",\n    };\n  }\n\n  const originalContent = readFileSync(packageJsonPath, \"utf-8\");\n  const agentPrefix = getAgentPrefix(agent, packageManager);\n\n  if (!agentPrefix) {\n    return {\n      success: false,\n      filePath: packageJsonPath,\n      message: `Unknown agent: ${agent}`,\n    };\n  }\n\n  const allPrefixVariants = getAllAgentPrefixVariants(agent);\n  const hasExistingPrefix = allPrefixVariants.some((prefix) =>\n    originalContent.includes(prefix),\n  );\n\n  if (hasExistingPrefix) {\n    return {\n      success: true,\n      filePath: packageJsonPath,\n      message: `Agent ${agent} dev script is already configured`,\n      noChanges: true,\n    };\n  }\n\n  try {\n    const packageJson = JSON.parse(originalContent);\n\n    let targetScriptKey = \"dev\";\n    if (!packageJson.scripts?.dev) {\n      const devScriptKeys = Object.keys(packageJson.scripts || {}).filter(\n        (key) => key.startsWith(\"dev\"),\n      );\n      if (devScriptKeys.length > 0) {\n        targetScriptKey = devScriptKeys[0];\n      } else {\n        return {\n          success: true,\n          filePath: packageJsonPath,\n          message: \"No dev script found in package.json\",\n          noChanges: true,\n          warning: `Could not inject agent into package.json (no dev script found).\\nRun this command manually before starting your dev server:\\n  ${agentPrefix} <your dev command>`,\n        };\n      }\n    }\n\n    const currentDevScript = packageJson.scripts[targetScriptKey];\n\n    for (const installedAgent of installedAgents) {\n      const installedPrefixVariants = getAllAgentPrefixVariants(installedAgent);\n      const hasInstalledAgentPrefix = installedPrefixVariants.some((prefix) =>\n        currentDevScript.includes(prefix),\n      );\n      if (hasInstalledAgentPrefix) {\n        return {\n          success: true,\n          filePath: packageJsonPath,\n          message: `Agent ${installedAgent} is already in ${targetScriptKey} script`,\n          noChanges: true,\n        };\n      }\n    }\n\n    packageJson.scripts[targetScriptKey] = `${agentPrefix} ${currentDevScript}`;\n\n    const newContent = JSON.stringify(packageJson, null, 2) + \"\\n\";\n\n    return {\n      success: true,\n      filePath: packageJsonPath,\n      message: `Add ${agent} server to ${targetScriptKey} script`,\n      originalContent,\n      newContent,\n    };\n  } catch {\n    return {\n      success: false,\n      filePath: packageJsonPath,\n      message: \"Failed to parse package.json\",\n    };\n  }\n};\n\nexport const applyPackageJsonTransform = (\n  result: PackageJsonTransformResult,\n): { success: boolean; error?: string } => {\n  if (result.success && result.newContent && result.filePath) {\n    if (!canWriteToFile(result.filePath)) {\n      return {\n        success: false,\n        error: `Cannot write to ${result.filePath}. Check file permissions.`,\n      };\n    }\n\n    try {\n      writeFileSync(result.filePath, result.newContent);\n      return { success: true };\n    } catch (error) {\n      return {\n        success: false,\n        error: `Failed to write to ${result.filePath}: ${error instanceof Error ? error.message : \"Unknown error\"}`,\n      };\n    }\n  }\n  return { success: true };\n};\n\nconst formatOptionsForNextjs = (options: ReactGrabOptions): string => {\n  const parts: string[] = [];\n\n  if (options.activationKey) {\n    parts.push(`activationKey: ${JSON.stringify(options.activationKey)}`);\n  }\n\n  if (options.activationMode) {\n    parts.push(`activationMode: \"${options.activationMode}\"`);\n  }\n\n  if (options.keyHoldDuration !== undefined) {\n    parts.push(`keyHoldDuration: ${options.keyHoldDuration}`);\n  }\n\n  if (options.allowActivationInsideInput !== undefined) {\n    parts.push(\n      `allowActivationInsideInput: ${options.allowActivationInsideInput}`,\n    );\n  }\n\n  if (options.maxContextLines !== undefined) {\n    parts.push(`maxContextLines: ${options.maxContextLines}`);\n  }\n\n  return `{ ${parts.join(\", \")} }`;\n};\n\nconst formatOptionsAsJson = (options: ReactGrabOptions): string => {\n  const cleanOptions: Record<string, unknown> = {};\n\n  if (options.activationKey) {\n    cleanOptions.activationKey = options.activationKey;\n  }\n\n  if (options.activationMode) {\n    cleanOptions.activationMode = options.activationMode;\n  }\n\n  if (options.keyHoldDuration !== undefined) {\n    cleanOptions.keyHoldDuration = options.keyHoldDuration;\n  }\n\n  if (options.allowActivationInsideInput !== undefined) {\n    cleanOptions.allowActivationInsideInput =\n      options.allowActivationInsideInput;\n  }\n\n  if (options.maxContextLines !== undefined) {\n    cleanOptions.maxContextLines = options.maxContextLines;\n  }\n\n  return JSON.stringify(cleanOptions);\n};\n\nconst findReactGrabFile = (\n  projectRoot: string,\n  framework: Framework,\n  nextRouterType: NextRouterType,\n): string | null => {\n  switch (framework) {\n    case \"next\":\n      if (nextRouterType === \"app\") {\n        return findLayoutFile(projectRoot);\n      }\n      return findDocumentFile(projectRoot);\n    case \"vite\": {\n      const entryFile = findEntryFile(projectRoot);\n      if (entryFile && hasReactGrabCode(readFileSync(entryFile, \"utf-8\"))) {\n        return entryFile;\n      }\n      const indexHtml = findIndexHtml(projectRoot);\n      if (indexHtml && hasReactGrabCode(readFileSync(indexHtml, \"utf-8\"))) {\n        return indexHtml;\n      }\n      return entryFile;\n    }\n    case \"tanstack\":\n      return findTanStackRootFile(projectRoot);\n    case \"webpack\":\n      return findEntryFile(projectRoot);\n    default:\n      return null;\n  }\n};\n\nconst addOptionsToNextScript = (\n  originalContent: string,\n  options: ReactGrabOptions,\n  filePath: string,\n): TransformResult => {\n  const reactGrabScriptMatch = originalContent.match(\n    /(<Script[\\s\\S]*?react-grab[\\s\\S]*?)\\s*(\\/?>)/i,\n  );\n\n  if (!reactGrabScriptMatch) {\n    return {\n      success: false,\n      filePath,\n      message: \"Could not find React Grab Script tag\",\n    };\n  }\n\n  const scriptTag = reactGrabScriptMatch[0];\n  const scriptOpening = reactGrabScriptMatch[1];\n  const scriptClosing = reactGrabScriptMatch[2];\n\n  const existingDataOptionsMatch = scriptTag.match(\n    /data-options=\\{JSON\\.stringify\\([^)]+\\)\\}/,\n  );\n\n  const dataOptionsAttr = `data-options={JSON.stringify(\\n              ${formatOptionsForNextjs(options)}\\n            )}`;\n\n  let newScriptTag: string;\n  if (existingDataOptionsMatch) {\n    newScriptTag = scriptTag.replace(\n      existingDataOptionsMatch[0],\n      dataOptionsAttr,\n    );\n  } else {\n    newScriptTag = `${scriptOpening}\\n            ${dataOptionsAttr}\\n          ${scriptClosing}`;\n  }\n\n  const newContent = originalContent.replace(scriptTag, newScriptTag);\n\n  return {\n    success: true,\n    filePath,\n    message: \"Update React Grab options\",\n    originalContent,\n    newContent,\n  };\n};\n\nconst addOptionsToViteScript = (\n  originalContent: string,\n  options: ReactGrabOptions,\n  filePath: string,\n): TransformResult => {\n  const reactGrabImportWithInitMatch = originalContent.match(\n    /import\\s*\\(\\s*[\"']react-grab[\"']\\s*\\)(?:\\.then\\s*\\(\\s*\\(m\\)\\s*=>\\s*m\\.init\\s*\\([^)]*\\)\\s*\\))?/,\n  );\n\n  if (!reactGrabImportWithInitMatch) {\n    return {\n      success: false,\n      filePath,\n      message: \"Could not find React Grab import\",\n    };\n  }\n\n  const optionsJson = formatOptionsAsJson(options);\n  const newImport = `import(\"react-grab\").then((m) => m.init(${optionsJson}))`;\n\n  const newContent = originalContent.replace(\n    reactGrabImportWithInitMatch[0],\n    newImport,\n  );\n\n  return {\n    success: true,\n    filePath,\n    message: \"Update React Grab options\",\n    originalContent,\n    newContent,\n  };\n};\n\nconst addOptionsToWebpackImport = (\n  originalContent: string,\n  options: ReactGrabOptions,\n  filePath: string,\n): TransformResult => {\n  const reactGrabImportWithInitMatch = originalContent.match(\n    /import\\s*\\(\\s*[\"']react-grab[\"']\\s*\\)(?:\\.then\\s*\\(\\s*\\(m\\)\\s*=>\\s*m\\.init\\s*\\([^)]*\\)\\s*\\))?/,\n  );\n\n  if (!reactGrabImportWithInitMatch) {\n    return {\n      success: false,\n      filePath,\n      message: \"Could not find React Grab import\",\n    };\n  }\n\n  const optionsJson = formatOptionsAsJson(options);\n  const newImport = `import(\"react-grab\").then((m) => m.init(${optionsJson}))`;\n\n  const newContent = originalContent.replace(\n    reactGrabImportWithInitMatch[0],\n    newImport,\n  );\n\n  return {\n    success: true,\n    filePath,\n    message: \"Update React Grab options\",\n    originalContent,\n    newContent,\n  };\n};\n\nconst addOptionsToTanStackImport = (\n  originalContent: string,\n  options: ReactGrabOptions,\n  filePath: string,\n): TransformResult => {\n  const reactGrabImportWithInitMatch = originalContent.match(\n    /(?:void\\s+import\\s*\\(\\s*[\"']react-grab[\"']\\s*\\)|import\\s*\\(\\s*[\"']react-grab\\/core[\"']\\s*\\)\\.then\\s*\\(\\s*\\(\\s*\\{\\s*init\\s*\\}\\s*\\)\\s*=>\\s*init\\s*\\([^)]*\\)\\s*\\))/,\n  );\n\n  if (!reactGrabImportWithInitMatch) {\n    return {\n      success: false,\n      filePath,\n      message: \"Could not find React Grab import\",\n    };\n  }\n\n  const optionsJson = formatOptionsAsJson(options);\n  const newImport = `import(\"react-grab/core\").then(({ init }) => init(${optionsJson}))`;\n\n  const newContent = originalContent.replace(\n    reactGrabImportWithInitMatch[0],\n    newImport,\n  );\n\n  return {\n    success: true,\n    filePath,\n    message: \"Update React Grab options\",\n    originalContent,\n    newContent,\n  };\n};\n\nexport const previewOptionsTransform = (\n  projectRoot: string,\n  framework: Framework,\n  nextRouterType: NextRouterType,\n  options: ReactGrabOptions,\n): TransformResult => {\n  const filePath = findReactGrabFile(projectRoot, framework, nextRouterType);\n\n  if (!filePath) {\n    return {\n      success: false,\n      filePath: \"\",\n      message: \"Could not find file containing React Grab configuration\",\n    };\n  }\n\n  const originalContent = readFileSync(filePath, \"utf-8\");\n\n  if (!hasReactGrabCode(originalContent)) {\n    return {\n      success: false,\n      filePath,\n      message: \"Could not find React Grab code in the file\",\n    };\n  }\n\n  switch (framework) {\n    case \"next\":\n      return addOptionsToNextScript(originalContent, options, filePath);\n    case \"vite\":\n      return addOptionsToViteScript(originalContent, options, filePath);\n    case \"tanstack\":\n      return addOptionsToTanStackImport(originalContent, options, filePath);\n    case \"webpack\":\n      return addOptionsToWebpackImport(originalContent, options, filePath);\n    default:\n      return {\n        success: false,\n        filePath,\n        message: `Unknown framework: ${framework}`,\n      };\n  }\n};\n\nexport const applyOptionsTransform = (\n  result: TransformResult,\n): { success: boolean; error?: string } => {\n  return applyTransform(result);\n};\n\nconst removeAgentFromNextApp = (\n  originalContent: string,\n  agent: string,\n  filePath: string,\n): TransformResult => {\n  const agentPackage = `@react-grab/${agent}`;\n\n  if (!originalContent.includes(agentPackage)) {\n    return {\n      success: true,\n      filePath,\n      message: `Agent ${agent} is not configured in this file`,\n      noChanges: true,\n    };\n  }\n\n  const agentScriptPattern = new RegExp(\n    `\\\\s*\\\\{process\\\\.env\\\\.NODE_ENV === \"development\" && \\\\(\\\\s*<Script[^>]*${agentPackage.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}[^>]*\\\\/>\\\\s*\\\\)\\\\}`,\n    \"gs\",\n  );\n\n  const simpleScriptPattern = new RegExp(\n    `\\\\s*<Script[^>]*${agentPackage.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}[^>]*\\\\/>`,\n    \"gi\",\n  );\n\n  let newContent = originalContent.replace(agentScriptPattern, \"\");\n\n  if (newContent === originalContent) {\n    newContent = originalContent.replace(simpleScriptPattern, \"\");\n  }\n\n  if (newContent === originalContent) {\n    return {\n      success: false,\n      filePath,\n      message: `Could not find agent ${agent} script to remove`,\n    };\n  }\n\n  return {\n    success: true,\n    filePath,\n    message: `Remove ${agent} agent`,\n    originalContent,\n    newContent,\n  };\n};\n\nconst removeAgentFromVite = (\n  originalContent: string,\n  agent: string,\n  filePath: string,\n): TransformResult => {\n  const agentPackage = `@react-grab/${agent}`;\n\n  if (!originalContent.includes(agentPackage)) {\n    return {\n      success: true,\n      filePath,\n      message: `Agent ${agent} is not configured in this file`,\n      noChanges: true,\n    };\n  }\n\n  const agentImportPattern = new RegExp(\n    `\\\\s*import\\\\s*\\\\(\\\\s*[\"']${agentPackage.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}/client[\"']\\\\s*\\\\);?`,\n    \"g\",\n  );\n\n  const newContent = originalContent.replace(agentImportPattern, \"\");\n\n  if (newContent === originalContent) {\n    return {\n      success: false,\n      filePath,\n      message: `Could not find agent ${agent} import to remove`,\n    };\n  }\n\n  return {\n    success: true,\n    filePath,\n    message: `Remove ${agent} agent`,\n    originalContent,\n    newContent,\n  };\n};\n\nconst removeAgentFromWebpack = (\n  originalContent: string,\n  agent: string,\n  filePath: string,\n): TransformResult => {\n  const agentPackage = `@react-grab/${agent}`;\n\n  if (!originalContent.includes(agentPackage)) {\n    return {\n      success: true,\n      filePath,\n      message: `Agent ${agent} is not configured in this file`,\n      noChanges: true,\n    };\n  }\n\n  const agentImportPattern = new RegExp(\n    `\\\\s*import\\\\s*\\\\(\\\\s*[\"']${agentPackage.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}/client[\"']\\\\s*\\\\);?`,\n    \"g\",\n  );\n\n  const newContent = originalContent.replace(agentImportPattern, \"\");\n\n  if (newContent === originalContent) {\n    return {\n      success: false,\n      filePath,\n      message: `Could not find agent ${agent} import to remove`,\n    };\n  }\n\n  return {\n    success: true,\n    filePath,\n    message: `Remove ${agent} agent`,\n    originalContent,\n    newContent,\n  };\n};\n\nconst removeAgentFromTanStack = (\n  originalContent: string,\n  agent: string,\n  filePath: string,\n): TransformResult => {\n  const agentPackage = `@react-grab/${agent}`;\n\n  if (!originalContent.includes(agentPackage)) {\n    return {\n      success: true,\n      filePath,\n      message: `Agent ${agent} is not configured in this file`,\n      noChanges: true,\n    };\n  }\n\n  const agentImportPattern = new RegExp(\n    `\\\\s*void\\\\s+import\\\\s*\\\\(\\\\s*[\"']${agentPackage.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}/client[\"']\\\\s*\\\\);?`,\n    \"g\",\n  );\n\n  const newContent = originalContent.replace(agentImportPattern, \"\");\n\n  if (newContent === originalContent) {\n    return {\n      success: false,\n      filePath,\n      message: `Could not find agent ${agent} import to remove`,\n    };\n  }\n\n  return {\n    success: true,\n    filePath,\n    message: `Remove ${agent} agent`,\n    originalContent,\n    newContent,\n  };\n};\n\nexport const previewAgentRemoval = (\n  projectRoot: string,\n  framework: Framework,\n  nextRouterType: NextRouterType,\n  agent: string,\n): TransformResult => {\n  const filePath = findReactGrabFile(projectRoot, framework, nextRouterType);\n\n  if (!filePath) {\n    return {\n      success: true,\n      filePath: \"\",\n      message: \"Could not find file containing React Grab configuration\",\n      noChanges: true,\n    };\n  }\n\n  const originalContent = readFileSync(filePath, \"utf-8\");\n\n  switch (framework) {\n    case \"next\":\n      return removeAgentFromNextApp(originalContent, agent, filePath);\n    case \"vite\":\n      return removeAgentFromVite(originalContent, agent, filePath);\n    case \"tanstack\":\n      return removeAgentFromTanStack(originalContent, agent, filePath);\n    case \"webpack\":\n      return removeAgentFromWebpack(originalContent, agent, filePath);\n    default:\n      return {\n        success: false,\n        filePath,\n        message: `Unknown framework: ${framework}`,\n      };\n  }\n};\n\nexport const previewPackageJsonAgentRemoval = (\n  projectRoot: string,\n  agent: string,\n): PackageJsonTransformResult => {\n  const packageJsonPath = join(projectRoot, \"package.json\");\n\n  if (!existsSync(packageJsonPath)) {\n    return {\n      success: true,\n      filePath: \"\",\n      message: \"Could not find package.json\",\n      noChanges: true,\n    };\n  }\n\n  const originalContent = readFileSync(packageJsonPath, \"utf-8\");\n  const allPrefixVariants = getAllAgentPrefixVariants(agent);\n\n  if (allPrefixVariants.length === 0) {\n    return {\n      success: true,\n      filePath: packageJsonPath,\n      message: `Unknown agent: ${agent}`,\n      noChanges: true,\n    };\n  }\n\n  const hasAnyPrefix = allPrefixVariants.some((prefix) =>\n    originalContent.includes(prefix),\n  );\n\n  if (!hasAnyPrefix) {\n    return {\n      success: true,\n      filePath: packageJsonPath,\n      message: `Agent ${agent} dev script is not configured`,\n      noChanges: true,\n    };\n  }\n\n  try {\n    const packageJson = JSON.parse(originalContent);\n\n    for (const scriptKey of Object.keys(packageJson.scripts || {})) {\n      let scriptValue = packageJson.scripts[scriptKey];\n      if (typeof scriptValue === \"string\") {\n        for (const prefix of allPrefixVariants) {\n          if (scriptValue.includes(prefix)) {\n            scriptValue = scriptValue\n              .replace(prefix + \" \", \"\")\n              .replace(prefix, \"\");\n          }\n        }\n        packageJson.scripts[scriptKey] = scriptValue;\n      }\n    }\n\n    const newContent = JSON.stringify(packageJson, null, 2) + \"\\n\";\n\n    return {\n      success: true,\n      filePath: packageJsonPath,\n      message: `Remove ${agent} server from dev script`,\n      originalContent,\n      newContent,\n    };\n  } catch {\n    return {\n      success: false,\n      filePath: packageJsonPath,\n      message: \"Failed to parse package.json\",\n    };\n  }\n};\n\nexport const previewCdnTransform = (\n  projectRoot: string,\n  framework: Framework,\n  nextRouterType: NextRouterType,\n  targetCdnDomain: string,\n): TransformResult => {\n  const filePath = findReactGrabFile(projectRoot, framework, nextRouterType);\n  if (!filePath) {\n    return {\n      success: false,\n      filePath: \"\",\n      message: \"Could not find React Grab file\",\n    };\n  }\n  const originalContent = readFileSync(filePath, \"utf-8\");\n  const newContent = originalContent\n    .replace(\n      /(https?:)?\\/\\/[^/\\s\"']+(?=\\/(?:@?react-grab))/g,\n      `//${targetCdnDomain}`,\n    )\n    .replace(\n      /(https?:)?\\/\\/[^/\\s\"']*react-grab[^/\\s\"']*\\.com(?=\\/script\\.js)/g,\n      `//${targetCdnDomain}`,\n    );\n  if (newContent === originalContent) {\n    return {\n      success: true,\n      filePath,\n      message: \"CDN already set\",\n      noChanges: true,\n    };\n  }\n  return {\n    success: true,\n    filePath,\n    message: \"Update CDN\",\n    originalContent,\n    newContent,\n  };\n};\n"
  },
  {
    "path": "packages/cli/test/configure.test.ts",
    "content": "import { vi, describe, expect, it, beforeEach } from \"vitest\";\nimport {\n  previewOptionsTransform,\n  applyOptionsTransform,\n  type ReactGrabOptions,\n} from \"../src/utils/transform.js\";\n\nvi.mock(\"node:fs\", () => ({\n  existsSync: vi.fn(),\n  readFileSync: vi.fn(),\n  writeFileSync: vi.fn(),\n  accessSync: vi.fn(),\n  constants: { W_OK: 2 },\n}));\n\nimport { existsSync, readFileSync, writeFileSync, accessSync } from \"node:fs\";\n\nconst mockExistsSync = vi.mocked(existsSync);\nconst mockReadFileSync = vi.mocked(readFileSync);\nconst mockWriteFileSync = vi.mocked(writeFileSync);\nconst mockAccessSync = vi.mocked(accessSync);\n\nbeforeEach(() => {\n  vi.clearAllMocks();\n});\n\ndescribe(\"previewOptionsTransform - Next.js App Router\", () => {\n  const layoutWithReactGrab = `import Script from \"next/script\";\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head>\n        {process.env.NODE_ENV === \"development\" && (\n          <Script\n            src=\"//unpkg.com/react-grab/dist/index.global.js\"\n            crossOrigin=\"anonymous\"\n            strategy=\"beforeInteractive\"\n          />\n        )}\n      </head>\n      <body>{children}</body>\n    </html>\n  );\n}`;\n\n  it(\"should add activationKey option to existing React Grab script\", () => {\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"layout.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(layoutWithReactGrab);\n\n    const options: ReactGrabOptions = {\n      activationKey: \"Meta+K\",\n    };\n\n    const result = previewOptionsTransform(\"/test\", \"next\", \"app\", options);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"data-options\");\n    expect(result.newContent).toContain(\"activationKey\");\n    expect(result.newContent).toContain(\"Meta+K\");\n  });\n\n  it(\"should preserve valid JSX format when adding data-options to self-closing Script\", () => {\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"layout.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(layoutWithReactGrab);\n\n    const options: ReactGrabOptions = {\n      activationMode: \"toggle\",\n    };\n\n    const result = previewOptionsTransform(\"/test\", \"next\", \"app\", options);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).not.toMatch(/\\/\\s*\\n\\s*data-options/);\n    expect(result.newContent).toMatch(/data-options.*\\n\\s*\\/>/s);\n  });\n\n  it(\"should not split self-closing tag when adding data-options\", () => {\n    const layoutWithSelfClosingScript = `import Script from \"next/script\";\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <Script\n          src=\"//unpkg.com/react-grab/dist/index.global.js\"\n          crossOrigin=\"anonymous\"\n          strategy=\"beforeInteractive\"\n        />\n      </head>\n      <body>{children}</body>\n    </html>\n  );\n}`;\n\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"layout.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(layoutWithSelfClosingScript);\n\n    const options: ReactGrabOptions = {\n      activationMode: \"toggle\",\n      allowActivationInsideInput: false,\n      maxContextLines: 3,\n    };\n\n    const result = previewOptionsTransform(\"/test\", \"next\", \"app\", options);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"data-options={JSON.stringify(\");\n    expect(result.newContent).toContain('activationMode: \"toggle\"');\n    expect(result.newContent).toContain(\"allowActivationInsideInput: false\");\n    expect(result.newContent).toContain(\"maxContextLines: 3\");\n    expect(result.newContent).toContain(\"/>\");\n    expect(result.newContent).not.toMatch(/\\}\\)\\s*\\n\\s*\\n\\s*\\/>/);\n    expect(result.newContent).not.toMatch(\n      /strategy=\"beforeInteractive\"\\s*\\/\\s*\\n/,\n    );\n  });\n\n  it(\"should not add extra blank line before closing tag\", () => {\n    const layoutWithScript = `import Script from \"next/script\";\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head>\n        {process.env.NODE_ENV === \"development\" && (\n          <Script\n            src=\"//unpkg.com/react-grab/dist/index.global.js\"\n            crossOrigin=\"anonymous\"\n            strategy=\"beforeInteractive\"\n          />\n        )}\n      </head>\n      <body>{children}</body>\n    </html>\n  );\n}`;\n\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"layout.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(layoutWithScript);\n\n    const options: ReactGrabOptions = {\n      activationKey: \"Meta+K\",\n    };\n\n    const result = previewOptionsTransform(\"/test\", \"next\", \"app\", options);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"data-options\");\n    expect(result.newContent).not.toMatch(/\\}\\)\\n\\s*\\n\\s*\\/>/);\n  });\n\n  it(\"should add multiple options to React Grab script\", () => {\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"layout.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(layoutWithReactGrab);\n\n    const options: ReactGrabOptions = {\n      activationKey: \"Ctrl+Shift+G\",\n      activationMode: \"toggle\",\n      keyHoldDuration: 200,\n    };\n\n    const result = previewOptionsTransform(\"/test\", \"next\", \"app\", options);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"activationKey\");\n    expect(result.newContent).toContain(\"Ctrl+Shift+G\");\n    expect(result.newContent).toContain(\"activationMode\");\n    expect(result.newContent).toContain(\"toggle\");\n    expect(result.newContent).toContain(\"keyHoldDuration\");\n    expect(result.newContent).toContain(\"200\");\n  });\n\n  it(\"should add allowActivationInsideInput option\", () => {\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"layout.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(layoutWithReactGrab);\n\n    const options: ReactGrabOptions = {\n      allowActivationInsideInput: false,\n    };\n\n    const result = previewOptionsTransform(\"/test\", \"next\", \"app\", options);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"allowActivationInsideInput\");\n    expect(result.newContent).toContain(\"false\");\n  });\n\n  it(\"should add maxContextLines option\", () => {\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"layout.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(layoutWithReactGrab);\n\n    const options: ReactGrabOptions = {\n      maxContextLines: 5,\n    };\n\n    const result = previewOptionsTransform(\"/test\", \"next\", \"app\", options);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"maxContextLines\");\n    expect(result.newContent).toContain(\"5\");\n  });\n\n  it(\"should update existing data-options attribute\", () => {\n    const layoutWithOptions = `import Script from \"next/script\";\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head>\n        {process.env.NODE_ENV === \"development\" && (\n          <Script\n            src=\"//unpkg.com/react-grab/dist/index.global.js\"\n            crossOrigin=\"anonymous\"\n            strategy=\"beforeInteractive\"\n            data-options={JSON.stringify({ activationKey: \"Alt\" })}\n          />\n        )}\n      </head>\n      <body>{children}</body>\n    </html>\n  );\n}`;\n\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"layout.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(layoutWithOptions);\n\n    const options: ReactGrabOptions = {\n      activationKey: \"Meta+Space\",\n    };\n\n    const result = previewOptionsTransform(\"/test\", \"next\", \"app\", options);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"Meta+Space\");\n    expect(result.newContent).not.toContain('\"Alt\"');\n  });\n\n  it(\"should fail when React Grab is not found\", () => {\n    const layoutWithoutReactGrab = `export default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <body>{children}</body>\n    </html>\n  );\n}`;\n\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"layout.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(layoutWithoutReactGrab);\n\n    const options: ReactGrabOptions = {\n      activationKey: \"Meta+K\",\n    };\n\n    const result = previewOptionsTransform(\"/test\", \"next\", \"app\", options);\n\n    expect(result.success).toBe(false);\n    expect(result.message).toContain(\"Could not find React Grab\");\n  });\n\n  it(\"should fail when layout file not found\", () => {\n    mockExistsSync.mockReturnValue(false);\n\n    const options: ReactGrabOptions = {\n      activationKey: \"Meta+K\",\n    };\n\n    const result = previewOptionsTransform(\"/test\", \"next\", \"app\", options);\n\n    expect(result.success).toBe(false);\n    expect(result.message).toContain(\"Could not find file\");\n  });\n});\n\ndescribe(\"previewOptionsTransform - Next.js Pages Router\", () => {\n  const documentWithReactGrab = `import { Html, Head, Main, NextScript } from \"next/document\";\nimport Script from \"next/script\";\n\nexport default function Document() {\n  return (\n    <Html>\n      <Head>\n        <Script\n          src=\"//unpkg.com/react-grab/dist/index.global.js\"\n          crossOrigin=\"anonymous\"\n          strategy=\"beforeInteractive\"\n        />\n      </Head>\n      <body>\n        <Main />\n        <NextScript />\n      </body>\n    </Html>\n  );\n}`;\n\n  it(\"should add options to Pages Router document\", () => {\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"_document.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(documentWithReactGrab);\n\n    const options: ReactGrabOptions = {\n      activationKey: \"Meta+G\",\n      activationMode: \"hold\",\n    };\n\n    const result = previewOptionsTransform(\"/test\", \"next\", \"pages\", options);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"data-options\");\n    expect(result.newContent).toContain(\"Meta+G\");\n    expect(result.newContent).toContain(\"hold\");\n  });\n});\n\ndescribe(\"previewOptionsTransform - Vite\", () => {\n  const entryWithReactGrab = `if (import.meta.env.DEV) {\n  import(\"react-grab\");\n}\n\nimport React from \"react\";\nimport ReactDOM from \"react-dom/client\";`;\n\n  it(\"should add options to Vite import\", () => {\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"main.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(entryWithReactGrab);\n\n    const options: ReactGrabOptions = {\n      activationKey: \"Space\",\n    };\n\n    const result = previewOptionsTransform(\"/test\", \"vite\", \"unknown\", options);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"init(\");\n    expect(result.newContent).toContain('\"activationKey\":\"Space\"');\n  });\n\n  it(\"should update existing options in Vite import without duplicating\", () => {\n    const entryWithExistingOptions = `if (import.meta.env.DEV) {\n  import(\"react-grab\").then((m) => m.init({\"activationKey\":\"g\"}));\n}\n\nimport React from \"react\";\nimport ReactDOM from \"react-dom/client\";`;\n\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"main.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(entryWithExistingOptions);\n\n    const options: ReactGrabOptions = {\n      activationKey: \"Meta+K\",\n    };\n\n    const result = previewOptionsTransform(\"/test\", \"vite\", \"unknown\", options);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain('\"activationKey\":\"Meta+K\"');\n    expect(result.newContent).not.toContain('\"activationKey\":\"g\"');\n    const initCount = (result.newContent!.match(/\\.then\\(/g) || []).length;\n    expect(initCount).toBe(1);\n  });\n\n  it(\"should add multiple options to Vite import\", () => {\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"main.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(entryWithReactGrab);\n\n    const options: ReactGrabOptions = {\n      activationKey: \"Alt+E\",\n      activationMode: \"toggle\",\n      maxContextLines: 10,\n    };\n\n    const result = previewOptionsTransform(\"/test\", \"vite\", \"unknown\", options);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\".then((m) => m.init(\");\n    expect(result.newContent).toContain('\"activationKey\":\"Alt+E\"');\n    expect(result.newContent).toContain('\"activationMode\":\"toggle\"');\n    expect(result.newContent).toContain('\"maxContextLines\":10');\n  });\n\n  it(\"should fail when React Grab import not found\", () => {\n    const entryWithoutReactGrab = `import React from \"react\";\nimport ReactDOM from \"react-dom/client\";`;\n\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"main.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(entryWithoutReactGrab);\n\n    const options: ReactGrabOptions = {\n      activationKey: \"Space\",\n    };\n\n    const result = previewOptionsTransform(\"/test\", \"vite\", \"unknown\", options);\n\n    expect(result.success).toBe(false);\n  });\n});\n\ndescribe(\"previewOptionsTransform - Webpack\", () => {\n  const entryWithReactGrab = `if (process.env.NODE_ENV === \"development\") {\n  import(\"react-grab\");\n}\n\nimport React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport App from \"./App\";\n\nReactDOM.createRoot(document.getElementById(\"root\")!).render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>\n);`;\n\n  it(\"should add options to Webpack import\", () => {\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"index.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(entryWithReactGrab);\n\n    const options: ReactGrabOptions = {\n      activationKey: \"Ctrl+K\",\n    };\n\n    const result = previewOptionsTransform(\n      \"/test\",\n      \"webpack\",\n      \"unknown\",\n      options,\n    );\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\".then((m) => m.init(\");\n    expect(result.newContent).toContain('\"activationKey\":\"Ctrl+K\"');\n  });\n\n  it(\"should update existing options in Webpack import without duplicating\", () => {\n    const entryWithExistingOptions = `if (process.env.NODE_ENV === \"development\") {\n  import(\"react-grab\").then((m) => m.init({\"activationKey\":\"Ctrl+G\"}));\n}\n\nimport React from \"react\";\nimport ReactDOM from \"react-dom/client\";`;\n\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"index.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(entryWithExistingOptions);\n\n    const options: ReactGrabOptions = {\n      activationKey: \"Space\",\n    };\n\n    const result = previewOptionsTransform(\n      \"/test\",\n      \"webpack\",\n      \"unknown\",\n      options,\n    );\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain('\"activationKey\":\"Space\"');\n    expect(result.newContent).not.toContain('\"activationKey\":\"Ctrl+G\"');\n    const initCount = (result.newContent!.match(/\\.then\\(/g) || []).length;\n    expect(initCount).toBe(1);\n  });\n\n  it(\"should handle all configuration options\", () => {\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"index.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(entryWithReactGrab);\n\n    const options: ReactGrabOptions = {\n      activationKey: \"Meta+Shift+D\",\n      activationMode: \"hold\",\n      keyHoldDuration: 300,\n      allowActivationInsideInput: true,\n      maxContextLines: 7,\n    };\n\n    const result = previewOptionsTransform(\n      \"/test\",\n      \"webpack\",\n      \"unknown\",\n      options,\n    );\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain('\"activationKey\":\"Meta+Shift+D\"');\n    expect(result.newContent).toContain('\"activationMode\":\"hold\"');\n    expect(result.newContent).toContain('\"keyHoldDuration\":300');\n    expect(result.newContent).toContain('\"allowActivationInsideInput\":true');\n    expect(result.newContent).toContain('\"maxContextLines\":7');\n  });\n});\n\ndescribe(\"previewOptionsTransform - Unknown framework\", () => {\n  it(\"should fail for unknown framework (no file found)\", () => {\n    mockExistsSync.mockReturnValue(false);\n\n    const options: ReactGrabOptions = {\n      activationKey: \"Meta+K\",\n    };\n\n    const result = previewOptionsTransform(\n      \"/test\",\n      \"unknown\",\n      \"unknown\",\n      options,\n    );\n\n    expect(result.success).toBe(false);\n    expect(result.message).toContain(\"Could not find file\");\n  });\n});\n\ndescribe(\"applyOptionsTransform\", () => {\n  it(\"should write file when result has newContent and file is writable\", () => {\n    mockAccessSync.mockReturnValue(undefined);\n    mockWriteFileSync.mockReturnValue(undefined);\n\n    const result = {\n      success: true,\n      filePath: \"/test/layout.tsx\",\n      message: \"Update React Grab options\",\n      originalContent: \"old content\",\n      newContent: \"new content with options\",\n    };\n\n    const writeResult = applyOptionsTransform(result);\n\n    expect(writeResult.success).toBe(true);\n    expect(mockWriteFileSync).toHaveBeenCalledWith(\n      \"/test/layout.tsx\",\n      \"new content with options\",\n    );\n  });\n\n  it(\"should return error when file is not writable\", () => {\n    mockAccessSync.mockImplementation(() => {\n      throw new Error(\"EACCES\");\n    });\n\n    const result = {\n      success: true,\n      filePath: \"/test/layout.tsx\",\n      message: \"Update React Grab options\",\n      originalContent: \"old content\",\n      newContent: \"new content\",\n    };\n\n    const writeResult = applyOptionsTransform(result);\n\n    expect(writeResult.success).toBe(false);\n    expect(writeResult.error).toContain(\"Cannot write to\");\n    expect(mockWriteFileSync).not.toHaveBeenCalled();\n  });\n\n  it(\"should not write file when result has noChanges\", () => {\n    const result = {\n      success: true,\n      filePath: \"/test/layout.tsx\",\n      message: \"No changes needed\",\n      noChanges: true,\n    };\n\n    const writeResult = applyOptionsTransform(result);\n\n    expect(writeResult.success).toBe(true);\n    expect(mockWriteFileSync).not.toHaveBeenCalled();\n  });\n\n  it(\"should not write file when result is not successful\", () => {\n    const result = {\n      success: false,\n      filePath: \"/test/layout.tsx\",\n      message: \"Error\",\n    };\n\n    const writeResult = applyOptionsTransform(result);\n\n    expect(writeResult.success).toBe(true);\n    expect(mockWriteFileSync).not.toHaveBeenCalled();\n  });\n});\n\ndescribe(\"Activation key formats\", () => {\n  const layoutWithReactGrab = `import Script from \"next/script\";\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <Script\n          src=\"//unpkg.com/react-grab/dist/index.global.js\"\n          crossOrigin=\"anonymous\"\n          strategy=\"beforeInteractive\"\n        />\n      </head>\n      <body>{children}</body>\n    </html>\n  );\n}`;\n\n  beforeEach(() => {\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"layout.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(layoutWithReactGrab);\n  });\n\n  it(\"should handle simple key like Space\", () => {\n    const options: ReactGrabOptions = {\n      activationKey: \"Space\",\n    };\n\n    const result = previewOptionsTransform(\"/test\", \"next\", \"app\", options);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"Space\");\n  });\n\n  it(\"should handle single letter key\", () => {\n    const options: ReactGrabOptions = {\n      activationKey: \"g\",\n    };\n\n    const result = previewOptionsTransform(\"/test\", \"next\", \"app\", options);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain('\"g\"');\n  });\n\n  it(\"should handle modifier + key combo\", () => {\n    const options: ReactGrabOptions = {\n      activationKey: \"Meta+K\",\n    };\n\n    const result = previewOptionsTransform(\"/test\", \"next\", \"app\", options);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"Meta+K\");\n  });\n\n  it(\"should handle multiple modifiers + key\", () => {\n    const options: ReactGrabOptions = {\n      activationKey: \"Ctrl+Shift+Alt+G\",\n    };\n\n    const result = previewOptionsTransform(\"/test\", \"next\", \"app\", options);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"Ctrl+Shift+Alt+G\");\n  });\n\n  it(\"should handle function keys\", () => {\n    const options: ReactGrabOptions = {\n      activationKey: \"F1\",\n    };\n\n    const result = previewOptionsTransform(\"/test\", \"next\", \"app\", options);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"F1\");\n  });\n\n  it(\"should handle Escape key\", () => {\n    const options: ReactGrabOptions = {\n      activationKey: \"Escape\",\n    };\n\n    const result = previewOptionsTransform(\"/test\", \"next\", \"app\", options);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"Escape\");\n  });\n\n  it(\"should handle Meta+Escape combo\", () => {\n    const options: ReactGrabOptions = {\n      activationKey: \"Meta+Escape\",\n    };\n\n    const result = previewOptionsTransform(\"/test\", \"next\", \"app\", options);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"Meta+Escape\");\n  });\n});\n"
  },
  {
    "path": "packages/cli/test/detect.test.ts",
    "content": "import { vi, describe, expect, it, beforeEach } from \"vitest\";\nimport {\n  detectFramework,\n  detectMonorepo,\n  detectNextRouterType,\n  detectReactGrab,\n  detectInstalledAgents,\n  detectUnsupportedFramework,\n  detectAvailableAgentCLIs,\n} from \"../src/utils/detect.js\";\n\nvi.mock(\"node:fs\", () => ({\n  existsSync: vi.fn(),\n  readFileSync: vi.fn(),\n}));\n\nvi.mock(\"node:child_process\", () => ({\n  execSync: vi.fn(),\n}));\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { execSync } from \"node:child_process\";\n\nconst mockExistsSync = vi.mocked(existsSync);\nconst mockReadFileSync = vi.mocked(readFileSync);\nconst mockExecSync = vi.mocked(execSync);\n\nbeforeEach(() => {\n  vi.clearAllMocks();\n});\n\ndescribe(\"detectFramework\", () => {\n  it(\"should detect Next.js\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(\n      JSON.stringify({ dependencies: { next: \"14.0.0\", react: \"18.0.0\" } }),\n    );\n\n    expect(detectFramework(\"/test\")).toBe(\"next\");\n  });\n\n  it(\"should detect Vite\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(\n      JSON.stringify({ devDependencies: { vite: \"5.0.0\" } }),\n    );\n\n    expect(detectFramework(\"/test\")).toBe(\"vite\");\n  });\n\n  it(\"should detect Webpack\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(\n      JSON.stringify({ devDependencies: { webpack: \"5.0.0\" } }),\n    );\n\n    expect(detectFramework(\"/test\")).toBe(\"webpack\");\n  });\n\n  it(\"should return unknown when no framework detected\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(\n      JSON.stringify({ dependencies: { react: \"18.0.0\" } }),\n    );\n\n    expect(detectFramework(\"/test\")).toBe(\"unknown\");\n  });\n\n  it(\"should return unknown when no package.json exists\", () => {\n    mockExistsSync.mockReturnValue(false);\n\n    expect(detectFramework(\"/test\")).toBe(\"unknown\");\n  });\n\n  it(\"should return unknown for malformed package.json\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(\"{ invalid json }\");\n\n    expect(detectFramework(\"/test\")).toBe(\"unknown\");\n  });\n\n  it(\"should prioritize Next.js over Vite if both are present\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(\n      JSON.stringify({\n        dependencies: { next: \"14.0.0\" },\n        devDependencies: { vite: \"5.0.0\" },\n      }),\n    );\n\n    expect(detectFramework(\"/test\")).toBe(\"next\");\n  });\n});\n\ndescribe(\"detectNextRouterType\", () => {\n  it(\"should detect App Router when app/ exists\", () => {\n    mockExistsSync.mockImplementation((path) => {\n      return String(path).endsWith(\"/app\");\n    });\n\n    expect(detectNextRouterType(\"/test\")).toBe(\"app\");\n  });\n\n  it(\"should detect App Router when src/app/ exists\", () => {\n    mockExistsSync.mockImplementation((path) => {\n      return String(path).endsWith(\"/src/app\");\n    });\n\n    expect(detectNextRouterType(\"/test\")).toBe(\"app\");\n  });\n\n  it(\"should detect Pages Router when pages/ exists\", () => {\n    mockExistsSync.mockImplementation((path) => {\n      return String(path).endsWith(\"/pages\");\n    });\n\n    expect(detectNextRouterType(\"/test\")).toBe(\"pages\");\n  });\n\n  it(\"should detect Pages Router when src/pages/ exists\", () => {\n    mockExistsSync.mockImplementation((path) => {\n      return String(path).endsWith(\"/src/pages\");\n    });\n\n    expect(detectNextRouterType(\"/test\")).toBe(\"pages\");\n  });\n\n  it(\"should prefer App Router if both exist\", () => {\n    mockExistsSync.mockImplementation((path) => {\n      const pathStr = String(path);\n      return pathStr.endsWith(\"/app\") || pathStr.endsWith(\"/pages\");\n    });\n\n    expect(detectNextRouterType(\"/test\")).toBe(\"app\");\n  });\n\n  it(\"should return unknown when no router directories exist\", () => {\n    mockExistsSync.mockReturnValue(false);\n\n    expect(detectNextRouterType(\"/test\")).toBe(\"unknown\");\n  });\n});\n\ndescribe(\"detectMonorepo\", () => {\n  it(\"should detect pnpm monorepo\", () => {\n    mockExistsSync.mockImplementation((path) => {\n      return String(path).endsWith(\"pnpm-workspace.yaml\");\n    });\n\n    expect(detectMonorepo(\"/test\")).toBe(true);\n  });\n\n  it(\"should detect lerna monorepo\", () => {\n    mockExistsSync.mockImplementation((path) => {\n      return String(path).endsWith(\"lerna.json\");\n    });\n\n    expect(detectMonorepo(\"/test\")).toBe(true);\n  });\n\n  it(\"should detect npm/yarn workspaces\", () => {\n    mockExistsSync.mockImplementation((path) => {\n      return String(path).endsWith(\"package.json\");\n    });\n    mockReadFileSync.mockReturnValue(\n      JSON.stringify({ workspaces: [\"packages/*\"] }),\n    );\n\n    expect(detectMonorepo(\"/test\")).toBe(true);\n  });\n\n  it(\"should return false for non-monorepo\", () => {\n    mockExistsSync.mockImplementation((path) => {\n      return String(path).endsWith(\"package.json\");\n    });\n    mockReadFileSync.mockReturnValue(\n      JSON.stringify({ dependencies: { react: \"18.0.0\" } }),\n    );\n\n    expect(detectMonorepo(\"/test\")).toBe(false);\n  });\n});\n\ndescribe(\"detectReactGrab\", () => {\n  it(\"should detect react-grab in dependencies\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(\n      JSON.stringify({ dependencies: { \"react-grab\": \"1.0.0\" } }),\n    );\n\n    expect(detectReactGrab(\"/test\")).toBe(true);\n  });\n\n  it(\"should detect react-grab in devDependencies\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(\n      JSON.stringify({ devDependencies: { \"react-grab\": \"1.0.0\" } }),\n    );\n\n    expect(detectReactGrab(\"/test\")).toBe(true);\n  });\n\n  it(\"should return false when react-grab is not installed\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(\n      JSON.stringify({ dependencies: { react: \"18.0.0\" } }),\n    );\n\n    expect(detectReactGrab(\"/test\")).toBe(false);\n  });\n\n  it(\"should return false when no package.json exists\", () => {\n    mockExistsSync.mockReturnValue(false);\n\n    expect(detectReactGrab(\"/test\")).toBe(false);\n  });\n\n  it(\"should return false for malformed package.json\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(\"not valid json\");\n\n    expect(detectReactGrab(\"/test\")).toBe(false);\n  });\n});\n\ndescribe(\"detectInstalledAgents\", () => {\n  it(\"should detect installed agents\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(\n      JSON.stringify({\n        devDependencies: {\n          \"@react-grab/cursor\": \"1.0.0\",\n          \"@react-grab/claude-code\": \"1.0.0\",\n        },\n      }),\n    );\n\n    const agents = detectInstalledAgents(\"/test\");\n    expect(agents).toContain(\"cursor\");\n    expect(agents).toContain(\"claude-code\");\n    expect(agents).not.toContain(\"opencode\");\n  });\n\n  it(\"should return empty array when no agents installed\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(\n      JSON.stringify({ dependencies: { \"react-grab\": \"1.0.0\" } }),\n    );\n\n    expect(detectInstalledAgents(\"/test\")).toEqual([]);\n  });\n\n  it(\"should return empty array when no package.json exists\", () => {\n    mockExistsSync.mockReturnValue(false);\n\n    expect(detectInstalledAgents(\"/test\")).toEqual([]);\n  });\n\n  it(\"should return empty array for malformed package.json\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(\"{ broken }\");\n\n    expect(detectInstalledAgents(\"/test\")).toEqual([]);\n  });\n\n  it(\"should detect mcp when @react-grab/mcp is installed\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(\n      JSON.stringify({\n        devDependencies: {\n          \"@react-grab/mcp\": \"0.1.0\",\n        },\n      }),\n    );\n\n    const agents = detectInstalledAgents(\"/test\");\n    expect(agents).toContain(\"mcp\");\n  });\n});\n\ndescribe(\"detectMonorepo\", () => {\n  it(\"should return false for malformed package.json\", () => {\n    mockExistsSync.mockImplementation((path) => {\n      return String(path).endsWith(\"package.json\");\n    });\n    mockReadFileSync.mockReturnValue(\"invalid\");\n\n    expect(detectMonorepo(\"/test\")).toBe(false);\n  });\n});\n\ndescribe(\"detectUnsupportedFramework\", () => {\n  it(\"should detect Remix\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(\n      JSON.stringify({ dependencies: { \"@remix-run/react\": \"2.0.0\" } }),\n    );\n\n    expect(detectUnsupportedFramework(\"/test\")).toBe(\"remix\");\n  });\n\n  it(\"should detect Astro\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(\n      JSON.stringify({ devDependencies: { astro: \"4.0.0\" } }),\n    );\n\n    expect(detectUnsupportedFramework(\"/test\")).toBe(\"astro\");\n  });\n\n  it(\"should detect SvelteKit\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(\n      JSON.stringify({ devDependencies: { \"@sveltejs/kit\": \"2.0.0\" } }),\n    );\n\n    expect(detectUnsupportedFramework(\"/test\")).toBe(\"sveltekit\");\n  });\n\n  it(\"should detect Gatsby\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(\n      JSON.stringify({ dependencies: { gatsby: \"5.0.0\" } }),\n    );\n\n    expect(detectUnsupportedFramework(\"/test\")).toBe(\"gatsby\");\n  });\n\n  it(\"should return null for supported frameworks\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(\n      JSON.stringify({ dependencies: { next: \"14.0.0\" } }),\n    );\n\n    expect(detectUnsupportedFramework(\"/test\")).toBe(null);\n  });\n\n  it(\"should return null when no package.json exists\", () => {\n    mockExistsSync.mockReturnValue(false);\n\n    expect(detectUnsupportedFramework(\"/test\")).toBe(null);\n  });\n\n  it(\"should return null for malformed package.json\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(\"invalid json\");\n\n    expect(detectUnsupportedFramework(\"/test\")).toBe(null);\n  });\n});\n\ndescribe(\"detectAvailableAgentCLIs\", () => {\n  it(\"should return all available CLIs\", () => {\n    mockExecSync.mockImplementation(() => Buffer.from(\"\"));\n\n    const available = detectAvailableAgentCLIs();\n    expect(available).toContain(\"claude\");\n    expect(available).toContain(\"cursor-agent\");\n    expect(available).toContain(\"opencode\");\n  });\n\n  it(\"should return only available CLIs\", () => {\n    mockExecSync.mockImplementation((command) => {\n      const commandString = String(command);\n      if (commandString.includes(\"claude\")) {\n        return Buffer.from(\"/usr/local/bin/claude\");\n      }\n      if (commandString.includes(\"opencode\")) {\n        return Buffer.from(\"/usr/local/bin/opencode\");\n      }\n      throw new Error(\"Command not found\");\n    });\n\n    const available = detectAvailableAgentCLIs();\n    expect(available).toContain(\"claude\");\n    expect(available).toContain(\"opencode\");\n    expect(available).not.toContain(\"cursor-agent\");\n  });\n\n  it(\"should return empty array when no CLIs are available\", () => {\n    mockExecSync.mockImplementation(() => {\n      throw new Error(\"Command not found\");\n    });\n\n    expect(detectAvailableAgentCLIs()).toEqual([]);\n  });\n});\n"
  },
  {
    "path": "packages/cli/test/diff.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport { generateDiff, formatDiff } from \"../src/utils/diff.js\";\n\ndescribe(\"generateDiff\", () => {\n  it(\"should detect added lines\", () => {\n    const original = \"line1\\nline2\";\n    const updated = \"line1\\nline2\\nline3\";\n\n    const diff = generateDiff(original, updated);\n\n    expect(diff).toContainEqual({\n      type: \"unchanged\",\n      content: \"line1\",\n      lineNumber: 1,\n    });\n    expect(diff).toContainEqual({\n      type: \"unchanged\",\n      content: \"line2\",\n      lineNumber: 2,\n    });\n    expect(diff).toContainEqual({\n      type: \"added\",\n      content: \"line3\",\n      lineNumber: 3,\n    });\n  });\n\n  it(\"should detect removed lines\", () => {\n    const original = \"line1\\nline2\\nline3\";\n    const updated = \"line1\\nline3\";\n\n    const diff = generateDiff(original, updated);\n\n    expect(\n      diff.some((line) => line.type === \"removed\" && line.content === \"line2\"),\n    ).toBe(true);\n  });\n\n  it(\"should handle identical content\", () => {\n    const content = \"line1\\nline2\";\n\n    const diff = generateDiff(content, content);\n\n    expect(diff.every((line) => line.type === \"unchanged\")).toBe(true);\n  });\n\n  it(\"should handle empty strings\", () => {\n    const diff = generateDiff(\"\", \"line1\");\n\n    expect(diff).toContainEqual({\n      type: \"added\",\n      content: \"line1\",\n      lineNumber: 1,\n    });\n  });\n\n  it(\"should handle complex changes\", () => {\n    const original = `import React from \"react\";\n\nfunction App() {\n  return <div>Hello</div>;\n}`;\n\n    const updated = `import React from \"react\";\nimport Script from \"next/script\";\n\nfunction App() {\n  return <div>Hello World</div>;\n}`;\n\n    const diff = generateDiff(original, updated);\n\n    expect(\n      diff.some(\n        (line) => line.type === \"added\" && line.content.includes(\"next/script\"),\n      ),\n    ).toBe(true);\n    expect(\n      diff.some(\n        (line) =>\n          line.type === \"removed\" && line.content.includes(\"Hello</div>\"),\n      ),\n    ).toBe(true);\n    expect(\n      diff.some(\n        (line) => line.type === \"added\" && line.content.includes(\"Hello World\"),\n      ),\n    ).toBe(true);\n  });\n});\n\ndescribe(\"formatDiff\", () => {\n  it(\"should format added lines in green\", () => {\n    const diff = [\n      { type: \"unchanged\" as const, content: \"line1\", lineNumber: 1 },\n      { type: \"added\" as const, content: \"line2\", lineNumber: 2 },\n    ];\n\n    const formatted = formatDiff(diff);\n\n    expect(formatted).toContain(\"+ line2\");\n    expect(formatted).toContain(\"\\x1b[32m\");\n  });\n\n  it(\"should format removed lines in red\", () => {\n    const diff = [\n      { type: \"unchanged\" as const, content: \"line1\", lineNumber: 1 },\n      { type: \"removed\" as const, content: \"line2\" },\n    ];\n\n    const formatted = formatDiff(diff);\n\n    expect(formatted).toContain(\"- line2\");\n    expect(formatted).toContain(\"\\x1b[31m\");\n  });\n\n  it(\"should show no changes message when diff is empty\", () => {\n    const diff = [\n      { type: \"unchanged\" as const, content: \"line1\", lineNumber: 1 },\n      { type: \"unchanged\" as const, content: \"line2\", lineNumber: 2 },\n    ];\n\n    const formatted = formatDiff(diff);\n\n    expect(formatted).toContain(\"No changes\");\n  });\n\n  it(\"should limit context lines around changes\", () => {\n    const diff = [\n      { type: \"unchanged\" as const, content: \"line1\", lineNumber: 1 },\n      { type: \"unchanged\" as const, content: \"line2\", lineNumber: 2 },\n      { type: \"unchanged\" as const, content: \"line3\", lineNumber: 3 },\n      { type: \"added\" as const, content: \"new line 1\", lineNumber: 4 },\n      { type: \"unchanged\" as const, content: \"line4\", lineNumber: 5 },\n      { type: \"unchanged\" as const, content: \"line5\", lineNumber: 6 },\n      { type: \"unchanged\" as const, content: \"line6\", lineNumber: 7 },\n      { type: \"unchanged\" as const, content: \"line7\", lineNumber: 8 },\n      { type: \"unchanged\" as const, content: \"line8\", lineNumber: 9 },\n      { type: \"unchanged\" as const, content: \"line9\", lineNumber: 10 },\n      { type: \"added\" as const, content: \"new line 2\", lineNumber: 11 },\n      { type: \"unchanged\" as const, content: \"line10\", lineNumber: 12 },\n    ];\n\n    const formatted = formatDiff(diff, 2);\n\n    expect(formatted).toContain(\"...\");\n    expect(formatted).toContain(\"new line 1\");\n    expect(formatted).toContain(\"new line 2\");\n  });\n});\n"
  },
  {
    "path": "packages/cli/test/install-mcp.test.ts",
    "content": "import fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { afterEach, beforeEach, describe, expect, it } from \"vitest\";\nimport {\n  type ClientDefinition,\n  upsertIntoJsonc,\n  installJsonClient,\n  installTomlClient,\n  getMcpClientNames,\n  getOpenCodeConfigPath,\n} from \"../src/utils/install-mcp.js\";\n\nlet tempDir: string;\n\nbeforeEach(() => {\n  tempDir = fs.mkdtempSync(path.join(os.tmpdir(), \"install-mcp-test-\"));\n});\n\nafterEach(() => {\n  fs.rmSync(tempDir, { recursive: true, force: true });\n});\n\nconst makeJsonClient = (\n  overrides: Partial<ClientDefinition> = {},\n): ClientDefinition => ({\n  name: \"TestClient\",\n  configPath: path.join(tempDir, \"config.json\"),\n  configKey: \"mcpServers\",\n  format: \"json\",\n  serverConfig: { command: \"npx\", args: [\"-y\", \"@react-grab/mcp\", \"--stdio\"] },\n  ...overrides,\n});\n\nconst makeTomlClient = (\n  overrides: Partial<ClientDefinition> = {},\n): ClientDefinition => ({\n  name: \"TestToml\",\n  configPath: path.join(tempDir, \"config.toml\"),\n  configKey: \"mcp_servers\",\n  format: \"toml\",\n  serverConfig: { command: \"npx\", args: [\"-y\", \"@react-grab/mcp\", \"--stdio\"] },\n  ...overrides,\n});\n\ndescribe(\"getMcpClientNames\", () => {\n  it(\"should return all 9 client names\", () => {\n    const names = getMcpClientNames();\n\n    expect(names).toHaveLength(9);\n    expect(names).toContain(\"Claude Code\");\n    expect(names).toContain(\"Codex\");\n    expect(names).toContain(\"Cursor\");\n    expect(names).toContain(\"OpenCode\");\n    expect(names).toContain(\"VS Code\");\n    expect(names).toContain(\"Amp\");\n    expect(names).toContain(\"Droid\");\n    expect(names).toContain(\"Windsurf\");\n    expect(names).toContain(\"Zed\");\n  });\n});\n\ndescribe(\"installJsonClient\", () => {\n  it(\"should create a new config file when none exists\", () => {\n    const client = makeJsonClient();\n\n    installJsonClient(client);\n\n    const content = JSON.parse(fs.readFileSync(client.configPath, \"utf8\"));\n    expect(content.mcpServers[\"react-grab-mcp\"]).toEqual(client.serverConfig);\n  });\n\n  it(\"should merge into an existing config file\", () => {\n    const client = makeJsonClient();\n    fs.writeFileSync(\n      client.configPath,\n      JSON.stringify({\n        mcpServers: { \"other-server\": { command: \"other\" } },\n      }),\n    );\n\n    installJsonClient(client);\n\n    const content = JSON.parse(fs.readFileSync(client.configPath, \"utf8\"));\n    expect(content.mcpServers[\"other-server\"]).toEqual({ command: \"other\" });\n    expect(content.mcpServers[\"react-grab-mcp\"]).toEqual(client.serverConfig);\n  });\n\n  it(\"should overwrite existing react-grab-mcp entry\", () => {\n    const client = makeJsonClient();\n    fs.writeFileSync(\n      client.configPath,\n      JSON.stringify({\n        mcpServers: { \"react-grab-mcp\": { command: \"old\" } },\n      }),\n    );\n\n    installJsonClient(client);\n\n    const content = JSON.parse(fs.readFileSync(client.configPath, \"utf8\"));\n    expect(content.mcpServers[\"react-grab-mcp\"]).toEqual(client.serverConfig);\n  });\n\n  it(\"should create the configKey when it does not exist\", () => {\n    const client = makeJsonClient();\n    fs.writeFileSync(\n      client.configPath,\n      JSON.stringify({ someOtherKey: \"value\" }),\n    );\n\n    installJsonClient(client);\n\n    const content = JSON.parse(fs.readFileSync(client.configPath, \"utf8\"));\n    expect(content.someOtherKey).toBe(\"value\");\n    expect(content.mcpServers[\"react-grab-mcp\"]).toEqual(client.serverConfig);\n  });\n\n  it(\"should create nested directories if needed\", () => {\n    const client = makeJsonClient({\n      configPath: path.join(tempDir, \"deep\", \"nested\", \"config.json\"),\n    });\n\n    installJsonClient(client);\n\n    expect(fs.existsSync(client.configPath)).toBe(true);\n    const content = JSON.parse(fs.readFileSync(client.configPath, \"utf8\"));\n    expect(content.mcpServers[\"react-grab-mcp\"]).toEqual(client.serverConfig);\n  });\n\n  it(\"should handle a dot-separated configKey like amp.mcpServers\", () => {\n    const client = makeJsonClient({ configKey: \"amp.mcpServers\" });\n\n    installJsonClient(client);\n\n    const content = JSON.parse(fs.readFileSync(client.configPath, \"utf8\"));\n    expect(content[\"amp.mcpServers\"][\"react-grab-mcp\"]).toEqual(\n      client.serverConfig,\n    );\n  });\n});\n\ndescribe(\"upsertIntoJsonc\", () => {\n  it(\"should insert into existing configKey section\", () => {\n    const filePath = path.join(tempDir, \"settings.json\");\n    const content = `// comment\\n{\\n  \"context_servers\": {\\n    \"existing\": {}\\n  }\\n}`;\n    fs.writeFileSync(filePath, content);\n\n    upsertIntoJsonc(filePath, content, \"context_servers\", \"react-grab-mcp\", {\n      command: \"npx\",\n    });\n\n    const result = fs.readFileSync(filePath, \"utf8\");\n    expect(result).toContain('\"react-grab-mcp\"');\n    expect(result).toContain(\"// comment\");\n    expect(result).toContain('\"existing\"');\n  });\n\n  it(\"should add a new configKey section when none exists\", () => {\n    const filePath = path.join(tempDir, \"settings.json\");\n    const content = `// comment\\n{\\n  \"theme\": \"dark\"\\n}`;\n    fs.writeFileSync(filePath, content);\n\n    upsertIntoJsonc(filePath, content, \"context_servers\", \"react-grab-mcp\", {\n      command: \"npx\",\n    });\n\n    const result = fs.readFileSync(filePath, \"utf8\");\n    expect(result).toContain('\"context_servers\"');\n    expect(result).toContain('\"react-grab-mcp\"');\n    expect(result).toContain(\"// comment\");\n    expect(result).toContain('\"theme\"');\n  });\n\n  it(\"should overwrite existing server entry\", () => {\n    const filePath = path.join(tempDir, \"settings.json\");\n    const content = `{\\n  \"servers\": {\\n    \"react-grab-mcp\": { \"old\": true }\\n  }\\n}`;\n    fs.writeFileSync(filePath, content);\n\n    upsertIntoJsonc(filePath, content, \"servers\", \"react-grab-mcp\", {\n      command: \"new\",\n    });\n\n    const result = fs.readFileSync(filePath, \"utf8\");\n    expect(result).toContain('\"command\": \"new\"');\n    expect(result).not.toContain('\"old\"');\n  });\n});\n\ndescribe(\"getOpenCodeConfigPath\", () => {\n  let originalXdgConfigHome: string | undefined;\n\n  beforeEach(() => {\n    originalXdgConfigHome = process.env.XDG_CONFIG_HOME;\n    process.env.XDG_CONFIG_HOME = tempDir;\n  });\n\n  afterEach(() => {\n    if (originalXdgConfigHome === undefined) {\n      delete process.env.XDG_CONFIG_HOME;\n    } else {\n      process.env.XDG_CONFIG_HOME = originalXdgConfigHome;\n    }\n  });\n\n  it(\"should prefer opencode.jsonc when both files exist\", () => {\n    const opencodeDir = path.join(tempDir, \"opencode\");\n    fs.mkdirSync(opencodeDir, { recursive: true });\n    fs.writeFileSync(path.join(opencodeDir, \"opencode.json\"), \"{}\");\n    fs.writeFileSync(path.join(opencodeDir, \"opencode.jsonc\"), \"{}\");\n\n    const result = getOpenCodeConfigPath();\n\n    expect(result).toBe(path.join(opencodeDir, \"opencode.jsonc\"));\n  });\n\n  it(\"should use opencode.json when only it exists\", () => {\n    const opencodeDir = path.join(tempDir, \"opencode\");\n    fs.mkdirSync(opencodeDir, { recursive: true });\n    fs.writeFileSync(path.join(opencodeDir, \"opencode.json\"), \"{}\");\n\n    const result = getOpenCodeConfigPath();\n\n    expect(result).toBe(path.join(opencodeDir, \"opencode.json\"));\n  });\n\n  it(\"should default to opencode.jsonc when neither file exists\", () => {\n    const result = getOpenCodeConfigPath();\n\n    expect(result).toBe(path.join(tempDir, \"opencode\", \"opencode.jsonc\"));\n  });\n});\n\ndescribe(\"installTomlClient\", () => {\n  it(\"should create a new TOML file when none exists\", () => {\n    const client = makeTomlClient();\n\n    installTomlClient(client);\n\n    const content = fs.readFileSync(client.configPath, \"utf8\");\n    expect(content).toContain(\"[mcp_servers.react-grab-mcp]\");\n    expect(content).toContain('command = \"npx\"');\n  });\n\n  it(\"should append to an existing TOML file\", () => {\n    const client = makeTomlClient();\n    fs.writeFileSync(\n      client.configPath,\n      '[mcp_servers.other]\\ncommand = \"other\"\\n',\n    );\n\n    installTomlClient(client);\n\n    const content = fs.readFileSync(client.configPath, \"utf8\");\n    expect(content).toContain(\"[mcp_servers.other]\");\n    expect(content).toContain(\"[mcp_servers.react-grab-mcp]\");\n  });\n\n  it(\"should replace an existing react-grab-mcp section\", () => {\n    const client = makeTomlClient();\n    fs.writeFileSync(\n      client.configPath,\n      '[mcp_servers.react-grab-mcp]\\ncommand = \"old\"\\n\\n[other]\\nkey = \"val\"\\n',\n    );\n\n    installTomlClient(client);\n\n    const content = fs.readFileSync(client.configPath, \"utf8\");\n    expect(content).toContain('command = \"npx\"');\n    expect(content).not.toContain('command = \"old\"');\n    expect(content).toContain(\"[other]\");\n  });\n\n  it(\"should create nested directories if needed\", () => {\n    const client = makeTomlClient({\n      configPath: path.join(tempDir, \"deep\", \"config.toml\"),\n    });\n\n    installTomlClient(client);\n\n    expect(fs.existsSync(client.configPath)).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/cli/test/install.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport {\n  getPackagesToInstall,\n  getPackagesToUninstall,\n} from \"../src/utils/install.js\";\n\ndescribe(\"getPackagesToInstall\", () => {\n  it(\"should return only react-grab when no agent and includeReactGrab is true\", () => {\n    const packages = getPackagesToInstall(\"none\", true);\n\n    expect(packages).toEqual([\"react-grab\"]);\n  });\n\n  it(\"should return react-grab and agent package\", () => {\n    const packages = getPackagesToInstall(\"cursor\", true);\n\n    expect(packages).toEqual([\"react-grab\", \"@react-grab/cursor\"]);\n  });\n\n  it(\"should return only agent package when includeReactGrab is false\", () => {\n    const packages = getPackagesToInstall(\"claude-code\", false);\n\n    expect(packages).toEqual([\"@react-grab/claude-code\"]);\n  });\n\n  it(\"should return empty array when no agent and includeReactGrab is false\", () => {\n    const packages = getPackagesToInstall(\"none\", false);\n\n    expect(packages).toEqual([]);\n  });\n\n  it(\"should handle all agent types\", () => {\n    const agents = [\"claude-code\", \"cursor\", \"opencode\"] as const;\n\n    for (const agent of agents) {\n      const packages = getPackagesToInstall(agent, false);\n      expect(packages).toEqual([`@react-grab/${agent}`]);\n    }\n  });\n\n  it(\"should return @react-grab/mcp when agent is mcp\", () => {\n    const packages = getPackagesToInstall(\"mcp\", false);\n\n    expect(packages).toEqual([\"@react-grab/mcp\"]);\n  });\n\n  it(\"should return react-grab and @react-grab/mcp when agent is mcp and includeReactGrab is true\", () => {\n    const packages = getPackagesToInstall(\"mcp\", true);\n\n    expect(packages).toEqual([\"react-grab\", \"@react-grab/mcp\"]);\n  });\n});\n\ndescribe(\"getPackagesToUninstall\", () => {\n  it(\"should return @react-grab/mcp for mcp agent\", () => {\n    const packages = getPackagesToUninstall(\"mcp\");\n\n    expect(packages).toEqual([\"@react-grab/mcp\"]);\n  });\n\n  it(\"should return legacy agent package\", () => {\n    const packages = getPackagesToUninstall(\"cursor\");\n\n    expect(packages).toEqual([\"@react-grab/cursor\"]);\n  });\n});\n"
  },
  {
    "path": "packages/cli/test/templates.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport {\n  NEXT_APP_ROUTER_SCRIPT,\n  NEXT_APP_ROUTER_SCRIPT_WITH_AGENT,\n  VITE_IMPORT,\n  VITE_IMPORT_WITH_AGENT,\n  WEBPACK_IMPORT,\n  WEBPACK_IMPORT_WITH_AGENT,\n} from \"../src/utils/templates.js\";\n\ndescribe(\"Next.js App Router templates\", () => {\n  it(\"should generate basic script without agent\", () => {\n    expect(NEXT_APP_ROUTER_SCRIPT).toContain(\"react-grab\");\n    expect(NEXT_APP_ROUTER_SCRIPT).toContain(\"process.env.NODE_ENV\");\n    expect(NEXT_APP_ROUTER_SCRIPT).toContain(\"development\");\n    expect(NEXT_APP_ROUTER_SCRIPT).toContain(\"beforeInteractive\");\n  });\n\n  it(\"should generate script with agent\", () => {\n    const script = NEXT_APP_ROUTER_SCRIPT_WITH_AGENT(\"cursor\");\n\n    expect(script).toContain(\"react-grab\");\n    expect(script).toContain(\"@react-grab/cursor\");\n    expect(script).toContain(\"lazyOnload\");\n  });\n\n  it(\"should return basic script when agent is none\", () => {\n    const script = NEXT_APP_ROUTER_SCRIPT_WITH_AGENT(\"none\");\n\n    expect(script).toContain(\"react-grab\");\n    expect(script).not.toContain(\"@react-grab/\");\n  });\n\n  it(\"should include all agent types correctly\", () => {\n    const agents = [\"claude-code\", \"cursor\", \"opencode\"] as const;\n\n    for (const agent of agents) {\n      const script = NEXT_APP_ROUTER_SCRIPT_WITH_AGENT(agent);\n      expect(script).toContain(`@react-grab/${agent}`);\n    }\n  });\n});\n\ndescribe(\"Vite templates\", () => {\n  it(\"should generate basic import without agent\", () => {\n    expect(VITE_IMPORT).toContain('import(\"react-grab\")');\n    expect(VITE_IMPORT).toContain(\"import.meta.env.DEV\");\n  });\n\n  it(\"should generate import with agent\", () => {\n    const importBlock = VITE_IMPORT_WITH_AGENT(\"opencode\");\n\n    expect(importBlock).toContain(\"react-grab\");\n    expect(importBlock).toContain(\"@react-grab/opencode\");\n  });\n\n  it(\"should return basic import when agent is none\", () => {\n    const importBlock = VITE_IMPORT_WITH_AGENT(\"none\");\n\n    expect(importBlock).toContain(\"react-grab\");\n    expect(importBlock).not.toContain(\"@react-grab/\");\n  });\n});\n\ndescribe(\"Webpack templates\", () => {\n  it(\"should generate basic import without agent\", () => {\n    expect(WEBPACK_IMPORT).toContain('import(\"react-grab\")');\n    expect(WEBPACK_IMPORT).toContain(\"process.env.NODE_ENV\");\n    expect(WEBPACK_IMPORT).toContain(\"development\");\n  });\n\n  it(\"should generate import with agent\", () => {\n    const importBlock = WEBPACK_IMPORT_WITH_AGENT(\"claude-code\");\n\n    expect(importBlock).toContain(\"react-grab\");\n    expect(importBlock).toContain(\"@react-grab/claude-code\");\n  });\n\n  it(\"should return basic import when agent is none\", () => {\n    const importBlock = WEBPACK_IMPORT_WITH_AGENT(\"none\");\n\n    expect(importBlock).toContain(\"react-grab\");\n    expect(importBlock).not.toContain(\"@react-grab/\");\n  });\n});\n"
  },
  {
    "path": "packages/cli/test/transform.test.ts",
    "content": "import { vi, describe, expect, it, beforeEach } from \"vitest\";\nimport {\n  previewTransform,\n  applyTransform,\n  previewPackageJsonTransform,\n  applyPackageJsonTransform,\n  previewAgentRemoval,\n  previewPackageJsonAgentRemoval,\n} from \"../src/utils/transform.js\";\n\nvi.mock(\"node:fs\", () => ({\n  existsSync: vi.fn(),\n  readFileSync: vi.fn(),\n  writeFileSync: vi.fn(),\n  accessSync: vi.fn(),\n  constants: { W_OK: 2 },\n}));\n\nimport { existsSync, readFileSync, writeFileSync, accessSync } from \"node:fs\";\n\nconst mockExistsSync = vi.mocked(existsSync);\nconst mockReadFileSync = vi.mocked(readFileSync);\nconst mockWriteFileSync = vi.mocked(writeFileSync);\nconst mockAccessSync = vi.mocked(accessSync);\n\nbeforeEach(() => {\n  vi.clearAllMocks();\n});\n\ndescribe(\"adding agent to existing installation with no prior agents\", () => {\n  it(\"should add agent to layout when React Grab exists but no agents are installed\", () => {\n    const layoutWithReactGrabNoAgent = `import Script from \"next/script\";\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <Script src=\"//unpkg.com/react-grab/dist/index.global.js\" strategy=\"beforeInteractive\" />\n      </head>\n      <body>{children}</body>\n    </html>\n  );\n}`;\n\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"layout.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(layoutWithReactGrabNoAgent);\n\n    const result = previewTransform(\n      \"/test\",\n      \"next\",\n      \"app\",\n      \"claude-code\",\n      true,\n    );\n\n    expect(result.success).toBe(true);\n    expect(result.noChanges).toBeFalsy();\n    expect(result.newContent).toContain(\"@react-grab/claude-code\");\n  });\n\n  it(\"should add agent to package.json when installedAgents is empty\", () => {\n    const packageJsonContent = JSON.stringify(\n      {\n        name: \"my-app\",\n        scripts: {\n          dev: \"next dev\",\n        },\n      },\n      null,\n      2,\n    );\n\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(packageJsonContent);\n\n    const result = previewPackageJsonTransform(\n      \"/test\",\n      \"claude-code\",\n      [],\n      \"npm\",\n    );\n\n    expect(result.success).toBe(true);\n    expect(result.noChanges).toBeFalsy();\n    expect(result.newContent).toContain(\n      \"npx @react-grab/claude-code@latest &&\",\n    );\n  });\n});\n\ndescribe(\"previewTransform - Next.js App Router\", () => {\n  const layoutContent = `import type { Metadata } from \"next\";\n\nexport const metadata: Metadata = {\n  title: \"My App\",\n};\n\nexport default function RootLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <html lang=\"en\">\n      <body>{children}</body>\n    </html>\n  );\n}`;\n\n  it(\"should add React Grab to layout.tsx\", () => {\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"layout.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(layoutContent);\n\n    const result = previewTransform(\"/test\", \"next\", \"app\", \"none\", false);\n\n    expect(result.success).toBe(true);\n    expect(result.filePath).toContain(\"layout.tsx\");\n    expect(result.newContent).toContain('import Script from \"next/script\"');\n    expect(result.newContent).toContain(\"react-grab\");\n  });\n\n  it(\"should add React Grab with agent to layout.tsx\", () => {\n    const layoutWithHead = `export default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head></head>\n      <body>{children}</body>\n    </html>\n  );\n}`;\n\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"layout.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(layoutWithHead);\n\n    const result = previewTransform(\"/test\", \"next\", \"app\", \"cursor\", false);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"react-grab\");\n    expect(result.newContent).toContain(\"@react-grab/cursor\");\n  });\n\n  it(\"should not duplicate if React Grab already exists\", () => {\n    const layoutWithReactGrab = `import Script from \"next/script\";\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <Script src=\"//unpkg.com/react-grab/dist/index.global.js\" />\n      </head>\n      <body>{children}</body>\n    </html>\n  );\n}`;\n\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"layout.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(layoutWithReactGrab);\n\n    const result = previewTransform(\"/test\", \"next\", \"app\", \"none\", false);\n\n    expect(result.success).toBe(true);\n    expect(result.noChanges).toBe(true);\n  });\n\n  it(\"should add agent to existing React Grab installation\", () => {\n    const layoutWithReactGrab = `import Script from \"next/script\";\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <Script src=\"//unpkg.com/react-grab/dist/index.global.js\" strategy=\"beforeInteractive\" />\n      </head>\n      <body>{children}</body>\n    </html>\n  );\n}`;\n\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"layout.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(layoutWithReactGrab);\n\n    const result = previewTransform(\"/test\", \"next\", \"app\", \"cursor\", true);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"@react-grab/cursor\");\n  });\n\n  it(\"should add base script without agent client when agent is mcp\", () => {\n    const layoutWithHead = `export default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head></head>\n      <body>{children}</body>\n    </html>\n  );\n}`;\n\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"layout.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(layoutWithHead);\n\n    const result = previewTransform(\"/test\", \"next\", \"app\", \"mcp\", false);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"react-grab\");\n    expect(result.newContent).not.toContain(\"@react-grab/mcp\");\n  });\n\n  it(\"should fail when layout file not found\", () => {\n    mockExistsSync.mockReturnValue(false);\n\n    const result = previewTransform(\"/test\", \"next\", \"app\", \"none\", false);\n\n    expect(result.success).toBe(false);\n    expect(result.message).toContain(\"Could not find\");\n  });\n});\n\ndescribe(\"previewTransform - Vite\", () => {\n  const entryContent = `import React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport App from \"./App\";\n\nReactDOM.createRoot(document.getElementById(\"root\")!).render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>\n);`;\n\n  it(\"should add React Grab to entry file\", () => {\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"main.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(entryContent);\n\n    const result = previewTransform(\"/test\", \"vite\", \"unknown\", \"none\", false);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain('import(\"react-grab\")');\n    expect(result.newContent).toContain(\"import.meta.env.DEV\");\n  });\n\n  it(\"should add React Grab with agent to entry file\", () => {\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"main.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(entryContent);\n\n    const result = previewTransform(\n      \"/test\",\n      \"vite\",\n      \"unknown\",\n      \"opencode\",\n      false,\n    );\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"react-grab\");\n    expect(result.newContent).toContain(\"@react-grab/opencode\");\n  });\n\n  it(\"should add agent to existing React Grab installation\", () => {\n    const entryWithReactGrab = `if (import.meta.env.DEV) {\n  import(\"react-grab\");\n}\n\nimport React from \"react\";\nimport ReactDOM from \"react-dom/client\";`;\n\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"main.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(entryWithReactGrab);\n\n    const result = previewTransform(\n      \"/test\",\n      \"vite\",\n      \"unknown\",\n      \"claude-code\",\n      true,\n    );\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"@react-grab/claude-code\");\n  });\n\n  it(\"should add base script without agent client when agent is mcp\", () => {\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"main.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(entryContent);\n\n    const result = previewTransform(\"/test\", \"vite\", \"unknown\", \"mcp\", false);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"react-grab\");\n    expect(result.newContent).not.toContain(\"@react-grab/mcp\");\n  });\n});\n\ndescribe(\"previewTransform - Webpack\", () => {\n  const entryContent = `import React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport App from \"./App\";\n\nReactDOM.createRoot(document.getElementById(\"root\")!).render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>\n);`;\n\n  it(\"should add React Grab to entry file\", () => {\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"index.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(entryContent);\n\n    const result = previewTransform(\n      \"/test\",\n      \"webpack\",\n      \"unknown\",\n      \"none\",\n      false,\n    );\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain('import(\"react-grab\")');\n    expect(result.newContent).toContain(\"process.env.NODE_ENV\");\n  });\n\n  it(\"should add React Grab with agent to entry file\", () => {\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"main.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(entryContent);\n\n    const result = previewTransform(\n      \"/test\",\n      \"webpack\",\n      \"unknown\",\n      \"cursor\",\n      false,\n    );\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"react-grab\");\n    expect(result.newContent).toContain(\"@react-grab/cursor\");\n  });\n});\n\ndescribe(\"previewTransform - Next.js Pages Router\", () => {\n  it(\"should fail with helpful message when _document.tsx not found\", () => {\n    mockExistsSync.mockReturnValue(false);\n\n    const result = previewTransform(\"/test\", \"next\", \"pages\", \"none\", false);\n\n    expect(result.success).toBe(false);\n    expect(result.message).toContain(\"Could not find pages/_document.tsx\");\n    expect(result.message).toContain(\"import { Html, Head, Main, NextScript }\");\n    expect(result.message).toContain(\"export default function Document()\");\n  });\n\n  it(\"should add React Grab to existing _document.tsx\", () => {\n    const documentContent = `import { Html, Head, Main, NextScript } from \"next/document\";\n\nexport default function Document() {\n  return (\n    <Html>\n      <Head></Head>\n      <body>\n        <Main />\n        <NextScript />\n      </body>\n    </Html>\n  );\n}`;\n\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"_document.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(documentContent);\n\n    const result = previewTransform(\"/test\", \"next\", \"pages\", \"none\", false);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"react-grab\");\n    expect(result.newContent).toContain('import Script from \"next/script\"');\n  });\n\n  it(\"should add agent to existing Pages Router installation\", () => {\n    const documentWithReactGrab = `import { Html, Head, Main, NextScript } from \"next/document\";\nimport Script from \"next/script\";\n\nexport default function Document() {\n  return (\n    <Html>\n      <Head>\n        <Script src=\"//unpkg.com/react-grab/dist/index.global.js\" strategy=\"beforeInteractive\" />\n      </Head>\n      <body>\n        <Main />\n        <NextScript />\n      </body>\n    </Html>\n  );\n}`;\n\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"_document.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(documentWithReactGrab);\n\n    const result = previewTransform(\"/test\", \"next\", \"pages\", \"cursor\", true);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"@react-grab/cursor\");\n  });\n});\n\ndescribe(\"previewTransform - Vite edge cases\", () => {\n  it(\"should fail when entry file not found\", () => {\n    mockExistsSync.mockReturnValue(false);\n\n    const result = previewTransform(\"/test\", \"vite\", \"unknown\", \"none\", false);\n\n    expect(result.success).toBe(false);\n    expect(result.message).toContain(\"Could not find entry file\");\n  });\n\n  it(\"should add agent to existing Vite installation\", () => {\n    const entryWithReactGrab = `if (import.meta.env.DEV) {\n  import(\"react-grab\");\n}\n\nimport React from \"react\";\nimport ReactDOM from \"react-dom/client\";`;\n\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"main.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(entryWithReactGrab);\n\n    const result = previewTransform(\n      \"/test\",\n      \"vite\",\n      \"unknown\",\n      \"opencode\",\n      true,\n    );\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"@react-grab/opencode\");\n  });\n\n  it(\"should detect existing React Grab in index.html as already installed\", () => {\n    const indexWithReactGrab = `<!doctype html>\n<html lang=\"en\">\n  <head>\n    <script type=\"module\">\n      if (import.meta.env.DEV) {\n        import(\"react-grab\");\n      }\n    </script>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n  </body>\n</html>`;\n\n    mockExistsSync.mockImplementation((path) => {\n      const pathStr = String(path);\n      return pathStr.endsWith(\"index.html\") || pathStr.endsWith(\"main.tsx\");\n    });\n    mockReadFileSync.mockImplementation((path) => {\n      if (String(path).endsWith(\"index.html\")) return indexWithReactGrab;\n      return `import React from \"react\";`;\n    });\n\n    const result = previewTransform(\"/test\", \"vite\", \"unknown\", \"none\", false);\n\n    expect(result.success).toBe(true);\n    expect(result.noChanges).toBe(true);\n  });\n\n  it(\"should add agent to existing React Grab in index.html\", () => {\n    const indexWithReactGrab = `<!doctype html>\n<html lang=\"en\">\n  <head>\n    <script type=\"module\">\n      if (import.meta.env.DEV) {\n        import(\"react-grab\");\n      }\n    </script>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n  </body>\n</html>`;\n\n    mockExistsSync.mockImplementation((path) => {\n      const pathStr = String(path);\n      return pathStr.endsWith(\"index.html\") || pathStr.endsWith(\"main.tsx\");\n    });\n    mockReadFileSync.mockImplementation((path) => {\n      if (String(path).endsWith(\"index.html\")) return indexWithReactGrab;\n      return `import React from \"react\";`;\n    });\n\n    const result = previewTransform(\"/test\", \"vite\", \"unknown\", \"cursor\", true);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"@react-grab/cursor\");\n    expect(result.filePath).toContain(\"index.html\");\n  });\n});\n\ndescribe(\"previewTransform - Webpack edge cases\", () => {\n  it(\"should fail when entry file not found\", () => {\n    mockExistsSync.mockReturnValue(false);\n\n    const result = previewTransform(\n      \"/test\",\n      \"webpack\",\n      \"unknown\",\n      \"none\",\n      false,\n    );\n\n    expect(result.success).toBe(false);\n    expect(result.message).toContain(\"Could not find entry file\");\n  });\n\n  it(\"should add agent to existing Webpack installation\", () => {\n    const entryWithReactGrab = `if (process.env.NODE_ENV === \"development\") {\n  import(\"react-grab\");\n}\n\nimport React from \"react\";\nimport ReactDOM from \"react-dom/client\";`;\n\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"index.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(entryWithReactGrab);\n\n    const result = previewTransform(\n      \"/test\",\n      \"webpack\",\n      \"unknown\",\n      \"opencode\",\n      true,\n    );\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"@react-grab/opencode\");\n  });\n});\n\ndescribe(\"previewTransform - Unknown framework\", () => {\n  it(\"should fail for unknown framework\", () => {\n    const result = previewTransform(\n      \"/test\",\n      \"unknown\",\n      \"unknown\",\n      \"none\",\n      false,\n    );\n\n    expect(result.success).toBe(false);\n    expect(result.message).toContain(\"Unknown framework\");\n  });\n});\n\ndescribe(\"applyTransform\", () => {\n  it(\"should write file when result has newContent and file is writable\", () => {\n    mockAccessSync.mockImplementation(() => undefined);\n\n    const result = {\n      success: true,\n      filePath: \"/test/file.tsx\",\n      message: \"Test\",\n      originalContent: \"old\",\n      newContent: \"new\",\n    };\n\n    const writeResult = applyTransform(result);\n\n    expect(writeResult.success).toBe(true);\n    expect(mockWriteFileSync).toHaveBeenCalledWith(\"/test/file.tsx\", \"new\");\n  });\n\n  it(\"should return error when file is not writable\", () => {\n    mockAccessSync.mockImplementation(() => {\n      throw new Error(\"EACCES\");\n    });\n\n    const result = {\n      success: true,\n      filePath: \"/test/file.tsx\",\n      message: \"Test\",\n      originalContent: \"old\",\n      newContent: \"new\",\n    };\n\n    const writeResult = applyTransform(result);\n\n    expect(writeResult.success).toBe(false);\n    expect(writeResult.error).toContain(\"Cannot write to\");\n    expect(mockWriteFileSync).not.toHaveBeenCalled();\n  });\n\n  it(\"should not write file when result has no newContent\", () => {\n    const result = {\n      success: true,\n      filePath: \"/test/file.tsx\",\n      message: \"Test\",\n      noChanges: true,\n    };\n\n    const writeResult = applyTransform(result);\n\n    expect(writeResult.success).toBe(true);\n    expect(mockWriteFileSync).not.toHaveBeenCalled();\n  });\n\n  it(\"should not write file when result is not successful\", () => {\n    const result = {\n      success: false,\n      filePath: \"\",\n      message: \"Error\",\n    };\n\n    const writeResult = applyTransform(result);\n\n    expect(writeResult.success).toBe(true);\n    expect(mockWriteFileSync).not.toHaveBeenCalled();\n  });\n\n  it(\"should return error when writeFileSync throws\", () => {\n    mockAccessSync.mockImplementation(() => undefined);\n    mockWriteFileSync.mockImplementation(() => {\n      throw new Error(\"Disk full\");\n    });\n\n    const result = {\n      success: true,\n      filePath: \"/test/file.tsx\",\n      message: \"Test\",\n      originalContent: \"old\",\n      newContent: \"new\",\n    };\n\n    const writeResult = applyTransform(result);\n\n    expect(writeResult.success).toBe(false);\n    expect(writeResult.error).toContain(\"Failed to write to\");\n    expect(writeResult.error).toContain(\"Disk full\");\n  });\n\n  it(\"should not write when filePath is empty\", () => {\n    const result = {\n      success: true,\n      filePath: \"\",\n      message: \"Test\",\n      originalContent: \"old\",\n      newContent: \"new\",\n    };\n\n    const writeResult = applyTransform(result);\n\n    expect(writeResult.success).toBe(true);\n    expect(mockWriteFileSync).not.toHaveBeenCalled();\n  });\n});\n\ndescribe(\"previewPackageJsonTransform\", () => {\n  const packageJsonContent = JSON.stringify(\n    {\n      name: \"my-app\",\n      scripts: {\n        dev: \"next dev --turbopack\",\n        build: \"next build\",\n      },\n    },\n    null,\n    2,\n  );\n\n  it(\"should add agent prefix to dev script\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(packageJsonContent);\n\n    const result = previewPackageJsonTransform(\"/test\", \"cursor\", []);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"npx @react-grab/cursor@latest &&\");\n    expect(result.newContent).toContain(\"next dev --turbopack\");\n  });\n\n  it(\"should add claude-code prefix to dev script\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(packageJsonContent);\n\n    const result = previewPackageJsonTransform(\"/test\", \"claude-code\", []);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\n      \"npx @react-grab/claude-code@latest &&\",\n    );\n  });\n\n  it(\"should add opencode prefix to dev script\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(packageJsonContent);\n\n    const result = previewPackageJsonTransform(\"/test\", \"opencode\", []);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"npx @react-grab/opencode@latest &&\");\n  });\n\n  it(\"should skip when agent is none\", () => {\n    const result = previewPackageJsonTransform(\"/test\", \"none\", []);\n\n    expect(result.success).toBe(true);\n    expect(result.noChanges).toBe(true);\n  });\n\n  it(\"should skip package.json when agent is mcp\", () => {\n    const result = previewPackageJsonTransform(\"/test\", \"mcp\", []);\n\n    expect(result.success).toBe(true);\n    expect(result.noChanges).toBe(true);\n    expect(result.message).toContain(\"MCP\");\n  });\n\n  it(\"should not duplicate if agent is already configured\", () => {\n    const packageJsonWithAgent = JSON.stringify(\n      {\n        name: \"my-app\",\n        scripts: {\n          dev: \"npx @react-grab/cursor@latest && next dev --turbopack\",\n        },\n      },\n      null,\n      2,\n    );\n\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(packageJsonWithAgent);\n\n    const result = previewPackageJsonTransform(\"/test\", \"cursor\", []);\n\n    expect(result.success).toBe(true);\n    expect(result.noChanges).toBe(true);\n  });\n\n  it(\"should not add another agent if one is already installed\", () => {\n    const packageJsonWithAgent = JSON.stringify(\n      {\n        name: \"my-app\",\n        scripts: {\n          dev: \"npx @react-grab/claude-code@latest && next dev\",\n        },\n      },\n      null,\n      2,\n    );\n\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(packageJsonWithAgent);\n\n    const result = previewPackageJsonTransform(\"/test\", \"cursor\", [\n      \"claude-code\",\n    ]);\n\n    expect(result.success).toBe(true);\n    expect(result.noChanges).toBe(true);\n  });\n\n  it(\"should fail when package.json not found\", () => {\n    mockExistsSync.mockReturnValue(false);\n\n    const result = previewPackageJsonTransform(\"/test\", \"cursor\", []);\n\n    expect(result.success).toBe(false);\n    expect(result.message).toContain(\"Could not find package.json\");\n  });\n\n  it(\"should return warning when no dev script exists\", () => {\n    const packageJsonNoDev = JSON.stringify(\n      {\n        name: \"my-app\",\n        scripts: {\n          build: \"next build\",\n        },\n      },\n      null,\n      2,\n    );\n\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(packageJsonNoDev);\n\n    const result = previewPackageJsonTransform(\"/test\", \"cursor\", []);\n\n    expect(result.success).toBe(true);\n    expect(result.noChanges).toBe(true);\n    expect(result.warning).toContain(\n      \"Could not inject agent into package.json\",\n    );\n    expect(result.warning).toContain(\"npx @react-grab/cursor@latest\");\n  });\n\n  it(\"should use dev* script when no exact dev script exists\", () => {\n    const packageJsonDevVariant = JSON.stringify(\n      {\n        name: \"my-app\",\n        scripts: {\n          \"dev:server\": \"next dev\",\n          build: \"next build\",\n        },\n      },\n      null,\n      2,\n    );\n\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(packageJsonDevVariant);\n\n    const result = previewPackageJsonTransform(\"/test\", \"cursor\", []);\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"npx @react-grab/cursor@latest &&\");\n    expect(result.newContent).toContain('\"dev:server\"');\n    expect(result.message).toContain(\"dev:server\");\n  });\n\n  it(\"should fail when package.json is invalid JSON\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(\"{ invalid json }\");\n\n    const result = previewPackageJsonTransform(\"/test\", \"cursor\", []);\n\n    expect(result.success).toBe(false);\n    expect(result.message).toContain(\"Failed to parse package.json\");\n  });\n\n  it(\"should use bunx for bun package manager\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(packageJsonContent);\n\n    const result = previewPackageJsonTransform(\"/test\", \"cursor\", [], \"bun\");\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"bunx @react-grab/cursor@latest &&\");\n    expect(result.newContent).toContain(\"next dev --turbopack\");\n  });\n\n  it(\"should use pnpm dlx for pnpm package manager\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(packageJsonContent);\n\n    const result = previewPackageJsonTransform(\"/test\", \"cursor\", [], \"pnpm\");\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\n      \"pnpm dlx @react-grab/cursor@latest &&\",\n    );\n    expect(result.newContent).toContain(\"next dev --turbopack\");\n  });\n\n  it(\"should use npx for npm package manager\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(packageJsonContent);\n\n    const result = previewPackageJsonTransform(\"/test\", \"cursor\", [], \"npm\");\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"npx @react-grab/cursor@latest &&\");\n    expect(result.newContent).toContain(\"next dev --turbopack\");\n  });\n\n  it(\"should use npx for yarn package manager\", () => {\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(packageJsonContent);\n\n    const result = previewPackageJsonTransform(\"/test\", \"cursor\", [], \"yarn\");\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).toContain(\"npx @react-grab/cursor@latest &&\");\n    expect(result.newContent).toContain(\"next dev --turbopack\");\n  });\n\n  it(\"should detect existing bunx prefix and not duplicate\", () => {\n    const packageJsonWithBunx = JSON.stringify(\n      {\n        name: \"my-app\",\n        scripts: {\n          dev: \"bunx @react-grab/cursor@latest && next dev\",\n        },\n      },\n      null,\n      2,\n    );\n\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(packageJsonWithBunx);\n\n    const result = previewPackageJsonTransform(\"/test\", \"cursor\", [], \"npm\");\n\n    expect(result.success).toBe(true);\n    expect(result.noChanges).toBe(true);\n  });\n\n  it(\"should detect existing pnpm dlx prefix and not duplicate\", () => {\n    const packageJsonWithPnpm = JSON.stringify(\n      {\n        name: \"my-app\",\n        scripts: {\n          dev: \"pnpm dlx @react-grab/cursor@latest && next dev\",\n        },\n      },\n      null,\n      2,\n    );\n\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(packageJsonWithPnpm);\n\n    const result = previewPackageJsonTransform(\"/test\", \"cursor\", [], \"bun\");\n\n    expect(result.success).toBe(true);\n    expect(result.noChanges).toBe(true);\n  });\n\n  it(\"should detect existing yarn dlx prefix and not duplicate\", () => {\n    const packageJsonWithYarnDlx = JSON.stringify(\n      {\n        name: \"my-app\",\n        scripts: {\n          dev: \"yarn dlx @react-grab/cursor@latest && next dev\",\n        },\n      },\n      null,\n      2,\n    );\n\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(packageJsonWithYarnDlx);\n\n    const result = previewPackageJsonTransform(\"/test\", \"cursor\", [], \"npm\");\n\n    expect(result.success).toBe(true);\n    expect(result.noChanges).toBe(true);\n  });\n\n  it(\"should show correct package manager command in warning when no dev script\", () => {\n    const packageJsonNoDev = JSON.stringify(\n      {\n        name: \"my-app\",\n        scripts: {\n          build: \"next build\",\n        },\n      },\n      null,\n      2,\n    );\n\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(packageJsonNoDev);\n\n    const bunResult = previewPackageJsonTransform(\"/test\", \"cursor\", [], \"bun\");\n    expect(bunResult.warning).toContain(\"bunx @react-grab/cursor@latest\");\n\n    const pnpmResult = previewPackageJsonTransform(\n      \"/test\",\n      \"cursor\",\n      [],\n      \"pnpm\",\n    );\n    expect(pnpmResult.warning).toContain(\"pnpm dlx @react-grab/cursor@latest\");\n  });\n});\n\ndescribe(\"previewAgentRemoval\", () => {\n  it(\"should remove MCP script from Next.js layout\", () => {\n    const layoutWithMcp = `import Script from \"next/script\";\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <Script src=\"//unpkg.com/react-grab/dist/index.global.js\" strategy=\"beforeInteractive\" />\n        {process.env.NODE_ENV === \"development\" && (\n          <Script src=\"//unpkg.com/@react-grab/mcp/dist/client.global.js\" strategy=\"lazyOnload\" />\n        )}\n      </head>\n      <body>{children}</body>\n    </html>\n  );\n}`;\n\n    mockExistsSync.mockImplementation((path) =>\n      String(path).endsWith(\"layout.tsx\"),\n    );\n    mockReadFileSync.mockReturnValue(layoutWithMcp);\n\n    const result = previewAgentRemoval(\"/test\", \"next\", \"app\", \"mcp\");\n\n    expect(result.success).toBe(true);\n    expect(result.newContent).not.toContain(\"@react-grab/mcp\");\n    expect(result.newContent).toContain(\"react-grab\");\n  });\n});\n\ndescribe(\"previewPackageJsonAgentRemoval\", () => {\n  it(\"should return noChanges for mcp since it has no dev script prefix\", () => {\n    const packageJsonContent = JSON.stringify(\n      { name: \"my-app\", scripts: { dev: \"next dev\" } },\n      null,\n      2,\n    );\n\n    mockExistsSync.mockReturnValue(true);\n    mockReadFileSync.mockReturnValue(packageJsonContent);\n\n    const result = previewPackageJsonAgentRemoval(\"/test\", \"mcp\");\n\n    expect(result.success).toBe(true);\n    expect(result.noChanges).toBe(true);\n  });\n});\n\ndescribe(\"applyPackageJsonTransform\", () => {\n  it(\"should write file when result has newContent and file is writable\", () => {\n    vi.clearAllMocks();\n    mockAccessSync.mockReturnValue(undefined);\n    mockWriteFileSync.mockReturnValue(undefined);\n\n    const result = {\n      success: true,\n      filePath: \"/test/package.json\",\n      message: \"Test\",\n      originalContent: \"old\",\n      newContent: \"new\",\n    };\n\n    const writeResult = applyPackageJsonTransform(result);\n\n    expect(writeResult.success).toBe(true);\n    expect(mockWriteFileSync).toHaveBeenCalledWith(\"/test/package.json\", \"new\");\n  });\n\n  it(\"should return error when file is not writable\", () => {\n    vi.clearAllMocks();\n    mockAccessSync.mockImplementation(() => {\n      throw new Error(\"EACCES\");\n    });\n\n    const result = {\n      success: true,\n      filePath: \"/test/package.json\",\n      message: \"Test\",\n      originalContent: \"old\",\n      newContent: \"new\",\n    };\n\n    const writeResult = applyPackageJsonTransform(result);\n\n    expect(writeResult.success).toBe(false);\n    expect(writeResult.error).toContain(\"Cannot write to\");\n  });\n\n  it(\"should not write file when result has noChanges\", () => {\n    vi.clearAllMocks();\n\n    const result = {\n      success: true,\n      filePath: \"/test/package.json\",\n      message: \"Test\",\n      noChanges: true,\n    };\n\n    const writeResult = applyPackageJsonTransform(result);\n\n    expect(writeResult.success).toBe(true);\n    expect(mockWriteFileSync).not.toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "packages/cli/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"noEmit\": true\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/cli/tsup.config.ts",
    "content": "import fs from \"node:fs\";\nimport { defineConfig } from \"tsup\";\n\nconst packageJson = JSON.parse(fs.readFileSync(\"package.json\", \"utf8\")) as {\n  version: string;\n};\n\nexport default defineConfig([\n  {\n    entry: {\n      cli: \"./src/cli.ts\",\n    },\n    format: [\"cjs\", \"esm\"],\n    dts: true,\n    clean: true,\n    splitting: false,\n    sourcemap: false,\n    target: \"node18\",\n    platform: \"node\",\n    treeshake: true,\n    noExternal: [\"zod\"],\n    env: {\n      VERSION: process.env.VERSION ?? packageJson.version,\n    },\n    banner: {\n      js: \"#!/usr/bin/env node\",\n    },\n  },\n]);\n"
  },
  {
    "path": "packages/cli/vitest.config.ts",
    "content": "import { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n  test: {\n    globals: true,\n    environment: \"node\",\n    include: [\"test/**/*.test.ts\"],\n    testTimeout: 10000,\n  },\n});\n"
  },
  {
    "path": "packages/design-system/package.json",
    "content": "{\n  \"name\": \"@react-grab/design-system\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"main\": \"dist/index.js\",\n  \"module\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"import\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"require\": {\n        \"types\": \"./dist/index.d.cts\",\n        \"default\": \"./dist/index.cjs\"\n      }\n    }\n  },\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"react-grab\": \"workspace:*\",\n    \"solid-js\": \"^1.9.10\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.28.5\",\n    \"@babel/preset-typescript\": \"^7.28.5\",\n    \"babel-preset-solid\": \"^1.9.10\",\n    \"esbuild-plugin-babel\": \"^0.2.3\",\n    \"tsup\": \"^8.2.4\"\n  }\n}\n"
  },
  {
    "path": "packages/design-system/src/index.tsx",
    "content": "// @ts-expect-error - CSS imported as text via tsup loader\nimport cssText from \"react-grab/dist/styles.css\";\nimport { render } from \"solid-js/web\";\nimport { createSignal, For, onCleanup, onMount, Show } from \"solid-js\";\nimport { SelectionLabel } from \"react-grab/src/components/selection-label/index.js\";\nimport { ContextMenu } from \"react-grab/src/components/context-menu.js\";\nimport { ToolbarContent } from \"react-grab/src/components/toolbar/toolbar-content.js\";\nimport { HistoryDropdown } from \"react-grab/src/components/history-dropdown.js\";\nimport type {\n  OverlayBounds,\n  SelectionLabelStatus,\n  HistoryItem,\n} from \"react-grab/src/types.js\";\n\ntype ComponentType = \"label\" | \"context-menu\" | \"toolbar\" | \"history-dropdown\";\n\ninterface DesignSystemStateProps {\n  tagName?: string;\n  componentName?: string;\n  elementsCount?: number;\n  status?: SelectionLabelStatus;\n  hasAgent?: boolean;\n\n  isPromptMode?: boolean;\n  inputValue?: string;\n  replyToPrompt?: string;\n  statusText?: string;\n  isPendingDismiss?: boolean;\n  isPendingAbort?: boolean;\n  error?: string;\n  isContextMenuOpen?: boolean;\n  supportsUndo?: boolean;\n  supportsFollowUp?: boolean;\n  filePath?: string;\n  hasFilePath?: boolean;\n  showMoreOptions?: boolean;\n  dismissButtonText?: string;\n  previousPrompt?: string;\n  hasOnDismiss?: boolean;\n  hasOnUndo?: boolean;\n  hasOnRetry?: boolean;\n  hasOnAcknowledge?: boolean;\n  isToolbarActive?: boolean;\n  isToolbarEnabled?: boolean;\n  isToolbarCollapsed?: boolean;\n  toolbarSnapEdge?: \"top\" | \"bottom\" | \"left\" | \"right\";\n  toolbarHistoryItemCount?: number;\n  toolbarHasUnreadHistoryItems?: boolean;\n  historyItems?: HistoryItem[];\n}\n\ninterface AnimationFrame {\n  props: DesignSystemStateProps;\n  durationMs: number;\n}\n\ninterface DesignSystemState {\n  id: string;\n  label: string;\n  description: string;\n  component: ComponentType;\n  props: DesignSystemStateProps;\n  animationSequence?: AnimationFrame[];\n}\n\nconst DESIGN_SYSTEM_STATES: DesignSystemState[] = [\n  // ══════════════════════════════════════════════════════════════════════════\n  // SELECTION LABEL STATES\n  // ══════════════════════════════════════════════════════════════════════════\n\n  // === IDLE STATES ===\n  {\n    id: \"idle-default\",\n    label: \"Idle (Default)\",\n    description: 'TagBadge + \"Click to Copy\"',\n    component: \"label\",\n    props: {\n      tagName: \"button\",\n      componentName: \"Button\",\n      status: \"idle\",\n      hasAgent: false,\n    },\n  },\n  {\n    id: \"idle-with-filepath\",\n    label: \"Idle (With File Path)\",\n    description: \"Clickable tag badge with open icon\",\n    component: \"label\",\n    props: {\n      tagName: \"div\",\n      componentName: \"Header\",\n      status: \"idle\",\n      hasAgent: false,\n      filePath: \"src/components/Header.tsx\",\n    },\n  },\n  {\n    id: \"idle-context-menu-open\",\n    label: \"Idle (Context Menu Open)\",\n    description: \"Open indicator icon visible\",\n    component: \"label\",\n    props: {\n      tagName: \"main\",\n      componentName: \"Main\",\n      status: \"idle\",\n      hasAgent: false,\n      isContextMenuOpen: true,\n      filePath: \"src/components/Main.tsx\",\n    },\n  },\n  {\n    id: \"idle-multi-element\",\n    label: \"Idle (Multi-Element)\",\n    description: 'Shows \"3 elements\" instead of tag',\n    component: \"label\",\n    props: {\n      tagName: \"div\",\n      elementsCount: 3,\n      status: \"idle\",\n      hasAgent: false,\n    },\n  },\n  {\n    id: \"idle-tag-only\",\n    label: \"Idle (Tag Only)\",\n    description: \"HTML tag without component name\",\n    component: \"label\",\n    props: {\n      tagName: \"section\",\n      status: \"idle\",\n      hasAgent: false,\n    },\n  },\n  {\n    id: \"idle-long-component-name\",\n    label: \"Idle (Long Component)\",\n    description: \"Very long component name truncation\",\n    component: \"label\",\n    props: {\n      tagName: \"div\",\n      componentName: \"SuperLongComponentNameThatShouldDefinitelyTruncate\",\n      status: \"idle\",\n      hasAgent: false,\n    },\n  },\n  {\n    id: \"idle-long-tag-name\",\n    label: \"Idle (Long Tag)\",\n    description: \"Long custom element tag name\",\n    component: \"label\",\n    props: {\n      tagName: \"my-super-long-custom-web-component-element\",\n      status: \"idle\",\n      hasAgent: false,\n    },\n  },\n  {\n    id: \"idle-long-filepath\",\n    label: \"Idle (Long File Path)\",\n    description: \"Deeply nested file path truncation\",\n    component: \"label\",\n    props: {\n      tagName: \"div\",\n      componentName: \"Button\",\n      status: \"idle\",\n      hasAgent: false,\n      filePath:\n        \"src/components/ui/forms/inputs/buttons/primary/PrimaryButton.tsx\",\n    },\n  },\n  {\n    id: \"idle-large-element-count\",\n    label: \"Idle (Large Count)\",\n    description: \"Many elements selected (99)\",\n    component: \"label\",\n    props: {\n      tagName: \"div\",\n      elementsCount: 99,\n      status: \"idle\",\n      hasAgent: false,\n    },\n  },\n  {\n    id: \"idle-long-both\",\n    label: \"Idle (Long Tag + Component)\",\n    description: \"Both tag and component name long\",\n    component: \"label\",\n    props: {\n      tagName: \"custom-interactive-element\",\n      componentName: \"InteractiveCustomElementWrapper\",\n      status: \"idle\",\n      hasAgent: false,\n      filePath: \"src/wrappers/InteractiveCustomElementWrapper.tsx\",\n    },\n  },\n  {\n    id: \"idle-agent-not-connected\",\n    label: \"Idle (Agent Not Connected)\",\n    description: \"Agent available but not connected\",\n    component: \"label\",\n    props: {\n      tagName: \"div\",\n      componentName: \"Panel\",\n      status: \"idle\",\n      hasAgent: true,\n    },\n  },\n\n  // === PROMPT MODE STATES ===\n  {\n    id: \"prompt-empty\",\n    label: \"Prompt (Empty)\",\n    description: \"Input field ready for typing\",\n    component: \"label\",\n    props: {\n      tagName: \"div\",\n      componentName: \"Card\",\n      status: \"idle\",\n      hasAgent: true,\n      isPromptMode: true,\n      inputValue: \"\",\n    },\n  },\n  {\n    id: \"prompt-with-text\",\n    label: \"Prompt (With Text)\",\n    description: \"Input field with user text\",\n    component: \"label\",\n    props: {\n      tagName: \"form\",\n      componentName: \"Form\",\n      status: \"idle\",\n      hasAgent: true,\n      isPromptMode: true,\n      inputValue: \"make the button larger\",\n    },\n  },\n  {\n    id: \"prompt-with-reply\",\n    label: \"Prompt (Reply Mode)\",\n    description: 'Shows \"previously:\" quote above input',\n    component: \"label\",\n    props: {\n      tagName: \"span\",\n      componentName: \"Text\",\n      status: \"idle\",\n      hasAgent: true,\n      isPromptMode: true,\n      inputValue: \"now make it blue\",\n      replyToPrompt: \"make the button larger\",\n    },\n  },\n  {\n    id: \"prompt-multiline\",\n    label: \"Prompt (Multi-line)\",\n    description: \"Long text that wraps to multiple lines\",\n    component: \"label\",\n    props: {\n      tagName: \"div\",\n      componentName: \"Container\",\n      status: \"idle\",\n      hasAgent: true,\n      isPromptMode: true,\n      inputValue:\n        \"make the button bigger and change the background color to a nice gradient from blue to purple\",\n    },\n  },\n  {\n    id: \"prompt-long-reply\",\n    label: \"Prompt (Long Previous)\",\n    description: \"Long previously: text that truncates\",\n    component: \"label\",\n    props: {\n      tagName: \"button\",\n      componentName: \"Submit\",\n      status: \"idle\",\n      hasAgent: true,\n      isPromptMode: true,\n      inputValue: \"also add rounded corners\",\n      replyToPrompt:\n        \"make the button larger and add a hover effect with a nice shadow underneath it\",\n    },\n  },\n  {\n    id: \"pending-dismiss\",\n    label: \"Pending Dismiss\",\n    description: '\"Discard?\" confirmation dialog',\n    component: \"label\",\n    props: {\n      tagName: \"header\",\n      componentName: \"Header\",\n      status: \"idle\",\n      hasAgent: true,\n      isPromptMode: true,\n      isPendingDismiss: true,\n    },\n  },\n\n  // === COPYING STATES ===\n  {\n    id: \"copying-simple\",\n    label: \"Copying (Simple)\",\n    description: '\"Grabbing...\" with pulse animation',\n    component: \"label\",\n    props: {\n      tagName: \"input\",\n      componentName: \"TextField\",\n      status: \"copying\",\n      hasAgent: false,\n      statusText: \"Grabbing…\",\n    },\n  },\n  {\n    id: \"copying-with-prompt\",\n    label: \"Copying (With Prompt)\",\n    description: \"Disabled input + stop button\",\n    component: \"label\",\n    props: {\n      tagName: \"section\",\n      componentName: \"Section\",\n      status: \"copying\",\n      hasAgent: true,\n      inputValue: \"add form validation\",\n      statusText: \"Thinking…\",\n    },\n  },\n  {\n    id: \"pending-abort\",\n    label: \"Pending Abort\",\n    description: '\"Discard?\" during copy operation',\n    component: \"label\",\n    props: {\n      tagName: \"article\",\n      componentName: \"Article\",\n      status: \"copying\",\n      hasAgent: true,\n      isPendingAbort: true,\n    },\n  },\n  {\n    id: \"copying-applying\",\n    label: \"Copying (Applying)\",\n    description: '\"Applying changes…\" status variant',\n    component: \"label\",\n    props: {\n      tagName: \"form\",\n      componentName: \"LoginForm\",\n      status: \"copying\",\n      hasAgent: true,\n      inputValue: \"add validation\",\n      statusText: \"Applying changes…\",\n    },\n  },\n  {\n    id: \"copying-analyzing\",\n    label: \"Copying (Analyzing)\",\n    description: '\"Analyzing…\" status variant',\n    component: \"label\",\n    props: {\n      tagName: \"table\",\n      componentName: \"DataTable\",\n      status: \"copying\",\n      hasAgent: true,\n      inputValue: \"make columns sortable\",\n      statusText: \"Analyzing…\",\n    },\n  },\n  {\n    id: \"copying-long-input\",\n    label: \"Copying (Long Input)\",\n    description: \"Long prompt during copying\",\n    component: \"label\",\n    props: {\n      tagName: \"div\",\n      componentName: \"Modal\",\n      status: \"copying\",\n      hasAgent: true,\n      inputValue:\n        \"add a close button in the top right corner with an X icon and make it dismiss the modal when clicked\",\n      statusText: \"Thinking…\",\n    },\n  },\n  {\n    id: \"copying-long-component\",\n    label: \"Copying (Long Component)\",\n    description: \"Long component name while copying\",\n    component: \"label\",\n    props: {\n      tagName: \"div\",\n      componentName: \"InteractiveDataVisualizationChart\",\n      status: \"copying\",\n      hasAgent: true,\n      inputValue: \"add tooltips\",\n      statusText: \"Applying…\",\n    },\n  },\n\n  // === COMPLETION STATES ===\n  {\n    id: \"copied-simple\",\n    label: \"Copied (Simple)\",\n    description: 'Checkmark + \"Copied\" text only',\n    component: \"label\",\n    props: {\n      tagName: \"nav\",\n      componentName: \"Navigation\",\n      status: \"copied\",\n      hasAgent: false,\n      hasOnDismiss: false,\n      hasOnUndo: false,\n    },\n  },\n  {\n    id: \"copied-with-actions\",\n    label: \"Copied (With Actions)\",\n    description: \"Undo + Keep buttons\",\n    component: \"label\",\n    props: {\n      tagName: \"footer\",\n      componentName: \"Footer\",\n      status: \"copied\",\n      hasAgent: true,\n      statusText: \"Applied changes\",\n      supportsUndo: true,\n    },\n  },\n  {\n    id: \"copied-with-followup\",\n    label: \"Copied (With Follow-up)\",\n    description: \"Follow-up input field below\",\n    component: \"label\",\n    props: {\n      tagName: \"aside\",\n      componentName: \"Sidebar\",\n      status: \"copied\",\n      hasAgent: true,\n      statusText: \"Done\",\n      supportsUndo: true,\n      supportsFollowUp: true,\n    },\n  },\n  {\n    id: \"copied-no-dismiss\",\n    label: \"Copied (No Dismiss)\",\n    description: \"Checkmark + status only, no Keep button\",\n    component: \"label\",\n    props: {\n      tagName: \"span\",\n      componentName: \"Badge\",\n      status: \"copied\",\n      hasAgent: true,\n      statusText: \"Applied\",\n      hasOnDismiss: false,\n    },\n  },\n  {\n    id: \"copied-no-undo\",\n    label: \"Copied (No Undo)\",\n    description: \"Keep button but no Undo\",\n    component: \"label\",\n    props: {\n      tagName: \"li\",\n      componentName: \"ListItem\",\n      status: \"copied\",\n      hasAgent: true,\n      statusText: \"Changes saved\",\n      supportsUndo: false,\n    },\n  },\n  {\n    id: \"copied-with-more-options\",\n    label: \"Copied (More Options)\",\n    description: \"Ellipsis button for context menu\",\n    component: \"label\",\n    props: {\n      tagName: \"div\",\n      componentName: \"Widget\",\n      status: \"copied\",\n      hasAgent: true,\n      statusText: \"Updated\",\n      supportsUndo: true,\n      showMoreOptions: true,\n    },\n  },\n  {\n    id: \"copied-custom-dismiss\",\n    label: \"Copied (Custom Dismiss)\",\n    description: '\"Accept\" instead of \"Keep\"',\n    component: \"label\",\n    props: {\n      tagName: \"section\",\n      componentName: \"Hero\",\n      status: \"copied\",\n      hasAgent: true,\n      statusText: \"Ready\",\n      supportsUndo: true,\n      dismissButtonText: \"Accept\",\n    },\n  },\n  {\n    id: \"copied-followup-placeholder\",\n    label: \"Copied (Follow-up Placeholder)\",\n    description: \"Previous prompt as placeholder\",\n    component: \"label\",\n    props: {\n      tagName: \"header\",\n      componentName: \"TopBar\",\n      status: \"copied\",\n      hasAgent: true,\n      statusText: \"Done\",\n      supportsUndo: true,\n      supportsFollowUp: true,\n      previousPrompt: \"make it bigger\",\n    },\n  },\n\n  // === AGENT EDGE CASES ===\n  {\n    id: \"agent-long-component\",\n    label: \"Agent (Long Component)\",\n    description: \"Long component name with prompt\",\n    component: \"label\",\n    props: {\n      tagName: \"div\",\n      componentName: \"VeryLongComponentNameThatShouldTruncateInTheUI\",\n      status: \"idle\",\n      hasAgent: true,\n      isPromptMode: true,\n      inputValue: \"\",\n    },\n  },\n  {\n    id: \"agent-long-status\",\n    label: \"Agent (Long Status)\",\n    description: \"Very long status text during operation\",\n    component: \"label\",\n    props: {\n      tagName: \"form\",\n      componentName: \"SearchForm\",\n      status: \"copying\",\n      hasAgent: true,\n      inputValue: \"add validation\",\n      statusText: \"Analyzing component structure and dependencies…\",\n    },\n  },\n  {\n    id: \"agent-long-previous\",\n    label: \"Agent (Long Previous Prompt)\",\n    description: \"Very long previous prompt truncation\",\n    component: \"label\",\n    props: {\n      tagName: \"div\",\n      componentName: \"Card\",\n      status: \"idle\",\n      hasAgent: true,\n      isPromptMode: true,\n      inputValue: \"and also fix the spacing\",\n      replyToPrompt:\n        \"make the card have rounded corners with a subtle shadow and increase the padding on all sides to make it feel more spacious\",\n    },\n  },\n  {\n    id: \"agent-copied-long-status\",\n    label: \"Agent Copied (Long Status)\",\n    description: \"Long completion status message\",\n    component: \"label\",\n    props: {\n      tagName: \"section\",\n      componentName: \"HeroSection\",\n      status: \"copied\",\n      hasAgent: true,\n      statusText: \"Successfully applied 5 changes across 3 files\",\n      supportsUndo: true,\n      supportsFollowUp: true,\n    },\n  },\n  {\n    id: \"agent-copied-long-placeholder\",\n    label: \"Agent Copied (Long Placeholder)\",\n    description: \"Long previous prompt as placeholder\",\n    component: \"label\",\n    props: {\n      tagName: \"nav\",\n      componentName: \"Navbar\",\n      status: \"copied\",\n      hasAgent: true,\n      statusText: \"Done\",\n      supportsUndo: true,\n      supportsFollowUp: true,\n      previousPrompt:\n        \"make the navbar sticky with a blur background effect and add smooth scroll behavior\",\n    },\n  },\n  {\n    id: \"agent-all-features\",\n    label: \"Agent (All Features)\",\n    description: \"Undo + Follow-up + More options\",\n    component: \"label\",\n    props: {\n      tagName: \"div\",\n      componentName: \"Dashboard\",\n      status: \"copied\",\n      hasAgent: true,\n      statusText: \"Applied changes\",\n      supportsUndo: true,\n      supportsFollowUp: true,\n      showMoreOptions: true,\n      previousPrompt: \"add dark mode\",\n    },\n  },\n  {\n    id: \"agent-single-char-component\",\n    label: \"Agent (Single Char)\",\n    description: \"Single character component name\",\n    component: \"label\",\n    props: {\n      tagName: \"i\",\n      componentName: \"I\",\n      status: \"idle\",\n      hasAgent: true,\n      isPromptMode: true,\n      inputValue: \"\",\n    },\n  },\n  {\n    id: \"agent-numeric-component\",\n    label: \"Agent (Numeric Name)\",\n    description: \"Component name with numbers\",\n    component: \"label\",\n    props: {\n      tagName: \"div\",\n      componentName: \"Card2024V2\",\n      status: \"idle\",\n      hasAgent: true,\n      isPromptMode: true,\n      inputValue: \"update the styles\",\n    },\n  },\n\n  // === ERROR STATES ===\n  {\n    id: \"error\",\n    label: \"Error\",\n    description: \"Error message with Retry + Ok\",\n    component: \"label\",\n    props: {\n      tagName: \"dialog\",\n      componentName: \"Modal\",\n      status: \"error\",\n      error: \"Failed to copy element\",\n    },\n  },\n  {\n    id: \"error-retry-only\",\n    label: \"Error (Retry Only)\",\n    description: \"Retry button, no Ok\",\n    component: \"label\",\n    props: {\n      tagName: \"form\",\n      componentName: \"Search\",\n      status: \"error\",\n      error: \"Connection timeout\",\n      hasOnRetry: true,\n      hasOnAcknowledge: false,\n    },\n  },\n  {\n    id: \"error-ok-only\",\n    label: \"Error (Ok Only)\",\n    description: \"Ok button, no Retry\",\n    component: \"label\",\n    props: {\n      tagName: \"div\",\n      componentName: \"Alert\",\n      status: \"error\",\n      error: \"Operation cancelled\",\n      hasOnRetry: false,\n      hasOnAcknowledge: true,\n    },\n  },\n  {\n    id: \"error-long-message\",\n    label: \"Error (Long Message)\",\n    description: \"Truncated error > 50 chars\",\n    component: \"label\",\n    props: {\n      tagName: \"section\",\n      componentName: \"Dashboard\",\n      status: \"error\",\n      error:\n        \"The server returned an unexpected error response. Please check your network connection and try again later.\",\n    },\n  },\n  {\n    id: \"error-long-component\",\n    label: \"Error (Long Component)\",\n    description: \"Error with long component name\",\n    component: \"label\",\n    props: {\n      tagName: \"div\",\n      componentName: \"VeryLongComponentNameThatMightOverflow\",\n      status: \"error\",\n      error: \"Component not found\",\n    },\n  },\n  {\n    id: \"error-no-buttons\",\n    label: \"Error (No Buttons)\",\n    description: \"Error with no action buttons\",\n    component: \"label\",\n    props: {\n      tagName: \"span\",\n      componentName: \"Text\",\n      status: \"error\",\n      error: \"Unknown error occurred\",\n      hasOnRetry: false,\n      hasOnAcknowledge: false,\n    },\n  },\n\n  // ══════════════════════════════════════════════════════════════════════════\n  // CONTEXT MENU STATES (Right-Click Menu)\n  // ══════════════════════════════════════════════════════════════════════════\n  {\n    id: \"context-menu-basic\",\n    label: \"Context Menu (Basic)\",\n    description: \"Copy, Copy HTML options\",\n    component: \"context-menu\",\n    props: {\n      tagName: \"button\",\n      componentName: \"Button\",\n      hasFilePath: false,\n    },\n  },\n  {\n    id: \"context-menu-with-open\",\n    label: \"Context Menu (With Open)\",\n    description: \"Includes Open option with file path\",\n    component: \"context-menu\",\n    props: {\n      tagName: \"div\",\n      componentName: \"Header\",\n      hasFilePath: true,\n      filePath: \"src/components/Header.tsx\",\n    },\n  },\n  {\n    id: \"context-menu-tag-only\",\n    label: \"Context Menu (Tag Only)\",\n    description: \"HTML tag without component name\",\n    component: \"context-menu\",\n    props: {\n      tagName: \"section\",\n      hasFilePath: false,\n    },\n  },\n  {\n    id: \"context-menu-long-component\",\n    label: \"Context Menu (Long Name)\",\n    description: \"Long component name in header\",\n    component: \"context-menu\",\n    props: {\n      tagName: \"div\",\n      componentName: \"SuperLongComponentNameThatNeedsTruncation\",\n      hasFilePath: true,\n      filePath: \"src/components/SuperLongComponentNameThatNeedsTruncation.tsx\",\n    },\n  },\n  {\n    id: \"context-menu-long-tag\",\n    label: \"Context Menu (Long Tag)\",\n    description: \"Long custom element tag\",\n    component: \"context-menu\",\n    props: {\n      tagName: \"my-custom-interactive-web-component\",\n      hasFilePath: false,\n    },\n  },\n\n  // ══════════════════════════════════════════════════════════════════════════\n  // TOOLBAR STATES\n  // ══════════════════════════════════════════════════════════════════════════\n  {\n    id: \"toolbar-default\",\n    label: \"Toolbar (Default)\",\n    description: \"Inactive, enabled state\",\n    component: \"toolbar\",\n    props: {\n      isToolbarActive: false,\n      isToolbarEnabled: true,\n    },\n  },\n  {\n    id: \"toolbar-active\",\n    label: \"Toolbar (Active)\",\n    description: \"Selection mode active\",\n    component: \"toolbar\",\n    props: {\n      isToolbarActive: true,\n      isToolbarEnabled: true,\n      toolbarHistoryItemCount: 3,\n    },\n  },\n  {\n    id: \"toolbar-disabled\",\n    label: \"Toolbar (Disabled)\",\n    description: \"Toggle switch off\",\n    component: \"toolbar\",\n    props: {\n      isToolbarActive: false,\n      isToolbarEnabled: false,\n    },\n  },\n  {\n    id: \"toolbar-active-disabled\",\n    label: \"Toolbar (Active + Disabled)\",\n    description: \"Active but toggle off\",\n    component: \"toolbar\",\n    props: {\n      isToolbarActive: true,\n      isToolbarEnabled: false,\n    },\n  },\n  {\n    id: \"toolbar-collapsed-bottom\",\n    label: \"Toolbar (Collapsed Bottom)\",\n    description: \"Minimized, snapped to bottom edge\",\n    component: \"toolbar\",\n    props: {\n      isToolbarActive: false,\n      isToolbarEnabled: true,\n      isToolbarCollapsed: true,\n      toolbarSnapEdge: \"bottom\",\n    },\n  },\n  {\n    id: \"toolbar-collapsed-top\",\n    label: \"Toolbar (Collapsed Top)\",\n    description: \"Minimized, snapped to top edge\",\n    component: \"toolbar\",\n    props: {\n      isToolbarActive: false,\n      isToolbarEnabled: true,\n      isToolbarCollapsed: true,\n      toolbarSnapEdge: \"top\",\n    },\n  },\n  {\n    id: \"toolbar-collapsed-left\",\n    label: \"Toolbar (Collapsed Left)\",\n    description: \"Minimized, snapped to left edge\",\n    component: \"toolbar\",\n    props: {\n      isToolbarActive: false,\n      isToolbarEnabled: true,\n      isToolbarCollapsed: true,\n      toolbarSnapEdge: \"left\",\n    },\n  },\n  {\n    id: \"toolbar-collapsed-right\",\n    label: \"Toolbar (Collapsed Right)\",\n    description: \"Minimized, snapped to right edge\",\n    component: \"toolbar\",\n    props: {\n      isToolbarActive: false,\n      isToolbarEnabled: true,\n      isToolbarCollapsed: true,\n      toolbarSnapEdge: \"right\",\n    },\n  },\n  {\n    id: \"toolbar-history-read\",\n    label: \"Toolbar (History Read)\",\n    description: \"Inbox icon, no unread items\",\n    component: \"toolbar\",\n    props: {\n      isToolbarActive: true,\n      isToolbarEnabled: true,\n      toolbarHistoryItemCount: 5,\n      toolbarHasUnreadHistoryItems: false,\n    },\n  },\n  {\n    id: \"toolbar-history-unread\",\n    label: \"Toolbar (History Unread)\",\n    description: \"Inbox icon with unread indicator\",\n    component: \"toolbar\",\n    props: {\n      isToolbarActive: true,\n      isToolbarEnabled: true,\n      toolbarHistoryItemCount: 3,\n      toolbarHasUnreadHistoryItems: true,\n    },\n  },\n\n  // ══════════════════════════════════════════════════════════════════════════\n  // HISTORY DROPDOWN STATES\n  // ══════════════════════════════════════════════════════════════════════════\n  {\n    id: \"history-empty\",\n    label: \"History (Empty)\",\n    description: \"No copied elements yet\",\n    component: \"history-dropdown\",\n    props: {\n      historyItems: [],\n    },\n  },\n  {\n    id: \"history-single-item\",\n    label: \"History (Single Item)\",\n    description: \"One copied element\",\n    component: \"history-dropdown\",\n    props: {\n      historyItems: [\n        {\n          id: \"history-1\",\n          content: \"<Button />\",\n          elementName: \"Button\",\n          tagName: \"button\",\n          componentName: \"Button\",\n          isComment: false,\n          timestamp: Date.now() - 30_000,\n        },\n      ],\n    },\n  },\n  {\n    id: \"history-multiple-items\",\n    label: \"History (Multiple Items)\",\n    description: \"Several copied elements\",\n    component: \"history-dropdown\",\n    props: {\n      historyItems: [\n        {\n          id: \"history-1\",\n          content: \"<Header />\",\n          elementName: \"Header\",\n          tagName: \"header\",\n          componentName: \"Header\",\n          isComment: false,\n          timestamp: Date.now() - 15_000,\n        },\n        {\n          id: \"history-2\",\n          content: \"<Navigation />\",\n          elementName: \"Navigation\",\n          tagName: \"nav\",\n          componentName: \"Navigation\",\n          isComment: false,\n          timestamp: Date.now() - 120_000,\n        },\n        {\n          id: \"history-3\",\n          content: \"<Footer />\",\n          elementName: \"Footer\",\n          tagName: \"footer\",\n          componentName: \"Footer\",\n          isComment: false,\n          timestamp: Date.now() - 3_600_000,\n        },\n      ],\n    },\n  },\n  {\n    id: \"history-with-comments\",\n    label: \"History (With Comments)\",\n    description: \"Items with comment annotations\",\n    component: \"history-dropdown\",\n    props: {\n      historyItems: [\n        {\n          id: \"history-1\",\n          content: \"<Card />\",\n          elementName: \"Card\",\n          tagName: \"div\",\n          componentName: \"Card\",\n          isComment: true,\n          commentText: \"make it bigger\",\n          timestamp: Date.now() - 10_000,\n        },\n        {\n          id: \"history-2\",\n          content: \"<Sidebar />\",\n          elementName: \"Sidebar\",\n          tagName: \"aside\",\n          componentName: \"Sidebar\",\n          isComment: true,\n          commentText: \"add dark mode support\",\n          timestamp: Date.now() - 300_000,\n        },\n        {\n          id: \"history-3\",\n          content: \"<Button />\",\n          elementName: \"Button\",\n          tagName: \"button\",\n          componentName: \"Button\",\n          isComment: false,\n          timestamp: Date.now() - 7_200_000,\n        },\n      ],\n    },\n  },\n  {\n    id: \"history-tag-only\",\n    label: \"History (Tag Only)\",\n    description: \"Items without component names\",\n    component: \"history-dropdown\",\n    props: {\n      historyItems: [\n        {\n          id: \"history-1\",\n          content: \"<section />\",\n          elementName: \"section\",\n          tagName: \"section\",\n          isComment: false,\n          timestamp: Date.now() - 60_000,\n        },\n        {\n          id: \"history-2\",\n          content: \"<div />\",\n          elementName: \"div\",\n          tagName: \"div\",\n          isComment: false,\n          timestamp: Date.now() - 180_000,\n        },\n      ],\n    },\n  },\n  {\n    id: \"history-long-names\",\n    label: \"History (Long Names)\",\n    description: \"Long component names truncation\",\n    component: \"history-dropdown\",\n    props: {\n      historyItems: [\n        {\n          id: \"history-1\",\n          content: \"<InteractiveDataVisualizationChart />\",\n          elementName: \"InteractiveDataVisualizationChart\",\n          tagName: \"div\",\n          componentName: \"InteractiveDataVisualizationChart\",\n          isComment: true,\n          commentText: \"add tooltips on hover with data values and percentage\",\n          timestamp: Date.now() - 5_000,\n        },\n        {\n          id: \"history-2\",\n          content: \"<SuperLongComponentNameWrapper />\",\n          elementName: \"SuperLongComponentNameWrapper\",\n          tagName: \"custom-interactive-element\",\n          componentName: \"SuperLongComponentNameWrapper\",\n          isComment: false,\n          timestamp: Date.now() - 86_400_000,\n        },\n      ],\n    },\n  },\n  {\n    id: \"history-many-items\",\n    label: \"History (Many Items)\",\n    description: \"Scrollable list with many items\",\n    component: \"history-dropdown\",\n    props: {\n      historyItems: [\n        {\n          id: \"history-1\",\n          content: \"<Header />\",\n          elementName: \"Header\",\n          tagName: \"header\",\n          componentName: \"Header\",\n          isComment: false,\n          timestamp: Date.now() - 10_000,\n        },\n        {\n          id: \"history-2\",\n          content: \"<Navigation />\",\n          elementName: \"Navigation\",\n          tagName: \"nav\",\n          componentName: \"Navigation\",\n          isComment: true,\n          commentText: \"make it sticky\",\n          timestamp: Date.now() - 60_000,\n        },\n        {\n          id: \"history-3\",\n          content: \"<Card />\",\n          elementName: \"Card\",\n          tagName: \"div\",\n          componentName: \"Card\",\n          isComment: false,\n          timestamp: Date.now() - 300_000,\n        },\n        {\n          id: \"history-4\",\n          content: \"<Button />\",\n          elementName: \"Button\",\n          tagName: \"button\",\n          componentName: \"Button\",\n          isComment: true,\n          commentText: \"increase padding\",\n          timestamp: Date.now() - 600_000,\n        },\n        {\n          id: \"history-5\",\n          content: \"<Footer />\",\n          elementName: \"Footer\",\n          tagName: \"footer\",\n          componentName: \"Footer\",\n          isComment: false,\n          timestamp: Date.now() - 1_800_000,\n        },\n        {\n          id: \"history-6\",\n          content: \"<Sidebar />\",\n          elementName: \"Sidebar\",\n          tagName: \"aside\",\n          componentName: \"Sidebar\",\n          isComment: false,\n          timestamp: Date.now() - 3_600_000,\n        },\n        {\n          id: \"history-7\",\n          content: \"<Modal />\",\n          elementName: \"Modal\",\n          tagName: \"dialog\",\n          componentName: \"Modal\",\n          isComment: true,\n          commentText: \"add animation\",\n          timestamp: Date.now() - 7_200_000,\n        },\n        {\n          id: \"history-8\",\n          content: \"<Form />\",\n          elementName: \"Form\",\n          tagName: \"form\",\n          componentName: \"Form\",\n          isComment: false,\n          timestamp: Date.now() - 43_200_000,\n        },\n      ],\n    },\n  },\n\n  // ══════════════════════════════════════════════════════════════════════════\n  // ANIMATION SEQUENCES\n  // ══════════════════════════════════════════════════════════════════════════\n  {\n    id: \"anim-copy-flow\",\n    label: \"Copy Flow\",\n    description: \"idle → copying → ✓ copied\",\n    component: \"label\",\n    props: {\n      tagName: \"button\",\n      componentName: \"Button\",\n      status: \"idle\",\n      hasAgent: false,\n    },\n    animationSequence: [\n      {\n        props: {\n          tagName: \"button\",\n          componentName: \"Button\",\n          status: \"idle\",\n          hasAgent: false,\n        },\n        durationMs: 1500,\n      },\n      {\n        props: {\n          tagName: \"button\",\n          componentName: \"Button\",\n          status: \"copying\",\n          hasAgent: false,\n          statusText: \"Grabbing…\",\n        },\n        durationMs: 2000,\n      },\n      {\n        props: {\n          tagName: \"button\",\n          componentName: \"Button\",\n          status: \"copied\",\n          hasAgent: false,\n          hasOnDismiss: false,\n          hasOnUndo: false,\n          showMoreOptions: true,\n        },\n        durationMs: 2000,\n      },\n    ],\n  },\n  {\n    id: \"anim-agent-flow\",\n    label: \"Agent Flow\",\n    description: \"prompt → thinking → done\",\n    component: \"label\",\n    props: {\n      tagName: \"div\",\n      componentName: \"Card\",\n      status: \"idle\",\n      hasAgent: true,\n      isPromptMode: true,\n      inputValue: \"\",\n    },\n    animationSequence: [\n      {\n        props: {\n          tagName: \"div\",\n          componentName: \"Card\",\n          status: \"idle\",\n          hasAgent: true,\n          isPromptMode: true,\n          inputValue: \"\",\n        },\n        durationMs: 1000,\n      },\n      {\n        props: {\n          tagName: \"div\",\n          componentName: \"Card\",\n          status: \"idle\",\n          hasAgent: true,\n          isPromptMode: true,\n          inputValue: \"make it\",\n        },\n        durationMs: 400,\n      },\n      {\n        props: {\n          tagName: \"div\",\n          componentName: \"Card\",\n          status: \"idle\",\n          hasAgent: true,\n          isPromptMode: true,\n          inputValue: \"make it bigger\",\n        },\n        durationMs: 800,\n      },\n      {\n        props: {\n          tagName: \"div\",\n          componentName: \"Card\",\n          status: \"copying\",\n          hasAgent: true,\n          inputValue: \"make it bigger\",\n          statusText: \"Thinking…\",\n        },\n        durationMs: 1500,\n      },\n      {\n        props: {\n          tagName: \"div\",\n          componentName: \"Card\",\n          status: \"copying\",\n          hasAgent: true,\n          inputValue: \"make it bigger\",\n          statusText: \"Applying changes…\",\n        },\n        durationMs: 1500,\n      },\n      {\n        props: {\n          tagName: \"div\",\n          componentName: \"Card\",\n          status: \"copied\",\n          hasAgent: true,\n          statusText: \"Applied changes\",\n          supportsUndo: true,\n          supportsFollowUp: true,\n          showMoreOptions: true,\n        },\n        durationMs: 2500,\n      },\n    ],\n  },\n  {\n    id: \"anim-error-flow\",\n    label: \"Error Flow\",\n    description: \"idle → copying → error\",\n    component: \"label\",\n    props: {\n      tagName: \"form\",\n      componentName: \"Form\",\n      status: \"idle\",\n      hasAgent: false,\n    },\n    animationSequence: [\n      {\n        props: {\n          tagName: \"form\",\n          componentName: \"Form\",\n          status: \"idle\",\n          hasAgent: false,\n        },\n        durationMs: 1500,\n      },\n      {\n        props: {\n          tagName: \"form\",\n          componentName: \"Form\",\n          status: \"copying\",\n          hasAgent: false,\n          statusText: \"Grabbing…\",\n        },\n        durationMs: 2000,\n      },\n      {\n        props: {\n          tagName: \"form\",\n          componentName: \"Form\",\n          status: \"error\",\n          error: \"Failed to copy element\",\n          hasOnRetry: true,\n          hasOnAcknowledge: true,\n        },\n        durationMs: 2500,\n      },\n    ],\n  },\n  {\n    id: \"anim-discard-flow\",\n    label: \"Discard Prompt\",\n    description: \"prompt → pending dismiss → cancelled\",\n    component: \"label\",\n    props: {\n      tagName: \"header\",\n      componentName: \"Header\",\n      status: \"idle\",\n      hasAgent: true,\n      isPromptMode: true,\n      inputValue: \"change color\",\n    },\n    animationSequence: [\n      {\n        props: {\n          tagName: \"header\",\n          componentName: \"Header\",\n          status: \"idle\",\n          hasAgent: true,\n          isPromptMode: true,\n          inputValue: \"change color\",\n        },\n        durationMs: 1500,\n      },\n      {\n        props: {\n          tagName: \"header\",\n          componentName: \"Header\",\n          status: \"idle\",\n          hasAgent: true,\n          isPromptMode: true,\n          isPendingDismiss: true,\n        },\n        durationMs: 2000,\n      },\n      {\n        props: {\n          tagName: \"header\",\n          componentName: \"Header\",\n          status: \"idle\",\n          hasAgent: true,\n          isPromptMode: true,\n          inputValue: \"change color\",\n        },\n        durationMs: 1500,\n      },\n    ],\n  },\n  {\n    id: \"anim-abort-flow\",\n    label: \"Abort Operation\",\n    description: \"copying → pending abort → idle\",\n    component: \"label\",\n    props: {\n      tagName: \"section\",\n      componentName: \"Section\",\n      status: \"copying\",\n      hasAgent: true,\n      inputValue: \"add animation\",\n      statusText: \"Thinking…\",\n    },\n    animationSequence: [\n      {\n        props: {\n          tagName: \"section\",\n          componentName: \"Section\",\n          status: \"copying\",\n          hasAgent: true,\n          inputValue: \"add animation\",\n          statusText: \"Thinking…\",\n        },\n        durationMs: 1500,\n      },\n      {\n        props: {\n          tagName: \"section\",\n          componentName: \"Section\",\n          status: \"copying\",\n          hasAgent: true,\n          isPendingAbort: true,\n        },\n        durationMs: 2000,\n      },\n      {\n        props: {\n          tagName: \"section\",\n          componentName: \"Section\",\n          status: \"idle\",\n          hasAgent: true,\n          isPromptMode: true,\n          inputValue: \"\",\n        },\n        durationMs: 1500,\n      },\n    ],\n  },\n  {\n    id: \"anim-toolbar-toggle\",\n    label: \"Toolbar Toggle\",\n    description: \"inactive → active → inactive\",\n    component: \"toolbar\",\n    props: {\n      isToolbarActive: false,\n      isToolbarEnabled: true,\n    },\n    animationSequence: [\n      {\n        props: { isToolbarActive: false, isToolbarEnabled: true },\n        durationMs: 1500,\n      },\n      {\n        props: { isToolbarActive: true, isToolbarEnabled: true },\n        durationMs: 2000,\n      },\n      {\n        props: { isToolbarActive: false, isToolbarEnabled: true },\n        durationMs: 1500,\n      },\n    ],\n  },\n  {\n    id: \"anim-toolbar-enable\",\n    label: \"Toolbar Enable\",\n    description: \"disabled → enabled → disabled\",\n    component: \"toolbar\",\n    props: {\n      isToolbarActive: false,\n      isToolbarEnabled: false,\n    },\n    animationSequence: [\n      {\n        props: { isToolbarActive: false, isToolbarEnabled: false },\n        durationMs: 1500,\n      },\n      {\n        props: { isToolbarActive: false, isToolbarEnabled: true },\n        durationMs: 2000,\n      },\n      {\n        props: { isToolbarActive: false, isToolbarEnabled: false },\n        durationMs: 1500,\n      },\n    ],\n  },\n  {\n    id: \"anim-followup-flow\",\n    label: \"Follow-up\",\n    description: \"copied → follow-up → new result\",\n    component: \"label\",\n    props: {\n      tagName: \"nav\",\n      componentName: \"Navbar\",\n      status: \"copied\",\n      hasAgent: true,\n      statusText: \"Done\",\n      supportsUndo: true,\n      supportsFollowUp: true,\n      showMoreOptions: true,\n    },\n    animationSequence: [\n      {\n        props: {\n          tagName: \"nav\",\n          componentName: \"Navbar\",\n          status: \"copied\",\n          hasAgent: true,\n          statusText: \"Done\",\n          supportsUndo: true,\n          supportsFollowUp: true,\n          showMoreOptions: true,\n        },\n        durationMs: 2000,\n      },\n      {\n        props: {\n          tagName: \"nav\",\n          componentName: \"Navbar\",\n          status: \"copying\",\n          hasAgent: true,\n          inputValue: \"also make it sticky\",\n          statusText: \"Thinking…\",\n        },\n        durationMs: 2000,\n      },\n      {\n        props: {\n          tagName: \"nav\",\n          componentName: \"Navbar\",\n          status: \"copied\",\n          hasAgent: true,\n          statusText: \"Applied 2 changes\",\n          supportsUndo: true,\n          supportsFollowUp: true,\n          showMoreOptions: true,\n        },\n        durationMs: 2000,\n      },\n    ],\n  },\n  {\n    id: \"anim-undo-flow\",\n    label: \"Undo Flow\",\n    description: \"copied → undo → idle\",\n    component: \"label\",\n    props: {\n      tagName: \"div\",\n      componentName: \"Modal\",\n      status: \"copied\",\n      hasAgent: true,\n      statusText: \"Applied changes\",\n      supportsUndo: true,\n      showMoreOptions: true,\n    },\n    animationSequence: [\n      {\n        props: {\n          tagName: \"div\",\n          componentName: \"Modal\",\n          status: \"copied\",\n          hasAgent: true,\n          statusText: \"Applied changes\",\n          supportsUndo: true,\n          supportsFollowUp: true,\n          showMoreOptions: true,\n        },\n        durationMs: 2000,\n      },\n      {\n        props: {\n          tagName: \"div\",\n          componentName: \"Modal\",\n          status: \"idle\",\n          hasAgent: true,\n          isPromptMode: true,\n          inputValue: \"\",\n        },\n        durationMs: 2000,\n      },\n    ],\n  },\n  {\n    id: \"anim-retry-flow\",\n    label: \"Retry After Error\",\n    description: \"error → retry → ✓ copied\",\n    component: \"label\",\n    props: {\n      tagName: \"table\",\n      componentName: \"DataTable\",\n      status: \"error\",\n      error: \"Connection timeout\",\n      hasOnRetry: true,\n      hasOnAcknowledge: true,\n    },\n    animationSequence: [\n      {\n        props: {\n          tagName: \"table\",\n          componentName: \"DataTable\",\n          status: \"error\",\n          error: \"Connection timeout\",\n          hasOnRetry: true,\n          hasOnAcknowledge: true,\n        },\n        durationMs: 2000,\n      },\n      {\n        props: {\n          tagName: \"table\",\n          componentName: \"DataTable\",\n          status: \"copying\",\n          statusText: \"Retrying…\",\n        },\n        durationMs: 1500,\n      },\n      {\n        props: {\n          tagName: \"table\",\n          componentName: \"DataTable\",\n          status: \"copied\",\n          hasOnDismiss: false,\n          hasOnUndo: false,\n          showMoreOptions: true,\n        },\n        durationMs: 2000,\n      },\n    ],\n  },\n  {\n    id: \"anim-agent-error-flow\",\n    label: \"Agent Error\",\n    description: \"prompt → thinking → error\",\n    component: \"label\",\n    props: {\n      tagName: \"aside\",\n      componentName: \"Sidebar\",\n      status: \"idle\",\n      hasAgent: true,\n      isPromptMode: true,\n      inputValue: \"\",\n    },\n    animationSequence: [\n      {\n        props: {\n          tagName: \"aside\",\n          componentName: \"Sidebar\",\n          status: \"idle\",\n          hasAgent: true,\n          isPromptMode: true,\n          inputValue: \"\",\n        },\n        durationMs: 1000,\n      },\n      {\n        props: {\n          tagName: \"aside\",\n          componentName: \"Sidebar\",\n          status: \"idle\",\n          hasAgent: true,\n          isPromptMode: true,\n          inputValue: \"make it collapsible\",\n        },\n        durationMs: 800,\n      },\n      {\n        props: {\n          tagName: \"aside\",\n          componentName: \"Sidebar\",\n          status: \"copying\",\n          hasAgent: true,\n          statusText: \"Thinking…\",\n        },\n        durationMs: 2000,\n      },\n      {\n        props: {\n          tagName: \"aside\",\n          componentName: \"Sidebar\",\n          status: \"error\",\n          hasAgent: true,\n          error: \"Agent failed to respond\",\n          hasOnRetry: true,\n          hasOnAcknowledge: true,\n        },\n        durationMs: 2500,\n      },\n    ],\n  },\n  {\n    id: \"anim-acknowledge-error\",\n    label: \"Acknowledge Error\",\n    description: \"error → ok → dismissed\",\n    component: \"label\",\n    props: {\n      tagName: \"ul\",\n      componentName: \"List\",\n      status: \"error\",\n      error: \"Element not found\",\n      hasOnRetry: true,\n      hasOnAcknowledge: true,\n    },\n    animationSequence: [\n      {\n        props: {\n          tagName: \"ul\",\n          componentName: \"List\",\n          status: \"error\",\n          error: \"Element not found\",\n          hasOnRetry: true,\n          hasOnAcknowledge: true,\n        },\n        durationMs: 2500,\n      },\n      {\n        props: { tagName: \"ul\", componentName: \"List\", status: \"idle\" },\n        durationMs: 2000,\n      },\n    ],\n  },\n  {\n    id: \"anim-connect-agent\",\n    label: \"Agent Connect\",\n    description: \"disconnected → connected → prompt\",\n    component: \"label\",\n    props: {\n      tagName: \"footer\",\n      componentName: \"Footer\",\n      status: \"idle\",\n      hasAgent: true,\n    },\n    animationSequence: [\n      {\n        props: {\n          tagName: \"footer\",\n          componentName: \"Footer\",\n          status: \"idle\",\n          hasAgent: true,\n        },\n        durationMs: 2000,\n      },\n      {\n        props: {\n          tagName: \"footer\",\n          componentName: \"Footer\",\n          status: \"idle\",\n          hasAgent: true,\n          isPromptMode: true,\n          inputValue: \"\",\n        },\n        durationMs: 2500,\n      },\n    ],\n  },\n  {\n    id: \"anim-context-menu-flow\",\n    label: \"Context Menu\",\n    description: \"context menu options\",\n    component: \"context-menu\",\n    props: {\n      tagName: \"article\",\n      componentName: \"BlogPost\",\n      hasFilePath: false,\n    },\n    animationSequence: [\n      {\n        props: {\n          tagName: \"article\",\n          componentName: \"BlogPost\",\n          hasFilePath: false,\n        },\n        durationMs: 2000,\n      },\n      {\n        props: {\n          tagName: \"article\",\n          componentName: \"BlogPost\",\n          hasFilePath: true,\n          filePath: \"src/components/BlogPost.tsx\",\n        },\n        durationMs: 2500,\n      },\n      {\n        props: {\n          tagName: \"article\",\n          componentName: \"BlogPost\",\n          hasFilePath: false,\n        },\n        durationMs: 2000,\n      },\n    ],\n  },\n  {\n    id: \"anim-toolbar-collapse-bottom\",\n    label: \"Toolbar Collapse (Bottom)\",\n    description: \"expanded → collapsed → expanded\",\n    component: \"toolbar\",\n    props: {\n      isToolbarActive: true,\n      isToolbarEnabled: true,\n      isToolbarCollapsed: false,\n      toolbarSnapEdge: \"bottom\",\n    },\n    animationSequence: [\n      {\n        props: {\n          isToolbarActive: true,\n          isToolbarEnabled: true,\n          isToolbarCollapsed: false,\n          toolbarSnapEdge: \"bottom\",\n        },\n        durationMs: 2000,\n      },\n      {\n        props: {\n          isToolbarActive: true,\n          isToolbarEnabled: true,\n          isToolbarCollapsed: true,\n          toolbarSnapEdge: \"bottom\",\n        },\n        durationMs: 2000,\n      },\n      {\n        props: {\n          isToolbarActive: true,\n          isToolbarEnabled: true,\n          isToolbarCollapsed: false,\n          toolbarSnapEdge: \"bottom\",\n        },\n        durationMs: 2000,\n      },\n    ],\n  },\n  {\n    id: \"anim-toolbar-collapse-right\",\n    label: \"Toolbar Collapse (Right)\",\n    description: \"expanded → collapsed → expanded\",\n    component: \"toolbar\",\n    props: {\n      isToolbarActive: true,\n      isToolbarEnabled: true,\n      isToolbarCollapsed: false,\n      toolbarSnapEdge: \"right\",\n    },\n    animationSequence: [\n      {\n        props: {\n          isToolbarActive: true,\n          isToolbarEnabled: true,\n          isToolbarCollapsed: false,\n          toolbarSnapEdge: \"right\",\n        },\n        durationMs: 2000,\n      },\n      {\n        props: {\n          isToolbarActive: true,\n          isToolbarEnabled: true,\n          isToolbarCollapsed: true,\n          toolbarSnapEdge: \"right\",\n        },\n        durationMs: 2000,\n      },\n      {\n        props: {\n          isToolbarActive: true,\n          isToolbarEnabled: true,\n          isToolbarCollapsed: false,\n          toolbarSnapEdge: \"right\",\n        },\n        durationMs: 2000,\n      },\n    ],\n  },\n  {\n    id: \"anim-copy-with-file\",\n    label: \"Copy with File Path\",\n    description: \"idle → copying → ✓ copied\",\n    component: \"label\",\n    props: {\n      tagName: \"span\",\n      componentName: \"Badge\",\n      status: \"idle\",\n      filePath: \"src/components/Badge.tsx\",\n    },\n    animationSequence: [\n      {\n        props: {\n          tagName: \"span\",\n          componentName: \"Badge\",\n          status: \"idle\",\n          filePath: \"src/components/Badge.tsx\",\n        },\n        durationMs: 1500,\n      },\n      {\n        props: {\n          tagName: \"span\",\n          componentName: \"Badge\",\n          status: \"copying\",\n          filePath: \"src/components/Badge.tsx\",\n          statusText: \"Grabbing…\",\n        },\n        durationMs: 2000,\n      },\n      {\n        props: {\n          tagName: \"span\",\n          componentName: \"Badge\",\n          status: \"copied\",\n          filePath: \"src/components/Badge.tsx\",\n          hasOnDismiss: false,\n          hasOnUndo: false,\n          showMoreOptions: true,\n        },\n        durationMs: 2000,\n      },\n    ],\n  },\n  {\n    id: \"anim-multiple-elements\",\n    label: \"Multiple Elements\",\n    description: \"1 → 5 selected → copying → ✓ copied\",\n    component: \"label\",\n    props: {\n      tagName: \"li\",\n      componentName: \"ListItem\",\n      status: \"idle\",\n      elementsCount: 1,\n    },\n    animationSequence: [\n      {\n        props: {\n          tagName: \"li\",\n          componentName: \"ListItem\",\n          status: \"idle\",\n          elementsCount: 1,\n        },\n        durationMs: 2000,\n      },\n      {\n        props: {\n          tagName: \"li\",\n          componentName: \"ListItem\",\n          status: \"idle\",\n          elementsCount: 5,\n        },\n        durationMs: 2000,\n      },\n      {\n        props: {\n          tagName: \"li\",\n          componentName: \"ListItem\",\n          status: \"copying\",\n          elementsCount: 5,\n          statusText: \"Grabbing 5…\",\n        },\n        durationMs: 2000,\n      },\n      {\n        props: {\n          tagName: \"li\",\n          componentName: \"ListItem\",\n          status: \"copied\",\n          elementsCount: 5,\n          hasOnDismiss: false,\n          hasOnUndo: false,\n          showMoreOptions: true,\n        },\n        durationMs: 2000,\n      },\n    ],\n  },\n  {\n    id: \"anim-long-operation\",\n    label: \"Long Operation\",\n    description: \"copying with status updates\",\n    component: \"label\",\n    props: {\n      tagName: \"main\",\n      componentName: \"Dashboard\",\n      status: \"copying\",\n      hasAgent: true,\n      statusText: \"Starting…\",\n    },\n    animationSequence: [\n      {\n        props: {\n          tagName: \"main\",\n          componentName: \"Dashboard\",\n          status: \"idle\",\n          hasAgent: true,\n          isPromptMode: true,\n          inputValue: \"redesign the layout\",\n        },\n        durationMs: 1500,\n      },\n      {\n        props: {\n          tagName: \"main\",\n          componentName: \"Dashboard\",\n          status: \"copying\",\n          hasAgent: true,\n          statusText: \"Analyzing…\",\n        },\n        durationMs: 1500,\n      },\n      {\n        props: {\n          tagName: \"main\",\n          componentName: \"Dashboard\",\n          status: \"copying\",\n          hasAgent: true,\n          statusText: \"Generating code…\",\n        },\n        durationMs: 1500,\n      },\n      {\n        props: {\n          tagName: \"main\",\n          componentName: \"Dashboard\",\n          status: \"copying\",\n          hasAgent: true,\n          statusText: \"Applying changes…\",\n        },\n        durationMs: 1500,\n      },\n      {\n        props: {\n          tagName: \"main\",\n          componentName: \"Dashboard\",\n          status: \"copied\",\n          hasAgent: true,\n          statusText: \"Done\",\n          supportsUndo: true,\n          supportsFollowUp: true,\n          showMoreOptions: true,\n        },\n        durationMs: 2000,\n      },\n    ],\n  },\n  {\n    id: \"anim-reply-to-prompt\",\n    label: \"Reply to Prompt\",\n    description: \"completed → edit with previous\",\n    component: \"label\",\n    props: {\n      tagName: \"div\",\n      componentName: \"Card\",\n      status: \"copied\",\n      hasAgent: true,\n      statusText: \"Done\",\n      supportsFollowUp: true,\n      showMoreOptions: true,\n    },\n    animationSequence: [\n      {\n        props: {\n          tagName: \"div\",\n          componentName: \"Card\",\n          status: \"copied\",\n          hasAgent: true,\n          statusText: \"Done\",\n          supportsUndo: true,\n          supportsFollowUp: true,\n          showMoreOptions: true,\n        },\n        durationMs: 2000,\n      },\n      {\n        props: {\n          tagName: \"div\",\n          componentName: \"Card\",\n          status: \"idle\",\n          hasAgent: true,\n          isPromptMode: true,\n          inputValue: \"\",\n          previousPrompt: \"make it bigger\",\n          replyToPrompt: \"make it bigger\",\n        },\n        durationMs: 2500,\n      },\n      {\n        props: {\n          tagName: \"div\",\n          componentName: \"Card\",\n          status: \"idle\",\n          hasAgent: true,\n          isPromptMode: true,\n          inputValue: \"also add shadow\",\n          previousPrompt: \"make it bigger\",\n          replyToPrompt: \"make it bigger\",\n        },\n        durationMs: 1500,\n      },\n    ],\n  },\n];\n\nconst CELL_SIZE_PX = 300;\nconst TARGET_HEIGHT_PX = 48;\nconst GAP_PX = 16;\n\nconst CARD_BORDER_RADIUS_PX = 8;\nconst CARD_HEADER_PADDING = \"12px 14px\";\nconst CARD_CONTENT_PADDING_PX = 16;\nconst CARD_TITLE_FONT_SIZE_PX = 13;\nconst CARD_DESCRIPTION_FONT_SIZE_PX = 11;\nconst CARD_TITLE_GAP_PX = 2;\n\nconst REFRESH_BUTTON_SIZE_PX = 20;\nconst REFRESH_BUTTON_BORDER_RADIUS_PX = 4;\n\nconst HEADER_PADDING = \"16px 24px\";\nconst HEADER_TITLE_FONT_SIZE_PX = 14;\nconst HEADER_BUTTONS_GAP_PX = 8;\n\nconst TOGGLE_BUTTON_PADDING = \"5px 10px\";\nconst TOGGLE_BUTTON_GAP_PX = 6;\nconst TOGGLE_BUTTON_BORDER_RADIUS_PX = 6;\nconst TOGGLE_BUTTON_FONT_SIZE_PX = 12;\n\nconst SECTION_TITLE_FONT_SIZE_PX = 11;\nconst SECTION_TITLE_MARGIN_BOTTOM_PX = 12;\n\nconst FPS_METER_POSITION_PX = 16;\nconst FPS_METER_PADDING = \"6px 10px\";\nconst FPS_METER_BORDER_RADIUS_PX = 6;\nconst FPS_METER_FONT_SIZE_PX = 12;\n\nconst TARGET_BORDER_RADIUS_PX = 6;\nconst TARGET_FONT_SIZE_PX = 12;\n\nconst TRANSITION_DURATION = \"0.15s ease\";\n\nconst STORAGE_KEY_THEME = \"react-grab-design-system-theme\";\nconst STORAGE_KEY_STARRED = \"react-grab-design-system-starred\";\n\nconst generateRandomSuffix = (length: number): string => {\n  const chars =\n    \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n  let result = \"\";\n  for (let i = 0; i < length; i++) {\n    result += chars.charAt(Math.floor(Math.random() * chars.length));\n  }\n  return result;\n};\n\nconst elongateProps = (\n  props: DesignSystemStateProps,\n): DesignSystemStateProps => {\n  const elongateString = (value: string | undefined): string | undefined => {\n    if (!value) return value;\n    return value + generateRandomSuffix(20 + Math.floor(Math.random() * 30));\n  };\n\n  return {\n    ...props,\n    tagName: elongateString(props.tagName),\n    componentName: elongateString(props.componentName),\n    filePath: elongateString(props.filePath),\n    statusText: elongateString(props.statusText),\n    error: elongateString(props.error),\n    inputValue: elongateString(props.inputValue),\n    replyToPrompt: elongateString(props.replyToPrompt),\n    previousPrompt: elongateString(props.previousPrompt),\n    dismissButtonText: elongateString(props.dismissButtonText),\n  };\n};\n\nconst loadTheme = (): boolean => {\n  try {\n    const saved = localStorage.getItem(STORAGE_KEY_THEME);\n    if (saved === \"light\") return false;\n    return true;\n  } catch {\n    return true;\n  }\n};\n\nconst saveTheme = (isDark: boolean): void => {\n  try {\n    localStorage.setItem(STORAGE_KEY_THEME, isDark ? \"dark\" : \"light\");\n  } catch {}\n};\n\nconst loadStarred = (): Set<string> => {\n  try {\n    const saved = localStorage.getItem(STORAGE_KEY_STARRED);\n    if (!saved) return new Set();\n    return new Set(JSON.parse(saved) as string[]);\n  } catch {\n    return new Set();\n  }\n};\n\nconst saveStarred = (starred: Set<string>): void => {\n  try {\n    localStorage.setItem(STORAGE_KEY_STARRED, JSON.stringify([...starred]));\n  } catch {}\n};\n\ninterface ThemeColors {\n  background: string;\n  cardBackground: string;\n  cardContentBackground: string;\n  cardBorder: string;\n  cardShadow: string;\n  titleText: string;\n  descriptionText: string;\n  targetBackground: string;\n  targetBorder: string;\n  targetText: string;\n  toggleBackground: string;\n  toggleBorder: string;\n  toggleText: string;\n  sectionTitle: string;\n}\n\nconst DARK_THEME: ThemeColors = {\n  background: \"#000000\",\n  cardBackground: \"rgba(255, 255, 255, 0.05)\",\n  cardContentBackground: \"rgba(0, 0, 0, 0.6)\",\n  cardBorder: \"rgba(255, 255, 255, 0.1)\",\n  cardShadow: \"0 8px 30px rgba(0, 0, 0, 0.3)\",\n  titleText: \"#ffffff\",\n  descriptionText: \"rgba(255, 255, 255, 0.5)\",\n  targetBackground: \"rgba(215, 95, 203, 0.1)\",\n  targetBorder: \"rgba(215, 95, 203, 0.3)\",\n  targetText: \"rgba(215, 95, 203, 0.7)\",\n  toggleBackground: \"rgba(255, 255, 255, 0.05)\",\n  toggleBorder: \"rgba(255, 255, 255, 0.1)\",\n  toggleText: \"#ffffff\",\n  sectionTitle: \"rgba(255, 255, 255, 0.4)\",\n};\n\nconst LIGHT_THEME: ThemeColors = {\n  background: \"#f5f5f5\",\n  cardBackground: \"#ffffff\",\n  cardContentBackground: \"rgba(0, 0, 0, 0.03)\",\n  cardBorder: \"rgba(0, 0, 0, 0.1)\",\n  cardShadow: \"0 8px 30px rgba(0, 0, 0, 0.08)\",\n  titleText: \"#0a0a0a\",\n  descriptionText: \"rgba(0, 0, 0, 0.5)\",\n  targetBackground: \"rgba(215, 95, 203, 0.08)\",\n  targetBorder: \"rgba(215, 95, 203, 0.3)\",\n  targetText: \"rgba(215, 95, 203, 0.7)\",\n  toggleBackground: \"#ffffff\",\n  toggleBorder: \"rgba(0, 0, 0, 0.1)\",\n  toggleText: \"#0a0a0a\",\n  sectionTitle: \"rgba(0, 0, 0, 0.4)\",\n};\n\nconst createToggleButtonStyle = (\n  theme: ThemeColors,\n): Record<string, string> => ({\n  display: \"flex\",\n  \"align-items\": \"center\",\n  gap: `${TOGGLE_BUTTON_GAP_PX}px`,\n  padding: TOGGLE_BUTTON_PADDING,\n  \"background-color\": theme.toggleBackground,\n  border: `1px solid ${theme.toggleBorder}`,\n  \"border-radius\": `${TOGGLE_BUTTON_BORDER_RADIUS_PX}px`,\n  color: theme.toggleText,\n  \"font-size\": `${TOGGLE_BUTTON_FONT_SIZE_PX}px`,\n  \"font-weight\": \"500\",\n  cursor: \"pointer\",\n  transition: `all ${TRANSITION_DURATION}`,\n});\n\nconst createCardContainerStyle = (\n  theme: ThemeColors,\n): Record<string, string> => ({\n  display: \"flex\",\n  \"flex-direction\": \"column\",\n  \"background-color\": theme.cardBackground,\n  \"border-radius\": `${CARD_BORDER_RADIUS_PX}px`,\n  border: `1px solid ${theme.cardBorder}`,\n  \"box-shadow\": theme.cardShadow,\n  overflow: \"hidden\",\n  \"aspect-ratio\": \"1\",\n  transition: `all ${TRANSITION_DURATION}`,\n});\n\nconst createCardHeaderStyle = (theme: ThemeColors): Record<string, string> => ({\n  display: \"flex\",\n  \"justify-content\": \"space-between\",\n  \"align-items\": \"flex-start\",\n  padding: CARD_HEADER_PADDING,\n  \"border-bottom\": `1px solid ${theme.cardBorder}`,\n});\n\nconst createCardContentStyle = (\n  theme: ThemeColors,\n): Record<string, string> => ({\n  flex: \"1\",\n  display: \"flex\",\n  \"flex-direction\": \"column\",\n  \"align-items\": \"center\",\n  \"justify-content\": \"center\",\n  padding: `${CARD_CONTENT_PADDING_PX}px`,\n  position: \"relative\",\n  \"background-color\": theme.cardContentBackground,\n});\n\nconst createTargetStyle = (theme: ThemeColors): Record<string, string> => ({\n  width: \"100%\",\n  height: `${TARGET_HEIGHT_PX}px`,\n  \"background-color\": theme.targetBackground,\n  border: `1px solid ${theme.targetBorder}`,\n  \"border-radius\": `${TARGET_BORDER_RADIUS_PX}px`,\n  display: \"flex\",\n  \"align-items\": \"center\",\n  \"justify-content\": \"center\",\n  color: theme.targetText,\n  \"font-size\": `${TARGET_FONT_SIZE_PX}px`,\n  \"font-family\": \"ui-monospace, SFMono-Regular, monospace\",\n});\n\ninterface StateCardProps {\n  state: DesignSystemState;\n  theme: ThemeColors;\n  getBounds: () => OverlayBounds | undefined;\n  registerCell: (element: HTMLDivElement) => void;\n  onRefresh: () => void;\n  getTargetDisplayText: () => string;\n  isStarred: boolean;\n  onToggleStar: () => void;\n  isScrambled: boolean;\n}\n\nconst StateCard = (props: StateCardProps) => {\n  const [isCardRefreshing, setIsCardRefreshing] = createSignal(false);\n  const [isPlaying, setIsPlaying] = createSignal(false);\n  const [frameIndex, setFrameIndex] = createSignal(0);\n\n  let animationTimeout: ReturnType<typeof setTimeout> | undefined;\n\n  const clearAnimationTimeout = () => {\n    if (animationTimeout) {\n      clearTimeout(animationTimeout);\n      animationTimeout = undefined;\n    }\n  };\n\n  const hasAnimation = () => Boolean(props.state.animationSequence?.length);\n  const frameCount = () => props.state.animationSequence?.length ?? 0;\n\n  const boundsAnchor = () => {\n    const bounds = props.getBounds();\n    if (!bounds) return null;\n    return {\n      x: bounds.x + bounds.width / 2,\n      y: bounds.y + bounds.height,\n      width: bounds.width,\n    };\n  };\n\n  const currentProps = (): DesignSystemStateProps => {\n    const baseProps =\n      hasAnimation() && props.state.animationSequence\n        ? props.state.animationSequence[frameIndex()].props\n        : props.state.props;\n    return props.isScrambled ? elongateProps(baseProps) : baseProps;\n  };\n\n  const scheduleNextFrame = () => {\n    if (!props.state.animationSequence) return;\n\n    const sequence = props.state.animationSequence;\n    const currentFrame = sequence[frameIndex()];\n\n    animationTimeout = setTimeout(() => {\n      const nextIndex = (frameIndex() + 1) % sequence.length;\n      setFrameIndex(nextIndex);\n\n      if (isPlaying()) {\n        scheduleNextFrame();\n      }\n    }, currentFrame.durationMs);\n  };\n\n  const handleTogglePlay = (event: MouseEvent) => {\n    event.stopPropagation();\n    if (isPlaying()) {\n      setIsPlaying(false);\n      clearAnimationTimeout();\n    } else {\n      setIsPlaying(true);\n      scheduleNextFrame();\n    }\n  };\n\n  const handleSliderChange = (event: Event) => {\n    const inputElement = event.target as HTMLInputElement;\n    const selectedFrameIndex = Number(inputElement.value);\n    setFrameIndex(selectedFrameIndex);\n    if (isPlaying()) {\n      setIsPlaying(false);\n      clearAnimationTimeout();\n    }\n  };\n\n  onCleanup(() => {\n    clearAnimationTimeout();\n  });\n\n  const handleCardRefresh = (event: MouseEvent) => {\n    event.stopPropagation();\n    setIsCardRefreshing(true);\n    setFrameIndex(0);\n    setIsPlaying(false);\n    clearAnimationTimeout();\n    props.onRefresh();\n    queueMicrotask(() => setIsCardRefreshing(false));\n  };\n\n  const handleToggleStar = (event: MouseEvent) => {\n    event.stopPropagation();\n    props.onToggleStar();\n  };\n\n  return (\n    <div style={createCardContainerStyle(props.theme)}>\n      <div style={createCardHeaderStyle(props.theme)}>\n        <div\n          style={{\n            display: \"flex\",\n            \"flex-direction\": \"column\",\n            gap: `${CARD_TITLE_GAP_PX}px`,\n            flex: \"1\",\n            \"min-width\": \"0\",\n          }}\n        >\n          <div style={{ display: \"flex\", \"align-items\": \"center\", gap: \"6px\" }}>\n            <span\n              style={{\n                color: props.theme.titleText,\n                \"font-size\": `${CARD_TITLE_FONT_SIZE_PX}px`,\n                \"font-weight\": \"500\",\n                \"line-height\": \"1.3\",\n              }}\n            >\n              {props.state.label}\n            </span>\n            <Show when={hasAnimation()}>\n              <span\n                style={{\n                  color: props.theme.descriptionText,\n                  \"font-size\": \"10px\",\n                  \"font-weight\": \"500\",\n                }}\n              >\n                {frameIndex() + 1}/{frameCount()}\n              </span>\n            </Show>\n          </div>\n          <span\n            style={{\n              color: props.theme.descriptionText,\n              \"font-size\": `${CARD_DESCRIPTION_FONT_SIZE_PX}px`,\n              \"line-height\": \"1.3\",\n            }}\n          >\n            {props.state.description}\n          </span>\n          <Show when={hasAnimation()}>\n            <div\n              style={{\n                display: \"flex\",\n                \"align-items\": \"center\",\n                gap: \"8px\",\n                \"margin-top\": \"8px\",\n              }}\n            >\n              <button\n                onClick={handleTogglePlay}\n                style={{\n                  display: \"flex\",\n                  \"align-items\": \"center\",\n                  \"justify-content\": \"center\",\n                  width: \"20px\",\n                  height: \"20px\",\n                  padding: \"0\",\n                  \"background-color\": \"transparent\",\n                  border: `1px solid ${props.theme.cardBorder}`,\n                  \"border-radius\": \"4px\",\n                  color: props.theme.titleText,\n                  \"font-size\": \"10px\",\n                  cursor: \"pointer\",\n                  \"flex-shrink\": \"0\",\n                  transition: `all ${TRANSITION_DURATION}`,\n                }}\n                title={isPlaying() ? \"Pause\" : \"Play\"}\n              >\n                {isPlaying() ? \"⏸\" : \"▶\"}\n              </button>\n              <input\n                type=\"range\"\n                min=\"0\"\n                max={frameCount() - 1}\n                value={frameIndex()}\n                onInput={handleSliderChange}\n                style={{\n                  flex: \"1\",\n                  height: \"4px\",\n                  cursor: \"pointer\",\n                  \"accent-color\": props.theme.titleText,\n                }}\n              />\n            </div>\n          </Show>\n        </div>\n        <div\n          style={{\n            display: \"flex\",\n            \"align-items\": \"center\",\n            gap: \"4px\",\n            \"flex-shrink\": \"0\",\n          }}\n        >\n          <button\n            onClick={handleToggleStar}\n            style={{\n              display: \"flex\",\n              \"align-items\": \"center\",\n              \"justify-content\": \"center\",\n              width: `${REFRESH_BUTTON_SIZE_PX}px`,\n              height: `${REFRESH_BUTTON_SIZE_PX}px`,\n              padding: \"0\",\n              \"background-color\": \"transparent\",\n              border: `1px solid ${props.isStarred ? \"rgba(250, 204, 21, 0.5)\" : props.theme.cardBorder}`,\n              \"border-radius\": `${REFRESH_BUTTON_BORDER_RADIUS_PX}px`,\n              color: props.isStarred\n                ? \"rgba(250, 204, 21, 1)\"\n                : props.theme.descriptionText,\n              \"font-size\": `${TOGGLE_BUTTON_FONT_SIZE_PX}px`,\n              cursor: \"pointer\",\n              transition: `all ${TRANSITION_DURATION}`,\n            }}\n            title={props.isStarred ? \"Unstar this card\" : \"Star this card\"}\n          >\n            {props.isStarred ? \"★\" : \"☆\"}\n          </button>\n          <button\n            onClick={handleCardRefresh}\n            style={{\n              display: \"flex\",\n              \"align-items\": \"center\",\n              \"justify-content\": \"center\",\n              width: `${REFRESH_BUTTON_SIZE_PX}px`,\n              height: `${REFRESH_BUTTON_SIZE_PX}px`,\n              padding: \"0\",\n              \"background-color\": \"transparent\",\n              border: `1px solid ${props.theme.cardBorder}`,\n              \"border-radius\": `${REFRESH_BUTTON_BORDER_RADIUS_PX}px`,\n              color: props.theme.descriptionText,\n              \"font-size\": `${TOGGLE_BUTTON_FONT_SIZE_PX}px`,\n              cursor: \"pointer\",\n              transition: `all ${TRANSITION_DURATION}`,\n            }}\n            title=\"Refresh this card\"\n          >\n            ↻\n          </button>\n        </div>\n      </div>\n\n      <div style={createCardContentStyle(props.theme)}>\n        <Show when={!isCardRefreshing()}>\n          <Show\n            when={\n              props.state.component !== \"toolbar\" &&\n              props.state.component !== \"history-dropdown\"\n            }\n          >\n            <div\n              ref={(element) => props.registerCell(element)}\n              style={createTargetStyle(props.theme)}\n            >\n              {props.getTargetDisplayText()}\n            </div>\n          </Show>\n\n          <Show when={props.state.component === \"label\"}>\n            <SelectionLabel\n              tagName={currentProps().tagName}\n              componentName={currentProps().componentName}\n              elementsCount={currentProps().elementsCount}\n              selectionBounds={props.getBounds()}\n              mouseX={boundsAnchor()?.x}\n              visible={true}\n              status={currentProps().status}\n              hasAgent={currentProps().hasAgent}\n              isPromptMode={currentProps().isPromptMode}\n              inputValue={currentProps().inputValue}\n              replyToPrompt={currentProps().replyToPrompt}\n              statusText={currentProps().statusText}\n              isPendingDismiss={currentProps().isPendingDismiss}\n              isPendingAbort={currentProps().isPendingAbort}\n              error={currentProps().error}\n              isContextMenuOpen={currentProps().isContextMenuOpen}\n              supportsUndo={currentProps().supportsUndo}\n              supportsFollowUp={currentProps().supportsFollowUp}\n              filePath={currentProps().filePath}\n              dismissButtonText={currentProps().dismissButtonText}\n              previousPrompt={currentProps().previousPrompt}\n              onOpen={currentProps().filePath ? () => {} : undefined}\n              onInputChange={() => {}}\n              onSubmit={() => {}}\n              onToggleExpand={() => {}}\n              onConfirmDismiss={() => {}}\n              onCancelDismiss={() => {}}\n              onConfirmAbort={() => {}}\n              onCancelAbort={() => {}}\n              onAcknowledgeError={\n                currentProps().hasOnAcknowledge !== false ? () => {} : undefined\n              }\n              onRetry={\n                currentProps().hasOnRetry !== false ? () => {} : undefined\n              }\n              onDismiss={\n                currentProps().hasOnDismiss !== false ? () => {} : undefined\n              }\n              onUndo={currentProps().hasOnUndo !== false ? () => {} : undefined}\n              onFollowUpSubmit={() => {}}\n              onAbort={() => {}}\n              onShowContextMenu={\n                currentProps().showMoreOptions ? () => {} : undefined\n              }\n            />\n          </Show>\n\n          <Show when={props.state.component === \"context-menu\"}>\n            <ContextMenu\n              position={\n                boundsAnchor()\n                  ? { x: boundsAnchor()!.x, y: boundsAnchor()!.y }\n                  : null\n              }\n              selectionBounds={props.getBounds() ?? null}\n              tagName={currentProps().tagName}\n              componentName={currentProps().componentName}\n              hasFilePath={currentProps().hasFilePath ?? false}\n              actions={[\n                {\n                  id: \"copy\",\n                  label: \"Copy\",\n                  shortcut: \"C\",\n                  onAction: () => {},\n                },\n                { id: \"copy-html\", label: \"Copy HTML\", onAction: () => {} },\n                {\n                  id: \"open\",\n                  label: \"Open\",\n                  shortcut: \"O\",\n                  enabled: currentProps().hasFilePath ?? false,\n                  onAction: () => {},\n                },\n                {\n                  id: \"comment\",\n                  label: \"Comment\",\n                  shortcut: \"Enter\",\n                  onAction: () => {},\n                },\n              ]}\n              onDismiss={() => {}}\n              onHide={() => {}}\n            />\n          </Show>\n\n          <Show when={props.state.component === \"toolbar\"}>\n            <ToolbarContent\n              isActive={currentProps().isToolbarActive ?? false}\n              enabled={currentProps().isToolbarEnabled ?? true}\n              isCollapsed={currentProps().isToolbarCollapsed}\n              snapEdge={currentProps().toolbarSnapEdge}\n              isHistoryExpanded={\n                (currentProps().toolbarHistoryItemCount ?? 0) > 0\n              }\n            />\n          </Show>\n\n          <Show when={props.state.component === \"history-dropdown\"}>\n            <div\n              style={{\n                position: \"absolute\",\n                bottom: `${CARD_CONTENT_PADDING_PX}px`,\n                left: \"50%\",\n                transform: \"translateX(-50%)\",\n              }}\n            >\n              <div ref={(element) => props.registerCell(element)}>\n                <ToolbarContent\n                  isActive={true}\n                  enabled={true}\n                  isHistoryExpanded={true}\n                />\n              </div>\n            </div>\n            <HistoryDropdown\n              position={\n                boundsAnchor()\n                  ? {\n                      ...boundsAnchor()!,\n                      edge: \"top\" as const,\n                      toolbarWidth: boundsAnchor()!.width,\n                    }\n                  : null\n              }\n              items={currentProps().historyItems ?? []}\n            />\n          </Show>\n        </Show>\n      </div>\n    </div>\n  );\n};\n\ninterface FpsMeterProps {\n  theme: ThemeColors;\n}\n\nconst FpsMeter = (props: FpsMeterProps) => {\n  const [fps, setFps] = createSignal(0);\n  let frameCount = 0;\n  let lastTime = performance.now();\n  let animationFrameId: number | undefined;\n\n  const measureFps = () => {\n    frameCount++;\n    const currentTime = performance.now();\n    const elapsed = currentTime - lastTime;\n\n    if (elapsed >= 1000) {\n      setFps(Math.round((frameCount * 1000) / elapsed));\n      frameCount = 0;\n      lastTime = currentTime;\n    }\n\n    animationFrameId = requestAnimationFrame(measureFps);\n  };\n\n  onMount(() => {\n    animationFrameId = requestAnimationFrame(measureFps);\n  });\n\n  onCleanup(() => {\n    if (animationFrameId) {\n      cancelAnimationFrame(animationFrameId);\n    }\n  });\n\n  return (\n    <div\n      style={{\n        position: \"fixed\",\n        bottom: `${FPS_METER_POSITION_PX}px`,\n        right: `${FPS_METER_POSITION_PX}px`,\n        padding: FPS_METER_PADDING,\n        \"background-color\": props.theme.cardBackground,\n        border: `1px solid ${props.theme.cardBorder}`,\n        \"border-radius\": `${FPS_METER_BORDER_RADIUS_PX}px`,\n        \"font-family\": \"ui-monospace, SFMono-Regular, monospace\",\n        \"font-size\": `${FPS_METER_FONT_SIZE_PX}px`,\n        color: props.theme.titleText,\n        \"z-index\": \"9999\",\n        \"backdrop-filter\": \"blur(8px)\",\n      }}\n    >\n      {fps()} FPS\n    </div>\n  );\n};\n\nconst DesignSystemGrid = () => {\n  const [cellRefs, setCellRefs] = createSignal<Map<string, HTMLDivElement>>(\n    new Map(),\n  );\n  const [boundsVersion, setBoundsVersion] = createSignal(0);\n  const [isDarkMode, setIsDarkMode] = createSignal(loadTheme());\n  const [isRefreshing, setIsRefreshing] = createSignal(false);\n  const [starredIds, setStarredIds] = createSignal<Set<string>>(loadStarred());\n  const [searchQuery, setSearchQuery] = createSignal(\"\");\n  const [isScrambled, setIsScrambled] = createSignal(false);\n\n  const handleRefresh = () => {\n    setIsRefreshing(true);\n    setCellRefs(new Map());\n    queueMicrotask(() => setIsRefreshing(false));\n  };\n\n  const handleToggleTheme = () => {\n    setIsDarkMode((prev) => {\n      const newValue = !prev;\n      saveTheme(newValue);\n      return newValue;\n    });\n  };\n\n  const handleToggleStar = (id: string) => {\n    setStarredIds((prev) => {\n      const next = new Set(prev);\n      if (next.has(id)) {\n        next.delete(id);\n      } else {\n        next.add(id);\n      }\n      saveStarred(next);\n      return next;\n    });\n  };\n\n  const isStarred = (id: string): boolean => starredIds().has(id);\n\n  const theme = () => (isDarkMode() ? DARK_THEME : LIGHT_THEME);\n\n  const sectionTitleStyle = () => ({\n    display: \"block\",\n    color: theme().sectionTitle,\n    \"font-size\": `${SECTION_TITLE_FONT_SIZE_PX}px`,\n    \"font-weight\": \"600\",\n    \"text-transform\": \"uppercase\",\n    \"letter-spacing\": \"0.05em\",\n    \"margin-bottom\": `${SECTION_TITLE_MARGIN_BOTTOM_PX}px`,\n  });\n\n  const gridStyle = () => ({\n    display: \"grid\",\n    \"grid-template-columns\": `repeat(auto-fill, minmax(${CELL_SIZE_PX}px, 1fr))`,\n    gap: `${GAP_PX}px`,\n  });\n\n  const getTargetDisplayText = (state: DesignSystemState): string => {\n    if (state.component === \"toolbar\") {\n      return \"\";\n    }\n    if (state.props.elementsCount && state.props.elementsCount > 1) {\n      return `<${state.props.elementsCount} elements>`;\n    }\n    return `<${state.props.componentName || state.props.tagName || \"element\"} />`;\n  };\n\n  const registerCell = (id: string, element: HTMLDivElement) => {\n    setCellRefs((prev) => {\n      const next = new Map(prev);\n      next.set(id, element);\n      return next;\n    });\n    setBoundsVersion((version) => version + 1);\n  };\n\n  const getBoundsForCell = (id: string): OverlayBounds | undefined => {\n    boundsVersion();\n    const element = cellRefs().get(id);\n    if (!element) return undefined;\n    const rect = element.getBoundingClientRect();\n    return {\n      x: rect.x,\n      y: rect.y,\n      width: rect.width,\n      height: rect.height,\n      borderRadius: `${TARGET_BORDER_RADIUS_PX}px`,\n      transform: \"\",\n    };\n  };\n\n  let resizeObserver: ResizeObserver | undefined;\n  let containerRef: HTMLDivElement | undefined;\n\n  const handleScroll = () => {\n    setBoundsVersion((version) => version + 1);\n  };\n\n  const setupResizeObserver = (container: HTMLDivElement) => {\n    containerRef = container;\n    resizeObserver = new ResizeObserver(() => {\n      setBoundsVersion((version) => version + 1);\n    });\n    resizeObserver.observe(container);\n    window.addEventListener(\"scroll\", handleScroll, true);\n    container.addEventListener(\"scroll\", handleScroll, true);\n  };\n\n  onCleanup(() => {\n    resizeObserver?.disconnect();\n    window.removeEventListener(\"scroll\", handleScroll, true);\n    containerRef?.removeEventListener(\"scroll\", handleScroll, true);\n  });\n\n  const hasAnimation = (state: DesignSystemState): boolean =>\n    Boolean(state.animationSequence?.length);\n\n  const matchesSearch = (state: DesignSystemState): boolean => {\n    const query = searchQuery().toLowerCase().trim();\n    if (!query) return true;\n    return (\n      state.label.toLowerCase().includes(query) ||\n      state.description.toLowerCase().includes(query) ||\n      state.id.toLowerCase().includes(query) ||\n      (state.props.componentName?.toLowerCase().includes(query) ?? false) ||\n      (state.props.tagName?.toLowerCase().includes(query) ?? false)\n    );\n  };\n\n  const starredStates = () =>\n    DESIGN_SYSTEM_STATES.filter(\n      (state) => starredIds().has(state.id) && matchesSearch(state),\n    );\n  const labelStates = () =>\n    DESIGN_SYSTEM_STATES.filter(\n      (state) =>\n        state.component === \"label\" &&\n        !state.props.hasAgent &&\n        !hasAnimation(state) &&\n        matchesSearch(state),\n    );\n  const contextMenuStates = () =>\n    DESIGN_SYSTEM_STATES.filter(\n      (state) =>\n        state.component === \"context-menu\" &&\n        !hasAnimation(state) &&\n        matchesSearch(state),\n    );\n  const toolbarStates = () =>\n    DESIGN_SYSTEM_STATES.filter(\n      (state) =>\n        state.component === \"toolbar\" &&\n        !hasAnimation(state) &&\n        matchesSearch(state),\n    );\n  const agentLabelStates = () =>\n    DESIGN_SYSTEM_STATES.filter(\n      (state) =>\n        state.component === \"label\" &&\n        state.props.hasAgent &&\n        !hasAnimation(state) &&\n        matchesSearch(state),\n    );\n  const historyDropdownStates = () =>\n    DESIGN_SYSTEM_STATES.filter(\n      (state) =>\n        state.component === \"history-dropdown\" &&\n        !hasAnimation(state) &&\n        matchesSearch(state),\n    );\n  const flowStates = () =>\n    DESIGN_SYSTEM_STATES.filter(\n      (state) => hasAnimation(state) && matchesSearch(state),\n    );\n\n  const createRefreshHandler = (id: string) => () => {\n    setCellRefs((prev) => {\n      const next = new Map(prev);\n      next.delete(id);\n      return next;\n    });\n  };\n\n  return (\n    <div\n      ref={setupResizeObserver}\n      style={{\n        display: \"flex\",\n        \"flex-direction\": \"column\",\n        \"min-height\": \"100vh\",\n        \"background-color\": theme().background,\n        \"font-family\":\n          'Geist, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif',\n        transition: `background-color ${TRANSITION_DURATION}`,\n      }}\n    >\n      {/* Header */}\n      <div\n        style={{\n          display: \"flex\",\n          \"justify-content\": \"space-between\",\n          \"align-items\": \"center\",\n          padding: HEADER_PADDING,\n          \"border-bottom\": `1px solid ${theme().cardBorder}`,\n        }}\n      >\n        <div style={{ display: \"flex\", \"align-items\": \"center\", gap: \"10px\" }}>\n          <svg\n            width=\"24\"\n            height=\"24\"\n            viewBox=\"0 0 294 294\"\n            fill=\"none\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n          >\n            <g clip-path=\"url(#clip0_logo)\">\n              <mask\n                id=\"mask0_logo\"\n                style=\"mask-type:luminance\"\n                maskUnits=\"userSpaceOnUse\"\n                x=\"0\"\n                y=\"0\"\n                width=\"294\"\n                height=\"294\"\n              >\n                <path d=\"M294 0H0V294H294V0Z\" fill=\"white\" />\n              </mask>\n              <g mask=\"url(#mask0_logo)\">\n                <path\n                  d=\"M144.599 47.4924C169.712 27.3959 194.548 20.0265 212.132 30.1797C227.847 39.2555 234.881 60.3243 231.926 89.516C231.677 92.0069 231.328 94.5423 230.94 97.1058L228.526 110.14C228.517 110.136 228.505 110.132 228.495 110.127C228.486 110.165 228.479 110.203 228.468 110.24L216.255 105.741C216.256 105.736 216.248 105.728 216.248 105.723C207.915 103.125 199.421 101.075 190.82 99.5888L190.696 99.5588L173.526 97.2648L173.511 97.2631C173.492 97.236 173.467 97.2176 173.447 97.1905C163.862 96.2064 154.233 95.7166 144.599 95.7223C134.943 95.7162 125.295 96.219 115.693 97.2286C110.075 105.033 104.859 113.118 100.063 121.453C95.2426 129.798 90.8624 138.391 86.939 147.193C90.8624 155.996 95.2426 164.588 100.063 172.933C104.866 181.302 110.099 189.417 115.741 197.245C115.749 197.245 115.758 197.246 115.766 197.247L115.752 197.27L115.745 197.283L115.754 197.296L126.501 211.013L126.574 211.089C132.136 217.767 138.126 224.075 144.507 229.974L144.609 230.082L154.572 238.287C154.539 238.319 154.506 238.35 154.472 238.38C154.485 238.392 154.499 238.402 154.513 238.412L143.846 247.482L143.827 247.497C126.56 261.128 109.472 268.745 94.8019 268.745C88.5916 268.837 82.4687 267.272 77.0657 264.208C61.3496 255.132 54.3164 234.062 57.2707 204.871C57.528 202.307 57.8806 199.694 58.2904 197.054C28.3363 185.327 9.52301 167.51 9.52301 147.193C9.52301 129.042 24.2476 112.396 50.9901 100.375C53.3443 99.3163 55.7938 98.3058 58.2904 97.3526C57.8806 94.7023 57.528 92.0803 57.2707 89.516C54.3164 60.3243 61.3496 39.2555 77.0657 30.1797C94.6494 20.0265 119.486 27.3959 144.599 47.4924ZM70.6423 201.315C70.423 202.955 70.2229 204.566 70.0704 206.168C67.6686 229.567 72.5478 246.628 83.3615 252.988L83.5176 253.062C95.0399 259.717 114.015 254.426 134.782 238.38C125.298 229.45 116.594 219.725 108.764 209.314C95.8516 207.742 83.0977 205.066 70.6423 201.315ZM80.3534 163.438C77.34 171.677 74.8666 180.104 72.9484 188.664C81.1787 191.224 89.5657 193.247 98.0572 194.724L98.4618 194.813C95.2115 189.865 92.0191 184.66 88.9311 179.378C85.8433 174.097 83.003 168.768 80.3534 163.438ZM60.759 110.203C59.234 110.839 57.7378 111.475 56.27 112.11C34.7788 121.806 22.3891 134.591 22.3891 147.193C22.3891 160.493 36.4657 174.297 60.7494 184.26C63.7439 171.581 67.8124 159.182 72.9104 147.193C67.822 135.23 63.7566 122.855 60.759 110.203ZM98.4137 99.6404C89.8078 101.145 81.3075 103.206 72.9676 105.809C74.854 114.203 77.2741 122.468 80.2132 130.554L80.3059 130.939C82.9938 125.6 85.8049 120.338 88.8834 115.008C91.9618 109.679 95.1544 104.569 98.4137 99.6404ZM94.9258 38.5215C90.9331 38.4284 86.9866 39.3955 83.4891 41.3243C72.6291 47.6015 67.6975 64.5954 70.0424 87.9446L70.0416 88.2194C70.194 89.8208 70.3941 91.4325 70.6134 93.0624C83.0737 89.3364 95.8263 86.6703 108.736 85.0924C116.57 74.6779 125.28 64.9532 134.773 56.0249C119.877 44.5087 105.895 38.5215 94.9258 38.5215ZM205.737 41.3148C202.268 39.398 198.355 38.4308 194.394 38.5099L194.29 38.512C183.321 38.512 169.34 44.4991 154.444 56.0153C163.93 64.9374 172.634 74.6557 180.462 85.064C193.375 86.6345 206.128 89.3102 218.584 93.0624C218.812 91.4325 219.003 89.8118 219.165 88.2098C221.548 64.7099 216.65 47.6164 205.737 41.3148ZM144.552 64.3097C138.104 70.2614 132.054 76.6306 126.443 83.3765C132.39 82.995 138.426 82.8046 144.552 82.8046C150.727 82.8046 156.778 83.0143 162.707 83.3765C157.08 76.6293 151.015 70.2596 144.552 64.3097Z\"\n                  fill={theme().titleText}\n                />\n                <path\n                  d=\"M144.598 47.4924C169.712 27.3959 194.547 20.0265 212.131 30.1797C227.847 39.2555 234.88 60.3243 231.926 89.516C231.677 92.0069 231.327 94.5423 230.941 97.1058L228.526 110.14L228.496 110.127C228.487 110.165 228.478 110.203 228.469 110.24L216.255 105.741L216.249 105.723C207.916 103.125 199.42 101.075 190.82 99.5888L190.696 99.5588L173.525 97.2648L173.511 97.263C173.492 97.236 173.468 97.2176 173.447 97.1905C163.863 96.2064 154.234 95.7166 144.598 95.7223C134.943 95.7162 125.295 96.219 115.693 97.2286C110.075 105.033 104.859 113.118 100.063 121.453C95.2426 129.798 90.8622 138.391 86.939 147.193C90.8622 155.996 95.2426 164.588 100.063 172.933C104.866 181.302 110.099 189.417 115.741 197.245L115.766 197.247L115.752 197.27L115.745 197.283L115.754 197.296L126.501 211.013L126.574 211.089C132.136 217.767 138.126 224.075 144.506 229.974L144.61 230.082L154.572 238.287C154.539 238.319 154.506 238.35 154.473 238.38L154.512 238.412L143.847 247.482L143.827 247.497C126.56 261.13 109.472 268.745 94.8018 268.745C88.5915 268.837 82.4687 267.272 77.0657 264.208C61.3496 255.132 54.3162 234.062 57.2707 204.871C57.528 202.307 57.8806 199.694 58.2904 197.054C28.3362 185.327 9.52298 167.51 9.52298 147.193C9.52298 129.042 24.2476 112.396 50.9901 100.375C53.3443 99.3163 55.7938 98.3058 58.2904 97.3526C57.8806 94.7023 57.528 92.0803 57.2707 89.516C54.3162 60.3243 61.3496 39.2555 77.0657 30.1797C94.6493 20.0265 119.486 27.3959 144.598 47.4924ZM70.6422 201.315C70.423 202.955 70.2229 204.566 70.0704 206.168C67.6686 229.567 72.5478 246.628 83.3615 252.988L83.5175 253.062C95.0399 259.717 114.015 254.426 134.782 238.38C125.298 229.45 116.594 219.725 108.764 209.314C95.8515 207.742 83.0977 205.066 70.6422 201.315ZM80.3534 163.438C77.34 171.677 74.8666 180.104 72.9484 188.664C81.1786 191.224 89.5657 193.247 98.0572 194.724L98.4618 194.813C95.2115 189.865 92.0191 184.66 88.931 179.378C85.8433 174.097 83.003 168.768 80.3534 163.438ZM60.7589 110.203C59.234 110.839 57.7378 111.475 56.2699 112.11C34.7788 121.806 22.3891 134.591 22.3891 147.193C22.3891 160.493 36.4657 174.297 60.7494 184.26C63.7439 171.581 67.8124 159.182 72.9103 147.193C67.822 135.23 63.7566 122.855 60.7589 110.203ZM98.4137 99.6404C89.8078 101.145 81.3075 103.206 72.9676 105.809C74.8539 114.203 77.2741 122.468 80.2132 130.554L80.3059 130.939C82.9938 125.6 85.8049 120.338 88.8834 115.008C91.9618 109.679 95.1544 104.569 98.4137 99.6404ZM94.9258 38.5215C90.9331 38.4284 86.9866 39.3955 83.4891 41.3243C72.629 47.6015 67.6975 64.5954 70.0424 87.9446L70.0415 88.2194C70.194 89.8208 70.3941 91.4325 70.6134 93.0624C83.0737 89.3364 95.8262 86.6703 108.736 85.0924C116.57 74.6779 125.28 64.9532 134.772 56.0249C119.877 44.5087 105.895 38.5215 94.9258 38.5215ZM205.737 41.3148C202.268 39.398 198.355 38.4308 194.394 38.5099L194.291 38.512C183.321 38.512 169.34 44.4991 154.443 56.0153C163.929 64.9374 172.634 74.6557 180.462 85.064C193.374 86.6345 206.129 89.3102 218.584 93.0624C218.813 91.4325 219.003 89.8118 219.166 88.2098C221.548 64.7099 216.65 47.6164 205.737 41.3148ZM144.551 64.3097C138.103 70.2614 132.055 76.6306 126.443 83.3765C132.389 82.995 138.427 82.8046 144.551 82.8046C150.727 82.8046 156.779 83.0143 162.707 83.3765C157.079 76.6293 151.015 70.2596 144.551 64.3097Z\"\n                  fill=\"#FF40E0\"\n                />\n              </g>\n              <mask\n                id=\"mask1_logo\"\n                style=\"mask-type:luminance\"\n                maskUnits=\"userSpaceOnUse\"\n                x=\"102\"\n                y=\"84\"\n                width=\"161\"\n                height=\"162\"\n              >\n                <path\n                  d=\"M235.282 84.827L102.261 112.259L129.693 245.28L262.714 217.848L235.282 84.827Z\"\n                  fill=\"white\"\n                />\n              </mask>\n              <g mask=\"url(#mask1_logo)\">\n                <path\n                  d=\"M136.863 129.916L213.258 141.224C220.669 142.322 222.495 152.179 215.967 155.856L187.592 171.843L184.135 204.227C183.339 211.678 173.564 213.901 169.624 207.526L129.021 141.831C125.503 136.14 130.245 128.936 136.863 129.916Z\"\n                  fill=\"#FF40E0\"\n                  stroke=\"#FF40E0\"\n                  stroke-width=\"0.817337\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                />\n              </g>\n            </g>\n            <defs>\n              <clipPath id=\"clip0_logo\">\n                <rect width=\"294\" height=\"294\" fill=\"white\" />\n              </clipPath>\n            </defs>\n          </svg>\n          <span\n            style={{\n              color: theme().titleText,\n              \"font-size\": `${HEADER_TITLE_FONT_SIZE_PX}px`,\n              \"font-weight\": \"600\",\n              \"letter-spacing\": \"-0.01em\",\n            }}\n          >\n            Design System\n          </span>\n        </div>\n        <div\n          style={{\n            display: \"flex\",\n            \"align-items\": \"center\",\n            gap: `${HEADER_BUTTONS_GAP_PX}px`,\n            flex: \"1\",\n            \"max-width\": \"400px\",\n          }}\n        >\n          <input\n            type=\"text\"\n            placeholder=\"Search states…\"\n            value={searchQuery()}\n            onInput={(event) => setSearchQuery(event.currentTarget.value)}\n            style={{\n              flex: \"1\",\n              padding: \"6px 12px\",\n              \"border-radius\": \"6px\",\n              border: `1px solid ${theme().cardBorder}`,\n              \"background-color\": theme().cardBackground,\n              color: theme().titleText,\n              \"font-size\": \"13px\",\n              \"font-family\": \"inherit\",\n              outline: \"none\",\n              transition: `all ${TRANSITION_DURATION}`,\n            }}\n          />\n        </div>\n        <div\n          style={{\n            display: \"flex\",\n            \"align-items\": \"center\",\n            gap: `${HEADER_BUTTONS_GAP_PX}px`,\n          }}\n        >\n          <button\n            onClick={() => setIsScrambled((prev) => !prev)}\n            style={{\n              ...createToggleButtonStyle(theme()),\n              \"background-color\": isScrambled()\n                ? \"rgba(215, 95, 203, 0.2)\"\n                : theme().toggleBackground,\n              \"border-color\": isScrambled()\n                ? \"rgba(215, 95, 203, 0.5)\"\n                : theme().toggleBorder,\n            }}\n          >\n            {isScrambled() ? \"✓ Scramble\" : \"Scramble\"}\n          </button>\n          <button\n            onClick={handleRefresh}\n            style={createToggleButtonStyle(theme())}\n          >\n            ↻ Refresh\n          </button>\n          <button\n            onClick={handleToggleTheme}\n            style={createToggleButtonStyle(theme())}\n          >\n            {isDarkMode() ? \"Dark\" : \"Light\"}\n          </button>\n        </div>\n      </div>\n\n      <Show when={!isRefreshing()}>\n        {/* Starred Section */}\n        <Show when={starredStates().length > 0}>\n          <div style={{ padding: `${GAP_PX}px 24px` }}>\n            <span\n              style={{\n                ...sectionTitleStyle(),\n                color: \"rgba(250, 204, 21, 0.8)\",\n              }}\n            >\n              ★ Starred ({starredStates().length})\n            </span>\n            <div style={gridStyle()}>\n              <For each={starredStates()}>\n                {(state) => (\n                  <StateCard\n                    state={state}\n                    theme={theme()}\n                    getBounds={() => getBoundsForCell(state.id)}\n                    registerCell={(element) => registerCell(state.id, element)}\n                    onRefresh={createRefreshHandler(state.id)}\n                    getTargetDisplayText={() => getTargetDisplayText(state)}\n                    isStarred={isStarred(state.id)}\n                    onToggleStar={() => handleToggleStar(state.id)}\n                    isScrambled={isScrambled()}\n                  />\n                )}\n              </For>\n            </div>\n          </div>\n        </Show>\n\n        {/* Flows Section */}\n        <Show when={flowStates().length > 0}>\n          <div style={{ padding: `${GAP_PX}px 24px` }}>\n            <span style={sectionTitleStyle()}>Flows</span>\n            <div style={gridStyle()}>\n              <For each={flowStates()}>\n                {(state) => (\n                  <StateCard\n                    state={state}\n                    theme={theme()}\n                    getBounds={() => getBoundsForCell(state.id)}\n                    registerCell={(element) => registerCell(state.id, element)}\n                    onRefresh={createRefreshHandler(state.id)}\n                    getTargetDisplayText={() => getTargetDisplayText(state)}\n                    isStarred={isStarred(state.id)}\n                    onToggleStar={() => handleToggleStar(state.id)}\n                    isScrambled={isScrambled()}\n                  />\n                )}\n              </For>\n            </div>\n          </div>\n        </Show>\n\n        {/* Selection Label Section */}\n        <Show when={labelStates().length > 0}>\n          <div style={{ padding: `${GAP_PX}px 24px` }}>\n            <span style={sectionTitleStyle()}>Selection Label</span>\n            <div style={gridStyle()}>\n              <For each={labelStates()}>\n                {(state) => (\n                  <StateCard\n                    state={state}\n                    theme={theme()}\n                    getBounds={() => getBoundsForCell(state.id)}\n                    registerCell={(element) => registerCell(state.id, element)}\n                    onRefresh={createRefreshHandler(state.id)}\n                    getTargetDisplayText={() => getTargetDisplayText(state)}\n                    isStarred={isStarred(state.id)}\n                    onToggleStar={() => handleToggleStar(state.id)}\n                    isScrambled={isScrambled()}\n                  />\n                )}\n              </For>\n            </div>\n          </div>\n        </Show>\n\n        {/* Context Menu Section */}\n        <Show when={contextMenuStates().length > 0}>\n          <div style={{ padding: `${GAP_PX}px 24px` }}>\n            <span style={sectionTitleStyle()}>Context Menu (Right-Click)</span>\n            <div style={gridStyle()}>\n              <For each={contextMenuStates()}>\n                {(state) => (\n                  <StateCard\n                    state={state}\n                    theme={theme()}\n                    getBounds={() => getBoundsForCell(state.id)}\n                    registerCell={(element) => registerCell(state.id, element)}\n                    onRefresh={createRefreshHandler(state.id)}\n                    getTargetDisplayText={() => getTargetDisplayText(state)}\n                    isStarred={isStarred(state.id)}\n                    onToggleStar={() => handleToggleStar(state.id)}\n                    isScrambled={isScrambled()}\n                  />\n                )}\n              </For>\n            </div>\n          </div>\n        </Show>\n\n        {/* Toolbar Section */}\n        <Show when={toolbarStates().length > 0}>\n          <div style={{ padding: `${GAP_PX}px 24px` }}>\n            <span style={sectionTitleStyle()}>Toolbar</span>\n            <div style={gridStyle()}>\n              <For each={toolbarStates()}>\n                {(state) => (\n                  <StateCard\n                    state={state}\n                    theme={theme()}\n                    getBounds={() => getBoundsForCell(state.id)}\n                    registerCell={(element) => registerCell(state.id, element)}\n                    onRefresh={createRefreshHandler(state.id)}\n                    getTargetDisplayText={() => getTargetDisplayText(state)}\n                    isStarred={isStarred(state.id)}\n                    onToggleStar={() => handleToggleStar(state.id)}\n                    isScrambled={isScrambled()}\n                  />\n                )}\n              </For>\n            </div>\n          </div>\n        </Show>\n\n        {/* History Dropdown Section */}\n        <Show when={historyDropdownStates().length > 0}>\n          <div style={{ padding: `${GAP_PX}px 24px` }}>\n            <span style={sectionTitleStyle()}>History Dropdown</span>\n            <div style={gridStyle()}>\n              <For each={historyDropdownStates()}>\n                {(state) => (\n                  <StateCard\n                    state={state}\n                    theme={theme()}\n                    getBounds={() => getBoundsForCell(state.id)}\n                    registerCell={(element) => registerCell(state.id, element)}\n                    onRefresh={createRefreshHandler(state.id)}\n                    getTargetDisplayText={() => getTargetDisplayText(state)}\n                    isStarred={isStarred(state.id)}\n                    onToggleStar={() => handleToggleStar(state.id)}\n                    isScrambled={isScrambled()}\n                  />\n                )}\n              </For>\n            </div>\n          </div>\n        </Show>\n\n        {/* Agent States Section */}\n        <Show when={agentLabelStates().length > 0}>\n          <div style={{ padding: `${GAP_PX}px 24px` }}>\n            <span style={sectionTitleStyle()}>Agent States</span>\n            <div style={gridStyle()}>\n              <For each={agentLabelStates()}>\n                {(state) => (\n                  <StateCard\n                    state={state}\n                    theme={theme()}\n                    getBounds={() => getBoundsForCell(state.id)}\n                    registerCell={(element) => registerCell(state.id, element)}\n                    onRefresh={createRefreshHandler(state.id)}\n                    getTargetDisplayText={() => getTargetDisplayText(state)}\n                    isStarred={isStarred(state.id)}\n                    onToggleStar={() => handleToggleStar(state.id)}\n                    isScrambled={isScrambled()}\n                  />\n                )}\n              </For>\n            </div>\n          </div>\n        </Show>\n\n        {/* No Results */}\n        <Show\n          when={\n            searchQuery().trim() &&\n            starredStates().length +\n              flowStates().length +\n              labelStates().length +\n              contextMenuStates().length +\n              toolbarStates().length +\n              historyDropdownStates().length +\n              agentLabelStates().length ===\n              0\n          }\n        >\n          <div style={{ padding: \"48px 24px\", \"text-align\": \"center\" }}>\n            <span\n              style={{ color: theme().descriptionText, \"font-size\": \"14px\" }}\n            >\n              No states match \"{searchQuery()}\"\n            </span>\n          </div>\n        </Show>\n      </Show>\n\n      {/* FPS Meter */}\n      <FpsMeter theme={theme()} />\n    </div>\n  );\n};\n\nexport interface DesignSystemPreviewOptions {\n  onDispose?: () => void;\n}\n\nexport const renderDesignSystemPreview = (\n  container: HTMLElement,\n  options?: DesignSystemPreviewOptions,\n): (() => void) => {\n  const shadowHost = document.createElement(\"div\");\n  shadowHost.setAttribute(\"data-react-grab-design-system\", \"true\");\n  shadowHost.style.position = \"relative\";\n  shadowHost.style.width = \"100%\";\n  shadowHost.style.minHeight = \"100vh\";\n\n  const shadowRoot = shadowHost.attachShadow({ mode: \"open\" });\n\n  if (cssText) {\n    const styleElement = document.createElement(\"style\");\n    styleElement.textContent = cssText as string;\n    shadowRoot.appendChild(styleElement);\n  }\n\n  const fontLink = document.createElement(\"link\");\n  fontLink.rel = \"stylesheet\";\n  fontLink.href =\n    \"https://fonts.googleapis.com/css2?family=Geist:wght@500&display=swap\";\n  shadowRoot.appendChild(fontLink);\n\n  const renderRoot = document.createElement(\"div\");\n  renderRoot.style.width = \"100%\";\n  shadowRoot.appendChild(renderRoot);\n\n  container.appendChild(shadowHost);\n\n  const dispose = render(() => <DesignSystemGrid />, renderRoot);\n\n  return () => {\n    dispose();\n    container.removeChild(shadowHost);\n    options?.onDispose?.();\n  };\n};\n"
  },
  {
    "path": "packages/design-system/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"lib\": [\"ESNext\", \"DOM\", \"DOM.Iterable\"],\n    \"jsx\": \"preserve\",\n    \"jsxImportSource\": \"solid-js\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/design-system/tsup.config.ts",
    "content": "import { defineConfig, type Options } from \"tsup\";\nimport babel from \"esbuild-plugin-babel\";\n\nconst options: Options = {\n  clean: true,\n  dts: true,\n  entry: [\"./src/index.tsx\"],\n  env: {\n    NODE_ENV: process.env.NODE_ENV ?? \"development\",\n  },\n  format: [\"cjs\", \"esm\"],\n  loader: {\n    \".css\": \"text\",\n  },\n  minify: process.env.NODE_ENV === \"production\",\n  noExternal: [\"solid-js\", /^react-grab\\/src/, \"react-grab/dist/styles.css\"],\n  outDir: \"./dist\",\n  platform: \"browser\",\n  sourcemap: false,\n  splitting: false,\n  target: \"esnext\",\n  treeshake: true,\n  esbuildPlugins: [\n    // eslint-disable-next-line @typescript-eslint/no-unsafe-call -- babel is not typed\n    babel({\n      filter: /\\.(tsx|jsx)$/,\n      config: {\n        presets: [\n          [\"@babel/preset-typescript\", { onlyRemoveTypeImports: true }],\n          \"babel-preset-solid\",\n        ],\n      },\n    }),\n  ],\n};\n\nexport default defineConfig(options);\n"
  },
  {
    "path": "packages/e2e-playground/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Grab E2E Playground</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/e2e-playground/package.json",
    "content": "{\n  \"name\": \"@react-grab/e2e-playground\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"vite build\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"clsx\": \"^2.1.1\",\n    \"react\": \"19.1.2\",\n    \"react-dom\": \"19.1.2\",\n    \"react-grab\": \"workspace:*\",\n    \"tailwind-merge\": \"^2.6.0\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/vite\": \"4.0.0-beta.8\",\n    \"@types/react\": \"^19.0.4\",\n    \"@types/react-dom\": \"^19.0.2\",\n    \"@vitejs/plugin-react\": \"^4.3.4\",\n    \"tailwindcss\": \"4.0.0-beta.8\",\n    \"typescript\": \"^5.7.3\",\n    \"vite\": \"^6.0.2\"\n  }\n}\n"
  },
  {
    "path": "packages/e2e-playground/src/App.tsx",
    "content": "import { useState, useRef, useEffect } from \"react\";\n\ninterface Todo {\n  id: number;\n  title: string;\n}\n\nconst TodoItem = ({ todo }: { todo: Todo }) => {\n  return (\n    <li>\n      <span>{todo.title}</span>\n    </li>\n  );\n};\n\nconst TodoList = () => {\n  const todos: Todo[] = [\n    { id: 1, title: \"Buy groceries\" },\n    { id: 2, title: \"Walk the dog\" },\n    { id: 3, title: \"Read a book\" },\n    { id: 4, title: \"Write code\" },\n    { id: 5, title: \"Exercise\" },\n    { id: 6, title: \"Call mom\" },\n    { id: 7, title: \"Write tests\" },\n  ];\n\n  return (\n    <div className=\"p-4 border rounded-lg\" data-testid=\"todo-list\">\n      <h1 className=\"text-xl font-bold mb-4\">Todo List</h1>\n      <ul className=\"space-y-2\">\n        {todos.map((todo) => (\n          <TodoItem key={todo.id} todo={todo} />\n        ))}\n      </ul>\n    </div>\n  );\n};\n\nconst NestedCard = ({\n  title,\n  children,\n}: {\n  title: string;\n  children: React.ReactNode;\n}) => {\n  return (\n    <div className=\"border rounded-lg p-4 bg-gray-50\" data-testid=\"nested-card\">\n      <h3 className=\"font-semibold mb-2\" data-testid=\"card-title\">\n        {title}\n      </h3>\n      <div className=\"pl-4\" data-testid=\"card-content\">\n        {children}\n      </div>\n    </div>\n  );\n};\n\nconst DeeplyNested = () => {\n  return (\n    <NestedCard title=\"Outer Card\">\n      <NestedCard title=\"Middle Card\">\n        <NestedCard title=\"Inner Card\">\n          <p data-testid=\"deeply-nested-text\">This is deeply nested content</p>\n          <button\n            className=\"bg-blue-500 text-white px-2 py-1 rounded text-sm\"\n            data-testid=\"nested-button\"\n          >\n            Nested Button\n          </button>\n        </NestedCard>\n      </NestedCard>\n    </NestedCard>\n  );\n};\n\nconst FormSection = () => {\n  const [inputValue, setInputValue] = useState(\"\");\n  const [textareaValue, setTextareaValue] = useState(\"\");\n\n  return (\n    <section className=\"border rounded-lg p-4\" data-testid=\"form-section\">\n      <h2 className=\"text-lg font-bold mb-4\">Form Elements</h2>\n      <div className=\"space-y-4\">\n        <div>\n          <label\n            htmlFor=\"test-input\"\n            className=\"block text-sm font-medium mb-1\"\n          >\n            Text Input\n          </label>\n          <input\n            id=\"test-input\"\n            type=\"text\"\n            value={inputValue}\n            onChange={(event) => setInputValue(event.target.value)}\n            className=\"border rounded px-3 py-2 w-full\"\n            placeholder=\"Type something...\"\n            data-testid=\"test-input\"\n          />\n        </div>\n        <div>\n          <label\n            htmlFor=\"test-textarea\"\n            className=\"block text-sm font-medium mb-1\"\n          >\n            Textarea\n          </label>\n          <textarea\n            id=\"test-textarea\"\n            value={textareaValue}\n            onChange={(event) => setTextareaValue(event.target.value)}\n            className=\"border rounded px-3 py-2 w-full h-20\"\n            placeholder=\"Type a longer message...\"\n            data-testid=\"test-textarea\"\n          />\n        </div>\n        <div className=\"flex gap-2\">\n          <button\n            type=\"button\"\n            className=\"bg-green-500 text-white px-4 py-2 rounded\"\n            data-testid=\"submit-button\"\n          >\n            Submit\n          </button>\n          <button\n            type=\"button\"\n            className=\"bg-gray-500 text-white px-4 py-2 rounded\"\n            data-testid=\"cancel-button\"\n          >\n            Cancel\n          </button>\n        </div>\n      </div>\n    </section>\n  );\n};\n\nconst ScrollableSection = () => {\n  const items = Array.from({ length: 50 }, (_, i) => ({\n    id: i + 1,\n    title: `Scrollable Item ${i + 1}`,\n    description: `This is the description for item ${i + 1}. It contains some text to make it more realistic.`,\n  }));\n\n  return (\n    <section className=\"border rounded-lg p-4\" data-testid=\"scrollable-section\">\n      <h2 className=\"text-lg font-bold mb-4\">Scrollable Content</h2>\n      <div\n        className=\"h-64 overflow-y-auto border rounded\"\n        data-testid=\"scroll-container\"\n      >\n        <ul className=\"divide-y\">\n          {items.map((item) => (\n            <li\n              key={item.id}\n              className=\"p-3 hover:bg-gray-50\"\n              data-testid={`scroll-item-${item.id}`}\n            >\n              <div className=\"font-medium\">{item.title}</div>\n              <div className=\"text-sm text-gray-500\">{item.description}</div>\n            </li>\n          ))}\n        </ul>\n      </div>\n    </section>\n  );\n};\n\nconst DynamicElements = () => {\n  const [elements, setElements] = useState([\n    { id: 1, text: \"Dynamic Element 1\" },\n    { id: 2, text: \"Dynamic Element 2\" },\n    { id: 3, text: \"Dynamic Element 3\" },\n  ]);\n\n  const removeElement = (id: number) => {\n    setElements((prev) => prev.filter((element) => element.id !== id));\n  };\n\n  const addElement = () => {\n    setElements((prev) => {\n      const newId = Math.max(0, ...prev.map((element) => element.id)) + 1;\n      return [...prev, { id: newId, text: `Dynamic Element ${newId}` }];\n    });\n  };\n\n  return (\n    <section className=\"border rounded-lg p-4\" data-testid=\"dynamic-section\">\n      <h2 className=\"text-lg font-bold mb-4\">Dynamic Elements</h2>\n      <div className=\"space-y-2 mb-4\">\n        {elements.map((element) => (\n          <div\n            key={element.id}\n            className=\"flex items-center justify-between p-2 bg-gray-100 rounded\"\n            data-testid={`dynamic-element-${element.id}`}\n          >\n            <span>{element.text}</span>\n            <button\n              onClick={() => removeElement(element.id)}\n              className=\"text-red-500 hover:text-red-700 px-2\"\n              data-testid={`remove-element-${element.id}`}\n            >\n              Remove\n            </button>\n          </div>\n        ))}\n      </div>\n      <button\n        onClick={addElement}\n        className=\"bg-blue-500 text-white px-4 py-2 rounded\"\n        data-testid=\"add-element-button\"\n      >\n        Add Element\n      </button>\n    </section>\n  );\n};\n\nconst EdgeElements = () => {\n  return (\n    <>\n      <div\n        className=\"fixed top-0 left-0 bg-red-500 text-white px-2 py-1 text-xs z-50\"\n        data-testid=\"edge-top-left\"\n      >\n        Top Left\n      </div>\n      <div\n        className=\"fixed top-0 right-0 bg-green-500 text-white px-2 py-1 text-xs z-50\"\n        data-testid=\"edge-top-right\"\n      >\n        Top Right\n      </div>\n      <div\n        className=\"fixed bottom-0 left-0 bg-blue-500 text-white px-2 py-1 text-xs z-50\"\n        data-testid=\"edge-bottom-left\"\n      >\n        Bottom Left\n      </div>\n      <div\n        className=\"fixed bottom-0 right-0 bg-purple-500 text-white px-2 py-1 text-xs z-50\"\n        data-testid=\"edge-bottom-right\"\n      >\n        Bottom Right\n      </div>\n    </>\n  );\n};\n\nconst VariousElements = () => {\n  return (\n    <section className=\"border rounded-lg p-4\" data-testid=\"various-section\">\n      <h2 className=\"text-lg font-bold mb-4\">Various Element Types</h2>\n      <div className=\"space-y-4\">\n        <div className=\"flex gap-4 items-center\">\n          <span className=\"text-sm\" data-testid=\"span-element\">\n            Span Element\n          </span>\n          <strong data-testid=\"strong-element\">Strong Element</strong>\n          <em data-testid=\"em-element\">Emphasized</em>\n          <code className=\"bg-gray-100 px-1 rounded\" data-testid=\"code-element\">\n            code element\n          </code>\n        </div>\n\n        <div className=\"flex gap-2\">\n          <a\n            href=\"#\"\n            className=\"text-blue-500 underline\"\n            data-testid=\"link-element\"\n          >\n            Link Element\n          </a>\n          <button\n            className=\"border px-2 py-1 rounded\"\n            data-testid=\"plain-button\"\n          >\n            Plain Button\n          </button>\n        </div>\n\n        <table\n          className=\"border-collapse border w-full\"\n          data-testid=\"table-element\"\n        >\n          <thead>\n            <tr className=\"bg-gray-100\">\n              <th className=\"border p-2\" data-testid=\"th-1\">\n                Header 1\n              </th>\n              <th className=\"border p-2\" data-testid=\"th-2\">\n                Header 2\n              </th>\n              <th className=\"border p-2\" data-testid=\"th-3\">\n                Header 3\n              </th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td className=\"border p-2\" data-testid=\"td-1-1\">\n                Cell 1-1\n              </td>\n              <td className=\"border p-2\" data-testid=\"td-1-2\">\n                Cell 1-2\n              </td>\n              <td className=\"border p-2\" data-testid=\"td-1-3\">\n                Cell 1-3\n              </td>\n            </tr>\n            <tr>\n              <td className=\"border p-2\" data-testid=\"td-2-1\">\n                Cell 2-1\n              </td>\n              <td className=\"border p-2\" data-testid=\"td-2-2\">\n                Cell 2-2\n              </td>\n              <td className=\"border p-2\" data-testid=\"td-2-3\">\n                Cell 2-3\n              </td>\n            </tr>\n          </tbody>\n        </table>\n\n        <div className=\"flex gap-2\">\n          <img\n            src=\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='50' height='50'%3E%3Crect fill='%23ddd' width='50' height='50'/%3E%3C/svg%3E\"\n            alt=\"Placeholder\"\n            className=\"border\"\n            data-testid=\"img-element\"\n          />\n          <div\n            className=\"w-12 h-12 bg-gradient-to-r from-blue-500 to-purple-500 rounded\"\n            data-testid=\"gradient-div\"\n          />\n        </div>\n\n        <article\n          className=\"p-3 bg-gray-50 rounded\"\n          data-testid=\"article-element\"\n        >\n          <header data-testid=\"article-header\">\n            <h4 className=\"font-semibold\">Article Title</h4>\n          </header>\n          <p className=\"text-sm text-gray-600\" data-testid=\"article-content\">\n            Article content goes here. This is a semantic article element.\n          </p>\n          <footer\n            className=\"text-xs text-gray-400 mt-2\"\n            data-testid=\"article-footer\"\n          >\n            Article Footer\n          </footer>\n        </article>\n      </div>\n    </section>\n  );\n};\n\nconst AnimatedElements = () => {\n  return (\n    <section className=\"border rounded-lg p-4\" data-testid=\"animated-section\">\n      <h2 className=\"text-lg font-bold mb-4\">Animated Elements</h2>\n      <div className=\"space-y-4\">\n        <div\n          className=\"w-8 h-8 bg-blue-500 rounded-full animate-pulse\"\n          data-testid=\"animated-pulse\"\n        />\n        <div\n          className=\"w-8 h-8 bg-green-500 rounded animate-spin\"\n          data-testid=\"animated-spin\"\n        />\n        <div\n          className=\"w-8 h-8 bg-red-500 rounded animate-bounce\"\n          data-testid=\"animated-bounce\"\n        />\n      </div>\n    </section>\n  );\n};\n\nconst ZeroDimensionElements = () => {\n  return (\n    <section\n      className=\"border rounded-lg p-4\"\n      data-testid=\"zero-dimension-section\"\n    >\n      <h2 className=\"text-lg font-bold mb-4\">Edge Case Elements</h2>\n      <div className=\"space-y-2\">\n        <div className=\"w-0 h-0\" data-testid=\"zero-size-element\" />\n        <div className=\"invisible\" data-testid=\"invisible-element\">\n          Invisible Element\n        </div>\n        <div className=\"opacity-0\" data-testid=\"transparent-element\">\n          Transparent Element\n        </div>\n        <div className=\"overflow-hidden w-20\">\n          <span className=\"whitespace-nowrap\" data-testid=\"overflow-element\">\n            This text is very long and will overflow its container\n          </span>\n        </div>\n      </div>\n    </section>\n  );\n};\n\nconst DropdownSection = () => {\n  const [isOpen, setIsOpen] = useState(false);\n  const dropdownRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const handleClickOutside = (event: MouseEvent | PointerEvent) => {\n      if (\n        dropdownRef.current &&\n        !dropdownRef.current.contains(event.target as Node)\n      ) {\n        setIsOpen(false);\n      }\n    };\n\n    if (isOpen) {\n      document.addEventListener(\"mousedown\", handleClickOutside);\n      document.addEventListener(\"pointerdown\", handleClickOutside);\n    }\n\n    return () => {\n      document.removeEventListener(\"mousedown\", handleClickOutside);\n      document.removeEventListener(\"pointerdown\", handleClickOutside);\n    };\n  }, [isOpen]);\n\n  return (\n    <section className=\"border rounded-lg p-4\" data-testid=\"dropdown-section\">\n      <h2 className=\"text-lg font-bold mb-4\">Dropdown Test</h2>\n      <div className=\"relative\" ref={dropdownRef}>\n        <button\n          onClick={() => setIsOpen(!isOpen)}\n          className=\"bg-blue-500 text-white px-4 py-2 rounded\"\n          data-testid=\"dropdown-trigger\"\n        >\n          {isOpen ? \"Close Dropdown\" : \"Open Dropdown\"}\n        </button>\n        {isOpen && (\n          <div\n            className=\"absolute top-full left-0 mt-2 w-48 bg-white border rounded-lg shadow-lg z-50\"\n            data-testid=\"dropdown-menu\"\n          >\n            <button\n              className=\"w-full text-left px-4 py-2 hover:bg-gray-100\"\n              data-testid=\"dropdown-item-1\"\n            >\n              Option 1\n            </button>\n            <button\n              className=\"w-full text-left px-4 py-2 hover:bg-gray-100\"\n              data-testid=\"dropdown-item-2\"\n            >\n              Option 2\n            </button>\n            <button\n              className=\"w-full text-left px-4 py-2 hover:bg-gray-100\"\n              data-testid=\"dropdown-item-3\"\n            >\n              Option 3\n            </button>\n          </div>\n        )}\n      </div>\n    </section>\n  );\n};\n\nconst ModalDialogSection = () => {\n  const [isOpen, setIsOpen] = useState(false);\n  const [dismissCount, setDismissCount] = useState(0);\n  const [dismissReason, setDismissReason] = useState<string | null>(null);\n  const dialogRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (!isOpen) return;\n\n    const handlePointerDown = (event: PointerEvent) => {\n      if (\n        dialogRef.current &&\n        !dialogRef.current.contains(event.target as Node)\n      ) {\n        setDismissCount((previous) => previous + 1);\n        setDismissReason(\"pointerdown outside (capture)\");\n        setIsOpen(false);\n      }\n    };\n\n    window.addEventListener(\"pointerdown\", handlePointerDown, {\n      capture: true,\n    });\n\n    return () => {\n      window.removeEventListener(\"pointerdown\", handlePointerDown, {\n        capture: true,\n      });\n    };\n  }, [isOpen]);\n\n  return (\n    <section\n      className=\"border rounded-lg p-4\"\n      data-testid=\"modal-dialog-section\"\n    >\n      <h2 className=\"text-lg font-bold mb-4\">\n        Modal Dialog (pointerdown dismiss)\n      </h2>\n      <button\n        onClick={() => setIsOpen(true)}\n        className=\"bg-indigo-500 text-white px-4 py-2 rounded\"\n        data-testid=\"modal-trigger\"\n      >\n        Open Modal\n      </button>\n      <div className=\"mt-2 text-sm text-gray-600\" data-testid=\"dismiss-info\">\n        Dismiss count: {dismissCount}\n        {dismissReason && ` (last: ${dismissReason})`}\n      </div>\n      {isOpen && (\n        <div className=\"fixed inset-0 z-[9999] flex items-center justify-center\">\n          <div\n            className=\"fixed inset-0 bg-black/50\"\n            data-testid=\"modal-backdrop\"\n          />\n          <div\n            ref={dialogRef}\n            className=\"relative bg-white rounded-lg shadow-xl p-6 w-96 z-10\"\n            data-testid=\"modal-content\"\n          >\n            <h3 className=\"text-lg font-bold mb-2\">Modal Title</h3>\n            <p className=\"mb-4\">\n              Click inside here while React Grab is active. The modal should NOT\n              close.\n            </p>\n            <button\n              className=\"bg-blue-500 text-white px-3 py-1 rounded\"\n              data-testid=\"modal-inner-button\"\n            >\n              Button Inside Modal\n            </button>\n            <button\n              onClick={() => setIsOpen(false)}\n              className=\"ml-2 bg-gray-300 px-3 py-1 rounded\"\n              data-testid=\"modal-close-button\"\n            >\n              Close\n            </button>\n          </div>\n        </div>\n      )}\n    </section>\n  );\n};\n\nconst PointerUpModalSection = () => {\n  const [isOpen, setIsOpen] = useState(false);\n  const [dismissCount, setDismissCount] = useState(0);\n  const dialogRef = useRef<HTMLDivElement>(null);\n  const pointerDownTargetRef = useRef<EventTarget | null>(null);\n\n  useEffect(() => {\n    if (!isOpen) return;\n\n    const handlePointerDown = (event: PointerEvent) => {\n      pointerDownTargetRef.current = event.composedPath?.()[0] ?? event.target;\n    };\n\n    const handlePointerUp = (event: PointerEvent) => {\n      const downTarget = pointerDownTargetRef.current;\n      pointerDownTargetRef.current = null;\n      if (!downTarget) return;\n\n      if (\n        dialogRef.current &&\n        !dialogRef.current.contains(downTarget as Node)\n      ) {\n        setDismissCount((previous) => previous + 1);\n        setIsOpen(false);\n      }\n    };\n\n    window.addEventListener(\"pointerdown\", handlePointerDown, {\n      capture: true,\n    });\n    window.addEventListener(\"pointerup\", handlePointerUp, { capture: true });\n\n    return () => {\n      window.removeEventListener(\"pointerdown\", handlePointerDown, {\n        capture: true,\n      });\n      window.removeEventListener(\"pointerup\", handlePointerUp, {\n        capture: true,\n      });\n    };\n  }, [isOpen]);\n\n  return (\n    <section\n      className=\"border rounded-lg p-4\"\n      data-testid=\"pointerup-modal-section\"\n    >\n      <h2 className=\"text-lg font-bold mb-4\">\n        Modal Dialog (pointerdown+pointerup dismiss, Headless UI style)\n      </h2>\n      <button\n        onClick={() => setIsOpen(true)}\n        className=\"bg-teal-500 text-white px-4 py-2 rounded\"\n        data-testid=\"pointerup-modal-trigger\"\n      >\n        Open Modal\n      </button>\n      <div\n        className=\"mt-2 text-sm text-gray-600\"\n        data-testid=\"pointerup-dismiss-info\"\n      >\n        Dismiss count: {dismissCount}\n      </div>\n      {isOpen && (\n        <div className=\"fixed inset-0 z-[9999] flex items-center justify-center\">\n          <div className=\"fixed inset-0 bg-black/50\" />\n          <div\n            ref={dialogRef}\n            className=\"relative bg-white rounded-lg shadow-xl p-6 w-96 z-10\"\n            data-testid=\"pointerup-modal-content\"\n          >\n            <h3 className=\"text-lg font-bold mb-2\">Headless UI Style Modal</h3>\n            <p className=\"mb-4\">\n              Uses pointerdown+pointerup pair for outside detection.\n            </p>\n            <button\n              className=\"bg-blue-500 text-white px-3 py-1 rounded\"\n              data-testid=\"pointerup-modal-inner-button\"\n            >\n              Button Inside Modal\n            </button>\n            <button\n              onClick={() => setIsOpen(false)}\n              className=\"ml-2 bg-gray-300 px-3 py-1 rounded\"\n              data-testid=\"pointerup-modal-close-button\"\n            >\n              Close\n            </button>\n          </div>\n        </div>\n      )}\n    </section>\n  );\n};\n\nconst HiddenToggleSection = () => {\n  const [isVisible, setIsVisible] = useState(true);\n  const elementRef = useRef<HTMLDivElement>(null);\n\n  return (\n    <section\n      className=\"border rounded-lg p-4\"\n      data-testid=\"hidden-toggle-section\"\n    >\n      <h2 className=\"text-lg font-bold mb-4\">Visibility Toggle</h2>\n      <button\n        onClick={() => setIsVisible(!isVisible)}\n        className=\"bg-gray-500 text-white px-4 py-2 rounded mb-4\"\n        data-testid=\"toggle-visibility-button\"\n      >\n        {isVisible ? \"Hide Element\" : \"Show Element\"}\n      </button>\n      {isVisible && (\n        <div\n          ref={elementRef}\n          className=\"p-4 bg-yellow-100 rounded\"\n          data-testid=\"toggleable-element\"\n        >\n          This element can be hidden\n        </div>\n      )}\n    </section>\n  );\n};\n\nexport default function App() {\n  return (\n    <div className=\"min-h-[200vh] p-12 flex flex-col gap-8 pb-32\">\n      <EdgeElements />\n\n      <header className=\"mb-4\">\n        <h1 className=\"text-2xl font-bold\" data-testid=\"main-title\">\n          React Grab E2E Test Page\n        </h1>\n        <p className=\"text-gray-600\" data-testid=\"main-description\">\n          Comprehensive test page for E2E testing\n        </p>\n      </header>\n\n      <TodoList />\n\n      <DeeplyNested />\n\n      <FormSection />\n\n      <ScrollableSection />\n\n      <DynamicElements />\n\n      <VariousElements />\n\n      <AnimatedElements />\n\n      <ZeroDimensionElements />\n\n      <DropdownSection />\n\n      <ModalDialogSection />\n\n      <PointerUpModalSection />\n\n      <HiddenToggleSection />\n\n      <div\n        className=\"h-96 flex items-center justify-center bg-gray-100 rounded-lg\"\n        data-testid=\"spacer-section\"\n      >\n        <p className=\"text-gray-400\">Spacer for scroll testing</p>\n      </div>\n\n      <footer\n        className=\"text-center text-gray-400 text-sm\"\n        data-testid=\"footer\"\n      >\n        End of test page\n      </footer>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/e2e-playground/src/index.css",
    "content": "@import \"tailwindcss\";\n"
  },
  {
    "path": "packages/e2e-playground/src/main.tsx",
    "content": "import { StrictMode } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { init } from \"react-grab\";\nimport \"./index.css\";\nimport App from \"./App.tsx\";\n\ndeclare global {\n  interface Window {\n    initReactGrab: typeof init;\n  }\n}\n\nwindow.initReactGrab = init;\n\ncreateRoot(document.getElementById(\"root\")!).render(\n  <StrictMode>\n    <App />\n  </StrictMode>,\n);\n"
  },
  {
    "path": "packages/e2e-playground/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"esModuleInterop\": true,\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/e2e-playground/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"allowSyntheticDefaultImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "packages/e2e-playground/vite.config.ts",
    "content": "import tailwindcss from \"@tailwindcss/vite\";\nimport react from \"@vitejs/plugin-react\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({\n  plugins: [react(), tailwindcss()],\n  server: {\n    port: 5175,\n    strictPort: true,\n  },\n  resolve: {\n    alias: {\n      \"@\": \"/src\",\n    },\n  },\n});\n"
  },
  {
    "path": "packages/grab/CHANGELOG.md",
    "content": "# grab\n\n## 0.1.28\n\n### Patch Changes\n\n- fix\n- Updated dependencies\n  - @react-grab/cli@0.1.28\n\n## 0.1.27\n\n### Patch Changes\n\n- fix: install instructions\n- Updated dependencies\n  - @react-grab/cli@0.1.27\n\n## 0.1.26\n\n### Patch Changes\n\n- fix: minor tweaks\n- Updated dependencies\n  - @react-grab/cli@0.1.26\n\n## 0.1.25\n\n### Patch Changes\n\n- fix: primtiives\n- Updated dependencies\n  - @react-grab/cli@0.1.25\n\n## 0.1.24\n\n### Patch Changes\n\n- primitives\n- Updated dependencies\n  - @react-grab/cli@0.1.24\n\n## 0.1.23\n\n### Patch Changes\n\n- fix: npx command\n- Updated dependencies\n  - @react-grab/cli@0.1.23\n\n## 0.1.22\n\n### Patch Changes\n\n- fix: freezing\n- Updated dependencies\n  - @react-grab/cli@0.1.22\n\n## 0.1.21\n\n### Patch Changes\n\n- fix: up and down selection\n- Updated dependencies\n  - @react-grab/cli@0.1.21\n\n## 0.1.20\n\n### Patch Changes\n\n- fix: selection performacne\n- Updated dependencies\n  - @react-grab/cli@0.1.20\n\n## 0.1.19\n\n### Patch Changes\n\n- fix: gsap\n- Updated dependencies\n  - @react-grab/cli@0.1.19\n\n## 0.1.18\n\n### Patch Changes\n\n- fix: minor issues\n- Updated dependencies\n  - @react-grab/cli@0.1.18\n\n## 0.1.17\n\n### Patch Changes\n\n- fix: mcp\n- Updated dependencies\n  - @react-grab/cli@0.1.17\n\n## 0.1.16\n\n### Patch Changes\n\n- fix: environment detection\n- Updated dependencies\n  - @react-grab/cli@0.1.16\n\n## 0.1.15\n\n### Patch Changes\n\n- fix: animations and ux\n- Updated dependencies\n  - @react-grab/cli@0.1.15\n\n## 0.1.14\n\n### Patch Changes\n\n- fix: improve recent UX\n- Updated dependencies\n  - @react-grab/cli@0.1.14\n\n## 0.1.13\n\n### Patch Changes\n\n- fix MCP client injection\n- Updated dependencies\n  - @react-grab/cli@0.1.13\n\n## 0.1.12\n\n### Patch Changes\n\n- feat: MCP\n- Updated dependencies\n  - @react-grab/cli@0.1.12\n\n## 0.1.11\n\n### Patch Changes\n\n- fix: claude code provider\n- Updated dependencies\n  - @react-grab/cli@0.1.11\n\n## 0.1.10\n\n### Patch Changes\n\n- feat: cdn in cli\n- Updated dependencies\n  - @react-grab/cli@0.1.10\n\n## 0.1.9\n\n### Patch Changes\n\n- fix: startServer not exported in providers\n- Updated dependencies\n  - @react-grab/cli@0.1.9\n\n## 0.1.8\n\n### Patch Changes\n\n- fix: providers not detaching\n- Updated dependencies\n  - @react-grab/cli@0.1.8\n\n## 0.1.7\n\n### Patch Changes\n\n- fix: react freezing safety\n- Updated dependencies\n  - @react-grab/cli@0.1.7\n\n## 0.1.6\n\n### Patch Changes\n\n- fix: compat with React Scan\n- Updated dependencies\n  - @react-grab/cli@0.1.6\n\n## 0.1.5\n\n### Patch Changes\n\n- fix: fullscreen inset the root\n- Updated dependencies\n  - @react-grab/cli@0.1.5\n\n## 0.1.4\n\n### Patch Changes\n\n- fix: improve cli edge cases\n- Updated dependencies\n  - @react-grab/cli@0.1.4\n\n## 0.1.3\n\n### Patch Changes\n\n- fix: @react-grab/utils not publishing\n- Updated dependencies\n  - @react-grab/cli@0.1.3\n\n## 0.1.2\n\n### Patch Changes\n\n- fix: packages not being published\n- Updated dependencies\n  - @react-grab/cli@0.1.2\n\n## 0.1.1\n\n### Patch Changes\n\n- fix: clicking element after keyboard navigation\n- Updated dependencies\n  - @react-grab/cli@0.1.1\n\n## 0.1.0\n\n### Minor Changes\n\n- 81adb50: feat: browser\n\n### Patch Changes\n\n- 81adb50: fix: shell script\n- fb2b037: fix: cli\n- a3d5a94: fix: cli global install\n- 81adb50: feat: react support\n- 81adb50: fix: use matching CLI version for prerelease builds\n- 81adb50: fix: a11y\n- a5e7a6a: fix: optimize loading speed of cli\n- 90af3f6: fix: CLI hanging\n- 81adb50: fix: shell script\n- 78efee2: fix: cli\n- 074e593: fix: cli\n- 5cd3709: fix: decouple browser out from react-grab\n- 54c4867: ui improvements\n- Updated dependencies [81adb50]\n- Updated dependencies [fb2b037]\n- Updated dependencies [a3d5a94]\n- Updated dependencies [81adb50]\n- Updated dependencies [81adb50]\n- Updated dependencies [a5e7a6a]\n- Updated dependencies [90af3f6]\n- Updated dependencies [81adb50]\n- Updated dependencies [81adb50]\n- Updated dependencies [78efee2]\n- Updated dependencies [074e593]\n- Updated dependencies [5cd3709]\n- Updated dependencies [54c4867]\n  - @react-grab/cli@0.1.0\n\n## 0.1.0-beta.13\n\n### Patch Changes\n\n- ui improvements\n- Updated dependencies\n  - @react-grab/cli@0.1.0-beta.13\n\n## 0.1.0-beta.12\n\n### Patch Changes\n\n- fix: decouple browser out from react-grab\n- Updated dependencies\n  - @react-grab/cli@0.1.0-beta.12\n\n## 0.1.0-beta.11\n\n### Patch Changes\n\n- fix: cli global install\n- Updated dependencies\n  - @react-grab/cli@0.1.0-beta.11\n\n## 0.1.0-beta.10\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - @react-grab/cli@0.1.0-beta.10\n\n## 0.1.0-beta.9\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - @react-grab/cli@0.1.0-beta.9\n\n## 0.1.0-beta.8\n\n### Patch Changes\n\n- fix: cli\n\n## 0.1.0-beta.7\n\n### Patch Changes\n\n- fix: CLI hanging\n\n## 0.1.0-beta.6\n\n### Patch Changes\n\n- fix: optimize loading speed of cli\n\n## 0.1.0-beta.5\n\n### Patch Changes\n\n- fix: a11y\n\n## 0.1.0-beta.4\n\n### Patch Changes\n\n- feat: react support\n\n## 0.1.0-beta.3\n\n### Patch Changes\n\n- fix: use matching CLI version for prerelease builds\n\n## 0.1.0-beta.2\n\n### Patch Changes\n\n- fix: shell script\n\n## 0.1.0-beta.1\n\n### Patch Changes\n\n- fix: shell script\n\n## 0.1.0-beta.0\n\n### Minor Changes\n\n- feat: browser\n\n## 0.0.98\n\n### Patch Changes\n\n- feat: new state architecture and context menu\n\n## 0.0.97\n\n### Patch Changes\n\n- fix: sourcemap error\n\n## 0.0.96\n\n### Patch Changes\n\n- fix: fiber access timeout handling\n\n## 0.0.95\n\n### Patch Changes\n\n- fix: selecting buttons with disabled states\n\n## 0.0.94\n\n### Patch Changes\n\n- fix: browser crashing on selection bug\n\n## 0.0.93\n\n### Patch Changes\n\n- fix: copying not working\n\n## 0.0.92\n\n### Patch Changes\n\n- refactor: use state machines instead of signals\n\n## 0.0.91\n\n### Patch Changes\n\n- feat: dock\n\n## 0.0.90\n\n### Patch Changes\n\n- fix: check visual edit endpoint\n\n## 0.0.89\n\n### Patch Changes\n\n- fix: many bugfixes\n\n## 0.0.88\n\n### Patch Changes\n\n- fix: deprecation errors\n\n## 0.0.87\n\n### Patch Changes\n\n- feat: visual edits\n\n## 0.0.86\n\n### Patch Changes\n\n- fix: editing\n\n## 0.0.85\n\n### Patch Changes\n\n- fix: check versions on each provider\n\n## 0.0.84\n\n### Patch Changes\n\n- fix: migrate from cross-spawn to execa to fix deprecation issue\n\n## 0.0.83\n\n### Patch Changes\n\n- feat: timings during agent processing\n\n## 0.0.82\n\n### Patch Changes\n\n- fix: agent support\n\n## 0.0.81\n\n### Patch Changes\n\n- feat: codex and gemini support\n\n## 0.0.80\n\n### Patch Changes\n\n- fix: replies and undo\n\n## 0.0.79\n\n### Patch Changes\n\n- fix: claude code exit issue\n\n## 0.0.78\n\n### Patch Changes\n\n- fix: cancel animation\n\n## 0.0.77\n\n### Patch Changes\n\n- fix: new cli proxying\n"
  },
  {
    "path": "packages/grab/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Aiden Bai\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": "packages/grab/README.md",
    "content": "# <img src=\"https://github.com/aidenybai/react-grab/blob/main/.github/public/logo.png?raw=true\" width=\"60\" align=\"center\" /> Grab\n\n[![size](https://img.shields.io/bundlephobia/minzip/grab?label=gzip&style=flat&colorA=000000&colorB=000000)](https://bundlephobia.com/package/grab)\n[![version](https://img.shields.io/npm/v/grab?style=flat&colorA=000000&colorB=000000)](https://npmjs.com/package/grab)\n[![downloads](https://img.shields.io/npm/dt/grab.svg?style=flat&colorA=000000&colorB=000000)](https://npmjs.com/package/grab)\n\nSelect context for coding agents directly from your website\n\nHow? Point at any element and press **⌘C** (Mac) or **Ctrl+C** (Windows/Linux) to copy the file name, React component, and HTML source code.\n\nIt makes tools like Cursor, Claude Code, Copilot run up to [**3× faster**](https://react-grab.com/blog/intro) and more accurate.\n\n### [Try out a demo! →](https://react-grab.com)\n\n![React Grab Demo](https://github.com/aidenybai/react-grab/blob/main/packages/website/public/demo.gif?raw=true)\n\n## Install\n\nRun this command at your project root (where `next.config.ts` or `vite.config.ts` is located):\n\n```bash\nnpx -y grab@latest init\n```\n\n## Connect to MCP\n\n```bash\nnpx -y grab@latest add mcp\n```\n\n## Usage\n\nOnce installed, hover over any UI element in your browser and press:\n\n- **⌘C** (Cmd+C) on Mac\n- **Ctrl+C** on Windows/Linux\n\nThis copies the element's context (file name, React component, and HTML source code) to your clipboard ready to paste into your coding agent. For example:\n\n```js\n<a class=\"ml-auto inline-block text-sm\" href=\"#\">\n  Forgot your password?\n</a>\nin LoginForm at components/login-form.tsx:46:19\n```\n\n## Manual Installation\n\nIf you're using a React framework or build tool, view instructions below:\n\n#### Next.js (App router)\n\nAdd this inside of your `app/layout.tsx`:\n\n```jsx\nimport Script from \"next/script\";\n\nexport default function RootLayout({ children }) {\n  return (\n    <html>\n      <head>\n        {process.env.NODE_ENV === \"development\" && (\n          <Script\n            src=\"//unpkg.com/grab/dist/index.global.js\"\n            crossOrigin=\"anonymous\"\n            strategy=\"beforeInteractive\"\n          />\n        )}\n      </head>\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\n#### Next.js (Pages router)\n\nAdd this into your `pages/_document.tsx`:\n\n```jsx\nimport { Html, Head, Main, NextScript } from \"next/document\";\n\nexport default function Document() {\n  return (\n    <Html lang=\"en\">\n      <Head>\n        {process.env.NODE_ENV === \"development\" && (\n          <Script\n            src=\"//unpkg.com/grab/dist/index.global.js\"\n            crossOrigin=\"anonymous\"\n            strategy=\"beforeInteractive\"\n          />\n        )}\n      </Head>\n      <body>\n        <Main />\n        <NextScript />\n      </body>\n    </Html>\n  );\n}\n```\n\n#### Vite\n\nAdd this at the top of your main entry file (e.g., `src/main.tsx`):\n\n```tsx\nif (import.meta.env.DEV) {\n  import(\"grab\");\n}\n```\n\n#### Webpack\n\nFirst, install React Grab:\n\n```bash\nnpm install grab\n```\n\nThen add this at the top of your main entry file (e.g., `src/index.tsx` or `src/main.tsx`):\n\n```tsx\nif (process.env.NODE_ENV === \"development\") {\n  import(\"grab\");\n}\n```\n\n## Plugins\n\nUse plugins to extend React Grab's built-in UI with context menu actions, toolbar menu items, lifecycle hooks, and theme overrides. Plugins run within React Grab.\n\nRegister a plugin using the `registerPlugin` and `unregisterPlugin` exports:\n\n```js\nimport { registerPlugin } from \"grab\";\n\nregisterPlugin({\n  name: \"my-plugin\",\n  hooks: {\n    onElementSelect: (element) => {\n      console.log(\"Selected:\", element.tagName);\n    },\n  },\n});\n```\n\nIn React, register inside a `useEffect`:\n\n```jsx\nimport { registerPlugin, unregisterPlugin } from \"grab\";\n\nuseEffect(() => {\n  registerPlugin({\n    name: \"my-plugin\",\n    actions: [\n      {\n        id: \"my-action\",\n        label: \"My Action\",\n        shortcut: \"M\",\n        onAction: (context) => {\n          console.log(\"Action on:\", context.element);\n          context.hideContextMenu();\n        },\n      },\n    ],\n  });\n\n  return () => unregisterPlugin(\"my-plugin\");\n}, []);\n```\n\nActions use a `target` field to control where they appear. Omit `target` (or set `\"context-menu\"`) for the right-click menu, or set `\"toolbar\"` for the toolbar dropdown:\n\n```js\nactions: [\n  {\n    id: \"inspect\",\n    label: \"Inspect\",\n    shortcut: \"I\",\n    onAction: (ctx) => console.dir(ctx.element),\n  },\n  {\n    id: \"toggle-freeze\",\n    label: \"Freeze\",\n    target: \"toolbar\",\n    isActive: () => isFrozen,\n    onAction: () => toggleFreeze(),\n  },\n];\n```\n\nSee [`packages/react-grab/src/types.ts`](https://github.com/aidenybai/react-grab/blob/main/packages/react-grab/src/types.ts) for the full `Plugin`, `PluginHooks`, and `PluginConfig` interfaces.\n\n## Resources & Contributing Back\n\nWant to try it out? Check out [our demo](https://react-grab.com).\n\nLooking to contribute back? Check out the [Contributing Guide](https://github.com/aidenybai/react-grab/blob/main/CONTRIBUTING.md).\n\nWant to talk to the community? Hop in our [Discord](https://discord.com/invite/G7zxfUzkm7) and share your ideas and what you've built with React Grab.\n\nFind a bug? Head over to our [issue tracker](https://github.com/aidenybai/react-grab/issues) and we'll do our best to help. We love pull requests, too!\n\nWe expect all contributors to abide by the terms of our [Code of Conduct](https://github.com/aidenybai/react-grab/blob/main/.github/CODE_OF_CONDUCT.md).\n\n[**→ Start contributing on GitHub**](https://github.com/aidenybai/react-grab/blob/main/CONTRIBUTING.md)\n\n### License\n\nReact Grab is MIT-licensed open-source software.\n\n_Thank you to [Andrew Luetgers](https://github.com/andrewluetgers) for donating the `grab` npm package name._\n"
  },
  {
    "path": "packages/grab/bin/cli.js",
    "content": "#!/usr/bin/env node\nimport \"@react-grab/cli\";\n"
  },
  {
    "path": "packages/grab/package.json",
    "content": "{\n  \"name\": \"grab\",\n  \"version\": \"0.1.28\",\n  \"description\": \"Select context for coding agents directly from your website\",\n  \"keywords\": [\n    \"agent\",\n    \"context\",\n    \"grab\",\n    \"react\",\n    \"react-grab\"\n  ],\n  \"homepage\": \"https://react-grab.com\",\n  \"bugs\": {\n    \"url\": \"https://github.com/aidenybai/react-grab/issues\"\n  },\n  \"license\": \"MIT\",\n  \"author\": {\n    \"name\": \"Aiden Bai\",\n    \"email\": \"aiden@million.dev\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/aidenybai/react-grab.git\"\n  },\n  \"bin\": {\n    \"grab\": \"./bin/cli.js\"\n  },\n  \"files\": [\n    \"bin\",\n    \"dist\",\n    \"package.json\",\n    \"README.md\",\n    \"LICENSE\"\n  ],\n  \"type\": \"module\",\n  \"main\": \"dist/index.js\",\n  \"module\": \"dist/index.js\",\n  \"browser\": \"dist/index.global.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \"./package.json\": \"./package.json\",\n    \".\": {\n      \"import\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"require\": {\n        \"types\": \"./dist/index.d.cts\",\n        \"default\": \"./dist/index.cjs\"\n      }\n    },\n    \"./core\": {\n      \"import\": {\n        \"types\": \"./dist/core/index.d.ts\",\n        \"default\": \"./dist/core/index.js\"\n      },\n      \"require\": {\n        \"types\": \"./dist/core/index.d.cts\",\n        \"default\": \"./dist/core/index.cjs\"\n      }\n    },\n    \"./dist/*\": \"./dist/*.js\",\n    \"./dist/*.js\": \"./dist/*.js\",\n    \"./dist/*.cjs\": \"./dist/*.cjs\",\n    \"./dist/*.mjs\": \"./dist/*.mjs\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"scripts\": {\n    \"build\": \"node scripts/build.js\"\n  },\n  \"dependencies\": {\n    \"@react-grab/cli\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "packages/grab/scripts/build.js",
    "content": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst grabRoot = path.resolve(__dirname, \"..\");\nconst reactGrabRoot = path.resolve(__dirname, \"../../react-grab\");\nconst repoRoot = path.resolve(__dirname, \"../../..\");\n\nconst copyDistFiles = () => {\n  const sourceDir = path.join(reactGrabRoot, \"dist\");\n  const destDir = path.join(grabRoot, \"dist\");\n\n  if (!fs.existsSync(sourceDir)) {\n    console.error(\n      `react-grab dist folder not found at ${sourceDir}. Run 'pnpm build' in react-grab first.`,\n    );\n    process.exit(1);\n    return;\n  }\n\n  if (fs.existsSync(destDir)) {\n    fs.rmSync(destDir, { recursive: true });\n  }\n\n  fs.cpSync(sourceDir, destDir, { recursive: true });\n\n  const files = fs.readdirSync(sourceDir);\n  console.log(`Copied ${files.length} items from react-grab/dist to grab/dist`);\n};\n\nconst copyReactGrabReadme = () => {\n  const sourceReadme = path.join(repoRoot, \"README.md\");\n  const destReadme = path.join(reactGrabRoot, \"README.md\");\n\n  if (!fs.existsSync(sourceReadme)) {\n    throw new Error(`README.md not found at ${sourceReadme}`);\n  }\n\n  fs.copyFileSync(sourceReadme, destReadme);\n  console.log(\"Copied README.md to react-grab/README.md\");\n};\n\nconst transformReadme = () => {\n  const sourceReadme = path.join(repoRoot, \"README.md\");\n  const destReadme = path.join(grabRoot, \"README.md\");\n\n  if (!fs.existsSync(sourceReadme)) {\n    throw new Error(`README.md not found at ${sourceReadme}`);\n  }\n\n  let content = fs.readFileSync(sourceReadme, \"utf8\");\n\n  content = content\n    .replace(\n      /# <img .* \\/>.*React Grab/m,\n      '# <img src=\"https://github.com/aidenybai/react-grab/blob/main/.github/public/logo.png?raw=true\" width=\"60\" align=\"center\" /> Grab',\n    )\n    .replace(/bundlephobia\\/minzip\\/react-grab/g, \"bundlephobia/minzip/grab\")\n    .replace(\n      /bundlephobia\\.com\\/package\\/react-grab/g,\n      \"bundlephobia.com/package/grab\",\n    )\n    .replace(\n      /img\\.shields\\.io\\/npm\\/v\\/react-grab/g,\n      \"img.shields.io/npm/v/grab\",\n    )\n    .replace(\n      /img\\.shields\\.io\\/npm\\/dt\\/react-grab/g,\n      \"img.shields.io/npm/dt/grab\",\n    )\n    .replace(/npmjs\\.com\\/package\\/react-grab/g, \"npmjs.com/package/grab\")\n    .replace(/npm install react-grab/g, \"npm install grab\")\n    .replace(/npm i react-grab/g, \"npm i grab\")\n    .replace(/npx( -y)? react-grab@latest/g, \"npx$1 grab@latest\")\n    .replace(/unpkg\\.com\\/react-grab/g, \"unpkg.com/grab\")\n    .replace(/import\\(\"react-grab\"\\)/g, 'import(\"grab\")')\n    .replace(/from \"react-grab\\/core\"/g, 'from \"grab/core\"')\n    .replace(/from \"react-grab\\/primitives\"/g, 'from \"grab/primitives\"')\n    .replace(/from \"react-grab\"/g, 'from \"grab\"');\n\n  fs.writeFileSync(destReadme, content);\n  console.log(\"Generated grab/README.md from root README.md\");\n};\n\nconst syncPackageJson = () => {\n  const sourcePackageJson = path.join(reactGrabRoot, \"package.json\");\n  const destPackageJson = path.join(grabRoot, \"package.json\");\n\n  const sourcePackage = JSON.parse(fs.readFileSync(sourcePackageJson, \"utf8\"));\n  const destPackage = JSON.parse(fs.readFileSync(destPackageJson, \"utf8\"));\n\n  destPackage.version = sourcePackage.version;\n  destPackage.description = sourcePackage.description;\n  destPackage.keywords = sourcePackage.keywords;\n  destPackage.homepage = sourcePackage.homepage;\n  destPackage.bugs = sourcePackage.bugs;\n  destPackage.repository = sourcePackage.repository;\n  destPackage.author = sourcePackage.author;\n  destPackage.license = sourcePackage.license;\n\n  fs.writeFileSync(\n    destPackageJson,\n    JSON.stringify(destPackage, null, 2) + \"\\n\",\n  );\n  console.log(`Synced package.json (version: ${destPackage.version})`);\n};\n\nconst copyLicense = () => {\n  const sourceLicense = path.join(repoRoot, \"LICENSE\");\n  const destLicense = path.join(grabRoot, \"LICENSE\");\n\n  if (fs.existsSync(sourceLicense)) {\n    fs.copyFileSync(sourceLicense, destLicense);\n    console.log(\"Copied LICENSE\");\n  }\n};\n\nconst main = () => {\n  console.log(\"Building grab package...\\n\");\n\n  copyDistFiles();\n  copyReactGrabReadme();\n  transformReadme();\n  syncPackageJson();\n  copyLicense();\n\n  console.log(\"\\nDone!\");\n};\n\nmain();\n"
  },
  {
    "path": "packages/grab/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"esModuleInterop\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "packages/gym/.gitignore",
    "content": ".next\n"
  },
  {
    "path": "packages/gym/app/api/provider/[name]/route.ts",
    "content": "import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { NextResponse, type NextRequest } from \"next/server\";\n\nconst PROVIDER_MAP: Record<string, string> = {\n  \"claude-code\": \"provider-claude-code\",\n  cursor: \"provider-cursor\",\n  opencode: \"provider-opencode\",\n  amp: \"provider-amp\",\n  codex: \"provider-codex\",\n  gemini: \"provider-gemini\",\n  droid: \"provider-droid\",\n  copilot: \"provider-copilot\",\n  mcp: \"mcp\",\n};\n\nexport const GET = async (\n  _request: NextRequest,\n  { params }: { params: Promise<{ name: string }> },\n): Promise<NextResponse> => {\n  const { name } = await params;\n  const packageDir = PROVIDER_MAP[name];\n\n  if (!packageDir) {\n    return new NextResponse(\"Provider not found\", { status: 404 });\n  }\n\n  const scriptPath = join(\n    process.cwd(),\n    \"..\",\n    packageDir,\n    \"dist\",\n    \"client.global.js\",\n  );\n\n  try {\n    const content = await readFile(scriptPath, \"utf-8\");\n    return new NextResponse(content, {\n      headers: {\n        \"Content-Type\": \"application/javascript\",\n        \"Cache-Control\": \"no-cache\",\n      },\n    });\n  } catch {\n    return new NextResponse(\"Script not found\", { status: 404 });\n  }\n};\n"
  },
  {
    "path": "packages/gym/app/dashboard/data.json",
    "content": "[\n  {\n    \"id\": 1,\n    \"header\": \"Cover page\",\n    \"type\": \"Cover page\",\n    \"status\": \"In Process\",\n    \"target\": \"18\",\n    \"limit\": \"5\",\n    \"reviewer\": \"Eddie Lake\"\n  },\n  {\n    \"id\": 2,\n    \"header\": \"Table of contents\",\n    \"type\": \"Table of contents\",\n    \"status\": \"Done\",\n    \"target\": \"29\",\n    \"limit\": \"24\",\n    \"reviewer\": \"Eddie Lake\"\n  },\n  {\n    \"id\": 3,\n    \"header\": \"Executive summary\",\n    \"type\": \"Narrative\",\n    \"status\": \"Done\",\n    \"target\": \"10\",\n    \"limit\": \"13\",\n    \"reviewer\": \"Eddie Lake\"\n  },\n  {\n    \"id\": 4,\n    \"header\": \"Technical approach\",\n    \"type\": \"Narrative\",\n    \"status\": \"Done\",\n    \"target\": \"27\",\n    \"limit\": \"23\",\n    \"reviewer\": \"Jamik Tashpulatov\"\n  },\n  {\n    \"id\": 5,\n    \"header\": \"Design\",\n    \"type\": \"Narrative\",\n    \"status\": \"In Process\",\n    \"target\": \"2\",\n    \"limit\": \"16\",\n    \"reviewer\": \"Jamik Tashpulatov\"\n  },\n  {\n    \"id\": 6,\n    \"header\": \"Capabilities\",\n    \"type\": \"Narrative\",\n    \"status\": \"In Process\",\n    \"target\": \"20\",\n    \"limit\": \"8\",\n    \"reviewer\": \"Jamik Tashpulatov\"\n  },\n  {\n    \"id\": 7,\n    \"header\": \"Integration with existing systems\",\n    \"type\": \"Narrative\",\n    \"status\": \"In Process\",\n    \"target\": \"19\",\n    \"limit\": \"21\",\n    \"reviewer\": \"Jamik Tashpulatov\"\n  },\n  {\n    \"id\": 8,\n    \"header\": \"Innovation and Advantages\",\n    \"type\": \"Narrative\",\n    \"status\": \"Done\",\n    \"target\": \"25\",\n    \"limit\": \"26\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 9,\n    \"header\": \"Overview of EMR's Innovative Solutions\",\n    \"type\": \"Technical content\",\n    \"status\": \"Done\",\n    \"target\": \"7\",\n    \"limit\": \"23\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 10,\n    \"header\": \"Advanced Algorithms and Machine Learning\",\n    \"type\": \"Narrative\",\n    \"status\": \"Done\",\n    \"target\": \"30\",\n    \"limit\": \"28\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 11,\n    \"header\": \"Adaptive Communication Protocols\",\n    \"type\": \"Narrative\",\n    \"status\": \"Done\",\n    \"target\": \"9\",\n    \"limit\": \"31\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 12,\n    \"header\": \"Advantages Over Current Technologies\",\n    \"type\": \"Narrative\",\n    \"status\": \"Done\",\n    \"target\": \"12\",\n    \"limit\": \"0\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 13,\n    \"header\": \"Past Performance\",\n    \"type\": \"Narrative\",\n    \"status\": \"Done\",\n    \"target\": \"22\",\n    \"limit\": \"33\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 14,\n    \"header\": \"Customer Feedback and Satisfaction Levels\",\n    \"type\": \"Narrative\",\n    \"status\": \"Done\",\n    \"target\": \"15\",\n    \"limit\": \"34\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 15,\n    \"header\": \"Implementation Challenges and Solutions\",\n    \"type\": \"Narrative\",\n    \"status\": \"Done\",\n    \"target\": \"3\",\n    \"limit\": \"35\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 16,\n    \"header\": \"Security Measures and Data Protection Policies\",\n    \"type\": \"Narrative\",\n    \"status\": \"In Process\",\n    \"target\": \"6\",\n    \"limit\": \"36\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 17,\n    \"header\": \"Scalability and Future Proofing\",\n    \"type\": \"Narrative\",\n    \"status\": \"Done\",\n    \"target\": \"4\",\n    \"limit\": \"37\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 18,\n    \"header\": \"Cost-Benefit Analysis\",\n    \"type\": \"Plain language\",\n    \"status\": \"Done\",\n    \"target\": \"14\",\n    \"limit\": \"38\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 19,\n    \"header\": \"User Training and Onboarding Experience\",\n    \"type\": \"Narrative\",\n    \"status\": \"Done\",\n    \"target\": \"17\",\n    \"limit\": \"39\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 20,\n    \"header\": \"Future Development Roadmap\",\n    \"type\": \"Narrative\",\n    \"status\": \"Done\",\n    \"target\": \"11\",\n    \"limit\": \"40\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 21,\n    \"header\": \"System Architecture Overview\",\n    \"type\": \"Technical content\",\n    \"status\": \"In Process\",\n    \"target\": \"24\",\n    \"limit\": \"18\",\n    \"reviewer\": \"Maya Johnson\"\n  },\n  {\n    \"id\": 22,\n    \"header\": \"Risk Management Plan\",\n    \"type\": \"Narrative\",\n    \"status\": \"Done\",\n    \"target\": \"15\",\n    \"limit\": \"22\",\n    \"reviewer\": \"Carlos Rodriguez\"\n  },\n  {\n    \"id\": 23,\n    \"header\": \"Compliance Documentation\",\n    \"type\": \"Legal\",\n    \"status\": \"In Process\",\n    \"target\": \"31\",\n    \"limit\": \"27\",\n    \"reviewer\": \"Sarah Chen\"\n  },\n  {\n    \"id\": 24,\n    \"header\": \"API Documentation\",\n    \"type\": \"Technical content\",\n    \"status\": \"Done\",\n    \"target\": \"8\",\n    \"limit\": \"12\",\n    \"reviewer\": \"Raj Patel\"\n  },\n  {\n    \"id\": 25,\n    \"header\": \"User Interface Mockups\",\n    \"type\": \"Visual\",\n    \"status\": \"In Process\",\n    \"target\": \"19\",\n    \"limit\": \"25\",\n    \"reviewer\": \"Leila Ahmadi\"\n  },\n  {\n    \"id\": 26,\n    \"header\": \"Database Schema\",\n    \"type\": \"Technical content\",\n    \"status\": \"Done\",\n    \"target\": \"22\",\n    \"limit\": \"20\",\n    \"reviewer\": \"Thomas Wilson\"\n  },\n  {\n    \"id\": 27,\n    \"header\": \"Testing Methodology\",\n    \"type\": \"Technical content\",\n    \"status\": \"In Process\",\n    \"target\": \"17\",\n    \"limit\": \"14\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 28,\n    \"header\": \"Deployment Strategy\",\n    \"type\": \"Narrative\",\n    \"status\": \"Done\",\n    \"target\": \"26\",\n    \"limit\": \"30\",\n    \"reviewer\": \"Eddie Lake\"\n  },\n  {\n    \"id\": 29,\n    \"header\": \"Budget Breakdown\",\n    \"type\": \"Financial\",\n    \"status\": \"In Process\",\n    \"target\": \"13\",\n    \"limit\": \"16\",\n    \"reviewer\": \"Jamik Tashpulatov\"\n  },\n  {\n    \"id\": 30,\n    \"header\": \"Market Analysis\",\n    \"type\": \"Research\",\n    \"status\": \"Done\",\n    \"target\": \"29\",\n    \"limit\": \"32\",\n    \"reviewer\": \"Sophia Martinez\"\n  },\n  {\n    \"id\": 31,\n    \"header\": \"Competitor Comparison\",\n    \"type\": \"Research\",\n    \"status\": \"In Process\",\n    \"target\": \"21\",\n    \"limit\": \"19\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 32,\n    \"header\": \"Maintenance Plan\",\n    \"type\": \"Technical content\",\n    \"status\": \"Done\",\n    \"target\": \"16\",\n    \"limit\": \"23\",\n    \"reviewer\": \"Alex Thompson\"\n  },\n  {\n    \"id\": 33,\n    \"header\": \"User Personas\",\n    \"type\": \"Research\",\n    \"status\": \"In Process\",\n    \"target\": \"27\",\n    \"limit\": \"24\",\n    \"reviewer\": \"Nina Patel\"\n  },\n  {\n    \"id\": 34,\n    \"header\": \"Accessibility Compliance\",\n    \"type\": \"Legal\",\n    \"status\": \"Done\",\n    \"target\": \"18\",\n    \"limit\": \"21\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 35,\n    \"header\": \"Performance Metrics\",\n    \"type\": \"Technical content\",\n    \"status\": \"In Process\",\n    \"target\": \"23\",\n    \"limit\": \"26\",\n    \"reviewer\": \"David Kim\"\n  },\n  {\n    \"id\": 36,\n    \"header\": \"Disaster Recovery Plan\",\n    \"type\": \"Technical content\",\n    \"status\": \"Done\",\n    \"target\": \"14\",\n    \"limit\": \"17\",\n    \"reviewer\": \"Jamik Tashpulatov\"\n  },\n  {\n    \"id\": 37,\n    \"header\": \"Third-party Integrations\",\n    \"type\": \"Technical content\",\n    \"status\": \"In Process\",\n    \"target\": \"25\",\n    \"limit\": \"28\",\n    \"reviewer\": \"Eddie Lake\"\n  },\n  {\n    \"id\": 38,\n    \"header\": \"User Feedback Summary\",\n    \"type\": \"Research\",\n    \"status\": \"Done\",\n    \"target\": \"20\",\n    \"limit\": \"15\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 39,\n    \"header\": \"Localization Strategy\",\n    \"type\": \"Narrative\",\n    \"status\": \"In Process\",\n    \"target\": \"12\",\n    \"limit\": \"19\",\n    \"reviewer\": \"Maria Garcia\"\n  },\n  {\n    \"id\": 40,\n    \"header\": \"Mobile Compatibility\",\n    \"type\": \"Technical content\",\n    \"status\": \"Done\",\n    \"target\": \"28\",\n    \"limit\": \"31\",\n    \"reviewer\": \"James Wilson\"\n  },\n  {\n    \"id\": 41,\n    \"header\": \"Data Migration Plan\",\n    \"type\": \"Technical content\",\n    \"status\": \"In Process\",\n    \"target\": \"19\",\n    \"limit\": \"22\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 42,\n    \"header\": \"Quality Assurance Protocols\",\n    \"type\": \"Technical content\",\n    \"status\": \"Done\",\n    \"target\": \"30\",\n    \"limit\": \"33\",\n    \"reviewer\": \"Priya Singh\"\n  },\n  {\n    \"id\": 43,\n    \"header\": \"Stakeholder Analysis\",\n    \"type\": \"Research\",\n    \"status\": \"In Process\",\n    \"target\": \"11\",\n    \"limit\": \"14\",\n    \"reviewer\": \"Eddie Lake\"\n  },\n  {\n    \"id\": 44,\n    \"header\": \"Environmental Impact Assessment\",\n    \"type\": \"Research\",\n    \"status\": \"Done\",\n    \"target\": \"24\",\n    \"limit\": \"27\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 45,\n    \"header\": \"Intellectual Property Rights\",\n    \"type\": \"Legal\",\n    \"status\": \"In Process\",\n    \"target\": \"17\",\n    \"limit\": \"20\",\n    \"reviewer\": \"Sarah Johnson\"\n  },\n  {\n    \"id\": 46,\n    \"header\": \"Customer Support Framework\",\n    \"type\": \"Narrative\",\n    \"status\": \"Done\",\n    \"target\": \"22\",\n    \"limit\": \"25\",\n    \"reviewer\": \"Jamik Tashpulatov\"\n  },\n  {\n    \"id\": 47,\n    \"header\": \"Version Control Strategy\",\n    \"type\": \"Technical content\",\n    \"status\": \"In Process\",\n    \"target\": \"15\",\n    \"limit\": \"18\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 48,\n    \"header\": \"Continuous Integration Pipeline\",\n    \"type\": \"Technical content\",\n    \"status\": \"Done\",\n    \"target\": \"26\",\n    \"limit\": \"29\",\n    \"reviewer\": \"Michael Chen\"\n  },\n  {\n    \"id\": 49,\n    \"header\": \"Regulatory Compliance\",\n    \"type\": \"Legal\",\n    \"status\": \"In Process\",\n    \"target\": \"13\",\n    \"limit\": \"16\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 50,\n    \"header\": \"User Authentication System\",\n    \"type\": \"Technical content\",\n    \"status\": \"Done\",\n    \"target\": \"28\",\n    \"limit\": \"31\",\n    \"reviewer\": \"Eddie Lake\"\n  },\n  {\n    \"id\": 51,\n    \"header\": \"Data Analytics Framework\",\n    \"type\": \"Technical content\",\n    \"status\": \"In Process\",\n    \"target\": \"21\",\n    \"limit\": \"24\",\n    \"reviewer\": \"Jamik Tashpulatov\"\n  },\n  {\n    \"id\": 52,\n    \"header\": \"Cloud Infrastructure\",\n    \"type\": \"Technical content\",\n    \"status\": \"Done\",\n    \"target\": \"16\",\n    \"limit\": \"19\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 53,\n    \"header\": \"Network Security Measures\",\n    \"type\": \"Technical content\",\n    \"status\": \"In Process\",\n    \"target\": \"29\",\n    \"limit\": \"32\",\n    \"reviewer\": \"Lisa Wong\"\n  },\n  {\n    \"id\": 54,\n    \"header\": \"Project Timeline\",\n    \"type\": \"Planning\",\n    \"status\": \"Done\",\n    \"target\": \"14\",\n    \"limit\": \"17\",\n    \"reviewer\": \"Eddie Lake\"\n  },\n  {\n    \"id\": 55,\n    \"header\": \"Resource Allocation\",\n    \"type\": \"Planning\",\n    \"status\": \"In Process\",\n    \"target\": \"27\",\n    \"limit\": \"30\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 56,\n    \"header\": \"Team Structure and Roles\",\n    \"type\": \"Planning\",\n    \"status\": \"Done\",\n    \"target\": \"20\",\n    \"limit\": \"23\",\n    \"reviewer\": \"Jamik Tashpulatov\"\n  },\n  {\n    \"id\": 57,\n    \"header\": \"Communication Protocols\",\n    \"type\": \"Planning\",\n    \"status\": \"In Process\",\n    \"target\": \"15\",\n    \"limit\": \"18\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 58,\n    \"header\": \"Success Metrics\",\n    \"type\": \"Planning\",\n    \"status\": \"Done\",\n    \"target\": \"30\",\n    \"limit\": \"33\",\n    \"reviewer\": \"Eddie Lake\"\n  },\n  {\n    \"id\": 59,\n    \"header\": \"Internationalization Support\",\n    \"type\": \"Technical content\",\n    \"status\": \"In Process\",\n    \"target\": \"23\",\n    \"limit\": \"26\",\n    \"reviewer\": \"Jamik Tashpulatov\"\n  },\n  {\n    \"id\": 60,\n    \"header\": \"Backup and Recovery Procedures\",\n    \"type\": \"Technical content\",\n    \"status\": \"Done\",\n    \"target\": \"18\",\n    \"limit\": \"21\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 61,\n    \"header\": \"Monitoring and Alerting System\",\n    \"type\": \"Technical content\",\n    \"status\": \"In Process\",\n    \"target\": \"25\",\n    \"limit\": \"28\",\n    \"reviewer\": \"Daniel Park\"\n  },\n  {\n    \"id\": 62,\n    \"header\": \"Code Review Guidelines\",\n    \"type\": \"Technical content\",\n    \"status\": \"Done\",\n    \"target\": \"12\",\n    \"limit\": \"15\",\n    \"reviewer\": \"Eddie Lake\"\n  },\n  {\n    \"id\": 63,\n    \"header\": \"Documentation Standards\",\n    \"type\": \"Technical content\",\n    \"status\": \"In Process\",\n    \"target\": \"27\",\n    \"limit\": \"30\",\n    \"reviewer\": \"Jamik Tashpulatov\"\n  },\n  {\n    \"id\": 64,\n    \"header\": \"Release Management Process\",\n    \"type\": \"Planning\",\n    \"status\": \"Done\",\n    \"target\": \"22\",\n    \"limit\": \"25\",\n    \"reviewer\": \"Assign reviewer\"\n  },\n  {\n    \"id\": 65,\n    \"header\": \"Feature Prioritization Matrix\",\n    \"type\": \"Planning\",\n    \"status\": \"In Process\",\n    \"target\": \"19\",\n    \"limit\": \"22\",\n    \"reviewer\": \"Emma Davis\"\n  },\n  {\n    \"id\": 66,\n    \"header\": \"Technical Debt Assessment\",\n    \"type\": \"Technical content\",\n    \"status\": \"Done\",\n    \"target\": \"24\",\n    \"limit\": \"27\",\n    \"reviewer\": \"Eddie Lake\"\n  },\n  {\n    \"id\": 67,\n    \"header\": \"Capacity Planning\",\n    \"type\": \"Planning\",\n    \"status\": \"In Process\",\n    \"target\": \"21\",\n    \"limit\": \"24\",\n    \"reviewer\": \"Jamik Tashpulatov\"\n  },\n  {\n    \"id\": 68,\n    \"header\": \"Service Level Agreements\",\n    \"type\": \"Legal\",\n    \"status\": \"Done\",\n    \"target\": \"26\",\n    \"limit\": \"29\",\n    \"reviewer\": \"Assign reviewer\"\n  }\n]\n"
  },
  {
    "path": "packages/gym/app/dashboard/page.tsx",
    "content": "import { AppSidebar } from \"@/components/app-sidebar\";\nimport { ChartAreaInteractive } from \"@/components/chart-area-interactive\";\nimport { Counter } from \"@/components/counter\";\nimport { DataTable } from \"@/components/data-table\";\nimport { SectionCards } from \"@/components/section-cards\";\nimport { SheetDemo } from \"@/components/sheet-demo\";\nimport { SiteHeader } from \"@/components/site-header\";\nimport { SidebarInset, SidebarProvider } from \"@/components/ui/sidebar\";\n\nimport data from \"./data.json\";\n\nexport default function Page() {\n  return (\n    <SidebarProvider\n      style={\n        {\n          \"--sidebar-width\": \"calc(var(--spacing) * 72)\",\n          \"--header-height\": \"calc(var(--spacing) * 12)\",\n        } as React.CSSProperties\n      }\n    >\n      <AppSidebar variant=\"inset\" />\n      <SidebarInset>\n        <SiteHeader />\n        <div className=\"flex flex-1 flex-col\">\n          <div className=\"@container/main flex flex-1 flex-col gap-2\">\n            <div className=\"flex flex-col gap-4 py-4 md:gap-6 md:py-6\">\n              <div className=\"px-4 lg:px-6\">\n                <SheetDemo />\n              </div>\n              <Counter />\n              <SectionCards />\n              <div className=\"px-4 lg:px-6\">\n                <ChartAreaInteractive />\n              </div>\n              <DataTable data={data} />\n            </div>\n          </div>\n        </div>\n      </SidebarInset>\n    </SidebarProvider>\n  );\n}\n"
  },
  {
    "path": "packages/gym/app/freeze-demo/layout.tsx",
    "content": "export default function FreezeDemoLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return <>{children}</>;\n}\n"
  },
  {
    "path": "packages/gym/app/freeze-demo/page.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\nconst TIMER_INTERVAL_MS = 10;\nconst VELOCITY_PX = 3;\n\nconst BouncingTimer = () => {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const timerRef = useRef<HTMLDivElement>(null);\n  const positionRef = useRef({ x: 50, y: 50 });\n  const velocityRef = useRef({ x: VELOCITY_PX, y: VELOCITY_PX });\n  const [hue, setHue] = useState(0);\n  const [elapsedTime, setElapsedTime] = useState(0);\n  const [position, setPosition] = useState({ x: 50, y: 50 });\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setElapsedTime((previousTime) => previousTime + TIMER_INTERVAL_MS);\n    }, TIMER_INTERVAL_MS);\n\n    return () => clearInterval(interval);\n  }, []);\n\n  useEffect(() => {\n    const animationFrame = () => {\n      const container = containerRef.current;\n      const timer = timerRef.current;\n      if (!container || !timer) return;\n\n      const containerRect = container.getBoundingClientRect();\n      const timerRect = timer.getBoundingClientRect();\n\n      const maxX = containerRect.width - timerRect.width;\n      const maxY = containerRect.height - timerRect.height;\n\n      let nextX = positionRef.current.x + velocityRef.current.x;\n      let nextY = positionRef.current.y + velocityRef.current.y;\n\n      let didBounce = false;\n\n      if (nextX <= 0 || nextX >= maxX) {\n        velocityRef.current.x *= -1;\n        nextX = Math.max(0, Math.min(nextX, maxX));\n        didBounce = true;\n      }\n\n      if (nextY <= 0 || nextY >= maxY) {\n        velocityRef.current.y *= -1;\n        nextY = Math.max(0, Math.min(nextY, maxY));\n        didBounce = true;\n      }\n\n      positionRef.current = { x: nextX, y: nextY };\n      setPosition({ x: nextX, y: nextY });\n\n      if (didBounce) {\n        setHue((previousHue) => (previousHue + 60) % 360);\n      }\n    };\n\n    const interval = setInterval(animationFrame, 16);\n    return () => clearInterval(interval);\n  }, []);\n\n  const formatTime = (milliseconds: number) => {\n    const totalSeconds = Math.floor(milliseconds / 1000);\n    const minutes = Math.floor(totalSeconds / 60);\n    const seconds = totalSeconds % 60;\n    const centiseconds = Math.floor((milliseconds % 1000) / 10);\n    return `${minutes.toString().padStart(2, \"0\")}:${seconds.toString().padStart(2, \"0\")}.${centiseconds.toString().padStart(2, \"0\")}`;\n  };\n\n  return (\n    <div\n      ref={containerRef}\n      className=\"relative h-full w-full overflow-hidden bg-black\"\n    >\n      <div\n        ref={timerRef}\n        className=\"absolute select-none text-6xl font-bold tabular-nums transition-colors duration-300 md:text-8xl\"\n        style={{\n          left: position.x,\n          top: position.y,\n          color: `hsl(${hue}, 100%, 50%)`,\n          textShadow: `0 0 20px hsl(${hue}, 100%, 50%), 0 0 40px hsl(${hue}, 100%, 50%)`,\n        }}\n      >\n        {formatTime(elapsedTime)}\n      </div>\n    </div>\n  );\n};\n\nexport default function FreezeDemoPage() {\n  return (\n    <div className=\"h-screen w-screen\">\n      <BouncingTimer />\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/gym/app/globals.css",
    "content": "@import \"tailwindcss\";\n@import \"tw-animate-css\";\n\n@custom-variant dark (&:is(.dark *));\n\n@theme inline {\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --font-sans: var(--font-geist-sans);\n  --font-mono: var(--font-geist-mono);\n  --color-sidebar-ring: var(--sidebar-ring);\n  --color-sidebar-border: var(--sidebar-border);\n  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n  --color-sidebar-accent: var(--sidebar-accent);\n  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n  --color-sidebar-primary: var(--sidebar-primary);\n  --color-sidebar-foreground: var(--sidebar-foreground);\n  --color-sidebar: var(--sidebar);\n  --color-chart-5: var(--chart-5);\n  --color-chart-4: var(--chart-4);\n  --color-chart-3: var(--chart-3);\n  --color-chart-2: var(--chart-2);\n  --color-chart-1: var(--chart-1);\n  --color-ring: var(--ring);\n  --color-input: var(--input);\n  --color-border: var(--border);\n  --color-destructive: var(--destructive);\n  --color-accent-foreground: var(--accent-foreground);\n  --color-accent: var(--accent);\n  --color-muted-foreground: var(--muted-foreground);\n  --color-muted: var(--muted);\n  --color-secondary-foreground: var(--secondary-foreground);\n  --color-secondary: var(--secondary);\n  --color-primary-foreground: var(--primary-foreground);\n  --color-primary: var(--primary);\n  --color-popover-foreground: var(--popover-foreground);\n  --color-popover: var(--popover);\n  --color-card-foreground: var(--card-foreground);\n  --color-card: var(--card);\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}\n\n:root {\n  --radius: 0.625rem;\n  --background: oklch(1 0 0);\n  --foreground: oklch(0.145 0 0);\n  --card: oklch(1 0 0);\n  --card-foreground: oklch(0.145 0 0);\n  --popover: oklch(1 0 0);\n  --popover-foreground: oklch(0.145 0 0);\n  --primary: oklch(0.205 0 0);\n  --primary-foreground: oklch(0.985 0 0);\n  --secondary: oklch(0.97 0 0);\n  --secondary-foreground: oklch(0.205 0 0);\n  --muted: oklch(0.97 0 0);\n  --muted-foreground: oklch(0.556 0 0);\n  --accent: oklch(0.97 0 0);\n  --accent-foreground: oklch(0.205 0 0);\n  --destructive: oklch(0.577 0.245 27.325);\n  --border: oklch(0.922 0 0);\n  --input: oklch(0.922 0 0);\n  --ring: oklch(0.708 0 0);\n  --chart-1: oklch(0.646 0.222 41.116);\n  --chart-2: oklch(0.6 0.118 184.704);\n  --chart-3: oklch(0.398 0.07 227.392);\n  --chart-4: oklch(0.828 0.189 84.429);\n  --chart-5: oklch(0.769 0.188 70.08);\n  --sidebar: hsl(0 0% 98%);\n  --sidebar-foreground: hsl(240 5.3% 26.1%);\n  --sidebar-primary: hsl(240 5.9% 10%);\n  --sidebar-primary-foreground: hsl(0 0% 98%);\n  --sidebar-accent: hsl(240 4.8% 95.9%);\n  --sidebar-accent-foreground: hsl(240 5.9% 10%);\n  --sidebar-border: hsl(220 13% 91%);\n  --sidebar-ring: hsl(217.2 91.2% 59.8%);\n}\n\n.dark {\n  --background: oklch(0.145 0 0);\n  --foreground: oklch(0.985 0 0);\n  --card: oklch(0.205 0 0);\n  --card-foreground: oklch(0.985 0 0);\n  --popover: oklch(0.205 0 0);\n  --popover-foreground: oklch(0.985 0 0);\n  --primary: oklch(0.922 0 0);\n  --primary-foreground: oklch(0.205 0 0);\n  --secondary: oklch(0.269 0 0);\n  --secondary-foreground: oklch(0.985 0 0);\n  --muted: oklch(0.269 0 0);\n  --muted-foreground: oklch(0.708 0 0);\n  --accent: oklch(0.269 0 0);\n  --accent-foreground: oklch(0.985 0 0);\n  --destructive: oklch(0.704 0.191 22.216);\n  --border: oklch(1 0 0 / 10%);\n  --input: oklch(1 0 0 / 15%);\n  --ring: oklch(0.556 0 0);\n  --chart-1: oklch(0.488 0.243 264.376);\n  --chart-2: oklch(0.696 0.17 162.48);\n  --chart-3: oklch(0.769 0.188 70.08);\n  --chart-4: oklch(0.627 0.265 303.9);\n  --chart-5: oklch(0.645 0.246 16.439);\n  --sidebar: hsl(240 5.9% 10%);\n  --sidebar-foreground: hsl(240 4.8% 95.9%);\n  --sidebar-primary: hsl(224.3 76.3% 48%);\n  --sidebar-primary-foreground: hsl(0 0% 100%);\n  --sidebar-accent: hsl(240 3.7% 15.9%);\n  --sidebar-accent-foreground: hsl(240 4.8% 95.9%);\n  --sidebar-border: hsl(240 3.7% 15.9%);\n  --sidebar-ring: hsl(217.2 91.2% 59.8%);\n}\n\n@layer base {\n  * {\n    @apply border-border outline-ring/50;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n"
  },
  {
    "path": "packages/gym/app/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { Geist, Geist_Mono } from \"next/font/google\";\nimport { ThemeProvider } from \"@/components/theme-provider\";\nimport \"react-grab\";\nimport \"./globals.css\";\n\nconst geistSans = Geist({\n  variable: \"--font-geist-sans\",\n  subsets: [\"latin\"],\n});\n\nconst geistMono = Geist_Mono({\n  variable: \"--font-geist-mono\",\n  subsets: [\"latin\"],\n});\n\nexport const metadata: Metadata = {\n  title: \"React Grab Gym\",\n  description: \"React Grab testing playground with shadcn/ui\",\n};\n\nexport default function RootLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  return (\n    <html lang=\"en\" suppressHydrationWarning>\n      <body\n        className={`${geistSans.variable} ${geistMono.variable} antialiased`}\n      >\n        <ThemeProvider\n          attribute=\"class\"\n          defaultTheme=\"system\"\n          enableSystem\n          disableTransitionOnChange\n        >\n          {children}\n        </ThemeProvider>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "packages/gym/app/login/page.tsx",
    "content": "import { LoginForm } from \"@/components/login-form\";\n\nexport default function Page() {\n  return (\n    <div className=\"flex min-h-svh w-full items-center justify-center p-6 md:p-10\">\n      <div className=\"w-full max-w-sm\">\n        <LoginForm />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/gym/app/page.tsx",
    "content": "import { redirect } from \"next/navigation\";\n\nexport default function Home() {\n  redirect(\"/dashboard\");\n}\n"
  },
  {
    "path": "packages/gym/app/playground/page.tsx",
    "content": "import { AppSidebar } from \"@/components/app-sidebar\";\nimport { SiteHeader } from \"@/components/site-header\";\nimport { SidebarInset, SidebarProvider } from \"@/components/ui/sidebar\";\nimport { AgentPlayground } from \"@/components/agent-playground\";\n\nexport default function PlaygroundPage() {\n  return (\n    <SidebarProvider\n      style={\n        {\n          \"--sidebar-width\": \"calc(var(--spacing) * 72)\",\n          \"--header-height\": \"calc(var(--spacing) * 12)\",\n        } as React.CSSProperties\n      }\n    >\n      <AppSidebar variant=\"inset\" />\n      <SidebarInset>\n        <SiteHeader />\n        <div className=\"flex flex-1 flex-col p-4 lg:p-6\">\n          <AgentPlayground />\n        </div>\n      </SidebarInset>\n    </SidebarProvider>\n  );\n}\n"
  },
  {
    "path": "packages/gym/components/agent-playground.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState, useRef, useCallback } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Input } from \"@/components/ui/input\";\nimport type { ReactGrabAPI } from \"react-grab\";\n\ninterface RelayClient {\n  onMessage: (callback: (message: RelayMessage) => void) => () => void;\n  onConnectionChange: (callback: (connected: boolean) => void) => () => void;\n  onHandlersChange: (callback: (handlers: string[]) => void) => () => void;\n  getAvailableHandlers: () => string[];\n  isConnected: () => boolean;\n}\n\ninterface RelayMessage {\n  type: string;\n  agentId?: string;\n  sessionId?: string;\n  content?: string;\n  handlers?: string[];\n}\n\ndeclare global {\n  interface Window {\n    __REACT_GRAB__?: ReactGrabAPI;\n    __REACT_GRAB_RELAY__?: RelayClient;\n  }\n}\n\ninterface LogEntry {\n  type: string;\n  message: string;\n  time: Date;\n}\n\nconst LOG_TYPE_STYLES: Record<string, { icon: string; color: string }> = {\n  info: { icon: \"◆\", color: \"text-muted-foreground\" },\n  connect: { icon: \"●\", color: \"text-green-500\" },\n  disconnect: { icon: \"○\", color: \"text-red-500\" },\n  handlers: { icon: \"↔\", color: \"text-blue-500\" },\n  status: { icon: \"◉\", color: \"text-purple-500\" },\n  done: { icon: \"✓\", color: \"text-green-500\" },\n  error: { icon: \"✕\", color: \"text-red-500\" },\n};\n\nconst MAX_LOG_ENTRIES = 50;\nconst STATUS_TRUNCATE_LENGTH = 60;\nconst RELAY_CHECK_INTERVAL_MS = 100;\n\nconst PROVIDER_SCRIPTS: Record<string, string> = {\n  claude: \"/@provider-claude-code/client.global.js\",\n  cursor: \"/@provider-cursor/client.global.js\",\n  opencode: \"/@provider-opencode/client.global.js\",\n  amp: \"/@provider-amp/client.global.js\",\n  codex: \"/@provider-codex/client.global.js\",\n  gemini: \"/@provider-gemini/client.global.js\",\n  droid: \"/@provider-droid/client.global.js\",\n  mcp: \"/@provider-mcp/client.global.js\",\n};\n\ninterface ProviderBadgeProps {\n  provider: string;\n  isActive: boolean;\n  onClick?: () => void;\n}\n\nconst ProviderBadge = ({ provider, isActive, onClick }: ProviderBadgeProps) => {\n  return (\n    <Badge\n      variant={isActive ? \"default\" : \"outline\"}\n      className=\"cursor-pointer\"\n      onClick={onClick}\n    >\n      {provider}\n    </Badge>\n  );\n};\n\ninterface AgentPlaygroundProps {\n  loadedProviders: string[];\n  failedProviders: string[];\n  availableProviders: string[];\n}\n\nexport const AgentPlaygroundContent = ({\n  loadedProviders,\n  failedProviders,\n  availableProviders,\n}: AgentPlaygroundProps) => {\n  const [logs, setLogs] = useState<LogEntry[]>([]);\n  const [relayConnected, setRelayConnected] = useState(false);\n  const [relayHandlers, setRelayHandlers] = useState<string[]>([]);\n  const didInit = useRef(false);\n  const logsEndRef = useRef<HTMLDivElement>(null);\n\n  const addLog = useCallback((type: string, message: string) => {\n    setLogs((previousLogs) => {\n      const newLogs = [...previousLogs, { type, message, time: new Date() }];\n      if (newLogs.length > MAX_LOG_ENTRIES) {\n        return newLogs.slice(-MAX_LOG_ENTRIES);\n      }\n      return newLogs;\n    });\n  }, []);\n\n  useEffect(() => {\n    logsEndRef.current?.scrollIntoView({ behavior: \"smooth\" });\n  }, [logs]);\n\n  useEffect(() => {\n    if (didInit.current) return;\n    didInit.current = true;\n\n    const api = window.__REACT_GRAB__;\n    if (!api) {\n      queueMicrotask(() => {\n        addLog(\"error\", \"React Grab not initialized\");\n      });\n      return;\n    }\n\n    queueMicrotask(() => {\n      for (const provider of failedProviders) {\n        addLog(\"error\", `Failed to load: ${provider}`);\n      }\n\n      if (loadedProviders.length === 0 && failedProviders.length === 0) {\n        addLog(\n          \"info\",\n          \"No providers loaded. Add ?provider=cursor,claude to URL\",\n        );\n      } else {\n        for (const provider of loadedProviders) {\n          addLog(\"info\", `Loaded: ${provider}`);\n        }\n      }\n    });\n  }, [loadedProviders, failedProviders, addLog]);\n\n  useEffect(() => {\n    let relayCleanup: (() => void) | false = false;\n\n    const checkForRelay = () => {\n      const relayClient = window.__REACT_GRAB_RELAY__;\n      if (!relayClient) return false;\n\n      const isConnected = relayClient.isConnected();\n      setRelayConnected(isConnected);\n      setRelayHandlers(relayClient.getAvailableHandlers());\n\n      const unsubscribeConnection = relayClient.onConnectionChange(\n        (connected) => {\n          setRelayConnected(connected);\n          addLog(\n            connected ? \"connect\" : \"disconnect\",\n            connected ? \"Relay connected\" : \"Relay disconnected\",\n          );\n        },\n      );\n\n      const unsubscribeHandlers = relayClient.onHandlersChange((handlers) => {\n        setRelayHandlers(handlers);\n        if (handlers.length > 0) {\n          addLog(\"handlers\", `Available: ${handlers.join(\", \")}`);\n        }\n      });\n\n      const unsubscribeMessage = relayClient.onMessage((message) => {\n        if (\n          message.type === \"agent-status\" &&\n          message.content &&\n          message.agentId\n        ) {\n          const truncatedContent =\n            message.content.length > STATUS_TRUNCATE_LENGTH\n              ? `${message.content.slice(0, STATUS_TRUNCATE_LENGTH)}…`\n              : message.content;\n          addLog(\"status\", `[${message.agentId}] ${truncatedContent}`);\n        } else if (message.type === \"agent-done\" && message.agentId) {\n          addLog(\"done\", `[${message.agentId}] Completed`);\n        } else if (message.type === \"agent-error\" && message.agentId) {\n          const errorContent = message.content || \"Unknown error\";\n          addLog(\"error\", `[${message.agentId}] ${errorContent}`);\n        }\n      });\n\n      return () => {\n        unsubscribeConnection();\n        unsubscribeHandlers();\n        unsubscribeMessage();\n      };\n    };\n\n    const cleanup = checkForRelay();\n    if (cleanup) return cleanup;\n\n    const intervalId = setInterval(() => {\n      const result = checkForRelay();\n      if (result) {\n        relayCleanup = result;\n        clearInterval(intervalId);\n      }\n    }, RELAY_CHECK_INTERVAL_MS);\n\n    return () => {\n      clearInterval(intervalId);\n      if (relayCleanup) {\n        relayCleanup();\n      }\n    };\n  }, [addLog]);\n\n  const handleAddProvider = (provider: string) => {\n    const currentProviders =\n      new URLSearchParams(window.location.search).get(\"provider\") ?? \"\";\n    const providerList = currentProviders ? currentProviders.split(\",\") : [];\n\n    if (providerList.includes(provider)) {\n      return;\n    }\n\n    providerList.push(provider);\n    const newUrl = new URL(window.location.href);\n    newUrl.searchParams.set(\"provider\", providerList.join(\",\"));\n    window.location.assign(newUrl.toString());\n  };\n\n  const handleRemoveProvider = (provider: string) => {\n    const currentProviders =\n      new URLSearchParams(window.location.search).get(\"provider\") ?? \"\";\n    const providerList = currentProviders\n      .split(\",\")\n      .filter((providerInList) => providerInList !== provider);\n\n    const newUrl = new URL(window.location.href);\n    if (providerList.length === 0) {\n      newUrl.searchParams.delete(\"provider\");\n    } else {\n      newUrl.searchParams.set(\"provider\", providerList.join(\",\"));\n    }\n    window.location.assign(newUrl.toString());\n  };\n\n  const inactiveProviders = availableProviders.filter(\n    (provider) =>\n      !loadedProviders.includes(provider) &&\n      !failedProviders.includes(provider),\n  );\n\n  return (\n    <div className=\"flex flex-col gap-6\">\n      <div className=\"flex items-center justify-between\">\n        <div>\n          <h1 className=\"text-2xl font-semibold tracking-tight\">\n            Agent Playground\n          </h1>\n          <p className=\"text-muted-foreground text-sm\">\n            Select any element and choose an agent from the context menu\n          </p>\n        </div>\n        <div className=\"flex items-center gap-3\">\n          <div className=\"flex items-center gap-1.5\">\n            <span\n              className={`w-2 h-2 rounded-full ${\n                relayConnected ? \"bg-green-500\" : \"bg-red-500\"\n              }`}\n            />\n            <span className=\"text-xs text-muted-foreground\">\n              {relayConnected ? \"Connected\" : \"Disconnected\"}\n            </span>\n          </div>\n          <Button onClick={() => window.__REACT_GRAB__?.activate()}>\n            Grab Element\n          </Button>\n        </div>\n      </div>\n\n      <Card>\n        <CardHeader className=\"pb-3\">\n          <CardTitle className=\"text-sm font-medium\">Test Elements</CardTitle>\n        </CardHeader>\n        <CardContent className=\"flex flex-col gap-4\">\n          <div className=\"flex gap-2\">\n            <Button variant=\"default\">Submit</Button>\n            <Button variant=\"outline\">Cancel</Button>\n          </div>\n          <Card>\n            <CardContent className=\"p-4\">\n              <div className=\"text-sm font-medium\">User Card</div>\n              <div className=\"text-muted-foreground text-xs mt-1\">\n                john@example.com\n              </div>\n            </CardContent>\n          </Card>\n          <Input type=\"text\" placeholder=\"Search…\" />\n        </CardContent>\n      </Card>\n\n      <Card>\n        <CardHeader className=\"pb-3\">\n          <div className=\"flex items-center justify-between\">\n            <CardTitle className=\"text-sm font-medium\">Providers</CardTitle>\n            {relayHandlers.length > 0 && (\n              <span className=\"text-xs text-muted-foreground\">\n                ({relayHandlers.length} handler\n                {relayHandlers.length !== 1 ? \"s\" : \"\"} ready)\n              </span>\n            )}\n          </div>\n        </CardHeader>\n        <CardContent>\n          <div className=\"flex flex-wrap gap-2\">\n            {loadedProviders.length === 0 &&\n            failedProviders.length === 0 &&\n            inactiveProviders.length === 0 ? (\n              <span className=\"text-muted-foreground text-sm\">\n                None available\n              </span>\n            ) : (\n              <>\n                {loadedProviders.map((provider) => (\n                  <ProviderBadge\n                    key={provider}\n                    provider={provider}\n                    isActive={true}\n                    onClick={() => handleRemoveProvider(provider)}\n                  />\n                ))}\n                {failedProviders.map((provider) => (\n                  <Badge\n                    key={provider}\n                    variant=\"destructive\"\n                    className=\"cursor-pointer\"\n                    onClick={() => handleRemoveProvider(provider)}\n                  >\n                    {provider} ✕\n                  </Badge>\n                ))}\n                {inactiveProviders.map((provider) => (\n                  <ProviderBadge\n                    key={provider}\n                    provider={provider}\n                    isActive={false}\n                    onClick={() => handleAddProvider(provider)}\n                  />\n                ))}\n              </>\n            )}\n          </div>\n        </CardContent>\n      </Card>\n\n      <Card>\n        <CardHeader className=\"pb-3\">\n          <div className=\"flex items-center justify-between\">\n            <CardTitle className=\"text-sm font-medium\">Activity</CardTitle>\n            {logs.length > 0 && (\n              <Button variant=\"ghost\" size=\"sm\" onClick={() => setLogs([])}>\n                Clear\n              </Button>\n            )}\n          </div>\n        </CardHeader>\n        <CardContent>\n          <div className=\"bg-muted/50 rounded-lg p-1 min-h-[180px] max-h-[300px] overflow-y-auto\">\n            {logs.length === 0 ? (\n              <div className=\"px-3 py-2 text-muted-foreground text-sm\">\n                Waiting…\n              </div>\n            ) : (\n              <div className=\"flex flex-col\">\n                {logs.map((log, logIndex) => {\n                  const style =\n                    LOG_TYPE_STYLES[log.type] ?? LOG_TYPE_STYLES.info;\n                  return (\n                    <div\n                      key={logIndex}\n                      className=\"flex items-start gap-3 px-3 py-1.5 rounded-md hover:bg-muted/50 transition-colors\"\n                    >\n                      <span\n                        className={`${style.color} text-xs w-3 mt-0.5 shrink-0`}\n                      >\n                        {style.icon}\n                      </span>\n                      <span className=\"text-foreground/70 text-sm flex-1 break-all font-mono\">\n                        {log.message}\n                      </span>\n                      <span className=\"text-muted-foreground text-xs tabular-nums shrink-0\">\n                        {log.time.toLocaleTimeString()}\n                      </span>\n                    </div>\n                  );\n                })}\n                <div ref={logsEndRef} />\n              </div>\n            )}\n          </div>\n        </CardContent>\n      </Card>\n    </div>\n  );\n};\n\nconst loadProviderScript = (provider: string): Promise<string> => {\n  const scriptSrc = PROVIDER_SCRIPTS[provider];\n\n  if (!scriptSrc) {\n    return Promise.reject(new Error(`Unknown provider: ${provider}`));\n  }\n\n  return new Promise((resolve, reject) => {\n    const script = document.createElement(\"script\");\n    script.src = scriptSrc;\n    script.onload = () => resolve(provider);\n    script.onerror = () =>\n      reject(new Error(`Failed to load provider: ${provider}`));\n    document.head.appendChild(script);\n  });\n};\n\nexport const AgentPlayground = () => {\n  const [state, setState] = useState<{\n    loaded: string[];\n    failed: string[];\n    isLoading: boolean;\n  }>({ loaded: [], failed: [], isLoading: true });\n\n  useEffect(() => {\n    const loadAllProviders = async () => {\n      const urlProviders = new URLSearchParams(window.location.search).get(\n        \"provider\",\n      );\n      const envProviders = process.env.NEXT_PUBLIC_PROVIDER;\n\n      const providerString = urlProviders ?? envProviders;\n      if (!providerString) {\n        setState({ loaded: [], failed: [], isLoading: false });\n        return;\n      }\n\n      const providers = providerString\n        .split(\",\")\n        .map((provider) => provider.trim())\n        .filter(Boolean);\n\n      if (providers.length === 0) {\n        setState({ loaded: [], failed: [], isLoading: false });\n        return;\n      }\n\n      const results = await Promise.allSettled(\n        providers.map((provider) => loadProviderScript(provider)),\n      );\n\n      const loaded: string[] = [];\n      const failed: string[] = [];\n\n      providers.forEach((provider, index) => {\n        const result = results[index];\n        if (result.status === \"fulfilled\") {\n          loaded.push(result.value);\n        } else {\n          console.error(\n            `Failed to load provider \"${provider}\":`,\n            result.reason,\n          );\n          failed.push(provider);\n        }\n      });\n\n      setState({ loaded, failed, isLoading: false });\n    };\n\n    loadAllProviders();\n  }, []);\n\n  if (state.isLoading) {\n    return (\n      <div className=\"flex items-center justify-center min-h-[400px]\">\n        <div className=\"text-muted-foreground\">Loading providers...</div>\n      </div>\n    );\n  }\n\n  return (\n    <AgentPlaygroundContent\n      loadedProviders={state.loaded}\n      failedProviders={state.failed}\n      availableProviders={Object.keys(PROVIDER_SCRIPTS)}\n    />\n  );\n};\n"
  },
  {
    "path": "packages/gym/components/app-sidebar.tsx",
    "content": "import * as React from \"react\";\n\nimport { SearchForm } from \"@/components/search-form\";\nimport { VersionSwitcher } from \"@/components/version-switcher\";\nimport {\n  Sidebar,\n  SidebarContent,\n  SidebarGroup,\n  SidebarGroupContent,\n  SidebarGroupLabel,\n  SidebarHeader,\n  SidebarMenu,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  SidebarRail,\n} from \"@/components/ui/sidebar\";\n\nconst data = {\n  versions: [\"react-grab\", \"v0.1.0\"],\n  navMain: [\n    {\n      title: \"React Grab\",\n      url: \"#\",\n      items: [\n        {\n          title: \"Dashboard\",\n          url: \"/dashboard\",\n        },\n        {\n          title: \"Agent Playground\",\n          url: \"/playground\",\n        },\n        {\n          title: \"Freeze Demo\",\n          url: \"/freeze-demo\",\n        },\n        {\n          title: \"Login\",\n          url: \"/login\",\n        },\n      ],\n    },\n    {\n      title: \"Documentation\",\n      url: \"#\",\n      items: [\n        {\n          title: \"Getting Started\",\n          url: \"#\",\n        },\n        {\n          title: \"Installation\",\n          url: \"#\",\n        },\n        {\n          title: \"Configuration\",\n          url: \"#\",\n        },\n      ],\n    },\n    {\n      title: \"Providers\",\n      url: \"#\",\n      items: [\n        {\n          title: \"Cursor\",\n          url: \"#\",\n        },\n        {\n          title: \"Claude Code\",\n          url: \"#\",\n        },\n        {\n          title: \"OpenCode\",\n          url: \"#\",\n        },\n        {\n          title: \"Codex\",\n          url: \"#\",\n        },\n        {\n          title: \"Gemini\",\n          url: \"#\",\n        },\n        {\n          title: \"AMP\",\n          url: \"#\",\n        },\n        {\n          title: \"AMI\",\n          url: \"#\",\n        },\n        {\n          title: \"Droid\",\n          url: \"#\",\n        },\n      ],\n    },\n  ],\n};\n\nexport function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {\n  return (\n    <Sidebar {...props}>\n      <SidebarHeader>\n        <VersionSwitcher\n          versions={data.versions}\n          defaultVersion={data.versions[0]}\n        />\n        <SearchForm />\n      </SidebarHeader>\n      <SidebarContent>\n        {/* We create a SidebarGroup for each parent. */}\n        {data.navMain.map((item) => (\n          <SidebarGroup key={item.title}>\n            <SidebarGroupLabel>{item.title}</SidebarGroupLabel>\n            <SidebarGroupContent>\n              <SidebarMenu>\n                {item.items.map((navItem) => {\n                  const isDisabled = navItem.url === \"#\";\n                  return (\n                    <SidebarMenuItem key={navItem.title}>\n                      <SidebarMenuButton\n                        asChild\n                        className={\n                          isDisabled ? \"opacity-50 cursor-not-allowed\" : \"\"\n                        }\n                      >\n                        <a href={navItem.url}>{navItem.title}</a>\n                      </SidebarMenuButton>\n                    </SidebarMenuItem>\n                  );\n                })}\n              </SidebarMenu>\n            </SidebarGroupContent>\n          </SidebarGroup>\n        ))}\n      </SidebarContent>\n      <SidebarRail />\n    </Sidebar>\n  );\n}\n"
  },
  {
    "path": "packages/gym/components/chart-area-interactive.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Area, AreaChart, CartesianGrid, XAxis } from \"recharts\";\n\nimport { useIsMobile } from \"@/hooks/use-mobile\";\nimport {\n  Card,\n  CardAction,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport {\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent,\n  type ChartConfig,\n} from \"@/components/ui/chart\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport { ToggleGroup, ToggleGroupItem } from \"@/components/ui/toggle-group\";\n\nexport const description = \"An interactive area chart\";\n\nconst chartData = [\n  { date: \"2024-04-01\", desktop: 222, mobile: 150 },\n  { date: \"2024-04-02\", desktop: 97, mobile: 180 },\n  { date: \"2024-04-03\", desktop: 167, mobile: 120 },\n  { date: \"2024-04-04\", desktop: 242, mobile: 260 },\n  { date: \"2024-04-05\", desktop: 373, mobile: 290 },\n  { date: \"2024-04-06\", desktop: 301, mobile: 340 },\n  { date: \"2024-04-07\", desktop: 245, mobile: 180 },\n  { date: \"2024-04-08\", desktop: 409, mobile: 320 },\n  { date: \"2024-04-09\", desktop: 59, mobile: 110 },\n  { date: \"2024-04-10\", desktop: 261, mobile: 190 },\n  { date: \"2024-04-11\", desktop: 327, mobile: 350 },\n  { date: \"2024-04-12\", desktop: 292, mobile: 210 },\n  { date: \"2024-04-13\", desktop: 342, mobile: 380 },\n  { date: \"2024-04-14\", desktop: 137, mobile: 220 },\n  { date: \"2024-04-15\", desktop: 120, mobile: 170 },\n  { date: \"2024-04-16\", desktop: 138, mobile: 190 },\n  { date: \"2024-04-17\", desktop: 446, mobile: 360 },\n  { date: \"2024-04-18\", desktop: 364, mobile: 410 },\n  { date: \"2024-04-19\", desktop: 243, mobile: 180 },\n  { date: \"2024-04-20\", desktop: 89, mobile: 150 },\n  { date: \"2024-04-21\", desktop: 137, mobile: 200 },\n  { date: \"2024-04-22\", desktop: 224, mobile: 170 },\n  { date: \"2024-04-23\", desktop: 138, mobile: 230 },\n  { date: \"2024-04-24\", desktop: 387, mobile: 290 },\n  { date: \"2024-04-25\", desktop: 215, mobile: 250 },\n  { date: \"2024-04-26\", desktop: 75, mobile: 130 },\n  { date: \"2024-04-27\", desktop: 383, mobile: 420 },\n  { date: \"2024-04-28\", desktop: 122, mobile: 180 },\n  { date: \"2024-04-29\", desktop: 315, mobile: 240 },\n  { date: \"2024-04-30\", desktop: 454, mobile: 380 },\n  { date: \"2024-05-01\", desktop: 165, mobile: 220 },\n  { date: \"2024-05-02\", desktop: 293, mobile: 310 },\n  { date: \"2024-05-03\", desktop: 247, mobile: 190 },\n  { date: \"2024-05-04\", desktop: 385, mobile: 420 },\n  { date: \"2024-05-05\", desktop: 481, mobile: 390 },\n  { date: \"2024-05-06\", desktop: 498, mobile: 520 },\n  { date: \"2024-05-07\", desktop: 388, mobile: 300 },\n  { date: \"2024-05-08\", desktop: 149, mobile: 210 },\n  { date: \"2024-05-09\", desktop: 227, mobile: 180 },\n  { date: \"2024-05-10\", desktop: 293, mobile: 330 },\n  { date: \"2024-05-11\", desktop: 335, mobile: 270 },\n  { date: \"2024-05-12\", desktop: 197, mobile: 240 },\n  { date: \"2024-05-13\", desktop: 197, mobile: 160 },\n  { date: \"2024-05-14\", desktop: 448, mobile: 490 },\n  { date: \"2024-05-15\", desktop: 473, mobile: 380 },\n  { date: \"2024-05-16\", desktop: 338, mobile: 400 },\n  { date: \"2024-05-17\", desktop: 499, mobile: 420 },\n  { date: \"2024-05-18\", desktop: 315, mobile: 350 },\n  { date: \"2024-05-19\", desktop: 235, mobile: 180 },\n  { date: \"2024-05-20\", desktop: 177, mobile: 230 },\n  { date: \"2024-05-21\", desktop: 82, mobile: 140 },\n  { date: \"2024-05-22\", desktop: 81, mobile: 120 },\n  { date: \"2024-05-23\", desktop: 252, mobile: 290 },\n  { date: \"2024-05-24\", desktop: 294, mobile: 220 },\n  { date: \"2024-05-25\", desktop: 201, mobile: 250 },\n  { date: \"2024-05-26\", desktop: 213, mobile: 170 },\n  { date: \"2024-05-27\", desktop: 420, mobile: 460 },\n  { date: \"2024-05-28\", desktop: 233, mobile: 190 },\n  { date: \"2024-05-29\", desktop: 78, mobile: 130 },\n  { date: \"2024-05-30\", desktop: 340, mobile: 280 },\n  { date: \"2024-05-31\", desktop: 178, mobile: 230 },\n  { date: \"2024-06-01\", desktop: 178, mobile: 200 },\n  { date: \"2024-06-02\", desktop: 470, mobile: 410 },\n  { date: \"2024-06-03\", desktop: 103, mobile: 160 },\n  { date: \"2024-06-04\", desktop: 439, mobile: 380 },\n  { date: \"2024-06-05\", desktop: 88, mobile: 140 },\n  { date: \"2024-06-06\", desktop: 294, mobile: 250 },\n  { date: \"2024-06-07\", desktop: 323, mobile: 370 },\n  { date: \"2024-06-08\", desktop: 385, mobile: 320 },\n  { date: \"2024-06-09\", desktop: 438, mobile: 480 },\n  { date: \"2024-06-10\", desktop: 155, mobile: 200 },\n  { date: \"2024-06-11\", desktop: 92, mobile: 150 },\n  { date: \"2024-06-12\", desktop: 492, mobile: 420 },\n  { date: \"2024-06-13\", desktop: 81, mobile: 130 },\n  { date: \"2024-06-14\", desktop: 426, mobile: 380 },\n  { date: \"2024-06-15\", desktop: 307, mobile: 350 },\n  { date: \"2024-06-16\", desktop: 371, mobile: 310 },\n  { date: \"2024-06-17\", desktop: 475, mobile: 520 },\n  { date: \"2024-06-18\", desktop: 107, mobile: 170 },\n  { date: \"2024-06-19\", desktop: 341, mobile: 290 },\n  { date: \"2024-06-20\", desktop: 408, mobile: 450 },\n  { date: \"2024-06-21\", desktop: 169, mobile: 210 },\n  { date: \"2024-06-22\", desktop: 317, mobile: 270 },\n  { date: \"2024-06-23\", desktop: 480, mobile: 530 },\n  { date: \"2024-06-24\", desktop: 132, mobile: 180 },\n  { date: \"2024-06-25\", desktop: 141, mobile: 190 },\n  { date: \"2024-06-26\", desktop: 434, mobile: 380 },\n  { date: \"2024-06-27\", desktop: 448, mobile: 490 },\n  { date: \"2024-06-28\", desktop: 149, mobile: 200 },\n  { date: \"2024-06-29\", desktop: 103, mobile: 160 },\n  { date: \"2024-06-30\", desktop: 446, mobile: 400 },\n];\n\nconst chartConfig = {\n  visitors: {\n    label: \"Visitors\",\n  },\n  desktop: {\n    label: \"Desktop\",\n    color: \"var(--primary)\",\n  },\n  mobile: {\n    label: \"Mobile\",\n    color: \"var(--primary)\",\n  },\n} satisfies ChartConfig;\n\nexport function ChartAreaInteractive() {\n  const isMobile = useIsMobile();\n  const [timeRange, setTimeRange] = React.useState(\"90d\");\n\n  React.useEffect(() => {\n    if (isMobile) {\n      setTimeRange(\"7d\");\n    }\n  }, [isMobile]);\n\n  const filteredData = chartData.filter((item) => {\n    const date = new Date(item.date);\n    const referenceDate = new Date(\"2024-06-30\");\n    let daysToSubtract = 90;\n    if (timeRange === \"30d\") {\n      daysToSubtract = 30;\n    } else if (timeRange === \"7d\") {\n      daysToSubtract = 7;\n    }\n    const startDate = new Date(referenceDate);\n    startDate.setDate(startDate.getDate() - daysToSubtract);\n    return date >= startDate;\n  });\n\n  return (\n    <Card className=\"@container/card\">\n      <CardHeader>\n        <CardTitle>Total Visitors</CardTitle>\n        <CardDescription>\n          <span className=\"hidden @[540px]/card:block\">\n            Total for the last 3 months\n          </span>\n          <span className=\"@[540px]/card:hidden\">Last 3 months</span>\n        </CardDescription>\n        <CardAction>\n          <ToggleGroup\n            type=\"single\"\n            value={timeRange}\n            onValueChange={setTimeRange}\n            variant=\"outline\"\n            className=\"hidden *:data-[slot=toggle-group-item]:!px-4 @[767px]/card:flex\"\n          >\n            <ToggleGroupItem value=\"90d\">Last 3 months</ToggleGroupItem>\n            <ToggleGroupItem value=\"30d\">Last 30 days</ToggleGroupItem>\n            <ToggleGroupItem value=\"7d\">Last 7 days</ToggleGroupItem>\n          </ToggleGroup>\n          <Select value={timeRange} onValueChange={setTimeRange}>\n            <SelectTrigger\n              className=\"flex w-40 **:data-[slot=select-value]:block **:data-[slot=select-value]:truncate @[767px]/card:hidden\"\n              size=\"sm\"\n              aria-label=\"Select a value\"\n            >\n              <SelectValue placeholder=\"Last 3 months\" />\n            </SelectTrigger>\n            <SelectContent className=\"rounded-xl\">\n              <SelectItem value=\"90d\" className=\"rounded-lg\">\n                Last 3 months\n              </SelectItem>\n              <SelectItem value=\"30d\" className=\"rounded-lg\">\n                Last 30 days\n              </SelectItem>\n              <SelectItem value=\"7d\" className=\"rounded-lg\">\n                Last 7 days\n              </SelectItem>\n            </SelectContent>\n          </Select>\n        </CardAction>\n      </CardHeader>\n      <CardContent className=\"px-2 pt-4 sm:px-6 sm:pt-6\">\n        <ChartContainer\n          config={chartConfig}\n          className=\"aspect-auto h-[250px] w-full\"\n        >\n          <AreaChart data={filteredData}>\n            <defs>\n              <linearGradient id=\"fillDesktop\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n                <stop\n                  offset=\"5%\"\n                  stopColor=\"var(--color-desktop)\"\n                  stopOpacity={1.0}\n                />\n                <stop\n                  offset=\"95%\"\n                  stopColor=\"var(--color-desktop)\"\n                  stopOpacity={0.1}\n                />\n              </linearGradient>\n              <linearGradient id=\"fillMobile\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n                <stop\n                  offset=\"5%\"\n                  stopColor=\"var(--color-mobile)\"\n                  stopOpacity={0.8}\n                />\n                <stop\n                  offset=\"95%\"\n                  stopColor=\"var(--color-mobile)\"\n                  stopOpacity={0.1}\n                />\n              </linearGradient>\n            </defs>\n            <CartesianGrid vertical={false} />\n            <XAxis\n              dataKey=\"date\"\n              tickLine={false}\n              axisLine={false}\n              tickMargin={8}\n              minTickGap={32}\n              tickFormatter={(value) => {\n                const date = new Date(value);\n                return date.toLocaleDateString(\"en-US\", {\n                  month: \"short\",\n                  day: \"numeric\",\n                });\n              }}\n            />\n            <ChartTooltip\n              cursor={false}\n              content={\n                <ChartTooltipContent\n                  labelFormatter={(value) => {\n                    return new Date(value).toLocaleDateString(\"en-US\", {\n                      month: \"short\",\n                      day: \"numeric\",\n                    });\n                  }}\n                  indicator=\"dot\"\n                />\n              }\n            />\n            <Area\n              dataKey=\"mobile\"\n              type=\"natural\"\n              fill=\"url(#fillMobile)\"\n              stroke=\"var(--color-mobile)\"\n              stackId=\"a\"\n            />\n            <Area\n              dataKey=\"desktop\"\n              type=\"natural\"\n              fill=\"url(#fillDesktop)\"\n              stroke=\"var(--color-desktop)\"\n              stackId=\"a\"\n            />\n          </AreaChart>\n        </ChartContainer>\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "packages/gym/components/counter.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\nconst COUNTER_INTERVAL_MS = 100;\n\nexport const Counter = () => {\n  const [count, setCount] = useState(0);\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setCount((previousCount) => previousCount + 1);\n    }, COUNTER_INTERVAL_MS);\n\n    return () => clearInterval(interval);\n  }, []);\n\n  return (\n    <div className=\"px-4 lg:px-6\">\n      <div className=\"rounded-lg border bg-card p-4 text-card-foreground shadow-sm\">\n        <div className=\"text-sm font-medium text-muted-foreground\">Counter</div>\n        <div className=\"text-2xl font-bold tabular-nums\">{count}</div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/gym/components/data-table.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  closestCenter,\n  DndContext,\n  KeyboardSensor,\n  MouseSensor,\n  TouchSensor,\n  useSensor,\n  useSensors,\n  type DragEndEvent,\n  type UniqueIdentifier,\n} from \"@dnd-kit/core\";\nimport { restrictToVerticalAxis } from \"@dnd-kit/modifiers\";\nimport {\n  arrayMove,\n  SortableContext,\n  useSortable,\n  verticalListSortingStrategy,\n} from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport {\n  IconChevronDown,\n  IconChevronLeft,\n  IconChevronRight,\n  IconChevronsLeft,\n  IconChevronsRight,\n  IconCircleCheckFilled,\n  IconDotsVertical,\n  IconGripVertical,\n  IconLayoutColumns,\n  IconLoader,\n  IconPlus,\n  IconTrendingUp,\n} from \"@tabler/icons-react\";\nimport {\n  flexRender,\n  getCoreRowModel,\n  getFacetedRowModel,\n  getFacetedUniqueValues,\n  getFilteredRowModel,\n  getPaginationRowModel,\n  getSortedRowModel,\n  useReactTable,\n  type ColumnDef,\n  type ColumnFiltersState,\n  type Row,\n  type SortingState,\n  type VisibilityState,\n} from \"@tanstack/react-table\";\nimport { Area, AreaChart, CartesianGrid, XAxis } from \"recharts\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nimport { useIsMobile } from \"@/hooks/use-mobile\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent,\n  type ChartConfig,\n} from \"@/components/ui/chart\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport {\n  Drawer,\n  DrawerClose,\n  DrawerContent,\n  DrawerDescription,\n  DrawerFooter,\n  DrawerHeader,\n  DrawerTitle,\n  DrawerTrigger,\n} from \"@/components/ui/drawer\";\nimport {\n  DropdownMenu,\n  DropdownMenuCheckboxItem,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport { Separator } from \"@/components/ui/separator\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\n\nexport const schema = z.object({\n  id: z.number(),\n  header: z.string(),\n  type: z.string(),\n  status: z.string(),\n  target: z.string(),\n  limit: z.string(),\n  reviewer: z.string(),\n});\n\n// Create a separate component for the drag handle\nfunction DragHandle({ id }: { id: number }) {\n  const { attributes, listeners } = useSortable({\n    id,\n  });\n\n  return (\n    <Button\n      {...attributes}\n      {...listeners}\n      variant=\"ghost\"\n      size=\"icon\"\n      className=\"text-muted-foreground size-7 hover:bg-transparent\"\n    >\n      <IconGripVertical className=\"text-muted-foreground size-3\" />\n      <span className=\"sr-only\">Drag to reorder</span>\n    </Button>\n  );\n}\n\nconst columns: ColumnDef<z.infer<typeof schema>>[] = [\n  {\n    id: \"drag\",\n    header: () => null,\n    cell: ({ row }) => <DragHandle id={row.original.id} />,\n  },\n  {\n    id: \"select\",\n    header: ({ table }) => (\n      <div className=\"flex items-center justify-center\">\n        <Checkbox\n          checked={\n            table.getIsAllPageRowsSelected() ||\n            (table.getIsSomePageRowsSelected() && \"indeterminate\")\n          }\n          onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}\n          aria-label=\"Select all\"\n        />\n      </div>\n    ),\n    cell: ({ row }) => (\n      <div className=\"flex items-center justify-center\">\n        <Checkbox\n          checked={row.getIsSelected()}\n          onCheckedChange={(value) => row.toggleSelected(!!value)}\n          aria-label=\"Select row\"\n        />\n      </div>\n    ),\n    enableSorting: false,\n    enableHiding: false,\n  },\n  {\n    accessorKey: \"header\",\n    header: \"Header\",\n    cell: ({ row }) => {\n      return <TableCellViewer item={row.original} />;\n    },\n    enableHiding: false,\n  },\n  {\n    accessorKey: \"type\",\n    header: \"Section Type\",\n    cell: ({ row }) => (\n      <div className=\"w-32\">\n        <Badge variant=\"outline\" className=\"text-muted-foreground px-1.5\">\n          {row.original.type}\n        </Badge>\n      </div>\n    ),\n  },\n  {\n    accessorKey: \"status\",\n    header: \"Status\",\n    cell: ({ row }) => (\n      <Badge variant=\"outline\" className=\"text-muted-foreground px-1.5\">\n        {row.original.status === \"Done\" ? (\n          <IconCircleCheckFilled className=\"fill-green-500 dark:fill-green-400\" />\n        ) : (\n          <IconLoader />\n        )}\n        {row.original.status}\n      </Badge>\n    ),\n  },\n  {\n    accessorKey: \"target\",\n    header: () => <div className=\"w-full text-right\">Target</div>,\n    cell: ({ row }) => (\n      <form\n        onSubmit={(e) => {\n          e.preventDefault();\n          toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {\n            loading: `Saving ${row.original.header}`,\n            success: \"Done\",\n            error: \"Error\",\n          });\n        }}\n      >\n        <Label htmlFor={`${row.original.id}-target`} className=\"sr-only\">\n          Target\n        </Label>\n        <Input\n          className=\"hover:bg-input/30 focus-visible:bg-background dark:hover:bg-input/30 dark:focus-visible:bg-input/30 h-8 w-16 border-transparent bg-transparent text-right shadow-none focus-visible:border dark:bg-transparent\"\n          defaultValue={row.original.target}\n          id={`${row.original.id}-target`}\n        />\n      </form>\n    ),\n  },\n  {\n    accessorKey: \"limit\",\n    header: () => <div className=\"w-full text-right\">Limit</div>,\n    cell: ({ row }) => (\n      <form\n        onSubmit={(e) => {\n          e.preventDefault();\n          toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {\n            loading: `Saving ${row.original.header}`,\n            success: \"Done\",\n            error: \"Error\",\n          });\n        }}\n      >\n        <Label htmlFor={`${row.original.id}-limit`} className=\"sr-only\">\n          Limit\n        </Label>\n        <Input\n          className=\"hover:bg-input/30 focus-visible:bg-background dark:hover:bg-input/30 dark:focus-visible:bg-input/30 h-8 w-16 border-transparent bg-transparent text-right shadow-none focus-visible:border dark:bg-transparent\"\n          defaultValue={row.original.limit}\n          id={`${row.original.id}-limit`}\n        />\n      </form>\n    ),\n  },\n  {\n    accessorKey: \"reviewer\",\n    header: \"Reviewer\",\n    cell: ({ row }) => {\n      const isAssigned = row.original.reviewer !== \"Assign reviewer\";\n\n      if (isAssigned) {\n        return row.original.reviewer;\n      }\n\n      return (\n        <>\n          <Label htmlFor={`${row.original.id}-reviewer`} className=\"sr-only\">\n            Reviewer\n          </Label>\n          <Select>\n            <SelectTrigger\n              className=\"w-38 **:data-[slot=select-value]:block **:data-[slot=select-value]:truncate\"\n              size=\"sm\"\n              id={`${row.original.id}-reviewer`}\n            >\n              <SelectValue placeholder=\"Assign reviewer\" />\n            </SelectTrigger>\n            <SelectContent align=\"end\">\n              <SelectItem value=\"Eddie Lake\">Eddie Lake</SelectItem>\n              <SelectItem value=\"Jamik Tashpulatov\">\n                Jamik Tashpulatov\n              </SelectItem>\n            </SelectContent>\n          </Select>\n        </>\n      );\n    },\n  },\n  {\n    id: \"actions\",\n    cell: () => (\n      <DropdownMenu>\n        <DropdownMenuTrigger asChild>\n          <Button\n            variant=\"ghost\"\n            className=\"data-[state=open]:bg-muted text-muted-foreground flex size-8\"\n            size=\"icon\"\n          >\n            <IconDotsVertical />\n            <span className=\"sr-only\">Open menu</span>\n          </Button>\n        </DropdownMenuTrigger>\n        <DropdownMenuContent align=\"end\" className=\"w-32\">\n          <DropdownMenuItem>Edit</DropdownMenuItem>\n          <DropdownMenuItem>Make a copy</DropdownMenuItem>\n          <DropdownMenuItem>Favorite</DropdownMenuItem>\n          <DropdownMenuSeparator />\n          <DropdownMenuItem variant=\"destructive\">Delete</DropdownMenuItem>\n        </DropdownMenuContent>\n      </DropdownMenu>\n    ),\n  },\n];\n\nfunction DraggableRow({ row }: { row: Row<z.infer<typeof schema>> }) {\n  const { transform, transition, setNodeRef, isDragging } = useSortable({\n    id: row.original.id,\n  });\n\n  return (\n    <TableRow\n      data-state={row.getIsSelected() && \"selected\"}\n      data-dragging={isDragging}\n      ref={setNodeRef}\n      className=\"relative z-0 data-[dragging=true]:z-10 data-[dragging=true]:opacity-80\"\n      style={{\n        transform: CSS.Transform.toString(transform),\n        transition: transition,\n      }}\n    >\n      {row.getVisibleCells().map((cell) => (\n        <TableCell key={cell.id}>\n          {flexRender(cell.column.columnDef.cell, cell.getContext())}\n        </TableCell>\n      ))}\n    </TableRow>\n  );\n}\n\nexport function DataTable({\n  data: initialData,\n}: {\n  data: z.infer<typeof schema>[];\n}) {\n  const [data, setData] = React.useState(() => initialData);\n  const [rowSelection, setRowSelection] = React.useState({});\n  const [columnVisibility, setColumnVisibility] =\n    React.useState<VisibilityState>({});\n  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(\n    [],\n  );\n  const [sorting, setSorting] = React.useState<SortingState>([]);\n  const [pagination, setPagination] = React.useState({\n    pageIndex: 0,\n    pageSize: 10,\n  });\n  const sortableId = React.useId();\n  const sensors = useSensors(\n    useSensor(MouseSensor, {}),\n    useSensor(TouchSensor, {}),\n    useSensor(KeyboardSensor, {}),\n  );\n\n  const dataIds = React.useMemo<UniqueIdentifier[]>(\n    () => data?.map(({ id }) => id) || [],\n    [data],\n  );\n\n  const table = useReactTable({\n    // eslint-disable-line react-hooks/incompatible-library\n    data,\n    columns,\n    state: {\n      sorting,\n      columnVisibility,\n      rowSelection,\n      columnFilters,\n      pagination,\n    },\n    getRowId: (row) => row.id.toString(),\n    enableRowSelection: true,\n    onRowSelectionChange: setRowSelection,\n    onSortingChange: setSorting,\n    onColumnFiltersChange: setColumnFilters,\n    onColumnVisibilityChange: setColumnVisibility,\n    onPaginationChange: setPagination,\n    getCoreRowModel: getCoreRowModel(),\n    getFilteredRowModel: getFilteredRowModel(),\n    getPaginationRowModel: getPaginationRowModel(),\n    getSortedRowModel: getSortedRowModel(),\n    getFacetedRowModel: getFacetedRowModel(),\n    getFacetedUniqueValues: getFacetedUniqueValues(),\n  });\n\n  function handleDragEnd(event: DragEndEvent) {\n    const { active, over } = event;\n    if (active && over && active.id !== over.id) {\n      setData((data) => {\n        const oldIndex = dataIds.indexOf(active.id);\n        const newIndex = dataIds.indexOf(over.id);\n        return arrayMove(data, oldIndex, newIndex);\n      });\n    }\n  }\n\n  return (\n    <Tabs\n      defaultValue=\"outline\"\n      className=\"w-full flex-col justify-start gap-6\"\n    >\n      <div className=\"flex items-center justify-between px-4 lg:px-6\">\n        <Label htmlFor=\"view-selector\" className=\"sr-only\">\n          View\n        </Label>\n        <Select defaultValue=\"outline\">\n          <SelectTrigger\n            className=\"flex w-fit @4xl/main:hidden\"\n            size=\"sm\"\n            id=\"view-selector\"\n          >\n            <SelectValue placeholder=\"Select a view\" />\n          </SelectTrigger>\n          <SelectContent>\n            <SelectItem value=\"outline\">Outline</SelectItem>\n            <SelectItem value=\"past-performance\">Past Performance</SelectItem>\n            <SelectItem value=\"key-personnel\">Key Personnel</SelectItem>\n            <SelectItem value=\"focus-documents\">Focus Documents</SelectItem>\n          </SelectContent>\n        </Select>\n        <TabsList className=\"**:data-[slot=badge]:bg-muted-foreground/30 hidden **:data-[slot=badge]:size-5 **:data-[slot=badge]:rounded-full **:data-[slot=badge]:px-1 @4xl/main:flex\">\n          <TabsTrigger value=\"outline\">Outline</TabsTrigger>\n          <TabsTrigger value=\"past-performance\">\n            Past Performance <Badge variant=\"secondary\">3</Badge>\n          </TabsTrigger>\n          <TabsTrigger value=\"key-personnel\">\n            Key Personnel <Badge variant=\"secondary\">2</Badge>\n          </TabsTrigger>\n          <TabsTrigger value=\"focus-documents\">Focus Documents</TabsTrigger>\n        </TabsList>\n        <div className=\"flex items-center gap-2\">\n          <DropdownMenu>\n            <DropdownMenuTrigger asChild>\n              <Button variant=\"outline\" size=\"sm\">\n                <IconLayoutColumns />\n                <span className=\"hidden lg:inline\">Customize Columns</span>\n                <span className=\"lg:hidden\">Columns</span>\n                <IconChevronDown />\n              </Button>\n            </DropdownMenuTrigger>\n            <DropdownMenuContent align=\"end\" className=\"w-56\">\n              {table\n                .getAllColumns()\n                .filter(\n                  (column) =>\n                    typeof column.accessorFn !== \"undefined\" &&\n                    column.getCanHide(),\n                )\n                .map((column) => {\n                  return (\n                    <DropdownMenuCheckboxItem\n                      key={column.id}\n                      className=\"capitalize\"\n                      checked={column.getIsVisible()}\n                      onCheckedChange={(value) =>\n                        column.toggleVisibility(!!value)\n                      }\n                    >\n                      {column.id}\n                    </DropdownMenuCheckboxItem>\n                  );\n                })}\n            </DropdownMenuContent>\n          </DropdownMenu>\n          <Button variant=\"outline\" size=\"sm\">\n            <IconPlus />\n            <span className=\"hidden lg:inline\">Add Section</span>\n          </Button>\n        </div>\n      </div>\n      <TabsContent\n        value=\"outline\"\n        className=\"relative flex flex-col gap-4 overflow-auto px-4 lg:px-6\"\n      >\n        <div className=\"overflow-hidden rounded-lg border\">\n          <DndContext\n            collisionDetection={closestCenter}\n            modifiers={[restrictToVerticalAxis]}\n            onDragEnd={handleDragEnd}\n            sensors={sensors}\n            id={sortableId}\n          >\n            <Table>\n              <TableHeader className=\"bg-muted sticky top-0 z-10\">\n                {table.getHeaderGroups().map((headerGroup) => (\n                  <TableRow key={headerGroup.id}>\n                    {headerGroup.headers.map((header) => {\n                      return (\n                        <TableHead key={header.id} colSpan={header.colSpan}>\n                          {header.isPlaceholder\n                            ? null\n                            : flexRender(\n                                header.column.columnDef.header,\n                                header.getContext(),\n                              )}\n                        </TableHead>\n                      );\n                    })}\n                  </TableRow>\n                ))}\n              </TableHeader>\n              <TableBody className=\"**:data-[slot=table-cell]:first:w-8\">\n                {table.getRowModel().rows?.length ? (\n                  <SortableContext\n                    items={dataIds}\n                    strategy={verticalListSortingStrategy}\n                  >\n                    {table.getRowModel().rows.map((row) => (\n                      <DraggableRow key={row.id} row={row} />\n                    ))}\n                  </SortableContext>\n                ) : (\n                  <TableRow>\n                    <TableCell\n                      colSpan={columns.length}\n                      className=\"h-24 text-center\"\n                    >\n                      No results.\n                    </TableCell>\n                  </TableRow>\n                )}\n              </TableBody>\n            </Table>\n          </DndContext>\n        </div>\n        <div className=\"flex items-center justify-between px-4\">\n          <div className=\"text-muted-foreground hidden flex-1 text-sm lg:flex\">\n            {table.getFilteredSelectedRowModel().rows.length} of{\" \"}\n            {table.getFilteredRowModel().rows.length} row(s) selected.\n          </div>\n          <div className=\"flex w-full items-center gap-8 lg:w-fit\">\n            <div className=\"hidden items-center gap-2 lg:flex\">\n              <Label htmlFor=\"rows-per-page\" className=\"text-sm font-medium\">\n                Rows per page\n              </Label>\n              <Select\n                value={`${table.getState().pagination.pageSize}`}\n                onValueChange={(value) => {\n                  table.setPageSize(Number(value));\n                }}\n              >\n                <SelectTrigger size=\"sm\" className=\"w-20\" id=\"rows-per-page\">\n                  <SelectValue\n                    placeholder={table.getState().pagination.pageSize}\n                  />\n                </SelectTrigger>\n                <SelectContent side=\"top\">\n                  {[10, 20, 30, 40, 50].map((pageSize) => (\n                    <SelectItem key={pageSize} value={`${pageSize}`}>\n                      {pageSize}\n                    </SelectItem>\n                  ))}\n                </SelectContent>\n              </Select>\n            </div>\n            <div className=\"flex w-fit items-center justify-center text-sm font-medium\">\n              Page {table.getState().pagination.pageIndex + 1} of{\" \"}\n              {table.getPageCount()}\n            </div>\n            <div className=\"ml-auto flex items-center gap-2 lg:ml-0\">\n              <Button\n                variant=\"outline\"\n                className=\"hidden h-8 w-8 p-0 lg:flex\"\n                onClick={() => table.setPageIndex(0)}\n                disabled={!table.getCanPreviousPage()}\n              >\n                <span className=\"sr-only\">Go to first page</span>\n                <IconChevronsLeft />\n              </Button>\n              <Button\n                variant=\"outline\"\n                className=\"size-8\"\n                size=\"icon\"\n                onClick={() => table.previousPage()}\n                disabled={!table.getCanPreviousPage()}\n              >\n                <span className=\"sr-only\">Go to previous page</span>\n                <IconChevronLeft />\n              </Button>\n              <Button\n                variant=\"outline\"\n                className=\"size-8\"\n                size=\"icon\"\n                onClick={() => table.nextPage()}\n                disabled={!table.getCanNextPage()}\n              >\n                <span className=\"sr-only\">Go to next page</span>\n                <IconChevronRight />\n              </Button>\n              <Button\n                variant=\"outline\"\n                className=\"hidden size-8 lg:flex\"\n                size=\"icon\"\n                onClick={() => table.setPageIndex(table.getPageCount() - 1)}\n                disabled={!table.getCanNextPage()}\n              >\n                <span className=\"sr-only\">Go to last page</span>\n                <IconChevronsRight />\n              </Button>\n            </div>\n          </div>\n        </div>\n      </TabsContent>\n      <TabsContent\n        value=\"past-performance\"\n        className=\"flex flex-col px-4 lg:px-6\"\n      >\n        <div className=\"aspect-video w-full flex-1 rounded-lg border border-dashed\"></div>\n      </TabsContent>\n      <TabsContent value=\"key-personnel\" className=\"flex flex-col px-4 lg:px-6\">\n        <div className=\"aspect-video w-full flex-1 rounded-lg border border-dashed\"></div>\n      </TabsContent>\n      <TabsContent\n        value=\"focus-documents\"\n        className=\"flex flex-col px-4 lg:px-6\"\n      >\n        <div className=\"aspect-video w-full flex-1 rounded-lg border border-dashed\"></div>\n      </TabsContent>\n    </Tabs>\n  );\n}\n\nconst chartData = [\n  { month: \"January\", desktop: 186, mobile: 80 },\n  { month: \"February\", desktop: 305, mobile: 200 },\n  { month: \"March\", desktop: 237, mobile: 120 },\n  { month: \"April\", desktop: 73, mobile: 190 },\n  { month: \"May\", desktop: 209, mobile: 130 },\n  { month: \"June\", desktop: 214, mobile: 140 },\n];\n\nconst chartConfig = {\n  desktop: {\n    label: \"Desktop\",\n    color: \"var(--primary)\",\n  },\n  mobile: {\n    label: \"Mobile\",\n    color: \"var(--primary)\",\n  },\n} satisfies ChartConfig;\n\nfunction TableCellViewer({ item }: { item: z.infer<typeof schema> }) {\n  const isMobile = useIsMobile();\n\n  return (\n    <Drawer direction={isMobile ? \"bottom\" : \"right\"}>\n      <DrawerTrigger asChild>\n        <Button variant=\"link\" className=\"text-foreground w-fit px-0 text-left\">\n          {item.header}\n        </Button>\n      </DrawerTrigger>\n      <DrawerContent>\n        <DrawerHeader className=\"gap-1\">\n          <DrawerTitle>{item.header}</DrawerTitle>\n          <DrawerDescription>\n            Showing total visitors for the last 6 months\n          </DrawerDescription>\n        </DrawerHeader>\n        <div className=\"flex flex-col gap-4 overflow-y-auto px-4 text-sm\">\n          {!isMobile && (\n            <>\n              <ChartContainer config={chartConfig}>\n                <AreaChart\n                  accessibilityLayer\n                  data={chartData}\n                  margin={{\n                    left: 0,\n                    right: 10,\n                  }}\n                >\n                  <CartesianGrid vertical={false} />\n                  <XAxis\n                    dataKey=\"month\"\n                    tickLine={false}\n                    axisLine={false}\n                    tickMargin={8}\n                    tickFormatter={(value) => value.slice(0, 3)}\n                    hide\n                  />\n                  <ChartTooltip\n                    cursor={false}\n                    content={<ChartTooltipContent indicator=\"dot\" />}\n                  />\n                  <Area\n                    dataKey=\"mobile\"\n                    type=\"natural\"\n                    fill=\"var(--color-mobile)\"\n                    fillOpacity={0.6}\n                    stroke=\"var(--color-mobile)\"\n                    stackId=\"a\"\n                  />\n                  <Area\n                    dataKey=\"desktop\"\n                    type=\"natural\"\n                    fill=\"var(--color-desktop)\"\n                    fillOpacity={0.4}\n                    stroke=\"var(--color-desktop)\"\n                    stackId=\"a\"\n                  />\n                </AreaChart>\n              </ChartContainer>\n              <Separator />\n              <div className=\"grid gap-2\">\n                <div className=\"flex gap-2 leading-none font-medium\">\n                  Trending up by 5.2% this month{\" \"}\n                  <IconTrendingUp className=\"size-4\" />\n                </div>\n                <div className=\"text-muted-foreground\">\n                  Showing total visitors for the last 6 months. This is just\n                  some random text to test the layout. It spans multiple lines\n                  and should wrap around.\n                </div>\n              </div>\n              <Separator />\n            </>\n          )}\n          <form className=\"flex flex-col gap-4\">\n            <div className=\"flex flex-col gap-3\">\n              <Label htmlFor=\"header\">Header</Label>\n              <Input id=\"header\" defaultValue={item.header} />\n            </div>\n            <div className=\"grid grid-cols-2 gap-4\">\n              <div className=\"flex flex-col gap-3\">\n                <Label htmlFor=\"type\">Type</Label>\n                <Select defaultValue={item.type}>\n                  <SelectTrigger id=\"type\" className=\"w-full\">\n                    <SelectValue placeholder=\"Select a type\" />\n                  </SelectTrigger>\n                  <SelectContent>\n                    <SelectItem value=\"Table of Contents\">\n                      Table of Contents\n                    </SelectItem>\n                    <SelectItem value=\"Executive Summary\">\n                      Executive Summary\n                    </SelectItem>\n                    <SelectItem value=\"Technical Approach\">\n                      Technical Approach\n                    </SelectItem>\n                    <SelectItem value=\"Design\">Design</SelectItem>\n                    <SelectItem value=\"Capabilities\">Capabilities</SelectItem>\n                    <SelectItem value=\"Focus Documents\">\n                      Focus Documents\n                    </SelectItem>\n                    <SelectItem value=\"Narrative\">Narrative</SelectItem>\n                    <SelectItem value=\"Cover Page\">Cover Page</SelectItem>\n                  </SelectContent>\n                </Select>\n              </div>\n              <div className=\"flex flex-col gap-3\">\n                <Label htmlFor=\"status\">Status</Label>\n                <Select defaultValue={item.status}>\n                  <SelectTrigger id=\"status\" className=\"w-full\">\n                    <SelectValue placeholder=\"Select a status\" />\n                  </SelectTrigger>\n                  <SelectContent>\n                    <SelectItem value=\"Done\">Done</SelectItem>\n                    <SelectItem value=\"In Progress\">In Progress</SelectItem>\n                    <SelectItem value=\"Not Started\">Not Started</SelectItem>\n                  </SelectContent>\n                </Select>\n              </div>\n            </div>\n            <div className=\"grid grid-cols-2 gap-4\">\n              <div className=\"flex flex-col gap-3\">\n                <Label htmlFor=\"target\">Target</Label>\n                <Input id=\"target\" defaultValue={item.target} />\n              </div>\n              <div className=\"flex flex-col gap-3\">\n                <Label htmlFor=\"limit\">Limit</Label>\n                <Input id=\"limit\" defaultValue={item.limit} />\n              </div>\n            </div>\n            <div className=\"flex flex-col gap-3\">\n              <Label htmlFor=\"reviewer\">Reviewer</Label>\n              <Select defaultValue={item.reviewer}>\n                <SelectTrigger id=\"reviewer\" className=\"w-full\">\n                  <SelectValue placeholder=\"Select a reviewer\" />\n                </SelectTrigger>\n                <SelectContent>\n                  <SelectItem value=\"Eddie Lake\">Eddie Lake</SelectItem>\n                  <SelectItem value=\"Jamik Tashpulatov\">\n                    Jamik Tashpulatov\n                  </SelectItem>\n                  <SelectItem value=\"Emily Whalen\">Emily Whalen</SelectItem>\n                </SelectContent>\n              </Select>\n            </div>\n          </form>\n        </div>\n        <DrawerFooter>\n          <Button>Submit</Button>\n          <DrawerClose asChild>\n            <Button variant=\"outline\">Done</Button>\n          </DrawerClose>\n        </DrawerFooter>\n      </DrawerContent>\n    </Drawer>\n  );\n}\n"
  },
  {
    "path": "packages/gym/components/login-form.tsx",
    "content": "import { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport {\n  Field,\n  FieldDescription,\n  FieldGroup,\n  FieldLabel,\n} from \"@/components/ui/field\";\nimport { Input } from \"@/components/ui/input\";\n\nexport function LoginForm({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div className={cn(\"flex flex-col gap-6\", className)} {...props}>\n      <Card>\n        <CardHeader>\n          <CardTitle>Login to your account</CardTitle>\n          <CardDescription>\n            Enter your email below to login to your account\n          </CardDescription>\n        </CardHeader>\n        <CardContent>\n          <form>\n            <FieldGroup>\n              <Field>\n                <FieldLabel htmlFor=\"email\">Email</FieldLabel>\n                <Input\n                  id=\"email\"\n                  type=\"email\"\n                  placeholder=\"m@example.com\"\n                  required\n                />\n              </Field>\n              <Field>\n                <div className=\"flex items-center\">\n                  <FieldLabel htmlFor=\"password\">Password</FieldLabel>\n                  <a\n                    href=\"#\"\n                    className=\"ml-auto inline-block text-sm underline-offset-4 hover:underline\"\n                  >\n                    Forgot your password?\n                  </a>\n                </div>\n                <Input id=\"password\" type=\"password\" required />\n              </Field>\n              <Field>\n                <Button type=\"submit\">Login</Button>\n                <Button variant=\"outline\" type=\"button\">\n                  Login with Google\n                </Button>\n                <FieldDescription className=\"text-center\">\n                  Don&apos;t have an account? <a href=\"#\">Sign up</a>\n                </FieldDescription>\n              </Field>\n            </FieldGroup>\n          </form>\n        </CardContent>\n      </Card>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/gym/components/nav-user.tsx",
    "content": "\"use client\";\n\nimport {\n  IconCreditCard,\n  IconDotsVertical,\n  IconLogout,\n  IconNotification,\n  IconUserCircle,\n} from \"@tabler/icons-react\";\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport {\n  SidebarMenu,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  useSidebar,\n} from \"@/components/ui/sidebar\";\n\nexport function NavUser({\n  user,\n}: {\n  user: {\n    name: string;\n    email: string;\n    avatar: string;\n  };\n}) {\n  const { isMobile } = useSidebar();\n\n  return (\n    <SidebarMenu>\n      <SidebarMenuItem>\n        <DropdownMenu>\n          <DropdownMenuTrigger asChild>\n            <SidebarMenuButton\n              size=\"lg\"\n              className=\"data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground\"\n            >\n              <Avatar className=\"h-8 w-8 rounded-lg grayscale\">\n                <AvatarImage src={user.avatar} alt={user.name} />\n                <AvatarFallback className=\"rounded-lg\">CN</AvatarFallback>\n              </Avatar>\n              <div className=\"grid flex-1 text-left text-sm leading-tight\">\n                <span className=\"truncate font-medium\">{user.name}</span>\n                <span className=\"text-muted-foreground truncate text-xs\">\n                  {user.email}\n                </span>\n              </div>\n              <IconDotsVertical className=\"ml-auto size-4\" />\n            </SidebarMenuButton>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent\n            className=\"w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg\"\n            side={isMobile ? \"bottom\" : \"right\"}\n            align=\"end\"\n            sideOffset={4}\n          >\n            <DropdownMenuLabel className=\"p-0 font-normal\">\n              <div className=\"flex items-center gap-2 px-1 py-1.5 text-left text-sm\">\n                <Avatar className=\"h-8 w-8 rounded-lg\">\n                  <AvatarImage src={user.avatar} alt={user.name} />\n                  <AvatarFallback className=\"rounded-lg\">CN</AvatarFallback>\n                </Avatar>\n                <div className=\"grid flex-1 text-left text-sm leading-tight\">\n                  <span className=\"truncate font-medium\">{user.name}</span>\n                  <span className=\"text-muted-foreground truncate text-xs\">\n                    {user.email}\n                  </span>\n                </div>\n              </div>\n            </DropdownMenuLabel>\n            <DropdownMenuSeparator />\n            <DropdownMenuGroup>\n              <DropdownMenuItem>\n                <IconUserCircle />\n                Account\n              </DropdownMenuItem>\n              <DropdownMenuItem>\n                <IconCreditCard />\n                Billing\n              </DropdownMenuItem>\n              <DropdownMenuItem>\n                <IconNotification />\n                Notifications\n              </DropdownMenuItem>\n            </DropdownMenuGroup>\n            <DropdownMenuSeparator />\n            <DropdownMenuItem>\n              <IconLogout />\n              Log out\n            </DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </SidebarMenuItem>\n    </SidebarMenu>\n  );\n}\n"
  },
  {
    "path": "packages/gym/components/search-form.tsx",
    "content": "import { Search } from \"lucide-react\";\n\nimport { Label } from \"@/components/ui/label\";\nimport {\n  SidebarGroup,\n  SidebarGroupContent,\n  SidebarInput,\n} from \"@/components/ui/sidebar\";\n\nexport function SearchForm({ ...props }: React.ComponentProps<\"form\">) {\n  return (\n    <form {...props}>\n      <SidebarGroup className=\"py-0\">\n        <SidebarGroupContent className=\"relative\">\n          <Label htmlFor=\"search\" className=\"sr-only\">\n            Search\n          </Label>\n          <SidebarInput\n            id=\"search\"\n            placeholder=\"Search the docs...\"\n            className=\"pl-8\"\n          />\n          <Search className=\"pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 opacity-50 select-none\" />\n        </SidebarGroupContent>\n      </SidebarGroup>\n    </form>\n  );\n}\n"
  },
  {
    "path": "packages/gym/components/section-cards.tsx",
    "content": "import { IconTrendingDown, IconTrendingUp } from \"@tabler/icons-react\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport {\n  Card,\n  CardAction,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\n\nexport function SectionCards() {\n  return (\n    <div className=\"*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:shadow-xs lg:px-6 @xl/main:grid-cols-2 @5xl/main:grid-cols-4\">\n      <Card className=\"@container/card\">\n        <CardHeader>\n          <CardDescription>Total Revenue</CardDescription>\n          <CardTitle className=\"text-2xl font-semibold tabular-nums @[250px]/card:text-3xl\">\n            $1,250.00\n          </CardTitle>\n          <CardAction>\n            <Badge variant=\"outline\">\n              <IconTrendingUp />\n              +12.5%\n            </Badge>\n          </CardAction>\n        </CardHeader>\n        <CardFooter className=\"flex-col items-start gap-1.5 text-sm\">\n          <div className=\"line-clamp-1 flex gap-2 font-medium\">\n            Trending up this month <IconTrendingUp className=\"size-4\" />\n          </div>\n          <div className=\"text-muted-foreground\">\n            Visitors for the last 6 months\n          </div>\n        </CardFooter>\n      </Card>\n      <Card className=\"@container/card\">\n        <CardHeader>\n          <CardDescription>New Customers</CardDescription>\n          <CardTitle className=\"text-2xl font-semibold tabular-nums @[250px]/card:text-3xl\">\n            1,234\n          </CardTitle>\n          <CardAction>\n            <Badge variant=\"outline\">\n              <IconTrendingDown />\n              -20%\n            </Badge>\n          </CardAction>\n        </CardHeader>\n        <CardFooter className=\"flex-col items-start gap-1.5 text-sm\">\n          <div className=\"line-clamp-1 flex gap-2 font-medium\">\n            Down 20% this period <IconTrendingDown className=\"size-4\" />\n          </div>\n          <div className=\"text-muted-foreground\">\n            Acquisition needs attention\n          </div>\n        </CardFooter>\n      </Card>\n      <Card className=\"@container/card\">\n        <CardHeader>\n          <CardDescription>Active Accounts</CardDescription>\n          <CardTitle className=\"text-2xl font-semibold tabular-nums @[250px]/card:text-3xl\">\n            45,678\n          </CardTitle>\n          <CardAction>\n            <Badge variant=\"outline\">\n              <IconTrendingUp />\n              +12.5%\n            </Badge>\n          </CardAction>\n        </CardHeader>\n        <CardFooter className=\"flex-col items-start gap-1.5 text-sm\">\n          <div className=\"line-clamp-1 flex gap-2 font-medium\">\n            Strong user retention <IconTrendingUp className=\"size-4\" />\n          </div>\n          <div className=\"text-muted-foreground\">Engagement exceed targets</div>\n        </CardFooter>\n      </Card>\n      <Card className=\"@container/card\">\n        <CardHeader>\n          <CardDescription>Growth Rate</CardDescription>\n          <CardTitle className=\"text-2xl font-semibold tabular-nums @[250px]/card:text-3xl\">\n            4.5%\n          </CardTitle>\n          <CardAction>\n            <Badge variant=\"outline\">\n              <IconTrendingUp />\n              +4.5%\n            </Badge>\n          </CardAction>\n        </CardHeader>\n        <CardFooter className=\"flex-col items-start gap-1.5 text-sm\">\n          <div className=\"line-clamp-1 flex gap-2 font-medium\">\n            Steady performance increase <IconTrendingUp className=\"size-4\" />\n          </div>\n          <div className=\"text-muted-foreground\">Meets growth projections</div>\n        </CardFooter>\n      </Card>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/gym/components/sheet-demo.tsx",
    "content": "\"use client\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n  Sheet,\n  SheetContent,\n  SheetDescription,\n  SheetHeader,\n  SheetTitle,\n  SheetTrigger,\n} from \"@/components/ui/sheet\";\n\nexport const SheetDemo = () => {\n  return (\n    <Sheet>\n      <SheetTrigger asChild>\n        <Button variant=\"outline\">Open Sheet</Button>\n      </SheetTrigger>\n      <SheetContent>\n        <SheetHeader>\n          <SheetTitle>Sheet Test</SheetTitle>\n          <SheetDescription>\n            Try to grab elements inside this sheet. Clicking the react-grab\n            cursor icon should not close the sheet.\n          </SheetDescription>\n        </SheetHeader>\n        <div className=\"flex flex-col gap-4 p-4\">\n          <Button variant=\"default\">Button Inside Sheet</Button>\n          <Button variant=\"outline\">Another Button</Button>\n          <Input type=\"text\" placeholder=\"Input inside sheet…\" />\n          <Card>\n            <CardContent className=\"p-4\">\n              <div className=\"text-sm font-medium\">Card Inside Sheet</div>\n              <div className=\"text-muted-foreground text-xs mt-1\">\n                This card is inside the sheet\n              </div>\n            </CardContent>\n          </Card>\n        </div>\n      </SheetContent>\n    </Sheet>\n  );\n};\n"
  },
  {
    "path": "packages/gym/components/site-header.tsx",
    "content": "import { Button } from \"@/components/ui/button\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { SidebarTrigger } from \"@/components/ui/sidebar\";\nimport { ThemeToggle } from \"@/components/theme-toggle\";\n\nexport const SiteHeader = () => {\n  return (\n    <header className=\"flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)\">\n      <div className=\"flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6\">\n        <SidebarTrigger className=\"-ml-1\" />\n        <Separator\n          orientation=\"vertical\"\n          className=\"mx-2 data-[orientation=vertical]:h-4\"\n        />\n        <h1 className=\"text-base font-medium\">React Grab Gym</h1>\n        <div className=\"ml-auto flex items-center gap-2\">\n          <ThemeToggle />\n          <Button variant=\"ghost\" asChild size=\"sm\" className=\"hidden sm:flex\">\n            <a\n              href=\"https://github.com/AidenYuanDev/react-grab\"\n              rel=\"noopener noreferrer\"\n              target=\"_blank\"\n              className=\"dark:text-foreground\"\n            >\n              GitHub\n            </a>\n          </Button>\n        </div>\n      </div>\n    </header>\n  );\n};\n"
  },
  {
    "path": "packages/gym/components/theme-provider.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ThemeProvider as NextThemesProvider } from \"next-themes\";\n\nexport const ThemeProvider = ({\n  children,\n  ...props\n}: React.ComponentProps<typeof NextThemesProvider>) => {\n  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;\n};\n"
  },
  {
    "path": "packages/gym/components/theme-toggle.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Moon, Sun } from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\n\nexport const ThemeToggle = () => {\n  const { setTheme } = useTheme();\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button variant=\"ghost\" size=\"icon\">\n          <Sun className=\"h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0\" />\n          <Moon className=\"absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100\" />\n          <span className=\"sr-only\">Toggle theme</span>\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\">\n        <DropdownMenuItem onClick={() => setTheme(\"light\")}>\n          Light\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={() => setTheme(\"dark\")}>\n          Dark\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={() => setTheme(\"system\")}>\n          System\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n"
  },
  {
    "path": "packages/gym/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/utils\";\n\nfunction Avatar({\n  className,\n  ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Root>) {\n  return (\n    <AvatarPrimitive.Root\n      data-slot=\"avatar\"\n      className={cn(\n        \"relative flex size-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": "packages/gym/components/ui/badge.tsx",
    "content": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst badgeVariants = cva(\n  \"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90\",\n        secondary:\n          \"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90\",\n        destructive:\n          \"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n        outline:\n          \"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nfunction Badge({\n  className,\n  variant,\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"span\"> &\n  VariantProps<typeof badgeVariants> & { asChild?: boolean }) {\n  const Comp = asChild ? Slot : \"span\";\n\n  return (\n    <Comp\n      data-slot=\"badge\"\n      className={cn(badgeVariants({ variant }), className)}\n      {...props}\n    />\n  );\n}\n\nexport { Badge, badgeVariants };\n"
  },
  {
    "path": "packages/gym/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/utils\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n        outline:\n          \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost:\n          \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n        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      },\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": "packages/gym/components/ui/card.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Card({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card\"\n      className={cn(\n        \"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-header\"\n      className={cn(\n        \"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-title\"\n      className={cn(\"leading-none font-semibold\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardDescription({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardAction({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-action\"\n      className={cn(\n        \"col-start-2 row-span-2 row-start-1 self-start justify-self-end\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-content\"\n      className={cn(\"px-6\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-footer\"\n      className={cn(\"flex items-center px-6 [.border-t]:pt-6\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Card,\n  CardHeader,\n  CardFooter,\n  CardTitle,\n  CardAction,\n  CardDescription,\n  CardContent,\n};\n"
  },
  {
    "path": "packages/gym/components/ui/chart.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as RechartsPrimitive from \"recharts\";\n\nimport { cn } from \"@/lib/utils\";\n\n// Format: { THEME_NAME: CSS_SELECTOR }\nconst THEMES = { light: \"\", dark: \".dark\" } as const;\n\nexport type ChartConfig = {\n  [k in string]: {\n    label?: React.ReactNode;\n    icon?: React.ComponentType;\n  } & (\n    | { color?: string; theme?: never }\n    | { color?: never; theme: Record<keyof typeof THEMES, string> }\n  );\n};\n\ntype ChartContextProps = {\n  config: ChartConfig;\n};\n\nconst ChartContext = React.createContext<ChartContextProps | null>(null);\n\nfunction useChart() {\n  const context = React.useContext(ChartContext);\n\n  if (!context) {\n    throw new Error(\"useChart must be used within a <ChartContainer />\");\n  }\n\n  return context;\n}\n\nfunction ChartContainer({\n  id,\n  className,\n  children,\n  config,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  config: ChartConfig;\n  children: React.ComponentProps<\n    typeof RechartsPrimitive.ResponsiveContainer\n  >[\"children\"];\n}) {\n  const uniqueId = React.useId();\n  const chartId = `chart-${id || uniqueId.replace(/:/g, \"\")}`;\n\n  return (\n    <ChartContext.Provider value={{ config }}>\n      <div\n        data-slot=\"chart\"\n        data-chart={chartId}\n        className={cn(\n          \"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden\",\n          className,\n        )}\n        {...props}\n      >\n        <ChartStyle id={chartId} config={config} />\n        <RechartsPrimitive.ResponsiveContainer>\n          {children}\n        </RechartsPrimitive.ResponsiveContainer>\n      </div>\n    </ChartContext.Provider>\n  );\n}\n\nconst ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {\n  const colorConfig = Object.entries(config).filter(\n    ([, config]) => config.theme || config.color,\n  );\n\n  if (!colorConfig.length) {\n    return null;\n  }\n\n  return (\n    <style\n      dangerouslySetInnerHTML={{\n        __html: Object.entries(THEMES)\n          .map(\n            ([theme, prefix]) => `\n${prefix} [data-chart=${id}] {\n${colorConfig\n  .map(([key, itemConfig]) => {\n    const color =\n      itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||\n      itemConfig.color;\n    return color ? `  --color-${key}: ${color};` : null;\n  })\n  .join(\"\\n\")}\n}\n`,\n          )\n          .join(\"\\n\"),\n      }}\n    />\n  );\n};\n\nconst ChartTooltip = RechartsPrimitive.Tooltip;\n\nfunction ChartTooltipContent({\n  active,\n  payload,\n  className,\n  indicator = \"dot\",\n  hideLabel = false,\n  hideIndicator = false,\n  label,\n  labelFormatter,\n  labelClassName,\n  formatter,\n  color,\n  nameKey,\n  labelKey,\n}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &\n  React.ComponentProps<\"div\"> & {\n    hideLabel?: boolean;\n    hideIndicator?: boolean;\n    indicator?: \"line\" | \"dot\" | \"dashed\";\n    nameKey?: string;\n    labelKey?: string;\n  }) {\n  const { config } = useChart();\n\n  const tooltipLabel = React.useMemo(() => {\n    if (hideLabel || !payload?.length) {\n      return null;\n    }\n\n    const [item] = payload;\n    const key = `${labelKey || item?.dataKey || item?.name || \"value\"}`;\n    const itemConfig = getPayloadConfigFromPayload(config, item, key);\n    const value =\n      !labelKey && typeof label === \"string\"\n        ? config[label as keyof typeof config]?.label || label\n        : itemConfig?.label;\n\n    if (labelFormatter) {\n      return (\n        <div className={cn(\"font-medium\", labelClassName)}>\n          {labelFormatter(value, payload)}\n        </div>\n      );\n    }\n\n    if (!value) {\n      return null;\n    }\n\n    return <div className={cn(\"font-medium\", labelClassName)}>{value}</div>;\n  }, [\n    label,\n    labelFormatter,\n    payload,\n    hideLabel,\n    labelClassName,\n    config,\n    labelKey,\n  ]);\n\n  if (!active || !payload?.length) {\n    return null;\n  }\n\n  const nestLabel = payload.length === 1 && indicator !== \"dot\";\n\n  return (\n    <div\n      className={cn(\n        \"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl\",\n        className,\n      )}\n    >\n      {!nestLabel ? tooltipLabel : null}\n      <div className=\"grid gap-1.5\">\n        {payload\n          .filter((item) => item.type !== \"none\")\n          .map((item, index) => {\n            const key = `${nameKey || item.name || item.dataKey || \"value\"}`;\n            const itemConfig = getPayloadConfigFromPayload(config, item, key);\n            const indicatorColor = color || item.payload.fill || item.color;\n\n            return (\n              <div\n                key={item.dataKey}\n                className={cn(\n                  \"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5\",\n                  indicator === \"dot\" && \"items-center\",\n                )}\n              >\n                {formatter && item?.value !== undefined && item.name ? (\n                  formatter(item.value, item.name, item, index, item.payload)\n                ) : (\n                  <>\n                    {itemConfig?.icon ? (\n                      <itemConfig.icon />\n                    ) : (\n                      !hideIndicator && (\n                        <div\n                          className={cn(\n                            \"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)\",\n                            {\n                              \"h-2.5 w-2.5\": indicator === \"dot\",\n                              \"w-1\": indicator === \"line\",\n                              \"w-0 border-[1.5px] border-dashed bg-transparent\":\n                                indicator === \"dashed\",\n                              \"my-0.5\": nestLabel && indicator === \"dashed\",\n                            },\n                          )}\n                          style={\n                            {\n                              \"--color-bg\": indicatorColor,\n                              \"--color-border\": indicatorColor,\n                            } as React.CSSProperties\n                          }\n                        />\n                      )\n                    )}\n                    <div\n                      className={cn(\n                        \"flex flex-1 justify-between leading-none\",\n                        nestLabel ? \"items-end\" : \"items-center\",\n                      )}\n                    >\n                      <div className=\"grid gap-1.5\">\n                        {nestLabel ? tooltipLabel : null}\n                        <span className=\"text-muted-foreground\">\n                          {itemConfig?.label || item.name}\n                        </span>\n                      </div>\n                      {item.value && (\n                        <span className=\"text-foreground font-mono font-medium tabular-nums\">\n                          {item.value.toLocaleString()}\n                        </span>\n                      )}\n                    </div>\n                  </>\n                )}\n              </div>\n            );\n          })}\n      </div>\n    </div>\n  );\n}\n\nconst ChartLegend = RechartsPrimitive.Legend;\n\nfunction ChartLegendContent({\n  className,\n  hideIcon = false,\n  payload,\n  verticalAlign = \"bottom\",\n  nameKey,\n}: React.ComponentProps<\"div\"> &\n  Pick<RechartsPrimitive.LegendProps, \"payload\" | \"verticalAlign\"> & {\n    hideIcon?: boolean;\n    nameKey?: string;\n  }) {\n  const { config } = useChart();\n\n  if (!payload?.length) {\n    return null;\n  }\n\n  return (\n    <div\n      className={cn(\n        \"flex items-center justify-center gap-4\",\n        verticalAlign === \"top\" ? \"pb-3\" : \"pt-3\",\n        className,\n      )}\n    >\n      {payload\n        .filter((item) => item.type !== \"none\")\n        .map((item) => {\n          const key = `${nameKey || item.dataKey || \"value\"}`;\n          const itemConfig = getPayloadConfigFromPayload(config, item, key);\n\n          return (\n            <div\n              key={item.value}\n              className={cn(\n                \"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3\",\n              )}\n            >\n              {itemConfig?.icon && !hideIcon ? (\n                <itemConfig.icon />\n              ) : (\n                <div\n                  className=\"h-2 w-2 shrink-0 rounded-[2px]\"\n                  style={{\n                    backgroundColor: item.color,\n                  }}\n                />\n              )}\n              {itemConfig?.label}\n            </div>\n          );\n        })}\n    </div>\n  );\n}\n\n// Helper to extract item config from a payload.\nfunction getPayloadConfigFromPayload(\n  config: ChartConfig,\n  payload: unknown,\n  key: string,\n) {\n  if (typeof payload !== \"object\" || payload === null) {\n    return undefined;\n  }\n\n  const payloadPayload =\n    \"payload\" in payload &&\n    typeof payload.payload === \"object\" &&\n    payload.payload !== null\n      ? payload.payload\n      : undefined;\n\n  let configLabelKey: string = key;\n\n  if (\n    key in payload &&\n    typeof payload[key as keyof typeof payload] === \"string\"\n  ) {\n    configLabelKey = payload[key as keyof typeof payload] as string;\n  } else if (\n    payloadPayload &&\n    key in payloadPayload &&\n    typeof payloadPayload[key as keyof typeof payloadPayload] === \"string\"\n  ) {\n    configLabelKey = payloadPayload[\n      key as keyof typeof payloadPayload\n    ] as string;\n  }\n\n  return configLabelKey in config\n    ? config[configLabelKey]\n    : config[key as keyof typeof config];\n}\n\nexport {\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent,\n  ChartLegend,\n  ChartLegendContent,\n  ChartStyle,\n};\n"
  },
  {
    "path": "packages/gym/components/ui/checkbox.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\";\nimport { CheckIcon } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Checkbox({\n  className,\n  ...props\n}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {\n  return (\n    <CheckboxPrimitive.Root\n      data-slot=\"checkbox\"\n      className={cn(\n        \"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    >\n      <CheckboxPrimitive.Indicator\n        data-slot=\"checkbox-indicator\"\n        className=\"grid place-content-center text-current transition-none\"\n      >\n        <CheckIcon className=\"size-3.5\" />\n      </CheckboxPrimitive.Indicator>\n    </CheckboxPrimitive.Root>\n  );\n}\n\nexport { Checkbox };\n"
  },
  {
    "path": "packages/gym/components/ui/drawer.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Drawer as DrawerPrimitive } from \"vaul\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Drawer({\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Root>) {\n  return <DrawerPrimitive.Root data-slot=\"drawer\" {...props} />;\n}\n\nfunction DrawerTrigger({\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {\n  return <DrawerPrimitive.Trigger data-slot=\"drawer-trigger\" {...props} />;\n}\n\nfunction DrawerPortal({\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {\n  return <DrawerPrimitive.Portal data-slot=\"drawer-portal\" {...props} />;\n}\n\nfunction DrawerClose({\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Close>) {\n  return <DrawerPrimitive.Close data-slot=\"drawer-close\" {...props} />;\n}\n\nfunction DrawerOverlay({\n  className,\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {\n  return (\n    <DrawerPrimitive.Overlay\n      data-slot=\"drawer-overlay\"\n      className={cn(\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DrawerContent({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Content>) {\n  return (\n    <DrawerPortal data-slot=\"drawer-portal\">\n      <DrawerOverlay />\n      <DrawerPrimitive.Content\n        data-slot=\"drawer-content\"\n        className={cn(\n          \"group/drawer-content bg-background fixed z-50 flex h-auto flex-col\",\n          \"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b\",\n          \"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t\",\n          \"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm\",\n          \"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm\",\n          className,\n        )}\n        {...props}\n      >\n        <div className=\"bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block\" />\n        {children}\n      </DrawerPrimitive.Content>\n    </DrawerPortal>\n  );\n}\n\nfunction DrawerHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"drawer-header\"\n      className={cn(\n        \"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DrawerFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"drawer-footer\"\n      className={cn(\"mt-auto flex flex-col gap-2 p-4\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DrawerTitle({\n  className,\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Title>) {\n  return (\n    <DrawerPrimitive.Title\n      data-slot=\"drawer-title\"\n      className={cn(\"text-foreground font-semibold\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DrawerDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Description>) {\n  return (\n    <DrawerPrimitive.Description\n      data-slot=\"drawer-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Drawer,\n  DrawerPortal,\n  DrawerOverlay,\n  DrawerTrigger,\n  DrawerClose,\n  DrawerContent,\n  DrawerHeader,\n  DrawerFooter,\n  DrawerTitle,\n  DrawerDescription,\n};\n"
  },
  {
    "path": "packages/gym/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/utils\";\n\nfunction DropdownMenu({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {\n  return <DropdownMenuPrimitive.Root data-slot=\"dropdown-menu\" {...props} />;\n}\n\nfunction DropdownMenuPortal({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {\n  return (\n    <DropdownMenuPrimitive.Portal data-slot=\"dropdown-menu-portal\" {...props} />\n  );\n}\n\nfunction DropdownMenuTrigger({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {\n  return (\n    <DropdownMenuPrimitive.Trigger\n      data-slot=\"dropdown-menu-trigger\"\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuContent({\n  className,\n  sideOffset = 4,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {\n  return (\n    <DropdownMenuPrimitive.Portal>\n      <DropdownMenuPrimitive.Content\n        data-slot=\"dropdown-menu-content\"\n        sideOffset={sideOffset}\n        className={cn(\n          \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md\",\n          className,\n        )}\n        {...props}\n      />\n    </DropdownMenuPrimitive.Portal>\n  );\n}\n\nfunction DropdownMenuGroup({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {\n  return (\n    <DropdownMenuPrimitive.Group data-slot=\"dropdown-menu-group\" {...props} />\n  );\n}\n\nfunction DropdownMenuItem({\n  className,\n  inset,\n  variant = \"default\",\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {\n  inset?: boolean;\n  variant?: \"default\" | \"destructive\";\n}) {\n  return (\n    <DropdownMenuPrimitive.Item\n      data-slot=\"dropdown-menu-item\"\n      data-inset={inset}\n      data-variant={variant}\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuCheckboxItem({\n  className,\n  children,\n  checked,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {\n  return (\n    <DropdownMenuPrimitive.CheckboxItem\n      data-slot=\"dropdown-menu-checkbox-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      checked={checked}\n      {...props}\n    >\n      <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n        <DropdownMenuPrimitive.ItemIndicator>\n          <CheckIcon className=\"size-4\" />\n        </DropdownMenuPrimitive.ItemIndicator>\n      </span>\n      {children}\n    </DropdownMenuPrimitive.CheckboxItem>\n  );\n}\n\nfunction DropdownMenuRadioGroup({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {\n  return (\n    <DropdownMenuPrimitive.RadioGroup\n      data-slot=\"dropdown-menu-radio-group\"\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuRadioItem({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {\n  return (\n    <DropdownMenuPrimitive.RadioItem\n      data-slot=\"dropdown-menu-radio-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    >\n      <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n        <DropdownMenuPrimitive.ItemIndicator>\n          <CircleIcon className=\"size-2 fill-current\" />\n        </DropdownMenuPrimitive.ItemIndicator>\n      </span>\n      {children}\n    </DropdownMenuPrimitive.RadioItem>\n  );\n}\n\nfunction DropdownMenuLabel({\n  className,\n  inset,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {\n  inset?: boolean;\n}) {\n  return (\n    <DropdownMenuPrimitive.Label\n      data-slot=\"dropdown-menu-label\"\n      data-inset={inset}\n      className={cn(\n        \"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {\n  return (\n    <DropdownMenuPrimitive.Separator\n      data-slot=\"dropdown-menu-separator\"\n      className={cn(\"bg-border -mx-1 my-1 h-px\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuShortcut({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"dropdown-menu-shortcut\"\n      className={cn(\n        \"text-muted-foreground ml-auto text-xs tracking-widest\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuSub({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {\n  return <DropdownMenuPrimitive.Sub data-slot=\"dropdown-menu-sub\" {...props} />;\n}\n\nfunction DropdownMenuSubTrigger({\n  className,\n  inset,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {\n  inset?: boolean;\n}) {\n  return (\n    <DropdownMenuPrimitive.SubTrigger\n      data-slot=\"dropdown-menu-sub-trigger\"\n      data-inset={inset}\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <ChevronRightIcon className=\"ml-auto size-4\" />\n    </DropdownMenuPrimitive.SubTrigger>\n  );\n}\n\nfunction DropdownMenuSubContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {\n  return (\n    <DropdownMenuPrimitive.SubContent\n      data-slot=\"dropdown-menu-sub-content\"\n      className={cn(\n        \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport {\n  DropdownMenu,\n  DropdownMenuPortal,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuLabel,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuSub,\n  DropdownMenuSubTrigger,\n  DropdownMenuSubContent,\n};\n"
  },
  {
    "path": "packages/gym/components/ui/field.tsx",
    "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Label } from \"@/components/ui/label\";\nimport { Separator } from \"@/components/ui/separator\";\n\nfunction FieldSet({ className, ...props }: React.ComponentProps<\"fieldset\">) {\n  return (\n    <fieldset\n      data-slot=\"field-set\"\n      className={cn(\n        \"flex flex-col gap-6\",\n        \"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction FieldLegend({\n  className,\n  variant = \"legend\",\n  ...props\n}: React.ComponentProps<\"legend\"> & { variant?: \"legend\" | \"label\" }) {\n  return (\n    <legend\n      data-slot=\"field-legend\"\n      data-variant={variant}\n      className={cn(\n        \"mb-3 font-medium\",\n        \"data-[variant=legend]:text-base\",\n        \"data-[variant=label]:text-sm\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction FieldGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"field-group\"\n      className={cn(\n        \"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nconst fieldVariants = cva(\n  \"group/field flex w-full gap-3 data-[invalid=true]:text-destructive\",\n  {\n    variants: {\n      orientation: {\n        vertical: [\"flex-col [&>*]:w-full [&>.sr-only]:w-auto\"],\n        horizontal: [\n          \"flex-row items-center\",\n          \"[&>[data-slot=field-label]]:flex-auto\",\n          \"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px\",\n        ],\n        responsive: [\n          \"flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto\",\n          \"@md/field-group:[&>[data-slot=field-label]]:flex-auto\",\n          \"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px\",\n        ],\n      },\n    },\n    defaultVariants: {\n      orientation: \"vertical\",\n    },\n  },\n);\n\nfunction Field({\n  className,\n  orientation = \"vertical\",\n  ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof fieldVariants>) {\n  return (\n    <div\n      role=\"group\"\n      data-slot=\"field\"\n      data-orientation={orientation}\n      className={cn(fieldVariants({ orientation }), className)}\n      {...props}\n    />\n  );\n}\n\nfunction FieldContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"field-content\"\n      className={cn(\n        \"group/field-content flex flex-1 flex-col gap-1.5 leading-snug\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction FieldLabel({\n  className,\n  ...props\n}: React.ComponentProps<typeof Label>) {\n  return (\n    <Label\n      data-slot=\"field-label\"\n      className={cn(\n        \"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50\",\n        \"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4\",\n        \"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction FieldTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"field-label\"\n      className={cn(\n        \"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction FieldDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n  return (\n    <p\n      data-slot=\"field-description\"\n      className={cn(\n        \"text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance\",\n        \"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5\",\n        \"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction FieldSeparator({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  children?: React.ReactNode;\n}) {\n  return (\n    <div\n      data-slot=\"field-separator\"\n      data-content={!!children}\n      className={cn(\n        \"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2\",\n        className,\n      )}\n      {...props}\n    >\n      <Separator className=\"absolute inset-0 top-1/2\" />\n      {children && (\n        <span\n          className=\"bg-background text-muted-foreground relative mx-auto block w-fit px-2\"\n          data-slot=\"field-separator-content\"\n        >\n          {children}\n        </span>\n      )}\n    </div>\n  );\n}\n\nfunction FieldError({\n  className,\n  children,\n  errors,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  errors?: Array<{ message?: string } | undefined>;\n}) {\n  const content = useMemo(() => {\n    if (children) {\n      return children;\n    }\n\n    if (!errors?.length) {\n      return null;\n    }\n\n    const uniqueErrors = [\n      ...new Map(errors.map((error) => [error?.message, error])).values(),\n    ];\n\n    if (uniqueErrors?.length == 1) {\n      return uniqueErrors[0]?.message;\n    }\n\n    return (\n      <ul className=\"ml-4 flex list-disc flex-col gap-1\">\n        {uniqueErrors.map(\n          (error, index) =>\n            error?.message && <li key={index}>{error.message}</li>,\n        )}\n      </ul>\n    );\n  }, [children, errors]);\n\n  if (!content) {\n    return null;\n  }\n\n  return (\n    <div\n      role=\"alert\"\n      data-slot=\"field-error\"\n      className={cn(\"text-destructive text-sm font-normal\", className)}\n      {...props}\n    >\n      {content}\n    </div>\n  );\n}\n\nexport {\n  Field,\n  FieldLabel,\n  FieldDescription,\n  FieldError,\n  FieldGroup,\n  FieldLegend,\n  FieldSeparator,\n  FieldSet,\n  FieldContent,\n  FieldTitle,\n};\n"
  },
  {
    "path": "packages/gym/components/ui/input.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Input({ className, type, ...props }: React.ComponentProps<\"input\">) {\n  return (\n    <input\n      type={type}\n      data-slot=\"input\"\n      className={cn(\n        \"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n        \"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]\",\n        \"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Input };\n"
  },
  {
    "path": "packages/gym/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/utils\";\n\nfunction Label({\n  className,\n  ...props\n}: React.ComponentProps<typeof LabelPrimitive.Root>) {\n  return (\n    <LabelPrimitive.Root\n      data-slot=\"label\"\n      className={cn(\n        \"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Label };\n"
  },
  {
    "path": "packages/gym/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/utils\";\n\nfunction Select({\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Root>) {\n  return <SelectPrimitive.Root data-slot=\"select\" {...props} />;\n}\n\nfunction SelectGroup({\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Group>) {\n  return <SelectPrimitive.Group data-slot=\"select-group\" {...props} />;\n}\n\nfunction SelectValue({\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Value>) {\n  return <SelectPrimitive.Value data-slot=\"select-value\" {...props} />;\n}\n\nfunction SelectTrigger({\n  className,\n  size = \"default\",\n  children,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {\n  size?: \"sm\" | \"default\";\n}) {\n  return (\n    <SelectPrimitive.Trigger\n      data-slot=\"select-trigger\"\n      data-size={size}\n      className={cn(\n        \"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <SelectPrimitive.Icon asChild>\n        <ChevronDownIcon className=\"size-4 opacity-50\" />\n      </SelectPrimitive.Icon>\n    </SelectPrimitive.Trigger>\n  );\n}\n\nfunction SelectContent({\n  className,\n  children,\n  position = \"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\n        data-slot=\"select-item-indicator\"\n        className=\"absolute right-2 flex size-3.5 items-center justify-center\"\n      >\n        <SelectPrimitive.ItemIndicator>\n          <CheckIcon className=\"size-4\" />\n        </SelectPrimitive.ItemIndicator>\n      </span>\n      <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n    </SelectPrimitive.Item>\n  );\n}\n\nfunction SelectSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Separator>) {\n  return (\n    <SelectPrimitive.Separator\n      data-slot=\"select-separator\"\n      className={cn(\"bg-border pointer-events-none -mx-1 my-1 h-px\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SelectScrollUpButton({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {\n  return (\n    <SelectPrimitive.ScrollUpButton\n      data-slot=\"select-scroll-up-button\"\n      className={cn(\n        \"flex cursor-default items-center justify-center py-1\",\n        className,\n      )}\n      {...props}\n    >\n      <ChevronUpIcon className=\"size-4\" />\n    </SelectPrimitive.ScrollUpButton>\n  );\n}\n\nfunction SelectScrollDownButton({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {\n  return (\n    <SelectPrimitive.ScrollDownButton\n      data-slot=\"select-scroll-down-button\"\n      className={cn(\n        \"flex cursor-default items-center justify-center py-1\",\n        className,\n      )}\n      {...props}\n    >\n      <ChevronDownIcon className=\"size-4\" />\n    </SelectPrimitive.ScrollDownButton>\n  );\n}\n\nexport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectLabel,\n  SelectScrollDownButton,\n  SelectScrollUpButton,\n  SelectSeparator,\n  SelectTrigger,\n  SelectValue,\n};\n"
  },
  {
    "path": "packages/gym/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/utils\";\n\nfunction Separator({\n  className,\n  orientation = \"horizontal\",\n  decorative = true,\n  ...props\n}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {\n  return (\n    <SeparatorPrimitive.Root\n      data-slot=\"separator\"\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        \"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Separator };\n"
  },
  {
    "path": "packages/gym/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/utils\";\n\nfunction Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {\n  return <SheetPrimitive.Root data-slot=\"sheet\" {...props} />;\n}\n\nfunction SheetTrigger({\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {\n  return <SheetPrimitive.Trigger data-slot=\"sheet-trigger\" {...props} />;\n}\n\nfunction SheetClose({\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Close>) {\n  return <SheetPrimitive.Close data-slot=\"sheet-close\" {...props} />;\n}\n\nfunction SheetPortal({\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Portal>) {\n  return <SheetPrimitive.Portal data-slot=\"sheet-portal\" {...props} />;\n}\n\nfunction SheetOverlay({\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {\n  return (\n    <SheetPrimitive.Overlay\n      data-slot=\"sheet-overlay\"\n      className={cn(\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction SheetContent({\n  className,\n  children,\n  side = \"right\",\n  ...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": "packages/gym/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/utils\";\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_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(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  const id = React.useId();\n  const width = React.useMemo(() => {\n    const hash = id\n      .split(\"\")\n      .reduce((acc, char) => acc + char.charCodeAt(0), 0);\n    return `${(hash % 40) + 50}%`;\n  }, [id]);\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": "packages/gym/components/ui/skeleton.tsx",
    "content": "import { cn } from \"@/lib/utils\";\n\nfunction Skeleton({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"skeleton\"\n      className={cn(\"bg-accent animate-pulse rounded-md\", className)}\n      {...props}\n    />\n  );\n}\n\nexport { Skeleton };\n"
  },
  {
    "path": "packages/gym/components/ui/table.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Table({ className, ...props }: React.ComponentProps<\"table\">) {\n  return (\n    <div\n      data-slot=\"table-container\"\n      className=\"relative w-full overflow-x-auto\"\n    >\n      <table\n        data-slot=\"table\"\n        className={cn(\"w-full caption-bottom text-sm\", className)}\n        {...props}\n      />\n    </div>\n  );\n}\n\nfunction TableHeader({ className, ...props }: React.ComponentProps<\"thead\">) {\n  return (\n    <thead\n      data-slot=\"table-header\"\n      className={cn(\"[&_tr]:border-b\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction TableBody({ className, ...props }: React.ComponentProps<\"tbody\">) {\n  return (\n    <tbody\n      data-slot=\"table-body\"\n      className={cn(\"[&_tr:last-child]:border-0\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction TableFooter({ className, ...props }: React.ComponentProps<\"tfoot\">) {\n  return (\n    <tfoot\n      data-slot=\"table-footer\"\n      className={cn(\n        \"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableRow({ className, ...props }: React.ComponentProps<\"tr\">) {\n  return (\n    <tr\n      data-slot=\"table-row\"\n      className={cn(\n        \"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableHead({ className, ...props }: React.ComponentProps<\"th\">) {\n  return (\n    <th\n      data-slot=\"table-head\"\n      className={cn(\n        \"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableCell({ className, ...props }: React.ComponentProps<\"td\">) {\n  return (\n    <td\n      data-slot=\"table-cell\"\n      className={cn(\n        \"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableCaption({\n  className,\n  ...props\n}: React.ComponentProps<\"caption\">) {\n  return (\n    <caption\n      data-slot=\"table-caption\"\n      className={cn(\"text-muted-foreground mt-4 text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Table,\n  TableHeader,\n  TableBody,\n  TableFooter,\n  TableHead,\n  TableRow,\n  TableCell,\n  TableCaption,\n};\n"
  },
  {
    "path": "packages/gym/components/ui/tabs.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Tabs({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Root>) {\n  return (\n    <TabsPrimitive.Root\n      data-slot=\"tabs\"\n      className={cn(\"flex flex-col gap-2\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction TabsList({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.List>) {\n  return (\n    <TabsPrimitive.List\n      data-slot=\"tabs-list\"\n      className={cn(\n        \"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TabsTrigger({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {\n  return (\n    <TabsPrimitive.Trigger\n      data-slot=\"tabs-trigger\"\n      className={cn(\n        \"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TabsContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Content>) {\n  return (\n    <TabsPrimitive.Content\n      data-slot=\"tabs-content\"\n      className={cn(\"flex-1 outline-none\", className)}\n      {...props}\n    />\n  );\n}\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent };\n"
  },
  {
    "path": "packages/gym/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/utils\";\nimport { toggleVariants } from \"@/components/ui/toggle\";\n\nconst ToggleGroupContext = React.createContext<\n  VariantProps<typeof toggleVariants> & {\n    spacing?: number;\n  }\n>({\n  size: \"default\",\n  variant: \"default\",\n  spacing: 0,\n});\n\nfunction ToggleGroup({\n  className,\n  variant,\n  size,\n  spacing = 0,\n  children,\n  ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &\n  VariantProps<typeof toggleVariants> & {\n    spacing?: number;\n  }) {\n  return (\n    <ToggleGroupPrimitive.Root\n      data-slot=\"toggle-group\"\n      data-variant={variant}\n      data-size={size}\n      data-spacing={spacing}\n      style={{ \"--gap\": spacing } as React.CSSProperties}\n      className={cn(\n        \"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs\",\n        className,\n      )}\n      {...props}\n    >\n      <ToggleGroupContext.Provider value={{ variant, size, spacing }}>\n        {children}\n      </ToggleGroupContext.Provider>\n    </ToggleGroupPrimitive.Root>\n  );\n}\n\nfunction ToggleGroupItem({\n  className,\n  children,\n  variant,\n  size,\n  ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &\n  VariantProps<typeof toggleVariants>) {\n  const context = React.useContext(ToggleGroupContext);\n\n  return (\n    <ToggleGroupPrimitive.Item\n      data-slot=\"toggle-group-item\"\n      data-variant={context.variant || variant}\n      data-size={context.size || size}\n      data-spacing={context.spacing}\n      className={cn(\n        toggleVariants({\n          variant: context.variant || variant,\n          size: context.size || size,\n        }),\n        \"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10\",\n        \"data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </ToggleGroupPrimitive.Item>\n  );\n}\n\nexport { ToggleGroup, ToggleGroupItem };\n"
  },
  {
    "path": "packages/gym/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/utils\";\n\nconst toggleVariants = cva(\n  \"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-transparent\",\n        outline:\n          \"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground\",\n      },\n      size: {\n        default: \"h-9 px-2 min-w-9\",\n        sm: \"h-8 px-1.5 min-w-8\",\n        lg: \"h-10 px-2.5 min-w-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nfunction Toggle({\n  className,\n  variant,\n  size,\n  ...props\n}: React.ComponentProps<typeof TogglePrimitive.Root> &\n  VariantProps<typeof toggleVariants>) {\n  return (\n    <TogglePrimitive.Root\n      data-slot=\"toggle\"\n      className={cn(toggleVariants({ variant, size, className }))}\n      {...props}\n    />\n  );\n}\n\nexport { Toggle, toggleVariants };\n"
  },
  {
    "path": "packages/gym/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/utils\";\n\nfunction TooltipProvider({\n  delayDuration = 0,\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {\n  return (\n    <TooltipPrimitive.Provider\n      data-slot=\"tooltip-provider\"\n      delayDuration={delayDuration}\n      {...props}\n    />\n  );\n}\n\nfunction Tooltip({\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Root>) {\n  return (\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": "packages/gym/components/version-switcher.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Check, ChevronsUpDown, GalleryVerticalEnd } from \"lucide-react\";\n\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport {\n  SidebarMenu,\n  SidebarMenuButton,\n  SidebarMenuItem,\n} from \"@/components/ui/sidebar\";\n\nexport function VersionSwitcher({\n  versions,\n  defaultVersion,\n}: {\n  versions: string[];\n  defaultVersion: string;\n}) {\n  const [selectedVersion, setSelectedVersion] = React.useState(defaultVersion);\n\n  return (\n    <SidebarMenu>\n      <SidebarMenuItem>\n        <DropdownMenu>\n          <DropdownMenuTrigger asChild>\n            <SidebarMenuButton\n              size=\"lg\"\n              className=\"data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground\"\n            >\n              <div className=\"bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg\">\n                <GalleryVerticalEnd className=\"size-4\" />\n              </div>\n              <div className=\"flex flex-col gap-0.5 leading-none\">\n                <span className=\"font-medium\">Documentation</span>\n                <span className=\"\">v{selectedVersion}</span>\n              </div>\n              <ChevronsUpDown className=\"ml-auto\" />\n            </SidebarMenuButton>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent\n            className=\"w-(--radix-dropdown-menu-trigger-width)\"\n            align=\"start\"\n          >\n            {versions.map((version) => (\n              <DropdownMenuItem\n                key={version}\n                onSelect={() => setSelectedVersion(version)}\n              >\n                v{version}{\" \"}\n                {version === selectedVersion && <Check className=\"ml-auto\" />}\n              </DropdownMenuItem>\n            ))}\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </SidebarMenuItem>\n    </SidebarMenu>\n  );\n}\n"
  },
  {
    "path": "packages/gym/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"\",\n    \"css\": \"app/globals.css\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"iconLibrary\": \"lucide\",\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  },\n  \"registries\": {}\n}\n"
  },
  {
    "path": "packages/gym/hooks/use-mobile.ts",
    "content": "import * as React from \"react\";\n\nconst MOBILE_BREAKPOINT = 768;\n\nexport const useIsMobile = (): boolean => {\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": "packages/gym/instrumentation-client.ts",
    "content": "export {};\n"
  },
  {
    "path": "packages/gym/lib/utils.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport const cn = (...inputs: ClassValue[]) => {\n  return twMerge(clsx(inputs));\n};\n"
  },
  {
    "path": "packages/gym/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.\n"
  },
  {
    "path": "packages/gym/next.config.ts",
    "content": "import type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {\n  async rewrites() {\n    return [\n      {\n        source: \"/@provider-:name/client.global.js\",\n        destination: \"/api/provider/:name\",\n      },\n    ];\n  },\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "packages/gym/package.json",
    "content": "{\n  \"name\": \"@react-grab/gym\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"next dev --turbopack --port 6789\",\n    \"dev:claude\": \"REACT_GRAB_CWD=$(cd ../.. && pwd) node ../provider-claude-code/dist/cli.cjs && NEXT_PUBLIC_PROVIDER=claude next dev --turbopack --port 6789\",\n    \"dev:cursor\": \"REACT_GRAB_CWD=$(cd ../.. && pwd) node ../provider-cursor/dist/cli.cjs && NEXT_PUBLIC_PROVIDER=cursor next dev --turbopack --port 6789\",\n    \"dev:opencode\": \"REACT_GRAB_CWD=$(cd ../.. && pwd) node ../provider-opencode/dist/cli.cjs && NEXT_PUBLIC_PROVIDER=opencode next dev --turbopack --port 6789\",\n    \"dev:codex\": \"REACT_GRAB_CWD=$(cd ../.. && pwd) node ../provider-codex/dist/cli.cjs && NEXT_PUBLIC_PROVIDER=codex next dev --turbopack --port 6789\",\n    \"dev:gemini\": \"REACT_GRAB_CWD=$(cd ../.. && pwd) node ../provider-gemini/dist/cli.cjs && NEXT_PUBLIC_PROVIDER=gemini next dev --turbopack --port 6789\",\n    \"dev:amp\": \"REACT_GRAB_CWD=$(cd ../.. && pwd) node ../provider-amp/dist/cli.cjs && NEXT_PUBLIC_PROVIDER=amp next dev --turbopack --port 6789\",\n    \"dev:droid\": \"REACT_GRAB_CWD=$(cd ../.. && pwd) node ../provider-droid/dist/cli.cjs && NEXT_PUBLIC_PROVIDER=droid next dev --turbopack --port 6789\",\n    \"dev:copilot\": \"REACT_GRAB_CWD=$(cd ../.. && pwd) node ../provider-copilot/dist/cli.cjs && NEXT_PUBLIC_PROVIDER=copilot next dev --turbopack --port 6789\",\n    \"dev:mcp\": \"NEXT_PUBLIC_PROVIDER=mcp next dev --turbopack --port 6789\",\n    \"dev:all\": \"node scripts/start-all-servers.js & sleep 2 && NEXT_PUBLIC_PROVIDER=cursor,claude,opencode,codex,gemini,amp,droid,copilot,mcp next dev --turbopack --port 6789\",\n    \"servers\": \"node scripts/start-all-servers.js\",\n    \"servers:cursor\": \"REACT_GRAB_CWD=$(cd ../.. && pwd) node ../provider-cursor/dist/cli.cjs\",\n    \"servers:claude\": \"REACT_GRAB_CWD=$(cd ../.. && pwd) node ../provider-claude-code/dist/cli.cjs\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\"\n  },\n  \"dependencies\": {\n    \"@dnd-kit/core\": \"^6.3.1\",\n    \"@dnd-kit/modifiers\": \"^9.0.0\",\n    \"@dnd-kit/sortable\": \"^10.0.0\",\n    \"@dnd-kit/utilities\": \"^3.2.2\",\n    \"@radix-ui/react-avatar\": \"^1.1.11\",\n    \"@radix-ui/react-checkbox\": \"^1.3.3\",\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-select\": \"^2.2.6\",\n    \"@radix-ui/react-separator\": \"^1.1.8\",\n    \"@radix-ui/react-slot\": \"^1.2.4\",\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    \"@tabler/icons-react\": \"^3.36.1\",\n    \"@tanstack/react-table\": \"^8.21.3\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"date-fns\": \"^4.1.0\",\n    \"input-otp\": \"^1.4.2\",\n    \"lucide-react\": \"^0.553.0\",\n    \"next\": \"15.3.8\",\n    \"next-themes\": \"^0.4.6\",\n    \"react\": \"19.0.1\",\n    \"react-day-picker\": \"^9.11.1\",\n    \"react-dom\": \"19.0.1\",\n    \"react-grab\": \"workspace:*\",\n    \"recharts\": \"2.15.4\",\n    \"sonner\": \"^2.0.7\",\n    \"tailwind-merge\": \"^2.6.0\",\n    \"vaul\": \"^1.1.2\",\n    \"zod\": \"^4.3.5\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/postcss\": \"^4\",\n    \"@types/node\": \"^20\",\n    \"@types/react\": \"^19\",\n    \"@types/react-dom\": \"^19\",\n    \"eslint\": \"^9\",\n    \"eslint-config-next\": \"15.3.6\",\n    \"tailwindcss\": \"^4\",\n    \"tw-animate-css\": \"^1.4.0\",\n    \"typescript\": \"^5\"\n  }\n}\n"
  },
  {
    "path": "packages/gym/postcss.config.mjs",
    "content": "const config = {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "packages/gym/scripts/start-all-servers.js",
    "content": "#!/usr/bin/env node\nimport { spawn } from \"child_process\";\nimport { fileURLToPath } from \"url\";\nimport { dirname, join } from \"path\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst packagesDir = join(__dirname, \"../..\");\n\nconst PROVIDERS_WITH_SERVERS = [\n  \"provider-cursor\",\n  \"provider-claude-code\",\n  \"provider-opencode\",\n  \"provider-codex\",\n  \"provider-gemini\",\n  \"provider-amp\",\n  \"provider-droid\",\n];\n\nconst startServer = (provider) => {\n  const cliPath = join(packagesDir, provider, \"dist\", \"cli.cjs\");\n  const cwd = join(packagesDir, \"..\");\n\n  console.log(`Starting ${provider} server...`);\n\n  const child = spawn(\"node\", [cliPath], {\n    cwd,\n    stdio: \"inherit\",\n    env: {\n      ...process.env,\n      REACT_GRAB_CWD: cwd,\n    },\n  });\n\n  child.on(\"error\", (error) => {\n    console.error(`Failed to start ${provider}:`, error.message);\n  });\n\n  child.on(\"exit\", (code, signal) => {\n    if (signal) {\n      console.log(`${provider} server terminated by signal ${signal}`);\n    } else if (code !== 0) {\n      console.error(`${provider} server crashed with exit code ${code}`);\n    } else {\n      console.log(`${provider} server exited`);\n    }\n  });\n\n  return child;\n};\n\nconst children = PROVIDERS_WITH_SERVERS.map(startServer);\n\nconst waitForChildrenToExit = () => {\n  const aliveChildren = children.filter(\n    (child) => child.exitCode === null && child.signalCode === null,\n  );\n  if (aliveChildren.length === 0) {\n    process.exit(0);\n  }\n\n  return Promise.all(\n    aliveChildren.map(\n      (child) =>\n        new Promise((resolve) => {\n          child.on(\"exit\", resolve);\n        }),\n    ),\n  ).then(() => process.exit(0));\n};\n\nprocess.on(\"SIGINT\", () => {\n  console.log(\"\\nShutting down all servers...\");\n  children.forEach((child) => child.kill(\"SIGINT\"));\n  waitForChildrenToExit();\n});\n\nprocess.on(\"SIGTERM\", () => {\n  children.forEach((child) => child.kill(\"SIGTERM\"));\n  waitForChildrenToExit();\n});\n"
  },
  {
    "path": "packages/gym/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\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": "packages/mcp/CHANGELOG.md",
    "content": "# @react-grab/mcp\n\n## 0.1.28\n\n### Patch Changes\n\n- fix\n- Updated dependencies\n  - react-grab@0.1.28\n\n## 0.1.27\n\n### Patch Changes\n\n- fix: install instructions\n- Updated dependencies\n  - react-grab@0.1.27\n\n## 0.1.26\n\n### Patch Changes\n\n- fix: minor tweaks\n- Updated dependencies\n  - react-grab@0.1.26\n\n## 0.1.25\n\n### Patch Changes\n\n- fix: primtiives\n- Updated dependencies\n  - react-grab@0.1.25\n\n## 0.1.24\n\n### Patch Changes\n\n- primitives\n- Updated dependencies\n  - react-grab@0.1.24\n\n## 0.1.23\n\n### Patch Changes\n\n- fix: npx command\n- Updated dependencies\n  - react-grab@0.1.23\n\n## 0.1.22\n\n### Patch Changes\n\n- fix: freezing\n- Updated dependencies\n  - react-grab@0.1.22\n\n## 0.1.21\n\n### Patch Changes\n\n- fix: up and down selection\n- Updated dependencies\n  - react-grab@0.1.21\n\n## 0.1.20\n\n### Patch Changes\n\n- fix: selection performacne\n- Updated dependencies\n  - react-grab@0.1.20\n\n## 0.1.19\n\n### Patch Changes\n\n- fix: gsap\n- Updated dependencies\n  - react-grab@0.1.19\n\n## 0.1.18\n\n### Patch Changes\n\n- fix: minor issues\n- Updated dependencies\n  - react-grab@0.1.18\n\n## 0.1.17\n\n### Patch Changes\n\n- fix: mcp\n- Updated dependencies\n  - react-grab@0.1.17\n\n## 0.1.16\n\n### Patch Changes\n\n- fix: environment detection\n- Updated dependencies\n  - react-grab@0.1.16\n\n## 0.1.15\n\n### Patch Changes\n\n- fix: animations and ux\n- Updated dependencies\n  - react-grab@0.1.15\n\n## 0.1.14\n\n### Patch Changes\n\n- fix: improve recent UX\n- Updated dependencies\n  - react-grab@0.1.14\n\n## 0.1.13\n\n### Patch Changes\n\n- fix MCP client injection\n- Updated dependencies\n  - react-grab@0.1.13\n\n## 0.1.1\n\n### Patch Changes\n\n- feat: MCP\n- Updated dependencies\n  - react-grab@0.1.12\n"
  },
  {
    "path": "packages/mcp/package.json",
    "content": "{\n  \"name\": \"@react-grab/mcp\",\n  \"version\": \"0.1.28\",\n  \"bin\": {\n    \"react-grab-mcp\": \"./dist/cli.cjs\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"type\": \"module\",\n  \"browser\": \"dist/client.global.js\",\n  \"exports\": {\n    \"./client\": {\n      \"types\": \"./dist/client.d.ts\",\n      \"import\": \"./dist/client.js\",\n      \"require\": \"./dist/client.cjs\"\n    },\n    \"./server\": {\n      \"types\": \"./dist/server.d.ts\",\n      \"import\": \"./dist/server.js\",\n      \"require\": \"./dist/server.cjs\"\n    },\n    \"./dist/*\": \"./dist/*.js\",\n    \"./dist/*.js\": \"./dist/*.js\"\n  },\n  \"scripts\": {\n    \"dev\": \"tsup --watch\",\n    \"build\": \"rm -rf dist && NODE_ENV=production tsup\"\n  },\n  \"dependencies\": {\n    \"@modelcontextprotocol/sdk\": \"^1.25.0\",\n    \"fkill\": \"^9.0.0\",\n    \"react-grab\": \"workspace:*\",\n    \"zod\": \"^3.25.0\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^22.10.7\",\n    \"tsup\": \"^8.4.0\"\n  }\n}\n"
  },
  {
    "path": "packages/mcp/src/cli.ts",
    "content": "#!/usr/bin/env node\nimport { startMcpServer } from \"./server.js\";\n\nstartMcpServer({\n  port: Number(process.env.PORT) || undefined,\n  stdio: process.argv.includes(\"--stdio\"),\n});\n"
  },
  {
    "path": "packages/mcp/src/client.ts",
    "content": "import type { init, ReactGrabAPI, Plugin, AgentContext } from \"react-grab/core\";\nimport { DEFAULT_MCP_PORT, HEALTH_CHECK_TIMEOUT_MS } from \"./constants.js\";\n\ninterface McpPluginOptions {\n  port?: number;\n}\n\nconst sendContextToServer = async (\n  contextUrl: string,\n  content: string[],\n  prompt?: string,\n): Promise<void> => {\n  await fetch(contextUrl, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ content, prompt }),\n  }).catch(() => {});\n};\n\nexport const createMcpPlugin = (options: McpPluginOptions = {}): Plugin => {\n  const port = options.port ?? DEFAULT_MCP_PORT;\n  const contextUrl = `http://localhost:${port}/context`;\n\n  return {\n    name: \"mcp\",\n    hooks: {\n      onCopySuccess: (_elements: Element[], content: string) => {\n        void sendContextToServer(contextUrl, [content]);\n      },\n      transformAgentContext: async (\n        context: AgentContext,\n      ): Promise<AgentContext> => {\n        await sendContextToServer(contextUrl, context.content, context.prompt);\n        return context;\n      },\n    },\n  };\n};\n\nconst isReactGrabApi = (value: unknown): value is ReactGrabAPI =>\n  typeof value === \"object\" && value !== null && \"registerPlugin\" in value;\n\ndeclare global {\n  interface Window {\n    __REACT_GRAB__?: ReturnType<typeof init>;\n  }\n}\n\nconst MCP_REACHABLE_KEY = \"react-grab-mcp-reachable\";\n\nconst checkIfMcpServerIsReachable = async (port: number): Promise<boolean> => {\n  const cached = sessionStorage.getItem(MCP_REACHABLE_KEY);\n  if (cached !== null) return cached === \"true\";\n\n  const isReachable = await fetch(`http://localhost:${port}/health`, {\n    signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS),\n  })\n    .then((response) => response.ok)\n    .catch(() => false);\n\n  sessionStorage.setItem(MCP_REACHABLE_KEY, String(isReachable));\n  return isReachable;\n};\n\nexport const attachMcpPlugin = async (): Promise<void> => {\n  if (typeof window === \"undefined\") return;\n\n  const isReachable = await checkIfMcpServerIsReachable(DEFAULT_MCP_PORT);\n  if (!isReachable) return;\n\n  const plugin = createMcpPlugin();\n\n  const attach = (api: ReactGrabAPI) => {\n    api.registerPlugin(plugin);\n  };\n\n  const existingApi = window.__REACT_GRAB__;\n  if (isReactGrabApi(existingApi)) {\n    attach(existingApi);\n    return;\n  }\n\n  window.addEventListener(\n    \"react-grab:init\",\n    (event: Event) => {\n      if (!(event instanceof CustomEvent)) return;\n      if (!isReactGrabApi(event.detail)) return;\n      attach(event.detail);\n    },\n    { once: true },\n  );\n\n  // HACK: Check again after adding listener in case of race condition\n  const apiAfterListener = window.__REACT_GRAB__;\n  if (isReactGrabApi(apiAfterListener)) {\n    attach(apiAfterListener);\n  }\n};\n\nattachMcpPlugin();\n"
  },
  {
    "path": "packages/mcp/src/constants.ts",
    "content": "export const CONTEXT_TTL_MS = 5 * 60 * 1000;\nexport const DEFAULT_MCP_PORT = 4723;\nexport const HEALTH_CHECK_TIMEOUT_MS = 1000;\nexport const POST_KILL_DELAY_MS = 100;\n"
  },
  {
    "path": "packages/mcp/src/server.ts",
    "content": "import { randomUUID } from \"node:crypto\";\nimport { createServer, type Server } from \"node:http\";\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport fkill from \"fkill\";\nimport { z } from \"zod\";\nimport {\n  CONTEXT_TTL_MS,\n  DEFAULT_MCP_PORT,\n  HEALTH_CHECK_TIMEOUT_MS,\n  POST_KILL_DELAY_MS,\n} from \"./constants.js\";\n\nconst sleep = (ms: number): Promise<void> =>\n  new Promise((resolve) => setTimeout(resolve, ms));\n\nconst agentContextSchema = z.object({\n  content: z\n    .array(z.string())\n    .describe(\"Array of context strings (HTML + component stack traces)\"),\n  prompt: z.string().optional().describe(\"User prompt or instruction\"),\n});\n\ntype AgentContext = z.infer<typeof agentContextSchema>;\n\ninterface StoredContext {\n  context: AgentContext;\n  submittedAt: number;\n}\n\nlet latestContext: StoredContext | null = null;\n\nconst textResult = (text: string) => ({\n  content: [{ type: \"text\" as const, text }],\n});\n\nconst formatContext = (context: AgentContext): string => {\n  const parts: string[] = [];\n  if (context.prompt) {\n    parts.push(`Prompt: ${context.prompt}`);\n  }\n  parts.push(`Elements:\\n${context.content.join(\"\\n\\n\")}`);\n  return parts.join(\"\\n\\n\");\n};\n\nconst createMcpServer = (): McpServer => {\n  const server = new McpServer(\n    { name: \"react-grab-mcp\", version: \"0.1.0\" },\n    { capabilities: { logging: {} } },\n  );\n\n  server.registerTool(\n    \"get_element_context\",\n    {\n      description:\n        \"Get the latest React Grab context that was submitted. Returns the most recent UI element selection with its prompt.\",\n    },\n    async () => {\n      if (!latestContext) {\n        return textResult(\"No context has been submitted yet.\");\n      }\n\n      const isExpired = Date.now() - latestContext.submittedAt > CONTEXT_TTL_MS;\n      if (isExpired) {\n        latestContext = null;\n        return textResult(\"No context has been submitted yet.\");\n      }\n\n      const result = textResult(formatContext(latestContext.context));\n      latestContext = null;\n      return result;\n    },\n  );\n\n  return server;\n};\n\nconst checkIfServerIsRunning = async (port: number): Promise<boolean> => {\n  try {\n    const response = await fetch(`http://localhost:${port}/health`, {\n      signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS),\n    });\n    return response.ok;\n  } catch {\n    return false;\n  }\n};\n\ninterface McpSession {\n  server: McpServer;\n  transport: StreamableHTTPServerTransport;\n}\n\nconst sessions = new Map<string, McpSession>();\n\nconst createHttpServer = (port: number): Server => {\n  return createServer(async (request, response) => {\n    const url = new URL(request.url ?? \"/\", `http://localhost:${port}`);\n\n    response.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n    response.setHeader(\n      \"Access-Control-Allow-Methods\",\n      \"POST, GET, DELETE, OPTIONS\",\n    );\n    response.setHeader(\n      \"Access-Control-Allow-Headers\",\n      \"Content-Type, mcp-session-id\",\n    );\n    response.setHeader(\"Access-Control-Expose-Headers\", \"mcp-session-id\");\n\n    if (request.method === \"OPTIONS\") {\n      response.writeHead(204).end();\n      return;\n    }\n\n    if (url.pathname === \"/health\") {\n      response\n        .writeHead(200, { \"Content-Type\": \"application/json\" })\n        .end(JSON.stringify({ status: \"ok\" }));\n      return;\n    }\n\n    if (url.pathname === \"/context\" && request.method === \"POST\") {\n      const chunks: Buffer[] = [];\n      for await (const chunk of request) {\n        chunks.push(chunk as Buffer);\n      }\n\n      try {\n        const body = JSON.parse(Buffer.concat(chunks).toString());\n        latestContext = {\n          context: agentContextSchema.parse(body),\n          submittedAt: Date.now(),\n        };\n        response\n          .writeHead(200, { \"Content-Type\": \"application/json\" })\n          .end(JSON.stringify({ status: \"ok\" }));\n      } catch {\n        response\n          .writeHead(400, { \"Content-Type\": \"application/json\" })\n          .end(JSON.stringify({ error: \"Invalid context payload\" }));\n      }\n      return;\n    }\n\n    if (url.pathname === \"/mcp\") {\n      const sessionId = request.headers[\"mcp-session-id\"] as string | undefined;\n      const existingSession = sessionId ? sessions.get(sessionId) : undefined;\n\n      if (existingSession) {\n        await existingSession.transport.handleRequest(request, response);\n        return;\n      }\n\n      if (request.method === \"POST\") {\n        const mcpServer = createMcpServer();\n        const transport = new StreamableHTTPServerTransport({\n          sessionIdGenerator: () => randomUUID(),\n        });\n\n        transport.onclose = () => {\n          if (transport.sessionId) {\n            sessions.delete(transport.sessionId);\n          }\n        };\n\n        await mcpServer.server.connect(transport);\n        await transport.handleRequest(request, response);\n\n        if (transport.sessionId) {\n          sessions.set(transport.sessionId, { server: mcpServer, transport });\n        }\n        return;\n      }\n\n      response.writeHead(400, { \"Content-Type\": \"application/json\" }).end(\n        JSON.stringify({\n          error: \"No valid session. Send an initialize request first.\",\n        }),\n      );\n      return;\n    }\n\n    response.writeHead(404).end(\"Not found\");\n  });\n};\n\nconst listenWithRetry = (httpServer: Server, port: number): Promise<void> =>\n  new Promise((resolve, reject) => {\n    httpServer.once(\"error\", async (error: NodeJS.ErrnoException) => {\n      if (error.code !== \"EADDRINUSE\") {\n        reject(error);\n        return;\n      }\n\n      await fkill(`:${port}`, { force: true, silent: true }).catch(() => {});\n      await sleep(POST_KILL_DELAY_MS);\n\n      httpServer.once(\"error\", reject);\n      httpServer.listen(port, () => resolve());\n    });\n\n    httpServer.listen(port, \"127.0.0.1\", () => resolve());\n  });\n\nconst startHttpServer = async (port: number): Promise<Server> => {\n  const isAlreadyRunning = await checkIfServerIsRunning(port);\n\n  if (!isAlreadyRunning) {\n    await fkill(`:${port}`, { force: true, silent: true }).catch(() => {});\n    await sleep(POST_KILL_DELAY_MS);\n  }\n\n  const httpServer = createHttpServer(port);\n  await listenWithRetry(httpServer, port);\n\n  const handleShutdown = () => {\n    httpServer.close();\n    process.exit(0);\n  };\n\n  process.on(\"SIGTERM\", handleShutdown);\n  process.on(\"SIGINT\", handleShutdown);\n\n  return httpServer;\n};\n\ninterface StartMcpServerOptions {\n  port?: number;\n  stdio?: boolean;\n}\n\nexport const startMcpServer = async ({\n  port = DEFAULT_MCP_PORT,\n  stdio = false,\n}: StartMcpServerOptions = {}): Promise<void> => {\n  if (stdio) {\n    const mcpServer = createMcpServer();\n    const transport = new StdioServerTransport();\n    await mcpServer.server.connect(transport);\n\n    startHttpServer(port).then(\n      () =>\n        console.error(`React Grab context server listening on port ${port}`),\n      (error) => console.error(`Failed to start context server: ${error}`),\n    );\n    return;\n  }\n\n  await startHttpServer(port);\n  console.log(\n    `React Grab MCP server listening on http://localhost:${port}/mcp`,\n  );\n};\n"
  },
  {
    "path": "packages/mcp/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"noEmit\": true,\n    \"types\": [\"node\"]\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/mcp/tsup.config.ts",
    "content": "import module from \"node:module\";\nimport { defineConfig } from \"tsup\";\n\nexport default defineConfig([\n  {\n    entry: {\n      client: \"./src/client.ts\",\n    },\n    format: [\"cjs\", \"esm\"],\n    dts: true,\n    clean: false,\n    splitting: false,\n    sourcemap: false,\n    target: \"esnext\",\n    platform: \"browser\",\n    treeshake: true,\n  },\n  {\n    entry: [\"./src/client.ts\"],\n    format: [\"iife\"],\n    globalName: \"ReactGrabMcp\",\n    outExtension: () => ({ js: \".global.js\" }),\n    dts: false,\n    clean: false,\n    minify: process.env.NODE_ENV === \"production\",\n    splitting: false,\n    sourcemap: false,\n    target: \"esnext\",\n    platform: \"browser\",\n    treeshake: true,\n    noExternal: [/.*/],\n  },\n  {\n    entry: {\n      server: \"./src/server.ts\",\n      cli: \"./src/cli.ts\",\n    },\n    format: [\"cjs\", \"esm\"],\n    dts: true,\n    clean: false,\n    splitting: false,\n    sourcemap: false,\n    target: \"node18\",\n    platform: \"node\",\n    treeshake: true,\n    noExternal: [/.*/],\n    external: [\n      ...module.builtinModules,\n      ...module.builtinModules.map((name) => `node:${name}`),\n    ],\n  },\n]);\n"
  },
  {
    "path": "packages/provider-amp/CHANGELOG.md",
    "content": "# @react-grab/amp\n\n## 0.1.28\n\n### Patch Changes\n\n- fix\n- Updated dependencies\n  - react-grab@0.1.28\n  - @react-grab/relay@0.1.28\n\n## 0.1.27\n\n### Patch Changes\n\n- fix: install instructions\n- Updated dependencies\n  - react-grab@0.1.27\n  - @react-grab/relay@0.1.27\n\n## 0.1.26\n\n### Patch Changes\n\n- fix: minor tweaks\n- Updated dependencies\n  - react-grab@0.1.26\n  - @react-grab/relay@0.1.26\n\n## 0.1.25\n\n### Patch Changes\n\n- fix: primtiives\n- Updated dependencies\n  - react-grab@0.1.25\n  - @react-grab/relay@0.1.25\n\n## 0.1.24\n\n### Patch Changes\n\n- primitives\n- Updated dependencies\n  - react-grab@0.1.24\n  - @react-grab/relay@0.1.24\n\n## 0.1.23\n\n### Patch Changes\n\n- fix: npx command\n- Updated dependencies\n  - react-grab@0.1.23\n  - @react-grab/relay@0.1.23\n\n## 0.1.22\n\n### Patch Changes\n\n- fix: freezing\n- Updated dependencies\n  - react-grab@0.1.22\n  - @react-grab/relay@0.1.22\n\n## 0.1.21\n\n### Patch Changes\n\n- fix: up and down selection\n- Updated dependencies\n  - react-grab@0.1.21\n  - @react-grab/relay@0.1.21\n\n## 0.1.20\n\n### Patch Changes\n\n- fix: selection performacne\n- Updated dependencies\n  - react-grab@0.1.20\n  - @react-grab/relay@0.1.20\n\n## 0.1.19\n\n### Patch Changes\n\n- fix: gsap\n- Updated dependencies\n  - react-grab@0.1.19\n  - @react-grab/relay@0.1.19\n\n## 0.1.18\n\n### Patch Changes\n\n- fix: minor issues\n- Updated dependencies\n  - react-grab@0.1.18\n  - @react-grab/relay@0.1.18\n\n## 0.1.17\n\n### Patch Changes\n\n- fix: mcp\n- Updated dependencies\n  - react-grab@0.1.17\n  - @react-grab/relay@0.1.17\n\n## 0.1.16\n\n### Patch Changes\n\n- fix: environment detection\n- Updated dependencies\n  - react-grab@0.1.16\n  - @react-grab/relay@0.1.16\n\n## 0.1.15\n\n### Patch Changes\n\n- fix: animations and ux\n- Updated dependencies\n  - react-grab@0.1.15\n  - @react-grab/relay@0.1.15\n\n## 0.1.14\n\n### Patch Changes\n\n- fix: improve recent UX\n- Updated dependencies\n  - react-grab@0.1.14\n  - @react-grab/relay@0.1.14\n\n## 0.1.13\n\n### Patch Changes\n\n- fix MCP client injection\n- Updated dependencies\n  - react-grab@0.1.13\n  - @react-grab/relay@0.1.13\n\n## 0.1.12\n\n### Patch Changes\n\n- feat: MCP\n- Updated dependencies\n  - react-grab@0.1.12\n  - @react-grab/relay@0.1.12\n\n## 0.1.11\n\n### Patch Changes\n\n- fix: claude code provider\n- Updated dependencies\n  - react-grab@0.1.11\n  - @react-grab/relay@0.1.11\n\n## 0.1.10\n\n### Patch Changes\n\n- feat: cdn in cli\n- Updated dependencies\n  - react-grab@0.1.10\n  - @react-grab/relay@0.1.10\n\n## 0.1.9\n\n### Patch Changes\n\n- fix: startServer not exported in providers\n- Updated dependencies\n  - react-grab@0.1.9\n  - @react-grab/relay@0.1.9\n\n## 0.1.8\n\n### Patch Changes\n\n- fix: providers not detaching\n- Updated dependencies\n  - react-grab@0.1.8\n  - @react-grab/relay@0.1.8\n\n## 0.1.7\n\n### Patch Changes\n\n- fix: react freezing safety\n- Updated dependencies\n  - react-grab@0.1.7\n  - @react-grab/relay@0.1.7\n\n## 0.1.6\n\n### Patch Changes\n\n- fix: compat with React Scan\n- Updated dependencies\n  - react-grab@0.1.6\n  - @react-grab/relay@0.1.6\n\n## 0.1.5\n\n### Patch Changes\n\n- fix: fullscreen inset the root\n- Updated dependencies\n  - react-grab@0.1.5\n  - @react-grab/relay@0.1.5\n\n## 0.1.4\n\n### Patch Changes\n\n- fix: improve cli edge cases\n- Updated dependencies\n  - react-grab@0.1.4\n  - @react-grab/relay@0.1.4\n\n## 0.1.3\n\n### Patch Changes\n\n- fix: @react-grab/utils not publishing\n- Updated dependencies\n  - react-grab@0.1.3\n  - @react-grab/relay@0.1.3\n\n## 0.1.2\n\n### Patch Changes\n\n- fix: packages not being published\n- Updated dependencies\n  - react-grab@0.1.2\n  - @react-grab/relay@0.1.2\n\n## 0.1.1\n\n### Patch Changes\n\n- fix: clicking element after keyboard navigation\n- Updated dependencies\n  - react-grab@0.1.1\n  - @react-grab/relay@0.1.1\n\n## 0.1.0\n\n### Minor Changes\n\n- 81adb50: feat: browser\n\n### Patch Changes\n\n- 81adb50: fix: shell script\n- fb2b037: fix: cli\n- a3d5a94: fix: cli global install\n- 81adb50: feat: react support\n- 81adb50: fix: a11y\n- a5e7a6a: fix: optimize loading speed of cli\n- 90af3f6: fix: CLI hanging\n- 81adb50: fix: shell script\n- 78efee2: fix: cli\n- 074e593: fix: cli\n- 5cd3709: fix: decouple browser out from react-grab\n- 54c4867: ui improvements\n- Updated dependencies [81adb50]\n- Updated dependencies [fb2b037]\n- Updated dependencies [a3d5a94]\n- Updated dependencies [81adb50]\n- Updated dependencies [81adb50]\n- Updated dependencies [616d3e8]\n- Updated dependencies [81adb50]\n- Updated dependencies [a5e7a6a]\n- Updated dependencies [90af3f6]\n- Updated dependencies [81adb50]\n- Updated dependencies [81adb50]\n- Updated dependencies [78efee2]\n- Updated dependencies [074e593]\n- Updated dependencies [5cd3709]\n- Updated dependencies [54c4867]\n  - react-grab@0.1.0\n  - @react-grab/relay@0.1.0\n\n## 0.1.0-beta.13\n\n### Patch Changes\n\n- ui improvements\n- Updated dependencies [616d3e8]\n- Updated dependencies\n  - react-grab@0.1.0-beta.13\n  - @react-grab/relay@0.1.0-beta.13\n\n## 0.1.0-beta.12\n\n### Patch Changes\n\n- fix: decouple browser out from react-grab\n- Updated dependencies\n  - react-grab@0.1.0-beta.12\n  - @react-grab/relay@0.1.0-beta.12\n\n## 0.1.0-beta.11\n\n### Patch Changes\n\n- fix: cli global install\n- Updated dependencies\n  - react-grab@0.1.0-beta.11\n  - @react-grab/relay@0.1.0-beta.11\n\n## 0.1.0-beta.10\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.10\n  - @react-grab/relay@0.1.0-beta.10\n\n## 0.1.0-beta.9\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.9\n  - @react-grab/relay@0.1.0-beta.9\n\n## 0.1.0-beta.8\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.8\n  - @react-grab/relay@0.1.0-beta.8\n\n## 0.1.0-beta.7\n\n### Patch Changes\n\n- fix: CLI hanging\n- Updated dependencies\n  - react-grab@0.1.0-beta.7\n  - @react-grab/relay@0.1.0-beta.7\n\n## 0.1.0-beta.6\n\n### Patch Changes\n\n- fix: optimize loading speed of cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.6\n  - @react-grab/relay@0.1.0-beta.6\n\n## 0.1.0-beta.5\n\n### Patch Changes\n\n- fix: a11y\n- Updated dependencies\n  - react-grab@0.1.0-beta.5\n  - @react-grab/relay@0.1.0-beta.5\n\n## 0.1.0-beta.4\n\n### Patch Changes\n\n- feat: react support\n- Updated dependencies\n  - react-grab@0.1.0-beta.4\n  - @react-grab/relay@0.1.0-beta.4\n\n## 0.1.0-beta.3\n\n### Patch Changes\n\n- Updated dependencies\n  - react-grab@0.1.0-beta.3\n\n## 0.1.0-beta.2\n\n### Patch Changes\n\n- fix: shell script\n- Updated dependencies\n  - react-grab@0.1.0-beta.2\n  - @react-grab/relay@0.1.0-beta.2\n\n## 0.1.0-beta.1\n\n### Patch Changes\n\n- fix: shell script\n- Updated dependencies\n  - react-grab@0.1.0-beta.1\n  - @react-grab/relay@0.1.0-beta.1\n\n## 0.1.0-beta.0\n\n### Minor Changes\n\n- feat: browser\n\n### Patch Changes\n\n- Updated dependencies\n  - react-grab@0.1.0-beta.0\n  - @react-grab/relay@0.1.0-beta.0\n\n## 0.0.98\n\n### Patch Changes\n\n- feat: new state architecture and context menu\n- Updated dependencies\n  - react-grab@0.0.98\n\n## 0.0.97\n\n### Patch Changes\n\n- fix: sourcemap error\n- Updated dependencies\n  - react-grab@0.0.97\n\n## 0.0.96\n\n### Patch Changes\n\n- fix: fiber access timeout handling\n- Updated dependencies\n  - react-grab@0.0.96\n\n## 0.0.95\n\n### Patch Changes\n\n- fix: selecting buttons with disabled states\n- Updated dependencies\n  - react-grab@0.0.95\n\n## 0.0.94\n\n### Patch Changes\n\n- fix: browser crashing on selection bug\n- Updated dependencies\n  - react-grab@0.0.94\n\n## 0.0.93\n\n### Patch Changes\n\n- fix: copying not working\n- Updated dependencies\n  - react-grab@0.0.93\n\n## 0.0.92\n\n### Patch Changes\n\n- refactor: use state machines instead of signals\n- Updated dependencies\n  - react-grab@0.0.92\n\n## 0.0.91\n\n### Patch Changes\n\n- feat: dock\n- Updated dependencies\n  - react-grab@0.0.91\n\n## 0.0.90\n\n### Patch Changes\n\n- fix: check visual edit endpoint\n- Updated dependencies\n  - react-grab@0.0.90\n\n## 0.0.89\n\n### Patch Changes\n\n- fix: many bugfixes\n- Updated dependencies\n  - react-grab@0.0.89\n\n## 0.0.88\n\n### Patch Changes\n\n- fix: deprecation errors\n- Updated dependencies\n  - react-grab@0.0.88\n\n## 0.0.87\n\n### Patch Changes\n\n- feat: visual edits\n- Updated dependencies\n  - react-grab@0.0.87\n\n## 0.0.86\n\n### Patch Changes\n\n- fix: editing\n- Updated dependencies\n  - react-grab@0.0.86\n\n## 0.0.85\n\n### Patch Changes\n\n- fix: check versions on each provider\n- Updated dependencies\n  - react-grab@0.0.85\n\n## 0.0.84\n\n### Patch Changes\n\n- fix: migrate from cross-spawn to execa to fix deprecation issue\n- Updated dependencies\n  - react-grab@0.0.84\n\n## 0.0.83\n\n### Patch Changes\n\n- feat: timings during agent processing\n- Updated dependencies\n  - react-grab@0.0.83\n\n## 0.0.82\n\n### Patch Changes\n\n- fix: agent support\n- Updated dependencies\n  - react-grab@0.0.82\n\n## 0.0.81\n\n### Patch Changes\n\n- feat: add Amp SDK provider with undo and follow-up support\n- Updated dependencies\n  - react-grab@0.0.81\n"
  },
  {
    "path": "packages/provider-amp/README.md",
    "content": "# @react-grab/amp\n\n[Amp](https://ampcode.com) provider for React Grab.\n\n## Installation\n\n```bash\nnpm install @react-grab/amp\n```\n\n## Usage\n\n### Server\n\nThe server runs on port `9567` and interfaces with the Amp SDK. Add to your `package.json`:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"npx @react-grab/amp@latest && next dev\"\n  }\n}\n```\n\n> **Note:** You must have an [Amp API key](https://ampcode.com/settings) set via `AMP_API_KEY` environment variable.\n\n### Client\n\nAdd the client script to your HTML:\n\n```html\n<script src=\"//unpkg.com/@react-grab/amp/dist/client.global.js\"></script>\n```\n\nOr import programmatically:\n\n```ts\nimport \"@react-grab/amp/client\";\n```\n\n## Features\n\n- **Follow-ups**: Continue conversations with thread continuity\n- **Undo**: Undo the last change made by Amp\n- **Streaming**: Real-time status updates during execution\n- **Tool calls**: See tool usage as it happens\n"
  },
  {
    "path": "packages/provider-amp/package.json",
    "content": "{\n  \"name\": \"@react-grab/amp\",\n  \"version\": \"0.1.28\",\n  \"bin\": {\n    \"react-grab-amp\": \"./dist/cli.cjs\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"type\": \"module\",\n  \"browser\": \"dist/client.global.js\",\n  \"exports\": {\n    \"./client\": {\n      \"types\": \"./dist/client.d.ts\",\n      \"import\": \"./dist/client.js\",\n      \"require\": \"./dist/client.cjs\"\n    },\n    \"./handler\": {\n      \"types\": \"./dist/handler.d.ts\",\n      \"import\": \"./dist/handler.js\",\n      \"require\": \"./dist/handler.cjs\"\n    },\n    \"./server\": {\n      \"types\": \"./dist/server.d.ts\",\n      \"import\": \"./dist/server.js\",\n      \"require\": \"./dist/server.cjs\"\n    },\n    \"./dist/*\": \"./dist/*.js\",\n    \"./dist/*.js\": \"./dist/*.js\"\n  },\n  \"scripts\": {\n    \"dev\": \"tsup --watch\",\n    \"build\": \"rm -rf dist && NODE_ENV=production tsup\"\n  },\n  \"dependencies\": {\n    \"@react-grab/relay\": \"workspace:*\",\n    \"@sourcegraph/amp-sdk\": \"^0.1.0-20251210081226-g90e3892\",\n    \"react-grab\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@react-grab/utils\": \"workspace:*\",\n    \"@types/node\": \"^22.10.7\",\n    \"tsup\": \"^8.4.0\"\n  }\n}\n"
  },
  {
    "path": "packages/provider-amp/src/cli.ts",
    "content": "#!/usr/bin/env node\nimport { spawn } from \"node:child_process\";\nimport { realpathSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\nconst realScriptPath = realpathSync(process.argv[1]);\nconst scriptDir = dirname(realScriptPath);\nconst serverPath = join(scriptDir, \"server.cjs\");\n\nconst child = spawn(\n  process.execPath,\n  [\"-e\", `require(${JSON.stringify(serverPath)}).startServer()`],\n  {\n    detached: true,\n    stdio: \"inherit\",\n  },\n);\n\nchild.unref();\nprocess.exit(0);\n"
  },
  {
    "path": "packages/provider-amp/src/client.ts",
    "content": "import type { AgentCompleteResult } from \"react-grab/core\";\nimport { createProviderClientPlugin } from \"@react-grab/relay/client\";\n\nexport type { AgentCompleteResult };\n\nconst { createAgentProvider: createAmpAgentProvider, attachAgent } =\n  createProviderClientPlugin({\n    agentId: \"amp\",\n    pluginName: \"amp-agent\",\n    actionId: \"edit-with-amp\",\n    actionLabel: \"Edit with Amp\",\n  });\n\nexport { createAmpAgentProvider, attachAgent };\n\nattachAgent();\n"
  },
  {
    "path": "packages/provider-amp/src/handler.ts",
    "content": "import { execute } from \"@sourcegraph/amp-sdk\";\nimport type {\n  AgentHandler,\n  AgentMessage,\n  AgentRunOptions,\n} from \"@react-grab/relay\";\nimport { COMPLETED_STATUS } from \"@react-grab/relay\";\n\nexport interface AmpAgentOptions extends AgentRunOptions {}\n\ninterface ThreadState {\n  threadId: string;\n}\n\nconst threadMap = new Map<string, ThreadState>();\nconst abortControllers = new Map<string, AbortController>();\nlet lastThreadId: string | undefined;\n\nconst extractTextFromContent = (\n  content: Array<{ type: string; text?: string; name?: string }>,\n): string => {\n  return content\n    .filter((item) => item.type === \"text\" && item.text)\n    .map((item) => item.text)\n    .join(\" \")\n    .trim();\n};\n\nconst runAmpAgent = async function* (\n  prompt: string,\n  options?: AmpAgentOptions,\n): AsyncGenerator<AgentMessage> {\n  const sessionId = options?.sessionId;\n  const abortController = new AbortController();\n\n  if (sessionId) {\n    abortControllers.set(sessionId, abortController);\n  }\n\n  const isAborted = () => {\n    if (options?.signal?.aborted) return true;\n    if (abortController.signal.aborted) return true;\n    return false;\n  };\n\n  try {\n    yield { type: \"status\", content: \"Thinking…\" };\n\n    const executeOptions: {\n      dangerouslyAllowAll: boolean;\n      cwd?: string;\n      continue?: boolean | string;\n    } = {\n      dangerouslyAllowAll: true,\n    };\n\n    executeOptions.cwd =\n      options?.cwd ?? process.env.REACT_GRAB_CWD ?? process.cwd();\n\n    const existingThread = sessionId ? threadMap.get(sessionId) : undefined;\n    if (existingThread) {\n      executeOptions.continue = existingThread.threadId;\n    }\n\n    let capturedThreadId: string | undefined;\n\n    for await (const message of execute({\n      prompt,\n      options: executeOptions,\n      signal: abortController.signal,\n    })) {\n      if (isAborted()) break;\n\n      switch (message.type) {\n        case \"system\":\n          if (message.subtype === \"init\") {\n            const systemMessage = message as { thread_id?: string };\n            if (systemMessage.thread_id) {\n              capturedThreadId = systemMessage.thread_id;\n            }\n            yield { type: \"status\", content: \"Session started...\" };\n          }\n          break;\n\n        case \"assistant\": {\n          const messageContent = message.message?.content;\n          if (messageContent && Array.isArray(messageContent)) {\n            const toolUse = messageContent.find(\n              (item: { type: string }) => item.type === \"tool_use\",\n            );\n            if (toolUse && \"name\" in toolUse) {\n              yield { type: \"status\", content: `Using ${toolUse.name}...` };\n            } else {\n              const textContent = extractTextFromContent(messageContent);\n              if (textContent && !isAborted()) {\n                yield { type: \"status\", content: textContent };\n              }\n            }\n          }\n          break;\n        }\n\n        case \"result\":\n          if (message.is_error) {\n            yield { type: \"error\", content: message.error || \"Unknown error\" };\n          } else {\n            yield { type: \"status\", content: COMPLETED_STATUS };\n          }\n          break;\n      }\n    }\n\n    if (sessionId && capturedThreadId && !isAborted()) {\n      threadMap.set(sessionId, { threadId: capturedThreadId });\n    }\n\n    if (capturedThreadId) {\n      lastThreadId = capturedThreadId;\n    }\n\n    if (!isAborted()) {\n      yield { type: \"done\", content: \"\" };\n    }\n  } catch (error) {\n    if (!isAborted()) {\n      const errorMessage =\n        error instanceof Error ? error.message : \"Unknown error\";\n      yield { type: \"error\", content: errorMessage };\n      yield { type: \"done\", content: \"\" };\n    }\n  } finally {\n    if (sessionId) {\n      abortControllers.delete(sessionId);\n    }\n  }\n};\n\nconst abortAmpAgent = (sessionId: string) => {\n  const abortController = abortControllers.get(sessionId);\n  if (abortController) {\n    abortController.abort();\n    abortControllers.delete(sessionId);\n  }\n};\n\nconst undoAmpAgent = async (): Promise<void> => {\n  if (!lastThreadId) {\n    return;\n  }\n\n  // HACK: consume all messages to complete the undo\n  for await (const _message of execute({\n    prompt: \"undo\",\n    options: {\n      dangerouslyAllowAll: true,\n      cwd: process.env.REACT_GRAB_CWD ?? process.cwd(),\n      continue: lastThreadId,\n    },\n  })) {\n  }\n};\n\nexport const ampAgentHandler: AgentHandler = {\n  agentId: \"amp\",\n  run: runAmpAgent,\n  abort: abortAmpAgent,\n  undo: undoAmpAgent,\n};\n"
  },
  {
    "path": "packages/provider-amp/src/server.ts",
    "content": "import { startProviderServer } from \"@react-grab/relay\";\nimport { ampAgentHandler } from \"./handler.js\";\n\nexport const startServer = () => {\n  startProviderServer(\"amp\", ampAgentHandler);\n};\n"
  },
  {
    "path": "packages/provider-amp/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"esModuleInterop\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"declaration\": true,\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\",\n    \"types\": [\"node\"]\n  },\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/provider-amp/tsup.config.ts",
    "content": "import fs from \"node:fs\";\nimport module from \"node:module\";\nimport { defineConfig } from \"tsup\";\n\nconst packageJson = JSON.parse(fs.readFileSync(\"package.json\", \"utf8\")) as {\n  version: string;\n};\n\nexport default defineConfig([\n  {\n    entry: {\n      handler: \"./src/handler.ts\",\n      cli: \"./src/cli.ts\",\n      server: \"./src/server.ts\",\n    },\n    format: [\"cjs\", \"esm\"],\n    dts: true,\n    clean: false,\n    splitting: false,\n    sourcemap: false,\n    target: \"node18\",\n    platform: \"node\",\n    treeshake: true,\n    noExternal: [/.*/],\n    external: [\n      ...module.builtinModules,\n      ...module.builtinModules.map((name) => `node:${name}`),\n    ],\n    env: {\n      VERSION: process.env.VERSION ?? packageJson.version,\n    },\n  },\n  {\n    entry: {\n      client: \"./src/client.ts\",\n    },\n    format: [\"cjs\", \"esm\"],\n    dts: true,\n    clean: false,\n    splitting: false,\n    sourcemap: false,\n    target: \"esnext\",\n    platform: \"browser\",\n    treeshake: true,\n    noExternal: [\"@react-grab/relay\"],\n  },\n  {\n    entry: [\"./src/client.ts\"],\n    format: [\"iife\"],\n    globalName: \"ReactGrabAmp\",\n    outExtension: () => ({ js: \".global.js\" }),\n    dts: false,\n    clean: false,\n    minify: process.env.NODE_ENV === \"production\",\n    splitting: false,\n    sourcemap: false,\n    target: \"esnext\",\n    platform: \"browser\",\n    treeshake: true,\n    noExternal: [/.*/],\n  },\n]);\n"
  },
  {
    "path": "packages/provider-claude-code/CHANGELOG.md",
    "content": "# @react-grab/claude-code\n\n## 0.1.28\n\n### Patch Changes\n\n- fix\n- Updated dependencies\n  - react-grab@0.1.28\n  - @react-grab/relay@0.1.28\n\n## 0.1.27\n\n### Patch Changes\n\n- fix: install instructions\n- Updated dependencies\n  - react-grab@0.1.27\n  - @react-grab/relay@0.1.27\n\n## 0.1.26\n\n### Patch Changes\n\n- fix: minor tweaks\n- Updated dependencies\n  - react-grab@0.1.26\n  - @react-grab/relay@0.1.26\n\n## 0.1.25\n\n### Patch Changes\n\n- fix: primtiives\n- Updated dependencies\n  - react-grab@0.1.25\n  - @react-grab/relay@0.1.25\n\n## 0.1.24\n\n### Patch Changes\n\n- primitives\n- Updated dependencies\n  - react-grab@0.1.24\n  - @react-grab/relay@0.1.24\n\n## 0.1.23\n\n### Patch Changes\n\n- fix: npx command\n- Updated dependencies\n  - react-grab@0.1.23\n  - @react-grab/relay@0.1.23\n\n## 0.1.22\n\n### Patch Changes\n\n- fix: freezing\n- Updated dependencies\n  - react-grab@0.1.22\n  - @react-grab/relay@0.1.22\n\n## 0.1.21\n\n### Patch Changes\n\n- fix: up and down selection\n- Updated dependencies\n  - react-grab@0.1.21\n  - @react-grab/relay@0.1.21\n\n## 0.1.20\n\n### Patch Changes\n\n- fix: selection performacne\n- Updated dependencies\n  - react-grab@0.1.20\n  - @react-grab/relay@0.1.20\n\n## 0.1.19\n\n### Patch Changes\n\n- fix: gsap\n- Updated dependencies\n  - react-grab@0.1.19\n  - @react-grab/relay@0.1.19\n\n## 0.1.18\n\n### Patch Changes\n\n- fix: minor issues\n- Updated dependencies\n  - react-grab@0.1.18\n  - @react-grab/relay@0.1.18\n\n## 0.1.17\n\n### Patch Changes\n\n- fix: mcp\n- Updated dependencies\n  - react-grab@0.1.17\n  - @react-grab/relay@0.1.17\n\n## 0.1.16\n\n### Patch Changes\n\n- fix: environment detection\n- Updated dependencies\n  - react-grab@0.1.16\n  - @react-grab/relay@0.1.16\n\n## 0.1.15\n\n### Patch Changes\n\n- fix: animations and ux\n- Updated dependencies\n  - react-grab@0.1.15\n  - @react-grab/relay@0.1.15\n\n## 0.1.14\n\n### Patch Changes\n\n- fix: improve recent UX\n- Updated dependencies\n  - react-grab@0.1.14\n  - @react-grab/relay@0.1.14\n\n## 0.1.13\n\n### Patch Changes\n\n- fix MCP client injection\n- Updated dependencies\n  - react-grab@0.1.13\n  - @react-grab/relay@0.1.13\n\n## 0.1.12\n\n### Patch Changes\n\n- feat: MCP\n- Updated dependencies\n  - react-grab@0.1.12\n  - @react-grab/relay@0.1.12\n\n## 0.1.11\n\n### Patch Changes\n\n- fix: claude code provider\n- Updated dependencies\n  - react-grab@0.1.11\n  - @react-grab/relay@0.1.11\n\n## 0.1.10\n\n### Patch Changes\n\n- feat: cdn in cli\n- Updated dependencies\n  - react-grab@0.1.10\n  - @react-grab/relay@0.1.10\n\n## 0.1.9\n\n### Patch Changes\n\n- fix: startServer not exported in providers\n- Updated dependencies\n  - react-grab@0.1.9\n  - @react-grab/relay@0.1.9\n\n## 0.1.8\n\n### Patch Changes\n\n- fix: providers not detaching\n- Updated dependencies\n  - react-grab@0.1.8\n  - @react-grab/relay@0.1.8\n\n## 0.1.7\n\n### Patch Changes\n\n- fix: react freezing safety\n- Updated dependencies\n  - react-grab@0.1.7\n  - @react-grab/relay@0.1.7\n\n## 0.1.6\n\n### Patch Changes\n\n- fix: compat with React Scan\n- Updated dependencies\n  - react-grab@0.1.6\n  - @react-grab/relay@0.1.6\n\n## 0.1.5\n\n### Patch Changes\n\n- fix: fullscreen inset the root\n- Updated dependencies\n  - react-grab@0.1.5\n  - @react-grab/relay@0.1.5\n\n## 0.1.4\n\n### Patch Changes\n\n- fix: improve cli edge cases\n- Updated dependencies\n  - react-grab@0.1.4\n  - @react-grab/relay@0.1.4\n\n## 0.1.3\n\n### Patch Changes\n\n- fix: @react-grab/utils not publishing\n- Updated dependencies\n  - react-grab@0.1.3\n  - @react-grab/relay@0.1.3\n\n## 0.1.2\n\n### Patch Changes\n\n- fix: packages not being published\n- Updated dependencies\n  - react-grab@0.1.2\n  - @react-grab/relay@0.1.2\n\n## 0.1.1\n\n### Patch Changes\n\n- fix: clicking element after keyboard navigation\n- Updated dependencies\n  - react-grab@0.1.1\n  - @react-grab/relay@0.1.1\n\n## 0.1.0\n\n### Minor Changes\n\n- 81adb50: feat: browser\n\n### Patch Changes\n\n- 81adb50: fix: shell script\n- fb2b037: fix: cli\n- a3d5a94: fix: cli global install\n- 81adb50: feat: react support\n- 81adb50: fix: a11y\n- a5e7a6a: fix: optimize loading speed of cli\n- 90af3f6: fix: CLI hanging\n- 81adb50: fix: shell script\n- 78efee2: fix: cli\n- 074e593: fix: cli\n- 5cd3709: fix: decouple browser out from react-grab\n- 54c4867: ui improvements\n- Updated dependencies [81adb50]\n- Updated dependencies [fb2b037]\n- Updated dependencies [a3d5a94]\n- Updated dependencies [81adb50]\n- Updated dependencies [81adb50]\n- Updated dependencies [616d3e8]\n- Updated dependencies [81adb50]\n- Updated dependencies [a5e7a6a]\n- Updated dependencies [90af3f6]\n- Updated dependencies [81adb50]\n- Updated dependencies [81adb50]\n- Updated dependencies [78efee2]\n- Updated dependencies [074e593]\n- Updated dependencies [5cd3709]\n- Updated dependencies [54c4867]\n  - react-grab@0.1.0\n  - @react-grab/relay@0.1.0\n\n## 0.1.0-beta.13\n\n### Patch Changes\n\n- ui improvements\n- Updated dependencies [616d3e8]\n- Updated dependencies\n  - react-grab@0.1.0-beta.13\n  - @react-grab/relay@0.1.0-beta.13\n\n## 0.1.0-beta.12\n\n### Patch Changes\n\n- fix: decouple browser out from react-grab\n- Updated dependencies\n  - react-grab@0.1.0-beta.12\n  - @react-grab/relay@0.1.0-beta.12\n\n## 0.1.0-beta.11\n\n### Patch Changes\n\n- fix: cli global install\n- Updated dependencies\n  - react-grab@0.1.0-beta.11\n  - @react-grab/relay@0.1.0-beta.11\n\n## 0.1.0-beta.10\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.10\n  - @react-grab/relay@0.1.0-beta.10\n\n## 0.1.0-beta.9\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.9\n  - @react-grab/relay@0.1.0-beta.9\n\n## 0.1.0-beta.8\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.8\n  - @react-grab/relay@0.1.0-beta.8\n\n## 0.1.0-beta.7\n\n### Patch Changes\n\n- fix: CLI hanging\n- Updated dependencies\n  - react-grab@0.1.0-beta.7\n  - @react-grab/relay@0.1.0-beta.7\n\n## 0.1.0-beta.6\n\n### Patch Changes\n\n- fix: optimize loading speed of cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.6\n  - @react-grab/relay@0.1.0-beta.6\n\n## 0.1.0-beta.5\n\n### Patch Changes\n\n- fix: a11y\n- Updated dependencies\n  - react-grab@0.1.0-beta.5\n  - @react-grab/relay@0.1.0-beta.5\n\n## 0.1.0-beta.4\n\n### Patch Changes\n\n- feat: react support\n- Updated dependencies\n  - react-grab@0.1.0-beta.4\n  - @react-grab/relay@0.1.0-beta.4\n\n## 0.1.0-beta.3\n\n### Patch Changes\n\n- Updated dependencies\n  - react-grab@0.1.0-beta.3\n\n## 0.1.0-beta.2\n\n### Patch Changes\n\n- fix: shell script\n- Updated dependencies\n  - react-grab@0.1.0-beta.2\n  - @react-grab/relay@0.1.0-beta.2\n\n## 0.1.0-beta.1\n\n### Patch Changes\n\n- fix: shell script\n- Updated dependencies\n  - react-grab@0.1.0-beta.1\n  - @react-grab/relay@0.1.0-beta.1\n\n## 0.1.0-beta.0\n\n### Minor Changes\n\n- feat: browser\n\n### Patch Changes\n\n- Updated dependencies\n  - react-grab@0.1.0-beta.0\n  - @react-grab/relay@0.1.0-beta.0\n\n## 0.0.98\n\n### Patch Changes\n\n- feat: new state architecture and context menu\n- Updated dependencies\n  - react-grab@0.0.98\n\n## 0.0.97\n\n### Patch Changes\n\n- fix: sourcemap error\n- Updated dependencies\n  - react-grab@0.0.97\n\n## 0.0.96\n\n### Patch Changes\n\n- fix: fiber access timeout handling\n- Updated dependencies\n  - react-grab@0.0.96\n\n## 0.0.95\n\n### Patch Changes\n\n- fix: selecting buttons with disabled states\n- Updated dependencies\n  - react-grab@0.0.95\n\n## 0.0.94\n\n### Patch Changes\n\n- fix: browser crashing on selection bug\n- Updated dependencies\n  - react-grab@0.0.94\n\n## 0.0.93\n\n### Patch Changes\n\n- fix: copying not working\n- Updated dependencies\n  - react-grab@0.0.93\n\n## 0.0.92\n\n### Patch Changes\n\n- refactor: use state machines instead of signals\n- Updated dependencies\n  - react-grab@0.0.92\n\n## 0.0.91\n\n### Patch Changes\n\n- feat: dock\n- Updated dependencies\n  - react-grab@0.0.91\n\n## 0.0.90\n\n### Patch Changes\n\n- fix: check visual edit endpoint\n- Updated dependencies\n  - react-grab@0.0.90\n\n## 0.0.89\n\n### Patch Changes\n\n- fix: many bugfixes\n- Updated dependencies\n  - react-grab@0.0.89\n\n## 0.0.88\n\n### Patch Changes\n\n- fix: deprecation errors\n- Updated dependencies\n  - react-grab@0.0.88\n\n## 0.0.87\n\n### Patch Changes\n\n- feat: visual edits\n- Updated dependencies\n  - react-grab@0.0.87\n\n## 0.0.86\n\n### Patch Changes\n\n- fix: editing\n- Updated dependencies\n  - react-grab@0.0.86\n\n## 0.0.85\n\n### Patch Changes\n\n- fix: check versions on each provider\n- Updated dependencies\n  - react-grab@0.0.85\n\n## 0.0.84\n\n### Patch Changes\n\n- fix: migrate from cross-spawn to execa to fix deprecation issue\n- Updated dependencies\n  - react-grab@0.0.84\n\n## 0.0.83\n\n### Patch Changes\n\n- feat: timings during agent processing\n- Updated dependencies\n  - react-grab@0.0.83\n\n## 0.0.82\n\n### Patch Changes\n\n- fix: agent support\n- Updated dependencies\n  - react-grab@0.0.82\n\n## 0.0.81\n\n### Patch Changes\n\n- feat: codex and gemini support\n- Updated dependencies\n  - react-grab@0.0.81\n\n## 0.0.80\n\n### Patch Changes\n\n- fix: replies and undo\n- Updated dependencies\n  - react-grab@0.0.80\n\n## 0.0.79\n\n### Patch Changes\n\n- fix: claude code exit issue\n- Updated dependencies\n  - react-grab@0.0.79\n\n## 0.0.78\n\n### Patch Changes\n\n- fix: cancel animation\n- Updated dependencies\n  - react-grab@0.0.78\n\n## 0.0.77\n\n### Patch Changes\n\n- fix: new cli proxying\n- Updated dependencies\n  - react-grab@0.0.77\n\n## 0.0.76\n\n### Patch Changes\n\n- feat: allow CLI under react-grab namespace\n- Updated dependencies\n  - react-grab@0.0.76\n\n## 0.0.75\n\n### Patch Changes\n\n- fix: issue with Illegal Invocation on next.js pages\n- Updated dependencies\n  - react-grab@0.0.75\n\n## 0.0.74\n\n### Patch Changes\n\n- fix: updateOptions\n- Updated dependencies\n  - react-grab@0.0.74\n\n## 0.0.73\n\n### Patch Changes\n\n- fix: improve cli\n- Updated dependencies\n  - react-grab@0.0.73\n\n## 0.0.72\n\n### Patch Changes\n\n- fix: shimmer effect\n- Updated dependencies\n  - react-grab@0.0.72\n\n## 0.0.71\n\n### Patch Changes\n\n- fix: ux nits\n- Updated dependencies\n  - react-grab@0.0.71\n\n## 0.0.70\n\n### Patch Changes\n\n- fix: react-grab cli flow when agents is used\n- Updated dependencies\n  - react-grab@0.0.70\n\n## 0.0.69\n\n### Patch Changes\n\n- fix: CLI on script tag\n- Updated dependencies\n  - react-grab@0.0.69\n\n## 0.0.68\n\n### Patch Changes\n\n- feat: opencode and cli installer\n- Updated dependencies\n  - react-grab@0.0.68\n\n## 0.0.67\n\n### Patch Changes\n\n- fix: logs\n- Updated dependencies\n  - react-grab@0.0.67\n\n## 0.0.66\n\n### Patch Changes\n\n- fix: flash animation\n- Updated dependencies\n  - react-grab@0.0.66\n\n## 0.0.65\n\n### Patch Changes\n\n- fix: instrumentation\n- Updated dependencies\n  - react-grab@0.0.65\n\n## 0.0.64\n\n### Patch Changes\n\n- fix: stream resumption\n- Updated dependencies\n  - react-grab@0.0.64\n\n## 0.0.63\n\n### Patch Changes\n\n- fix: x positioning of selection label\n- Updated dependencies\n  - react-grab@0.0.63\n\n## 0.0.62\n\n### Patch Changes\n\n- fix: stream resumption\n- Updated dependencies\n  - react-grab@0.0.62\n\n## 0.0.61\n\n### Patch Changes\n\n- fix: improved installation strategy\n- Updated dependencies\n  - react-grab@0.0.61\n\n## 0.0.60\n\n### Patch Changes\n\n- fix: loading states\n- Updated dependencies\n  - react-grab@0.0.60\n\n## 0.0.59\n\n### Patch Changes\n\n- fix: improve component name\n- Updated dependencies\n  - react-grab@0.0.59\n\n## 0.0.58\n\n### Patch Changes\n\n- fix: issues with stack\n- Updated dependencies\n  - react-grab@0.0.58\n\n## 0.0.57\n\n### Patch Changes\n\n- fix: improvements to UI\n- Updated dependencies\n  - react-grab@0.0.57\n\n## 0.0.56\n\n### Patch Changes\n\n- add Turborepo for monorepo build orchestration\n- Updated dependencies\n  - react-grab@0.0.56\n\n## 0.0.55\n\n### Patch Changes\n\n- beta\n- Updated dependencies\n  - react-grab@0.0.55\n"
  },
  {
    "path": "packages/provider-claude-code/README.md",
    "content": "# @react-grab/claude-code\n\nClaude Code agent provider for React Grab. Requires running a local server that interfaces with the Claude Agent SDK.\n\n## Installation\n\n```bash\nnpm install @react-grab/claude-code\n# or\npnpm add @react-grab/claude-code\n# or\nbun add @react-grab/claude-code\n# or\nyarn add @react-grab/claude-code\n```\n\n## Server Setup\n\nThe server runs on port `4567` by default.\n\n### Quick Start (CLI)\n\nStart the server in the background before running your dev server:\n\n```bash\nnpx @react-grab/claude-code@latest && pnpm run dev\n```\n\nThe server will run as a detached background process. **Note:** Stopping your dev server (Ctrl+C) won't stop the React Grab server. To stop it:\n\n```bash\npkill -f \"react-grab.*server\"\n```\n\n### Recommended: Config File (Automatic Lifecycle)\n\nFor better lifecycle management, start the server from your config file. This ensures the server stops when your dev server stops:\n\n### Vite\n\n```ts\n// vite.config.ts\nimport { startServer } from \"@react-grab/claude-code/server\";\n\nif (process.env.NODE_ENV === \"development\") {\n  startServer();\n}\n```\n\n### Next.js\n\n```ts\n// next.config.ts\nimport { startServer } from \"@react-grab/claude-code/server\";\n\nif (process.env.NODE_ENV === \"development\") {\n  startServer();\n}\n```\n\n## Client Usage\n\n### Script Tag\n\n```html\n<script src=\"//unpkg.com/react-grab/dist/index.global.js\"></script>\n<script src=\"//unpkg.com/@react-grab/claude-code/dist/client.global.js\"></script>\n```\n\n### Next.js\n\nUsing the `Script` component in your `app/layout.tsx`:\n\n```jsx\nimport Script from \"next/script\";\n\nexport default function RootLayout({ children }) {\n  return (\n    <html>\n      <head>\n        {process.env.NODE_ENV === \"development\" && (\n          <>\n            <Script\n              src=\"//unpkg.com/react-grab/dist/index.global.js\"\n              strategy=\"beforeInteractive\"\n            />\n            <Script\n              src=\"//unpkg.com/@react-grab/claude-code/dist/client.global.js\"\n              strategy=\"lazyOnload\"\n            />\n          </>\n        )}\n      </head>\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\n### ES Module\n\n```tsx\nimport { attachAgent } from \"@react-grab/claude-code/client\";\n\nattachAgent();\n```\n\n## How It Works\n\n```\n┌─────────────────┐      HTTP       ┌─────────────────┐      SDK       ┌─────────────────┐\n│                 │  localhost:4567 │                 │                │                 │\n│   React Grab    │ ──────────────► │     Server      │ ─────────────► │   Claude Code   │\n│    (Browser)    │ ◄────────────── │   (Node.js)     │ ◄───────────── │     (Agent)     │\n│                 │       SSE       │                 │                │                 │\n└─────────────────┘                 └─────────────────┘                └─────────────────┘\n      Client                              Server                            Agent\n```\n\n1. **React Grab** sends the selected element context to the server via HTTP POST\n2. **Server** receives the request and forwards it to Claude Code via the Agent SDK\n3. **Claude Code** processes the request and streams responses back\n4. **Server** relays status updates to the client via Server-Sent Events (SSE)\n"
  },
  {
    "path": "packages/provider-claude-code/package.json",
    "content": "{\n  \"name\": \"@react-grab/claude-code\",\n  \"version\": \"0.1.28\",\n  \"bin\": {\n    \"react-grab-claude-code\": \"./dist/cli.cjs\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"type\": \"module\",\n  \"browser\": \"dist/client.global.js\",\n  \"exports\": {\n    \"./client\": {\n      \"types\": \"./dist/client.d.ts\",\n      \"import\": \"./dist/client.js\",\n      \"require\": \"./dist/client.cjs\"\n    },\n    \"./handler\": {\n      \"types\": \"./dist/handler.d.ts\",\n      \"import\": \"./dist/handler.js\",\n      \"require\": \"./dist/handler.cjs\"\n    },\n    \"./server\": {\n      \"types\": \"./dist/server.d.ts\",\n      \"import\": \"./dist/server.js\",\n      \"require\": \"./dist/server.cjs\"\n    },\n    \"./dist/*\": \"./dist/*.js\",\n    \"./dist/*.js\": \"./dist/*.js\"\n  },\n  \"scripts\": {\n    \"dev\": \"tsup --watch\",\n    \"build\": \"rm -rf dist && NODE_ENV=production tsup\"\n  },\n  \"dependencies\": {\n    \"@anthropic-ai/claude-agent-sdk\": \"^0.1.0\",\n    \"@react-grab/relay\": \"workspace:*\",\n    \"react-grab\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@react-grab/utils\": \"workspace:*\",\n    \"@types/node\": \"^22.10.7\",\n    \"tsup\": \"^8.4.0\"\n  }\n}\n"
  },
  {
    "path": "packages/provider-claude-code/src/cli.ts",
    "content": "#!/usr/bin/env node\nimport { spawn } from \"node:child_process\";\nimport { realpathSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\nconst realScriptPath = realpathSync(process.argv[1]);\nconst scriptDir = dirname(realScriptPath);\nconst serverPath = join(scriptDir, \"server.cjs\");\n\nconst child = spawn(\n  process.execPath,\n  [\"-e\", `require(${JSON.stringify(serverPath)}).startServer()`],\n  {\n    detached: true,\n    stdio: \"inherit\",\n  },\n);\n\nchild.unref();\nprocess.exit(0);\n"
  },
  {
    "path": "packages/provider-claude-code/src/client.ts",
    "content": "import type { AgentCompleteResult } from \"react-grab/core\";\nimport { createProviderClientPlugin } from \"@react-grab/relay/client\";\n\nexport type { AgentCompleteResult };\n\nconst { createAgentProvider: createClaudeAgentProvider, attachAgent } =\n  createProviderClientPlugin({\n    agentId: \"claude-code\",\n    pluginName: \"claude-code-agent\",\n    actionId: \"edit-with-claude-code\",\n    actionLabel: \"Edit with Claude\",\n  });\n\nexport { createClaudeAgentProvider, attachAgent };\n\nattachAgent();\n"
  },
  {
    "path": "packages/provider-claude-code/src/handler.ts",
    "content": "import { execSync } from \"node:child_process\";\nimport {\n  query,\n  type Options,\n  type SDKAssistantMessage,\n} from \"@anthropic-ai/claude-agent-sdk\";\nimport type {\n  AgentHandler,\n  AgentMessage,\n  AgentRunOptions,\n} from \"@react-grab/relay\";\nimport { COMPLETED_STATUS } from \"@react-grab/relay\";\nimport { formatSpawnError } from \"@react-grab/utils/server\";\n\nexport interface ClaudeAgentOptions\n  extends AgentRunOptions, Omit<Options, \"cwd\"> {}\n\ntype ContentBlock = SDKAssistantMessage[\"message\"][\"content\"][number];\ntype TextContentBlock = Extract<ContentBlock, { type: \"text\" }>;\n\nconst claudeSessionMap = new Map<string, string>();\nconst abortedSessions = new Set<string>();\nlet lastClaudeSessionId: string | undefined;\n\nconst resolveClaudePath = (): string => {\n  const command =\n    process.platform === \"win32\" ? \"where claude\" : \"which claude\";\n  try {\n    const result = execSync(command, { encoding: \"utf8\" }).trim();\n    return result.split(\"\\n\")[0];\n  } catch {\n    return \"claude\";\n  }\n};\n\nconst isTextBlock = (block: ContentBlock): block is TextContentBlock =>\n  block.type === \"text\";\n\nconst runClaudeAgent = async function* (\n  prompt: string,\n  options?: ClaudeAgentOptions,\n): AsyncGenerator<AgentMessage> {\n  const sessionId = options?.sessionId;\n  const isAborted = () => {\n    if (options?.signal?.aborted) return true;\n    if (sessionId && abortedSessions.has(sessionId)) return true;\n    return false;\n  };\n\n  try {\n    yield { type: \"status\", content: \"Thinking…\" };\n\n    // HACK: https://github.com/anthropics/claude-code/issues/4619#issuecomment-3217014571\n    const env = { ...process.env };\n    delete env.NODE_OPTIONS;\n    delete env.VSCODE_INSPECTOR_OPTIONS;\n\n    const claudeSessionId = sessionId\n      ? claudeSessionMap.get(sessionId)\n      : undefined;\n\n    const queryResult = query({\n      prompt,\n      options: {\n        pathToClaudeCodeExecutable: resolveClaudePath(),\n        includePartialMessages: true,\n        permissionMode: \"bypassPermissions\",\n        env,\n        ...options,\n        cwd: options?.cwd ?? process.env.REACT_GRAB_CWD ?? process.cwd(),\n        ...(claudeSessionId ? { resume: claudeSessionId } : {}),\n      },\n    });\n\n    let capturedClaudeSessionId: string | undefined;\n\n    for await (const message of queryResult) {\n      if (isAborted()) break;\n\n      if (!capturedClaudeSessionId && message.session_id) {\n        capturedClaudeSessionId = message.session_id;\n      }\n\n      if (message.type === \"assistant\") {\n        const textContent = message.message.content\n          .filter(isTextBlock)\n          .map((block: TextContentBlock) => block.text)\n          .join(\" \");\n\n        if (textContent) {\n          yield { type: \"status\", content: textContent };\n        }\n      }\n\n      if (message.type === \"result\") {\n        yield {\n          type: \"status\",\n          content:\n            message.subtype === \"success\" ? COMPLETED_STATUS : \"Task finished\",\n        };\n      }\n    }\n\n    if (!isAborted() && capturedClaudeSessionId) {\n      if (sessionId) {\n        claudeSessionMap.set(sessionId, capturedClaudeSessionId);\n      }\n      lastClaudeSessionId = capturedClaudeSessionId;\n    }\n\n    if (!isAborted()) {\n      yield { type: \"done\", content: \"\" };\n    }\n  } catch (error) {\n    if (!isAborted()) {\n      const errorMessage =\n        error instanceof Error\n          ? formatSpawnError(error, \"claude\")\n          : \"Unknown error\";\n      const stderr =\n        error instanceof Error && \"stderr\" in error\n          ? String(error.stderr)\n          : undefined;\n      const fullError =\n        stderr && stderr.trim()\n          ? `${errorMessage}\\n\\nstderr:\\n${stderr.trim()}`\n          : errorMessage;\n      yield { type: \"error\", content: fullError };\n      yield { type: \"done\", content: \"\" };\n    }\n  } finally {\n    if (sessionId) {\n      abortedSessions.delete(sessionId);\n    }\n  }\n};\n\nconst abortClaudeAgent = (sessionId: string) => {\n  abortedSessions.add(sessionId);\n};\n\nconst undoClaudeAgent = async (): Promise<void> => {\n  if (!lastClaudeSessionId) {\n    return;\n  }\n\n  try {\n    const env = { ...process.env };\n    delete env.NODE_OPTIONS;\n    delete env.VSCODE_INSPECTOR_OPTIONS;\n\n    const queryResult = query({\n      prompt: \"undo\",\n      options: {\n        pathToClaudeCodeExecutable: resolveClaudePath(),\n        env,\n        cwd: process.env.REACT_GRAB_CWD ?? process.cwd(),\n        resume: lastClaudeSessionId,\n      },\n    });\n\n    // HACK: consume all messages to complete the undo\n    for await (const _message of queryResult) {\n    }\n  } catch (error) {\n    const errorMessage =\n      error instanceof Error\n        ? formatSpawnError(error, \"claude\")\n        : \"Unknown error\";\n    const stderr =\n      error instanceof Error && \"stderr\" in error\n        ? String(error.stderr)\n        : undefined;\n    const fullError =\n      stderr && stderr.trim()\n        ? `${errorMessage}\\n\\nstderr:\\n${stderr.trim()}`\n        : errorMessage;\n    throw new Error(`Undo failed: ${fullError}`);\n  }\n};\n\nexport const claudeAgentHandler: AgentHandler = {\n  agentId: \"claude-code\",\n  run: runClaudeAgent,\n  abort: abortClaudeAgent,\n  undo: undoClaudeAgent,\n};\n"
  },
  {
    "path": "packages/provider-claude-code/src/server.ts",
    "content": "import { startProviderServer } from \"@react-grab/relay\";\nimport { claudeAgentHandler } from \"./handler.js\";\n\nexport const startServer = () => {\n  startProviderServer(\"claude-code\", claudeAgentHandler);\n};\n"
  },
  {
    "path": "packages/provider-claude-code/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"noEmit\": true,\n    \"types\": [\"node\"]\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/provider-claude-code/tsup.config.ts",
    "content": "import fs from \"node:fs\";\nimport module from \"node:module\";\nimport { defineConfig } from \"tsup\";\n\nconst packageJson = JSON.parse(fs.readFileSync(\"package.json\", \"utf8\")) as {\n  version: string;\n};\n\nexport default defineConfig([\n  {\n    entry: {\n      handler: \"./src/handler.ts\",\n      cli: \"./src/cli.ts\",\n      server: \"./src/server.ts\",\n    },\n    format: [\"cjs\", \"esm\"],\n    dts: true,\n    clean: false,\n    splitting: false,\n    sourcemap: false,\n    target: \"node18\",\n    platform: \"node\",\n    treeshake: true,\n    noExternal: [/.*/],\n    external: [\n      ...module.builtinModules,\n      ...module.builtinModules.map((name) => `node:${name}`),\n    ],\n    env: {\n      VERSION: process.env.VERSION ?? packageJson.version,\n    },\n  },\n  {\n    entry: {\n      client: \"./src/client.ts\",\n    },\n    format: [\"cjs\", \"esm\"],\n    dts: true,\n    clean: false,\n    splitting: false,\n    sourcemap: false,\n    target: \"esnext\",\n    platform: \"browser\",\n    treeshake: true,\n    noExternal: [\"@react-grab/relay\"],\n  },\n  {\n    entry: [\"./src/client.ts\"],\n    format: [\"iife\"],\n    globalName: \"ReactGrabClaudeCode\",\n    outExtension: () => ({ js: \".global.js\" }),\n    dts: false,\n    clean: false,\n    minify: process.env.NODE_ENV === \"production\",\n    splitting: false,\n    sourcemap: false,\n    target: \"esnext\",\n    platform: \"browser\",\n    treeshake: true,\n    noExternal: [/.*/],\n  },\n]);\n"
  },
  {
    "path": "packages/provider-codex/CHANGELOG.md",
    "content": "# @react-grab/codex\n\n## 0.1.28\n\n### Patch Changes\n\n- fix\n- Updated dependencies\n  - react-grab@0.1.28\n  - @react-grab/relay@0.1.28\n\n## 0.1.27\n\n### Patch Changes\n\n- fix: install instructions\n- Updated dependencies\n  - react-grab@0.1.27\n  - @react-grab/relay@0.1.27\n\n## 0.1.26\n\n### Patch Changes\n\n- fix: minor tweaks\n- Updated dependencies\n  - react-grab@0.1.26\n  - @react-grab/relay@0.1.26\n\n## 0.1.25\n\n### Patch Changes\n\n- fix: primtiives\n- Updated dependencies\n  - react-grab@0.1.25\n  - @react-grab/relay@0.1.25\n\n## 0.1.24\n\n### Patch Changes\n\n- primitives\n- Updated dependencies\n  - react-grab@0.1.24\n  - @react-grab/relay@0.1.24\n\n## 0.1.23\n\n### Patch Changes\n\n- fix: npx command\n- Updated dependencies\n  - react-grab@0.1.23\n  - @react-grab/relay@0.1.23\n\n## 0.1.22\n\n### Patch Changes\n\n- fix: freezing\n- Updated dependencies\n  - react-grab@0.1.22\n  - @react-grab/relay@0.1.22\n\n## 0.1.21\n\n### Patch Changes\n\n- fix: up and down selection\n- Updated dependencies\n  - react-grab@0.1.21\n  - @react-grab/relay@0.1.21\n\n## 0.1.20\n\n### Patch Changes\n\n- fix: selection performacne\n- Updated dependencies\n  - react-grab@0.1.20\n  - @react-grab/relay@0.1.20\n\n## 0.1.19\n\n### Patch Changes\n\n- fix: gsap\n- Updated dependencies\n  - react-grab@0.1.19\n  - @react-grab/relay@0.1.19\n\n## 0.1.18\n\n### Patch Changes\n\n- fix: minor issues\n- Updated dependencies\n  - react-grab@0.1.18\n  - @react-grab/relay@0.1.18\n\n## 0.1.17\n\n### Patch Changes\n\n- fix: mcp\n- Updated dependencies\n  - react-grab@0.1.17\n  - @react-grab/relay@0.1.17\n\n## 0.1.16\n\n### Patch Changes\n\n- fix: environment detection\n- Updated dependencies\n  - react-grab@0.1.16\n  - @react-grab/relay@0.1.16\n\n## 0.1.15\n\n### Patch Changes\n\n- fix: animations and ux\n- Updated dependencies\n  - react-grab@0.1.15\n  - @react-grab/relay@0.1.15\n\n## 0.1.14\n\n### Patch Changes\n\n- fix: improve recent UX\n- Updated dependencies\n  - react-grab@0.1.14\n  - @react-grab/relay@0.1.14\n\n## 0.1.13\n\n### Patch Changes\n\n- fix MCP client injection\n- Updated dependencies\n  - react-grab@0.1.13\n  - @react-grab/relay@0.1.13\n\n## 0.1.12\n\n### Patch Changes\n\n- feat: MCP\n- Updated dependencies\n  - react-grab@0.1.12\n  - @react-grab/relay@0.1.12\n\n## 0.1.11\n\n### Patch Changes\n\n- fix: claude code provider\n- Updated dependencies\n  - react-grab@0.1.11\n  - @react-grab/relay@0.1.11\n\n## 0.1.10\n\n### Patch Changes\n\n- feat: cdn in cli\n- Updated dependencies\n  - react-grab@0.1.10\n  - @react-grab/relay@0.1.10\n\n## 0.1.9\n\n### Patch Changes\n\n- fix: startServer not exported in providers\n- Updated dependencies\n  - react-grab@0.1.9\n  - @react-grab/relay@0.1.9\n\n## 0.1.8\n\n### Patch Changes\n\n- fix: providers not detaching\n- Updated dependencies\n  - react-grab@0.1.8\n  - @react-grab/relay@0.1.8\n\n## 0.1.7\n\n### Patch Changes\n\n- fix: react freezing safety\n- Updated dependencies\n  - react-grab@0.1.7\n  - @react-grab/relay@0.1.7\n\n## 0.1.6\n\n### Patch Changes\n\n- fix: compat with React Scan\n- Updated dependencies\n  - react-grab@0.1.6\n  - @react-grab/relay@0.1.6\n\n## 0.1.5\n\n### Patch Changes\n\n- fix: fullscreen inset the root\n- Updated dependencies\n  - react-grab@0.1.5\n  - @react-grab/relay@0.1.5\n\n## 0.1.4\n\n### Patch Changes\n\n- fix: improve cli edge cases\n- Updated dependencies\n  - react-grab@0.1.4\n  - @react-grab/relay@0.1.4\n\n## 0.1.3\n\n### Patch Changes\n\n- fix: @react-grab/utils not publishing\n- Updated dependencies\n  - react-grab@0.1.3\n  - @react-grab/relay@0.1.3\n\n## 0.1.2\n\n### Patch Changes\n\n- fix: packages not being published\n- Updated dependencies\n  - react-grab@0.1.2\n  - @react-grab/relay@0.1.2\n\n## 0.1.1\n\n### Patch Changes\n\n- fix: clicking element after keyboard navigation\n- Updated dependencies\n  - react-grab@0.1.1\n  - @react-grab/relay@0.1.1\n\n## 0.1.0\n\n### Minor Changes\n\n- 81adb50: feat: browser\n\n### Patch Changes\n\n- 81adb50: fix: shell script\n- fb2b037: fix: cli\n- a3d5a94: fix: cli global install\n- 81adb50: feat: react support\n- 81adb50: fix: a11y\n- a5e7a6a: fix: optimize loading speed of cli\n- 90af3f6: fix: CLI hanging\n- 81adb50: fix: shell script\n- 78efee2: fix: cli\n- 074e593: fix: cli\n- 5cd3709: fix: decouple browser out from react-grab\n- 54c4867: ui improvements\n- Updated dependencies [81adb50]\n- Updated dependencies [fb2b037]\n- Updated dependencies [a3d5a94]\n- Updated dependencies [81adb50]\n- Updated dependencies [81adb50]\n- Updated dependencies [616d3e8]\n- Updated dependencies [81adb50]\n- Updated dependencies [a5e7a6a]\n- Updated dependencies [90af3f6]\n- Updated dependencies [81adb50]\n- Updated dependencies [81adb50]\n- Updated dependencies [78efee2]\n- Updated dependencies [074e593]\n- Updated dependencies [5cd3709]\n- Updated dependencies [54c4867]\n  - react-grab@0.1.0\n  - @react-grab/relay@0.1.0\n\n## 0.1.0-beta.13\n\n### Patch Changes\n\n- ui improvements\n- Updated dependencies [616d3e8]\n- Updated dependencies\n  - react-grab@0.1.0-beta.13\n  - @react-grab/relay@0.1.0-beta.13\n\n## 0.1.0-beta.12\n\n### Patch Changes\n\n- fix: decouple browser out from react-grab\n- Updated dependencies\n  - react-grab@0.1.0-beta.12\n  - @react-grab/relay@0.1.0-beta.12\n\n## 0.1.0-beta.11\n\n### Patch Changes\n\n- fix: cli global install\n- Updated dependencies\n  - react-grab@0.1.0-beta.11\n  - @react-grab/relay@0.1.0-beta.11\n\n## 0.1.0-beta.10\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.10\n  - @react-grab/relay@0.1.0-beta.10\n\n## 0.1.0-beta.9\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.9\n  - @react-grab/relay@0.1.0-beta.9\n\n## 0.1.0-beta.8\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.8\n  - @react-grab/relay@0.1.0-beta.8\n\n## 0.1.0-beta.7\n\n### Patch Changes\n\n- fix: CLI hanging\n- Updated dependencies\n  - react-grab@0.1.0-beta.7\n  - @react-grab/relay@0.1.0-beta.7\n\n## 0.1.0-beta.6\n\n### Patch Changes\n\n- fix: optimize loading speed of cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.6\n  - @react-grab/relay@0.1.0-beta.6\n\n## 0.1.0-beta.5\n\n### Patch Changes\n\n- fix: a11y\n- Updated dependencies\n  - react-grab@0.1.0-beta.5\n  - @react-grab/relay@0.1.0-beta.5\n\n## 0.1.0-beta.4\n\n### Patch Changes\n\n- feat: react support\n- Updated dependencies\n  - react-grab@0.1.0-beta.4\n  - @react-grab/relay@0.1.0-beta.4\n\n## 0.1.0-beta.3\n\n### Patch Changes\n\n- Updated dependencies\n  - react-grab@0.1.0-beta.3\n\n## 0.1.0-beta.2\n\n### Patch Changes\n\n- fix: shell script\n- Updated dependencies\n  - react-grab@0.1.0-beta.2\n  - @react-grab/relay@0.1.0-beta.2\n\n## 0.1.0-beta.1\n\n### Patch Changes\n\n- fix: shell script\n- Updated dependencies\n  - react-grab@0.1.0-beta.1\n  - @react-grab/relay@0.1.0-beta.1\n\n## 0.1.0-beta.0\n\n### Minor Changes\n\n- feat: browser\n\n### Patch Changes\n\n- Updated dependencies\n  - react-grab@0.1.0-beta.0\n  - @react-grab/relay@0.1.0-beta.0\n\n## 0.0.98\n\n### Patch Changes\n\n- feat: new state architecture and context menu\n- Updated dependencies\n  - react-grab@0.0.98\n\n## 0.0.97\n\n### Patch Changes\n\n- fix: sourcemap error\n- Updated dependencies\n  - react-grab@0.0.97\n\n## 0.0.96\n\n### Patch Changes\n\n- fix: fiber access timeout handling\n- Updated dependencies\n  - react-grab@0.0.96\n\n## 0.0.95\n\n### Patch Changes\n\n- fix: selecting buttons with disabled states\n- Updated dependencies\n  - react-grab@0.0.95\n\n## 0.0.94\n\n### Patch Changes\n\n- fix: browser crashing on selection bug\n- Updated dependencies\n  - react-grab@0.0.94\n\n## 0.0.93\n\n### Patch Changes\n\n- fix: copying not working\n- Updated dependencies\n  - react-grab@0.0.93\n\n## 0.0.92\n\n### Patch Changes\n\n- refactor: use state machines instead of signals\n- Updated dependencies\n  - react-grab@0.0.92\n\n## 0.0.91\n\n### Patch Changes\n\n- feat: dock\n- Updated dependencies\n  - react-grab@0.0.91\n\n## 0.0.90\n\n### Patch Changes\n\n- fix: check visual edit endpoint\n- Updated dependencies\n  - react-grab@0.0.90\n\n## 0.0.89\n\n### Patch Changes\n\n- fix: many bugfixes\n- Updated dependencies\n  - react-grab@0.0.89\n\n## 0.0.88\n\n### Patch Changes\n\n- fix: deprecation errors\n- Updated dependencies\n  - react-grab@0.0.88\n\n## 0.0.87\n\n### Patch Changes\n\n- feat: visual edits\n- Updated dependencies\n  - react-grab@0.0.87\n\n## 0.0.86\n\n### Patch Changes\n\n- fix: editing\n- Updated dependencies\n  - react-grab@0.0.86\n\n## 0.0.85\n\n### Patch Changes\n\n- fix: check versions on each provider\n- Updated dependencies\n  - react-grab@0.0.85\n\n## 0.0.84\n\n### Patch Changes\n\n- fix: migrate from cross-spawn to execa to fix deprecation issue\n- Updated dependencies\n  - react-grab@0.0.84\n\n## 0.0.83\n\n### Patch Changes\n\n- feat: timings during agent processing\n- Updated dependencies\n  - react-grab@0.0.83\n\n## 0.0.82\n\n### Patch Changes\n\n- fix: agent support\n- Updated dependencies\n  - react-grab@0.0.82\n\n## 0.0.81\n\n### Patch Changes\n\n- 3b47e87: feat: add OpenAI Codex SDK provider with undo and follow-up support\n- feat: codex and gemini support\n- Updated dependencies\n  - react-grab@0.0.81\n\n## 0.0.80\n\n### Patch Changes\n\n- feat: add OpenAI Codex SDK provider with undo and follow-up support\n- Updated dependencies\n  - react-grab@0.0.80\n"
  },
  {
    "path": "packages/provider-codex/README.md",
    "content": "# @react-grab/codex\n\nOpenAI Codex provider for React Grab.\n\n## Installation\n\n```bash\nnpm install @react-grab/codex\n```\n\n## Usage\n\n### Server\n\nThe server runs on port `7567` and interfaces with the Codex SDK. Add to your `package.json`:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"npx @react-grab/codex@latest && next dev\"\n  }\n}\n```\n\n> **Note:** You must have [Codex](https://github.com/openai/codex) installed (`npm i -g @openai/codex`).\n\n### Client\n\nAdd the client script to your HTML:\n\n```html\n<script src=\"//unpkg.com/@react-grab/codex/dist/client.global.js\"></script>\n```\n\nOr import programmatically:\n\n```ts\nimport \"@react-grab/codex/client\";\n```\n\n## Features\n\n- **Follow-ups**: Continue conversations with the same thread\n- **Undo**: Undo the last change made by Codex\n- **Streaming**: Real-time status updates during execution\n"
  },
  {
    "path": "packages/provider-codex/package.json",
    "content": "{\n  \"name\": \"@react-grab/codex\",\n  \"version\": \"0.1.28\",\n  \"bin\": {\n    \"react-grab-codex\": \"./dist/cli.cjs\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"type\": \"module\",\n  \"browser\": \"dist/client.global.js\",\n  \"exports\": {\n    \"./client\": {\n      \"types\": \"./dist/client.d.ts\",\n      \"import\": \"./dist/client.js\",\n      \"require\": \"./dist/client.cjs\"\n    },\n    \"./handler\": {\n      \"types\": \"./dist/handler.d.ts\",\n      \"import\": \"./dist/handler.js\",\n      \"require\": \"./dist/handler.cjs\"\n    },\n    \"./server\": {\n      \"types\": \"./dist/server.d.ts\",\n      \"import\": \"./dist/server.js\",\n      \"require\": \"./dist/server.cjs\"\n    },\n    \"./dist/*\": \"./dist/*.js\",\n    \"./dist/*.js\": \"./dist/*.js\"\n  },\n  \"scripts\": {\n    \"dev\": \"tsup --watch\",\n    \"build\": \"rm -rf dist && NODE_ENV=production tsup\"\n  },\n  \"dependencies\": {\n    \"@openai/codex-sdk\": \"^0.66.0\",\n    \"@react-grab/relay\": \"workspace:*\",\n    \"react-grab\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@react-grab/utils\": \"workspace:*\",\n    \"@types/node\": \"^22.10.7\",\n    \"tsup\": \"^8.4.0\"\n  }\n}\n"
  },
  {
    "path": "packages/provider-codex/src/cli.ts",
    "content": "#!/usr/bin/env node\nimport { spawn } from \"node:child_process\";\nimport { realpathSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\nconst realScriptPath = realpathSync(process.argv[1]);\nconst scriptDir = dirname(realScriptPath);\nconst serverPath = join(scriptDir, \"server.cjs\");\n\nconst child = spawn(\n  process.execPath,\n  [\"-e\", `require(${JSON.stringify(serverPath)}).startServer()`],\n  {\n    detached: true,\n    stdio: \"inherit\",\n  },\n);\n\nchild.unref();\nprocess.exit(0);\n"
  },
  {
    "path": "packages/provider-codex/src/client.ts",
    "content": "import type { AgentCompleteResult } from \"react-grab/core\";\nimport { createProviderClientPlugin } from \"@react-grab/relay/client\";\n\nexport type { AgentCompleteResult };\n\nconst { createAgentProvider: createCodexAgentProvider, attachAgent } =\n  createProviderClientPlugin({\n    agentId: \"codex\",\n    pluginName: \"codex-agent\",\n    actionId: \"edit-with-codex\",\n    actionLabel: \"Edit with Codex\",\n  });\n\nexport { createCodexAgentProvider, attachAgent };\n\nattachAgent();\n"
  },
  {
    "path": "packages/provider-codex/src/handler.ts",
    "content": "import { Codex } from \"@openai/codex-sdk\";\nimport type {\n  AgentHandler,\n  AgentMessage,\n  AgentRunOptions,\n} from \"@react-grab/relay\";\nimport { COMPLETED_STATUS } from \"@react-grab/relay\";\n\nexport interface CodexAgentOptions extends AgentRunOptions {\n  model?: string;\n  workingDirectory?: string;\n}\n\ntype CodexThread = ReturnType<Codex[\"startThread\"]>;\n\ninterface ThreadState {\n  thread: CodexThread;\n  threadId: string;\n}\n\ninterface CodexEventItem {\n  type: string;\n  text?: string;\n  command?: string;\n}\n\ninterface CodexEvent {\n  type: string;\n  item?: CodexEventItem;\n}\n\nlet codexInstance: Codex | null = null;\nconst threadMap = new Map<string, ThreadState>();\nconst abortControllers = new Map<string, AbortController>();\nlet lastSessionId: string | undefined;\n\nconst getCodexInstance = (): Codex => {\n  if (!codexInstance) {\n    codexInstance = new Codex();\n  }\n  return codexInstance;\n};\n\nconst getOrCreateThread = (\n  sessionId: string | undefined,\n  options?: CodexAgentOptions,\n): { thread: CodexThread; isExisting: boolean } => {\n  const codex = getCodexInstance();\n\n  if (sessionId && threadMap.has(sessionId)) {\n    return { thread: threadMap.get(sessionId)!.thread, isExisting: true };\n  }\n\n  const thread = codex.startThread({\n    workingDirectory:\n      options?.workingDirectory ?? process.env.REACT_GRAB_CWD ?? process.cwd(),\n  });\n\n  return { thread, isExisting: false };\n};\n\nconst formatStreamEvent = (event: CodexEvent): string | undefined => {\n  switch (event.type) {\n    case \"item.completed\":\n      if (event.item?.type === \"agent_message\" && event.item.text) {\n        return event.item.text;\n      }\n      if (event.item?.type === \"command_execution\" && event.item.command) {\n        return `Executed: ${event.item.command}`;\n      }\n      return undefined;\n    case \"turn.completed\":\n      return undefined;\n    default:\n      return undefined;\n  }\n};\n\nconst runCodexAgent = async function* (\n  prompt: string,\n  options?: CodexAgentOptions,\n): AsyncGenerator<AgentMessage> {\n  const sessionId = options?.sessionId;\n  const abortController = new AbortController();\n\n  if (sessionId) {\n    abortControllers.set(sessionId, abortController);\n  }\n\n  const isAborted = () => {\n    if (options?.signal?.aborted) return true;\n    if (abortController.signal.aborted) return true;\n    return false;\n  };\n\n  try {\n    yield { type: \"status\", content: \"Thinking…\" };\n\n    const { thread } = getOrCreateThread(sessionId, {\n      ...options,\n      workingDirectory: options?.workingDirectory ?? options?.cwd,\n    });\n\n    const result = await thread.runStreamed(prompt);\n\n    if (!result || !result.events) {\n      throw new Error(\n        \"Codex SDK returned an unexpected response: missing events stream\",\n      );\n    }\n\n    for await (const event of result.events) {\n      if (isAborted()) break;\n\n      const statusText = formatStreamEvent(event as CodexEvent);\n      if (statusText && !isAborted()) {\n        yield { type: \"status\", content: statusText };\n      }\n    }\n\n    if (sessionId && !isAborted() && thread.id) {\n      threadMap.set(sessionId, { thread, threadId: thread.id });\n      lastSessionId = sessionId;\n    }\n\n    if (!isAborted()) {\n      yield { type: \"status\", content: COMPLETED_STATUS };\n      yield { type: \"done\", content: \"\" };\n    }\n  } catch (error) {\n    if (!isAborted()) {\n      const errorMessage =\n        error instanceof Error ? error.message : \"Unknown error\";\n      yield { type: \"error\", content: errorMessage };\n      yield { type: \"done\", content: \"\" };\n    }\n  } finally {\n    if (sessionId) {\n      abortControllers.delete(sessionId);\n    }\n  }\n};\n\nconst abortCodexAgent = (sessionId: string) => {\n  const abortController = abortControllers.get(sessionId);\n  if (abortController) {\n    abortController.abort();\n    abortControllers.delete(sessionId);\n    threadMap.delete(sessionId);\n  }\n};\n\nconst undoCodexAgent = async (): Promise<void> => {\n  if (!lastSessionId) {\n    return;\n  }\n\n  const threadState = threadMap.get(lastSessionId);\n  if (!threadState) {\n    return;\n  }\n\n  const codex = getCodexInstance();\n  const thread = codex.resumeThread(threadState.threadId);\n  await thread.run(\"Please undo the last change you made.\");\n};\n\nexport const codexAgentHandler: AgentHandler = {\n  agentId: \"codex\",\n  run: runCodexAgent,\n  abort: abortCodexAgent,\n  undo: undoCodexAgent,\n};\n"
  },
  {
    "path": "packages/provider-codex/src/server.ts",
    "content": "import { startProviderServer } from \"@react-grab/relay\";\nimport { codexAgentHandler } from \"./handler.js\";\n\nexport const startServer = () => {\n  startProviderServer(\"codex\", codexAgentHandler);\n};\n"
  },
  {
    "path": "packages/provider-codex/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"esModuleInterop\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"declaration\": true,\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\",\n    \"types\": [\"node\"]\n  },\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/provider-codex/tsup.config.ts",
    "content": "import fs from \"node:fs\";\nimport module from \"node:module\";\nimport { defineConfig } from \"tsup\";\n\nconst packageJson = JSON.parse(fs.readFileSync(\"package.json\", \"utf8\")) as {\n  version: string;\n};\n\nexport default defineConfig([\n  {\n    entry: {\n      handler: \"./src/handler.ts\",\n      cli: \"./src/cli.ts\",\n      server: \"./src/server.ts\",\n    },\n    format: [\"cjs\", \"esm\"],\n    dts: true,\n    clean: false,\n    splitting: false,\n    sourcemap: false,\n    target: \"node18\",\n    platform: \"node\",\n    treeshake: true,\n    noExternal: [/.*/],\n    external: [\n      ...module.builtinModules,\n      ...module.builtinModules.map((name) => `node:${name}`),\n    ],\n    env: {\n      VERSION: process.env.VERSION ?? packageJson.version,\n    },\n  },\n  {\n    entry: {\n      client: \"./src/client.ts\",\n    },\n    format: [\"cjs\", \"esm\"],\n    dts: true,\n    clean: false,\n    splitting: false,\n    sourcemap: false,\n    target: \"esnext\",\n    platform: \"browser\",\n    treeshake: true,\n    noExternal: [\"@react-grab/relay\"],\n  },\n  {\n    entry: [\"./src/client.ts\"],\n    format: [\"iife\"],\n    globalName: \"ReactGrabCodex\",\n    outExtension: () => ({ js: \".global.js\" }),\n    dts: false,\n    clean: false,\n    minify: process.env.NODE_ENV === \"production\",\n    splitting: false,\n    sourcemap: false,\n    target: \"esnext\",\n    platform: \"browser\",\n    treeshake: true,\n    noExternal: [/.*/],\n  },\n]);\n"
  },
  {
    "path": "packages/provider-copilot/CHANGELOG.md",
    "content": "# @react-grab/copilot\n\n## 0.1.28\n\n### Patch Changes\n\n- fix\n- Updated dependencies\n  - react-grab@0.1.28\n  - @react-grab/relay@0.1.28\n\n## 0.1.27\n\n### Patch Changes\n\n- fix: install instructions\n- Updated dependencies\n  - react-grab@0.1.27\n  - @react-grab/relay@0.1.27\n\n## 0.1.26\n\n### Patch Changes\n\n- fix: minor tweaks\n- Updated dependencies\n  - react-grab@0.1.26\n  - @react-grab/relay@0.1.26\n\n## 0.1.25\n\n### Patch Changes\n\n- fix: primtiives\n- Updated dependencies\n  - react-grab@0.1.25\n  - @react-grab/relay@0.1.25\n\n## 0.1.24\n\n### Patch Changes\n\n- primitives\n- Updated dependencies\n  - react-grab@0.1.24\n  - @react-grab/relay@0.1.24\n\n## 0.1.23\n\n### Patch Changes\n\n- fix: npx command\n- Updated dependencies\n  - react-grab@0.1.23\n  - @react-grab/relay@0.1.23\n\n## 0.1.22\n\n### Patch Changes\n\n- fix: freezing\n- Updated dependencies\n  - react-grab@0.1.22\n  - @react-grab/relay@0.1.22\n\n## 0.1.21\n\n### Patch Changes\n\n- fix: up and down selection\n- Updated dependencies\n  - react-grab@0.1.21\n  - @react-grab/relay@0.1.21\n\n## 0.1.20\n\n### Patch Changes\n\n- fix: selection performacne\n- Updated dependencies\n  - react-grab@0.1.20\n  - @react-grab/relay@0.1.20\n\n## 0.1.19\n\n### Patch Changes\n\n- fix: gsap\n- Updated dependencies\n  - react-grab@0.1.19\n  - @react-grab/relay@0.1.19\n\n## 0.1.18\n\n### Patch Changes\n\n- fix: minor issues\n- Updated dependencies\n  - react-grab@0.1.18\n  - @react-grab/relay@0.1.18\n\n## 0.1.17\n\n### Patch Changes\n\n- fix: mcp\n- Updated dependencies\n  - react-grab@0.1.17\n  - @react-grab/relay@0.1.17\n\n## 0.1.16\n\n### Patch Changes\n\n- fix: environment detection\n- Updated dependencies\n  - react-grab@0.1.16\n  - @react-grab/relay@0.1.16\n"
  },
  {
    "path": "packages/provider-copilot/package.json",
    "content": "{\n  \"name\": \"@react-grab/copilot\",\n  \"version\": \"0.1.28\",\n  \"bin\": {\n    \"react-grab-copilot\": \"./dist/cli.cjs\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"type\": \"module\",\n  \"browser\": \"dist/client.global.js\",\n  \"exports\": {\n    \"./client\": {\n      \"types\": \"./dist/client.d.ts\",\n      \"import\": \"./dist/client.js\",\n      \"require\": \"./dist/client.cjs\"\n    },\n    \"./handler\": {\n      \"types\": \"./dist/handler.d.ts\",\n      \"import\": \"./dist/handler.js\",\n      \"require\": \"./dist/handler.cjs\"\n    },\n    \"./server\": {\n      \"types\": \"./dist/server.d.ts\",\n      \"import\": \"./dist/server.js\",\n      \"require\": \"./dist/server.cjs\"\n    },\n    \"./dist/*\": \"./dist/*.js\",\n    \"./dist/*.js\": \"./dist/*.js\"\n  },\n  \"scripts\": {\n    \"dev\": \"tsup --watch\",\n    \"build\": \"rm -rf dist && NODE_ENV=production tsup\"\n  },\n  \"dependencies\": {\n    \"@react-grab/relay\": \"workspace:*\",\n    \"execa\": \"^9.6.0\",\n    \"react-grab\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@react-grab/utils\": \"workspace:*\",\n    \"@types/node\": \"^22.10.7\",\n    \"tsup\": \"^8.4.0\"\n  }\n}\n"
  },
  {
    "path": "packages/provider-copilot/src/cli.ts",
    "content": "#!/usr/bin/env node\nimport { spawn } from \"node:child_process\";\nimport { realpathSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\nconst realScriptPath = realpathSync(process.argv[1]);\nconst scriptDir = dirname(realScriptPath);\nconst serverPath = join(scriptDir, \"server.cjs\");\n\nconst child = spawn(\n  process.execPath,\n  [\"-e\", `require(${JSON.stringify(serverPath)}).startServer()`],\n  {\n    detached: true,\n    stdio: \"inherit\",\n  },\n);\n\nchild.unref();\nprocess.exit(0);\n"
  },
  {
    "path": "packages/provider-copilot/src/client.ts",
    "content": "import type { AgentCompleteResult } from \"react-grab/core\";\nimport { createProviderClientPlugin } from \"@react-grab/relay/client\";\n\nexport type { AgentCompleteResult };\n\nconst { createAgentProvider: createCopilotAgentProvider, attachAgent } =\n  createProviderClientPlugin({\n    agentId: \"copilot\",\n    pluginName: \"copilot-agent\",\n    actionId: \"edit-with-copilot\",\n    actionLabel: \"Edit with Copilot\",\n  });\n\nexport { createCopilotAgentProvider, attachAgent };\n\nattachAgent();\n"
  },
  {
    "path": "packages/provider-copilot/src/handler.ts",
    "content": "import { execa, type ResultPromise } from \"execa\";\nimport type {\n  AgentHandler,\n  AgentMessage,\n  AgentRunOptions,\n} from \"@react-grab/relay\";\nimport { COMPLETED_STATUS } from \"@react-grab/relay\";\nimport { formatSpawnError } from \"@react-grab/utils/server\";\n\nexport interface CopilotAgentOptions extends AgentRunOptions {\n  model?: string;\n}\n\nconst copilotSessionMap = new Map<string, string>();\nconst activeProcesses = new Map<string, ResultPromise>();\nlet lastCopilotSessionId: string | undefined;\n\nconst runCopilotAgent = async function* (\n  prompt: string,\n  options?: CopilotAgentOptions,\n): AsyncGenerator<AgentMessage> {\n  const sessionId = options?.sessionId;\n  const copilotArgs = [\"-p\", prompt, \"--silent\", \"--allow-all\", \"--no-color\"];\n\n  if (options?.model) {\n    copilotArgs.push(\"--model\", options.model);\n  }\n\n  const copilotSessionId = sessionId\n    ? copilotSessionMap.get(sessionId)\n    : undefined;\n\n  if (copilotSessionId) {\n    copilotArgs.push(\"--resume\", copilotSessionId);\n  }\n\n  const workspacePath =\n    options?.cwd ?? process.env.REACT_GRAB_CWD ?? process.cwd();\n\n  let copilotProcess: ResultPromise | undefined;\n  let stderrBuffer = \"\";\n\n  try {\n    yield { type: \"status\", content: \"Thinking…\" };\n\n    copilotProcess = execa(\"copilot\", copilotArgs, {\n      stdout: \"pipe\",\n      stderr: \"pipe\",\n      cwd: workspacePath,\n      env: { ...process.env },\n    });\n\n    if (sessionId) {\n      activeProcesses.set(sessionId, copilotProcess);\n    }\n\n    if (copilotProcess.stderr) {\n      copilotProcess.stderr.on(\"data\", (chunk: Buffer) => {\n        stderrBuffer += chunk.toString();\n      });\n    }\n\n    const messageQueue: AgentMessage[] = [];\n    let resolveWait: (() => void) | null = null;\n    let processEnded = false;\n\n    const enqueueMessage = (message: AgentMessage) => {\n      messageQueue.push(message);\n      if (resolveWait) {\n        resolveWait();\n        resolveWait = null;\n      }\n    };\n\n    if (copilotProcess.stdout) {\n      copilotProcess.stdout.on(\"data\", (chunk: Buffer) => {\n        const trimmedChunk = chunk.toString().trim();\n        if (trimmedChunk) {\n          enqueueMessage({ type: \"status\", content: trimmedChunk });\n        }\n      });\n    }\n\n    const childProcess = copilotProcess;\n    childProcess.on(\"close\", (code) => {\n      if (sessionId) {\n        activeProcesses.delete(sessionId);\n      }\n\n      if (sessionId && !childProcess.killed) {\n        copilotSessionMap.set(sessionId, sessionId);\n        lastCopilotSessionId = sessionId;\n      }\n\n      processEnded = true;\n\n      if (!childProcess.killed) {\n        if (code !== 0) {\n          const errorDetail =\n            stderrBuffer.trim() || `copilot exited with code ${code}`;\n          enqueueMessage({ type: \"error\", content: errorDetail });\n        } else {\n          enqueueMessage({ type: \"status\", content: COMPLETED_STATUS });\n        }\n      }\n\n      enqueueMessage({ type: \"done\", content: \"\" });\n      if (resolveWait) {\n        resolveWait();\n        resolveWait = null;\n      }\n    });\n\n    childProcess.on(\"error\", (error) => {\n      if (sessionId) {\n        activeProcesses.delete(sessionId);\n      }\n      processEnded = true;\n      const isNotInstalled = \"code\" in error && error.code === \"ENOENT\";\n      if (isNotInstalled) {\n        enqueueMessage({\n          type: \"error\",\n          content:\n            \"copilot CLI is not installed. Please install GitHub Copilot CLI to use this provider.\\n\\nInstallation: npm install -g @github/copilot-cli\",\n        });\n      } else {\n        const errorMessage = formatSpawnError(error, \"copilot\");\n        const stderrContent = stderrBuffer.trim();\n        const fullError = stderrContent\n          ? `${errorMessage}\\n\\nstderr:\\n${stderrContent}`\n          : errorMessage;\n        enqueueMessage({ type: \"error\", content: fullError });\n      }\n      enqueueMessage({ type: \"done\", content: \"\" });\n      if (resolveWait) {\n        resolveWait();\n        resolveWait = null;\n      }\n    });\n\n    while (true) {\n      if (options?.signal?.aborted) {\n        if (copilotProcess && !copilotProcess.killed) {\n          copilotProcess.kill(\"SIGTERM\");\n        }\n        return;\n      }\n\n      if (messageQueue.length > 0) {\n        const message = messageQueue.shift()!;\n        if (message.type === \"done\") {\n          yield message;\n          return;\n        }\n        yield message;\n      } else if (processEnded) {\n        return;\n      } else {\n        await new Promise<void>((resolve) => {\n          resolveWait = resolve;\n        });\n      }\n    }\n  } catch (error) {\n    const errorMessage =\n      error instanceof Error\n        ? formatSpawnError(error, \"copilot\")\n        : \"Unknown error\";\n    const stderrContent = stderrBuffer.trim();\n    const fullError = stderrContent\n      ? `${errorMessage}\\n\\nstderr:\\n${stderrContent}`\n      : errorMessage;\n    yield { type: \"error\", content: fullError };\n    yield { type: \"done\", content: \"\" };\n  }\n};\n\nconst abortCopilotAgent = (sessionId: string) => {\n  const activeProcess = activeProcesses.get(sessionId);\n  if (activeProcess && !activeProcess.killed) {\n    activeProcess.kill(\"SIGTERM\");\n  }\n  activeProcesses.delete(sessionId);\n};\n\nconst undoCopilotAgent = async (): Promise<void> => {\n  if (!lastCopilotSessionId) {\n    return;\n  }\n\n  try {\n    const workspacePath = process.env.REACT_GRAB_CWD ?? process.cwd();\n\n    await execa(\n      \"copilot\",\n      [\n        \"-p\",\n        \"undo the last change you made\",\n        \"--silent\",\n        \"--allow-all\",\n        \"--no-color\",\n        \"--resume\",\n        lastCopilotSessionId,\n      ],\n      {\n        stdout: \"pipe\",\n        stderr: \"pipe\",\n        cwd: workspacePath,\n        env: { ...process.env },\n      },\n    );\n  } catch (error) {\n    const errorMessage =\n      error instanceof Error\n        ? formatSpawnError(error, \"copilot\")\n        : \"Unknown error\";\n    throw new Error(`Undo failed: ${errorMessage}`);\n  }\n};\n\nexport const copilotAgentHandler: AgentHandler = {\n  agentId: \"copilot\",\n  run: runCopilotAgent,\n  abort: abortCopilotAgent,\n  undo: undoCopilotAgent,\n};\n"
  },
  {
    "path": "packages/provider-copilot/src/server.ts",
    "content": "import { startProviderServer } from \"@react-grab/relay\";\nimport { copilotAgentHandler } from \"./handler.js\";\n\nexport const startServer = () => {\n  startProviderServer(\"copilot\", copilotAgentHandler);\n};\n"
  },
  {
    "path": "packages/provider-copilot/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"noEmit\": true,\n    \"types\": [\"node\"]\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/provider-copilot/tsup.config.ts",
    "content": "import fs from \"node:fs\";\nimport module from \"node:module\";\nimport { defineConfig } from \"tsup\";\n\nconst packageJson = JSON.parse(fs.readFileSync(\"package.json\", \"utf8\")) as {\n  version: string;\n};\n\nexport default defineConfig([\n  {\n    entry: {\n      handler: \"./src/handler.ts\",\n      cli: \"./src/cli.ts\",\n      server: \"./src/server.ts\",\n    },\n    format: [\"cjs\", \"esm\"],\n    dts: true,\n    clean: false,\n    splitting: false,\n    sourcemap: false,\n    target: \"node18\",\n    platform: \"node\",\n    treeshake: true,\n    noExternal: [/.*/],\n    external: [\n      ...module.builtinModules,\n      ...module.builtinModules.map((name) => `node:${name}`),\n    ],\n    env: {\n      VERSION: process.env.VERSION ?? packageJson.version,\n    },\n  },\n  {\n    entry: {\n      client: \"./src/client.ts\",\n    },\n    format: [\"cjs\", \"esm\"],\n    dts: true,\n    clean: false,\n    splitting: false,\n    sourcemap: false,\n    target: \"esnext\",\n    platform: \"browser\",\n    treeshake: true,\n    noExternal: [\"@react-grab/relay\"],\n  },\n  {\n    entry: [\"./src/client.ts\"],\n    format: [\"iife\"],\n    globalName: \"ReactGrabCopilot\",\n    outExtension: () => ({ js: \".global.js\" }),\n    dts: false,\n    clean: false,\n    minify: process.env.NODE_ENV === \"production\",\n    splitting: false,\n    sourcemap: false,\n    target: \"esnext\",\n    platform: \"browser\",\n    treeshake: true,\n    noExternal: [/.*/],\n  },\n]);\n"
  },
  {
    "path": "packages/provider-cursor/CHANGELOG.md",
    "content": "# @react-grab/cursor\n\n## 0.1.28\n\n### Patch Changes\n\n- fix\n- Updated dependencies\n  - react-grab@0.1.28\n  - @react-grab/relay@0.1.28\n\n## 0.1.27\n\n### Patch Changes\n\n- fix: install instructions\n- Updated dependencies\n  - react-grab@0.1.27\n  - @react-grab/relay@0.1.27\n\n## 0.1.26\n\n### Patch Changes\n\n- fix: minor tweaks\n- Updated dependencies\n  - react-grab@0.1.26\n  - @react-grab/relay@0.1.26\n\n## 0.1.25\n\n### Patch Changes\n\n- fix: primtiives\n- Updated dependencies\n  - react-grab@0.1.25\n  - @react-grab/relay@0.1.25\n\n## 0.1.24\n\n### Patch Changes\n\n- primitives\n- Updated dependencies\n  - react-grab@0.1.24\n  - @react-grab/relay@0.1.24\n\n## 0.1.23\n\n### Patch Changes\n\n- fix: npx command\n- Updated dependencies\n  - react-grab@0.1.23\n  - @react-grab/relay@0.1.23\n\n## 0.1.22\n\n### Patch Changes\n\n- fix: freezing\n- Updated dependencies\n  - react-grab@0.1.22\n  - @react-grab/relay@0.1.22\n\n## 0.1.21\n\n### Patch Changes\n\n- fix: up and down selection\n- Updated dependencies\n  - react-grab@0.1.21\n  - @react-grab/relay@0.1.21\n\n## 0.1.20\n\n### Patch Changes\n\n- fix: selection performacne\n- Updated dependencies\n  - react-grab@0.1.20\n  - @react-grab/relay@0.1.20\n\n## 0.1.19\n\n### Patch Changes\n\n- fix: gsap\n- Updated dependencies\n  - react-grab@0.1.19\n  - @react-grab/relay@0.1.19\n\n## 0.1.18\n\n### Patch Changes\n\n- fix: minor issues\n- Updated dependencies\n  - react-grab@0.1.18\n  - @react-grab/relay@0.1.18\n\n## 0.1.17\n\n### Patch Changes\n\n- fix: mcp\n- Updated dependencies\n  - react-grab@0.1.17\n  - @react-grab/relay@0.1.17\n\n## 0.1.16\n\n### Patch Changes\n\n- fix: environment detection\n- Updated dependencies\n  - react-grab@0.1.16\n  - @react-grab/relay@0.1.16\n\n## 0.1.15\n\n### Patch Changes\n\n- fix: animations and ux\n- Updated dependencies\n  - react-grab@0.1.15\n  - @react-grab/relay@0.1.15\n\n## 0.1.14\n\n### Patch Changes\n\n- fix: improve recent UX\n- Updated dependencies\n  - react-grab@0.1.14\n  - @react-grab/relay@0.1.14\n\n## 0.1.13\n\n### Patch Changes\n\n- fix MCP client injection\n- Updated dependencies\n  - react-grab@0.1.13\n  - @react-grab/relay@0.1.13\n\n## 0.1.12\n\n### Patch Changes\n\n- feat: MCP\n- Updated dependencies\n  - react-grab@0.1.12\n  - @react-grab/relay@0.1.12\n\n## 0.1.11\n\n### Patch Changes\n\n- fix: claude code provider\n- Updated dependencies\n  - react-grab@0.1.11\n  - @react-grab/relay@0.1.11\n\n## 0.1.10\n\n### Patch Changes\n\n- feat: cdn in cli\n- Updated dependencies\n  - react-grab@0.1.10\n  - @react-grab/relay@0.1.10\n\n## 0.1.9\n\n### Patch Changes\n\n- fix: startServer not exported in providers\n- Updated dependencies\n  - react-grab@0.1.9\n  - @react-grab/relay@0.1.9\n\n## 0.1.8\n\n### Patch Changes\n\n- fix: providers not detaching\n- Updated dependencies\n  - react-grab@0.1.8\n  - @react-grab/relay@0.1.8\n\n## 0.1.7\n\n### Patch Changes\n\n- fix: react freezing safety\n- Updated dependencies\n  - react-grab@0.1.7\n  - @react-grab/relay@0.1.7\n\n## 0.1.6\n\n### Patch Changes\n\n- fix: compat with React Scan\n- Updated dependencies\n  - react-grab@0.1.6\n  - @react-grab/relay@0.1.6\n\n## 0.1.5\n\n### Patch Changes\n\n- fix: fullscreen inset the root\n- Updated dependencies\n  - react-grab@0.1.5\n  - @react-grab/relay@0.1.5\n\n## 0.1.4\n\n### Patch Changes\n\n- fix: improve cli edge cases\n- Updated dependencies\n  - react-grab@0.1.4\n  - @react-grab/relay@0.1.4\n\n## 0.1.3\n\n### Patch Changes\n\n- fix: @react-grab/utils not publishing\n- Updated dependencies\n  - react-grab@0.1.3\n  - @react-grab/relay@0.1.3\n\n## 0.1.2\n\n### Patch Changes\n\n- fix: packages not being published\n- Updated dependencies\n  - react-grab@0.1.2\n  - @react-grab/relay@0.1.2\n\n## 0.1.1\n\n### Patch Changes\n\n- fix: clicking element after keyboard navigation\n- Updated dependencies\n  - react-grab@0.1.1\n  - @react-grab/relay@0.1.1\n\n## 0.1.0\n\n### Minor Changes\n\n- 81adb50: feat: browser\n\n### Patch Changes\n\n- 81adb50: fix: shell script\n- fb2b037: fix: cli\n- a3d5a94: fix: cli global install\n- 81adb50: feat: react support\n- 81adb50: fix: a11y\n- a5e7a6a: fix: optimize loading speed of cli\n- 90af3f6: fix: CLI hanging\n- 81adb50: fix: shell script\n- 78efee2: fix: cli\n- 074e593: fix: cli\n- 5cd3709: fix: decouple browser out from react-grab\n- 54c4867: ui improvements\n- Updated dependencies [81adb50]\n- Updated dependencies [fb2b037]\n- Updated dependencies [a3d5a94]\n- Updated dependencies [81adb50]\n- Updated dependencies [81adb50]\n- Updated dependencies [616d3e8]\n- Updated dependencies [81adb50]\n- Updated dependencies [a5e7a6a]\n- Updated dependencies [90af3f6]\n- Updated dependencies [81adb50]\n- Updated dependencies [81adb50]\n- Updated dependencies [78efee2]\n- Updated dependencies [074e593]\n- Updated dependencies [5cd3709]\n- Updated dependencies [54c4867]\n  - react-grab@0.1.0\n  - @react-grab/relay@0.1.0\n\n## 0.1.0-beta.13\n\n### Patch Changes\n\n- ui improvements\n- Updated dependencies [616d3e8]\n- Updated dependencies\n  - react-grab@0.1.0-beta.13\n  - @react-grab/relay@0.1.0-beta.13\n\n## 0.1.0-beta.12\n\n### Patch Changes\n\n- fix: decouple browser out from react-grab\n- Updated dependencies\n  - react-grab@0.1.0-beta.12\n  - @react-grab/relay@0.1.0-beta.12\n\n## 0.1.0-beta.11\n\n### Patch Changes\n\n- fix: cli global install\n- Updated dependencies\n  - react-grab@0.1.0-beta.11\n  - @react-grab/relay@0.1.0-beta.11\n\n## 0.1.0-beta.10\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.10\n  - @react-grab/relay@0.1.0-beta.10\n\n## 0.1.0-beta.9\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.9\n  - @react-grab/relay@0.1.0-beta.9\n\n## 0.1.0-beta.8\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.8\n  - @react-grab/relay@0.1.0-beta.8\n\n## 0.1.0-beta.7\n\n### Patch Changes\n\n- fix: CLI hanging\n- Updated dependencies\n  - react-grab@0.1.0-beta.7\n  - @react-grab/relay@0.1.0-beta.7\n\n## 0.1.0-beta.6\n\n### Patch Changes\n\n- fix: optimize loading speed of cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.6\n  - @react-grab/relay@0.1.0-beta.6\n\n## 0.1.0-beta.5\n\n### Patch Changes\n\n- fix: a11y\n- Updated dependencies\n  - react-grab@0.1.0-beta.5\n  - @react-grab/relay@0.1.0-beta.5\n\n## 0.1.0-beta.4\n\n### Patch Changes\n\n- feat: react support\n- Updated dependencies\n  - react-grab@0.1.0-beta.4\n  - @react-grab/relay@0.1.0-beta.4\n\n## 0.1.0-beta.3\n\n### Patch Changes\n\n- Updated dependencies\n  - react-grab@0.1.0-beta.3\n\n## 0.1.0-beta.2\n\n### Patch Changes\n\n- fix: shell script\n- Updated dependencies\n  - react-grab@0.1.0-beta.2\n  - @react-grab/relay@0.1.0-beta.2\n\n## 0.1.0-beta.1\n\n### Patch Changes\n\n- fix: shell script\n- Updated dependencies\n  - react-grab@0.1.0-beta.1\n  - @react-grab/relay@0.1.0-beta.1\n\n## 0.1.0-beta.0\n\n### Minor Changes\n\n- feat: browser\n\n### Patch Changes\n\n- Updated dependencies\n  - react-grab@0.1.0-beta.0\n  - @react-grab/relay@0.1.0-beta.0\n\n## 0.0.98\n\n### Patch Changes\n\n- feat: new state architecture and context menu\n- Updated dependencies\n  - react-grab@0.0.98\n\n## 0.0.97\n\n### Patch Changes\n\n- fix: sourcemap error\n- Updated dependencies\n  - react-grab@0.0.97\n\n## 0.0.96\n\n### Patch Changes\n\n- fix: fiber access timeout handling\n- Updated dependencies\n  - react-grab@0.0.96\n\n## 0.0.95\n\n### Patch Changes\n\n- fix: selecting buttons with disabled states\n- Updated dependencies\n  - react-grab@0.0.95\n\n## 0.0.94\n\n### Patch Changes\n\n- fix: browser crashing on selection bug\n- Updated dependencies\n  - react-grab@0.0.94\n\n## 0.0.93\n\n### Patch Changes\n\n- fix: copying not working\n- Updated dependencies\n  - react-grab@0.0.93\n\n## 0.0.92\n\n### Patch Changes\n\n- refactor: use state machines instead of signals\n- Updated dependencies\n  - react-grab@0.0.92\n\n## 0.0.91\n\n### Patch Changes\n\n- feat: dock\n- Updated dependencies\n  - react-grab@0.0.91\n\n## 0.0.90\n\n### Patch Changes\n\n- fix: check visual edit endpoint\n- Updated dependencies\n  - react-grab@0.0.90\n\n## 0.0.89\n\n### Patch Changes\n\n- fix: many bugfixes\n- Updated dependencies\n  - react-grab@0.0.89\n\n## 0.0.88\n\n### Patch Changes\n\n- fix: deprecation errors\n- Updated dependencies\n  - react-grab@0.0.88\n\n## 0.0.87\n\n### Patch Changes\n\n- feat: visual edits\n- Updated dependencies\n  - react-grab@0.0.87\n\n## 0.0.86\n\n### Patch Changes\n\n- fix: editing\n- Updated dependencies\n  - react-grab@0.0.86\n\n## 0.0.85\n\n### Patch Changes\n\n- fix: check versions on each provider\n- Updated dependencies\n  - react-grab@0.0.85\n\n## 0.0.84\n\n### Patch Changes\n\n- fix: migrate from cross-spawn to execa to fix deprecation issue\n- Updated dependencies\n  - react-grab@0.0.84\n\n## 0.0.83\n\n### Patch Changes\n\n- feat: timings during agent processing\n- Updated dependencies\n  - react-grab@0.0.83\n\n## 0.0.82\n\n### Patch Changes\n\n- fix: agent support\n- Updated dependencies\n  - react-grab@0.0.82\n\n## 0.0.81\n\n### Patch Changes\n\n- feat: codex and gemini support\n- Updated dependencies\n  - react-grab@0.0.81\n\n## 0.0.80\n\n### Patch Changes\n\n- fix: replies and undo\n- Updated dependencies\n  - react-grab@0.0.80\n\n## 0.0.79\n\n### Patch Changes\n\n- fix: claude code exit issue\n- Updated dependencies\n  - react-grab@0.0.79\n\n## 0.0.78\n\n### Patch Changes\n\n- fix: cancel animation\n- Updated dependencies\n  - react-grab@0.0.78\n\n## 0.0.77\n\n### Patch Changes\n\n- fix: new cli proxying\n- Updated dependencies\n  - react-grab@0.0.77\n\n## 0.0.76\n\n### Patch Changes\n\n- feat: allow CLI under react-grab namespace\n- Updated dependencies\n  - react-grab@0.0.76\n\n## 0.0.75\n\n### Patch Changes\n\n- fix: issue with Illegal Invocation on next.js pages\n- Updated dependencies\n  - react-grab@0.0.75\n\n## 0.0.74\n\n### Patch Changes\n\n- fix: updateOptions\n- Updated dependencies\n  - react-grab@0.0.74\n\n## 0.0.73\n\n### Patch Changes\n\n- fix: improve cli\n- Updated dependencies\n  - react-grab@0.0.73\n\n## 0.0.72\n\n### Patch Changes\n\n- fix: shimmer effect\n- Updated dependencies\n  - react-grab@0.0.72\n\n## 0.0.71\n\n### Patch Changes\n\n- fix: ux nits\n- Updated dependencies\n  - react-grab@0.0.71\n\n## 0.0.70\n\n### Patch Changes\n\n- fix: react-grab cli flow when agents is used\n- Updated dependencies\n  - react-grab@0.0.70\n\n## 0.0.69\n\n### Patch Changes\n\n- fix: CLI on script tag\n- Updated dependencies\n  - react-grab@0.0.69\n\n## 0.0.68\n\n### Patch Changes\n\n- feat: opencode and cli installer\n- Updated dependencies\n  - react-grab@0.0.68\n\n## 0.0.67\n\n### Patch Changes\n\n- fix: logs\n- Updated dependencies\n  - react-grab@0.0.67\n\n## 0.0.66\n\n### Patch Changes\n\n- fix: flash animation\n- Updated dependencies\n  - react-grab@0.0.66\n\n## 0.0.65\n\n### Patch Changes\n\n- fix: instrumentation\n- Updated dependencies\n  - react-grab@0.0.65\n\n## 0.0.64\n\n### Patch Changes\n\n- fix: stream resumption\n- Updated dependencies\n  - react-grab@0.0.64\n\n## 0.0.63\n\n### Patch Changes\n\n- fix: x positioning of selection label\n- Updated dependencies\n  - react-grab@0.0.63\n\n## 0.0.62\n\n### Patch Changes\n\n- fix: stream resumption\n- Updated dependencies\n  - react-grab@0.0.62\n\n## 0.0.61\n\n### Patch Changes\n\n- fix: improved installation strategy\n- Updated dependencies\n  - react-grab@0.0.61\n\n## 0.0.60\n\n### Patch Changes\n\n- fix: loading states\n- Updated dependencies\n  - react-grab@0.0.60\n\n## 0.0.59\n\n### Patch Changes\n\n- fix: improve component name\n- Updated dependencies\n  - react-grab@0.0.59\n\n## 0.0.58\n\n### Patch Changes\n\n- fix: issues with stack\n- Updated dependencies\n  - react-grab@0.0.58\n\n## 0.0.57\n\n### Patch Changes\n\n- fix: improvements to UI\n- Updated dependencies\n  - react-grab@0.0.57\n\n## 0.0.56\n\n### Patch Changes\n\n- add Turborepo for monorepo build orchestration\n- Updated dependencies\n  - react-grab@0.0.56\n\n## 0.0.55\n\n### Patch Changes\n\n- beta\n- Updated dependencies\n  - react-grab@0.0.55\n"
  },
  {
    "path": "packages/provider-cursor/README.md",
    "content": "# @react-grab/cursor\n\nCursor agent provider for React Grab. Requires running a local server that interfaces with the Cursor Agent CLI.\n\n## Installation\n\n```bash\nnpm install @react-grab/cursor\n# or\npnpm add @react-grab/cursor\n# or\nbun add @react-grab/cursor\n# or\nyarn add @react-grab/cursor\n```\n\n## Server Setup\n\nThe server runs on port `5567` by default.\n\n### Quick Start (CLI)\n\nStart the server in the background before running your dev server:\n\n```bash\nnpx @react-grab/cursor@latest && pnpm run dev\n```\n\nThe server will run as a detached background process. **Note:** Stopping your dev server (Ctrl+C) won't stop the React Grab server. To stop it:\n\n```bash\npkill -f \"react-grab.*server\"\n```\n\n### Recommended: Config File (Automatic Lifecycle)\n\nFor better lifecycle management, start the server from your config file. This ensures the server stops when your dev server stops:\n\n### Vite\n\n```ts\n// vite.config.ts\nimport { startServer } from \"@react-grab/cursor/server\";\n\nif (process.env.NODE_ENV === \"development\") {\n  startServer();\n}\n```\n\n### Next.js\n\n```ts\n// next.config.ts\nimport { startServer } from \"@react-grab/cursor/server\";\n\nif (process.env.NODE_ENV === \"development\") {\n  startServer();\n}\n```\n\n## Client Usage\n\n### Script Tag\n\n```html\n<script src=\"//unpkg.com/react-grab/dist/index.global.js\"></script>\n<script src=\"//unpkg.com/@react-grab/cursor/dist/client.global.js\"></script>\n```\n\n### Next.js\n\nUsing the `Script` component in your `app/layout.tsx`:\n\n```jsx\nimport Script from \"next/script\";\n\nexport default function RootLayout({ children }) {\n  return (\n    <html>\n      <head>\n        {process.env.NODE_ENV === \"development\" && (\n          <>\n            <Script\n              src=\"//unpkg.com/react-grab/dist/index.global.js\"\n              strategy=\"beforeInteractive\"\n            />\n            <Script\n              src=\"//unpkg.com/@react-grab/cursor/dist/client.global.js\"\n              strategy=\"lazyOnload\"\n            />\n          </>\n        )}\n      </head>\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\n### ES Module\n\n```tsx\nimport { attachAgent } from \"@react-grab/cursor/client\";\n\nattachAgent();\n```\n\n## How It Works\n\n```\n┌─────────────────┐      HTTP       ┌─────────────────┐     stdin      ┌─────────────────┐\n│                 │  localhost:5567 │                 │                │                 │\n│   React Grab    │ ──────────────► │     Server      │ ─────────────► │  cursor-agent   │\n│    (Browser)    │ ◄────────────── │   (Node.js)     │ ◄───────────── │      (CLI)      │\n│                 │       SSE       │                 │     stdout     │                 │\n└─────────────────┘                 └─────────────────┘                └─────────────────┘\n      Client                              Server                            Agent\n```\n\n1. **React Grab** sends the selected element context to the server via HTTP POST\n2. **Server** receives the request and spawns the `cursor-agent` CLI process\n3. **cursor-agent** processes the request and streams JSON responses to stdout\n4. **Server** relays status updates to the client via Server-Sent Events (SSE)\n"
  },
  {
    "path": "packages/provider-cursor/package.json",
    "content": "{\n  \"name\": \"@react-grab/cursor\",\n  \"version\": \"0.1.28\",\n  \"bin\": {\n    \"react-grab-cursor\": \"./dist/cli.cjs\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"type\": \"module\",\n  \"browser\": \"dist/client.global.js\",\n  \"exports\": {\n    \"./client\": {\n      \"types\": \"./dist/client.d.ts\",\n      \"import\": \"./dist/client.js\",\n      \"require\": \"./dist/client.cjs\"\n    },\n    \"./handler\": {\n      \"types\": \"./dist/handler.d.ts\",\n      \"import\": \"./dist/handler.js\",\n      \"require\": \"./dist/handler.cjs\"\n    },\n    \"./server\": {\n      \"types\": \"./dist/server.d.ts\",\n      \"import\": \"./dist/server.js\",\n      \"require\": \"./dist/server.cjs\"\n    },\n    \"./dist/*\": \"./dist/*.js\",\n    \"./dist/*.js\": \"./dist/*.js\"\n  },\n  \"scripts\": {\n    \"dev\": \"tsup --watch\",\n    \"build\": \"rm -rf dist && NODE_ENV=production tsup\"\n  },\n  \"dependencies\": {\n    \"@react-grab/relay\": \"workspace:*\",\n    \"execa\": \"^9.6.0\",\n    \"react-grab\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@react-grab/utils\": \"workspace:*\",\n    \"@types/node\": \"^22.10.7\",\n    \"tsup\": \"^8.4.0\"\n  }\n}\n"
  },
  {
    "path": "packages/provider-cursor/src/cli.ts",
    "content": "#!/usr/bin/env node\nimport { spawn } from \"node:child_process\";\nimport { realpathSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\nconst realScriptPath = realpathSync(process.argv[1]);\nconst scriptDir = dirname(realScriptPath);\nconst serverPath = join(scriptDir, \"server.cjs\");\n\nconst child = spawn(\n  process.execPath,\n  [\"-e\", `require(${JSON.stringify(serverPath)}).startServer()`],\n  {\n    detached: true,\n    stdio: \"inherit\",\n  },\n);\n\nchild.unref();\nprocess.exit(0);\n"
  },
  {
    "path": "packages/provider-cursor/src/client.ts",
    "content": "import type { AgentCompleteResult } from \"react-grab/core\";\nimport { createProviderClientPlugin } from \"@react-grab/relay/client\";\n\nexport type { AgentCompleteResult };\n\nconst { createAgentProvider: createCursorAgentProvider, attachAgent } =\n  createProviderClientPlugin({\n    agentId: \"cursor\",\n    pluginName: \"cursor-agent\",\n    actionId: \"edit-with-cursor\",\n    actionLabel: \"Edit with Cursor\",\n  });\n\nexport { createCursorAgentProvider, attachAgent };\n\nattachAgent();\n"
  },
  {
    "path": "packages/provider-cursor/src/handler.ts",
    "content": "import { execa, type ResultPromise } from \"execa\";\nimport type {\n  AgentHandler,\n  AgentMessage,\n  AgentRunOptions,\n} from \"@react-grab/relay\";\nimport { COMPLETED_STATUS } from \"@react-grab/relay\";\nimport { formatSpawnError } from \"@react-grab/utils/server\";\n\nexport interface CursorAgentOptions extends AgentRunOptions {\n  model?: string;\n  workspace?: string;\n}\n\ninterface CursorStreamEvent {\n  type: \"system\" | \"user\" | \"thinking\" | \"assistant\" | \"result\";\n  subtype?: \"init\" | \"delta\" | \"completed\" | \"success\" | \"error\";\n  message?: {\n    role: string;\n    content: Array<{ type: string; text: string }>;\n  };\n  result?: string;\n  is_error?: boolean;\n  session_id?: string;\n}\n\nconst cursorSessionMap = new Map<string, string>();\nconst activeProcesses = new Map<string, ResultPromise>();\nlet lastCursorChatId: string | undefined;\n\nconst parseStreamLine = (line: string): CursorStreamEvent | null => {\n  const trimmed = line.trim();\n  if (!trimmed) return null;\n\n  try {\n    return JSON.parse(trimmed) as CursorStreamEvent;\n  } catch {\n    return null;\n  }\n};\n\nconst extractTextFromMessage = (\n  message: CursorStreamEvent[\"message\"],\n): string => {\n  if (!message?.content) return \"\";\n\n  return message.content\n    .filter((block) => block.type === \"text\")\n    .map((block) => block.text)\n    .join(\" \")\n    .trim();\n};\n\nconst runCursorAgent = async function* (\n  prompt: string,\n  options?: CursorAgentOptions,\n): AsyncGenerator<AgentMessage> {\n  const cursorAgentArgs = [\n    \"--print\",\n    \"--output-format\",\n    \"stream-json\",\n    \"--force\",\n  ];\n\n  if (options?.model) {\n    cursorAgentArgs.push(\"--model\", options.model);\n  }\n\n  const workspacePath =\n    options?.workspace ??\n    options?.cwd ??\n    process.env.REACT_GRAB_CWD ??\n    process.cwd();\n\n  const cursorChatId = options?.sessionId\n    ? cursorSessionMap.get(options.sessionId)\n    : undefined;\n\n  if (cursorChatId) {\n    cursorAgentArgs.push(\"--resume\", cursorChatId);\n  }\n\n  let cursorProcess: ResultPromise | undefined;\n  let stderrBuffer = \"\";\n  let cleanupSignalListener: (() => void) | undefined;\n\n  try {\n    yield { type: \"status\", content: \"Thinking…\" };\n\n    cursorProcess = execa(\"cursor-agent\", cursorAgentArgs, {\n      stdin: \"pipe\",\n      stdout: \"pipe\",\n      stderr: \"pipe\",\n      env: { ...process.env },\n      cwd: workspacePath,\n    });\n\n    if (options?.sessionId) {\n      activeProcesses.set(options.sessionId, cursorProcess);\n    }\n\n    if (cursorProcess.stderr) {\n      cursorProcess.stderr.on(\"data\", (chunk: Buffer) => {\n        stderrBuffer += chunk.toString();\n      });\n    }\n\n    const messageQueue: AgentMessage[] = [];\n    let resolveWait: (() => void) | null = null;\n    let processEnded = false;\n    let aborted = false;\n    let capturedCursorChatId: string | undefined;\n\n    const enqueueMessage = (message: AgentMessage) => {\n      messageQueue.push(message);\n      if (resolveWait) {\n        resolveWait();\n        resolveWait = null;\n      }\n    };\n\n    const handleAbort = () => {\n      aborted = true;\n      if (cursorProcess && !cursorProcess.killed) {\n        cursorProcess.kill(\"SIGTERM\");\n      }\n      if (resolveWait) {\n        resolveWait();\n        resolveWait = null;\n      }\n    };\n\n    const signal = options?.signal;\n    if (signal) {\n      if (signal.aborted) {\n        handleAbort();\n      } else {\n        signal.addEventListener(\"abort\", handleAbort, { once: true });\n      }\n    }\n\n    cleanupSignalListener = () => {\n      if (signal) {\n        signal.removeEventListener(\"abort\", handleAbort);\n      }\n    };\n\n    const processLine = (line: string) => {\n      const event = parseStreamLine(line);\n      if (!event) return;\n\n      if (!capturedCursorChatId && event.session_id) {\n        capturedCursorChatId = event.session_id;\n      }\n\n      switch (event.type) {\n        case \"assistant\": {\n          const textContent = extractTextFromMessage(event.message);\n          if (textContent) {\n            enqueueMessage({ type: \"status\", content: textContent });\n          }\n          break;\n        }\n\n        case \"result\":\n          if (event.subtype === \"success\") {\n            enqueueMessage({ type: \"status\", content: COMPLETED_STATUS });\n          } else if (event.subtype === \"error\" || event.is_error) {\n            enqueueMessage({\n              type: \"error\",\n              content: event.result || \"Unknown error\",\n            });\n          } else {\n            enqueueMessage({ type: \"status\", content: \"Task finished\" });\n          }\n          break;\n      }\n    };\n\n    let buffer = \"\";\n\n    if (cursorProcess.stdout) {\n      cursorProcess.stdout.on(\"data\", (chunk: Buffer) => {\n        buffer += chunk.toString();\n\n        let newlineIndex;\n        while ((newlineIndex = buffer.indexOf(\"\\n\")) !== -1) {\n          const line = buffer.slice(0, newlineIndex);\n          buffer = buffer.slice(newlineIndex + 1);\n          processLine(line);\n        }\n      });\n    }\n\n    if (cursorProcess.stdin) {\n      cursorProcess.stdin.write(prompt);\n      cursorProcess.stdin.end();\n    }\n\n    const childProcess = cursorProcess;\n    childProcess.on(\"close\", (code) => {\n      if (options?.sessionId) {\n        activeProcesses.delete(options.sessionId);\n      }\n      if (buffer.trim()) {\n        processLine(buffer);\n      }\n      if (options?.sessionId && capturedCursorChatId) {\n        cursorSessionMap.set(options.sessionId, capturedCursorChatId);\n      }\n      if (capturedCursorChatId) {\n        lastCursorChatId = capturedCursorChatId;\n      }\n      processEnded = true;\n      if (code !== 0 && !childProcess.killed) {\n        enqueueMessage({\n          type: \"error\",\n          content: `cursor-agent exited with code ${code}`,\n        });\n      }\n      enqueueMessage({ type: \"done\", content: \"\" });\n      if (resolveWait) {\n        resolveWait();\n        resolveWait = null;\n      }\n    });\n\n    childProcess.on(\"error\", (error) => {\n      if (options?.sessionId) {\n        activeProcesses.delete(options.sessionId);\n      }\n      processEnded = true;\n      const isNotInstalled = \"code\" in error && error.code === \"ENOENT\";\n      if (isNotInstalled) {\n        enqueueMessage({\n          type: \"error\",\n          content:\n            \"cursor-agent is not installed. Please install the Cursor Agent CLI to use this provider.\\n\\nInstallation: https://cursor.com/docs/cli/overview\",\n        });\n      } else {\n        const errorMessage = formatSpawnError(error, \"cursor-agent\");\n        const stderrContent = stderrBuffer.trim();\n        const fullError = stderrContent\n          ? `${errorMessage}\\n\\nstderr:\\n${stderrContent}`\n          : errorMessage;\n        enqueueMessage({ type: \"error\", content: fullError });\n      }\n      enqueueMessage({ type: \"done\", content: \"\" });\n      if (resolveWait) {\n        resolveWait();\n        resolveWait = null;\n      }\n    });\n\n    try {\n      while (true) {\n        if (aborted) {\n          return;\n        }\n\n        if (messageQueue.length > 0) {\n          const message = messageQueue.shift()!;\n          if (message.type === \"done\") {\n            yield message;\n            return;\n          }\n          yield message;\n        } else if (processEnded) {\n          return;\n        } else {\n          await new Promise<void>((resolve) => {\n            resolveWait = resolve;\n          });\n        }\n      }\n    } finally {\n      cleanupSignalListener?.();\n    }\n  } catch (error) {\n    cleanupSignalListener?.();\n    const errorMessage =\n      error instanceof Error\n        ? formatSpawnError(error, \"cursor-agent\")\n        : \"Unknown error\";\n    const stderrContent = stderrBuffer.trim();\n    const fullError = stderrContent\n      ? `${errorMessage}\\n\\nstderr:\\n${stderrContent}`\n      : errorMessage;\n    yield { type: \"error\", content: fullError };\n    yield { type: \"done\", content: \"\" };\n  }\n};\n\nconst abortCursorAgent = (sessionId: string) => {\n  const activeProcess = activeProcesses.get(sessionId);\n  if (activeProcess && !activeProcess.killed) {\n    activeProcess.kill(\"SIGTERM\");\n    activeProcesses.delete(sessionId);\n  }\n};\n\nconst undoCursorAgent = async (): Promise<void> => {\n  if (!lastCursorChatId) {\n    return;\n  }\n\n  try {\n    const cursorAgentArgs = [\n      \"--print\",\n      \"--output-format\",\n      \"stream-json\",\n      \"--force\",\n      \"--resume\",\n      lastCursorChatId,\n    ];\n\n    const workspacePath = process.env.REACT_GRAB_CWD ?? process.cwd();\n\n    const cursorProcess = execa(\"cursor-agent\", cursorAgentArgs, {\n      stdin: \"pipe\",\n      stdout: \"pipe\",\n      stderr: \"pipe\",\n      env: { ...process.env },\n      cwd: workspacePath,\n    });\n\n    if (cursorProcess.stdin) {\n      cursorProcess.stdin.write(\"undo\");\n      cursorProcess.stdin.end();\n    }\n\n    await cursorProcess;\n  } catch (error) {\n    const errorMessage =\n      error instanceof Error\n        ? formatSpawnError(error, \"cursor-agent\")\n        : \"Unknown error\";\n    throw new Error(`Undo failed: ${errorMessage}`);\n  }\n};\n\nexport const cursorAgentHandler: AgentHandler = {\n  agentId: \"cursor\",\n  run: runCursorAgent,\n  abort: abortCursorAgent,\n  undo: undoCursorAgent,\n};\n"
  },
  {
    "path": "packages/provider-cursor/src/server.ts",
    "content": "import { startProviderServer } from \"@react-grab/relay\";\nimport { cursorAgentHandler } from \"./handler.js\";\n\nexport const startServer = () => {\n  startProviderServer(\"cursor\", cursorAgentHandler);\n};\n"
  },
  {
    "path": "packages/provider-cursor/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"noEmit\": true,\n    \"types\": [\"node\"]\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/provider-cursor/tsup.config.ts",
    "content": "import fs from \"node:fs\";\nimport module from \"node:module\";\nimport { defineConfig } from \"tsup\";\n\nconst packageJson = JSON.parse(fs.readFileSync(\"package.json\", \"utf8\")) as {\n  version: string;\n};\n\nexport default defineConfig([\n  {\n    entry: {\n      handler: \"./src/handler.ts\",\n      cli: \"./src/cli.ts\",\n      server: \"./src/server.ts\",\n    },\n    format: [\"cjs\", \"esm\"],\n    dts: true,\n    clean: false,\n    splitting: false,\n    sourcemap: false,\n    target: \"node18\",\n    platform: \"node\",\n    treeshake: true,\n    noExternal: [/.*/],\n    external: [\n      ...module.builtinModules,\n      ...module.builtinModules.map((name) => `node:${name}`),\n    ],\n    env: {\n      VERSION: process.env.VERSION ?? packageJson.version,\n    },\n  },\n  {\n    entry: {\n      client: \"./src/client.ts\",\n    },\n    format: [\"cjs\", \"esm\"],\n    dts: true,\n    clean: false,\n    splitting: false,\n    sourcemap: false,\n    target: \"esnext\",\n    platform: \"browser\",\n    treeshake: true,\n    noExternal: [\"@react-grab/relay\"],\n  },\n  {\n    entry: [\"./src/client.ts\"],\n    format: [\"iife\"],\n    globalName: \"ReactGrabCursor\",\n    outExtension: () => ({ js: \".global.js\" }),\n    dts: false,\n    clean: false,\n    minify: process.env.NODE_ENV === \"production\",\n    splitting: false,\n    sourcemap: false,\n    target: \"esnext\",\n    platform: \"browser\",\n    treeshake: true,\n    noExternal: [/.*/],\n  },\n]);\n"
  },
  {
    "path": "packages/provider-droid/CHANGELOG.md",
    "content": "# @react-grab/droid\n\n## 0.1.28\n\n### Patch Changes\n\n- fix\n- Updated dependencies\n  - react-grab@0.1.28\n  - @react-grab/relay@0.1.28\n\n## 0.1.27\n\n### Patch Changes\n\n- fix: install instructions\n- Updated dependencies\n  - react-grab@0.1.27\n  - @react-grab/relay@0.1.27\n\n## 0.1.26\n\n### Patch Changes\n\n- fix: minor tweaks\n- Updated dependencies\n  - react-grab@0.1.26\n  - @react-grab/relay@0.1.26\n\n## 0.1.25\n\n### Patch Changes\n\n- fix: primtiives\n- Updated dependencies\n  - react-grab@0.1.25\n  - @react-grab/relay@0.1.25\n\n## 0.1.24\n\n### Patch Changes\n\n- primitives\n- Updated dependencies\n  - react-grab@0.1.24\n  - @react-grab/relay@0.1.24\n\n## 0.1.23\n\n### Patch Changes\n\n- fix: npx command\n- Updated dependencies\n  - react-grab@0.1.23\n  - @react-grab/relay@0.1.23\n\n## 0.1.22\n\n### Patch Changes\n\n- fix: freezing\n- Updated dependencies\n  - react-grab@0.1.22\n  - @react-grab/relay@0.1.22\n\n## 0.1.21\n\n### Patch Changes\n\n- fix: up and down selection\n- Updated dependencies\n  - react-grab@0.1.21\n  - @react-grab/relay@0.1.21\n\n## 0.1.20\n\n### Patch Changes\n\n- fix: selection performacne\n- Updated dependencies\n  - react-grab@0.1.20\n  - @react-grab/relay@0.1.20\n\n## 0.1.19\n\n### Patch Changes\n\n- fix: gsap\n- Updated dependencies\n  - react-grab@0.1.19\n  - @react-grab/relay@0.1.19\n\n## 0.1.18\n\n### Patch Changes\n\n- fix: minor issues\n- Updated dependencies\n  - react-grab@0.1.18\n  - @react-grab/relay@0.1.18\n\n## 0.1.17\n\n### Patch Changes\n\n- fix: mcp\n- Updated dependencies\n  - react-grab@0.1.17\n  - @react-grab/relay@0.1.17\n\n## 0.1.16\n\n### Patch Changes\n\n- fix: environment detection\n- Updated dependencies\n  - react-grab@0.1.16\n  - @react-grab/relay@0.1.16\n\n## 0.1.15\n\n### Patch Changes\n\n- fix: animations and ux\n- Updated dependencies\n  - react-grab@0.1.15\n  - @react-grab/relay@0.1.15\n\n## 0.1.14\n\n### Patch Changes\n\n- fix: improve recent UX\n- Updated dependencies\n  - react-grab@0.1.14\n  - @react-grab/relay@0.1.14\n\n## 0.1.13\n\n### Patch Changes\n\n- fix MCP client injection\n- Updated dependencies\n  - react-grab@0.1.13\n  - @react-grab/relay@0.1.13\n\n## 0.1.12\n\n### Patch Changes\n\n- feat: MCP\n- Updated dependencies\n  - react-grab@0.1.12\n  - @react-grab/relay@0.1.12\n\n## 0.1.11\n\n### Patch Changes\n\n- fix: claude code provider\n- Updated dependencies\n  - react-grab@0.1.11\n  - @react-grab/relay@0.1.11\n\n## 0.1.10\n\n### Patch Changes\n\n- feat: cdn in cli\n- Updated dependencies\n  - react-grab@0.1.10\n  - @react-grab/relay@0.1.10\n\n## 0.1.9\n\n### Patch Changes\n\n- fix: startServer not exported in providers\n- Updated dependencies\n  - react-grab@0.1.9\n  - @react-grab/relay@0.1.9\n\n## 0.1.8\n\n### Patch Changes\n\n- fix: providers not detaching\n- Updated dependencies\n  - react-grab@0.1.8\n  - @react-grab/relay@0.1.8\n\n## 0.1.7\n\n### Patch Changes\n\n- fix: react freezing safety\n- Updated dependencies\n  - react-grab@0.1.7\n  - @react-grab/relay@0.1.7\n\n## 0.1.6\n\n### Patch Changes\n\n- fix: compat with React Scan\n- Updated dependencies\n  - react-grab@0.1.6\n  - @react-grab/relay@0.1.6\n\n## 0.1.5\n\n### Patch Changes\n\n- fix: fullscreen inset the root\n- Updated dependencies\n  - react-grab@0.1.5\n  - @react-grab/relay@0.1.5\n\n## 0.1.4\n\n### Patch Changes\n\n- fix: improve cli edge cases\n- Updated dependencies\n  - react-grab@0.1.4\n  - @react-grab/relay@0.1.4\n\n## 0.1.3\n\n### Patch Changes\n\n- fix: @react-grab/utils not publishing\n- Updated dependencies\n  - react-grab@0.1.3\n  - @react-grab/relay@0.1.3\n\n## 0.1.2\n\n### Patch Changes\n\n- fix: packages not being published\n- Updated dependencies\n  - react-grab@0.1.2\n  - @react-grab/relay@0.1.2\n\n## 0.1.1\n\n### Patch Changes\n\n- fix: clicking element after keyboard navigation\n- Updated dependencies\n  - react-grab@0.1.1\n  - @react-grab/relay@0.1.1\n\n## 0.1.0\n\n### Minor Changes\n\n- 81adb50: feat: browser\n\n### Patch Changes\n\n- 81adb50: fix: shell script\n- fb2b037: fix: cli\n- a3d5a94: fix: cli global install\n- 81adb50: feat: react support\n- 81adb50: fix: a11y\n- a5e7a6a: fix: optimize loading speed of cli\n- 90af3f6: fix: CLI hanging\n- 81adb50: fix: shell script\n- 78efee2: fix: cli\n- 074e593: fix: cli\n- 5cd3709: fix: decouple browser out from react-grab\n- 54c4867: ui improvements\n- Updated dependencies [81adb50]\n- Updated dependencies [fb2b037]\n- Updated dependencies [a3d5a94]\n- Updated dependencies [81adb50]\n- Updated dependencies [81adb50]\n- Updated dependencies [616d3e8]\n- Updated dependencies [81adb50]\n- Updated dependencies [a5e7a6a]\n- Updated dependencies [90af3f6]\n- Updated dependencies [81adb50]\n- Updated dependencies [81adb50]\n- Updated dependencies [78efee2]\n- Updated dependencies [074e593]\n- Updated dependencies [5cd3709]\n- Updated dependencies [54c4867]\n  - react-grab@0.1.0\n  - @react-grab/relay@0.1.0\n\n## 0.1.0-beta.13\n\n### Patch Changes\n\n- ui improvements\n- Updated dependencies [616d3e8]\n- Updated dependencies\n  - react-grab@0.1.0-beta.13\n  - @react-grab/relay@0.1.0-beta.13\n\n## 0.1.0-beta.12\n\n### Patch Changes\n\n- fix: decouple browser out from react-grab\n- Updated dependencies\n  - react-grab@0.1.0-beta.12\n  - @react-grab/relay@0.1.0-beta.12\n\n## 0.1.0-beta.11\n\n### Patch Changes\n\n- fix: cli global install\n- Updated dependencies\n  - react-grab@0.1.0-beta.11\n  - @react-grab/relay@0.1.0-beta.11\n\n## 0.1.0-beta.10\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.10\n  - @react-grab/relay@0.1.0-beta.10\n\n## 0.1.0-beta.9\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.9\n  - @react-grab/relay@0.1.0-beta.9\n\n## 0.1.0-beta.8\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.8\n  - @react-grab/relay@0.1.0-beta.8\n\n## 0.1.0-beta.7\n\n### Patch Changes\n\n- fix: CLI hanging\n- Updated dependencies\n  - react-grab@0.1.0-beta.7\n  - @react-grab/relay@0.1.0-beta.7\n\n## 0.1.0-beta.6\n\n### Patch Changes\n\n- fix: optimize loading speed of cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.6\n  - @react-grab/relay@0.1.0-beta.6\n\n## 0.1.0-beta.5\n\n### Patch Changes\n\n- fix: a11y\n- Updated dependencies\n  - react-grab@0.1.0-beta.5\n  - @react-grab/relay@0.1.0-beta.5\n\n## 0.1.0-beta.4\n\n### Patch Changes\n\n- feat: react support\n- Updated dependencies\n  - react-grab@0.1.0-beta.4\n  - @react-grab/relay@0.1.0-beta.4\n\n## 0.1.0-beta.3\n\n### Patch Changes\n\n- Updated dependencies\n  - react-grab@0.1.0-beta.3\n\n## 0.1.0-beta.2\n\n### Patch Changes\n\n- fix: shell script\n- Updated dependencies\n  - react-grab@0.1.0-beta.2\n  - @react-grab/relay@0.1.0-beta.2\n\n## 0.1.0-beta.1\n\n### Patch Changes\n\n- fix: shell script\n- Updated dependencies\n  - react-grab@0.1.0-beta.1\n  - @react-grab/relay@0.1.0-beta.1\n\n## 0.1.0-beta.0\n\n### Minor Changes\n\n- feat: browser\n\n### Patch Changes\n\n- Updated dependencies\n  - react-grab@0.1.0-beta.0\n  - @react-grab/relay@0.1.0-beta.0\n\n## 0.0.98\n\n### Patch Changes\n\n- feat: new state architecture and context menu\n- Updated dependencies\n  - react-grab@0.0.98\n\n## 0.0.97\n\n### Patch Changes\n\n- fix: sourcemap error\n- Updated dependencies\n  - react-grab@0.0.97\n\n## 0.0.96\n\n### Patch Changes\n\n- fix: fiber access timeout handling\n- Updated dependencies\n  - react-grab@0.0.96\n\n## 0.0.95\n\n### Patch Changes\n\n- fix: selecting buttons with disabled states\n- Updated dependencies\n  - react-grab@0.0.95\n\n## 0.0.94\n\n### Patch Changes\n\n- fix: browser crashing on selection bug\n- Updated dependencies\n  - react-grab@0.0.94\n\n## 0.0.93\n\n### Patch Changes\n\n- fix: copying not working\n- Updated dependencies\n  - react-grab@0.0.93\n\n## 0.0.92\n\n### Patch Changes\n\n- refactor: use state machines instead of signals\n- Updated dependencies\n  - react-grab@0.0.92\n\n## 0.0.91\n\n### Patch Changes\n\n- feat: dock\n- Updated dependencies\n  - react-grab@0.0.91\n\n## 0.0.90\n\n### Patch Changes\n\n- fix: check visual edit endpoint\n- Updated dependencies\n  - react-grab@0.0.90\n\n## 0.0.89\n\n### Patch Changes\n\n- fix: many bugfixes\n- Updated dependencies\n  - react-grab@0.0.89\n"
  },
  {
    "path": "packages/provider-droid/README.md",
    "content": "# @react-grab/droid\n\nFactory Droid provider for React Grab. Requires running a local server that interfaces with the Factory CLI (`droid exec`).\n\n## Installation\n\n```bash\nnpm install @react-grab/droid\n# or\npnpm add @react-grab/droid\n# or\nbun add @react-grab/droid\n# or\nyarn add @react-grab/droid\n```\n\n## Prerequisites\n\nYou must have the Factory CLI installed:\n\n```bash\ncurl -fsSL https://app.factory.ai/cli | sh\n```\n\nAnd set your Factory API key:\n\n```bash\nexport FACTORY_API_KEY=fk-...\n```\n\n## Server Setup\n\nThe server runs on port `10567` by default.\n\n### Quick Start (CLI)\n\nStart the server in the background before running your dev server:\n\n```bash\nnpx @react-grab/droid@latest && pnpm run dev\n```\n\nThe server will run as a detached background process. **Note:** Stopping your dev server (Ctrl+C) won't stop the React Grab server. To stop it:\n\n```bash\npkill -f \"react-grab.*server\"\n```\n\n### Recommended: Config File (Automatic Lifecycle)\n\nFor better lifecycle management, start the server from your config file. This ensures the server stops when your dev server stops:\n\n### Vite\n\n```ts\n// vite.config.ts\nimport { startServer } from \"@react-grab/droid/server\";\n\nif (process.env.NODE_ENV === \"development\") {\n  startServer();\n}\n```\n\n### Next.js\n\n```ts\n// next.config.ts\nimport { startServer } from \"@react-grab/droid/server\";\n\nif (process.env.NODE_ENV === \"development\") {\n  startServer();\n}\n```\n\n## Client Usage\n\n### Script Tag\n\n```html\n<script src=\"//unpkg.com/react-grab/dist/index.global.js\"></script>\n<script src=\"//unpkg.com/@react-grab/droid/dist/client.global.js\"></script>\n```\n\n### Next.js\n\nUsing the `Script` component in your `app/layout.tsx`:\n\n```jsx\nimport Script from \"next/script\";\n\nexport default function RootLayout({ children }) {\n  return (\n    <html>\n      <head>\n        {process.env.NODE_ENV === \"development\" && (\n          <>\n            <Script\n              src=\"//unpkg.com/react-grab/dist/index.global.js\"\n              strategy=\"beforeInteractive\"\n            />\n            <Script\n              src=\"//unpkg.com/@react-grab/droid/dist/client.global.js\"\n              strategy=\"lazyOnload\"\n            />\n          </>\n        )}\n      </head>\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\n### ES Module\n\n```tsx\nimport { attachAgent } from \"@react-grab/droid/client\";\n\nattachAgent();\n```\n\n## Configuration Options\n\nThe provider supports the following options:\n\n```typescript\ninterface DroidAgentOptions {\n  autoLevel?: \"low\" | \"medium\" | \"high\"; // Autonomy level (default: \"low\")\n  model?: string; // Model to use (e.g., \"claude-sonnet-4-5-20250929\")\n  reasoningEffort?: \"low\" | \"medium\" | \"high\";\n  workspace?: string; // Working directory\n}\n```\n\n## How It Works\n\n```\n┌─────────────────┐      HTTP       ┌─────────────────┐     stdin      ┌─────────────────┐\n│                 │ localhost:10567 │                 │                │                 │\n│   React Grab    │ ──────────────► │     Server      │ ─────────────► │   droid exec    │\n│    (Browser)    │ ◄────────────── │   (Node.js)     │ ◄───────────── │      (CLI)      │\n│                 │       SSE       │                 │     stdout     │                 │\n└─────────────────┘                 └─────────────────┘                └─────────────────┘\n      Client                              Server                            Agent\n```\n\n1. **React Grab** sends the selected element context to the server via HTTP POST\n2. **Server** receives the request and spawns `droid exec` with `--output-format stream-json`\n3. **droid exec** processes the request and streams JSON responses to stdout\n4. **Server** relays status updates to the client via Server-Sent Events (SSE)\n\n## Autonomy Levels\n\n- `low` (default): File edits only, no system modifications\n- `medium`: Includes package installations, local git operations\n- `high`: Full access including git push, deployments\n"
  },
  {
    "path": "packages/provider-droid/package.json",
    "content": "{\n  \"name\": \"@react-grab/droid\",\n  \"version\": \"0.1.28\",\n  \"bin\": {\n    \"react-grab-droid\": \"./dist/cli.cjs\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"type\": \"module\",\n  \"browser\": \"dist/client.global.js\",\n  \"exports\": {\n    \"./client\": {\n      \"types\": \"./dist/client.d.ts\",\n      \"import\": \"./dist/client.js\",\n      \"require\": \"./dist/client.cjs\"\n    },\n    \"./handler\": {\n      \"types\": \"./dist/handler.d.ts\",\n      \"import\": \"./dist/handler.js\",\n      \"require\": \"./dist/handler.cjs\"\n    },\n    \"./server\": {\n      \"types\": \"./dist/server.d.ts\",\n      \"import\": \"./dist/server.js\",\n      \"require\": \"./dist/server.cjs\"\n    },\n    \"./dist/*\": \"./dist/*.js\",\n    \"./dist/*.js\": \"./dist/*.js\"\n  },\n  \"scripts\": {\n    \"dev\": \"tsup --watch\",\n    \"build\": \"rm -rf dist && NODE_ENV=production tsup\"\n  },\n  \"dependencies\": {\n    \"@react-grab/relay\": \"workspace:*\",\n    \"execa\": \"^9.6.0\",\n    \"react-grab\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@react-grab/utils\": \"workspace:*\",\n    \"@types/node\": \"^22.10.7\",\n    \"tsup\": \"^8.4.0\"\n  }\n}\n"
  },
  {
    "path": "packages/provider-droid/src/cli.ts",
    "content": "#!/usr/bin/env node\nimport { spawn } from \"node:child_process\";\nimport { realpathSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\nconst realScriptPath = realpathSync(process.argv[1]);\nconst scriptDir = dirname(realScriptPath);\nconst serverPath = join(scriptDir, \"server.cjs\");\n\nconst child = spawn(\n  process.execPath,\n  [\"-e\", `require(${JSON.stringify(serverPath)}).startServer()`],\n  {\n    detached: true,\n    stdio: \"inherit\",\n  },\n);\n\nchild.unref();\nprocess.exit(0);\n"
  },
  {
    "path": "packages/provider-droid/src/client.ts",
    "content": "import type { AgentCompleteResult } from \"react-grab/core\";\nimport { createProviderClientPlugin } from \"@react-grab/relay/client\";\n\nexport type { AgentCompleteResult };\n\nconst { createAgentProvider: createDroidAgentProvider, attachAgent } =\n  createProviderClientPlugin({\n    agentId: \"droid\",\n    pluginName: \"droid-agent\",\n    actionId: \"edit-with-droid\",\n    actionLabel: \"Edit with Droid\",\n  });\n\nexport { createDroidAgentProvider, attachAgent };\n\nattachAgent();\n"
  },
  {
    "path": "packages/provider-droid/src/handler.ts",
    "content": "import { execa, type ResultPromise } from \"execa\";\nimport type {\n  AgentHandler,\n  AgentMessage,\n  AgentRunOptions,\n} from \"@react-grab/relay\";\nimport { COMPLETED_STATUS } from \"@react-grab/relay\";\nimport { formatSpawnError } from \"@react-grab/utils/server\";\n\nexport interface DroidAgentOptions extends AgentRunOptions {\n  autoLevel?: \"low\" | \"medium\" | \"high\";\n  model?: string;\n  reasoningEffort?: \"low\" | \"medium\" | \"high\";\n  workspace?: string;\n}\n\ninterface DroidStreamEvent {\n  type: \"system\" | \"message\" | \"tool_call\" | \"tool_result\" | \"completion\";\n  subtype?: \"init\" | \"success\" | \"error\";\n  role?: \"user\" | \"assistant\";\n  text?: string;\n  toolName?: string;\n  finalText?: string;\n  session_id?: string;\n  is_error?: boolean;\n}\n\nconst droidSessionMap = new Map<string, string>();\nconst activeProcesses = new Map<string, ResultPromise>();\nlet lastDroidSessionId: string | undefined;\n\nconst parseStreamLine = (line: string): DroidStreamEvent | null => {\n  const trimmed = line.trim();\n  if (!trimmed) return null;\n\n  try {\n    return JSON.parse(trimmed) as DroidStreamEvent;\n  } catch {\n    return null;\n  }\n};\n\nconst runDroidAgent = async function* (\n  prompt: string,\n  options?: DroidAgentOptions,\n): AsyncGenerator<AgentMessage> {\n  const droidArgs = [\"exec\", \"--output-format\", \"stream-json\"];\n\n  const autoLevel = options?.autoLevel ?? \"low\";\n  droidArgs.push(\"--auto\", autoLevel);\n\n  if (options?.model) {\n    droidArgs.push(\"--model\", options.model);\n  }\n\n  if (options?.reasoningEffort) {\n    droidArgs.push(\"--reasoning-effort\", options.reasoningEffort);\n  }\n\n  const workspacePath =\n    options?.workspace ??\n    options?.cwd ??\n    process.env.REACT_GRAB_CWD ??\n    process.cwd();\n  droidArgs.push(\"--cwd\", workspacePath);\n\n  const droidSessionId = options?.sessionId\n    ? droidSessionMap.get(options.sessionId)\n    : undefined;\n\n  if (droidSessionId) {\n    droidArgs.push(\"--session-id\", droidSessionId);\n  }\n\n  let droidProcess: ResultPromise | undefined;\n  let stderrBuffer = \"\";\n\n  try {\n    yield { type: \"status\", content: \"Thinking…\" };\n\n    droidProcess = execa(\"droid\", droidArgs, {\n      stdin: \"pipe\",\n      stdout: \"pipe\",\n      stderr: \"pipe\",\n      env: { ...process.env },\n    });\n\n    if (options?.sessionId) {\n      activeProcesses.set(options.sessionId, droidProcess);\n    }\n\n    if (droidProcess.stderr) {\n      droidProcess.stderr.on(\"data\", (chunk: Buffer) => {\n        stderrBuffer += chunk.toString();\n      });\n    }\n\n    const messageQueue: AgentMessage[] = [];\n    let resolveWait: (() => void) | null = null;\n    let processEnded = false;\n    let capturedDroidSessionId: string | undefined;\n\n    const enqueueMessage = (message: AgentMessage) => {\n      messageQueue.push(message);\n      if (resolveWait) {\n        resolveWait();\n        resolveWait = null;\n      }\n    };\n\n    const processLine = (line: string) => {\n      const event = parseStreamLine(line);\n      if (!event) return;\n\n      if (!capturedDroidSessionId && event.session_id) {\n        capturedDroidSessionId = event.session_id;\n      }\n\n      switch (event.type) {\n        case \"message\": {\n          if (event.role === \"assistant\" && event.text) {\n            enqueueMessage({ type: \"status\", content: event.text });\n          }\n          break;\n        }\n\n        case \"tool_call\": {\n          if (event.toolName) {\n            enqueueMessage({\n              type: \"status\",\n              content: `Running ${event.toolName}…`,\n            });\n          }\n          break;\n        }\n\n        case \"completion\": {\n          if (event.is_error) {\n            enqueueMessage({\n              type: \"error\",\n              content: event.finalText || \"Unknown error\",\n            });\n          } else {\n            enqueueMessage({ type: \"status\", content: COMPLETED_STATUS });\n          }\n          break;\n        }\n      }\n    };\n\n    let buffer = \"\";\n\n    if (droidProcess.stdout) {\n      droidProcess.stdout.on(\"data\", (chunk: Buffer) => {\n        buffer += chunk.toString();\n\n        let newlineIndex;\n        while ((newlineIndex = buffer.indexOf(\"\\n\")) !== -1) {\n          const line = buffer.slice(0, newlineIndex);\n          buffer = buffer.slice(newlineIndex + 1);\n          processLine(line);\n        }\n      });\n    }\n\n    if (droidProcess.stdin) {\n      droidProcess.stdin.write(prompt);\n      droidProcess.stdin.end();\n    }\n\n    const childProcess = droidProcess;\n    childProcess.on(\"close\", (code) => {\n      if (options?.sessionId) {\n        activeProcesses.delete(options.sessionId);\n      }\n      if (buffer.trim()) {\n        processLine(buffer);\n      }\n      if (options?.sessionId && capturedDroidSessionId) {\n        droidSessionMap.set(options.sessionId, capturedDroidSessionId);\n      }\n      if (capturedDroidSessionId) {\n        lastDroidSessionId = capturedDroidSessionId;\n      }\n      processEnded = true;\n      if (code !== 0 && !childProcess.killed) {\n        enqueueMessage({\n          type: \"error\",\n          content: `droid exec exited with code ${code}`,\n        });\n      }\n      enqueueMessage({ type: \"done\", content: \"\" });\n      if (resolveWait) {\n        resolveWait();\n        resolveWait = null;\n      }\n    });\n\n    childProcess.on(\"error\", (error) => {\n      if (options?.sessionId) {\n        activeProcesses.delete(options.sessionId);\n      }\n      processEnded = true;\n      const isNotInstalled = \"code\" in error && error.code === \"ENOENT\";\n      if (isNotInstalled) {\n        enqueueMessage({\n          type: \"error\",\n          content:\n            \"droid CLI is not installed. Please install Factory CLI to use this provider.\\n\\nInstallation: curl -fsSL https://app.factory.ai/cli | sh\",\n        });\n      } else {\n        const errorMessage = formatSpawnError(error, \"droid\");\n        const stderrContent = stderrBuffer.trim();\n        const fullError = stderrContent\n          ? `${errorMessage}\\n\\nstderr:\\n${stderrContent}`\n          : errorMessage;\n        enqueueMessage({ type: \"error\", content: fullError });\n      }\n      enqueueMessage({ type: \"done\", content: \"\" });\n      if (resolveWait) {\n        resolveWait();\n        resolveWait = null;\n      }\n    });\n\n    while (true) {\n      if (options?.signal?.aborted) {\n        if (droidProcess && !droidProcess.killed) {\n          droidProcess.kill(\"SIGTERM\");\n        }\n        return;\n      }\n\n      if (messageQueue.length > 0) {\n        const message = messageQueue.shift()!;\n        if (message.type === \"done\") {\n          yield message;\n          return;\n        }\n        yield message;\n      } else if (processEnded) {\n        return;\n      } else {\n        await new Promise<void>((resolve) => {\n          resolveWait = resolve;\n        });\n      }\n    }\n  } catch (error) {\n    const errorMessage =\n      error instanceof Error\n        ? formatSpawnError(error, \"droid\")\n        : \"Unknown error\";\n    const stderrContent = stderrBuffer.trim();\n    const fullError = stderrContent\n      ? `${errorMessage}\\n\\nstderr:\\n${stderrContent}`\n      : errorMessage;\n    yield { type: \"error\", content: fullError };\n    yield { type: \"done\", content: \"\" };\n  }\n};\n\nconst abortDroidAgent = (sessionId: string) => {\n  const activeProcess = activeProcesses.get(sessionId);\n  if (activeProcess && !activeProcess.killed) {\n    activeProcess.kill(\"SIGTERM\");\n  }\n  activeProcesses.delete(sessionId);\n};\n\nconst undoDroidAgent = async (): Promise<void> => {\n  if (!lastDroidSessionId) {\n    return;\n  }\n\n  try {\n    const droidArgs = [\n      \"exec\",\n      \"--output-format\",\n      \"stream-json\",\n      \"--auto\",\n      \"low\",\n      \"--session-id\",\n      lastDroidSessionId,\n    ];\n\n    const workspacePath = process.env.REACT_GRAB_CWD ?? process.cwd();\n\n    const droidProcess = execa(\"droid\", droidArgs, {\n      stdin: \"pipe\",\n      stdout: \"pipe\",\n      stderr: \"pipe\",\n      env: { ...process.env },\n      cwd: workspacePath,\n    });\n\n    if (droidProcess.stdin) {\n      droidProcess.stdin.write(\"undo the last change you made\");\n      droidProcess.stdin.end();\n    }\n\n    await droidProcess;\n  } catch (error) {\n    const errorMessage =\n      error instanceof Error\n        ? formatSpawnError(error, \"droid\")\n        : \"Unknown error\";\n    throw new Error(`Undo failed: ${errorMessage}`);\n  }\n};\n\nexport const droidAgentHandler: AgentHandler = {\n  agentId: \"droid\",\n  run: runDroidAgent,\n  abort: abortDroidAgent,\n  undo: undoDroidAgent,\n};\n"
  },
  {
    "path": "packages/provider-droid/src/server.ts",
    "content": "import { startProviderServer } from \"@react-grab/relay\";\nimport { droidAgentHandler } from \"./handler.js\";\n\nexport const startServer = () => {\n  startProviderServer(\"droid\", droidAgentHandler);\n};\n"
  },
  {
    "path": "packages/provider-droid/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"noEmit\": true,\n    \"types\": [\"node\"]\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/provider-droid/tsup.config.ts",
    "content": "import fs from \"node:fs\";\nimport module from \"node:module\";\nimport { defineConfig } from \"tsup\";\n\nconst packageJson = JSON.parse(fs.readFileSync(\"package.json\", \"utf8\")) as {\n  version: string;\n};\n\nexport default defineConfig([\n  {\n    entry: {\n      handler: \"./src/handler.ts\",\n      cli: \"./src/cli.ts\",\n      server: \"./src/server.ts\",\n    },\n    format: [\"cjs\", \"esm\"],\n    dts: true,\n    clean: false,\n    splitting: false,\n    sourcemap: false,\n    target: \"node18\",\n    platform: \"node\",\n    treeshake: true,\n    noExternal: [/.*/],\n    external: [\n      ...module.builtinModules,\n      ...module.builtinModules.map((name) => `node:${name}`),\n    ],\n    env: {\n      VERSION: process.env.VERSION ?? packageJson.version,\n    },\n  },\n  {\n    entry: {\n      client: \"./src/client.ts\",\n    },\n    format: [\"cjs\", \"esm\"],\n    dts: true,\n    clean: false,\n    splitting: false,\n    sourcemap: false,\n    target: \"esnext\",\n    platform: \"browser\",\n    treeshake: true,\n    noExternal: [\"@react-grab/relay\"],\n  },\n  {\n    entry: [\"./src/client.ts\"],\n    format: [\"iife\"],\n    globalName: \"ReactGrabDroid\",\n    outExtension: () => ({ js: \".global.js\" }),\n    dts: false,\n    clean: false,\n    minify: process.env.NODE_ENV === \"production\",\n    splitting: false,\n    sourcemap: false,\n    target: \"esnext\",\n    platform: \"browser\",\n    treeshake: true,\n    noExternal: [/.*/],\n  },\n]);\n"
  },
  {
    "path": "packages/provider-gemini/CHANGELOG.md",
    "content": "# @react-grab/gemini\n\n## 0.1.28\n\n### Patch Changes\n\n- fix\n- Updated dependencies\n  - react-grab@0.1.28\n  - @react-grab/relay@0.1.28\n\n## 0.1.27\n\n### Patch Changes\n\n- fix: install instructions\n- Updated dependencies\n  - react-grab@0.1.27\n  - @react-grab/relay@0.1.27\n\n## 0.1.26\n\n### Patch Changes\n\n- fix: minor tweaks\n- Updated dependencies\n  - react-grab@0.1.26\n  - @react-grab/relay@0.1.26\n\n## 0.1.25\n\n### Patch Changes\n\n- fix: primtiives\n- Updated dependencies\n  - react-grab@0.1.25\n  - @react-grab/relay@0.1.25\n\n## 0.1.24\n\n### Patch Changes\n\n- primitives\n- Updated dependencies\n  - react-grab@0.1.24\n  - @react-grab/relay@0.1.24\n\n## 0.1.23\n\n### Patch Changes\n\n- fix: npx command\n- Updated dependencies\n  - react-grab@0.1.23\n  - @react-grab/relay@0.1.23\n\n## 0.1.22\n\n### Patch Changes\n\n- fix: freezing\n- Updated dependencies\n  - react-grab@0.1.22\n  - @react-grab/relay@0.1.22\n\n## 0.1.21\n\n### Patch Changes\n\n- fix: up and down selection\n- Updated dependencies\n  - react-grab@0.1.21\n  - @react-grab/relay@0.1.21\n\n## 0.1.20\n\n### Patch Changes\n\n- fix: selection performacne\n- Updated dependencies\n  - react-grab@0.1.20\n  - @react-grab/relay@0.1.20\n\n## 0.1.19\n\n### Patch Changes\n\n- fix: gsap\n- Updated dependencies\n  - react-grab@0.1.19\n  - @react-grab/relay@0.1.19\n\n## 0.1.18\n\n### Patch Changes\n\n- fix: minor issues\n- Updated dependencies\n  - react-grab@0.1.18\n  - @react-grab/relay@0.1.18\n\n## 0.1.17\n\n### Patch Changes\n\n- fix: mcp\n- Updated dependencies\n  - react-grab@0.1.17\n  - @react-grab/relay@0.1.17\n\n## 0.1.16\n\n### Patch Changes\n\n- fix: environment detection\n- Updated dependencies\n  - react-grab@0.1.16\n  - @react-grab/relay@0.1.16\n\n## 0.1.15\n\n### Patch Changes\n\n- fix: animations and ux\n- Updated dependencies\n  - react-grab@0.1.15\n  - @react-grab/relay@0.1.15\n\n## 0.1.14\n\n### Patch Changes\n\n- fix: improve recent UX\n- Updated dependencies\n  - react-grab@0.1.14\n  - @react-grab/relay@0.1.14\n\n## 0.1.13\n\n### Patch Changes\n\n- fix MCP client injection\n- Updated dependencies\n  - react-grab@0.1.13\n  - @react-grab/relay@0.1.13\n\n## 0.1.12\n\n### Patch Changes\n\n- feat: MCP\n- Updated dependencies\n  - react-grab@0.1.12\n  - @react-grab/relay@0.1.12\n\n## 0.1.11\n\n### Patch Changes\n\n- fix: claude code provider\n- Updated dependencies\n  - react-grab@0.1.11\n  - @react-grab/relay@0.1.11\n\n## 0.1.10\n\n### Patch Changes\n\n- feat: cdn in cli\n- Updated dependencies\n  - react-grab@0.1.10\n  - @react-grab/relay@0.1.10\n\n## 0.1.9\n\n### Patch Changes\n\n- fix: startServer not exported in providers\n- Updated dependencies\n  - react-grab@0.1.9\n  - @react-grab/relay@0.1.9\n\n## 0.1.8\n\n### Patch Changes\n\n- fix: providers not detaching\n- Updated dependencies\n  - react-grab@0.1.8\n  - @react-grab/relay@0.1.8\n\n## 0.1.7\n\n### Patch Changes\n\n- fix: react freezing safety\n- Updated dependencies\n  - react-grab@0.1.7\n  - @react-grab/relay@0.1.7\n\n## 0.1.6\n\n### Patch Changes\n\n- fix: compat with React Scan\n- Updated dependencies\n  - react-grab@0.1.6\n  - @react-grab/relay@0.1.6\n\n## 0.1.5\n\n### Patch Changes\n\n- fix: fullscreen inset the root\n- Updated dependencies\n  - react-grab@0.1.5\n  - @react-grab/relay@0.1.5\n\n## 0.1.4\n\n### Patch Changes\n\n- fix: improve cli edge cases\n- Updated dependencies\n  - react-grab@0.1.4\n  - @react-grab/relay@0.1.4\n\n## 0.1.3\n\n### Patch Changes\n\n- fix: @react-grab/utils not publishing\n- Updated dependencies\n  - react-grab@0.1.3\n  - @react-grab/relay@0.1.3\n\n## 0.1.2\n\n### Patch Changes\n\n- fix: packages not being published\n- Updated dependencies\n  - react-grab@0.1.2\n  - @react-grab/relay@0.1.2\n\n## 0.1.1\n\n### Patch Changes\n\n- fix: clicking element after keyboard navigation\n- Updated dependencies\n  - react-grab@0.1.1\n  - @react-grab/relay@0.1.1\n\n## 0.1.0\n\n### Minor Changes\n\n- 81adb50: feat: browser\n\n### Patch Changes\n\n- 81adb50: fix: shell script\n- fb2b037: fix: cli\n- a3d5a94: fix: cli global install\n- 81adb50: feat: react support\n- 81adb50: fix: a11y\n- a5e7a6a: fix: optimize loading speed of cli\n- 90af3f6: fix: CLI hanging\n- 81adb50: fix: shell script\n- 78efee2: fix: cli\n- 074e593: fix: cli\n- 5cd3709: fix: decouple browser out from react-grab\n- 54c4867: ui improvements\n- Updated dependencies [81adb50]\n- Updated dependencies [fb2b037]\n- Updated dependencies [a3d5a94]\n- Updated dependencies [81adb50]\n- Updated dependencies [81adb50]\n- Updated dependencies [616d3e8]\n- Updated dependencies [81adb50]\n- Updated dependencies [a5e7a6a]\n- Updated dependencies [90af3f6]\n- Updated dependencies [81adb50]\n- Updated dependencies [81adb50]\n- Updated dependencies [78efee2]\n- Updated dependencies [074e593]\n- Updated dependencies [5cd3709]\n- Updated dependencies [54c4867]\n  - react-grab@0.1.0\n  - @react-grab/relay@0.1.0\n\n## 0.1.0-beta.13\n\n### Patch Changes\n\n- ui improvements\n- Updated dependencies [616d3e8]\n- Updated dependencies\n  - react-grab@0.1.0-beta.13\n  - @react-grab/relay@0.1.0-beta.13\n\n## 0.1.0-beta.12\n\n### Patch Changes\n\n- fix: decouple browser out from react-grab\n- Updated dependencies\n  - react-grab@0.1.0-beta.12\n  - @react-grab/relay@0.1.0-beta.12\n\n## 0.1.0-beta.11\n\n### Patch Changes\n\n- fix: cli global install\n- Updated dependencies\n  - react-grab@0.1.0-beta.11\n  - @react-grab/relay@0.1.0-beta.11\n\n## 0.1.0-beta.10\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.10\n  - @react-grab/relay@0.1.0-beta.10\n\n## 0.1.0-beta.9\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.9\n  - @react-grab/relay@0.1.0-beta.9\n\n## 0.1.0-beta.8\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.8\n  - @react-grab/relay@0.1.0-beta.8\n\n## 0.1.0-beta.7\n\n### Patch Changes\n\n- fix: CLI hanging\n- Updated dependencies\n  - react-grab@0.1.0-beta.7\n  - @react-grab/relay@0.1.0-beta.7\n\n## 0.1.0-beta.6\n\n### Patch Changes\n\n- fix: optimize loading speed of cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.6\n  - @react-grab/relay@0.1.0-beta.6\n\n## 0.1.0-beta.5\n\n### Patch Changes\n\n- fix: a11y\n- Updated dependencies\n  - react-grab@0.1.0-beta.5\n  - @react-grab/relay@0.1.0-beta.5\n\n## 0.1.0-beta.4\n\n### Patch Changes\n\n- feat: react support\n- Updated dependencies\n  - react-grab@0.1.0-beta.4\n  - @react-grab/relay@0.1.0-beta.4\n\n## 0.1.0-beta.3\n\n### Patch Changes\n\n- Updated dependencies\n  - react-grab@0.1.0-beta.3\n\n## 0.1.0-beta.2\n\n### Patch Changes\n\n- fix: shell script\n- Updated dependencies\n  - react-grab@0.1.0-beta.2\n  - @react-grab/relay@0.1.0-beta.2\n\n## 0.1.0-beta.1\n\n### Patch Changes\n\n- fix: shell script\n- Updated dependencies\n  - react-grab@0.1.0-beta.1\n  - @react-grab/relay@0.1.0-beta.1\n\n## 0.1.0-beta.0\n\n### Minor Changes\n\n- feat: browser\n\n### Patch Changes\n\n- Updated dependencies\n  - react-grab@0.1.0-beta.0\n  - @react-grab/relay@0.1.0-beta.0\n\n## 0.0.98\n\n### Patch Changes\n\n- feat: new state architecture and context menu\n- Updated dependencies\n  - react-grab@0.0.98\n\n## 0.0.97\n\n### Patch Changes\n\n- fix: sourcemap error\n- Updated dependencies\n  - react-grab@0.0.97\n\n## 0.0.96\n\n### Patch Changes\n\n- fix: fiber access timeout handling\n- Updated dependencies\n  - react-grab@0.0.96\n\n## 0.0.95\n\n### Patch Changes\n\n- fix: selecting buttons with disabled states\n- Updated dependencies\n  - react-grab@0.0.95\n\n## 0.0.94\n\n### Patch Changes\n\n- fix: browser crashing on selection bug\n- Updated dependencies\n  - react-grab@0.0.94\n\n## 0.0.93\n\n### Patch Changes\n\n- fix: copying not working\n- Updated dependencies\n  - react-grab@0.0.93\n\n## 0.0.92\n\n### Patch Changes\n\n- refactor: use state machines instead of signals\n- Updated dependencies\n  - react-grab@0.0.92\n\n## 0.0.91\n\n### Patch Changes\n\n- feat: dock\n- Updated dependencies\n  - react-grab@0.0.91\n\n## 0.0.90\n\n### Patch Changes\n\n- fix: check visual edit endpoint\n- Updated dependencies\n  - react-grab@0.0.90\n\n## 0.0.89\n\n### Patch Changes\n\n- fix: many bugfixes\n- Updated dependencies\n  - react-grab@0.0.89\n\n## 0.0.88\n\n### Patch Changes\n\n- fix: deprecation errors\n- Updated dependencies\n  - react-grab@0.0.88\n\n## 0.0.87\n\n### Patch Changes\n\n- feat: visual edits\n- Updated dependencies\n  - react-grab@0.0.87\n\n## 0.0.86\n\n### Patch Changes\n\n- fix: editing\n- Updated dependencies\n  - react-grab@0.0.86\n\n## 0.0.85\n\n### Patch Changes\n\n- fix: check versions on each provider\n- Updated dependencies\n  - react-grab@0.0.85\n\n## 0.0.84\n\n### Patch Changes\n\n- fix: migrate from cross-spawn to execa to fix deprecation issue\n- Updated dependencies\n  - react-grab@0.0.84\n\n## 0.0.83\n\n### Patch Changes\n\n- feat: timings during agent processing\n- Updated dependencies\n  - react-grab@0.0.83\n\n## 0.0.82\n\n### Patch Changes\n\n- fix: agent support\n- Updated dependencies\n  - react-grab@0.0.82\n\n## 0.0.81\n\n### Patch Changes\n\n- 3b47e87: feat: add Google Gemini CLI provider with streaming support\n- feat: codex and gemini support\n- Updated dependencies\n  - react-grab@0.0.81\n\n## 0.0.80\n\n### Patch Changes\n\n- feat: add Google Gemini CLI provider with streaming support\n- Updated dependencies\n  - react-grab@0.0.80\n"
  },
  {
    "path": "packages/provider-gemini/README.md",
    "content": "# @react-grab/gemini\n\nGoogle Gemini CLI provider for React Grab.\n\n## Installation\n\n```bash\nnpm install @react-grab/gemini\n```\n\n## Usage\n\n### Server\n\nThe server runs on port `8567` and interfaces with the Gemini CLI. Add to your `package.json`:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"npx @react-grab/gemini@latest && next dev\"\n  }\n}\n```\n\n> **Note:** You must have [Gemini CLI](https://github.com/google-gemini/gemini-cli) installed (`npm i -g @anthropic-ai/gemini-cli`).\n\n### Client\n\nAdd the client script to your HTML:\n\n```html\n<script src=\"//unpkg.com/@react-grab/gemini/dist/client.global.js\"></script>\n```\n\nOr import programmatically:\n\n```ts\nimport \"@react-grab/gemini/client\";\n```\n\n## Features\n\n- **Follow-ups**: Continue conversations with the same session\n- **Streaming**: Real-time status updates during execution\n- **Tool calls**: See tool usage as it happens\n"
  },
  {
    "path": "packages/provider-gemini/package.json",
    "content": "{\n  \"name\": \"@react-grab/gemini\",\n  \"version\": \"0.1.28\",\n  \"bin\": {\n    \"react-grab-gemini\": \"./dist/cli.cjs\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"type\": \"module\",\n  \"browser\": \"dist/client.global.js\",\n  \"exports\": {\n    \"./client\": {\n      \"types\": \"./dist/client.d.ts\",\n      \"import\": \"./dist/client.js\",\n      \"require\": \"./dist/client.cjs\"\n    },\n    \"./handler\": {\n      \"types\": \"./dist/handler.d.ts\",\n      \"import\": \"./dist/handler.js\",\n      \"require\": \"./dist/handler.cjs\"\n    },\n    \"./server\": {\n      \"types\": \"./dist/server.d.ts\",\n      \"import\": \"./dist/server.js\",\n      \"require\": \"./dist/server.cjs\"\n    },\n    \"./dist/*\": \"./dist/*.js\",\n    \"./dist/*.js\": \"./dist/*.js\"\n  },\n  \"scripts\": {\n    \"dev\": \"tsup --watch\",\n    \"build\": \"rm -rf dist && NODE_ENV=production tsup\"\n  },\n  \"dependencies\": {\n    \"@react-grab/relay\": \"workspace:*\",\n    \"execa\": \"^9.6.0\",\n    \"react-grab\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@react-grab/utils\": \"workspace:*\",\n    \"@types/node\": \"^22.10.7\",\n    \"tsup\": \"^8.4.0\"\n  }\n}\n"
  },
  {
    "path": "packages/provider-gemini/src/cli.ts",
    "content": "#!/usr/bin/env node\nimport { spawn } from \"node:child_process\";\nimport { realpathSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\nconst realScriptPath = realpathSync(process.argv[1]);\nconst scriptDir = dirname(realScriptPath);\nconst serverPath = join(scriptDir, \"server.cjs\");\n\nconst child = spawn(\n  process.execPath,\n  [\"-e\", `require(${JSON.stringify(serverPath)}).startServer()`],\n  {\n    detached: true,\n    stdio: \"inherit\",\n  },\n);\n\nchild.unref();\nprocess.exit(0);\n"
  },
  {
    "path": "packages/provider-gemini/src/client.ts",
    "content": "import type { AgentCompleteResult } from \"react-grab/core\";\nimport { createProviderClientPlugin } from \"@react-grab/relay/client\";\n\nexport type { AgentCompleteResult };\n\nconst { createAgentProvider: createGeminiAgentProvider, attachAgent } =\n  createProviderClientPlugin({\n    agentId: \"gemini\",\n    pluginName: \"gemini-agent\",\n    actionId: \"edit-with-gemini\",\n    actionLabel: \"Edit with Gemini\",\n  });\n\nexport { createGeminiAgentProvider, attachAgent };\n\nattachAgent();\n"
  },
  {
    "path": "packages/provider-gemini/src/handler.ts",
    "content": "import { execa, type ResultPromise } from \"execa\";\nimport type {\n  AgentHandler,\n  AgentMessage,\n  AgentRunOptions,\n} from \"@react-grab/relay\";\nimport { COMPLETED_STATUS } from \"@react-grab/relay\";\nimport { formatSpawnError } from \"@react-grab/utils/server\";\n\nexport interface GeminiAgentOptions extends AgentRunOptions {\n  model?: string;\n  includeDirectories?: string;\n}\n\ninterface GeminiStreamEvent {\n  type: \"init\" | \"message\" | \"tool_use\" | \"tool_result\" | \"error\" | \"result\";\n  role?: \"user\" | \"assistant\";\n  content?: string;\n  tool_name?: string;\n  tool_id?: string;\n  parameters?: Record<string, unknown>;\n  status?: \"success\" | \"error\";\n  output?: string;\n  session_id?: string;\n  stats?: Record<string, unknown>;\n  timestamp?: string;\n  delta?: boolean;\n}\n\nconst geminiSessionMap = new Map<string, string>();\nconst activeProcesses = new Map<string, ResultPromise>();\nlet lastGeminiSessionId: string | undefined;\n\nconst parseStreamLine = (line: string): GeminiStreamEvent | null => {\n  const trimmed = line.trim();\n  if (!trimmed) return null;\n\n  try {\n    return JSON.parse(trimmed) as GeminiStreamEvent;\n  } catch {\n    return null;\n  }\n};\n\nconst runGeminiAgent = async function* (\n  prompt: string,\n  options?: GeminiAgentOptions,\n): AsyncGenerator<AgentMessage> {\n  const geminiArgs = [\"--output-format\", \"stream-json\", \"--yolo\"];\n\n  if (options?.model) {\n    geminiArgs.push(\"--model\", options.model);\n  }\n\n  if (options?.includeDirectories) {\n    geminiArgs.push(\"--include-directories\", options.includeDirectories);\n  }\n\n  geminiArgs.push(prompt);\n\n  let geminiProcess: ResultPromise | undefined;\n  let stderrBuffer = \"\";\n\n  try {\n    yield { type: \"status\", content: \"Thinking…\" };\n\n    geminiProcess = execa(\"gemini\", geminiArgs, {\n      stdin: \"pipe\",\n      stdout: \"pipe\",\n      stderr: \"pipe\",\n      env: { ...process.env },\n      cwd: options?.cwd ?? process.env.REACT_GRAB_CWD ?? process.cwd(),\n    });\n\n    if (options?.sessionId) {\n      activeProcesses.set(options.sessionId, geminiProcess);\n    }\n\n    if (geminiProcess.stderr) {\n      geminiProcess.stderr.on(\"data\", (chunk: Buffer) => {\n        stderrBuffer += chunk.toString();\n      });\n    }\n\n    const messageQueue: AgentMessage[] = [];\n    let resolveWait: (() => void) | null = null;\n    let processEnded = false;\n    let capturedSessionId: string | undefined;\n\n    const enqueueMessage = (message: AgentMessage) => {\n      messageQueue.push(message);\n      if (resolveWait) {\n        resolveWait();\n        resolveWait = null;\n      }\n    };\n\n    const processLine = (line: string) => {\n      const event = parseStreamLine(line);\n      if (!event) return;\n\n      if (!capturedSessionId && event.session_id) {\n        capturedSessionId = event.session_id;\n      }\n\n      switch (event.type) {\n        case \"init\":\n          enqueueMessage({ type: \"status\", content: \"Session started...\" });\n          break;\n\n        case \"message\":\n          if (event.role === \"assistant\" && event.content) {\n            enqueueMessage({ type: \"status\", content: event.content });\n          }\n          break;\n\n        case \"tool_use\":\n          if (event.tool_name) {\n            enqueueMessage({\n              type: \"status\",\n              content: `Using ${event.tool_name}...`,\n            });\n          }\n          break;\n\n        case \"tool_result\":\n          if (event.status === \"error\" && event.output) {\n            enqueueMessage({\n              type: \"status\",\n              content: `Tool error: ${event.output}`,\n            });\n          }\n          break;\n\n        case \"error\":\n          if (event.content) {\n            enqueueMessage({ type: \"error\", content: event.content });\n          }\n          break;\n\n        case \"result\":\n          if (event.status === \"success\") {\n            enqueueMessage({ type: \"status\", content: COMPLETED_STATUS });\n          } else if (event.status === \"error\") {\n            enqueueMessage({ type: \"error\", content: \"Task failed\" });\n          }\n          break;\n      }\n    };\n\n    let buffer = \"\";\n\n    if (geminiProcess.stdout) {\n      geminiProcess.stdout.on(\"data\", (chunk: Buffer) => {\n        buffer += chunk.toString();\n\n        let newlineIndex;\n        while ((newlineIndex = buffer.indexOf(\"\\n\")) !== -1) {\n          const line = buffer.slice(0, newlineIndex);\n          buffer = buffer.slice(newlineIndex + 1);\n          processLine(line);\n        }\n      });\n    }\n\n    const childProcess = geminiProcess;\n    childProcess.on(\"close\", (code) => {\n      if (options?.sessionId) {\n        activeProcesses.delete(options.sessionId);\n      }\n      if (buffer.trim()) {\n        processLine(buffer);\n      }\n      if (options?.sessionId && capturedSessionId) {\n        geminiSessionMap.set(options.sessionId, capturedSessionId);\n      }\n      if (capturedSessionId) {\n        lastGeminiSessionId = capturedSessionId;\n      }\n      processEnded = true;\n      if (code !== 0 && !childProcess.killed) {\n        enqueueMessage({\n          type: \"error\",\n          content: `gemini exited with code ${code}`,\n        });\n      }\n      enqueueMessage({ type: \"done\", content: \"\" });\n      if (resolveWait) {\n        resolveWait();\n        resolveWait = null;\n      }\n    });\n\n    childProcess.on(\"error\", (error) => {\n      if (options?.sessionId) {\n        activeProcesses.delete(options.sessionId);\n      }\n      processEnded = true;\n      const isNotInstalled = \"code\" in error && error.code === \"ENOENT\";\n      if (isNotInstalled) {\n        enqueueMessage({\n          type: \"error\",\n          content:\n            \"gemini CLI is not installed. Please install the Gemini CLI to use this provider.\\n\\nInstallation: https://github.com/google-gemini/gemini-cli\",\n        });\n      } else {\n        const errorMessage = formatSpawnError(error, \"gemini\");\n        const stderrContent = stderrBuffer.trim();\n        const fullError = stderrContent\n          ? `${errorMessage}\\n\\nstderr:\\n${stderrContent}`\n          : errorMessage;\n        enqueueMessage({ type: \"error\", content: fullError });\n      }\n      enqueueMessage({ type: \"done\", content: \"\" });\n      if (resolveWait) {\n        resolveWait();\n        resolveWait = null;\n      }\n    });\n\n    while (true) {\n      if (options?.signal?.aborted) {\n        if (geminiProcess && !geminiProcess.killed) {\n          geminiProcess.kill(\"SIGTERM\");\n        }\n        return;\n      }\n\n      if (messageQueue.length > 0) {\n        const message = messageQueue.shift()!;\n        if (message.type === \"done\") {\n          yield message;\n          return;\n        }\n        yield message;\n      } else if (processEnded) {\n        return;\n      } else {\n        await new Promise<void>((resolve) => {\n          resolveWait = resolve;\n        });\n      }\n    }\n  } catch (error) {\n    const errorMessage =\n      error instanceof Error\n        ? formatSpawnError(error, \"gemini\")\n        : \"Unknown error\";\n    const stderrContent = stderrBuffer.trim();\n    const fullError = stderrContent\n      ? `${errorMessage}\\n\\nstderr:\\n${stderrContent}`\n      : errorMessage;\n    yield { type: \"error\", content: fullError };\n    yield { type: \"done\", content: \"\" };\n  }\n};\n\nconst abortGeminiAgent = (sessionId: string) => {\n  const activeProcess = activeProcesses.get(sessionId);\n  if (activeProcess && !activeProcess.killed) {\n    activeProcess.kill(\"SIGTERM\");\n    activeProcesses.delete(sessionId);\n  }\n};\n\nconst undoGeminiAgent = async (): Promise<void> => {\n  if (!lastGeminiSessionId) {\n    return;\n  }\n\n  try {\n    const geminiArgs = [\n      \"--output-format\",\n      \"stream-json\",\n      \"--yolo\",\n      \"--session\",\n      lastGeminiSessionId,\n      \"undo\",\n    ];\n\n    await execa(\"gemini\", geminiArgs, {\n      stdout: \"pipe\",\n      stderr: \"pipe\",\n      env: { ...process.env },\n      cwd: process.env.REACT_GRAB_CWD ?? process.cwd(),\n    });\n  } catch (error) {\n    const errorMessage =\n      error instanceof Error\n        ? formatSpawnError(error, \"gemini\")\n        : \"Unknown error\";\n    throw new Error(`Undo failed: ${errorMessage}`);\n  }\n};\n\nexport const geminiAgentHandler: AgentHandler = {\n  agentId: \"gemini\",\n  run: runGeminiAgent,\n  abort: abortGeminiAgent,\n  undo: undoGeminiAgent,\n};\n"
  },
  {
    "path": "packages/provider-gemini/src/server.ts",
    "content": "import { startProviderServer } from \"@react-grab/relay\";\nimport { geminiAgentHandler } from \"./handler.js\";\n\nexport const startServer = () => {\n  startProviderServer(\"gemini\", geminiAgentHandler);\n};\n"
  },
  {
    "path": "packages/provider-gemini/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"esModuleInterop\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"declaration\": true,\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\",\n    \"types\": [\"node\"]\n  },\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/provider-gemini/tsup.config.ts",
    "content": "import fs from \"node:fs\";\nimport module from \"node:module\";\nimport { defineConfig } from \"tsup\";\n\nconst packageJson = JSON.parse(fs.readFileSync(\"package.json\", \"utf8\")) as {\n  version: string;\n};\n\nexport default defineConfig([\n  {\n    entry: {\n      handler: \"./src/handler.ts\",\n      cli: \"./src/cli.ts\",\n      server: \"./src/server.ts\",\n    },\n    format: [\"cjs\", \"esm\"],\n    dts: true,\n    clean: false,\n    splitting: false,\n    sourcemap: false,\n    target: \"node18\",\n    platform: \"node\",\n    treeshake: true,\n    noExternal: [/.*/],\n    external: [\n      ...module.builtinModules,\n      ...module.builtinModules.map((name) => `node:${name}`),\n    ],\n    env: {\n      VERSION: process.env.VERSION ?? packageJson.version,\n    },\n  },\n  {\n    entry: {\n      client: \"./src/client.ts\",\n    },\n    format: [\"cjs\", \"esm\"],\n    dts: true,\n    clean: false,\n    splitting: false,\n    sourcemap: false,\n    target: \"esnext\",\n    platform: \"browser\",\n    treeshake: true,\n    noExternal: [\"@react-grab/relay\"],\n  },\n  {\n    entry: [\"./src/client.ts\"],\n    format: [\"iife\"],\n    globalName: \"ReactGrabGemini\",\n    outExtension: () => ({ js: \".global.js\" }),\n    dts: false,\n    clean: false,\n    minify: process.env.NODE_ENV === \"production\",\n    splitting: false,\n    sourcemap: false,\n    target: \"esnext\",\n    platform: \"browser\",\n    treeshake: true,\n    noExternal: [/.*/],\n  },\n]);\n"
  },
  {
    "path": "packages/provider-opencode/CHANGELOG.md",
    "content": "# @react-grab/opencode\n\n## 0.1.28\n\n### Patch Changes\n\n- fix\n- Updated dependencies\n  - react-grab@0.1.28\n  - @react-grab/relay@0.1.28\n\n## 0.1.27\n\n### Patch Changes\n\n- fix: install instructions\n- Updated dependencies\n  - react-grab@0.1.27\n  - @react-grab/relay@0.1.27\n\n## 0.1.26\n\n### Patch Changes\n\n- fix: minor tweaks\n- Updated dependencies\n  - react-grab@0.1.26\n  - @react-grab/relay@0.1.26\n\n## 0.1.25\n\n### Patch Changes\n\n- fix: primtiives\n- Updated dependencies\n  - react-grab@0.1.25\n  - @react-grab/relay@0.1.25\n\n## 0.1.24\n\n### Patch Changes\n\n- primitives\n- Updated dependencies\n  - react-grab@0.1.24\n  - @react-grab/relay@0.1.24\n\n## 0.1.23\n\n### Patch Changes\n\n- fix: npx command\n- Updated dependencies\n  - react-grab@0.1.23\n  - @react-grab/relay@0.1.23\n\n## 0.1.22\n\n### Patch Changes\n\n- fix: freezing\n- Updated dependencies\n  - react-grab@0.1.22\n  - @react-grab/relay@0.1.22\n\n## 0.1.21\n\n### Patch Changes\n\n- fix: up and down selection\n- Updated dependencies\n  - react-grab@0.1.21\n  - @react-grab/relay@0.1.21\n\n## 0.1.20\n\n### Patch Changes\n\n- fix: selection performacne\n- Updated dependencies\n  - react-grab@0.1.20\n  - @react-grab/relay@0.1.20\n\n## 0.1.19\n\n### Patch Changes\n\n- fix: gsap\n- Updated dependencies\n  - react-grab@0.1.19\n  - @react-grab/relay@0.1.19\n\n## 0.1.18\n\n### Patch Changes\n\n- fix: minor issues\n- Updated dependencies\n  - react-grab@0.1.18\n  - @react-grab/relay@0.1.18\n\n## 0.1.17\n\n### Patch Changes\n\n- fix: mcp\n- Updated dependencies\n  - react-grab@0.1.17\n  - @react-grab/relay@0.1.17\n\n## 0.1.16\n\n### Patch Changes\n\n- fix: environment detection\n- Updated dependencies\n  - react-grab@0.1.16\n  - @react-grab/relay@0.1.16\n\n## 0.1.15\n\n### Patch Changes\n\n- fix: animations and ux\n- Updated dependencies\n  - react-grab@0.1.15\n  - @react-grab/relay@0.1.15\n\n## 0.1.14\n\n### Patch Changes\n\n- fix: improve recent UX\n- Updated dependencies\n  - react-grab@0.1.14\n  - @react-grab/relay@0.1.14\n\n## 0.1.13\n\n### Patch Changes\n\n- fix MCP client injection\n- Updated dependencies\n  - react-grab@0.1.13\n  - @react-grab/relay@0.1.13\n\n## 0.1.12\n\n### Patch Changes\n\n- feat: MCP\n- Updated dependencies\n  - react-grab@0.1.12\n  - @react-grab/relay@0.1.12\n\n## 0.1.11\n\n### Patch Changes\n\n- fix: claude code provider\n- Updated dependencies\n  - react-grab@0.1.11\n  - @react-grab/relay@0.1.11\n\n## 0.1.10\n\n### Patch Changes\n\n- feat: cdn in cli\n- Updated dependencies\n  - react-grab@0.1.10\n  - @react-grab/relay@0.1.10\n\n## 0.1.9\n\n### Patch Changes\n\n- fix: startServer not exported in providers\n- Updated dependencies\n  - react-grab@0.1.9\n  - @react-grab/relay@0.1.9\n\n## 0.1.8\n\n### Patch Changes\n\n- fix: providers not detaching\n- Updated dependencies\n  - react-grab@0.1.8\n  - @react-grab/relay@0.1.8\n\n## 0.1.7\n\n### Patch Changes\n\n- fix: react freezing safety\n- Updated dependencies\n  - react-grab@0.1.7\n  - @react-grab/relay@0.1.7\n\n## 0.1.6\n\n### Patch Changes\n\n- fix: compat with React Scan\n- Updated dependencies\n  - react-grab@0.1.6\n  - @react-grab/relay@0.1.6\n\n## 0.1.5\n\n### Patch Changes\n\n- fix: fullscreen inset the root\n- Updated dependencies\n  - react-grab@0.1.5\n  - @react-grab/relay@0.1.5\n\n## 0.1.4\n\n### Patch Changes\n\n- fix: improve cli edge cases\n- Updated dependencies\n  - react-grab@0.1.4\n  - @react-grab/relay@0.1.4\n\n## 0.1.3\n\n### Patch Changes\n\n- fix: @react-grab/utils not publishing\n- Updated dependencies\n  - react-grab@0.1.3\n  - @react-grab/relay@0.1.3\n\n## 0.1.2\n\n### Patch Changes\n\n- fix: packages not being published\n- Updated dependencies\n  - react-grab@0.1.2\n  - @react-grab/relay@0.1.2\n\n## 0.1.1\n\n### Patch Changes\n\n- fix: clicking element after keyboard navigation\n- Updated dependencies\n  - react-grab@0.1.1\n  - @react-grab/relay@0.1.1\n\n## 0.1.0\n\n### Minor Changes\n\n- 81adb50: feat: browser\n\n### Patch Changes\n\n- 81adb50: fix: shell script\n- fb2b037: fix: cli\n- a3d5a94: fix: cli global install\n- 81adb50: feat: react support\n- 81adb50: fix: a11y\n- a5e7a6a: fix: optimize loading speed of cli\n- 90af3f6: fix: CLI hanging\n- 81adb50: fix: shell script\n- 78efee2: fix: cli\n- 074e593: fix: cli\n- 5cd3709: fix: decouple browser out from react-grab\n- 54c4867: ui improvements\n- Updated dependencies [81adb50]\n- Updated dependencies [fb2b037]\n- Updated dependencies [a3d5a94]\n- Updated dependencies [81adb50]\n- Updated dependencies [81adb50]\n- Updated dependencies [616d3e8]\n- Updated dependencies [81adb50]\n- Updated dependencies [a5e7a6a]\n- Updated dependencies [90af3f6]\n- Updated dependencies [81adb50]\n- Updated dependencies [81adb50]\n- Updated dependencies [78efee2]\n- Updated dependencies [074e593]\n- Updated dependencies [5cd3709]\n- Updated dependencies [54c4867]\n  - react-grab@0.1.0\n  - @react-grab/relay@0.1.0\n\n## 0.1.0-beta.13\n\n### Patch Changes\n\n- ui improvements\n- Updated dependencies [616d3e8]\n- Updated dependencies\n  - react-grab@0.1.0-beta.13\n  - @react-grab/relay@0.1.0-beta.13\n\n## 0.1.0-beta.12\n\n### Patch Changes\n\n- fix: decouple browser out from react-grab\n- Updated dependencies\n  - react-grab@0.1.0-beta.12\n  - @react-grab/relay@0.1.0-beta.12\n\n## 0.1.0-beta.11\n\n### Patch Changes\n\n- fix: cli global install\n- Updated dependencies\n  - react-grab@0.1.0-beta.11\n  - @react-grab/relay@0.1.0-beta.11\n\n## 0.1.0-beta.10\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.10\n  - @react-grab/relay@0.1.0-beta.10\n\n## 0.1.0-beta.9\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.9\n  - @react-grab/relay@0.1.0-beta.9\n\n## 0.1.0-beta.8\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.8\n  - @react-grab/relay@0.1.0-beta.8\n\n## 0.1.0-beta.7\n\n### Patch Changes\n\n- fix: CLI hanging\n- Updated dependencies\n  - react-grab@0.1.0-beta.7\n  - @react-grab/relay@0.1.0-beta.7\n\n## 0.1.0-beta.6\n\n### Patch Changes\n\n- fix: optimize loading speed of cli\n- Updated dependencies\n  - react-grab@0.1.0-beta.6\n  - @react-grab/relay@0.1.0-beta.6\n\n## 0.1.0-beta.5\n\n### Patch Changes\n\n- fix: a11y\n- Updated dependencies\n  - react-grab@0.1.0-beta.5\n  - @react-grab/relay@0.1.0-beta.5\n\n## 0.1.0-beta.4\n\n### Patch Changes\n\n- feat: react support\n- Updated dependencies\n  - react-grab@0.1.0-beta.4\n  - @react-grab/relay@0.1.0-beta.4\n\n## 0.1.0-beta.3\n\n### Patch Changes\n\n- Updated dependencies\n  - react-grab@0.1.0-beta.3\n\n## 0.1.0-beta.2\n\n### Patch Changes\n\n- fix: shell script\n- Updated dependencies\n  - react-grab@0.1.0-beta.2\n  - @react-grab/relay@0.1.0-beta.2\n\n## 0.1.0-beta.1\n\n### Patch Changes\n\n- fix: shell script\n- Updated dependencies\n  - react-grab@0.1.0-beta.1\n  - @react-grab/relay@0.1.0-beta.1\n\n## 0.1.0-beta.0\n\n### Minor Changes\n\n- feat: browser\n\n### Patch Changes\n\n- Updated dependencies\n  - react-grab@0.1.0-beta.0\n  - @react-grab/relay@0.1.0-beta.0\n\n## 0.0.98\n\n### Patch Changes\n\n- feat: new state architecture and context menu\n- Updated dependencies\n  - react-grab@0.0.98\n\n## 0.0.97\n\n### Patch Changes\n\n- fix: sourcemap error\n- Updated dependencies\n  - react-grab@0.0.97\n\n## 0.0.96\n\n### Patch Changes\n\n- fix: fiber access timeout handling\n- Updated dependencies\n  - react-grab@0.0.96\n\n## 0.0.95\n\n### Patch Changes\n\n- fix: selecting buttons with disabled states\n- Updated dependencies\n  - react-grab@0.0.95\n\n## 0.0.94\n\n### Patch Changes\n\n- fix: browser crashing on selection bug\n- Updated dependencies\n  - react-grab@0.0.94\n\n## 0.0.93\n\n### Patch Changes\n\n- fix: copying not working\n- Updated dependencies\n  - react-grab@0.0.93\n\n## 0.0.92\n\n### Patch Changes\n\n- refactor: use state machines instead of signals\n- Updated dependencies\n  - react-grab@0.0.92\n\n## 0.0.91\n\n### Patch Changes\n\n- feat: dock\n- Updated dependencies\n  - react-grab@0.0.91\n\n## 0.0.90\n\n### Patch Changes\n\n- fix: check visual edit endpoint\n- Updated dependencies\n  - react-grab@0.0.90\n\n## 0.0.89\n\n### Patch Changes\n\n- fix: many bugfixes\n- Updated dependencies\n  - react-grab@0.0.89\n\n## 0.0.88\n\n### Patch Changes\n\n- fix: deprecation errors\n- Updated dependencies\n  - react-grab@0.0.88\n\n## 0.0.87\n\n### Patch Changes\n\n- feat: visual edits\n- Updated dependencies\n  - react-grab@0.0.87\n\n## 0.0.86\n\n### Patch Changes\n\n- fix: editing\n- Updated dependencies\n  - react-grab@0.0.86\n\n## 0.0.85\n\n### Patch Changes\n\n- fix: check versions on each provider\n- Updated dependencies\n  - react-grab@0.0.85\n\n## 0.0.84\n\n### Patch Changes\n\n- fix: migrate from cross-spawn to execa to fix deprecation issue\n- Updated dependencies\n  - react-grab@0.0.84\n\n## 0.0.83\n\n### Patch Changes\n\n- feat: timings during agent processing\n- Updated dependencies\n  - react-grab@0.0.83\n\n## 0.0.82\n\n### Patch Changes\n\n- fix: agent support\n- Updated dependencies\n  - react-grab@0.0.82\n\n## 0.0.81\n\n### Patch Changes\n\n- feat: codex and gemini support\n- Updated dependencies\n  - react-grab@0.0.81\n\n## 0.0.80\n\n### Patch Changes\n\n- fix: replies and undo\n- Updated dependencies\n  - react-grab@0.0.80\n\n## 0.0.79\n\n### Patch Changes\n\n- fix: claude code exit issue\n- Updated dependencies\n  - react-grab@0.0.79\n\n## 0.0.78\n\n### Patch Changes\n\n- fix: cancel animation\n- Updated dependencies\n  - react-grab@0.0.78\n\n## 0.0.77\n\n### Patch Changes\n\n- fix: new cli proxying\n- Updated dependencies\n  - react-grab@0.0.77\n\n## 0.0.76\n\n### Patch Changes\n\n- feat: allow CLI under react-grab namespace\n- Updated dependencies\n  - react-grab@0.0.76\n\n## 0.0.75\n\n### Patch Changes\n\n- fix: issue with Illegal Invocation on next.js pages\n- Updated dependencies\n  - react-grab@0.0.75\n\n## 0.0.74\n\n### Patch Changes\n\n- fix: updateOptions\n- Updated dependencies\n  - react-grab@0.0.74\n\n## 0.0.73\n\n### Patch Changes\n\n- fix: improve cli\n- Updated dependencies\n  - react-grab@0.0.73\n\n## 0.0.72\n\n### Patch Changes\n\n- fix: shimmer effect\n- Updated dependencies\n  - react-grab@0.0.72\n\n## 0.0.71\n\n### Patch Changes\n\n- fix: ux nits\n- Updated dependencies\n  - react-grab@0.0.71\n\n## 0.0.70\n\n### Patch Changes\n\n- fix: react-grab cli flow when agents is used\n- Updated dependencies\n  - react-grab@0.0.70\n\n## 0.0.69\n\n### Patch Changes\n\n- fix: CLI on script tag\n- Updated dependencies\n  - react-grab@0.0.69\n\n## 0.0.68\n\n### Patch Changes\n\n- feat: opencode and cli installer\n- Updated dependencies\n  - react-grab@0.0.68\n"
  },
  {
    "path": "packages/provider-opencode/README.md",
    "content": "# @react-grab/opencode\n\nOpenCode agent provider for React Grab. Requires running a local server that interfaces with the OpenCode CLI.\n\n## Installation\n\n```bash\nnpm install @react-grab/opencode\n# or\npnpm add @react-grab/opencode\n# or\nbun add @react-grab/opencode\n# or\nyarn add @react-grab/opencode\n```\n\n## Server Setup\n\nThe server runs on port `6567` by default.\n\n### Quick Start (CLI)\n\nStart the server in the background before running your dev server:\n\n```bash\nnpx @react-grab/opencode@latest && pnpm run dev\n```\n\nThe server will run as a detached background process. **Note:** Stopping your dev server (Ctrl+C) won't stop the React Grab server. To stop it:\n\n```bash\npkill -f \"react-grab.*server\"\n```\n\n### Recommended: Config File (Automatic Lifecycle)\n\nFor better lifecycle management, start the server from your config file. This ensures the server stops when your dev server stops:\n\n### Vite\n\n```ts\n// vite.config.ts\nimport { startServer } from \"@react-grab/opencode/server\";\n\nif (process.env.NODE_ENV === \"development\") {\n  startServer();\n}\n```\n\n### Next.js\n\n```ts\n// next.config.ts\nimport { startServer } from \"@react-grab/opencode/server\";\n\nif (process.env.NODE_ENV === \"development\") {\n  startServer();\n}\n```\n\n> **Note:** You must have [OpenCode](https://opencode.ai) installed (`npm i -g opencode-ai@latest`).\n\n## Client Usage\n\n### Script Tag\n\n```html\n<script src=\"//unpkg.com/react-grab/dist/index.global.js\"></script>\n<script src=\"//unpkg.com/@react-grab/opencode/dist/client.global.js\"></script>\n```\n\n### Next.js\n\nUsing the `Script` component in your `app/layout.tsx`:\n\n```jsx\nimport Script from \"next/script\";\n\nexport default function RootLayout({ children }) {\n  return (\n    <html>\n      <head>\n        {process.env.NODE_ENV === \"development\" && (\n          <>\n            <Script\n              src=\"//unpkg.com/react-grab/dist/index.global.js\"\n              strategy=\"beforeInteractive\"\n            />\n            <Script\n              src=\"//unpkg.com/@react-grab/opencode/dist/client.global.js\"\n              strategy=\"lazyOnload\"\n            />\n          </>\n        )}\n      </head>\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\n### ES Module\n\n```tsx\nimport { attachAgent } from \"@react-grab/opencode/client\";\n\nattachAgent();\n```\n\n## Options\n\nYou can configure the OpenCode agent provider:\n\n```typescript\nimport { createOpenCodeAgentProvider } from \"@react-grab/opencode/client\";\n\nconst provider = createOpenCodeAgentProvider({\n  serverUrl: \"http://localhost:6567\", // Custom server URL\n  getOptions: () => ({\n    model: \"claude-sonnet-4-20250514\", // AI model to use\n    agent: \"build\", // Agent type: \"build\" or \"plan\"\n    directory: \"/path/to/project\", // Project directory\n  }),\n});\n```\n\n## How It Works\n\n```\n┌─────────────────┐      HTTP       ┌─────────────────┐     stdin      ┌─────────────────┐\n│                 │  localhost:6567 │                 │                │                 │\n│   React Grab    │ ──────────────► │     Server      │ ─────────────► │    opencode     │\n│    (Browser)    │ ◄────────────── │   (Node.js)     │ ◄───────────── │      (CLI)      │\n│                 │       SSE       │                 │     stdout     │                 │\n└─────────────────┘                 └─────────────────┘                └─────────────────┘\n      Client                              Server                            Agent\n```\n\n1. **React Grab** sends the selected element context to the server via HTTP POST\n2. **Server** receives the request and spawns the `opencode` CLI process\n3. **OpenCode** processes the request and streams JSON responses to stdout\n4. **Server** relays status updates to the client via Server-Sent Events (SSE)\n"
  },
  {
    "path": "packages/provider-opencode/package.json",
    "content": "{\n  \"name\": \"@react-grab/opencode\",\n  \"version\": \"0.1.28\",\n  \"bin\": {\n    \"react-grab-opencode\": \"./dist/cli.cjs\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"type\": \"module\",\n  \"browser\": \"dist/client.global.js\",\n  \"exports\": {\n    \"./client\": {\n      \"types\": \"./dist/client.d.ts\",\n      \"import\": \"./dist/client.js\",\n      \"require\": \"./dist/client.cjs\"\n    },\n    \"./handler\": {\n      \"types\": \"./dist/handler.d.ts\",\n      \"import\": \"./dist/handler.js\",\n      \"require\": \"./dist/handler.cjs\"\n    },\n    \"./server\": {\n      \"types\": \"./dist/server.d.ts\",\n      \"import\": \"./dist/server.js\",\n      \"require\": \"./dist/server.cjs\"\n    },\n    \"./dist/*\": \"./dist/*.js\",\n    \"./dist/*.js\": \"./dist/*.js\"\n  },\n  \"scripts\": {\n    \"dev\": \"tsup --watch\",\n    \"build\": \"rm -rf dist && NODE_ENV=production tsup\"\n  },\n  \"dependencies\": {\n    \"@opencode-ai/sdk\": \"^1.0.132\",\n    \"@react-grab/relay\": \"workspace:*\",\n    \"fkill\": \"^9.0.0\",\n    \"react-grab\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@react-grab/utils\": \"workspace:*\",\n    \"@types/node\": \"^22.10.7\",\n    \"tsup\": \"^8.4.0\"\n  }\n}\n"
  },
  {
    "path": "packages/provider-opencode/src/cli.ts",
    "content": "#!/usr/bin/env node\nimport { spawn } from \"node:child_process\";\nimport { realpathSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\nconst realScriptPath = realpathSync(process.argv[1]);\nconst scriptDir = dirname(realScriptPath);\nconst serverPath = join(scriptDir, \"server.cjs\");\n\nconst child = spawn(\n  process.execPath,\n  [\"-e\", `require(${JSON.stringify(serverPath)}).startServer()`],\n  {\n    detached: true,\n    stdio: \"inherit\",\n  },\n);\n\nchild.unref();\nprocess.exit(0);\n"
  },
  {
    "path": "packages/provider-opencode/src/client.ts",
    "content": "import type { AgentCompleteResult } from \"react-grab/core\";\nimport { createProviderClientPlugin } from \"@react-grab/relay/client\";\n\nexport type { AgentCompleteResult };\n\nconst { createAgentProvider: createOpenCodeAgentProvider, attachAgent } =\n  createProviderClientPlugin({\n    agentId: \"opencode\",\n    pluginName: \"opencode-agent\",\n    actionId: \"edit-with-opencode\",\n    actionLabel: \"Edit with OpenCode\",\n  });\n\nexport { createOpenCodeAgentProvider, attachAgent };\n\nattachAgent();\n"
  },
  {
    "path": "packages/provider-opencode/src/constants.ts",
    "content": "export const OPENCODE_SDK_PORT = 4096;\nexport const STATUS_TEXT_TRUNCATE_LENGTH = 100;\n"
  },
  {
    "path": "packages/provider-opencode/src/handler.ts",
    "content": "import { createOpencode } from \"@opencode-ai/sdk\";\nimport fkill from \"fkill\";\nimport type {\n  AgentHandler,\n  AgentMessage,\n  AgentRunOptions,\n} from \"@react-grab/relay\";\nimport { COMPLETED_STATUS, POST_KILL_DELAY_MS } from \"@react-grab/relay\";\nimport { sleep } from \"@react-grab/utils/server\";\nimport { OPENCODE_SDK_PORT, STATUS_TEXT_TRUNCATE_LENGTH } from \"./constants.js\";\n\nexport interface OpenCodeAgentOptions extends AgentRunOptions {\n  model?: string;\n  agent?: string;\n  directory?: string;\n}\n\ninterface OpenCodeInstance {\n  client: Awaited<ReturnType<typeof createOpencode>>[\"client\"];\n  server: Awaited<ReturnType<typeof createOpencode>>[\"server\"];\n}\n\ninterface OpenCodeEvent {\n  type: string;\n  properties?: {\n    sessionID?: string;\n    messageID?: string;\n    part?: {\n      type: string;\n      text?: string;\n      state?: string;\n      toolName?: string;\n      sessionID?: string;\n      messageID?: string;\n    };\n  };\n}\n\ninterface LastMessageInfo {\n  sessionId: string;\n  messageId: string;\n}\n\nlet opencodeInstance: OpenCodeInstance | null = null;\nlet initializationPromise: Promise<OpenCodeInstance> | null = null;\nconst sessionMap = new Map<string, string>();\nconst abortedSessions = new Set<string>();\nlet lastMessageInfo: LastMessageInfo | undefined;\n\nconst getOpenCodeClient = async () => {\n  if (opencodeInstance) {\n    return opencodeInstance.client;\n  }\n\n  if (!initializationPromise) {\n    initializationPromise = (async () => {\n      await fkill(`:${OPENCODE_SDK_PORT}`, { force: true, silent: true }).catch(\n        () => {},\n      );\n      await sleep(POST_KILL_DELAY_MS);\n      const instance = await createOpencode({\n        hostname: \"127.0.0.1\",\n        port: OPENCODE_SDK_PORT,\n      });\n      opencodeInstance = instance;\n      return instance;\n    })();\n  }\n\n  try {\n    const instance = await initializationPromise;\n    return instance.client;\n  } catch (error) {\n    initializationPromise = null;\n    throw error;\n  }\n};\n\nconst executeOpenCodePrompt = async (\n  prompt: string,\n  options?: OpenCodeAgentOptions,\n  onStatus?: (text: string) => void,\n  reactGrabSessionId?: string,\n  signal?: { aborted: boolean },\n): Promise<string> => {\n  const client = await getOpenCodeClient();\n\n  onStatus?.(\"Thinking…\");\n\n  let opencodeSessionId: string;\n\n  if (reactGrabSessionId && sessionMap.has(reactGrabSessionId)) {\n    opencodeSessionId = sessionMap.get(reactGrabSessionId)!;\n  } else {\n    const sessionResponse = await client.session.create({\n      body: { title: \"React Grab Session\" },\n    });\n\n    if (sessionResponse.error || !sessionResponse.data) {\n      throw new Error(\"Failed to create session\");\n    }\n\n    opencodeSessionId = sessionResponse.data.id;\n\n    if (reactGrabSessionId) {\n      sessionMap.set(reactGrabSessionId, opencodeSessionId);\n    }\n  }\n\n  const modelConfig = options?.model\n    ? {\n        providerID: options.model.split(\"/\")[0],\n        modelID: options.model.split(\"/\")[1] || options.model,\n      }\n    : undefined;\n\n  const eventStreamResult = await client.event.subscribe();\n\n  await client.session.promptAsync({\n    path: { id: opencodeSessionId },\n    body: {\n      ...(modelConfig && { model: modelConfig }),\n      parts: [{ type: \"text\", text: prompt }],\n    },\n  });\n\n  for await (const event of eventStreamResult.stream) {\n    if (signal?.aborted) break;\n\n    const eventData = event as OpenCodeEvent;\n\n    if (eventData.type === \"session.idle\") {\n      const idleSessionId = eventData.properties?.sessionID;\n      if (idleSessionId === opencodeSessionId) {\n        break;\n      }\n    }\n\n    if (\n      eventData.type === \"message.part.updated\" &&\n      eventData.properties?.part\n    ) {\n      const part = eventData.properties.part;\n\n      if (part.sessionID !== opencodeSessionId) continue;\n\n      if (part.messageID) {\n        lastMessageInfo = {\n          sessionId: opencodeSessionId,\n          messageId: part.messageID,\n        };\n      }\n\n      if (part.type === \"text\" && part.text) {\n        const truncatedText =\n          part.text.length > STATUS_TEXT_TRUNCATE_LENGTH\n            ? `${part.text.slice(0, STATUS_TEXT_TRUNCATE_LENGTH)}...`\n            : part.text;\n        onStatus?.(truncatedText);\n      } else if (part.type === \"tool-invocation\" && part.toolName) {\n        const stateLabel = part.state === \"running\" ? \"Running\" : \"Using\";\n        onStatus?.(`${stateLabel} ${part.toolName}`);\n      }\n    }\n  }\n\n  return opencodeSessionId;\n};\n\nconst runOpenCodeAgent = async function* (\n  prompt: string,\n  options?: OpenCodeAgentOptions,\n): AsyncGenerator<AgentMessage> {\n  const sessionId = options?.sessionId;\n  const signal = { aborted: false };\n\n  const isAborted = () => {\n    if (options?.signal?.aborted) {\n      signal.aborted = true;\n      return true;\n    }\n    if (sessionId && abortedSessions.has(sessionId)) {\n      signal.aborted = true;\n      return true;\n    }\n    return false;\n  };\n\n  const messageQueue: AgentMessage[] = [];\n  let resolveWait: (() => void) | null = null;\n\n  const enqueueMessage = (message: AgentMessage) => {\n    messageQueue.push(message);\n    if (resolveWait) {\n      resolveWait();\n      resolveWait = null;\n    }\n  };\n\n  try {\n    const executePromise = executeOpenCodePrompt(\n      prompt,\n      options,\n      (text) => {\n        if (!isAborted()) {\n          enqueueMessage({ type: \"status\", content: text });\n        }\n      },\n      sessionId,\n      signal,\n    );\n\n    let isDone = false;\n\n    executePromise\n      .then(() => {\n        if (!isAborted()) {\n          enqueueMessage({ type: \"status\", content: COMPLETED_STATUS });\n          enqueueMessage({ type: \"done\", content: \"\" });\n        }\n        isDone = true;\n        if (resolveWait) {\n          resolveWait();\n          resolveWait = null;\n        }\n      })\n      .catch((error) => {\n        if (!isAborted()) {\n          const errorMessage =\n            error instanceof Error ? error.message : \"Unknown error\";\n          const stderr =\n            error instanceof Error && \"stderr\" in error\n              ? String(error.stderr)\n              : undefined;\n          const fullError =\n            stderr && stderr.trim()\n              ? `${errorMessage}\\n\\nstderr:\\n${stderr.trim()}`\n              : errorMessage;\n          enqueueMessage({ type: \"error\", content: fullError });\n          enqueueMessage({ type: \"done\", content: \"\" });\n        }\n        isDone = true;\n        if (resolveWait) {\n          resolveWait();\n          resolveWait = null;\n        }\n      });\n\n    while (true) {\n      if (isAborted()) {\n        return;\n      }\n\n      if (messageQueue.length > 0) {\n        const message = messageQueue.shift()!;\n        if (message.type === \"done\") {\n          yield message;\n          return;\n        }\n        yield message;\n      } else if (isDone) {\n        return;\n      } else {\n        await new Promise<void>((resolve) => {\n          resolveWait = resolve;\n        });\n      }\n    }\n  } finally {\n    if (sessionId) {\n      abortedSessions.delete(sessionId);\n    }\n  }\n};\n\nconst abortOpenCodeAgent = (sessionId: string) => {\n  abortedSessions.add(sessionId);\n};\n\nconst undoOpenCodeAgent = async (): Promise<void> => {\n  if (!lastMessageInfo) {\n    return;\n  }\n\n  const client = await getOpenCodeClient();\n\n  await client.session.revert({\n    path: { id: lastMessageInfo.sessionId },\n    body: { messageID: lastMessageInfo.messageId },\n  });\n};\n\nexport const openCodeAgentHandler: AgentHandler = {\n  agentId: \"opencode\",\n  run: runOpenCodeAgent,\n  abort: abortOpenCodeAgent,\n  undo: undoOpenCodeAgent,\n};\n"
  },
  {
    "path": "packages/provider-opencode/src/server.ts",
    "content": "import { startProviderServer } from \"@react-grab/relay\";\nimport { openCodeAgentHandler } from \"./handler.js\";\n\nexport const startServer = () => {\n  startProviderServer(\"opencode\", openCodeAgentHandler);\n};\n"
  },
  {
    "path": "packages/provider-opencode/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"noEmit\": true,\n    \"types\": [\"node\"]\n  },\n  \"include\": [\"src\", \"tsup.config.ts\", \"src/server.ts\"]\n}\n"
  },
  {
    "path": "packages/provider-opencode/tsup.config.ts",
    "content": "import fs from \"node:fs\";\nimport module from \"node:module\";\nimport { defineConfig } from \"tsup\";\n\nconst packageJson = JSON.parse(fs.readFileSync(\"package.json\", \"utf8\")) as {\n  version: string;\n};\n\nexport default defineConfig([\n  {\n    entry: {\n      handler: \"./src/handler.ts\",\n      cli: \"./src/cli.ts\",\n      server: \"./src/server.ts\",\n    },\n    format: [\"cjs\", \"esm\"],\n    dts: true,\n    clean: false,\n    splitting: false,\n    sourcemap: false,\n    target: \"node18\",\n    platform: \"node\",\n    treeshake: true,\n    noExternal: [/.*/],\n    external: [\n      ...module.builtinModules,\n      ...module.builtinModules.map((name) => `node:${name}`),\n    ],\n    env: {\n      VERSION: process.env.VERSION ?? packageJson.version,\n    },\n  },\n  {\n    entry: {\n      client: \"./src/client.ts\",\n    },\n    format: [\"cjs\", \"esm\"],\n    dts: true,\n    clean: false,\n    splitting: false,\n    sourcemap: false,\n    target: \"esnext\",\n    platform: \"browser\",\n    treeshake: true,\n    noExternal: [\"@react-grab/relay\"],\n  },\n  {\n    entry: [\"./src/client.ts\"],\n    format: [\"iife\"],\n    globalName: \"ReactGrabOpenCode\",\n    outExtension: () => ({ js: \".global.js\" }),\n    dts: false,\n    clean: false,\n    minify: process.env.NODE_ENV === \"production\",\n    splitting: false,\n    sourcemap: false,\n    target: \"esnext\",\n    platform: \"browser\",\n    treeshake: true,\n    noExternal: [/.*/],\n  },\n]);\n"
  },
  {
    "path": "packages/react-grab/.oxlintrc.json",
    "content": "{\n  \"$schema\": \"./node_modules/oxlint/configuration_schema.json\",\n  \"plugins\": [\"typescript\"],\n  \"settings\": {\n    \"typescript\": {\n      \"tsconfigPath\": \"./tsconfig.json\"\n    }\n  },\n  \"categories\": {\n    \"correctness\": \"off\"\n  },\n  \"env\": {\n    \"builtin\": true\n  },\n  \"ignorePatterns\": [\n    \"node_modules/**\",\n    \"dist/**\",\n    \"eslint.config.mjs\",\n    \"bundled_*.mjs\",\n    \"*.mjs\",\n    \"*.cjs\",\n    \"*.js\",\n    \"*.json\",\n    \"*.md\",\n    \"bin/cli.js\"\n  ],\n  \"rules\": {\n    \"@typescript-eslint/ban-ts-comment\": \"error\",\n    \"no-array-constructor\": \"error\",\n    \"@typescript-eslint/no-duplicate-enum-values\": \"error\",\n    \"@typescript-eslint/no-empty-object-type\": \"error\",\n    \"@typescript-eslint/no-explicit-any\": \"error\",\n    \"@typescript-eslint/no-extra-non-null-assertion\": \"error\",\n    \"@typescript-eslint/no-misused-new\": \"error\",\n    \"@typescript-eslint/no-namespace\": \"error\",\n    \"@typescript-eslint/no-non-null-asserted-optional-chain\": \"error\",\n    \"@typescript-eslint/no-require-imports\": \"error\",\n    \"@typescript-eslint/no-this-alias\": \"error\",\n    \"@typescript-eslint/no-unnecessary-type-constraint\": \"error\",\n    \"@typescript-eslint/no-unsafe-declaration-merging\": \"error\",\n    \"@typescript-eslint/no-unsafe-function-type\": \"error\",\n    \"no-unused-expressions\": \"error\",\n    \"no-unused-vars\": \"error\",\n    \"@typescript-eslint/no-wrapper-object-types\": \"error\",\n    \"@typescript-eslint/prefer-as-const\": \"error\",\n    \"@typescript-eslint/prefer-namespace-keyword\": \"error\",\n    \"@typescript-eslint/triple-slash-reference\": \"error\",\n    \"@typescript-eslint/await-thenable\": \"error\",\n    \"@typescript-eslint/no-array-delete\": \"error\",\n    \"@typescript-eslint/no-base-to-string\": \"error\",\n    \"@typescript-eslint/no-duplicate-type-constituents\": \"error\",\n    \"@typescript-eslint/no-floating-promises\": \"error\",\n    \"@typescript-eslint/no-for-in-array\": \"error\",\n    \"@typescript-eslint/no-implied-eval\": \"error\",\n    \"@typescript-eslint/no-misused-promises\": \"error\",\n    \"@typescript-eslint/no-redundant-type-constituents\": \"error\",\n    \"@typescript-eslint/no-unnecessary-type-assertion\": \"error\",\n    \"@typescript-eslint/no-unsafe-argument\": \"error\",\n    \"@typescript-eslint/no-unsafe-assignment\": \"error\",\n    \"@typescript-eslint/no-unsafe-call\": \"error\",\n    \"@typescript-eslint/no-unsafe-enum-comparison\": \"error\",\n    \"@typescript-eslint/no-unsafe-member-access\": \"error\",\n    \"@typescript-eslint/no-unsafe-return\": \"error\",\n    \"@typescript-eslint/no-unsafe-unary-minus\": \"error\",\n    \"@typescript-eslint/only-throw-error\": \"error\",\n    \"@typescript-eslint/prefer-promise-reject-errors\": \"error\",\n    \"@typescript-eslint/require-await\": \"error\",\n    \"@typescript-eslint/restrict-plus-operands\": \"error\",\n    \"@typescript-eslint/restrict-template-expressions\": \"error\",\n    \"@typescript-eslint/unbound-method\": \"error\"\n  },\n  \"overrides\": [\n    {\n      \"files\": [\"**/*.ts\", \"**/*.tsx\", \"**/*.mts\", \"**/*.cts\"],\n      \"rules\": {\n        \"constructor-super\": \"off\",\n        \"no-class-assign\": \"off\",\n        \"no-const-assign\": \"off\",\n        \"no-dupe-class-members\": \"off\",\n        \"no-dupe-keys\": \"off\",\n        \"no-func-assign\": \"off\",\n        \"no-import-assign\": \"off\",\n        \"no-new-native-nonconstructor\": \"off\",\n        \"no-obj-calls\": \"off\",\n        \"no-redeclare\": \"off\",\n        \"no-setter-return\": \"off\",\n        \"no-this-before-super\": \"off\",\n        \"no-unsafe-negation\": \"off\",\n        \"no-var\": \"error\",\n        \"no-with\": \"off\",\n        \"prefer-rest-params\": \"error\",\n        \"prefer-spread\": \"error\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/react-grab/CHANGELOG.md",
    "content": "# react-grab\n\n## 0.1.28\n\n### Patch Changes\n\n- fix\n- Updated dependencies\n  - @react-grab/cli@0.1.28\n\n## 0.1.27\n\n### Patch Changes\n\n- fix: install instructions\n- Updated dependencies\n  - @react-grab/cli@0.1.27\n\n## 0.1.26\n\n### Patch Changes\n\n- fix: minor tweaks\n- Updated dependencies\n  - @react-grab/cli@0.1.26\n\n## 0.1.25\n\n### Patch Changes\n\n- fix: primtiives\n- Updated dependencies\n  - @react-grab/cli@0.1.25\n\n## 0.1.24\n\n### Patch Changes\n\n- primitives\n- Updated dependencies\n  - @react-grab/cli@0.1.24\n\n## 0.1.23\n\n### Patch Changes\n\n- fix: npx command\n- Updated dependencies\n  - @react-grab/cli@0.1.23\n\n## 0.1.22\n\n### Patch Changes\n\n- fix: freezing\n\n## 0.1.21\n\n### Patch Changes\n\n- fix: up and down selection\n\n## 0.1.20\n\n### Patch Changes\n\n- fix: selection performacne\n\n## 0.1.19\n\n### Patch Changes\n\n- fix: gsap\n\n## 0.1.18\n\n### Patch Changes\n\n- fix: minor issues\n\n## 0.1.17\n\n### Patch Changes\n\n- fix: mcp\n\n## 0.1.16\n\n### Patch Changes\n\n- fix: environment detection\n\n## 0.1.15\n\n### Patch Changes\n\n- fix: animations and ux\n\n## 0.1.14\n\n### Patch Changes\n\n- fix: improve recent UX\n\n## 0.1.13\n\n### Patch Changes\n\n- fix MCP client injection\n\n## 0.1.12\n\n### Patch Changes\n\n- feat: MCP\n\n## 0.1.11\n\n### Patch Changes\n\n- fix: claude code provider\n\n## 0.1.10\n\n### Patch Changes\n\n- feat: cdn in cli\n\n## 0.1.9\n\n### Patch Changes\n\n- fix: startServer not exported in providers\n\n## 0.1.8\n\n### Patch Changes\n\n- fix: providers not detaching\n\n## 0.1.7\n\n### Patch Changes\n\n- fix: react freezing safety\n\n## 0.1.6\n\n### Patch Changes\n\n- fix: compat with React Scan\n\n## 0.1.5\n\n### Patch Changes\n\n- fix: fullscreen inset the root\n\n## 0.1.4\n\n### Patch Changes\n\n- fix: improve cli edge cases\n\n## 0.1.3\n\n### Patch Changes\n\n- fix: @react-grab/utils not publishing\n\n## 0.1.2\n\n### Patch Changes\n\n- fix: packages not being published\n\n## 0.1.1\n\n### Patch Changes\n\n- fix: clicking element after keyboard navigation\n\n## 0.1.0\n\n### Minor Changes\n\n- 81adb50: feat: browser\n\n### Patch Changes\n\n- 81adb50: fix: shell script\n- fb2b037: fix: cli\n- a3d5a94: fix: cli global install\n- 81adb50: feat: react support\n- 81adb50: fix: use matching CLI version for prerelease builds\n- 616d3e8: fix: prevent form submission during IME composition\n\n  When typing CJK (Chinese, Japanese, Korean) characters using IME, pressing Enter to confirm character selection no longer incorrectly submits the form. Added `event.isComposing` check to skip form submission during active IME composition.\n\n- 81adb50: fix: a11y\n- a5e7a6a: fix: optimize loading speed of cli\n- 90af3f6: fix: CLI hanging\n- 81adb50: fix: shell script\n- 78efee2: fix: cli\n- 074e593: fix: cli\n- 5cd3709: fix: decouple browser out from react-grab\n- 54c4867: ui improvements\n\n## 0.1.0-beta.13\n\n### Patch Changes\n\n- 616d3e8: fix: prevent form submission during IME composition\n\n  When typing CJK (Chinese, Japanese, Korean) characters using IME, pressing Enter to confirm character selection no longer incorrectly submits the form. Added `event.isComposing` check to skip form submission during active IME composition.\n\n- ui improvements\n\n## 0.1.0-beta.12\n\n### Patch Changes\n\n- fix: decouple browser out from react-grab\n\n## 0.1.0-beta.11\n\n### Patch Changes\n\n- fix: cli global install\n\n## 0.1.0-beta.10\n\n### Patch Changes\n\n- fix: cli\n\n## 0.1.0-beta.9\n\n### Patch Changes\n\n- fix: cli\n\n## 0.1.0-beta.8\n\n### Patch Changes\n\n- fix: cli\n\n## 0.1.0-beta.7\n\n### Patch Changes\n\n- fix: CLI hanging\n\n## 0.1.0-beta.6\n\n### Patch Changes\n\n- fix: optimize loading speed of cli\n\n## 0.1.0-beta.5\n\n### Patch Changes\n\n- fix: a11y\n\n## 0.1.0-beta.4\n\n### Patch Changes\n\n- feat: react support\n\n## 0.1.0-beta.3\n\n### Patch Changes\n\n- fix: use matching CLI version for prerelease builds\n\n## 0.1.0-beta.2\n\n### Patch Changes\n\n- fix: shell script\n\n## 0.1.0-beta.1\n\n### Patch Changes\n\n- fix: shell script\n\n## 0.1.0-beta.0\n\n### Minor Changes\n\n- feat: browser\n\n## 0.0.98\n\n### Patch Changes\n\n- feat: new state architecture and context menu\n\n## 0.0.97\n\n### Patch Changes\n\n- fix: sourcemap error\n\n## 0.0.96\n\n### Patch Changes\n\n- fix: fiber access timeout handling\n\n## 0.0.95\n\n### Patch Changes\n\n- fix: selecting buttons with disabled states\n\n## 0.0.94\n\n### Patch Changes\n\n- fix: browser crashing on selection bug\n\n## 0.0.93\n\n### Patch Changes\n\n- fix: copying not working\n\n## 0.0.92\n\n### Patch Changes\n\n- refactor: use state machines instead of signals\n\n## 0.0.91\n\n### Patch Changes\n\n- feat: dock\n\n## 0.0.90\n\n### Patch Changes\n\n- fix: check visual edit endpoint\n\n## 0.0.89\n\n### Patch Changes\n\n- fix: many bugfixes\n\n## 0.0.88\n\n### Patch Changes\n\n- fix: deprecation errors\n\n## 0.0.87\n\n### Patch Changes\n\n- feat: visual edits\n\n## 0.0.86\n\n### Patch Changes\n\n- fix: editing\n\n## 0.0.85\n\n### Patch Changes\n\n- fix: check versions on each provider\n\n## 0.0.84\n\n### Patch Changes\n\n- fix: migrate from cross-spawn to execa to fix deprecation issue\n\n## 0.0.83\n\n### Patch Changes\n\n- feat: timings during agent processing\n\n## 0.0.82\n\n### Patch Changes\n\n- fix: agent support\n\n## 0.0.81\n\n### Patch Changes\n\n- feat: codex and gemini support\n\n## 0.0.80\n\n### Patch Changes\n\n- fix: replies and undo\n\n## 0.0.79\n\n### Patch Changes\n\n- fix: claude code exit issue\n\n## 0.0.78\n\n### Patch Changes\n\n- fix: cancel animation\n\n## 0.0.77\n\n### Patch Changes\n\n- fix: new cli proxying\n\n## 0.0.76\n\n### Patch Changes\n\n- feat: allow CLI under react-grab namespace\n\n## 0.0.75\n\n### Patch Changes\n\n- fix: issue with Illegal Invocation on next.js pages\n\n## 0.0.74\n\n### Patch Changes\n\n- fix: updateOptions\n\n## 0.0.73\n\n### Patch Changes\n\n- fix: improve cli\n\n## 0.0.72\n\n### Patch Changes\n\n- fix: shimmer effect\n\n## 0.0.71\n\n### Patch Changes\n\n- fix: ux nits\n\n## 0.0.70\n\n### Patch Changes\n\n- fix: react-grab cli flow when agents is used\n\n## 0.0.69\n\n### Patch Changes\n\n- fix: CLI on script tag\n\n## 0.0.68\n\n### Patch Changes\n\n- feat: opencode and cli installer\n\n## 0.0.67\n\n### Patch Changes\n\n- fix: logs\n\n## 0.0.66\n\n### Patch Changes\n\n- fix: flash animation\n\n## 0.0.65\n\n### Patch Changes\n\n- fix: instrumentation\n\n## 0.0.64\n\n### Patch Changes\n\n- fix: stream resumption\n\n## 0.0.63\n\n### Patch Changes\n\n- fix: x positioning of selection label\n\n## 0.0.62\n\n### Patch Changes\n\n- fix: stream resumption\n\n## 0.0.61\n\n### Patch Changes\n\n- fix: improved installation strategy\n\n## 0.0.60\n\n### Patch Changes\n\n- fix: loading states\n\n## 0.0.59\n\n### Patch Changes\n\n- fix: improve component name\n\n## 0.0.58\n\n### Patch Changes\n\n- fix: issues with stack\n\n## 0.0.57\n\n### Patch Changes\n\n- fix: improvements to UI\n\n## 0.0.56\n\n### Patch Changes\n\n- add Turborepo for monorepo build orchestration\n\n## 0.0.55\n\n### Patch Changes\n\n- add agent session management with abort handling and onAbort callback\n- add session progress animation and status display in AgentLabel component\n- add tagName and selectionBounds to session management for context\n- improve drag-and-drop logic with better bounds calculation for selected elements\n- add shimmer effect css animations to selection label\n- improve selection handling with frozen element for input submission\n- add copied state indicator\n- add debounced cursor visibility with SELECTION_CURSOR_SETTLE_DELAY_MS\n- add checks for editable elements to prevent cursor updates inside text areas\n- add size prop to IconToggle component for customizable dimensions\n- improve button placement logic and visibility handling in selection box\n- integrate BLUR_DEACTIVATION_THRESHOLD_MS for better activation state handling\n- add createLabelInstance function for better label instance tracking\n- improve input overlay styling with placeholder text adjustments\n- add streaming session handling and logging for session resume operations\n\n## 0.0.54\n\n### Patch Changes\n\n- disable logging by default (log: false)\n- add browser extension support\n- add script configuration options for minimal instrumentation\n- adjust state management and success label handling\n\n## 0.0.53\n\n### Patch Changes\n\n- improve focus state handling\n\n## 0.0.52\n\n### Patch Changes\n\n- improve copy state indicators\n\n## 0.0.51\n\n### Patch Changes\n\n- add detailed jsdoc comments for theme properties\n- enhance Theme interface with properties for selection box, cursor, crosshair, and labels\n\n## 0.0.50\n\n### Patch Changes\n\n- add extensibility api for custom integrations\n- increase key hold duration from 150ms to 200ms for better detection\n- improve element bounds calculation\n- add timestamp to version fetch url for cache busting\n\n## 0.0.49\n\n### Patch Changes\n\n- allow rapid re-activation of cmd+c shortcut after use\n- prevent default and stop propagation for enter key in cmd+c mode\n- improve styling and update dependencies\n\n## 0.0.48\n\n### Patch Changes\n\n- improve version fetching with timestamp parameter\n\n## 0.0.47\n\n### Patch Changes\n\n- use event.code instead of event.key for keyboard layout compatibility (dvorak, azerty, etc.)\n\n## 0.0.46\n\n### Patch Changes\n\n- improve instrumentation checks for non-react projects\n- enhance element handling in core functionality\n- fix redirect issues\n\n## 0.0.45\n\n### Patch Changes\n\n- improve input element handling and fix enter key deactivation\n- enhance clipboard functionality and grabbed box handling\n- update drag and auto-scroll constants for smoother interactions\n\n## 0.0.44\n\n### Patch Changes\n\n- add debug logging support\n\n## 0.0.43\n\n### Patch Changes\n\n- fix website implementation issues\n- improve hook implementations\n\n## 0.0.42\n\n### Patch Changes\n\n- improve cursor tracking behavior\n\n## 0.0.41\n\n### Patch Changes\n\n- code cleanup and improvements\n- improve copy version formatting\n\n## 0.0.40\n\n### Patch Changes\n\n- add text-only copy with markdown conversion using turndown\n- make cmd+c higher priority over other handlers\n- improve selection opacity handling\n- filter out Primitive. elements from instrumentation\n- remove prompt input from ReactGrabRenderer\n- update selection box styles for improved variant handling\n- fix source location detection\n\n## 0.0.39\n\n### Patch Changes\n\n- improve sourcemaps in production builds\n- make success notification follow cursor position after grabbing elements\n\n## 0.0.38\n\n### Patch Changes\n\n- add multi-select support\n- add browser extension groundwork\n\n## 0.0.37\n\n### Patch Changes\n\n- code cleanup and improvements\n\n## 0.0.36\n\n### Patch Changes\n\n- show progress indicator during copy operation\n\n## 0.0.35\n\n### Patch Changes\n\n- allow activation while cursor is inside input elements\n\n## 0.0.34\n\n### Patch Changes\n\n- improve click-through behavior and cleanup\n\n## 0.0.33\n\n### Patch Changes\n\n- major version rewrite with new crosshair design\n- code cleanup and optimizations\n\n## 0.0.32\n\n### Patch Changes\n\n- fix keybind conflict issues\n- website integration improvements\n\n## 0.0.31\n\n### Patch Changes\n\n- improve screenshot capture\n\n## 0.0.30\n\n### Patch Changes\n\n- improve instrumentation reliability\n\n## 0.0.29\n\n### Patch Changes\n\n- fix crosshair length calculation\n\n## 0.0.28\n\n### Patch Changes\n\n- add computed styles to grabbed element output\n\n## 0.0.27\n\n### Patch Changes\n\n- improve source location detection\n\n## 0.0.26\n\n### Patch Changes\n\n- improve overall performance\n\n## 0.0.25\n\n### Patch Changes\n\n- add new crosshair design\n- code cleanup\n\n## 0.0.24\n\n### Patch Changes\n\n- fix various edge cases\n\n## 0.0.23\n\n### Patch Changes\n\n- version bump\n\n## 0.0.21\n\n### Patch Changes\n\n- refactor codebase structure\n- migrate to new architecture\n\n## 0.0.20\n\n### Patch Changes\n\n- fix circular reference handling\n- enable grabbing of disabled elements (thanks @aymanch-03)\n- refactor event parameter naming in createSelectionOverlay\n\n## 0.0.19\n\n### Patch Changes\n\n- add windows and linux path support\n- prevent underlying element click handlers during overlay mode\n- improve react devtools compatibility\n\n## 0.0.18\n\n### Patch Changes\n\n- fix owner stack traversal\n\n## 0.0.17\n\n### Patch Changes\n\n- improve sourcemap support\n\n## 0.0.16\n\n### Patch Changes\n\n- improve documentation\n\n## 0.0.15\n\n### Patch Changes\n\n- various ux improvements\n\n## 0.0.14\n\n### Patch Changes\n\n- fix keyboard shortcut handling\n"
  },
  {
    "path": "packages/react-grab/README.md",
    "content": "# <img src=\"https://github.com/aidenybai/react-grab/blob/main/.github/public/logo.png?raw=true\" width=\"60\" align=\"center\" /> React Grab\n\n[![size](https://img.shields.io/bundlephobia/minzip/react-grab?label=gzip&style=flat&colorA=000000&colorB=000000)](https://bundlephobia.com/package/react-grab)\n[![version](https://img.shields.io/npm/v/react-grab?style=flat&colorA=000000&colorB=000000)](https://npmjs.com/package/react-grab)\n[![downloads](https://img.shields.io/npm/dt/react-grab.svg?style=flat&colorA=000000&colorB=000000)](https://npmjs.com/package/react-grab)\n\nSelect context for coding agents directly from your website\n\nHow? Point at any element and press **⌘C** (Mac) or **Ctrl+C** (Windows/Linux) to copy the file name, React component, and HTML source code.\n\nIt makes tools like Cursor, Claude Code, Copilot run up to [**3× faster**](https://react-grab.com/blog/intro) and more accurate.\n\n### [Try out a demo! →](https://react-grab.com)\n\n![React Grab Demo](https://github.com/aidenybai/react-grab/blob/main/packages/website/public/demo.gif?raw=true)\n\n## Install\n\nRun this command at your project root (where `next.config.ts` or `vite.config.ts` is located):\n\n```bash\nnpx -y grab@latest init\n```\n\n## Connect to MCP\n\n```bash\nnpx -y grab@latest add mcp\n```\n\n## Usage\n\nOnce installed, hover over any UI element in your browser and press:\n\n- **⌘C** (Cmd+C) on Mac\n- **Ctrl+C** on Windows/Linux\n\nThis copies the element's context (file name, React component, and HTML source code) to your clipboard ready to paste into your coding agent. For example:\n\n```js\n<a class=\"ml-auto inline-block text-sm\" href=\"#\">\n  Forgot your password?\n</a>\nin LoginForm at components/login-form.tsx:46:19\n```\n\n## Manual Installation\n\nIf you're using a React framework or build tool, view instructions below:\n\n#### Next.js (App router)\n\nAdd this inside of your `app/layout.tsx`:\n\n```jsx\nimport Script from \"next/script\";\n\nexport default function RootLayout({ children }) {\n  return (\n    <html>\n      <head>\n        {process.env.NODE_ENV === \"development\" && (\n          <Script\n            src=\"//unpkg.com/react-grab/dist/index.global.js\"\n            crossOrigin=\"anonymous\"\n            strategy=\"beforeInteractive\"\n          />\n        )}\n      </head>\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\n#### Next.js (Pages router)\n\nAdd this into your `pages/_document.tsx`:\n\n```jsx\nimport { Html, Head, Main, NextScript } from \"next/document\";\n\nexport default function Document() {\n  return (\n    <Html lang=\"en\">\n      <Head>\n        {process.env.NODE_ENV === \"development\" && (\n          <Script\n            src=\"//unpkg.com/react-grab/dist/index.global.js\"\n            crossOrigin=\"anonymous\"\n            strategy=\"beforeInteractive\"\n          />\n        )}\n      </Head>\n      <body>\n        <Main />\n        <NextScript />\n      </body>\n    </Html>\n  );\n}\n```\n\n#### Vite\n\nAdd this at the top of your main entry file (e.g., `src/main.tsx`):\n\n```tsx\nif (import.meta.env.DEV) {\n  import(\"react-grab\");\n}\n```\n\n#### Webpack\n\nFirst, install React Grab:\n\n```bash\nnpm install react-grab\n```\n\nThen add this at the top of your main entry file (e.g., `src/index.tsx` or `src/main.tsx`):\n\n```tsx\nif (process.env.NODE_ENV === \"development\") {\n  import(\"react-grab\");\n}\n```\n\n## Plugins\n\nUse plugins to extend React Grab's built-in UI with context menu actions, toolbar menu items, lifecycle hooks, and theme overrides. Plugins run within React Grab.\n\nRegister a plugin using the `registerPlugin` and `unregisterPlugin` exports:\n\n```js\nimport { registerPlugin } from \"react-grab\";\n\nregisterPlugin({\n  name: \"my-plugin\",\n  hooks: {\n    onElementSelect: (element) => {\n      console.log(\"Selected:\", element.tagName);\n    },\n  },\n});\n```\n\nIn React, register inside a `useEffect`:\n\n```jsx\nimport { registerPlugin, unregisterPlugin } from \"react-grab\";\n\nuseEffect(() => {\n  registerPlugin({\n    name: \"my-plugin\",\n    actions: [\n      {\n        id: \"my-action\",\n        label: \"My Action\",\n        shortcut: \"M\",\n        onAction: (context) => {\n          console.log(\"Action on:\", context.element);\n          context.hideContextMenu();\n        },\n      },\n    ],\n  });\n\n  return () => unregisterPlugin(\"my-plugin\");\n}, []);\n```\n\nActions use a `target` field to control where they appear. Omit `target` (or set `\"context-menu\"`) for the right-click menu, or set `\"toolbar\"` for the toolbar dropdown:\n\n```js\nactions: [\n  {\n    id: \"inspect\",\n    label: \"Inspect\",\n    shortcut: \"I\",\n    onAction: (ctx) => console.dir(ctx.element),\n  },\n  {\n    id: \"toggle-freeze\",\n    label: \"Freeze\",\n    target: \"toolbar\",\n    isActive: () => isFrozen,\n    onAction: () => toggleFreeze(),\n  },\n];\n```\n\nSee [`packages/react-grab/src/types.ts`](https://github.com/aidenybai/react-grab/blob/main/packages/react-grab/src/types.ts) for the full `Plugin`, `PluginHooks`, and `PluginConfig` interfaces.\n\n## Resources & Contributing Back\n\nWant to try it out? Check out [our demo](https://react-grab.com).\n\nLooking to contribute back? Check out the [Contributing Guide](https://github.com/aidenybai/react-grab/blob/main/CONTRIBUTING.md).\n\nWant to talk to the community? Hop in our [Discord](https://discord.com/invite/G7zxfUzkm7) and share your ideas and what you've built with React Grab.\n\nFind a bug? Head over to our [issue tracker](https://github.com/aidenybai/react-grab/issues) and we'll do our best to help. We love pull requests, too!\n\nWe expect all contributors to abide by the terms of our [Code of Conduct](https://github.com/aidenybai/react-grab/blob/main/.github/CODE_OF_CONDUCT.md).\n\n[**→ Start contributing on GitHub**](https://github.com/aidenybai/react-grab/blob/main/CONTRIBUTING.md)\n\n### License\n\nReact Grab is MIT-licensed open-source software.\n\n_Thank you to [Andrew Luetgers](https://github.com/andrewluetgers) for donating the `grab` npm package name._\n"
  },
  {
    "path": "packages/react-grab/bin/cli.js",
    "content": "#!/usr/bin/env node\nimport \"@react-grab/cli\";\n"
  },
  {
    "path": "packages/react-grab/e2e/activation-key-config.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\ntest.describe(\"Activation Key Configuration\", () => {\n  test.describe.configure({ mode: \"serial\" });\n\n  test.describe(\"Configuration via reinitialize\", () => {\n    test(\"should accept activationKey option\", async ({ reactGrab }) => {\n      await reactGrab.reinitialize({\n        activationKey: \"g\",\n      });\n\n      const state = await reactGrab.getState();\n      expect(typeof state.isActive).toBe(\"boolean\");\n    });\n\n    test(\"should accept modifier+key activationKey\", async ({ reactGrab }) => {\n      await reactGrab.reinitialize({\n        activationKey: \"Meta+k\",\n      });\n\n      const state = await reactGrab.getState();\n      expect(typeof state.isActive).toBe(\"boolean\");\n    });\n\n    test(\"should accept activationMode toggle option\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.reinitialize({\n        activationKey: \"g\",\n        activationMode: \"toggle\",\n      });\n\n      const state = await reactGrab.getState();\n      expect(typeof state.isActive).toBe(\"boolean\");\n    });\n\n    test(\"should accept activationMode hold option\", async ({ reactGrab }) => {\n      await reactGrab.reinitialize({\n        activationKey: \"Space\",\n        activationMode: \"hold\",\n      });\n\n      const state = await reactGrab.getState();\n      expect(typeof state.isActive).toBe(\"boolean\");\n    });\n\n    test(\"should accept keyHoldDuration option\", async ({ reactGrab }) => {\n      await reactGrab.reinitialize({\n        keyHoldDuration: 200,\n      });\n\n      const state = await reactGrab.getState();\n      expect(typeof state.isActive).toBe(\"boolean\");\n    });\n\n    test(\"should accept allowActivationInsideInput option\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.reinitialize({\n        allowActivationInsideInput: true,\n      });\n\n      const state = await reactGrab.getState();\n      expect(typeof state.isActive).toBe(\"boolean\");\n    });\n\n    test(\"should accept all options combined\", async ({ reactGrab }) => {\n      await reactGrab.reinitialize({\n        activationKey: \"Ctrl+Shift+g\",\n        activationMode: \"toggle\",\n        keyHoldDuration: 150,\n        allowActivationInsideInput: false,\n      });\n\n      const state = await reactGrab.getState();\n      expect(typeof state.isActive).toBe(\"boolean\");\n    });\n  });\n\n  test.describe(\"API activation with default config\", () => {\n    test(\"should activate via API\", async ({ reactGrab }) => {\n      expect(await reactGrab.isOverlayVisible()).toBe(false);\n\n      await reactGrab.activate();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n    });\n\n    test(\"should deactivate via Escape\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n      await reactGrab.deactivate();\n      expect(await reactGrab.isOverlayVisible()).toBe(false);\n    });\n\n    test(\"should toggle via API\", async ({ reactGrab }) => {\n      await reactGrab.toggle();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n      await reactGrab.toggle();\n      expect(await reactGrab.isOverlayVisible()).toBe(false);\n    });\n  });\n\n  test.describe(\"Selection with default config\", () => {\n    test(\"should show selection box\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='todo-list'] h1\");\n      await reactGrab.waitForSelectionBox();\n\n      expect(await reactGrab.isSelectionBoxVisible()).toBe(true);\n    });\n\n    test(\"should copy element\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='todo-list'] h1\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.clickElement(\"[data-testid='todo-list'] h1\");\n      await reactGrab.page.waitForTimeout(500);\n\n      const clipboardContent = await reactGrab.getClipboardContent();\n      expect(clipboardContent).toContain(\"Todo List\");\n    });\n  });\n\n  test.describe(\"Dynamic option updates\", () => {\n    test(\"should update activationKey via updateOptions\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.updateOptions({\n        activationKey: \"k\",\n      });\n\n      await reactGrab.activate();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n    });\n\n    test(\"should update activationMode via updateOptions\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.updateOptions({\n        activationMode: \"hold\",\n      });\n\n      await reactGrab.activate();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n    });\n\n    test(\"should update keyHoldDuration via updateOptions\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.updateOptions({\n        keyHoldDuration: 100,\n      });\n\n      await reactGrab.activate();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n    });\n  });\n\n  test.describe(\"Keyboard activation with hold duration\", () => {\n    test(\"should activate with default key after holding\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activateViaKeyboard();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n    });\n\n    test(\"should not activate without holding long enough\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.page.click(\"body\");\n      await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n      await reactGrab.page.keyboard.down(\"c\");\n      await reactGrab.page.waitForTimeout(50);\n      await reactGrab.page.keyboard.up(\"c\");\n      await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n\n      expect(await reactGrab.isOverlayVisible()).toBe(false);\n    });\n  });\n\n  test.describe(\"Input field interaction\", () => {\n    test(\"should activate in input by default\", async ({ reactGrab }) => {\n      await reactGrab.page.click(\"[data-testid='test-input']\");\n\n      await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n      await reactGrab.page.keyboard.down(\"c\");\n      await reactGrab.page.waitForTimeout(500);\n      await reactGrab.page.keyboard.up(\"c\");\n      await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n\n      await expect\n        .poll(() => reactGrab.isOverlayVisible(), { timeout: 1000 })\n        .toBe(true);\n    });\n\n    test(\"should not activate in input when disabled\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.reinitialize({ allowActivationInsideInput: false });\n      await reactGrab.page.click(\"[data-testid='test-input']\");\n\n      await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n      await reactGrab.page.keyboard.down(\"c\");\n      await expect\n        .poll(() => reactGrab.isOverlayVisible(), { timeout: 2000 })\n        .toBe(false);\n      await reactGrab.page.keyboard.up(\"c\");\n      await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n    });\n\n    test(\"should activate outside input after clicking away\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.page.click(\"[data-testid='test-input']\");\n      await reactGrab.page.click(\"body\", { position: { x: 10, y: 10 } });\n\n      await reactGrab.activateViaKeyboard();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n    });\n  });\n\n  test.describe(\"State persistence\", () => {\n    test(\"should maintain activation state after viewport resize\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n      await reactGrab.setViewportSize(1024, 768);\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n      await reactGrab.setViewportSize(1280, 720);\n    });\n\n    test(\"should maintain activation state after scroll\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n      await reactGrab.scrollPage(200);\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/activation.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\ntest.describe(\"Activation Flows\", () => {\n  test(\"should activate overlay via API\", async ({ reactGrab }) => {\n    const isVisibleBefore = await reactGrab.isOverlayVisible();\n    expect(isVisibleBefore).toBe(false);\n\n    await reactGrab.activate();\n\n    const isVisibleAfter = await reactGrab.isOverlayVisible();\n    expect(isVisibleAfter).toBe(true);\n  });\n\n  test(\"should not activate when pressing C without Cmd/Ctrl modifier\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.page.keyboard.down(\"c\");\n    await reactGrab.page.keyboard.up(\"c\");\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(false);\n  });\n\n  test(\"should deactivate overlay when pressing Escape\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n    await reactGrab.deactivate();\n\n    expect(await reactGrab.isOverlayVisible()).toBe(false);\n  });\n\n  test(\"should toggle activation state with repeated activation\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n    await reactGrab.deactivate();\n    expect(await reactGrab.isOverlayVisible()).toBe(false);\n\n    await reactGrab.activate();\n    expect(await reactGrab.isOverlayVisible()).toBe(true);\n  });\n\n  test(\"should maintain activation during mouse movement\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n    await reactGrab.page.mouse.move(100, 100);\n    await reactGrab.page.mouse.move(200, 200);\n    await reactGrab.page.mouse.move(300, 300);\n\n    expect(await reactGrab.isOverlayVisible()).toBe(true);\n  });\n\n  test(\"should create overlay host element with correct attribute\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n\n    const hostExists = await reactGrab.page.evaluate(() => {\n      const host = document.querySelector(\"[data-react-grab]\");\n      return host !== null && host.getAttribute(\"data-react-grab\") === \"true\";\n    });\n    expect(hostExists).toBe(true);\n  });\n\n  test(\"should have shadow DOM structure\", async ({ reactGrab }) => {\n    await reactGrab.activate();\n\n    const hasShadowRoot = await reactGrab.page.evaluate(() => {\n      const host = document.querySelector(\"[data-react-grab]\");\n      return host?.shadowRoot !== null;\n    });\n\n    expect(hasShadowRoot).toBe(true);\n  });\n});\n\ntest.describe(\"Activation Mode Configuration\", () => {\n  test(\"toggle mode should activate on first keyboard activation\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activateViaKeyboard();\n    expect(await reactGrab.isOverlayVisible()).toBe(true);\n  });\n\n  test(\"API toggle should deactivate on second call\", async ({ reactGrab }) => {\n    await reactGrab.toggle();\n    expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n    await reactGrab.toggle();\n    expect(await reactGrab.isOverlayVisible()).toBe(false);\n  });\n\n  test(\"keyboard activation in toggle mode requires Escape to deactivate\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activateViaKeyboard();\n    expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n    await reactGrab.deactivate();\n    expect(await reactGrab.isOverlayVisible()).toBe(false);\n  });\n\n  test(\"should activate when focused on input element\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.page.click(\"[data-testid='test-input']\");\n\n    await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n    await reactGrab.page.keyboard.down(\"c\");\n    await reactGrab.page.waitForTimeout(500);\n    await reactGrab.page.keyboard.up(\"c\");\n    await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n\n    await expect\n      .poll(() => reactGrab.isOverlayVisible(), { timeout: 1000 })\n      .toBe(true);\n  });\n\n  test(\"should activate when focused on textarea\", async ({ reactGrab }) => {\n    await reactGrab.page.click(\"[data-testid='test-textarea']\");\n\n    await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n    await reactGrab.page.keyboard.down(\"c\");\n    await reactGrab.page.waitForTimeout(500);\n    await reactGrab.page.keyboard.up(\"c\");\n    await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n\n    await expect\n      .poll(() => reactGrab.isOverlayVisible(), { timeout: 1000 })\n      .toBe(true);\n  });\n\n  test(\"activation should work after clicking outside input\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.page.click(\"[data-testid='test-input']\");\n    await reactGrab.page.click(\"body\", { position: { x: 10, y: 10 } });\n\n    await reactGrab.activateViaKeyboard();\n    expect(await reactGrab.isOverlayVisible()).toBe(true);\n  });\n\n  test(\"API activation should work even when input is focused\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.page.click(\"[data-testid='test-input']\");\n\n    await reactGrab.activate();\n\n    expect(await reactGrab.isOverlayVisible()).toBe(true);\n  });\n\n  test(\"should handle activation during page scroll\", async ({ reactGrab }) => {\n    await reactGrab.scrollPage(200);\n\n    await reactGrab.activate();\n\n    expect(await reactGrab.isOverlayVisible()).toBe(true);\n  });\n\n  test(\"should remain activated after viewport resize\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n    await reactGrab.setViewportSize(1024, 768);\n\n    expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n    await reactGrab.setViewportSize(1280, 720);\n  });\n\n  test(\"activation state should survive DOM changes\", async ({ reactGrab }) => {\n    await reactGrab.activate();\n    expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n    await reactGrab.page.evaluate(() => {\n      const newDiv = document.createElement(\"div\");\n      newDiv.textContent = \"Dynamic content\";\n      document.body.appendChild(newDiv);\n    });\n\n    expect(await reactGrab.isOverlayVisible()).toBe(true);\n  });\n\n  test(\"should handle multiple rapid API toggle calls\", async ({\n    reactGrab,\n  }) => {\n    for (let i = 0; i < 5; i++) {\n      await reactGrab.toggle();\n    }\n\n    const state = await reactGrab.getState();\n    expect(typeof state.isActive).toBe(\"boolean\");\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/agent-integration.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\ntest.describe(\"Agent Integration\", () => {\n  test.describe(\"Agent Provider Setup\", () => {\n    test(\"should configure mock agent provider\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      const isPromptMode = await reactGrab.isPromptModeActive();\n      expect(isPromptMode).toBe(true);\n    });\n\n    test(\"should allow agent provider with custom delay\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent({ delay: 1000 });\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      const isPromptMode = await reactGrab.isPromptModeActive();\n      expect(isPromptMode).toBe(true);\n    });\n\n    test(\"should allow custom status updates\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent({\n        delay: 500,\n        statusUpdates: [\"Starting...\", \"Processing...\", \"Finishing...\"],\n      });\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Test prompt\");\n      await reactGrab.submitInput();\n\n      await reactGrab.waitForAgentSession(2000);\n      const sessions = await reactGrab.getAgentSessions();\n      expect(sessions.length).toBeGreaterThan(0);\n    });\n  });\n\n  test.describe(\"Session Lifecycle\", () => {\n    test(\"should start session on input submit\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent({ delay: 1000 });\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Analyze this element\");\n      await reactGrab.submitInput();\n\n      await reactGrab.waitForAgentSession(3000);\n      await expect\n        .poll(() => reactGrab.isAgentSessionVisible(), { timeout: 3000 })\n        .toBe(true);\n    });\n\n    test(\"should show streaming status during processing\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent({ delay: 2000 });\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Test prompt\");\n      await reactGrab.submitInput();\n\n      await reactGrab.waitForAgentSession(3000);\n      const sessions = await reactGrab.getAgentSessions();\n      const streamingSession = sessions.find((s) => s.isStreaming);\n      expect(streamingSession).toBeDefined();\n    });\n\n    test(\"should complete session after processing\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent({ delay: 300 });\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Quick test\");\n      await reactGrab.submitInput();\n\n      await reactGrab.waitForAgentComplete(5000);\n      const sessions = await reactGrab.getAgentSessions();\n      const completedSession = sessions.find((s) => !s.isStreaming);\n      expect(completedSession).toBeDefined();\n    });\n\n    test(\"should display completion message\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent({ delay: 200 });\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Test\");\n      await reactGrab.submitInput();\n\n      await reactGrab.waitForAgentComplete(3000);\n\n      await expect.poll(() => reactGrab.getLabelStatusText()).toBeTruthy();\n    });\n  });\n\n  test.describe(\"Session Error Handling\", () => {\n    test(\"should handle agent errors gracefully\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent({\n        delay: 200,\n        error: \"Test error message\",\n      });\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Trigger error\");\n      await reactGrab.submitInput();\n\n      await expect\n        .poll(\n          async () => {\n            return reactGrab.page.evaluate((attrName) => {\n              const host = document.querySelector(`[${attrName}]`);\n              const shadowRoot = host?.shadowRoot;\n              if (!shadowRoot) return false;\n              const root = shadowRoot.querySelector(`[${attrName}]`);\n              return !!root?.querySelector(\"[data-react-grab-error]\");\n            }, \"data-react-grab\");\n          },\n          { timeout: 3000 },\n        )\n        .toBe(true);\n    });\n\n    test(\"should show retry option on error\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent({ delay: 100, error: \"Error occurred\" });\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Test\");\n      await reactGrab.submitInput();\n\n      await expect\n        .poll(\n          async () => {\n            return reactGrab.page.evaluate((attrName) => {\n              const host = document.querySelector(`[${attrName}]`);\n              const shadowRoot = host?.shadowRoot;\n              if (!shadowRoot) return false;\n              const root = shadowRoot.querySelector(`[${attrName}]`);\n              return (\n                root?.textContent?.toLowerCase().includes(\"retry\") ?? false\n              );\n            }, \"data-react-grab\");\n          },\n          { timeout: 2000 },\n        )\n        .toBe(true);\n    });\n  });\n\n  test.describe(\"Session Actions\", () => {\n    test(\"should dismiss session\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent({ delay: 100 });\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Test\");\n      await reactGrab.submitInput();\n\n      await reactGrab.waitForAgentComplete(3000);\n\n      await reactGrab.clickAgentDismiss();\n\n      await expect\n        .poll(() => reactGrab.isAgentSessionVisible(), { timeout: 2000 })\n        .toBe(false);\n    });\n\n    test(\"should abort streaming session\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent({ delay: 5000 });\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Long running task\");\n      await reactGrab.submitInput();\n\n      await reactGrab.waitForAgentSession(2000);\n\n      await reactGrab.clickAgentAbort();\n\n      await expect\n        .poll(\n          async () => {\n            return reactGrab.page.evaluate((attrName) => {\n              const host = document.querySelector(`[${attrName}]`);\n              const shadowRoot = host?.shadowRoot;\n              if (!shadowRoot) return false;\n              const root = shadowRoot.querySelector(`[${attrName}]`);\n              const text = root?.textContent?.toLowerCase() ?? \"\";\n              return (\n                text.includes(\"discard\") ||\n                text.includes(\"abort\") ||\n                text.includes(\"stop\")\n              );\n            }, \"data-react-grab\");\n          },\n          { timeout: 2000 },\n        )\n        .toBe(true);\n    });\n\n    test(\"should confirm abort\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent({ delay: 5000 });\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Long task\");\n      await reactGrab.submitInput();\n\n      await reactGrab.waitForAgentSession(2000);\n\n      await reactGrab.clickAgentAbort();\n      await reactGrab.confirmAgentAbort();\n\n      await expect\n        .poll(\n          async () => {\n            const sessions = await reactGrab.getAgentSessions();\n            return sessions.length;\n          },\n          { timeout: 2000 },\n        )\n        .toBeLessThanOrEqual(1);\n    });\n\n    test(\"should cancel abort\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent({ delay: 5000 });\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Long task\");\n      await reactGrab.submitInput();\n\n      await reactGrab.waitForAgentSession(2000);\n\n      await reactGrab.clickAgentAbort();\n      await reactGrab.cancelAgentAbort();\n\n      const sessions = await reactGrab.getAgentSessions();\n      expect(sessions.length).toBeGreaterThan(0);\n    });\n  });\n\n  test.describe(\"Undo/Redo Operations\", () => {\n    test(\"should support undo after completion\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent({ delay: 100 });\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Test\");\n      await reactGrab.submitInput();\n\n      await reactGrab.waitForAgentComplete(3000);\n\n      await expect\n        .poll(\n          async () => {\n            return reactGrab.page.evaluate((attrName) => {\n              const host = document.querySelector(`[${attrName}]`);\n              const shadowRoot = host?.shadowRoot;\n              if (!shadowRoot) return false;\n              const root = shadowRoot.querySelector(`[${attrName}]`);\n              return root?.textContent?.toLowerCase().includes(\"undo\") ?? false;\n            }, \"data-react-grab\");\n          },\n          { timeout: 2000 },\n        )\n        .toBe(true);\n    });\n\n    test(\"should trigger undo via keyboard shortcut\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent({ delay: 100 });\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Test\");\n      await reactGrab.submitInput();\n\n      await reactGrab.waitForAgentComplete(3000);\n      await reactGrab.clickAgentDismiss();\n\n      await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n      await reactGrab.page.keyboard.press(\"z\");\n      await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n\n      const state = await reactGrab.getState();\n      expect(state).toBeDefined();\n    });\n  });\n\n  test.describe(\"Follow-up Prompts\", () => {\n    test(\"should support follow-up prompts after completion\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent({ delay: 100 });\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Initial prompt\");\n      await reactGrab.submitInput();\n\n      await reactGrab.waitForAgentComplete(3000);\n\n      const hasFollowUpInput = await reactGrab.page.evaluate((attrName) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return false;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        return root?.querySelector(\"textarea, input\") !== null;\n      }, \"data-react-grab\");\n\n      expect(typeof hasFollowUpInput).toBe(\"boolean\");\n    });\n  });\n\n  test.describe(\"Multiple Sessions\", () => {\n    test(\"should handle multiple elements with separate sessions\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent({ delay: 500 });\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"First element\");\n      await reactGrab.submitInput();\n\n      await reactGrab.waitForAgentSession(2000);\n\n      const sessions = await reactGrab.getAgentSessions();\n      expect(sessions.length).toBeGreaterThan(0);\n    });\n  });\n\n  test.describe(\"Session State Persistence\", () => {\n    test(\"session should update bounds on scroll\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent({ delay: 2000 });\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Test scroll\");\n      await reactGrab.submitInput();\n\n      await reactGrab.waitForAgentSession(2000);\n\n      await reactGrab.scrollPage(50);\n\n      const isVisible = await reactGrab.isAgentSessionVisible();\n      expect(isVisible).toBe(true);\n    });\n\n    test(\"session should update bounds on resize\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent({ delay: 2000 });\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Test resize\");\n      await reactGrab.submitInput();\n\n      await reactGrab.waitForAgentSession(2000);\n\n      await reactGrab.setViewportSize(800, 600);\n\n      const isVisible = await reactGrab.isAgentSessionVisible();\n      expect(isVisible).toBe(true);\n\n      await reactGrab.setViewportSize(1280, 720);\n    });\n  });\n\n  test.describe(\"Edge Cases\", () => {\n    test(\"should handle empty prompt submission\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent({ delay: 100 });\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.submitInput();\n\n      const state = await reactGrab.getState();\n      expect(state).toBeDefined();\n    });\n\n    test(\"should handle rapid session starts\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent({ delay: 100 });\n\n      for (let i = 0; i < 3; i++) {\n        await reactGrab.enterPromptMode(\"li:first-child\");\n\n        await reactGrab.typeInInput(`Prompt ${i}`);\n        await reactGrab.submitInput();\n\n        await reactGrab.waitForAgentSession(5000);\n        await reactGrab.clickAgentDismiss();\n        await reactGrab.page.waitForTimeout(500);\n      }\n\n      const state = await reactGrab.getState();\n      expect(state).toBeDefined();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/agent-resume-race.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\nconst OLD_STREAM_ABORT_DELAY_MS = 150;\nconst RESUME_STATUS_INTERVAL_MS = 40;\nconst RACE_SETTLE_WAIT_MS = 500;\n\ninterface ResumeRaceAgentActionContext {\n  enterPromptMode?: (agent?: Record<string, unknown>) => void;\n}\n\ninterface ResumeRaceAgentInstallerWindow extends Window {\n  __INSTALL_RESUME_RACE_AGENT__?: () => void;\n}\n\ntest.describe(\"Agent Resume Race\", () => {\n  test(\"keeps resumed session visible when old cleanup finishes\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.page.evaluate(\n      ({ oldStreamAbortDelayMs, resumeStatusIntervalMs }) => {\n        const currentWindow = window as ResumeRaceAgentInstallerWindow;\n\n        const installResumeRaceAgent = (): void => {\n          const createAbortError = (): Error => {\n            const abortError = new Error(\"Aborted\");\n            abortError.name = \"AbortError\";\n            return abortError;\n          };\n\n          const waitForAbortWithDelay = (\n            signal: AbortSignal,\n            delayMs: number,\n          ): Promise<never> =>\n            new Promise<never>((_, reject) => {\n              const rejectWithAbortError = () => {\n                setTimeout(() => {\n                  reject(createAbortError());\n                }, delayMs);\n              };\n\n              if (signal.aborted) {\n                rejectWithAbortError();\n                return;\n              }\n\n              signal.addEventListener(\"abort\", rejectWithAbortError, {\n                once: true,\n              });\n            });\n\n          const createAgent = () => ({\n            provider: {\n              supportsResume: true,\n              supportsFollowUp: true,\n              async *send(_context: unknown, signal: AbortSignal) {\n                yield \"Processing...\";\n                await waitForAbortWithDelay(signal, oldStreamAbortDelayMs);\n              },\n              async *resume(_sessionId: string, signal: AbortSignal) {\n                while (!signal.aborted) {\n                  yield \"Processing...\";\n                  await new Promise((resolve) => {\n                    setTimeout(resolve, resumeStatusIntervalMs);\n                  });\n                }\n                throw createAbortError();\n              },\n            },\n            storage: window.localStorage,\n          });\n\n          const api = currentWindow.__REACT_GRAB__;\n          api?.unregisterPlugin(\"resume-race-agent\");\n          api?.registerPlugin({\n            name: \"resume-race-agent\",\n            actions: [\n              {\n                id: \"edit-with-resume-race-agent\",\n                label: \"Edit\",\n                shortcut: \"Enter\",\n                onAction: (context: ResumeRaceAgentActionContext) => {\n                  context.enterPromptMode?.(createAgent());\n                },\n                agent: createAgent(),\n              },\n            ],\n          });\n        };\n\n        currentWindow.__INSTALL_RESUME_RACE_AGENT__ = installResumeRaceAgent;\n        installResumeRaceAgent();\n      },\n      {\n        oldStreamAbortDelayMs: OLD_STREAM_ABORT_DELAY_MS,\n        resumeStatusIntervalMs: RESUME_STATUS_INTERVAL_MS,\n      },\n    );\n\n    await reactGrab.enterPromptMode(\"li:first-child\");\n    await reactGrab.typeInInput(\"Trigger resume race\");\n    await reactGrab.submitInput();\n    await reactGrab.waitForAgentSession(4000);\n\n    await reactGrab.page.evaluate(() => {\n      const currentWindow = window as ResumeRaceAgentInstallerWindow;\n      currentWindow.__INSTALL_RESUME_RACE_AGENT__?.();\n    });\n\n    await reactGrab.page.waitForTimeout(RACE_SETTLE_WAIT_MS);\n\n    await expect\n      .poll(() => reactGrab.isAgentSessionVisible(), {\n        timeout: 4000,\n      })\n      .toBe(true);\n\n    await reactGrab.clickAgentAbort();\n\n    await expect\n      .poll(\n        () =>\n          reactGrab.page.evaluate(\n            ({ attributeName, discardYesSelector }) => {\n              const host = document.querySelector(`[${attributeName}]`);\n              const shadowRoot = host?.shadowRoot;\n              if (!shadowRoot) return false;\n              const root = shadowRoot.querySelector(`[${attributeName}]`);\n              if (!root) return false;\n              return root.querySelector(discardYesSelector) !== null;\n            },\n            {\n              attributeName: \"data-react-grab\",\n              discardYesSelector: \"[data-react-grab-discard-yes]\",\n            },\n          ),\n        { timeout: 2000 },\n      )\n      .toBe(true);\n\n    await reactGrab.confirmAgentAbort();\n\n    await expect\n      .poll(() => reactGrab.isAgentSessionVisible(), {\n        timeout: 5000,\n      })\n      .toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/api-methods.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\ntest.describe(\"API Methods\", () => {\n  test.describe(\"Activation APIs\", () => {\n    test(\"activate() should activate the overlay\", async ({ reactGrab }) => {\n      expect(await reactGrab.isOverlayVisible()).toBe(false);\n\n      await reactGrab.activate();\n\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n    });\n\n    test(\"deactivate() should deactivate the overlay\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n      await reactGrab.page.evaluate(() => {\n        const api = (window as { __REACT_GRAB__?: { deactivate: () => void } })\n          .__REACT_GRAB__;\n        api?.deactivate();\n      });\n      await reactGrab.page.waitForTimeout(100);\n\n      expect(await reactGrab.isOverlayVisible()).toBe(false);\n    });\n\n    test(\"toggle() should toggle activation state\", async ({ reactGrab }) => {\n      expect(await reactGrab.isOverlayVisible()).toBe(false);\n\n      await reactGrab.toggle();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n      await reactGrab.toggle();\n      expect(await reactGrab.isOverlayVisible()).toBe(false);\n    });\n\n    test(\"isActive() should return correct state\", async ({ reactGrab }) => {\n      let state = await reactGrab.getState();\n      expect(state.isActive).toBe(false);\n\n      await reactGrab.activate();\n\n      state = await reactGrab.getState();\n      expect(state.isActive).toBe(true);\n    });\n\n    test(\"multiple rapid activations should be handled\", async ({\n      reactGrab,\n    }) => {\n      for (let i = 0; i < 5; i++) {\n        await reactGrab.activate();\n        await reactGrab.page.waitForTimeout(20);\n      }\n\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n    });\n\n    test(\"multiple rapid toggles should maintain consistency\", async ({\n      reactGrab,\n    }) => {\n      for (let i = 0; i < 6; i++) {\n        await reactGrab.toggle();\n        await reactGrab.page.waitForTimeout(50);\n      }\n\n      const isActive = await reactGrab.isOverlayVisible();\n      expect(typeof isActive).toBe(\"boolean\");\n    });\n  });\n\n  test.describe(\"getState()\", () => {\n    test(\"should return isActive correctly\", async ({ reactGrab }) => {\n      let state = await reactGrab.getState();\n      expect(state.isActive).toBe(false);\n\n      await reactGrab.activate();\n      state = await reactGrab.getState();\n      expect(state.isActive).toBe(true);\n    });\n\n    test(\"should return isDragging correctly during drag\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n\n      const listItem = reactGrab.page.locator(\"li\").first();\n      const box = await listItem.boundingBox();\n      if (!box) throw new Error(\"Could not get bounding box\");\n\n      await reactGrab.page.mouse.move(box.x - 10, box.y - 10);\n      await reactGrab.page.mouse.down();\n      await reactGrab.page.mouse.move(box.x + 100, box.y + 100, { steps: 5 });\n\n      const state = await reactGrab.getState();\n      expect(state.isDragging).toBe(true);\n\n      await reactGrab.page.mouse.up();\n    });\n\n    test(\"should return isCopying correctly during copy\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"h1\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.clickElement(\"h1\");\n\n      const checkCopyingState = async () => {\n        const state = await reactGrab.getState();\n        return state.isCopying;\n      };\n\n      const wasCopying = await checkCopyingState();\n      expect(typeof wasCopying).toBe(\"boolean\");\n    });\n\n    test(\"should return dragBounds during drag\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n\n      const listItem = reactGrab.page.locator(\"li\").first();\n      const box = await listItem.boundingBox();\n      if (!box) throw new Error(\"Could not get bounding box\");\n\n      await reactGrab.page.mouse.move(box.x - 20, box.y - 20);\n      await reactGrab.page.mouse.down();\n      await reactGrab.page.mouse.move(box.x + 150, box.y + 150, { steps: 10 });\n\n      const state = await reactGrab.getState();\n      if (state.dragBounds) {\n        expect(state.dragBounds.width).toBeGreaterThan(0);\n        expect(state.dragBounds.height).toBeGreaterThan(0);\n      }\n\n      await reactGrab.page.mouse.up();\n    });\n  });\n\n  test.describe(\"copyElement()\", () => {\n    test(\"should copy single element to clipboard\", async ({ reactGrab }) => {\n      const success = await reactGrab.copyElementViaApi(\n        \"[data-testid='todo-list'] h1\",\n      );\n      expect(success).toBe(true);\n\n      await reactGrab.page.waitForTimeout(500);\n      const clipboardContent = await reactGrab.getClipboardContent();\n      expect(clipboardContent).toContain(\"Todo List\");\n    });\n\n    test(\"should copy list item element\", async ({ reactGrab }) => {\n      const success = await reactGrab.copyElementViaApi(\"li:first-child\");\n      expect(success).toBe(true);\n\n      await reactGrab.page.waitForTimeout(500);\n      const clipboardContent = await reactGrab.getClipboardContent();\n      expect(clipboardContent).toBeTruthy();\n    });\n\n    test(\"should return false for non-existent element\", async ({\n      reactGrab,\n    }) => {\n      const success = await reactGrab.copyElementViaApi(\n        \".non-existent-element\",\n      );\n      expect(success).toBe(false);\n    });\n\n    test(\"should copy multiple elements via API\", async ({ reactGrab }) => {\n      const success = await reactGrab.page.evaluate(async () => {\n        const api = (\n          window as {\n            __REACT_GRAB__?: {\n              copyElement: (el: Element[]) => Promise<boolean>;\n            };\n          }\n        ).__REACT_GRAB__;\n        const elements = Array.from(document.querySelectorAll(\"li\")).slice(\n          0,\n          3,\n        );\n        if (!api || elements.length === 0) return false;\n        return api.copyElement(elements);\n      });\n      expect(success).toBe(true);\n    });\n  });\n\n  test.describe(\"Theme via setOptions\", () => {\n    test(\"setOptions({ theme }) should apply hue rotation filter\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.updateOptions({ theme: { hue: 90 } });\n      await reactGrab.activate();\n\n      const hasFilter = await reactGrab.page.evaluate(() => {\n        const host = document.querySelector(\"[data-react-grab]\");\n        const shadowRoot = host?.shadowRoot;\n        const root = shadowRoot?.querySelector(\n          \"[data-react-grab]\",\n        ) as HTMLElement;\n        return root?.style.filter?.includes(\"hue-rotate\") ?? false;\n      });\n\n      expect(hasFilter).toBe(true);\n    });\n\n    test(\"multiple theme updates via setOptions should accumulate\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.updateOptions({ theme: { hue: 45 } });\n      await reactGrab.updateOptions({\n        theme: { elementLabel: { enabled: false } },\n      });\n      await reactGrab.activate();\n\n      const hasFilter = await reactGrab.page.evaluate(() => {\n        const host = document.querySelector(\"[data-react-grab]\");\n        const shadowRoot = host?.shadowRoot;\n        const root = shadowRoot?.querySelector(\n          \"[data-react-grab]\",\n        ) as HTMLElement;\n        return root?.style.filter?.includes(\"hue-rotate(45deg)\") ?? false;\n      });\n\n      expect(hasFilter).toBe(true);\n    });\n  });\n\n  test.describe(\"dispose()\", () => {\n    test(\"should set hasInited to false on dispose\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n      await reactGrab.dispose();\n      await reactGrab.page.waitForTimeout(200);\n\n      const canReinit = await reactGrab.page.evaluate(() => {\n        const initFn = (window as { initReactGrab?: () => void }).initReactGrab;\n        return typeof initFn === \"function\";\n      });\n      expect(canReinit).toBe(true);\n    });\n\n    test(\"should remove overlay host element on dispose\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.dispose();\n\n      await reactGrab.page.waitForTimeout(100);\n\n      const hostExists = await reactGrab.page.evaluate(() => {\n        return document.querySelector(\"[data-react-grab]\") !== null;\n      });\n\n      expect(hostExists).toBe(true);\n    });\n\n    test(\"should allow re-initialization after dispose\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.dispose();\n\n      await reactGrab.reinitialize();\n\n      await reactGrab.activate();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n    });\n  });\n\n  test.describe(\"registerPlugin()\", () => {\n    test(\"should register plugin with hooks\", async ({ reactGrab }) => {\n      let callbackCalled = false;\n\n      await reactGrab.page.evaluate(() => {\n        (\n          window as { __TEST_CALLBACK_CALLED__?: boolean }\n        ).__TEST_CALLBACK_CALLED__ = false;\n        const api = (\n          window as {\n            __REACT_GRAB__?: {\n              registerPlugin: (plugin: Record<string, unknown>) => void;\n            };\n          }\n        ).__REACT_GRAB__;\n        api?.registerPlugin({\n          name: \"test-plugin\",\n          hooks: {\n            onActivate: () => {\n              (\n                window as { __TEST_CALLBACK_CALLED__?: boolean }\n              ).__TEST_CALLBACK_CALLED__ = true;\n            },\n          },\n        });\n      });\n\n      await reactGrab.activate();\n\n      callbackCalled = await reactGrab.page.evaluate(() => {\n        return (\n          (window as { __TEST_CALLBACK_CALLED__?: boolean })\n            .__TEST_CALLBACK_CALLED__ ?? false\n        );\n      });\n\n      expect(callbackCalled).toBe(true);\n    });\n\n    test(\"should allow registering plugin with multiple hooks\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.page.evaluate(() => {\n        (window as { __CALLBACKS__?: string[] }).__CALLBACKS__ = [];\n        const api = (\n          window as {\n            __REACT_GRAB__?: {\n              registerPlugin: (plugin: Record<string, unknown>) => void;\n            };\n          }\n        ).__REACT_GRAB__;\n        api?.registerPlugin({\n          name: \"test-plugin\",\n          hooks: {\n            onActivate: () => {\n              (window as { __CALLBACKS__?: string[] }).__CALLBACKS__?.push(\n                \"activate\",\n              );\n            },\n            onDeactivate: () => {\n              (window as { __CALLBACKS__?: string[] }).__CALLBACKS__?.push(\n                \"deactivate\",\n              );\n            },\n          },\n        });\n      });\n\n      await reactGrab.activate();\n      await reactGrab.deactivate();\n\n      const callbacks = await reactGrab.page.evaluate(() => {\n        return (window as { __CALLBACKS__?: string[] }).__CALLBACKS__ ?? [];\n      });\n\n      expect(callbacks).toContain(\"activate\");\n      expect(callbacks).toContain(\"deactivate\");\n    });\n  });\n\n  test.describe(\"setOptions() for agent\", () => {\n    test(\"should configure agent provider\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent();\n\n      const state = await reactGrab.page.evaluate(() => {\n        const api = (\n          window as {\n            __REACT_GRAB__?: { getState: () => Record<string, unknown> };\n          }\n        ).__REACT_GRAB__;\n        return api?.getState();\n      });\n\n      expect(state).toBeDefined();\n    });\n\n    test(\"should allow agent provider with custom options\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent({\n        delay: 100,\n        statusUpdates: [\"Custom status 1\", \"Custom status 2\"],\n      });\n\n      const hasAgent = await reactGrab.page.evaluate(() => {\n        const host = document.querySelector(\"[data-react-grab]\");\n        return host !== null;\n      });\n\n      expect(hasAgent).toBe(true);\n    });\n  });\n\n  test.describe(\"Edge Cases\", () => {\n    test(\"API should work after multiple activation cycles\", async ({\n      reactGrab,\n    }) => {\n      for (let i = 0; i < 3; i++) {\n        await reactGrab.activate();\n        await reactGrab.hoverElement(\"li\");\n        await reactGrab.waitForSelectionBox();\n        await reactGrab.deactivate();\n      }\n\n      await reactGrab.activate();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n    });\n\n    test(\"getState should be consistent with isActive\", async ({\n      reactGrab,\n    }) => {\n      const state1 = await reactGrab.getState();\n      const isActive1 = await reactGrab.isOverlayVisible();\n      expect(state1.isActive).toBe(isActive1);\n\n      await reactGrab.activate();\n\n      const state2 = await reactGrab.getState();\n      const isActive2 = await reactGrab.isOverlayVisible();\n      expect(state2.isActive).toBe(isActive2);\n    });\n\n    test(\"theme should persist across activation cycles\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.updateOptions({ theme: { hue: 120 } });\n      await reactGrab.activate();\n      await reactGrab.deactivate();\n      await reactGrab.activate();\n\n      const hasFilter = await reactGrab.page.evaluate(() => {\n        const host = document.querySelector(\"[data-react-grab]\");\n        const shadowRoot = host?.shadowRoot;\n        const root = shadowRoot?.querySelector(\n          \"[data-react-grab]\",\n        ) as HTMLElement;\n        return root?.style.filter?.includes(\"hue-rotate(120deg)\") ?? false;\n      });\n      expect(hasFilter).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/clear-history-prompt.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\nimport type { ReactGrabPageObject } from \"./fixtures.js\";\n\nconst copyElement = async (\n  reactGrab: ReactGrabPageObject,\n  selector: string,\n) => {\n  await reactGrab.activate();\n  await reactGrab.hoverElement(selector);\n  await reactGrab.waitForSelectionBox();\n  await reactGrab.clickElement(selector);\n  await expect\n    .poll(() => reactGrab.getClipboardContent(), { timeout: 5000 })\n    .toBeTruthy();\n  // HACK: Wait for copy feedback transition and history item addition\n  await reactGrab.page.waitForTimeout(300);\n};\n\ntest.describe(\"Toolbar Copy All Button\", () => {\n  test.describe(\"Visibility\", () => {\n    test(\"should not be visible before history dropdown is open\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n\n      const isVisible = await reactGrab.isToolbarCopyAllVisible();\n      expect(isVisible).toBe(false);\n    });\n\n    test(\"should become visible when history dropdown is open\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await reactGrab.clickHistoryButton();\n\n      await expect\n        .poll(() => reactGrab.isToolbarCopyAllVisible(), { timeout: 2000 })\n        .toBe(true);\n    });\n\n    test(\"should hide when history dropdown is closed\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await reactGrab.clickHistoryButton();\n\n      await expect\n        .poll(() => reactGrab.isToolbarCopyAllVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.clickHistoryButton();\n\n      await expect\n        .poll(() => reactGrab.isToolbarCopyAllVisible(), { timeout: 2000 })\n        .toBe(false);\n    });\n  });\n\n  test.describe(\"Copy Behavior\", () => {\n    test(\"should copy all history items to clipboard\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await copyElement(reactGrab, \"li:last-child\");\n\n      await reactGrab.page.evaluate(() => navigator.clipboard.writeText(\"\"));\n\n      await reactGrab.clickHistoryButton();\n      await reactGrab.clickToolbarCopyAll();\n\n      const clipboardContent = await reactGrab.getClipboardContent();\n      expect(clipboardContent).toContain(\"[1]\");\n      expect(clipboardContent).toContain(\"[2]\");\n    });\n\n    test(\"should show clear history prompt after copying\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await reactGrab.clickHistoryButton();\n      await reactGrab.clickToolbarCopyAll();\n\n      await expect\n        .poll(() => reactGrab.isClearHistoryPromptVisible(), { timeout: 2000 })\n        .toBe(true);\n    });\n  });\n});\n\ntest.describe(\"Clear History Prompt\", () => {\n  test.describe(\"Appearance\", () => {\n    test(\"should appear after toolbar copy all\", async ({ reactGrab }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await reactGrab.clickHistoryButton();\n      await reactGrab.clickToolbarCopyAll();\n\n      await expect\n        .poll(() => reactGrab.isClearHistoryPromptVisible(), { timeout: 2000 })\n        .toBe(true);\n    });\n\n    test(\"should appear after history dropdown copy all\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await reactGrab.clickHistoryButton();\n      await reactGrab.clickHistoryCopyAll();\n\n      await expect\n        .poll(() => reactGrab.isClearHistoryPromptVisible(), { timeout: 2000 })\n        .toBe(true);\n    });\n  });\n\n  test.describe(\"Confirm\", () => {\n    test(\"should clear history when confirmed via button click\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await copyElement(reactGrab, \"li:last-child\");\n\n      await reactGrab.clickHistoryButton();\n      await reactGrab.clickToolbarCopyAll();\n\n      await expect\n        .poll(() => reactGrab.isClearHistoryPromptVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.confirmClearHistoryPrompt();\n      await reactGrab.page.waitForTimeout(200);\n\n      await expect\n        .poll(() => reactGrab.isHistoryButtonVisible(), { timeout: 2000 })\n        .toBe(false);\n    });\n\n    test(\"should clear history when confirmed via Enter key\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n\n      await reactGrab.clickHistoryButton();\n      await reactGrab.clickToolbarCopyAll();\n\n      await expect\n        .poll(() => reactGrab.isClearHistoryPromptVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.pressEnter();\n      await reactGrab.page.waitForTimeout(200);\n\n      await expect\n        .poll(() => reactGrab.isClearHistoryPromptVisible(), { timeout: 2000 })\n        .toBe(false);\n\n      await expect\n        .poll(() => reactGrab.isHistoryButtonVisible(), { timeout: 2000 })\n        .toBe(false);\n    });\n\n    test(\"should dismiss the prompt after confirming\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n\n      await reactGrab.clickHistoryButton();\n      await reactGrab.clickToolbarCopyAll();\n\n      await expect\n        .poll(() => reactGrab.isClearHistoryPromptVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.confirmClearHistoryPrompt();\n\n      await expect\n        .poll(() => reactGrab.isClearHistoryPromptVisible(), { timeout: 2000 })\n        .toBe(false);\n    });\n  });\n\n  test.describe(\"Cancel\", () => {\n    test(\"should keep history when cancelled via button click\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n\n      await reactGrab.clickHistoryButton();\n      await reactGrab.clickToolbarCopyAll();\n\n      await expect\n        .poll(() => reactGrab.isClearHistoryPromptVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.cancelClearHistoryPrompt();\n      await reactGrab.page.waitForTimeout(200);\n\n      await expect\n        .poll(() => reactGrab.isClearHistoryPromptVisible(), { timeout: 2000 })\n        .toBe(false);\n\n      await expect\n        .poll(() => reactGrab.isHistoryButtonVisible(), { timeout: 2000 })\n        .toBe(true);\n    });\n\n    test(\"should dismiss prompt when cancelled via Escape key\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n\n      await reactGrab.clickHistoryButton();\n      await reactGrab.clickToolbarCopyAll();\n\n      await expect\n        .poll(() => reactGrab.isClearHistoryPromptVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.pressEscape();\n      await reactGrab.page.waitForTimeout(200);\n\n      await expect\n        .poll(() => reactGrab.isClearHistoryPromptVisible(), { timeout: 2000 })\n        .toBe(false);\n\n      await expect\n        .poll(() => reactGrab.isHistoryButtonVisible(), { timeout: 2000 })\n        .toBe(true);\n    });\n  });\n\n  test.describe(\"Dismiss Interactions\", () => {\n    test(\"should dismiss when opening context menu\", async ({ reactGrab }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n\n      await reactGrab.clickHistoryButton();\n      await reactGrab.clickToolbarCopyAll();\n\n      await expect\n        .poll(() => reactGrab.isClearHistoryPromptVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li:first-child\");\n\n      await expect\n        .poll(() => reactGrab.isClearHistoryPromptVisible(), { timeout: 2000 })\n        .toBe(false);\n    });\n\n    test(\"should dismiss when toolbar is disabled\", async ({ reactGrab }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n\n      await reactGrab.clickHistoryButton();\n      await reactGrab.clickToolbarCopyAll();\n\n      await expect\n        .poll(() => reactGrab.isClearHistoryPromptVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.clickToolbarEnabled();\n      await reactGrab.page.waitForTimeout(200);\n\n      await expect\n        .poll(() => reactGrab.isClearHistoryPromptVisible(), { timeout: 2000 })\n        .toBe(false);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/context-menu.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\ntest.describe(\"Context Menu\", () => {\n  test.describe(\"Visibility\", () => {\n    test(\"should show context menu on right-click while active\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li\");\n\n      const isContextMenuVisible = await reactGrab.isContextMenuVisible();\n      expect(isContextMenuVisible).toBe(true);\n    });\n\n    test(\"should not show context menu when inactive\", async ({\n      reactGrab,\n    }) => {\n      const isVisibleBefore = await reactGrab.isOverlayVisible();\n      expect(isVisibleBefore).toBe(false);\n\n      await reactGrab.rightClickElement(\"li\");\n\n      const isContextMenuVisible = await reactGrab.isContextMenuVisible();\n      expect(isContextMenuVisible).toBe(false);\n    });\n\n    test(\"should show context menu after keyboard activation\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activateViaKeyboard();\n      await reactGrab.hoverElement(\"li\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li\");\n\n      const isContextMenuVisible = await reactGrab.isContextMenuVisible();\n      expect(isContextMenuVisible).toBe(true);\n    });\n\n    test(\"should show context menu when right-clicking while holding activation keys\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n      await reactGrab.page.keyboard.down(\"c\");\n      await reactGrab.page.waitForTimeout(100);\n\n      const element = reactGrab.page.locator(\"li\").first();\n      await element.click({ button: \"right\", force: true });\n\n      await expect\n        .poll(() => reactGrab.isContextMenuVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.page.keyboard.up(\"c\");\n      await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n    });\n\n    test(\"should show context menu with Copy and Open items\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li\");\n\n      const isContextMenuVisible = await reactGrab.isContextMenuVisible();\n      expect(isContextMenuVisible).toBe(true);\n\n      const isCopyEnabled = await reactGrab.isContextMenuItemEnabled(\"Copy\");\n      expect(isCopyEnabled).toBe(true);\n\n      const isOpenEnabled = await reactGrab.isContextMenuItemEnabled(\"Open\");\n      expect(isOpenEnabled).toBe(true);\n    });\n  });\n\n  test.describe(\"Menu Items\", () => {\n    test(\"should copy element when clicking Copy\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='todo-list'] h1\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"[data-testid='todo-list'] h1\");\n      await reactGrab.clickContextMenuItem(\"Copy\");\n\n      await reactGrab.page.waitForTimeout(500);\n\n      const clipboardContent = await reactGrab.getClipboardContent();\n      expect(clipboardContent).toContain(\"Todo List\");\n    });\n\n    test(\"should copy list item content correctly\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li:first-child\");\n      await reactGrab.clickContextMenuItem(\"Copy\");\n\n      await reactGrab.page.waitForTimeout(500);\n\n      const clipboardContent = await reactGrab.getClipboardContent();\n      expect(clipboardContent).toBeTruthy();\n    });\n\n    test(\"should have Copy always enabled\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li\");\n\n      const isCopyEnabled = await reactGrab.isContextMenuItemEnabled(\"Copy\");\n      expect(isCopyEnabled).toBe(true);\n    });\n  });\n\n  test.describe(\"Dismissal\", () => {\n    test(\"should dismiss context menu on Escape\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li\");\n\n      const isVisibleBefore = await reactGrab.isContextMenuVisible();\n      expect(isVisibleBefore).toBe(true);\n\n      await reactGrab.page.keyboard.press(\"Escape\");\n      await reactGrab.page.waitForTimeout(200);\n\n      const isVisibleAfter = await reactGrab.isContextMenuVisible();\n      expect(isVisibleAfter).toBe(false);\n    });\n\n    test(\"should dismiss context menu when clicking outside\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li\");\n\n      const isVisibleBefore = await reactGrab.isContextMenuVisible();\n      expect(isVisibleBefore).toBe(true);\n\n      await reactGrab.page.mouse.click(10, 10);\n      await reactGrab.page.waitForTimeout(200);\n\n      const isVisibleAfter = await reactGrab.isContextMenuVisible();\n      expect(isVisibleAfter).toBe(false);\n    });\n\n    test(\"should dismiss context menu after Copy action\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li\");\n\n      await reactGrab.clickContextMenuItem(\"Copy\");\n      await reactGrab.page.waitForTimeout(300);\n\n      const isContextMenuVisible = await reactGrab.isContextMenuVisible();\n      expect(isContextMenuVisible).toBe(false);\n    });\n  });\n\n  test.describe(\"Selection Freezing\", () => {\n    test(\"should freeze element selection while context menu is open\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"h1\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"h1\");\n\n      const isContextMenuVisible = await reactGrab.isContextMenuVisible();\n      expect(isContextMenuVisible).toBe(true);\n\n      await reactGrab.page.mouse.move(100, 100);\n      await reactGrab.page.waitForTimeout(100);\n\n      const stillVisible = await reactGrab.isContextMenuVisible();\n      expect(stillVisible).toBe(true);\n    });\n\n    test(\"should maintain context menu while moving mouse\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"h1\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"h1\");\n\n      await reactGrab.page.mouse.move(500, 500);\n      await reactGrab.page.waitForTimeout(100);\n\n      const isContextMenuVisible = await reactGrab.isContextMenuVisible();\n      expect(isContextMenuVisible).toBe(true);\n    });\n  });\n\n  test.describe(\"Multiple Context Menus\", () => {\n    test(\"should allow opening new context menu after using previous one\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"h1\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"h1\");\n      await reactGrab.clickContextMenuItem(\"Copy\");\n\n      await reactGrab.page.waitForTimeout(300);\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li\");\n\n      const isContextMenuVisible = await reactGrab.isContextMenuVisible();\n      expect(isContextMenuVisible).toBe(true);\n    });\n\n    test(\"should allow opening context menu after clicking outside to dismiss\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"h1\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"h1\");\n\n      await reactGrab.page.mouse.click(10, 10);\n      await reactGrab.page.waitForTimeout(300);\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n\n      await expect\n        .poll(async () => reactGrab.isSelectionBoxVisible(), { timeout: 5000 })\n        .toBe(true);\n\n      await reactGrab.rightClickElement(\"li:first-child\");\n\n      const isContextMenuVisible = await reactGrab.isContextMenuVisible();\n      expect(isContextMenuVisible).toBe(true);\n    });\n\n    test(\"should show context menu on different elements consecutively\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n\n      await reactGrab.hoverElement(\"h1\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"h1\");\n      const firstMenuVisible = await reactGrab.isContextMenuVisible();\n      expect(firstMenuVisible).toBe(true);\n\n      await reactGrab.page.mouse.click(10, 10);\n      await reactGrab.page.waitForTimeout(200);\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li:first-child\");\n\n      const secondMenuVisible = await reactGrab.isContextMenuVisible();\n      expect(secondMenuVisible).toBe(true);\n    });\n\n    test(\"should keep context menu on original element when right-clicking different element while menu is open\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n\n      await reactGrab.hoverElement(\"h1\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"h1\");\n      const firstMenuVisible = await reactGrab.isContextMenuVisible();\n      expect(firstMenuVisible).toBe(true);\n\n      // Right-clicking elsewhere while menu is open should NOT switch to new element\n      await reactGrab.rightClickElement(\"li:first-child\");\n      await reactGrab.page.waitForTimeout(100);\n\n      const menuStillVisible = await reactGrab.isContextMenuVisible();\n      expect(menuStillVisible).toBe(true);\n    });\n  });\n\n  test.describe(\"Keyboard Navigation Integration\", () => {\n    test(\"should show context menu after keyboard navigation\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.pressArrowDown();\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.rightClickElement(\"li:nth-child(2)\");\n\n      const isContextMenuVisible = await reactGrab.isContextMenuVisible();\n      expect(isContextMenuVisible).toBe(true);\n    });\n\n    test(\"should copy correct element after keyboard navigation via context menu\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.pressArrowDown();\n      await reactGrab.page.waitForTimeout(100);\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.rightClickElement(\"li:nth-child(2)\");\n      await reactGrab.clickContextMenuItem(\"Copy\");\n\n      await reactGrab.page.waitForTimeout(500);\n\n      const clipboardContent = await reactGrab.getClipboardContent();\n      expect(clipboardContent).toContain(\"Walk the dog\");\n    });\n  });\n\n  test.describe(\"Element-specific Behavior\", () => {\n    test(\"should show context menu for heading element\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"h1\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"h1\");\n\n      const isContextMenuVisible = await reactGrab.isContextMenuVisible();\n      expect(isContextMenuVisible).toBe(true);\n    });\n\n    test(\"should show context menu for list element\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"ul\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"ul\");\n\n      const isContextMenuVisible = await reactGrab.isContextMenuVisible();\n      expect(isContextMenuVisible).toBe(true);\n    });\n\n    test(\"should show context menu for list item element\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:nth-child(2)\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li:nth-child(2)\");\n\n      const isContextMenuVisible = await reactGrab.isContextMenuVisible();\n      expect(isContextMenuVisible).toBe(true);\n    });\n  });\n\n  test.describe(\"Edge Cases\", () => {\n    test(\"should work correctly after scrolling page\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.scrollPage(100);\n      await reactGrab.page.waitForTimeout(100);\n\n      await reactGrab.hoverElement(\"li\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li\");\n\n      const isContextMenuVisible = await reactGrab.isContextMenuVisible();\n      expect(isContextMenuVisible).toBe(true);\n    });\n\n    test(\"should allow reopening after dismiss and copy flow\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li\");\n      await reactGrab.clickContextMenuItem(\"Copy\");\n\n      await reactGrab.page.waitForTimeout(500);\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"h1\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"h1\");\n\n      const isContextMenuVisible = await reactGrab.isContextMenuVisible();\n      expect(isContextMenuVisible).toBe(true);\n    });\n\n    test(\"should copy different elements via context menu\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='todo-list'] h1\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"[data-testid='todo-list'] h1\");\n      await reactGrab.clickContextMenuItem(\"Copy\");\n      await reactGrab.page.waitForTimeout(1600);\n\n      const firstCopy = await reactGrab.getClipboardContent();\n      expect(firstCopy).toContain(\"Todo List\");\n\n      await reactGrab.activate();\n      await reactGrab.page.waitForTimeout(100);\n      await reactGrab.hoverElement(\"[data-testid='todo-list'] li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\n        \"[data-testid='todo-list'] li:first-child\",\n      );\n      await reactGrab.clickContextMenuItem(\"Copy\");\n      await reactGrab.page.waitForTimeout(500);\n\n      const secondCopy = await reactGrab.getClipboardContent();\n      expect(secondCopy).toBeTruthy();\n      expect(secondCopy).not.toContain(\"Todo List\");\n    });\n  });\n\n  test.describe(\"Prompt Menu Item\", () => {\n    test(\"Edit item should appear when agent is configured\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li:first-child\");\n\n      const menuInfo = await reactGrab.getContextMenuInfo();\n      expect(menuInfo.isVisible).toBe(true);\n      expect(menuInfo.menuItems).toContain(\"Edit\");\n    });\n\n    test(\"Edit item should enter input mode when clicked\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li:first-child\");\n      await reactGrab.page.waitForTimeout(100);\n\n      await reactGrab.clickContextMenuItem(\"Edit\");\n      await reactGrab.page.waitForTimeout(200);\n\n      const isPromptMode = await reactGrab.isPromptModeActive();\n      expect(isPromptMode).toBe(true);\n    });\n\n    test(\"Edit with agent keeps overlay active\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li:first-child\");\n      await reactGrab.clickContextMenuItem(\"Edit\");\n\n      await expect\n        .poll(() => reactGrab.isOverlayVisible(), { timeout: 2000 })\n        .toBe(true);\n    });\n\n    test(\"Copy without agent deactivates after action\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li:first-child\");\n      await reactGrab.clickContextMenuItem(\"Copy\");\n\n      await expect\n        .poll(() => reactGrab.isOverlayVisible(), { timeout: 3000 })\n        .toBe(false);\n    });\n  });\n\n  test.describe(\"Context Menu Positioning\", () => {\n    test(\"context menu should appear near click position\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='todo-list'] li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\n        \"[data-testid='todo-list'] li:first-child\",\n      );\n      await reactGrab.page.waitForTimeout(200);\n\n      const menuInfo = await reactGrab.getContextMenuInfo();\n      expect(menuInfo.isVisible).toBe(true);\n      expect(menuInfo.position).toBeDefined();\n    });\n\n    test(\"context menu should stay within viewport at bottom edge\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='edge-bottom-left']\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"[data-testid='edge-bottom-left']\");\n\n      const menuInfo = await reactGrab.getContextMenuInfo();\n      const viewport = await reactGrab.getViewportSize();\n\n      if (menuInfo.position) {\n        expect(menuInfo.position.y).toBeLessThan(viewport.height);\n      }\n    });\n\n    test(\"context menu should stay within viewport at right edge\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='edge-top-right']\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"[data-testid='edge-top-right']\");\n\n      const menuInfo = await reactGrab.getContextMenuInfo();\n      const viewport = await reactGrab.getViewportSize();\n\n      if (menuInfo.position) {\n        expect(menuInfo.position.x).toBeLessThan(viewport.width);\n      }\n    });\n  });\n\n  test.describe(\"Custom Actions with Agent\", () => {\n    test(\"custom action with agent should appear in menu\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.page.evaluate(() => {\n        const api = (\n          window as {\n            __REACT_GRAB__?: {\n              unregisterPlugin: (name: string) => void;\n              registerPlugin: (plugin: Record<string, unknown>) => void;\n            };\n          }\n        ).__REACT_GRAB__;\n\n        const mockProvider = {\n          *send() {\n            yield \"Processing...\";\n            yield \"Completed\";\n          },\n          supportsFollowUp: true,\n        };\n\n        api?.unregisterPlugin(\"custom-agent-action\");\n        const agent = { provider: mockProvider };\n        api?.registerPlugin({\n          name: \"custom-agent-action\",\n          actions: [\n            {\n              id: \"custom-edit\",\n              label: \"Custom Edit\",\n              shortcut: \"E\",\n              onAction: (context: {\n                enterPromptMode?: (agent?: Record<string, unknown>) => void;\n              }) => {\n                context.enterPromptMode?.(agent);\n              },\n              agent,\n            },\n          ],\n        });\n      });\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li:first-child\");\n\n      const menuInfo = await reactGrab.getContextMenuInfo();\n      expect(menuInfo.isVisible).toBe(true);\n      expect(\n        menuInfo.menuItems.map((item: string) => item.toLowerCase()),\n      ).toContain(\"custom edit\");\n    });\n\n    test(\"custom action should trigger enterPromptMode\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.page.evaluate(() => {\n        const api = (\n          window as {\n            __REACT_GRAB__?: {\n              unregisterPlugin: (name: string) => void;\n              registerPlugin: (plugin: Record<string, unknown>) => void;\n            };\n          }\n        ).__REACT_GRAB__;\n\n        const mockProvider = {\n          *send() {\n            yield \"Processing...\";\n            yield \"Completed\";\n          },\n          supportsFollowUp: true,\n        };\n\n        api?.unregisterPlugin(\"custom-agent-action\");\n        const agent = { provider: mockProvider };\n        api?.registerPlugin({\n          name: \"custom-agent-action\",\n          actions: [\n            {\n              id: \"custom-edit\",\n              label: \"Custom Edit\",\n              shortcut: \"E\",\n              onAction: (context: {\n                enterPromptMode?: (agent?: Record<string, unknown>) => void;\n              }) => {\n                context.enterPromptMode?.(agent);\n              },\n              agent,\n            },\n          ],\n        });\n      });\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li:first-child\");\n      await reactGrab.page.waitForTimeout(100);\n\n      await reactGrab.clickContextMenuItem(\"Custom edit\");\n      await reactGrab.page.waitForTimeout(200);\n\n      const isPromptMode = await reactGrab.isPromptModeActive();\n      expect(isPromptMode).toBe(true);\n    });\n\n    test(\"action without agent should just execute onAction\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.page.evaluate(() => {\n        const api = (\n          window as {\n            __REACT_GRAB__?: {\n              unregisterPlugin: (name: string) => void;\n              registerPlugin: (plugin: Record<string, unknown>) => void;\n            };\n          }\n        ).__REACT_GRAB__;\n\n        api?.unregisterPlugin(\"plain-action\");\n        api?.registerPlugin({\n          name: \"plain-action\",\n          actions: [\n            {\n              id: \"plain-action\",\n              label: \"Plain Action\",\n              onAction: () => {\n                (\n                  window as { __plainActionCalled?: boolean }\n                ).__plainActionCalled = true;\n              },\n            },\n          ],\n        });\n      });\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li:first-child\");\n\n      const menuInfo = await reactGrab.getContextMenuInfo();\n      const lowerMenuItems = menuInfo.menuItems.map((item: string) =>\n        item.toLowerCase(),\n      );\n      expect(lowerMenuItems).toContain(\"plain action\");\n\n      await reactGrab.clickContextMenuItem(\"Plain Action\");\n      await reactGrab.page.waitForTimeout(100);\n\n      const actionCalled = await reactGrab.page.evaluate(\n        () =>\n          (window as { __plainActionCalled?: boolean }).__plainActionCalled ??\n          false,\n      );\n      expect(actionCalled).toBe(true);\n    });\n\n    test(\"multiple actions should all appear in menu\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.page.evaluate(() => {\n        const api = (\n          window as {\n            __REACT_GRAB__?: {\n              unregisterPlugin: (name: string) => void;\n              registerPlugin: (plugin: Record<string, unknown>) => void;\n            };\n          }\n        ).__REACT_GRAB__;\n\n        api?.unregisterPlugin(\"multi-actions\");\n        api?.registerPlugin({\n          name: \"multi-actions\",\n          actions: [\n            {\n              id: \"action-1\",\n              label: \"First Action\",\n              onAction: () => {},\n            },\n            {\n              id: \"action-2\",\n              label: \"Second Action\",\n              onAction: () => {},\n            },\n          ],\n        });\n      });\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li:first-child\");\n\n      const menuInfo = await reactGrab.getContextMenuInfo();\n      const lowerMenuItems = menuInfo.menuItems.map((item: string) =>\n        item.toLowerCase(),\n      );\n      expect(lowerMenuItems).toContain(\"first action\");\n      expect(lowerMenuItems).toContain(\"second action\");\n    });\n\n    test(\"action with shortcut should be triggerable via keyboard\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.page.evaluate(() => {\n        const api = (\n          window as {\n            __REACT_GRAB__?: {\n              unregisterPlugin: (name: string) => void;\n              registerPlugin: (plugin: Record<string, unknown>) => void;\n            };\n          }\n        ).__REACT_GRAB__;\n\n        api?.unregisterPlugin(\"keyboard-action\");\n        api?.registerPlugin({\n          name: \"keyboard-action\",\n          actions: [\n            {\n              id: \"keyboard-action\",\n              label: \"Keyboard Action\",\n              shortcut: \"K\",\n              onAction: () => {\n                (\n                  window as { __keyboardActionCalled?: boolean }\n                ).__keyboardActionCalled = true;\n              },\n            },\n          ],\n        });\n      });\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li:first-child\");\n      await reactGrab.page.waitForTimeout(100);\n\n      await reactGrab.pressModifierKeyCombo(\"k\");\n      await reactGrab.page.waitForTimeout(200);\n\n      const actionCalled = await reactGrab.page.evaluate(\n        () =>\n          (window as { __keyboardActionCalled?: boolean })\n            .__keyboardActionCalled ?? false,\n      );\n      expect(actionCalled).toBe(true);\n    });\n\n    test(\"disabled action should appear but be disabled\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.page.evaluate(() => {\n        const api = (\n          window as {\n            __REACT_GRAB__?: {\n              unregisterPlugin: (name: string) => void;\n              registerPlugin: (plugin: Record<string, unknown>) => void;\n            };\n          }\n        ).__REACT_GRAB__;\n\n        api?.unregisterPlugin(\"disabled-action\");\n        api?.registerPlugin({\n          name: \"disabled-action\",\n          actions: [\n            {\n              id: \"disabled-action\",\n              label: \"Disabled Action\",\n              enabled: false,\n              onAction: () => {},\n            },\n          ],\n        });\n      });\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li:first-child\");\n\n      const isDisabled = await reactGrab.page.evaluate((attrName) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return false;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        const button = root?.querySelector(\n          '[data-react-grab-menu-item=\"disabled action\"]',\n        );\n        return button?.hasAttribute(\"disabled\") ?? false;\n      }, \"data-react-grab\");\n      expect(isDisabled).toBe(true);\n    });\n\n    test(\"action enabled function should receive context\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.page.evaluate(() => {\n        const api = (\n          window as {\n            __REACT_GRAB__?: {\n              unregisterPlugin: (name: string) => void;\n              registerPlugin: (plugin: Record<string, unknown>) => void;\n            };\n          }\n        ).__REACT_GRAB__;\n\n        api?.unregisterPlugin(\"context-action\");\n        api?.registerPlugin({\n          name: \"context-action\",\n          actions: [\n            {\n              id: \"context-action\",\n              label: \"Context Action\",\n              enabled: (context: { element: Element }) => {\n                (window as { __enabledTagName?: string }).__enabledTagName =\n                  context.element.tagName;\n                return context.element.tagName.toLowerCase() === \"li\";\n              },\n              onAction: () => {},\n            },\n          ],\n        });\n      });\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li:first-child\");\n\n      const enabledTagName = await reactGrab.page.evaluate(\n        () => (window as { __enabledTagName?: string }).__enabledTagName,\n      );\n      expect(enabledTagName).toBe(\"LI\");\n\n      const menuInfo = await reactGrab.getContextMenuInfo();\n      const lowerMenuItems = menuInfo.menuItems.map((item: string) =>\n        item.toLowerCase(),\n      );\n      expect(lowerMenuItems).toContain(\"context action\");\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/copy-feedback.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\nconst FEEDBACK_DURATION_MS = 1500;\n\ntest.describe(\"Copy Feedback Behavior\", () => {\n  test.describe(\"Toggle Mode - Feedback Period Deactivation\", () => {\n    test(\"should deactivate immediately when key released during feedback period\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activateViaKeyboard();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n      await reactGrab.page.keyboard.down(\"c\");\n      await reactGrab.clickElement(\"li:first-child\");\n      await reactGrab.page.waitForTimeout(200);\n\n      await reactGrab.page.keyboard.up(\"c\");\n      await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n\n      await reactGrab.page.waitForTimeout(100);\n      expect(await reactGrab.isOverlayVisible()).toBe(false);\n    });\n\n    test(\"should stay active when key held through entire feedback period\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activateViaKeyboard();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n      await reactGrab.page.keyboard.down(\"c\");\n      await reactGrab.clickElement(\"li:first-child\");\n\n      await reactGrab.page.waitForTimeout(FEEDBACK_DURATION_MS + 200);\n\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n      await reactGrab.page.keyboard.up(\"c\");\n      await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n    });\n\n    test(\"should allow hovering different elements during feedback period\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activateViaKeyboard();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n      await reactGrab.page.keyboard.down(\"c\");\n      await reactGrab.clickElement(\"li:first-child\");\n\n      await reactGrab.hoverElement(\"h1\");\n      await expect\n        .poll(() => reactGrab.isSelectionBoxVisible(), {\n          timeout: FEEDBACK_DURATION_MS,\n        })\n        .toBe(true);\n\n      await reactGrab.page.keyboard.up(\"c\");\n      await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n    });\n\n    test(\"should show selection box following hover during feedback\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activateViaKeyboard();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n      await reactGrab.page.keyboard.down(\"c\");\n      await reactGrab.clickElement(\"li:first-child\");\n      await reactGrab.hoverElement(\"h1\");\n\n      await reactGrab.page.waitForTimeout(FEEDBACK_DURATION_MS + 500);\n\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n      const boundsAfter = await reactGrab.getSelectionBoxBounds();\n      expect(boundsAfter).not.toBeNull();\n\n      await reactGrab.page.keyboard.up(\"c\");\n      await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n    });\n\n    test(\"should deactivate at end of feedback if key released mid-feedback\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activateViaKeyboard();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n      await reactGrab.page.keyboard.down(\"c\");\n      await reactGrab.clickElement(\"li:first-child\");\n\n      await reactGrab.page.waitForTimeout(500);\n\n      await reactGrab.page.keyboard.up(\"c\");\n      await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n\n      await reactGrab.page.waitForTimeout(100);\n      expect(await reactGrab.isOverlayVisible()).toBe(false);\n    });\n  });\n\n  test.describe(\"Hold Mode - Feedback Period Behavior\", () => {\n    test(\"should deactivate immediately when key released during feedback in hold mode\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.updateOptions({ activationMode: \"hold\" });\n\n      await reactGrab.activate();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n      await reactGrab.page.keyboard.down(\"c\");\n      await reactGrab.clickElement(\"li:first-child\");\n\n      await reactGrab.page.waitForTimeout(200);\n\n      await reactGrab.page.keyboard.up(\"c\");\n      await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n\n      await reactGrab.page.waitForTimeout(100);\n      expect(await reactGrab.isOverlayVisible()).toBe(false);\n    });\n  });\n\n  test.describe(\"API Activation - Toggle Mode Behavior\", () => {\n    test(\"should deactivate after copy via API activation in toggle mode\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.clickElement(\"li:first-child\");\n\n      await expect\n        .poll(() => reactGrab.isOverlayVisible(), { timeout: 3000 })\n        .toBe(false);\n    });\n\n    test(\"should require re-activation for multiple copies via API\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.clickElement(\"li:first-child\");\n\n      await expect\n        .poll(() => reactGrab.isOverlayVisible(), { timeout: 3000 })\n        .toBe(false);\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"h1\");\n      await reactGrab.page.waitForTimeout(100);\n\n      const isVisible = await reactGrab.isSelectionBoxVisible();\n      expect(isVisible).toBe(true);\n    });\n  });\n\n  test.describe(\"Edge Cases\", () => {\n    test(\"should handle rapid key tap during feedback\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activateViaKeyboard();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n      await reactGrab.page.keyboard.down(\"c\");\n      await reactGrab.clickElement(\"li:first-child\");\n\n      await reactGrab.page.waitForTimeout(100);\n\n      await reactGrab.page.keyboard.up(\"c\");\n      await reactGrab.page.keyboard.down(\"c\");\n      await reactGrab.page.waitForTimeout(50);\n      await reactGrab.page.keyboard.up(\"c\");\n      await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n\n      await reactGrab.page.waitForTimeout(100);\n      expect(await reactGrab.isOverlayVisible()).toBe(false);\n    });\n\n    test(\"should handle modifier key release during feedback\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activateViaKeyboard();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n      await reactGrab.page.keyboard.down(\"c\");\n      await reactGrab.clickElement(\"li:first-child\");\n\n      await reactGrab.page.waitForTimeout(100);\n\n      await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n\n      await reactGrab.page.waitForTimeout(100);\n      expect(await reactGrab.isOverlayVisible()).toBe(false);\n\n      await reactGrab.page.keyboard.up(\"c\");\n    });\n\n    test(\"should copy to clipboard before deactivating\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activateViaKeyboard();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n      await reactGrab.hoverElement(\"[data-testid='main-title']\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n      await reactGrab.page.keyboard.down(\"c\");\n      await reactGrab.clickElement(\"[data-testid='main-title']\");\n\n      await expect\n        .poll(() => reactGrab.getClipboardContent(), {\n          timeout: FEEDBACK_DURATION_MS,\n        })\n        .toContain(\"React Grab\");\n\n      await reactGrab.page.keyboard.up(\"c\");\n      await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n    });\n\n    test(\"should handle multiple sequential copies while holding\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activateViaKeyboard();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n      await reactGrab.page.keyboard.down(\"c\");\n      await reactGrab.clickElement(\"li:first-child\");\n      await reactGrab.page.waitForTimeout(200);\n\n      await reactGrab.hoverElement(\"li:nth-child(2)\");\n      await reactGrab.page.waitForTimeout(100);\n      await reactGrab.clickElement(\"li:nth-child(2)\");\n      await reactGrab.page.waitForTimeout(200);\n\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n      await reactGrab.page.keyboard.up(\"c\");\n      await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n    });\n\n    test(\"should deactivate when escape pressed during feedback\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activateViaKeyboard();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n      await reactGrab.page.keyboard.down(\"c\");\n      await reactGrab.clickElement(\"li:first-child\");\n\n      await reactGrab.page.waitForTimeout(100);\n\n      await reactGrab.pressEscape();\n\n      await reactGrab.page.waitForTimeout(100);\n      expect(await reactGrab.isOverlayVisible()).toBe(false);\n\n      await reactGrab.page.keyboard.up(\"c\");\n      await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n    });\n  });\n\n  test.describe(\"Feedback Visual Indicators\", () => {\n    test(\"should show 'Copied' label after successful copy\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.clickElement(\"li:first-child\");\n\n      await expect\n        .poll(() => reactGrab.getLabelStatusText(), { timeout: 2000 })\n        .toBe(\"Copied\");\n    });\n\n    test(\"should show grabbed box animation during feedback\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.clickElement(\"li:first-child\");\n\n      await reactGrab.page.waitForTimeout(100);\n\n      const grabbedInfo = await reactGrab.getGrabbedBoxInfo();\n      expect(grabbedInfo.count).toBeGreaterThan(0);\n    });\n  });\n\n  test.describe(\"Immediate Grabbing Feedback\", () => {\n    test(\"should enter copying state immediately on click\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.clickElement(\"li:first-child\");\n\n      await expect\n        .poll(\n          async () => {\n            const state = await reactGrab.getState();\n            return state.isCopying || state.labelInstances.length > 0;\n          },\n          { timeout: 500 },\n        )\n        .toBe(true);\n    });\n\n    test(\"should create label instance with copying status on click\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.clickElement(\"li:first-child\");\n\n      await expect\n        .poll(\n          async () => {\n            const instances = await reactGrab.getLabelInstancesInfo();\n            return instances.some(\n              (instance) =>\n                instance.status === \"copying\" || instance.status === \"copied\",\n            );\n          },\n          { timeout: 500 },\n        )\n        .toBe(true);\n    });\n\n    test(\"should set progress cursor during copy\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.clickElement(\"li:first-child\");\n\n      await expect\n        .poll(\n          async () => {\n            const hasCursorOverride = await reactGrab.page.evaluate(() => {\n              const styleElement = document.querySelector(\n                \"[data-react-grab-cursor]\",\n              );\n              if (!styleElement) return false;\n              return styleElement.textContent?.includes(\"progress\") ?? false;\n            });\n            const state = await reactGrab.getState();\n            return hasCursorOverride || state.labelInstances.length > 0;\n          },\n          { timeout: 500 },\n        )\n        .toBe(true);\n    });\n\n    test(\"should show Grabbing label before copy completes\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='main-title']\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.clickElement(\"[data-testid='main-title']\");\n\n      await expect\n        .poll(\n          async () => {\n            const statusText = await reactGrab.getLabelStatusText();\n            return statusText !== null;\n          },\n          { timeout: 2000 },\n        )\n        .toBe(true);\n    });\n\n    test(\"should transition from Grabbing to Copied\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.clickElement(\"li:first-child\");\n\n      await expect\n        .poll(() => reactGrab.getLabelStatusText(), { timeout: 2000 })\n        .toBe(\"Copied\");\n\n      const state = await reactGrab.getState();\n      expect(state.isCopying).toBe(false);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/copy-styles.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\ntest.describe(\"Copy styles\", () => {\n  test.describe(\"Context Menu\", () => {\n    test(\"should show Copy styles in context menu\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='todo-list'] h1\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"[data-testid='todo-list'] h1\");\n\n      const menuInfo = await reactGrab.getContextMenuInfo();\n      expect(menuInfo.isVisible).toBe(true);\n      expect(\n        menuInfo.menuItems.map((item: string) => item.toLowerCase()),\n      ).toContain(\"copy styles\");\n    });\n\n    test(\"should copy CSS declarations to clipboard via context menu\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='todo-list'] h1\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"[data-testid='todo-list'] h1\");\n      await reactGrab.clickContextMenuItem(\"Copy styles\");\n\n      await expect\n        .poll(() => reactGrab.getClipboardContent(), { timeout: 5000 })\n        .toMatch(/[\\w-]+:\\s*.+;/);\n    });\n\n    test(\"should include className header when element has a class\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='todo-list'] h1\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"[data-testid='todo-list'] h1\");\n      await reactGrab.clickContextMenuItem(\"Copy styles\");\n\n      await expect\n        .poll(() => reactGrab.getClipboardContent(), { timeout: 5000 })\n        .toContain(\"className:\");\n    });\n\n    test(\"should contain CSS property-value pairs\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='submit-button']\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"[data-testid='submit-button']\");\n      await reactGrab.clickContextMenuItem(\"Copy styles\");\n\n      await expect\n        .poll(() => reactGrab.getClipboardContent(), { timeout: 5000 })\n        .toMatch(/[\\w-]+:\\s*.+;/);\n\n      const content = await reactGrab.getClipboardContent();\n      const hasRelevantProperty =\n        content.includes(\"background-color:\") ||\n        content.includes(\"color:\") ||\n        content.includes(\"padding-\");\n      expect(hasRelevantProperty).toBe(true);\n    });\n  });\n\n  test.describe(\"Feedback\", () => {\n    test(\"should show Copied feedback after Copy styles\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='submit-button']\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"[data-testid='submit-button']\");\n      await reactGrab.clickContextMenuItem(\"Copy styles\");\n\n      await expect\n        .poll(() => reactGrab.getLabelStatusText(), { timeout: 5000 })\n        .toBe(\"Copied\");\n    });\n\n    test(\"should dismiss context menu after Copy styles action\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li:first-child\");\n      await reactGrab.clickContextMenuItem(\"Copy styles\");\n\n      await expect\n        .poll(() => reactGrab.isContextMenuVisible(), { timeout: 2000 })\n        .toBe(false);\n    });\n  });\n\n  test.describe(\"Different Elements\", () => {\n    test(\"should copy CSS for element with background and color styles\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='gradient-div']\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"[data-testid='gradient-div']\");\n      await reactGrab.clickContextMenuItem(\"Copy styles\");\n\n      await expect\n        .poll(() => reactGrab.getClipboardContent(), { timeout: 5000 })\n        .toMatch(/[\\w-]+:\\s*.+;/);\n\n      const clipboardContent = await reactGrab.getClipboardContent();\n      expect(clipboardContent).toMatch(/width:|height:|background/);\n    });\n\n    test(\"should produce output for a plain element with no custom styles\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='deeply-nested-text']\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"[data-testid='deeply-nested-text']\");\n      await reactGrab.clickContextMenuItem(\"Copy styles\");\n\n      await expect\n        .poll(() => reactGrab.getClipboardContent(), { timeout: 5000 })\n        .toBeTruthy();\n    });\n\n    test(\"should copy different CSS for different elements\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='submit-button']\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"[data-testid='submit-button']\");\n      await reactGrab.clickContextMenuItem(\"Copy styles\");\n\n      await expect\n        .poll(() => reactGrab.getClipboardContent(), { timeout: 5000 })\n        .toMatch(/[\\w-]+:\\s*.+;/);\n\n      const firstCss = await reactGrab.getClipboardContent();\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='todo-list'] h1\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"[data-testid='todo-list'] h1\");\n      await reactGrab.clickContextMenuItem(\"Copy styles\");\n\n      await expect\n        .poll(\n          async () => {\n            const content = await reactGrab.getClipboardContent();\n            return content !== firstCss;\n          },\n          { timeout: 5000 },\n        )\n        .toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/disabled-elements.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\nconst CONTAINER_ID = \"disabled-test-container\";\n\ntest.describe(\"Disabled Element Selection\", () => {\n  test.beforeEach(async ({ reactGrab }) => {\n    await reactGrab.page.evaluate((containerId) => {\n      const container = document.createElement(\"div\");\n      container.id = containerId;\n      container.innerHTML = `\n        <div style=\"padding: 20px; margin: 20px; border: 1px solid #ccc;\">\n          <h3>Disabled Elements Test</h3>\n          <button disabled data-testid=\"disabled-button\" style=\"padding: 10px 20px;\">\n            Disabled Button\n          </button>\n          <input disabled data-testid=\"disabled-input\" value=\"Disabled Input\" style=\"margin-left: 10px; padding: 10px;\" />\n          <select disabled data-testid=\"disabled-select\" style=\"margin-left: 10px; padding: 10px;\">\n            <option>Disabled Select</option>\n          </select>\n          <textarea disabled data-testid=\"disabled-textarea\" style=\"margin-left: 10px; padding: 10px;\">Disabled Textarea</textarea>\n        </div>\n      `;\n      document.body.insertBefore(container, document.body.firstChild);\n    }, CONTAINER_ID);\n  });\n\n  test.afterEach(async ({ reactGrab }) => {\n    await reactGrab.page.evaluate((containerId) => {\n      document.getElementById(containerId)?.remove();\n    }, CONTAINER_ID);\n  });\n\n  test(\"should select disabled button element\", async ({ reactGrab }) => {\n    await reactGrab.activate();\n\n    const bounds = await reactGrab.getElementBounds(\n      \"[data-testid='disabled-button']\",\n    );\n    if (!bounds) throw new Error(\"Could not get element bounds\");\n\n    await reactGrab.page.mouse.move(\n      bounds.x + bounds.width / 2,\n      bounds.y + bounds.height / 2,\n    );\n    await reactGrab.waitForSelectionBox();\n\n    const isVisible = await reactGrab.isSelectionBoxVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"should select disabled input element\", async ({ reactGrab }) => {\n    await reactGrab.activate();\n\n    const bounds = await reactGrab.getElementBounds(\n      \"[data-testid='disabled-input']\",\n    );\n    if (!bounds) throw new Error(\"Could not get element bounds\");\n\n    await reactGrab.page.mouse.move(\n      bounds.x + bounds.width / 2,\n      bounds.y + bounds.height / 2,\n    );\n    await reactGrab.waitForSelectionBox();\n\n    const isVisible = await reactGrab.isSelectionBoxVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"should select disabled textarea element\", async ({ reactGrab }) => {\n    await reactGrab.activate();\n\n    const bounds = await reactGrab.getElementBounds(\n      \"[data-testid='disabled-textarea']\",\n    );\n    if (!bounds) throw new Error(\"Could not get element bounds\");\n\n    await reactGrab.page.mouse.move(\n      bounds.x + bounds.width / 2,\n      bounds.y + bounds.height / 2,\n    );\n    await reactGrab.waitForSelectionBox();\n\n    const isVisible = await reactGrab.isSelectionBoxVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"should select disabled select element\", async ({ reactGrab }) => {\n    await reactGrab.activate();\n\n    const bounds = await reactGrab.getElementBounds(\n      \"[data-testid='disabled-select']\",\n    );\n    if (!bounds) throw new Error(\"Could not get element bounds\");\n\n    await reactGrab.page.mouse.move(\n      bounds.x + bounds.width / 2,\n      bounds.y + bounds.height / 2,\n    );\n    await reactGrab.waitForSelectionBox();\n\n    const isVisible = await reactGrab.isSelectionBoxVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"should select element with pointer-events none\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.page.evaluate(() => {\n      const container = document.getElementById(\"disabled-test-container\");\n      const pointerEventsNoneElement = document.createElement(\"div\");\n      pointerEventsNoneElement.setAttribute(\n        \"data-testid\",\n        \"pointer-events-none\",\n      );\n      pointerEventsNoneElement.style.cssText =\n        \"pointer-events: none; padding: 20px; background: #f0f0f0; margin-top: 10px;\";\n      pointerEventsNoneElement.textContent = \"Pointer Events None Element\";\n      container?.appendChild(pointerEventsNoneElement);\n    });\n\n    await reactGrab.activate();\n\n    const bounds = await reactGrab.getElementBounds(\n      \"[data-testid='pointer-events-none']\",\n    );\n    if (!bounds) throw new Error(\"Could not get element bounds\");\n\n    await reactGrab.page.mouse.move(\n      bounds.x + bounds.width / 2,\n      bounds.y + bounds.height / 2,\n    );\n    await reactGrab.waitForSelectionBox();\n\n    const isVisible = await reactGrab.isSelectionBoxVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"should copy element with pointer-events none via click\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.page.evaluate(() => {\n      const container = document.getElementById(\"disabled-test-container\");\n      const pointerEventsNoneElement = document.createElement(\"div\");\n      pointerEventsNoneElement.setAttribute(\n        \"data-testid\",\n        \"pointer-events-none\",\n      );\n      pointerEventsNoneElement.style.cssText =\n        \"pointer-events: none; padding: 20px; background: #f0f0f0; margin-top: 10px;\";\n      pointerEventsNoneElement.textContent = \"Pointer Events None Content\";\n      container?.appendChild(pointerEventsNoneElement);\n    });\n\n    await reactGrab.activate();\n\n    const bounds = await reactGrab.getElementBounds(\n      \"[data-testid='pointer-events-none']\",\n    );\n    if (!bounds) throw new Error(\"Could not get element bounds\");\n\n    await reactGrab.page.mouse.move(\n      bounds.x + bounds.width / 2,\n      bounds.y + bounds.height / 2,\n    );\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.page.mouse.click(\n      bounds.x + bounds.width / 2,\n      bounds.y + bounds.height / 2,\n    );\n    await expect\n      .poll(() => reactGrab.getClipboardContent())\n      .toContain(\"Pointer Events None\");\n  });\n\n  test(\"should select nested disabled element inside enabled parent\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.page.evaluate(() => {\n      const container = document.getElementById(\"disabled-test-container\");\n      const wrapper = document.createElement(\"div\");\n      wrapper.setAttribute(\"data-testid\", \"enabled-wrapper\");\n      wrapper.style.cssText =\n        \"padding: 20px; background: #e0e0e0; margin-top: 10px;\";\n      wrapper.innerHTML = `\n        <span>Enabled wrapper</span>\n        <button disabled data-testid=\"nested-disabled-button\" style=\"margin-left: 10px; padding: 10px;\">\n          Nested Disabled\n        </button>\n      `;\n      container?.appendChild(wrapper);\n    });\n\n    await reactGrab.activate();\n\n    const bounds = await reactGrab.getElementBounds(\n      \"[data-testid='nested-disabled-button']\",\n    );\n    if (!bounds) throw new Error(\"Could not get element bounds\");\n\n    await reactGrab.page.mouse.move(\n      bounds.x + bounds.width / 2,\n      bounds.y + bounds.height / 2,\n    );\n    await reactGrab.waitForSelectionBox();\n\n    const isVisible = await reactGrab.isSelectionBoxVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"should include disabled elements in drag selection\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n\n    const containerBounds = await reactGrab.getElementBounds(\n      \"#disabled-test-container\",\n    );\n    if (!containerBounds) throw new Error(\"Could not get container bounds\");\n\n    await reactGrab.page.mouse.move(\n      containerBounds.x - 10,\n      containerBounds.y - 10,\n    );\n    await reactGrab.page.mouse.down();\n    await reactGrab.page.mouse.move(\n      containerBounds.x + containerBounds.width + 10,\n      containerBounds.y + containerBounds.height + 10,\n      { steps: 10 },\n    );\n    await reactGrab.page.mouse.up();\n    await reactGrab.page.waitForTimeout(500);\n\n    await expect\n      .poll(async () => {\n        const info = await reactGrab.getGrabbedBoxInfo();\n        return info.count;\n      })\n      .toBeGreaterThanOrEqual(1);\n  });\n});\n\ntest.describe(\"Pointer Events None - Arrow Navigation\", () => {\n  test.beforeEach(async ({ reactGrab }) => {\n    await reactGrab.page.evaluate((containerId) => {\n      const container = document.createElement(\"div\");\n      container.id = containerId;\n      document.body.insertBefore(container, document.body.firstChild);\n    }, CONTAINER_ID);\n  });\n\n  test.afterEach(async ({ reactGrab }) => {\n    await reactGrab.page.evaluate((containerId) => {\n      document.getElementById(containerId)?.remove();\n    }, CONTAINER_ID);\n  });\n\n  test(\"should support ArrowUp from pointer-events none element\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.page.evaluate((containerId) => {\n      const container = document.getElementById(containerId);\n      const parent = document.createElement(\"div\");\n      parent.setAttribute(\"data-testid\", \"arrow-up-parent\");\n      parent.style.cssText =\n        \"padding: 40px; background: #d0d0d0; margin-top: 10px;\";\n      const child = document.createElement(\"div\");\n      child.setAttribute(\"data-testid\", \"arrow-up-child\");\n      child.style.cssText =\n        \"pointer-events: none; padding: 20px; background: #f0f0f0;\";\n      child.textContent = \"Pointer Events None Child\";\n      parent.appendChild(child);\n      container?.appendChild(parent);\n    }, CONTAINER_ID);\n\n    await reactGrab.activate();\n\n    const bounds = await reactGrab.getElementBounds(\n      \"[data-testid='arrow-up-child']\",\n    );\n    if (!bounds) throw new Error(\"Could not get element bounds\");\n    await reactGrab.page.mouse.move(\n      bounds.x + bounds.width / 2,\n      bounds.y + bounds.height / 2,\n    );\n    await reactGrab.waitForSelectionBox();\n    expect(await reactGrab.isSelectionBoxVisible()).toBe(true);\n\n    await reactGrab.pressArrowUp();\n    await reactGrab.waitForSelectionBox();\n    expect(await reactGrab.isSelectionBoxVisible()).toBe(true);\n  });\n\n  test(\"should support ArrowDown back to pointer-events none element\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.page.evaluate((containerId) => {\n      const container = document.getElementById(containerId);\n      const parent = document.createElement(\"div\");\n      parent.setAttribute(\"data-testid\", \"arrow-down-parent\");\n      parent.style.cssText =\n        \"padding: 40px; background: #d0d0d0; margin-top: 10px;\";\n      const child = document.createElement(\"div\");\n      child.setAttribute(\"data-testid\", \"arrow-down-child\");\n      child.style.cssText =\n        \"pointer-events: none; padding: 20px; background: #f0f0f0;\";\n      child.textContent = \"Pointer Events None Child\";\n      parent.appendChild(child);\n      container?.appendChild(parent);\n    }, CONTAINER_ID);\n\n    await reactGrab.activate();\n\n    const bounds = await reactGrab.getElementBounds(\n      \"[data-testid='arrow-down-child']\",\n    );\n    if (!bounds) throw new Error(\"Could not get element bounds\");\n    await reactGrab.page.mouse.move(\n      bounds.x + bounds.width / 2,\n      bounds.y + bounds.height / 2,\n    );\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.pressArrowUp();\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.pressArrowDown();\n    await reactGrab.waitForSelectionBox();\n    expect(await reactGrab.isSelectionBoxVisible()).toBe(true);\n  });\n\n  test(\"should support round-trip navigation\", async ({ reactGrab }) => {\n    await reactGrab.page.evaluate((containerId) => {\n      const container = document.getElementById(containerId);\n      const parent = document.createElement(\"div\");\n      parent.setAttribute(\"data-testid\", \"round-trip-parent\");\n      parent.style.cssText =\n        \"padding: 40px; background: #d0d0d0; margin-top: 10px;\";\n      const child = document.createElement(\"div\");\n      child.setAttribute(\"data-testid\", \"round-trip-child\");\n      child.style.cssText =\n        \"pointer-events: none; padding: 20px; background: #f0f0f0;\";\n      child.textContent = \"Pointer Events None Child\";\n      parent.appendChild(child);\n      container?.appendChild(parent);\n    }, CONTAINER_ID);\n\n    await reactGrab.activate();\n\n    const bounds = await reactGrab.getElementBounds(\n      \"[data-testid='round-trip-child']\",\n    );\n    if (!bounds) throw new Error(\"Could not get element bounds\");\n    await reactGrab.page.mouse.move(\n      bounds.x + bounds.width / 2,\n      bounds.y + bounds.height / 2,\n    );\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.pressArrowUp();\n    await reactGrab.waitForSelectionBox();\n    expect(await reactGrab.isSelectionBoxVisible()).toBe(true);\n\n    await reactGrab.pressArrowDown();\n    await reactGrab.waitForSelectionBox();\n    expect(await reactGrab.isSelectionBoxVisible()).toBe(true);\n\n    await reactGrab.pressArrowUp();\n    await reactGrab.waitForSelectionBox();\n    expect(await reactGrab.isSelectionBoxVisible()).toBe(true);\n  });\n\n  test(\"should navigate through nested pointer-events none elements\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.page.evaluate((containerId) => {\n      const container = document.getElementById(containerId);\n      const grandparent = document.createElement(\"div\");\n      grandparent.setAttribute(\"data-testid\", \"nested-grandparent\");\n      grandparent.style.cssText =\n        \"padding: 60px; background: #c0c0c0; margin-top: 10px;\";\n      const parent = document.createElement(\"div\");\n      parent.setAttribute(\"data-testid\", \"nested-parent\");\n      parent.style.cssText =\n        \"pointer-events: none; padding: 40px; background: #d0d0d0;\";\n      const child = document.createElement(\"div\");\n      child.setAttribute(\"data-testid\", \"nested-child\");\n      child.style.cssText =\n        \"pointer-events: none; padding: 20px; background: #f0f0f0;\";\n      child.textContent = \"Deeply Nested Pointer Events None\";\n      parent.appendChild(child);\n      grandparent.appendChild(parent);\n      container?.appendChild(grandparent);\n    }, CONTAINER_ID);\n\n    await reactGrab.activate();\n\n    const bounds = await reactGrab.getElementBounds(\n      \"[data-testid='nested-child']\",\n    );\n    if (!bounds) throw new Error(\"Could not get element bounds\");\n    await reactGrab.page.mouse.move(\n      bounds.x + bounds.width / 2,\n      bounds.y + bounds.height / 2,\n    );\n    await reactGrab.waitForSelectionBox();\n    expect(await reactGrab.isSelectionBoxVisible()).toBe(true);\n\n    await reactGrab.pressArrowUp();\n    await reactGrab.waitForSelectionBox();\n    expect(await reactGrab.isSelectionBoxVisible()).toBe(true);\n\n    await reactGrab.pressArrowUp();\n    await reactGrab.waitForSelectionBox();\n    expect(await reactGrab.isSelectionBoxVisible()).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/drag-selection.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\ntest.describe(\"Drag Selection\", () => {\n  test(\"should create drag box when clicking and dragging\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n\n    const firstItem = reactGrab.page.locator(\"li\").first();\n    const firstBox = await firstItem.boundingBox();\n    if (!firstBox) throw new Error(\"Could not get bounding box\");\n\n    const startX = firstBox.x - 20;\n    const startY = firstBox.y - 20;\n\n    await reactGrab.page.mouse.move(startX, startY);\n    await reactGrab.page.mouse.down();\n    await reactGrab.page.waitForTimeout(50);\n\n    await reactGrab.page.mouse.move(startX + 100, startY + 100, { steps: 5 });\n    await reactGrab.page.waitForTimeout(100);\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(true);\n\n    await reactGrab.page.mouse.up();\n  });\n\n  test(\"should select multiple elements within drag bounds\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n\n    await reactGrab.dragSelect(\"li:first-child\", \"li:nth-child(3)\");\n    await reactGrab.page.waitForTimeout(500);\n\n    const clipboardContent = await reactGrab.getClipboardContent();\n    expect(clipboardContent).toBeTruthy();\n    expect(clipboardContent.length).toBeGreaterThan(0);\n  });\n\n  test(\"should copy all selected elements to clipboard\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n\n    await reactGrab.dragSelect(\"li:first-child\", \"li:nth-child(5)\");\n    await reactGrab.page.waitForTimeout(500);\n\n    const clipboardContent = await reactGrab.getClipboardContent();\n\n    expect(clipboardContent).toContain(\"Buy groceries\");\n  });\n\n  test(\"should cancel drag selection on Escape\", async ({ reactGrab }) => {\n    await reactGrab.activate();\n\n    const firstItem = reactGrab.page.locator(\"li\").first();\n    const firstBox = await firstItem.boundingBox();\n    if (!firstBox) throw new Error(\"Could not get bounding box\");\n\n    await reactGrab.page.mouse.move(firstBox.x - 10, firstBox.y - 10);\n    await reactGrab.page.mouse.down();\n    await reactGrab.page.mouse.move(firstBox.x + 200, firstBox.y + 200, {\n      steps: 5,\n    });\n\n    await reactGrab.pressEscape();\n    await reactGrab.page.mouse.up();\n\n    await reactGrab.page.waitForTimeout(100);\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(false);\n  });\n\n  test(\"should not trigger drag for small movements\", async ({ reactGrab }) => {\n    await reactGrab.activate();\n\n    const listItem = reactGrab.page.locator(\"li\").first();\n    const box = await listItem.boundingBox();\n    if (!box) throw new Error(\"Could not get bounding box\");\n\n    const centerX = box.x + box.width / 2;\n    const centerY = box.y + box.height / 2;\n\n    await reactGrab.page.mouse.move(centerX, centerY);\n    await reactGrab.page.mouse.down();\n    await reactGrab.page.mouse.move(centerX + 1, centerY + 1);\n    await reactGrab.page.mouse.up();\n\n    await reactGrab.page.waitForTimeout(500);\n\n    const clipboardContent = await reactGrab.getClipboardContent();\n    expect(clipboardContent).toBeTruthy();\n  });\n\n  test(\"should deactivate after drag selection in toggle mode\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n\n    await reactGrab.dragSelect(\"li:first-child\", \"li:nth-child(2)\");\n\n    await reactGrab.page.waitForTimeout(2000);\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(false);\n  });\n\n  test(\"should handle drag across entire list\", async ({ reactGrab }) => {\n    await reactGrab.activate();\n\n    await reactGrab.dragSelect(\n      \"[data-testid='todo-list'] li:first-child\",\n      \"[data-testid='todo-list'] li:last-child\",\n    );\n    await reactGrab.page.waitForTimeout(500);\n\n    const clipboardContent = await reactGrab.getClipboardContent();\n    expect(clipboardContent).toBeTruthy();\n    expect(clipboardContent).toContain(\"Buy groceries\");\n    expect(clipboardContent).toContain(\"Write tests\");\n  });\n\n  test(\"should show visual feedback during drag\", async ({ reactGrab }) => {\n    await reactGrab.activate();\n\n    const firstItem = reactGrab.page.locator(\"li\").first();\n    const lastItem = reactGrab.page.locator(\"li\").last();\n\n    const startBox = await firstItem.boundingBox();\n    const endBox = await lastItem.boundingBox();\n    if (!startBox || !endBox) throw new Error(\"Could not get bounding boxes\");\n\n    await reactGrab.page.mouse.move(startBox.x - 10, startBox.y - 10);\n    await reactGrab.page.mouse.down();\n\n    await reactGrab.page.mouse.move(\n      endBox.x + endBox.width + 10,\n      endBox.y + endBox.height + 10,\n      { steps: 10 },\n    );\n\n    const hasContent = await reactGrab.page.evaluate(() => {\n      const host = document.querySelector(\"[data-react-grab]\");\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return false;\n      const root = shadowRoot.querySelector(\"[data-react-grab]\");\n      return root !== null && root.innerHTML.length > 0;\n    });\n\n    expect(hasContent).toBe(true);\n\n    await reactGrab.page.mouse.up();\n  });\n});\n\ntest.describe(\"Drag Selection with Scroll\", () => {\n  test(\"should handle drag selection with scroll offset\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.scrollPage(100);\n    await reactGrab.page.waitForTimeout(100);\n\n    await reactGrab.activate();\n    await reactGrab.dragSelect(\"li:first-child\", \"li:nth-child(2)\");\n    await reactGrab.page.waitForTimeout(500);\n\n    const clipboardContent = await reactGrab.getClipboardContent();\n    expect(clipboardContent).toBeTruthy();\n  });\n\n  test(\"should maintain drag while scrolling\", async ({ reactGrab }) => {\n    await reactGrab.activate();\n\n    const firstItem = reactGrab.page.locator(\"li\").first();\n    const firstBox = await firstItem.boundingBox();\n    if (!firstBox) throw new Error(\"Could not get bounding box\");\n\n    await reactGrab.page.mouse.move(firstBox.x - 10, firstBox.y - 10);\n    await reactGrab.page.mouse.down();\n    await reactGrab.page.mouse.move(firstBox.x + 100, firstBox.y + 100, {\n      steps: 5,\n    });\n\n    await reactGrab.scrollPage(50);\n    await reactGrab.page.waitForTimeout(100);\n\n    await reactGrab.page.mouse.up();\n\n    const state = await reactGrab.getState();\n    expect(state).toBeDefined();\n  });\n\n  test(\"should select elements after scrolling down\", async ({ reactGrab }) => {\n    await reactGrab.activate();\n    await reactGrab.scrollPage(300);\n    await reactGrab.page.waitForTimeout(200);\n\n    const listItems = reactGrab.page.locator(\"li\");\n    const count = await listItems.count();\n\n    if (count > 0) {\n      await reactGrab.dragSelect(\"li:first-child\", \"li:nth-child(2)\");\n      await reactGrab.page.waitForTimeout(500);\n\n      const clipboardContent = await reactGrab.getClipboardContent();\n      expect(clipboardContent).toBeTruthy();\n    }\n  });\n\n  test(\"drag bounds should exist during drag operation\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n\n    const firstItem = reactGrab.page.locator(\"li\").first();\n    const firstBox = await firstItem.boundingBox();\n    if (!firstBox) throw new Error(\"Could not get bounding box\");\n\n    await reactGrab.page.mouse.move(firstBox.x - 10, firstBox.y - 10);\n    await reactGrab.page.mouse.down();\n    await reactGrab.page.mouse.move(firstBox.x + 200, firstBox.y + 200, {\n      steps: 5,\n    });\n    await reactGrab.page.waitForTimeout(100);\n\n    const bounds = await reactGrab.getDragBoxBounds();\n    expect(bounds).not.toBeNull();\n\n    await reactGrab.page.mouse.up();\n  });\n\n  test(\"drag selection should work in scrollable container\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n\n    const scrollContainer = reactGrab.page.locator(\n      \"[data-testid='scroll-container']\",\n    );\n    const box = await scrollContainer.boundingBox();\n\n    if (box) {\n      await reactGrab.page.mouse.move(box.x + 10, box.y + 10);\n      await reactGrab.page.mouse.down();\n      await reactGrab.page.mouse.move(box.x + 200, box.y + 100, { steps: 5 });\n      await reactGrab.page.mouse.up();\n      await reactGrab.page.waitForTimeout(500);\n\n      const clipboardContent = await reactGrab.getClipboardContent();\n      expect(clipboardContent).toBeTruthy();\n    }\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/edge-cases.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\ntest.describe(\"Edge Cases\", () => {\n  test.describe(\"Element Removal\", () => {\n    test(\"should handle element removed during hover\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='dynamic-element-1']\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.removeElement(\"[data-testid='dynamic-element-1']\");\n      await reactGrab.page.waitForTimeout(200);\n\n      const isActive = await reactGrab.isOverlayVisible();\n      expect(isActive).toBe(true);\n    });\n\n    test(\"should handle element removed during drag\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n\n      const element = reactGrab.page.locator(\n        \"[data-testid='dynamic-element-1']\",\n      );\n      const box = await element.boundingBox();\n      if (!box) throw new Error(\"Could not get bounding box\");\n\n      await reactGrab.page.mouse.move(box.x - 10, box.y - 10);\n      await reactGrab.page.mouse.down();\n      await reactGrab.page.mouse.move(box.x + 50, box.y + 50, { steps: 3 });\n\n      await reactGrab.removeElement(\"[data-testid='dynamic-element-1']\");\n      await reactGrab.page.waitForTimeout(100);\n\n      await reactGrab.page.mouse.up();\n\n      const isActive = await reactGrab.isOverlayVisible();\n      expect(typeof isActive).toBe(\"boolean\");\n    });\n\n    test(\"should recover after target element is removed\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='toggleable-element']\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.removeElement(\"[data-testid='toggleable-element']\");\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      const isVisible = await reactGrab.isSelectionBoxVisible();\n      expect(isVisible).toBe(true);\n    });\n  });\n\n  test.describe(\"Rapid Actions\", () => {\n    test(\"should handle rapid activation/deactivation cycles\", async ({\n      reactGrab,\n    }) => {\n      for (let i = 0; i < 10; i++) {\n        await reactGrab.activate();\n        await reactGrab.page.waitForTimeout(20);\n        await reactGrab.deactivate();\n        await reactGrab.page.waitForTimeout(20);\n      }\n\n      const state = await reactGrab.getState();\n      expect(typeof state.isActive).toBe(\"boolean\");\n    });\n\n    test(\"should handle rapid hover changes\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n\n      const elements = [\n        \"li:first-child\",\n        \"li:nth-child(2)\",\n        \"li:nth-child(3)\",\n        \"h1\",\n        \"ul\",\n      ];\n      for (const selector of elements) {\n        await reactGrab.hoverElement(selector);\n        await reactGrab.page.waitForTimeout(10);\n      }\n\n      const isActive = await reactGrab.isOverlayVisible();\n      expect(isActive).toBe(true);\n    });\n\n    test(\"should handle rapid clicks\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      for (let i = 0; i < 5; i++) {\n        await reactGrab.clickElement(\"li:first-child\");\n        await reactGrab.page.waitForTimeout(50);\n      }\n\n      await reactGrab.page.waitForTimeout(500);\n\n      const clipboard = await reactGrab.getClipboardContent();\n      expect(clipboard).toBeTruthy();\n    });\n\n    test(\"should handle rapid toggle calls\", async ({ reactGrab }) => {\n      for (let i = 0; i < 8; i++) {\n        await reactGrab.toggle();\n        await reactGrab.page.waitForTimeout(30);\n      }\n\n      const state = await reactGrab.getState();\n      expect(typeof state.isActive).toBe(\"boolean\");\n    });\n  });\n\n  test.describe(\"Visibility Changes\", () => {\n    test(\"should handle tab visibility change\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n\n      await reactGrab.page.evaluate(() => {\n        document.dispatchEvent(new Event(\"visibilitychange\"));\n        Object.defineProperty(document, \"hidden\", {\n          value: true,\n          writable: true,\n        });\n        document.dispatchEvent(new Event(\"visibilitychange\"));\n      });\n\n      await reactGrab.page.waitForTimeout(100);\n\n      await reactGrab.page.evaluate(() => {\n        Object.defineProperty(document, \"hidden\", {\n          value: false,\n          writable: true,\n        });\n        document.dispatchEvent(new Event(\"visibilitychange\"));\n      });\n\n      const state = await reactGrab.getState();\n      expect(state).toBeDefined();\n    });\n\n    test(\"should handle window blur and focus\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.page.evaluate(() => {\n        window.dispatchEvent(new Event(\"blur\"));\n      });\n      await reactGrab.page.waitForTimeout(100);\n\n      await reactGrab.page.evaluate(() => {\n        window.dispatchEvent(new Event(\"focus\"));\n      });\n      await reactGrab.page.waitForTimeout(100);\n\n      const state = await reactGrab.getState();\n      expect(state).toBeDefined();\n    });\n  });\n\n  test.describe(\"Scroll and Resize\", () => {\n    test(\"should handle scroll during drag operation\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n\n      const listItem = reactGrab.page.locator(\"li\").first();\n      const box = await listItem.boundingBox();\n      if (!box) throw new Error(\"Could not get bounding box\");\n\n      await reactGrab.page.mouse.move(box.x, box.y);\n      await reactGrab.page.mouse.down();\n      await reactGrab.page.mouse.move(box.x + 50, box.y + 50, { steps: 3 });\n\n      await reactGrab.scrollPage(100);\n\n      await reactGrab.page.mouse.up();\n\n      const state = await reactGrab.getState();\n      expect(state).toBeDefined();\n    });\n\n    test(\"should handle resize during selection\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.setViewportSize(800, 600);\n      await reactGrab.page.waitForTimeout(200);\n\n      const isActive = await reactGrab.isOverlayVisible();\n      expect(isActive).toBe(true);\n\n      await reactGrab.setViewportSize(1280, 720);\n    });\n\n    test(\"should handle rapid scroll events\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      for (let i = 0; i < 5; i++) {\n        await reactGrab.page.evaluate(() => {\n          window.scrollBy(0, 50);\n        });\n        await reactGrab.page.waitForTimeout(20);\n      }\n      await reactGrab.page.waitForTimeout(200);\n\n      const isActive = await reactGrab.isOverlayVisible();\n      expect(isActive).toBe(true);\n    });\n  });\n\n  test.describe(\"Memory and Cleanup\", () => {\n    test(\"dispose should clean up properly\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.dispose();\n      await reactGrab.page.waitForTimeout(200);\n\n      const canReinit = await reactGrab.page.evaluate(() => {\n        const initFn = (window as { initReactGrab?: () => void }).initReactGrab;\n        return typeof initFn === \"function\";\n      });\n      expect(canReinit).toBe(true);\n    });\n\n    test(\"should allow reinitialization after dispose\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.dispose();\n\n      await reactGrab.reinitialize();\n\n      await reactGrab.activate();\n      const isActive = await reactGrab.isOverlayVisible();\n      expect(isActive).toBe(true);\n    });\n\n    test(\"double initialization should be prevented\", async ({ reactGrab }) => {\n      await reactGrab.reinitialize();\n      await reactGrab.page.waitForTimeout(200);\n\n      const hostCount = await reactGrab.page.evaluate(() => {\n        return document.querySelectorAll(\"[data-react-grab]\").length;\n      });\n      expect(hostCount).toBe(1);\n    });\n  });\n\n  test.describe(\"Focus Management\", () => {\n    test(\"should restore focus to previously focused element\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.page.click(\"[data-testid='test-input']\");\n      await reactGrab.page.waitForTimeout(100);\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.clickElement(\"li:first-child\");\n\n      await expect\n        .poll(\n          () =>\n            reactGrab.page.evaluate(() =>\n              document.activeElement?.getAttribute(\"data-testid\"),\n            ),\n          { timeout: 5000 },\n        )\n        .toBe(\"test-input\");\n    });\n  });\n\n  test.describe(\"Context Menu Edge Cases\", () => {\n    test(\"should handle context menu on removed element\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='dynamic-element-3']\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.rightClickElement(\"[data-testid='dynamic-element-3']\");\n\n      await reactGrab.removeElement(\"[data-testid='dynamic-element-3']\");\n      await reactGrab.page.waitForTimeout(100);\n\n      await reactGrab.pressEscape();\n\n      const isActive = await reactGrab.isOverlayVisible();\n      expect(typeof isActive).toBe(\"boolean\");\n    });\n  });\n\n  test.describe(\"Copy Edge Cases\", () => {\n    test(\"should handle copy during visibility change\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.clickElement(\"li:first-child\");\n\n      await reactGrab.page.evaluate(() => {\n        document.dispatchEvent(new Event(\"visibilitychange\"));\n      });\n\n      await reactGrab.page.waitForTimeout(500);\n\n      const clipboard = await reactGrab.getClipboardContent();\n      expect(clipboard).toBeTruthy();\n    });\n  });\n\n  test.describe(\"Viewport Edge Cases\", () => {\n    test(\"should handle elements outside viewport\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n\n      const footer = reactGrab.page.locator(\"[data-testid='footer']\");\n      await footer.scrollIntoViewIfNeeded();\n      await reactGrab.page.waitForTimeout(200);\n\n      await reactGrab.hoverElement(\"[data-testid='footer']\");\n      await reactGrab.waitForSelectionBox();\n\n      const isActive = await reactGrab.isOverlayVisible();\n      expect(isActive).toBe(true);\n    });\n\n    test(\"should handle zero-dimension elements gracefully\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n\n      await reactGrab.page.mouse.move(100, 100);\n      await reactGrab.page.waitForTimeout(100);\n\n      const isActive = await reactGrab.isOverlayVisible();\n      expect(isActive).toBe(true);\n    });\n\n    test(\"should handle invisible elements\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n\n      await reactGrab.page.mouse.move(200, 200);\n      await reactGrab.page.waitForTimeout(100);\n\n      const isActive = await reactGrab.isOverlayVisible();\n      expect(isActive).toBe(true);\n    });\n  });\n\n  test.describe(\"State Consistency\", () => {\n    test(\"getState should be consistent across calls\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n\n      const state1 = await reactGrab.getState();\n      const state2 = await reactGrab.getState();\n\n      expect(state1.isActive).toBe(state2.isActive);\n      expect(state1.isDragging).toBe(state2.isDragging);\n      expect(state1.isCopying).toBe(state2.isCopying);\n    });\n\n    test(\"state should be correct after complex interaction sequence\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.pressArrowDown();\n      await reactGrab.page.waitForTimeout(100);\n\n      await reactGrab.rightClickElement(\"li:nth-child(2)\");\n      await reactGrab.page.waitForTimeout(100);\n\n      const state = await reactGrab.getState();\n      expect(state.isActive).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/element-context.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\ntest.describe(\"Element Context Fallback\", () => {\n  test.describe(\"React Elements\", () => {\n    test(\"should include component names in clipboard for React elements\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n\n      await reactGrab.hoverElement(\"[data-testid='todo-list'] h1\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.clickElement(\"[data-testid='todo-list'] h1\");\n\n      const clipboard = await reactGrab.getClipboardContent();\n      expect(clipboard).toContain(\"TodoList\");\n    });\n\n    test(\"should include HTML preview with tag and content\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n\n      await reactGrab.hoverElement(\"[data-testid='main-title']\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.clickElement(\"[data-testid='main-title']\");\n\n      const clipboard = await reactGrab.getClipboardContent();\n      expect(clipboard).toContain(\"<h1\");\n      expect(clipboard).toContain(\"React Grab\");\n    });\n\n    test(\"should include nested component names for deeply nested elements\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n\n      await reactGrab.hoverElement(\"[data-testid='nested-button']\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.clickElement(\"[data-testid='nested-button']\");\n\n      const clipboard = await reactGrab.getClipboardContent();\n      expect(clipboard).toContain(\"NestedCard\");\n    });\n  });\n\n  test.describe(\"Non-React Elements Fallback\", () => {\n    test(\"should fallback to HTML for plain DOM elements without React fiber\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.page.evaluate(() => {\n        const plainElement = document.createElement(\"div\");\n        plainElement.id = \"plain-dom-element\";\n        plainElement.className = \"test-class\";\n        plainElement.textContent = \"Plain DOM content\";\n        document.body.appendChild(plainElement);\n      });\n\n      await reactGrab.activate();\n\n      await reactGrab.hoverElement(\"#plain-dom-element\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.clickElement(\"#plain-dom-element\");\n\n      const clipboard = await reactGrab.getClipboardContent();\n      expect(clipboard).toContain(\"plain-dom-element\");\n      expect(clipboard).toContain(\"Plain DOM content\");\n    });\n\n    test(\"should include priority attrs for SVG elements\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.page.evaluate(() => {\n        const svgElement = document.createElementNS(\n          \"http://www.w3.org/2000/svg\",\n          \"svg\",\n        );\n        svgElement.id = \"test-svg-icon\";\n        svgElement.setAttribute(\"class\", \"icon-class\");\n        svgElement.setAttribute(\"aria-label\", \"Close the modal dialog\");\n        svgElement.setAttribute(\"viewBox\", \"0 0 24 24\");\n        svgElement.style.width = \"50px\";\n        svgElement.style.height = \"50px\";\n        document.body.appendChild(svgElement);\n      });\n\n      await reactGrab.activate();\n\n      await reactGrab.hoverElement(\"#test-svg-icon\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.clickElement(\"#test-svg-icon\");\n\n      const clipboard = await reactGrab.getClipboardContent();\n      expect(clipboard).toContain(\"<svg\");\n      expect(clipboard).toContain('id=\"test-svg-icon\"');\n      expect(clipboard).toContain('class=\"icon-class\"');\n      expect(clipboard).toContain('aria-label=\"Close the modal dialog\"');\n      expect(clipboard).not.toContain(\"viewBox\");\n    });\n\n    test(\"should truncate long outerHTML to max length\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.page.evaluate(() => {\n        const longElement = document.createElement(\"div\");\n        longElement.id = \"long-dom-element\";\n        longElement.className = \"a\".repeat(300);\n        longElement.textContent = \"b\".repeat(300);\n        document.body.appendChild(longElement);\n      });\n\n      await reactGrab.activate();\n\n      await reactGrab.hoverElement(\"#long-dom-element\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.clickElement(\"#long-dom-element\");\n\n      const clipboard = await reactGrab.getClipboardContent();\n      expect(clipboard).toContain(\"long-dom-element\");\n      expect(clipboard.length).toBeLessThanOrEqual(510);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/event-callbacks.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\ntest.describe(\"Event Callbacks\", () => {\n  test.beforeEach(async ({ reactGrab }) => {\n    await reactGrab.setupCallbackTracking();\n    await reactGrab.clearCallbackHistory();\n  });\n\n  test.describe(\"Activation Callbacks\", () => {\n    test(\"onActivate should fire when overlay is activated\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n\n      const args = await reactGrab.waitForCallback(\"onActivate\", 2000);\n      expect(args).toBeDefined();\n    });\n\n    test(\"onDeactivate should fire when overlay is deactivated\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.clearCallbackHistory();\n\n      await reactGrab.deactivate();\n\n      const args = await reactGrab.waitForCallback(\"onDeactivate\", 2000);\n      expect(args).toBeDefined();\n    });\n\n    test(\"onActivate should fire before onDeactivate in activation cycle\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.deactivate();\n\n      const history = await reactGrab.getCallbackHistory();\n      const activateIndex = history.findIndex((c) => c.name === \"onActivate\");\n      const deactivateIndex = history.findIndex(\n        (c) => c.name === \"onDeactivate\",\n      );\n\n      expect(activateIndex).toBeLessThan(deactivateIndex);\n    });\n\n    test(\"onActivate should only fire once per activation\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.page.waitForTimeout(200);\n\n      const history = await reactGrab.getCallbackHistory();\n      const activateCalls = history.filter((c) => c.name === \"onActivate\");\n\n      expect(activateCalls.length).toBe(1);\n    });\n  });\n\n  test.describe(\"Element Interaction Callbacks\", () => {\n    test(\"onElementHover should fire when hovering over elements\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.clearCallbackHistory();\n\n      await reactGrab.hoverElement(\"li:first-child\");\n\n      await expect\n        .poll(\n          async () => {\n            const history = await reactGrab.getCallbackHistory();\n            const hoverCalls = history.filter(\n              (c) => c.name === \"onElementHover\",\n            );\n            return hoverCalls.length;\n          },\n          { timeout: 2000 },\n        )\n        .toBeGreaterThan(0);\n    });\n\n    test(\"onElementHover should receive element as argument\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.clearCallbackHistory();\n\n      await reactGrab.hoverElement(\"h1\");\n\n      await expect\n        .poll(\n          async () => {\n            const history = await reactGrab.getCallbackHistory();\n            const hoverCalls = history.filter(\n              (c) => c.name === \"onElementHover\",\n            );\n            return hoverCalls.length;\n          },\n          { timeout: 5000 },\n        )\n        .toBeGreaterThan(0);\n    });\n\n    test(\"onElementSelect should fire when element is clicked\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.clearCallbackHistory();\n\n      await reactGrab.clickElement(\"li:first-child\");\n      await reactGrab.page.waitForTimeout(300);\n\n      const history = await reactGrab.getCallbackHistory();\n      const selectCalls = history.filter((c) => c.name === \"onElementSelect\");\n\n      expect(selectCalls.length).toBeGreaterThan(0);\n    });\n\n    test(\"onElementHover should fire for different elements\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.clearCallbackHistory();\n\n      await reactGrab.hoverElement(\"h1\");\n      await reactGrab.page.waitForTimeout(100);\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.page.waitForTimeout(100);\n      await reactGrab.hoverElement(\"ul\");\n      await reactGrab.page.waitForTimeout(100);\n\n      const history = await reactGrab.getCallbackHistory();\n      const hoverCalls = history.filter((c) => c.name === \"onElementHover\");\n\n      expect(hoverCalls.length).toBeGreaterThanOrEqual(3);\n    });\n  });\n\n  test.describe(\"Drag Callbacks\", () => {\n    test(\"onDragStart should fire when drag begins\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.clearCallbackHistory();\n\n      const listItem = reactGrab.page.locator(\"li\").first();\n      const box = await listItem.boundingBox();\n      if (!box) throw new Error(\"Could not get bounding box\");\n\n      await reactGrab.page.mouse.move(box.x - 10, box.y - 10);\n      await reactGrab.page.mouse.down();\n      await reactGrab.page.mouse.move(box.x + 100, box.y + 100, { steps: 5 });\n\n      const history = await reactGrab.getCallbackHistory();\n      const dragStartCalls = history.filter((c) => c.name === \"onDragStart\");\n\n      expect(dragStartCalls.length).toBe(1);\n\n      await reactGrab.page.mouse.up();\n    });\n\n    test(\"onDragEnd should fire when drag completes\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n\n      await reactGrab.dragSelect(\"li:first-child\", \"li:nth-child(3)\");\n      await reactGrab.page.waitForTimeout(300);\n\n      const history = await reactGrab.getCallbackHistory();\n      const dragEndCalls = history.filter((c) => c.name === \"onDragEnd\");\n\n      expect(dragEndCalls.length).toBeGreaterThanOrEqual(1);\n    });\n\n    test(\"onDragStart should include coordinates\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.clearCallbackHistory();\n\n      const listItem = reactGrab.page.locator(\"li\").first();\n      const box = await listItem.boundingBox();\n      if (!box) throw new Error(\"Could not get bounding box\");\n\n      const startX = box.x - 10;\n      const startY = box.y - 10;\n\n      await reactGrab.page.mouse.move(startX, startY);\n      await reactGrab.page.mouse.down();\n      await reactGrab.page.mouse.move(startX + 100, startY + 100, { steps: 5 });\n      await reactGrab.page.mouse.up();\n\n      const history = await reactGrab.getCallbackHistory();\n      const dragStartCalls = history.filter((c) => c.name === \"onDragStart\");\n\n      expect(dragStartCalls.length).toBeGreaterThan(0);\n    });\n  });\n\n  test.describe(\"Copy Callbacks\", () => {\n    test(\"onBeforeCopy should fire before clipboard write\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"h1\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.clearCallbackHistory();\n\n      await reactGrab.clickElement(\"h1\");\n      await reactGrab.page.waitForTimeout(500);\n\n      const history = await reactGrab.getCallbackHistory();\n      const beforeCopyCalls = history.filter((c) => c.name === \"onBeforeCopy\");\n\n      expect(beforeCopyCalls.length).toBeGreaterThan(0);\n    });\n\n    test(\"onAfterCopy should fire after clipboard write\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.clearCallbackHistory();\n\n      await reactGrab.clickElement(\"li:first-child\");\n      await reactGrab.page.waitForTimeout(500);\n\n      const history = await reactGrab.getCallbackHistory();\n      const afterCopyCalls = history.filter((c) => c.name === \"onAfterCopy\");\n\n      expect(afterCopyCalls.length).toBeGreaterThan(0);\n    });\n\n    test(\"onCopySuccess should fire with content on successful copy\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"h1\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.clearCallbackHistory();\n\n      await reactGrab.clickElement(\"h1\");\n      await reactGrab.page.waitForTimeout(500);\n\n      const history = await reactGrab.getCallbackHistory();\n      const successCalls = history.filter((c) => c.name === \"onCopySuccess\");\n\n      expect(successCalls.length).toBeGreaterThan(0);\n    });\n\n    test(\"copy callbacks should fire in correct order\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.clearCallbackHistory();\n\n      await reactGrab.clickElement(\"li:first-child\");\n      await reactGrab.page.waitForTimeout(500);\n\n      const history = await reactGrab.getCallbackHistory();\n      const beforeIndex = history.findIndex((c) => c.name === \"onBeforeCopy\");\n      const afterIndex = history.findIndex((c) => c.name === \"onAfterCopy\");\n\n      if (beforeIndex !== -1 && afterIndex !== -1) {\n        expect(beforeIndex).toBeLessThan(afterIndex);\n      }\n    });\n  });\n\n  test.describe(\"State Change Callback\", () => {\n    test(\"onStateChange should fire on activation\", async ({ reactGrab }) => {\n      await reactGrab.clearCallbackHistory();\n\n      await reactGrab.activate();\n      await reactGrab.page.waitForTimeout(100);\n\n      const history = await reactGrab.getCallbackHistory();\n      const stateChangeCalls = history.filter(\n        (c) => c.name === \"onStateChange\",\n      );\n\n      expect(stateChangeCalls.length).toBeGreaterThan(0);\n    });\n\n    test(\"onStateChange should fire on deactivation\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.clearCallbackHistory();\n\n      await reactGrab.deactivate();\n      await reactGrab.page.waitForTimeout(100);\n\n      const history = await reactGrab.getCallbackHistory();\n      const stateChangeCalls = history.filter(\n        (c) => c.name === \"onStateChange\",\n      );\n\n      expect(stateChangeCalls.length).toBeGreaterThan(0);\n    });\n\n    test(\"onStateChange should fire during drag\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.clearCallbackHistory();\n\n      const listItem = reactGrab.page.locator(\"li\").first();\n      const box = await listItem.boundingBox();\n      if (!box) throw new Error(\"Could not get bounding box\");\n\n      await reactGrab.page.mouse.move(box.x - 10, box.y - 10);\n      await reactGrab.page.mouse.down();\n      await reactGrab.page.mouse.move(box.x + 100, box.y + 100, { steps: 5 });\n      await reactGrab.page.waitForTimeout(100);\n      await reactGrab.page.mouse.up();\n\n      const history = await reactGrab.getCallbackHistory();\n      const stateChangeCalls = history.filter(\n        (c) => c.name === \"onStateChange\",\n      );\n\n      expect(stateChangeCalls.length).toBeGreaterThan(0);\n    });\n  });\n\n  test.describe(\"UI Element Callbacks\", () => {\n    test(\"onSelectionBox should fire when selection box appears\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.clearCallbackHistory();\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.page.waitForTimeout(100);\n\n      const history = await reactGrab.getCallbackHistory();\n      const selectionBoxCalls = history.filter(\n        (c) => c.name === \"onSelectionBox\",\n      );\n\n      expect(selectionBoxCalls.length).toBeGreaterThan(0);\n    });\n\n    test(\"onDragBox should fire during drag selection\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.clearCallbackHistory();\n\n      const listItem = reactGrab.page.locator(\"li\").first();\n      const box = await listItem.boundingBox();\n      if (!box) throw new Error(\"Could not get bounding box\");\n\n      await reactGrab.page.mouse.move(box.x - 20, box.y - 20);\n      await reactGrab.page.mouse.down();\n      await reactGrab.page.mouse.move(box.x + 150, box.y + 150, { steps: 10 });\n      await reactGrab.page.waitForTimeout(100);\n      await reactGrab.page.mouse.up();\n\n      const history = await reactGrab.getCallbackHistory();\n      const dragBoxCalls = history.filter((c) => c.name === \"onDragBox\");\n\n      expect(dragBoxCalls.length).toBeGreaterThan(0);\n    });\n\n    test(\"onGrabbedBox should fire when element is grabbed\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.clearCallbackHistory();\n\n      await reactGrab.clickElement(\"li:first-child\");\n      await reactGrab.page.waitForTimeout(300);\n\n      const history = await reactGrab.getCallbackHistory();\n      const grabbedBoxCalls = history.filter((c) => c.name === \"onGrabbedBox\");\n\n      expect(grabbedBoxCalls.length).toBeGreaterThan(0);\n    });\n  });\n\n  test.describe(\"Context Menu Callback\", () => {\n    test(\"onContextMenu should fire on right-click\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.clearCallbackHistory();\n\n      await reactGrab.rightClickElement(\"li:first-child\");\n\n      const history = await reactGrab.getCallbackHistory();\n      const contextMenuCalls = history.filter(\n        (c) => c.name === \"onContextMenu\",\n      );\n\n      expect(contextMenuCalls.length).toBe(1);\n    });\n  });\n\n  test.describe(\"Callback Integrity\", () => {\n    test(\"callbacks should not fire when overlay is inactive\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.clearCallbackHistory();\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.page.waitForTimeout(100);\n\n      const history = await reactGrab.getCallbackHistory();\n      const hoverCalls = history.filter((c) => c.name === \"onElementHover\");\n\n      expect(hoverCalls.length).toBe(0);\n    });\n\n    test(\"callbacks should include timestamps\", async ({ reactGrab }) => {\n      await reactGrab.clearCallbackHistory();\n\n      await reactGrab.activate();\n      await reactGrab.page.waitForTimeout(100);\n\n      const history = await reactGrab.getCallbackHistory();\n\n      expect(history.length).toBeGreaterThan(0);\n      expect(history[0].timestamp).toBeDefined();\n      expect(typeof history[0].timestamp).toBe(\"number\");\n    });\n\n    test(\"multiple callbacks should maintain order\", async ({ reactGrab }) => {\n      await reactGrab.clearCallbackHistory();\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"h1\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.clickElement(\"h1\");\n      await reactGrab.page.waitForTimeout(500);\n\n      const history = await reactGrab.getCallbackHistory();\n\n      for (let i = 1; i < history.length; i++) {\n        expect(history[i].timestamp).toBeGreaterThanOrEqual(\n          history[i - 1].timestamp,\n        );\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/fixtures.ts",
    "content": "import { test as base, expect, Page, Locator } from \"@playwright/test\";\n\nconst ATTRIBUTE_NAME = \"data-react-grab\";\nconst DEFAULT_KEY_HOLD_DURATION_MS = 200;\nconst ACTIVATION_BUFFER_MS = 200;\nconst PAGE_SETUP_MAX_ATTEMPTS = 2;\nconst PAGE_SETUP_NAVIGATION_TIMEOUT_MS = 8_000;\nconst PAGE_SETUP_API_TIMEOUT_MS = 8_000;\nconst MODIFIER_KEY = process.platform === \"darwin\" ? \"Meta\" : \"Control\";\n\ninterface ContextMenuInfo {\n  isVisible: boolean;\n  tagBadgeText: string | null;\n  menuItems: string[];\n  position: { x: number; y: number } | null;\n}\n\ninterface SelectionLabelInfo {\n  isVisible: boolean;\n  tagName: string | null;\n  componentName: string | null;\n  status: string | null;\n  elementsCount: number | null;\n  filePath: string | null;\n}\n\ninterface SelectionLabelBounds {\n  label: { x: number; y: number; width: number; height: number };\n  arrow: { x: number; y: number; width: number; height: number } | null;\n  viewport: { width: number; height: number };\n}\n\ninterface ToolbarInfo {\n  isVisible: boolean;\n  isCollapsed: boolean;\n  isVertical: boolean;\n  position: { x: number; y: number } | null;\n  dimensions: { width: number; height: number } | null;\n  snapEdge: string | null;\n}\n\ninterface AgentSessionInfo {\n  id: string;\n  status: string;\n  isStreaming: boolean;\n  error: string | null;\n  prompt: string;\n}\n\ninterface LabelInstanceInfo {\n  id: string;\n  status: string;\n  tagName: string;\n  componentName?: string;\n  createdAt: number;\n}\n\ninterface ReactGrabState {\n  isActive: boolean;\n  isDragging: boolean;\n  isCopying: boolean;\n  isPromptMode: boolean;\n  targetElement: boolean;\n  dragBounds: { x: number; y: number; width: number; height: number } | null;\n  grabbedBoxes: Array<{\n    id: string;\n    bounds: { x: number; y: number; width: number; height: number };\n    createdAt: number;\n  }>;\n  labelInstances: LabelInstanceInfo[];\n}\n\ninterface GrabbedBoxInfo {\n  count: number;\n  boxes: Array<{\n    id: string;\n    bounds: { x: number; y: number; width: number; height: number };\n  }>;\n}\n\ninterface HistoryDropdownInfo {\n  isVisible: boolean;\n  itemCount: number;\n}\n\ninterface ToolbarMenuInfo {\n  isVisible: boolean;\n  itemCount: number;\n  itemLabels: string[];\n}\n\nexport interface ReactGrabPageObject {\n  page: Page;\n  modifierKey: \"Meta\" | \"Control\";\n  activate: () => Promise<void>;\n  activateViaKeyboard: () => Promise<void>;\n  deactivate: () => Promise<void>;\n  holdToActivate: (durationMs?: number) => Promise<void>;\n  isOverlayVisible: () => Promise<boolean>;\n  getOverlayHost: () => Locator;\n  getShadowRoot: () => Promise<Element | null>;\n  hoverElement: (selector: string) => Promise<void>;\n  clickElement: (selector: string) => Promise<void>;\n  rightClickElement: (selector: string) => Promise<void>;\n  rightClickAtPosition: (x: number, y: number) => Promise<void>;\n  dragSelect: (startSelector: string, endSelector: string) => Promise<void>;\n  getClipboardContent: () => Promise<string>;\n  captureNextClipboardWrites: () => Promise<Record<string, string>>;\n  waitForSelectionBox: () => Promise<void>;\n  waitForSelectionSource: () => Promise<void>;\n  isContextMenuVisible: () => Promise<boolean>;\n  getContextMenuInfo: () => Promise<ContextMenuInfo>;\n  isContextMenuItemEnabled: (label: string) => Promise<boolean>;\n  clickContextMenuItem: (label: string) => Promise<void>;\n  isSelectionBoxVisible: () => Promise<boolean>;\n  pressEscape: () => Promise<void>;\n  pressArrowDown: () => Promise<void>;\n  pressArrowUp: () => Promise<void>;\n  pressArrowLeft: () => Promise<void>;\n  pressArrowRight: () => Promise<void>;\n  pressEnter: () => Promise<void>;\n  pressKey: (key: string) => Promise<void>;\n  pressKeyCombo: (modifiers: string[], key: string) => Promise<void>;\n  pressModifierKeyCombo: (key: string) => Promise<void>;\n  scrollPage: (deltaY: number) => Promise<void>;\n\n  enterPromptMode: (selector: string) => Promise<void>;\n  isPromptModeActive: () => Promise<boolean>;\n  typeInInput: (text: string) => Promise<void>;\n  getInputValue: () => Promise<string>;\n  submitInput: () => Promise<void>;\n  clearInput: () => Promise<void>;\n  isPendingDismissVisible: () => Promise<boolean>;\n\n  isToolbarVisible: () => Promise<boolean>;\n  isToolbarCollapsed: () => Promise<boolean>;\n  getToolbarInfo: () => Promise<ToolbarInfo>;\n  clickToolbarToggle: () => Promise<void>;\n  clickToolbarCollapse: () => Promise<void>;\n  dragToolbar: (deltaX: number, deltaY: number) => Promise<void>;\n  clickToolbarEnabled: () => Promise<void>;\n  dragToolbarFromButton: (\n    buttonSelector: string,\n    deltaX: number,\n    deltaY: number,\n  ) => Promise<void>;\n\n  isToolbarMenuButtonVisible: () => Promise<boolean>;\n  clickToolbarMenuButton: () => Promise<void>;\n  isToolbarMenuVisible: () => Promise<boolean>;\n  getToolbarMenuInfo: () => Promise<ToolbarMenuInfo>;\n  clickToolbarMenuItem: (actionId: string) => Promise<void>;\n\n  isHistoryButtonVisible: () => Promise<boolean>;\n  hasUnreadHistoryIndicator: () => Promise<boolean>;\n  clickHistoryButton: () => Promise<void>;\n  isHistoryDropdownVisible: () => Promise<boolean>;\n  getHistoryDropdownInfo: () => Promise<HistoryDropdownInfo>;\n  clickHistoryItem: (index: number) => Promise<void>;\n  clickHistoryItemRemove: (index: number) => Promise<void>;\n  clickHistoryItemCopy: (index: number) => Promise<void>;\n  clickHistoryCopyAll: () => Promise<void>;\n  clickHistoryClear: () => Promise<void>;\n  hoverHistoryItem: (index: number) => Promise<void>;\n  hoverHistoryButton: () => Promise<void>;\n  hoverCopyAllButton: () => Promise<void>;\n  clickToolbarCopyAll: () => Promise<void>;\n  isToolbarCopyAllVisible: () => Promise<boolean>;\n  isClearHistoryPromptVisible: () => Promise<boolean>;\n  confirmClearHistoryPrompt: () => Promise<void>;\n  cancelClearHistoryPrompt: () => Promise<void>;\n  getHistoryDropdownPosition: () => Promise<{\n    left: number;\n    top: number;\n  } | null>;\n\n  getSelectionLabelInfo: () => Promise<SelectionLabelInfo>;\n  getSelectionLabelBounds: () => Promise<SelectionLabelBounds | null>;\n  isSelectionLabelVisible: () => Promise<boolean>;\n  waitForSelectionLabel: () => Promise<void>;\n  getLabelStatusText: () => Promise<string | null>;\n\n  getGrabbedBoxInfo: () => Promise<GrabbedBoxInfo>;\n  getLabelInstancesInfo: () => Promise<LabelInstanceInfo[]>;\n  isGrabbedBoxVisible: () => Promise<boolean>;\n  getDragBoxBounds: () => Promise<{\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n  } | null>;\n  getSelectionBoxBounds: () => Promise<{\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n  } | null>;\n\n  getState: () => Promise<ReactGrabState>;\n  toggle: () => Promise<void>;\n  dispose: () => Promise<void>;\n  copyElementViaApi: (selector: string) => Promise<boolean>;\n  setAgent: (options: Record<string, unknown>) => Promise<void>;\n  updateOptions: (options: Record<string, unknown>) => Promise<void>;\n  reinitialize: (options?: Record<string, unknown>) => Promise<void>;\n\n  setupMockAgent: (options?: {\n    delay?: number;\n    error?: string;\n    statusUpdates?: string[];\n  }) => Promise<void>;\n  getAgentSessions: () => Promise<AgentSessionInfo[]>;\n  isAgentSessionVisible: () => Promise<boolean>;\n  waitForAgentSession: (timeout?: number) => Promise<void>;\n  waitForAgentComplete: (timeout?: number) => Promise<void>;\n  clickAgentDismiss: () => Promise<void>;\n  clickAgentUndo: () => Promise<void>;\n  clickAgentRetry: () => Promise<void>;\n  clickAgentAbort: () => Promise<void>;\n  confirmAgentAbort: () => Promise<void>;\n  cancelAgentAbort: () => Promise<void>;\n\n  touchStart: (x: number, y: number) => Promise<void>;\n  touchMove: (x: number, y: number) => Promise<void>;\n  touchEnd: (x: number, y: number) => Promise<void>;\n  touchTap: (selector: string) => Promise<void>;\n  touchDrag: (\n    startX: number,\n    startY: number,\n    endX: number,\n    endY: number,\n  ) => Promise<void>;\n  isTouchMode: () => Promise<boolean>;\n\n  setViewportSize: (width: number, height: number) => Promise<void>;\n  getViewportSize: () => Promise<{ width: number; height: number }>;\n\n  removeElement: (selector: string) => Promise<void>;\n  hideElement: (selector: string) => Promise<void>;\n  showElement: (selector: string) => Promise<void>;\n  getElementBounds: (\n    selector: string,\n  ) => Promise<{ x: number; y: number; width: number; height: number } | null>;\n  isDropdownOpen: () => Promise<boolean>;\n  openDropdown: () => Promise<void>;\n\n  setupCallbackTracking: () => Promise<void>;\n  getCallbackHistory: () => Promise<\n    Array<{ name: string; args: unknown[]; timestamp: number }>\n  >;\n  clearCallbackHistory: () => Promise<void>;\n  waitForCallback: (name: string, timeout?: number) => Promise<unknown[]>;\n}\n\nconst createReactGrabPageObject = (page: Page): ReactGrabPageObject => {\n  const getOverlayHost = () => page.locator(`[${ATTRIBUTE_NAME}]`).first();\n\n  const getShadowRoot = async () => {\n    return page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      return host?.shadowRoot?.querySelector(`[${attrName}]`) ?? null;\n    }, ATTRIBUTE_NAME);\n  };\n\n  const isOverlayVisible = async () => {\n    return page.evaluate(() => {\n      const api = (window as { __REACT_GRAB__?: { isActive: () => boolean } })\n        .__REACT_GRAB__;\n      return api?.isActive() ?? false;\n    });\n  };\n\n  const waitForActive = async (expectedState: boolean) => {\n    await page.waitForFunction(\n      (expected) => {\n        const api = (window as { __REACT_GRAB__?: { isActive: () => boolean } })\n          .__REACT_GRAB__;\n        return api?.isActive() === expected;\n      },\n      expectedState,\n      { timeout: 5000 },\n    );\n  };\n\n  const holdToActivate = async (durationMs = DEFAULT_KEY_HOLD_DURATION_MS) => {\n    await page.click(\"body\");\n    await page.keyboard.down(MODIFIER_KEY);\n    await page.keyboard.down(\"c\");\n    await page.waitForTimeout(durationMs + ACTIVATION_BUFFER_MS);\n  };\n\n  const activate = async () => {\n    await page.evaluate(() => {\n      const api = (window as { __REACT_GRAB__?: { activate: () => void } })\n        .__REACT_GRAB__;\n      api?.activate();\n    });\n    await waitForActive(true);\n  };\n\n  const activateViaKeyboard = async () => {\n    await holdToActivate();\n    await page.keyboard.up(\"c\");\n    await page.keyboard.up(MODIFIER_KEY);\n    await waitForActive(true);\n  };\n\n  const deactivate = async () => {\n    await page.keyboard.press(\"Escape\");\n    await waitForActive(false);\n  };\n\n  const hoverElement = async (selector: string) => {\n    const element = page.locator(selector).first();\n    await element.hover({ force: true });\n    await page.waitForTimeout(250);\n  };\n\n  const clickElement = async (selector: string) => {\n    const element = page.locator(selector).first();\n    await element.click({ force: true });\n  };\n\n  const dragSelect = async (startSelector: string, endSelector: string) => {\n    const startElement = page.locator(startSelector).first();\n    const endElement = page.locator(endSelector).last();\n\n    const startBox = await startElement.boundingBox();\n    const endBox = await endElement.boundingBox();\n\n    if (!startBox || !endBox) {\n      throw new Error(\"Could not get bounding boxes for drag selection\");\n    }\n\n    const startX = startBox.x - 10;\n    const startY = startBox.y - 10;\n    const endX = endBox.x + endBox.width + 10;\n    const endY = endBox.y + endBox.height + 10;\n\n    await page.mouse.move(startX, startY);\n    await page.mouse.down();\n    await page.mouse.move(endX, endY, { steps: 10 });\n    await page.mouse.up();\n  };\n\n  const getClipboardContent = async () => {\n    return page.evaluate(() => navigator.clipboard.readText());\n  };\n\n  const captureNextClipboardWrites = async () => {\n    return page.evaluate(() => {\n      return new Promise<Record<string, string>>((resolve) => {\n        const originalSetData = DataTransfer.prototype.setData;\n        const clipboardWrites: Record<string, string> = {};\n        DataTransfer.prototype.setData = function (\n          type: string,\n          value: string,\n        ) {\n          clipboardWrites[type] = value;\n          return originalSetData.call(this, type, value);\n        };\n\n        const cleanup = () => {\n          DataTransfer.prototype.setData = originalSetData;\n          resolve(clipboardWrites);\n        };\n\n        const safetyTimeout = setTimeout(cleanup, 5000);\n\n        document.addEventListener(\n          \"copy\",\n          () => {\n            clearTimeout(safetyTimeout);\n            queueMicrotask(cleanup);\n          },\n          { once: true, capture: true },\n        );\n      });\n    });\n  };\n\n  const waitForSelectionBox = async () => {\n    await page.waitForFunction(\n      () => {\n        const api = (\n          window as {\n            __REACT_GRAB__?: {\n              getState: () => {\n                isSelectionBoxVisible: boolean;\n                targetElement: unknown;\n              };\n            };\n          }\n        ).__REACT_GRAB__;\n        const state = api?.getState();\n        return state?.isSelectionBoxVisible || state?.targetElement !== null;\n      },\n      undefined,\n      { timeout: 10_000 },\n    );\n  };\n\n  const waitForSelectionSource = async () => {\n    await page.waitForFunction(\n      () => {\n        const api = (\n          window as {\n            __REACT_GRAB__?: {\n              getState: () => { selectionFilePath: string | null };\n            };\n          }\n        ).__REACT_GRAB__;\n        return api?.getState()?.selectionFilePath !== null;\n      },\n      undefined,\n      { timeout: 5000 },\n    );\n  };\n\n  const pressEscape = async () => {\n    await page.keyboard.press(\"Escape\");\n  };\n\n  const pressArrowDown = async () => {\n    await page.keyboard.press(\"ArrowDown\");\n  };\n\n  const pressArrowUp = async () => {\n    await page.keyboard.press(\"ArrowUp\");\n  };\n\n  const pressArrowLeft = async () => {\n    await page.keyboard.press(\"ArrowLeft\");\n  };\n\n  const pressArrowRight = async () => {\n    await page.keyboard.press(\"ArrowRight\");\n  };\n\n  const pressEnter = async () => {\n    await page.keyboard.press(\"Enter\");\n  };\n\n  const pressKey = async (key: string) => {\n    await page.keyboard.press(key);\n  };\n\n  const pressKeyCombo = async (modifiers: string[], key: string) => {\n    for (const modifier of modifiers) {\n      await page.keyboard.down(modifier);\n    }\n    await page.keyboard.press(key);\n    for (const modifier of [...modifiers].reverse()) {\n      await page.keyboard.up(modifier);\n    }\n  };\n\n  const pressModifierKeyCombo = async (key: string) => {\n    await page.keyboard.down(MODIFIER_KEY);\n    await page.keyboard.press(key);\n    await page.keyboard.up(MODIFIER_KEY);\n  };\n\n  const waitForContextMenu = async (visible: boolean) => {\n    await page.waitForFunction(\n      ({ attrName, expectedVisible }) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return !expectedVisible;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        if (!root) return !expectedVisible;\n        const menuItem = root.querySelector(\"[data-react-grab-menu-item]\");\n        return expectedVisible ? menuItem !== null : menuItem === null;\n      },\n      { attrName: ATTRIBUTE_NAME, expectedVisible: visible },\n      { timeout: 2000 },\n    );\n  };\n\n  const rightClickElement = async (selector: string) => {\n    const element = page.locator(selector).first();\n    await element.click({ button: \"right\", force: true });\n    const isActive = await isOverlayVisible();\n    if (isActive) {\n      await waitForContextMenu(true);\n    }\n  };\n\n  const rightClickAtPosition = async (x: number, y: number) => {\n    await page.mouse.click(x, y, { button: \"right\" });\n    const isActive = await isOverlayVisible();\n    if (isActive) {\n      await waitForContextMenu(true);\n    }\n  };\n\n  const isContextMenuVisible = async () => {\n    return page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return false;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return false;\n      const menuItem = root.querySelector(\"[data-react-grab-menu-item]\");\n      return menuItem !== null;\n    }, ATTRIBUTE_NAME);\n  };\n\n  const clickContextMenuItem = async (label: string) => {\n    await page.evaluate(\n      ({ attrName, itemLabel }) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) throw new Error(\"No shadow root found\");\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        if (!root) throw new Error(\"No inner root found\");\n        const button = root.querySelector<HTMLButtonElement>(\n          `[data-react-grab-menu-item=\"${itemLabel.toLowerCase()}\"]`,\n        );\n        if (!button)\n          throw new Error(`Context menu item \"${itemLabel}\" not found`);\n        button.click();\n      },\n      { attrName: ATTRIBUTE_NAME, itemLabel: label },\n    );\n    await waitForContextMenu(false);\n  };\n\n  const getContextMenuInfo = async (): Promise<ContextMenuInfo> => {\n    return page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot)\n        return {\n          isVisible: false,\n          tagBadgeText: null,\n          menuItems: [],\n          position: null,\n        };\n\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root)\n        return {\n          isVisible: false,\n          tagBadgeText: null,\n          menuItems: [],\n          position: null,\n        };\n\n      const contextMenu = root.querySelector<HTMLElement>(\n        \"[data-react-grab-context-menu]\",\n      );\n      if (!contextMenu)\n        return {\n          isVisible: false,\n          tagBadgeText: null,\n          menuItems: [],\n          position: null,\n        };\n\n      const menuItemButtons = Array.from(\n        contextMenu.querySelectorAll<HTMLButtonElement>(\n          \"[data-react-grab-menu-item]\",\n        ),\n      );\n      const menuItems = menuItemButtons.map((btn) => {\n        const item = btn.dataset.reactGrabMenuItem ?? \"\";\n        return item.charAt(0).toUpperCase() + item.slice(1);\n      });\n\n      const tagBadgeElement = contextMenu.querySelector(\"span\");\n      const tagBadgeText = tagBadgeElement?.textContent?.trim() ?? null;\n\n      const style = contextMenu.style;\n      const position =\n        style.left && style.top\n          ? { x: parseFloat(style.left), y: parseFloat(style.top) }\n          : null;\n\n      return { isVisible: true, tagBadgeText, menuItems, position };\n    }, ATTRIBUTE_NAME);\n  };\n\n  const isContextMenuItemEnabled = async (label: string): Promise<boolean> => {\n    return page.evaluate(\n      ({ attrName, itemLabel }) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return false;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        if (!root) return false;\n        const button = root.querySelector<HTMLButtonElement>(\n          `[data-react-grab-menu-item=\"${itemLabel.toLowerCase()}\"]`,\n        );\n        return button ? !button.disabled : false;\n      },\n      { attrName: ATTRIBUTE_NAME, itemLabel: label },\n    );\n  };\n\n  const isSelectionBoxVisible = async (): Promise<boolean> => {\n    return page.evaluate(() => {\n      const api = (\n        window as {\n          __REACT_GRAB__?: {\n            getState: () => { isSelectionBoxVisible: boolean };\n          };\n        }\n      ).__REACT_GRAB__;\n      return api?.getState()?.isSelectionBoxVisible ?? false;\n    });\n  };\n\n  const scrollPage = async (deltaY: number) => {\n    const scrollBefore = await page.evaluate(() => window.scrollY);\n    await page.mouse.wheel(0, deltaY);\n    await page\n      .waitForFunction(\n        (prevScroll) => window.scrollY !== prevScroll,\n        scrollBefore,\n        { timeout: 2000 },\n      )\n      .catch(() => {\n        // Scroll may not change if at edge of page, that's okay\n      });\n  };\n\n  const waitForPromptMode = async (active: boolean) => {\n    await page.waitForFunction(\n      (expected) => {\n        const api = (\n          window as {\n            __REACT_GRAB__?: { getState: () => { isPromptMode: boolean } };\n          }\n        ).__REACT_GRAB__;\n        return api?.getState()?.isPromptMode === expected;\n      },\n      active,\n      { timeout: 2000 },\n    );\n  };\n\n  const enterPromptMode = async (selector: string) => {\n    await activate();\n    await hoverElement(selector);\n    const isSelected = await page\n      .waitForFunction(\n        () => {\n          const api = (\n            window as {\n              __REACT_GRAB__?: {\n                getState: () => {\n                  isSelectionBoxVisible: boolean;\n                  targetElement: unknown;\n                };\n              };\n            }\n          ).__REACT_GRAB__;\n          const state = api?.getState();\n          return state?.isSelectionBoxVisible || state?.targetElement !== null;\n        },\n        undefined,\n        { timeout: 2000 },\n      )\n      .then(() => true)\n      .catch(() => false);\n    if (!isSelected) {\n      await hoverElement(selector);\n      await waitForSelectionBox();\n    }\n    await rightClickElement(selector);\n    await clickContextMenuItem(\"Edit\");\n    await waitForPromptMode(true);\n  };\n\n  const isPromptModeActive = async (): Promise<boolean> => {\n    return page.evaluate(() => {\n      const api = (\n        window as {\n          __REACT_GRAB__?: { getState: () => { isPromptMode: boolean } };\n        }\n      ).__REACT_GRAB__;\n      return api?.getState()?.isPromptMode ?? false;\n    });\n  };\n\n  const typeInInput = async (text: string) => {\n    await page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return;\n      const textarea = root.querySelector<HTMLTextAreaElement>(\n        \"[data-react-grab-input]\",\n      );\n      if (textarea) {\n        textarea.focus();\n      }\n    }, ATTRIBUTE_NAME);\n    await page.keyboard.insertText(text);\n  };\n\n  const getInputValue = async (): Promise<string> => {\n    return page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return \"\";\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return \"\";\n      const textarea = root.querySelector(\n        \"textarea[data-react-grab-ignore-events]\",\n      ) as HTMLTextAreaElement;\n      return textarea?.value ?? \"\";\n    }, ATTRIBUTE_NAME);\n  };\n\n  const submitInput = async () => {\n    await page.keyboard.press(\"Enter\");\n  };\n\n  const clearInput = async () => {\n    await page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return;\n      const textarea = root.querySelector(\n        \"textarea[data-react-grab-ignore-events]\",\n      ) as HTMLTextAreaElement;\n      if (textarea) {\n        textarea.value = \"\";\n        textarea.dispatchEvent(new Event(\"input\", { bubbles: true }));\n      }\n    }, ATTRIBUTE_NAME);\n  };\n\n  const isPendingDismissVisible = async (): Promise<boolean> => {\n    return page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return false;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return false;\n      const discardPrompt = root.querySelector(\n        \"[data-react-grab-discard-prompt]\",\n      );\n      return discardPrompt !== null;\n    }, ATTRIBUTE_NAME);\n  };\n\n  const isToolbarVisible = async (): Promise<boolean> => {\n    return page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return false;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return false;\n      const toolbar = root.querySelector<HTMLElement>(\n        \"[data-react-grab-toolbar]\",\n      );\n      if (!toolbar) return false;\n      const computedStyle = window.getComputedStyle(toolbar);\n      return computedStyle.opacity !== \"0\" && computedStyle.display !== \"none\";\n    }, ATTRIBUTE_NAME);\n  };\n\n  const isToolbarCollapsed = async (): Promise<boolean> => {\n    return page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return false;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return false;\n      const toolbar = root.querySelector<HTMLElement>(\n        \"[data-react-grab-toolbar]\",\n      );\n      if (!toolbar) return false;\n      const computedStyle = window.getComputedStyle(toolbar);\n      return computedStyle.cursor === \"pointer\";\n    }, ATTRIBUTE_NAME);\n  };\n\n  const getToolbarInfo = async (): Promise<ToolbarInfo> => {\n    const defaultInfo: ToolbarInfo = {\n      isVisible: false,\n      isCollapsed: false,\n      isVertical: false,\n      position: null,\n      dimensions: null,\n      snapEdge: null,\n    };\n\n    return page.evaluate(\n      ({ attrName, fallback }) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return fallback;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        if (!root) return fallback;\n\n        const toolbar = root.querySelector<HTMLElement>(\n          \"[data-react-grab-toolbar]\",\n        );\n        if (!toolbar) return fallback;\n\n        const computedStyle = window.getComputedStyle(toolbar);\n        const transform = toolbar.style.transform;\n        const translateMatch = transform.match(\n          /translate\\((-?\\d+(?:\\.\\d+)?)px,\\s*(-?\\d+(?:\\.\\d+)?)px\\)/,\n        );\n        const position = translateMatch\n          ? {\n              x: parseFloat(translateMatch[1]),\n              y: parseFloat(translateMatch[2]),\n            }\n          : null;\n\n        const viewportWidth = window.innerWidth;\n        const viewportHeight = window.innerHeight;\n        const rect = toolbar.getBoundingClientRect();\n        const dimensions = { width: rect.width, height: rect.height };\n\n        let snapEdge: string | null = null;\n        if (position) {\n          const SNAP_THRESHOLD = 30;\n          if (position.y <= SNAP_THRESHOLD) snapEdge = \"top\";\n          else if (position.y + rect.height >= viewportHeight - SNAP_THRESHOLD)\n            snapEdge = \"bottom\";\n          else if (position.x <= SNAP_THRESHOLD) snapEdge = \"left\";\n          else if (position.x + rect.width >= viewportWidth - SNAP_THRESHOLD)\n            snapEdge = \"right\";\n        }\n\n        const isCollapsed = computedStyle.cursor === \"pointer\";\n\n        const innerDiv = toolbar.querySelector(\"div\");\n        const innerStyle = innerDiv ? window.getComputedStyle(innerDiv) : null;\n        const isVertical = innerStyle?.flexDirection === \"column\";\n\n        return {\n          isVisible: computedStyle.opacity !== \"0\",\n          isCollapsed,\n          isVertical,\n          position,\n          dimensions,\n          snapEdge,\n        };\n      },\n      { attrName: ATTRIBUTE_NAME, fallback: defaultInfo },\n    );\n  };\n\n  const clickToolbarToggle = async () => {\n    const wasActive = await isOverlayVisible();\n    await page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return;\n      const toggleButton = root.querySelector<HTMLButtonElement>(\n        \"[data-react-grab-toolbar-toggle]\",\n      );\n      toggleButton?.click();\n    }, ATTRIBUTE_NAME);\n    await waitForActive(!wasActive);\n  };\n\n  const clickToolbarCollapse = async () => {\n    await page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return;\n      const collapseButton = root.querySelector<HTMLButtonElement>(\n        \"[data-react-grab-toolbar-collapse]\",\n      );\n      collapseButton?.click();\n    }, ATTRIBUTE_NAME);\n  };\n\n  const clickToolbarEnabled = async () => {\n    await page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return;\n      const enabledButton = root.querySelector<HTMLButtonElement>(\n        \"[data-react-grab-toolbar-enabled]\",\n      );\n      enabledButton?.click();\n    }, ATTRIBUTE_NAME);\n  };\n\n  const dragToolbar = async (deltaX: number, deltaY: number) => {\n    const toolbarRect = await page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return null;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return null;\n      const toolbar = root.querySelector<HTMLElement>(\n        \"[data-react-grab-toolbar]\",\n      );\n      if (!toolbar) return null;\n      const rect = toolbar.getBoundingClientRect();\n      return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };\n    }, ATTRIBUTE_NAME);\n\n    if (!toolbarRect) return;\n\n    const startX = toolbarRect.x + toolbarRect.width / 2;\n    const startY = toolbarRect.y + toolbarRect.height / 2;\n    const endX = startX + deltaX;\n    const endY = startY + deltaY;\n\n    await page.mouse.move(startX, startY);\n    await page.mouse.down();\n    await page.mouse.move(endX, endY, { steps: 10 });\n    await page.mouse.up();\n    // HACK: Wait for snap animation to complete\n    await page.waitForTimeout(300);\n  };\n\n  const dragToolbarFromButton = async (\n    buttonSelector: string,\n    deltaX: number,\n    deltaY: number,\n  ) => {\n    const buttonRect = await page.evaluate(\n      ({ attrName, selector }) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return null;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        if (!root) return null;\n        const button = root.querySelector<HTMLElement>(selector);\n        if (!button) return null;\n        const rect = button.getBoundingClientRect();\n        return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };\n      },\n      { attrName: ATTRIBUTE_NAME, selector: buttonSelector },\n    );\n\n    if (!buttonRect) return;\n\n    const startX = buttonRect.x + buttonRect.width / 2;\n    const startY = buttonRect.y + buttonRect.height / 2;\n    const endX = startX + deltaX;\n    const endY = startY + deltaY;\n\n    await page.mouse.move(startX, startY);\n    await page.mouse.down();\n    await page.mouse.move(endX, endY, { steps: 10 });\n    await page.mouse.up();\n    // HACK: Wait for snap animation to complete\n    await page.waitForTimeout(300);\n  };\n\n  const isToolbarMenuButtonVisible = async (): Promise<boolean> => {\n    return page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return false;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return false;\n      const menuButton = root.querySelector<HTMLElement>(\n        \"[data-react-grab-toolbar-menu]\",\n      );\n      if (!menuButton) return false;\n      const gridParent = menuButton.parentElement?.parentElement;\n      if (!gridParent) return false;\n      const computedStyle = window.getComputedStyle(gridParent);\n      return computedStyle.opacity !== \"0\";\n    }, ATTRIBUTE_NAME);\n  };\n\n  const waitForToolbarMenu = async (visible: boolean) => {\n    await page.waitForFunction(\n      ({ attrName, expectedVisible }) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return !expectedVisible;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        if (!root) return !expectedVisible;\n        const menu = root.querySelector<HTMLElement>(\n          \"[data-react-grab-toolbar-menu]\",\n        );\n        if (!expectedVisible) {\n          const dropdown = root.querySelector<HTMLElement>(\n            \"div[data-react-grab-toolbar-menu]:not([data-react-grab-toolbar])\",\n          );\n          return dropdown === null;\n        }\n        if (!menu) return false;\n        const dropdowns = root.querySelectorAll<HTMLElement>(\n          \"[data-react-grab-toolbar-menu]\",\n        );\n        for (let i = 0; i < dropdowns.length; i++) {\n          const dropdown = dropdowns[i];\n          if (dropdown.classList.contains(\"fixed\")) {\n            return getComputedStyle(dropdown).pointerEvents !== \"none\";\n          }\n        }\n        return false;\n      },\n      { attrName: ATTRIBUTE_NAME, expectedVisible: visible },\n      { timeout: 2000 },\n    );\n  };\n\n  const clickToolbarMenuButton = async () => {\n    const wasOpen = await isToolbarMenuVisible();\n    await clickShadowRootButton(\"[data-react-grab-toolbar-menu]\");\n    await waitForToolbarMenu(!wasOpen);\n  };\n\n  const isToolbarMenuVisible = async (): Promise<boolean> => {\n    return page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return false;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return false;\n      const dropdowns = root.querySelectorAll<HTMLElement>(\n        \"[data-react-grab-toolbar-menu]\",\n      );\n      for (let i = 0; i < dropdowns.length; i++) {\n        const dropdown = dropdowns[i];\n        if (\n          dropdown.classList.contains(\"fixed\") &&\n          getComputedStyle(dropdown).pointerEvents !== \"none\"\n        ) {\n          return true;\n        }\n      }\n      return false;\n    }, ATTRIBUTE_NAME);\n  };\n\n  const getToolbarMenuInfo = async (): Promise<ToolbarMenuInfo> => {\n    return page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot)\n        return { isVisible: false, itemCount: 0, itemLabels: [] };\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return { isVisible: false, itemCount: 0, itemLabels: [] };\n      const dropdowns = root.querySelectorAll<HTMLElement>(\n        \"[data-react-grab-toolbar-menu]\",\n      );\n      for (let i = 0; i < dropdowns.length; i++) {\n        const dropdown = dropdowns[i];\n        if (dropdown.classList.contains(\"fixed\")) {\n          const items = dropdown.querySelectorAll<HTMLButtonElement>(\n            \"[data-react-grab-menu-item]\",\n          );\n          const itemLabels = Array.from(items).map(\n            (item) => item.textContent?.trim() ?? \"\",\n          );\n          return {\n            isVisible: getComputedStyle(dropdown).pointerEvents !== \"none\",\n            itemCount: items.length,\n            itemLabels,\n          };\n        }\n      }\n      return { isVisible: false, itemCount: 0, itemLabels: [] };\n    }, ATTRIBUTE_NAME);\n  };\n\n  const clickToolbarMenuItem = async (actionId: string) => {\n    await page.evaluate(\n      ({ attrName, itemId }) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        if (!root) return;\n        const button = root.querySelector<HTMLButtonElement>(\n          `[data-react-grab-menu-item=\"${itemId}\"]`,\n        );\n        button?.click();\n      },\n      { attrName: ATTRIBUTE_NAME, itemId: actionId },\n    );\n  };\n\n  const isHistoryButtonVisible = async (): Promise<boolean> => {\n    return page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return false;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return false;\n      const historyButton = root.querySelector<HTMLElement>(\n        \"[data-react-grab-toolbar-history]\",\n      );\n      if (!historyButton) return false;\n      const gridParent = historyButton.parentElement?.parentElement;\n      if (!gridParent) return false;\n      const computedStyle = window.getComputedStyle(gridParent);\n      return computedStyle.opacity !== \"0\";\n    }, ATTRIBUTE_NAME);\n  };\n\n  const hasUnreadHistoryIndicator = async (): Promise<boolean> => {\n    return page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return false;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return false;\n      const historyButton = root.querySelector(\n        \"[data-react-grab-toolbar-history]\",\n      );\n      if (!historyButton) return false;\n      const unreadDot = historyButton.querySelector(\n        \"[data-react-grab-unread-indicator]\",\n      );\n      return unreadDot !== null;\n    }, ATTRIBUTE_NAME);\n  };\n\n  const waitForHistoryDropdown = async (visible: boolean) => {\n    await page.waitForFunction(\n      ({ attrName, expectedVisible }) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return !expectedVisible;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        if (!root) return !expectedVisible;\n        const dropdown = root.querySelector<HTMLElement>(\n          \"[data-react-grab-history-dropdown]\",\n        );\n        if (!expectedVisible) return dropdown === null;\n        if (!dropdown) return false;\n        return getComputedStyle(dropdown).pointerEvents !== \"none\";\n      },\n      { attrName: ATTRIBUTE_NAME, expectedVisible: visible },\n      { timeout: 5000 },\n    );\n  };\n\n  const clickShadowRootButton = async (selector: string) => {\n    await page.evaluate(\n      ({ attrName, buttonSelector }) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        if (!root) return;\n        root.querySelector<HTMLButtonElement>(buttonSelector)?.click();\n      },\n      { attrName: ATTRIBUTE_NAME, buttonSelector: selector },\n    );\n  };\n\n  const clickHistoryButton = async () => {\n    const wasOpen = await isHistoryDropdownVisible();\n    await clickShadowRootButton(\"[data-react-grab-toolbar-history]\");\n    await waitForHistoryDropdown(!wasOpen);\n  };\n\n  const isHistoryDropdownVisible = async (): Promise<boolean> => {\n    return page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return false;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return false;\n      const dropdown = root.querySelector(\"[data-react-grab-history-dropdown]\");\n      return dropdown !== null;\n    }, ATTRIBUTE_NAME);\n  };\n\n  const getHistoryDropdownInfo = async (): Promise<HistoryDropdownInfo> => {\n    return page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return { isVisible: false, itemCount: 0 };\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return { isVisible: false, itemCount: 0 };\n      const dropdown = root.querySelector(\"[data-react-grab-history-dropdown]\");\n      if (!dropdown) return { isVisible: false, itemCount: 0 };\n\n      return {\n        isVisible: true,\n        itemCount: dropdown.querySelectorAll(\"[data-react-grab-history-item]\")\n          .length,\n      };\n    }, ATTRIBUTE_NAME);\n  };\n\n  const clickHistoryItem = async (index: number) => {\n    await page.evaluate(\n      ({ attrName, itemIndex }) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        if (!root) return;\n        const items = root.querySelectorAll<HTMLButtonElement>(\n          \"[data-react-grab-history-item]\",\n        );\n        items[itemIndex]?.click();\n      },\n      { attrName: ATTRIBUTE_NAME, itemIndex: index },\n    );\n  };\n\n  const clickHistoryItemRemove = async (index: number) => {\n    await page.evaluate(\n      ({ attrName, itemIndex }) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        if (!root) return;\n        const items = root.querySelectorAll(\"[data-react-grab-history-item]\");\n        const item = items[itemIndex];\n        if (!item) return;\n        const removeButton = item.querySelector<HTMLButtonElement>(\n          \"[data-react-grab-history-item-remove]\",\n        );\n        removeButton?.click();\n      },\n      { attrName: ATTRIBUTE_NAME, itemIndex: index },\n    );\n  };\n\n  const clickHistoryItemCopy = async (index: number) => {\n    await page.evaluate(\n      ({ attrName, itemIndex }) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        if (!root) return;\n        const items = root.querySelectorAll(\"[data-react-grab-history-item]\");\n        const item = items[itemIndex];\n        if (!item) return;\n        const copyButton = item.querySelector<HTMLButtonElement>(\n          \"[data-react-grab-history-item-copy]\",\n        );\n        copyButton?.click();\n      },\n      { attrName: ATTRIBUTE_NAME, itemIndex: index },\n    );\n  };\n\n  const clickHistoryCopyAll = async () => {\n    await clickShadowRootButton(\"[data-react-grab-history-copy-all]\");\n  };\n\n  const clickHistoryClear = async () => {\n    await clickShadowRootButton(\"[data-react-grab-history-clear]\");\n    await waitForHistoryDropdown(false);\n  };\n\n  const hoverHistoryItem = async (index: number) => {\n    const itemRect = await page.evaluate(\n      ({ attrName, itemIndex }) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return null;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        if (!root) return null;\n        const items = root.querySelectorAll(\"[data-react-grab-history-item]\");\n        const button = items[itemIndex];\n        if (!button) return null;\n        const rect = button.getBoundingClientRect();\n        return {\n          x: rect.x,\n          y: rect.y,\n          width: rect.width,\n          height: rect.height,\n        };\n      },\n      { attrName: ATTRIBUTE_NAME, itemIndex: index },\n    );\n    if (itemRect) {\n      await page.mouse.move(\n        itemRect.x + itemRect.width / 2,\n        itemRect.y + itemRect.height / 2,\n      );\n      await page.waitForTimeout(100);\n    }\n  };\n\n  const hoverHistoryButton = async () => {\n    const buttonRect = await page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return null;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return null;\n      const button = root.querySelector<HTMLElement>(\n        \"[data-react-grab-toolbar-history]\",\n      );\n      if (!button) return null;\n      const rect = button.getBoundingClientRect();\n      return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };\n    }, ATTRIBUTE_NAME);\n    if (buttonRect) {\n      await page.mouse.move(\n        buttonRect.x + buttonRect.width / 2,\n        buttonRect.y + buttonRect.height / 2,\n      );\n      await page.waitForTimeout(100);\n    }\n  };\n\n  const hoverCopyAllButton = async () => {\n    const buttonRect = await page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return null;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return null;\n      const button = root.querySelector<HTMLElement>(\n        \"[data-react-grab-history-copy-all]\",\n      );\n      if (!button) return null;\n      const rect = button.getBoundingClientRect();\n      return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };\n    }, ATTRIBUTE_NAME);\n    if (buttonRect) {\n      await page.mouse.move(\n        buttonRect.x + buttonRect.width / 2,\n        buttonRect.y + buttonRect.height / 2,\n      );\n      await page.waitForTimeout(100);\n    }\n  };\n\n  const clickToolbarCopyAll = async () => {\n    await clickShadowRootButton(\"[data-react-grab-toolbar-copy-all]\");\n  };\n\n  const isToolbarCopyAllVisible = async (): Promise<boolean> => {\n    return page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return false;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return false;\n      const copyAllButton = root.querySelector<HTMLElement>(\n        \"[data-react-grab-toolbar-copy-all]\",\n      );\n      if (!copyAllButton) return false;\n      const gridParent = copyAllButton.parentElement?.parentElement;\n      if (!gridParent) return false;\n      const computedStyle = window.getComputedStyle(gridParent);\n      return computedStyle.opacity !== \"0\";\n    }, ATTRIBUTE_NAME);\n  };\n\n  const isClearHistoryPromptVisible = async (): Promise<boolean> => {\n    return page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return false;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return false;\n      const prompt = root.querySelector<HTMLElement>(\n        \"[data-react-grab-clear-history-prompt]\",\n      );\n      if (!prompt) return false;\n      return getComputedStyle(prompt).pointerEvents !== \"none\";\n    }, ATTRIBUTE_NAME);\n  };\n\n  const confirmClearHistoryPrompt = async () => {\n    await page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return;\n      const prompt = root.querySelector(\n        \"[data-react-grab-clear-history-prompt]\",\n      );\n      if (!prompt) return;\n      const yesButton = prompt.querySelector<HTMLButtonElement>(\n        \"[data-react-grab-discard-yes]\",\n      );\n      yesButton?.click();\n    }, ATTRIBUTE_NAME);\n  };\n\n  const cancelClearHistoryPrompt = async () => {\n    await page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return;\n      const prompt = root.querySelector(\n        \"[data-react-grab-clear-history-prompt]\",\n      );\n      if (!prompt) return;\n      const noButton = prompt.querySelector<HTMLButtonElement>(\n        \"[data-react-grab-discard-no]\",\n      );\n      noButton?.click();\n    }, ATTRIBUTE_NAME);\n  };\n\n  const getHistoryDropdownPosition = async (): Promise<{\n    left: number;\n    top: number;\n  } | null> => {\n    return page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return null;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return null;\n      const dropdown = root.querySelector<HTMLElement>(\n        \"[data-react-grab-history-dropdown]\",\n      );\n      if (!dropdown) return null;\n      return {\n        left: parseFloat(dropdown.style.left),\n        top: parseFloat(dropdown.style.top),\n      };\n    }, ATTRIBUTE_NAME);\n  };\n\n  const getSelectionLabelInfo = async (): Promise<SelectionLabelInfo> => {\n    return page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot)\n        return {\n          isVisible: false,\n          tagName: null,\n          componentName: null,\n          status: null,\n          elementsCount: null,\n          filePath: null,\n        };\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root)\n        return {\n          isVisible: false,\n          tagName: null,\n          componentName: null,\n          status: null,\n          elementsCount: null,\n          filePath: null,\n        };\n\n      const label = root.querySelector(\"[data-react-grab-selection-label]\");\n      if (!label)\n        return {\n          isVisible: false,\n          tagName: null,\n          componentName: null,\n          status: null,\n          elementsCount: null,\n          filePath: null,\n        };\n\n      let tagName: string | null = null;\n      let componentName: string | null = null;\n      let elementsCount: number | null = null;\n\n      const allSpans = Array.from(label.querySelectorAll(\"span\"));\n      for (const span of allSpans) {\n        const spanText = span.textContent?.trim() ?? \"\";\n        if (spanText.includes(\"elements\")) {\n          const match = spanText.match(/(\\d+)\\s*elements/);\n          elementsCount = match ? parseInt(match[1], 10) : null;\n        } else if (spanText.includes(\".\")) {\n          const parts = spanText.split(\".\");\n          componentName = parts[0] ?? null;\n          tagName = parts[1] ?? null;\n        } else if (spanText && !spanText.includes(\"Editing\") && !tagName) {\n          tagName = spanText;\n        }\n      }\n\n      const statusElement = label.querySelector(\".animate-pulse\");\n      const status = statusElement ? \"copying\" : \"idle\";\n\n      return {\n        isVisible: true,\n        tagName,\n        componentName,\n        status,\n        elementsCount,\n        filePath: null,\n      };\n    }, ATTRIBUTE_NAME);\n  };\n\n  const getSelectionLabelBounds =\n    async (): Promise<SelectionLabelBounds | null> => {\n      return page.evaluate((attrName) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return null;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        if (!root) return null;\n\n        const label = root.querySelector<HTMLElement>(\n          \"[data-react-grab-selection-label]\",\n        );\n        if (!label) return null;\n\n        const toRect = (rect: DOMRect) => ({\n          x: rect.x,\n          y: rect.y,\n          width: rect.width,\n          height: rect.height,\n        });\n\n        const arrowElement = label.querySelector<HTMLElement>(\n          \"[data-react-grab-arrow]\",\n        );\n\n        return {\n          label: toRect(label.getBoundingClientRect()),\n          arrow: arrowElement\n            ? toRect(arrowElement.getBoundingClientRect())\n            : null,\n          viewport: { width: window.innerWidth, height: window.innerHeight },\n        };\n      }, ATTRIBUTE_NAME);\n    };\n\n  const isSelectionLabelVisible = async (): Promise<boolean> => {\n    const info = await getSelectionLabelInfo();\n    return info.isVisible;\n  };\n\n  const waitForSelectionLabel = async () => {\n    await page.waitForFunction(\n      (attrName) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return false;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        if (!root) return false;\n        const label = root.querySelector(\"[data-react-grab-selection-label]\");\n        return label !== null;\n      },\n      ATTRIBUTE_NAME,\n      { timeout: 2000 },\n    );\n  };\n\n  const getLabelStatusText = async (): Promise<string | null> => {\n    return page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return null;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return null;\n\n      const pulsingElements = Array.from(\n        root.querySelectorAll(\".animate-pulse\"),\n      );\n      for (let i = 0; i < pulsingElements.length; i++) {\n        const element = pulsingElements[i];\n        const text = element.textContent?.trim();\n        if (text) return text;\n      }\n\n      const completedTexts = [\"Copied\", \"Completed\", \"Done\"];\n      for (let i = 0; i < completedTexts.length; i++) {\n        const text = completedTexts[i];\n        if (root.textContent?.includes(text)) return text;\n      }\n\n      return null;\n    }, ATTRIBUTE_NAME);\n  };\n\n  const getGrabbedBoxInfo = async (): Promise<GrabbedBoxInfo> => {\n    return page.evaluate(() => {\n      const api = (\n        window as {\n          __REACT_GRAB__?: {\n            getState: () => {\n              grabbedBoxes: Array<{\n                id: string;\n                bounds: { x: number; y: number; width: number; height: number };\n                createdAt: number;\n              }>;\n            };\n          };\n        }\n      ).__REACT_GRAB__;\n\n      const state = api?.getState();\n      const grabbedBoxes = state?.grabbedBoxes ?? [];\n\n      return {\n        count: grabbedBoxes.length,\n        boxes: grabbedBoxes.map((box) => ({\n          id: box.id,\n          bounds: box.bounds,\n        })),\n      };\n    });\n  };\n\n  const getLabelInstancesInfo = async (): Promise<LabelInstanceInfo[]> => {\n    return page.evaluate(() => {\n      const api = (\n        window as {\n          __REACT_GRAB__?: {\n            getState: () => {\n              labelInstances: Array<{\n                id: string;\n                status: string;\n                tagName: string;\n                componentName?: string;\n                createdAt: number;\n              }>;\n            };\n          };\n        }\n      ).__REACT_GRAB__;\n\n      const state = api?.getState();\n      return (state?.labelInstances ?? []).map((instance) => ({\n        id: instance.id,\n        status: instance.status,\n        tagName: instance.tagName,\n        componentName: instance.componentName,\n        createdAt: instance.createdAt,\n      }));\n    });\n  };\n\n  const isGrabbedBoxVisible = async (): Promise<boolean> => {\n    return page.evaluate(() => {\n      const api = (\n        window as {\n          __REACT_GRAB__?: {\n            getState: () => {\n              grabbedBoxes: Array<{ id: string }>;\n            };\n          };\n        }\n      ).__REACT_GRAB__;\n\n      const state = api?.getState();\n      return (state?.grabbedBoxes?.length ?? 0) > 0;\n    });\n  };\n\n  const getDragBoxBounds = async (): Promise<{\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n  } | null> => {\n    return page.evaluate(() => {\n      const api = (\n        window as {\n          __REACT_GRAB__?: {\n            getState: () => {\n              isDragBoxVisible: boolean;\n              dragBounds: {\n                x: number;\n                y: number;\n                width: number;\n                height: number;\n              } | null;\n            };\n          };\n        }\n      ).__REACT_GRAB__;\n      const state = api?.getState();\n      if (!state?.isDragBoxVisible || !state?.dragBounds) return null;\n      return state.dragBounds;\n    });\n  };\n\n  const getSelectionBoxBounds = async (): Promise<{\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n  } | null> => {\n    return page.evaluate(() => {\n      const api = (\n        window as {\n          __REACT_GRAB__?: {\n            getState: () => {\n              isSelectionBoxVisible: boolean;\n              targetElement: Element | null;\n            };\n          };\n        }\n      ).__REACT_GRAB__;\n      const state = api?.getState();\n      if (!state?.isSelectionBoxVisible || !state?.targetElement) return null;\n      const rect = state.targetElement.getBoundingClientRect();\n      if (rect.width > 0 && rect.height > 0) {\n        return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };\n      }\n      return null;\n    });\n  };\n\n  const getState = async (): Promise<ReactGrabState> => {\n    return page.evaluate(() => {\n      const api = (\n        window as { __REACT_GRAB__?: { getState: () => ReactGrabState } }\n      ).__REACT_GRAB__;\n      const state = api?.getState();\n      return (\n        state ?? {\n          isActive: false,\n          isDragging: false,\n          isCopying: false,\n          isPromptMode: false,\n          targetElement: false,\n          dragBounds: null,\n          grabbedBoxes: [],\n          labelInstances: [],\n        }\n      );\n    });\n  };\n\n  const toggle = async () => {\n    const wasActive = await isOverlayVisible();\n    await page.evaluate(() => {\n      const api = (window as { __REACT_GRAB__?: { toggle: () => void } })\n        .__REACT_GRAB__;\n      api?.toggle();\n    });\n    await waitForActive(!wasActive);\n  };\n\n  const dispose = async () => {\n    await page.evaluate(() => {\n      const api = (window as { __REACT_GRAB__?: { dispose: () => void } })\n        .__REACT_GRAB__;\n      api?.dispose();\n    });\n  };\n\n  const copyElementViaApi = async (selector: string): Promise<boolean> => {\n    return page.evaluate(async (sel) => {\n      const api = (\n        window as {\n          __REACT_GRAB__?: { copyElement: (el: Element) => Promise<boolean> };\n        }\n      ).__REACT_GRAB__;\n      const element = document.querySelector(sel);\n      if (!element || !api) return false;\n      return api.copyElement(element);\n    }, selector);\n  };\n\n  const setAgent = async (options: Record<string, unknown>) => {\n    await page.evaluate((opts) => {\n      const api = (\n        window as {\n          __REACT_GRAB__?: {\n            unregisterPlugin: (name: string) => void;\n            registerPlugin: (plugin: {\n              name: string;\n              actions: Array<Record<string, unknown>>;\n            }) => void;\n          };\n        }\n      ).__REACT_GRAB__;\n      api?.unregisterPlugin(\"test-agent\");\n      const agent = opts;\n      api?.registerPlugin({\n        name: \"test-agent\",\n        actions: [\n          {\n            id: \"edit-with-test-agent\",\n            label: \"Edit\",\n            shortcut: \"Enter\",\n            onAction: (context: {\n              enterPromptMode?: (agent?: Record<string, unknown>) => void;\n            }) => {\n              context.enterPromptMode?.(agent);\n            },\n            agent,\n          },\n        ],\n      });\n    }, options);\n  };\n\n  const updateOptions = async (options: Record<string, unknown>) => {\n    await page.evaluate((opts) => {\n      const api = (\n        window as {\n          __REACT_GRAB__?: {\n            setOptions: (o: Record<string, unknown>) => void;\n            unregisterPlugin: (name: string) => void;\n            registerPlugin: (plugin: Record<string, unknown>) => void;\n          };\n        }\n      ).__REACT_GRAB__;\n\n      const pluginKeys = [\"theme\", \"actions\"];\n      const hookKeys = [\n        \"onActivate\",\n        \"onDeactivate\",\n        \"onElementHover\",\n        \"onElementSelect\",\n        \"onDragStart\",\n        \"onDragEnd\",\n        \"onBeforeCopy\",\n        \"onAfterCopy\",\n        \"onCopySuccess\",\n        \"onCopyError\",\n        \"onStateChange\",\n        \"onPromptModeChange\",\n        \"onSelectionBox\",\n        \"onDragBox\",\n        \"onGrabbedBox\",\n        \"onContextMenu\",\n        \"onOpenFile\",\n        \"onElementLabel\",\n      ];\n\n      const pluginOpts: Record<string, unknown> = {};\n      const hooks: Record<string, unknown> = {};\n      const regularOpts: Record<string, unknown> = {};\n\n      for (const [key, value] of Object.entries(opts)) {\n        if (pluginKeys.includes(key)) {\n          pluginOpts[key] = value;\n        } else if (hookKeys.includes(key)) {\n          hooks[key] = value;\n        } else {\n          regularOpts[key] = value;\n        }\n      }\n\n      if (Object.keys(regularOpts).length > 0) {\n        api?.setOptions(regularOpts);\n      }\n\n      if (Object.keys(pluginOpts).length > 0 || Object.keys(hooks).length > 0) {\n        api?.unregisterPlugin(\"test-options\");\n        api?.registerPlugin({\n          name: \"test-options\",\n          ...pluginOpts,\n          ...(Object.keys(hooks).length > 0 ? { hooks } : {}),\n        });\n      }\n    }, options);\n  };\n\n  const reinitialize = async (options?: Record<string, unknown>) => {\n    await page.evaluate((opts) => {\n      const existingApi = (\n        window as { __REACT_GRAB__?: { dispose: () => void } }\n      ).__REACT_GRAB__;\n      existingApi?.dispose();\n\n      const initFn = (\n        window as { initReactGrab?: (o?: Record<string, unknown>) => void }\n      ).initReactGrab;\n      initFn?.(opts);\n    }, options);\n    await page.waitForFunction(\n      () => {\n        const api = (window as { __REACT_GRAB__?: unknown }).__REACT_GRAB__;\n        return api !== undefined;\n      },\n      undefined,\n      { timeout: 5000 },\n    );\n  };\n\n  const setupMockAgent = async (options?: {\n    delay?: number;\n    error?: string;\n    statusUpdates?: string[];\n  }) => {\n    await page.evaluate((opts) => {\n      const delay = opts?.delay ?? 500;\n      const error = opts?.error;\n      const statusUpdates = opts?.statusUpdates ?? [\n        \"Processing...\",\n        \"Almost done...\",\n      ];\n\n      const mockProvider = {\n        async *send() {\n          for (let i = 0; i < statusUpdates.length; i++) {\n            yield statusUpdates[i];\n            await new Promise((resolve) =>\n              setTimeout(resolve, delay / statusUpdates.length),\n            );\n          }\n          if (error) {\n            throw new Error(error);\n          }\n          yield \"Completed\";\n        },\n        supportsFollowUp: true,\n        undo: async () => {},\n        canUndo: () => true,\n        redo: async () => {},\n        canRedo: () => true,\n      };\n\n      const api = (\n        window as {\n          __REACT_GRAB__?: {\n            unregisterPlugin: (name: string) => void;\n            registerPlugin: (plugin: {\n              name: string;\n              actions: Array<Record<string, unknown>>;\n            }) => void;\n          };\n        }\n      ).__REACT_GRAB__;\n      api?.unregisterPlugin(\"mock-agent\");\n      const agent = { provider: mockProvider };\n      api?.registerPlugin({\n        name: \"mock-agent\",\n        actions: [\n          {\n            id: \"edit-with-mock-agent\",\n            label: \"Edit\",\n            shortcut: \"Enter\",\n            onAction: (context: {\n              enterPromptMode?: (agent?: Record<string, unknown>) => void;\n            }) => {\n              context.enterPromptMode?.(agent);\n            },\n            agent,\n          },\n        ],\n      });\n    }, options);\n  };\n\n  const getAgentSessions = async (): Promise<AgentSessionInfo[]> => {\n    return page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return [];\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return [];\n\n      const sessions: AgentSessionInfo[] = [];\n      const sessionElements = root.querySelectorAll(\n        \"[data-react-grab-ignore-events]\",\n      );\n\n      sessionElements.forEach((element) => {\n        const textContent = element.textContent ?? \"\";\n        if (\n          textContent.includes(\"Processing\") ||\n          textContent.includes(\"Completed\") ||\n          textContent.includes(\"Error\")\n        ) {\n          const statusMatch = textContent.match(\n            /(Processing|Completed|Error|Grabbing)/,\n          );\n          sessions.push({\n            id: `session-${sessions.length}`,\n            status: statusMatch?.[1] ?? \"unknown\",\n            isStreaming:\n              textContent.includes(\"Processing\") ||\n              textContent.includes(\"Grabbing\"),\n            error: textContent.includes(\"Error\") ? textContent : null,\n            prompt: \"\",\n          });\n        }\n      });\n\n      return sessions;\n    }, ATTRIBUTE_NAME);\n  };\n\n  const isAgentSessionVisible = async (): Promise<boolean> => {\n    const sessions = await getAgentSessions();\n    return sessions.length > 0;\n  };\n\n  const waitForAgentSession = async (timeout = 5000) => {\n    await page.waitForFunction(\n      (attrName) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return false;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        if (!root) return false;\n        const sessionElements = Array.from(\n          root.querySelectorAll(\"[data-react-grab-ignore-events]\"),\n        );\n        for (let i = 0; i < sessionElements.length; i++) {\n          const text = sessionElements[i].textContent ?? \"\";\n          if (\n            text.includes(\"Processing\") ||\n            text.includes(\"Completed\") ||\n            text.includes(\"Error\") ||\n            text.includes(\"Grabbing\")\n          ) {\n            return true;\n          }\n        }\n        return false;\n      },\n      ATTRIBUTE_NAME,\n      { timeout },\n    );\n  };\n\n  const waitForAgentComplete = async (timeout = 10000) => {\n    await page.waitForFunction(\n      (attrName) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return false;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        if (!root) return false;\n        const sessionElements = Array.from(\n          root.querySelectorAll(\"[data-react-grab-ignore-events]\"),\n        );\n        let hasSession = false;\n        let isStreaming = false;\n        for (let i = 0; i < sessionElements.length; i++) {\n          const text = sessionElements[i].textContent ?? \"\";\n          if (\n            text.includes(\"Processing\") ||\n            text.includes(\"Completed\") ||\n            text.includes(\"Error\") ||\n            text.includes(\"Grabbing\")\n          ) {\n            hasSession = true;\n            if (text.includes(\"Processing\") || text.includes(\"Grabbing\")) {\n              isStreaming = true;\n            }\n          }\n        }\n        return hasSession && !isStreaming;\n      },\n      ATTRIBUTE_NAME,\n      { timeout },\n    );\n  };\n\n  const clickAgentDismiss = async () => {\n    await page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return;\n\n      const dismissButton = root.querySelector<HTMLButtonElement>(\n        \"[data-react-grab-dismiss]\",\n      );\n      dismissButton?.click();\n    }, ATTRIBUTE_NAME);\n  };\n\n  const clickAgentUndo = async () => {\n    await page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return;\n\n      const undoButton = root.querySelector<HTMLButtonElement>(\n        \"[data-react-grab-undo]\",\n      );\n      undoButton?.click();\n    }, ATTRIBUTE_NAME);\n  };\n\n  const clickAgentRetry = async () => {\n    await page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return;\n\n      const retryButton = root.querySelector<HTMLButtonElement>(\n        \"[data-react-grab-retry]\",\n      );\n      retryButton?.click();\n    }, ATTRIBUTE_NAME);\n  };\n\n  const clickAgentAbort = async () => {\n    await page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return;\n\n      const abortButton = root.querySelector<HTMLButtonElement>(\n        \"[data-react-grab-abort]\",\n      );\n      abortButton?.click();\n    }, ATTRIBUTE_NAME);\n  };\n\n  const confirmAgentAbort = async () => {\n    await page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return;\n\n      const yesButton = root.querySelector<HTMLButtonElement>(\n        \"[data-react-grab-discard-yes]\",\n      );\n      yesButton?.click();\n    }, ATTRIBUTE_NAME);\n  };\n\n  const cancelAgentAbort = async () => {\n    await page.evaluate((attrName) => {\n      const host = document.querySelector(`[${attrName}]`);\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return;\n      const root = shadowRoot.querySelector(`[${attrName}]`);\n      if (!root) return;\n\n      const noButton = root.querySelector<HTMLButtonElement>(\n        \"[data-react-grab-discard-no]\",\n      );\n      noButton?.click();\n    }, ATTRIBUTE_NAME);\n  };\n\n  const dispatchPointerEvent = async (\n    type: \"pointerdown\" | \"pointermove\" | \"pointerup\",\n    x: number,\n    y: number,\n    pointerId = 1,\n  ) => {\n    await page.evaluate(\n      ({ type, x, y, pointerId }) => {\n        const target = document.elementFromPoint(x, y) || document.body;\n        const pointerEvent = new PointerEvent(type, {\n          bubbles: true,\n          cancelable: true,\n          clientX: x,\n          clientY: y,\n          screenX: x,\n          screenY: y,\n          pointerId,\n          pointerType: \"touch\",\n          isPrimary: true,\n          button: type === \"pointermove\" ? -1 : 0,\n          buttons: type === \"pointerup\" ? 0 : 1,\n        });\n        target.dispatchEvent(pointerEvent);\n      },\n      { type, x, y, pointerId },\n    );\n  };\n\n  const touchStart = async (x: number, y: number) => {\n    await dispatchPointerEvent(\"pointerdown\", x, y);\n  };\n\n  const touchMove = async (x: number, y: number) => {\n    await dispatchPointerEvent(\"pointermove\", x, y);\n  };\n\n  const touchEnd = async (x: number, y: number) => {\n    await dispatchPointerEvent(\"pointerup\", x, y);\n  };\n\n  const touchTap = async (selector: string) => {\n    const element = page.locator(selector).first();\n    const box = await element.boundingBox();\n    if (box) {\n      await page.touchscreen.tap(box.x + box.width / 2, box.y + box.height / 2);\n    }\n  };\n\n  const touchDrag = async (\n    startX: number,\n    startY: number,\n    endX: number,\n    endY: number,\n  ) => {\n    await dispatchPointerEvent(\"pointerdown\", startX, startY);\n\n    const steps = 10;\n    for (let i = 1; i <= steps; i++) {\n      const currentX = startX + ((endX - startX) * i) / steps;\n      const currentY = startY + ((endY - startY) * i) / steps;\n      await dispatchPointerEvent(\"pointermove\", currentX, currentY);\n    }\n\n    await dispatchPointerEvent(\"pointerup\", endX, endY);\n  };\n\n  const isTouchMode = async (): Promise<boolean> => {\n    return page.evaluate(() => {\n      const api = (\n        window as {\n          __REACT_GRAB__?: { getState: () => { isTouchMode?: boolean } };\n        }\n      ).__REACT_GRAB__;\n      return (\n        (api?.getState() as { isTouchMode?: boolean })?.isTouchMode ?? false\n      );\n    });\n  };\n\n  const setViewportSize = async (width: number, height: number) => {\n    await page.setViewportSize({ width, height });\n    await page.waitForFunction(\n      ({ expectedWidth, expectedHeight }) =>\n        window.innerWidth === expectedWidth &&\n        window.innerHeight === expectedHeight,\n      { expectedWidth: width, expectedHeight: height },\n      { timeout: 2000 },\n    );\n    await page.evaluate(() => {\n      window.dispatchEvent(new Event(\"resize\"));\n    });\n  };\n\n  const getViewportSize = async (): Promise<{\n    width: number;\n    height: number;\n  }> => {\n    return page.evaluate(() => ({\n      width: window.innerWidth,\n      height: window.innerHeight,\n    }));\n  };\n\n  const removeElement = async (selector: string) => {\n    await page.evaluate((sel) => {\n      const element = document.querySelector(sel);\n      element?.remove();\n    }, selector);\n  };\n\n  const hideElement = async (selector: string) => {\n    await page.evaluate((sel) => {\n      const element = document.querySelector(sel) as HTMLElement;\n      if (element) element.style.display = \"none\";\n    }, selector);\n  };\n\n  const showElement = async (selector: string) => {\n    await page.evaluate((sel) => {\n      const element = document.querySelector(sel) as HTMLElement;\n      if (element) element.style.display = \"\";\n    }, selector);\n  };\n\n  const getElementBounds = async (\n    selector: string,\n  ): Promise<{\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n  } | null> => {\n    const element = page.locator(selector).first();\n    const box = await element.boundingBox();\n    return box\n      ? { x: box.x, y: box.y, width: box.width, height: box.height }\n      : null;\n  };\n\n  const isDropdownOpen = async (): Promise<boolean> => {\n    const dropdownMenu = page.locator('[data-testid=\"dropdown-menu\"]');\n    return dropdownMenu.isVisible();\n  };\n\n  const openDropdown = async () => {\n    const trigger = page.locator('[data-testid=\"dropdown-trigger\"]');\n    await trigger.click();\n    await page.waitForSelector('[data-testid=\"dropdown-menu\"]', {\n      state: \"visible\",\n      timeout: 2000,\n    });\n  };\n\n  const setupCallbackTracking = async () => {\n    await page.evaluate(() => {\n      (\n        window as {\n          __CALLBACK_HISTORY__?: Array<{\n            name: string;\n            args: unknown[];\n            timestamp: number;\n          }>;\n        }\n      ).__CALLBACK_HISTORY__ = [];\n\n      const trackCallback =\n        (name: string) =>\n        (...args: unknown[]) => {\n          (\n            window as {\n              __CALLBACK_HISTORY__?: Array<{\n                name: string;\n                args: unknown[];\n                timestamp: number;\n              }>;\n            }\n          ).__CALLBACK_HISTORY__?.push({ name, args, timestamp: Date.now() });\n        };\n\n      const api = (\n        window as {\n          __REACT_GRAB__?: {\n            unregisterPlugin: (name: string) => void;\n            registerPlugin: (plugin: {\n              name: string;\n              hooks: Record<string, unknown>;\n            }) => void;\n          };\n        }\n      ).__REACT_GRAB__;\n      api?.unregisterPlugin(\"callback-tracking\");\n      api?.registerPlugin({\n        name: \"callback-tracking\",\n        hooks: {\n          onActivate: trackCallback(\"onActivate\"),\n          onDeactivate: trackCallback(\"onDeactivate\"),\n          onElementHover: trackCallback(\"onElementHover\"),\n          onElementSelect: trackCallback(\"onElementSelect\"),\n          onDragStart: trackCallback(\"onDragStart\"),\n          onDragEnd: trackCallback(\"onDragEnd\"),\n          onBeforeCopy: trackCallback(\"onBeforeCopy\"),\n          onAfterCopy: trackCallback(\"onAfterCopy\"),\n          onCopySuccess: trackCallback(\"onCopySuccess\"),\n          onCopyError: trackCallback(\"onCopyError\"),\n          onStateChange: trackCallback(\"onStateChange\"),\n          onPromptModeChange: trackCallback(\"onPromptModeChange\"),\n          onSelectionBox: trackCallback(\"onSelectionBox\"),\n          onDragBox: trackCallback(\"onDragBox\"),\n          onGrabbedBox: trackCallback(\"onGrabbedBox\"),\n          onContextMenu: trackCallback(\"onContextMenu\"),\n          onOpenFile: trackCallback(\"onOpenFile\"),\n        },\n      });\n    });\n  };\n\n  const getCallbackHistory = async (): Promise<\n    Array<{ name: string; args: unknown[]; timestamp: number }>\n  > => {\n    return page.evaluate(() => {\n      return (\n        (\n          window as {\n            __CALLBACK_HISTORY__?: Array<{\n              name: string;\n              args: unknown[];\n              timestamp: number;\n            }>;\n          }\n        ).__CALLBACK_HISTORY__ ?? []\n      );\n    });\n  };\n\n  const clearCallbackHistory = async () => {\n    await page.evaluate(() => {\n      (\n        window as {\n          __CALLBACK_HISTORY__?: Array<{\n            name: string;\n            args: unknown[];\n            timestamp: number;\n          }>;\n        }\n      ).__CALLBACK_HISTORY__ = [];\n    });\n  };\n\n  const waitForCallback = async (\n    name: string,\n    timeout = 5000,\n  ): Promise<unknown[]> => {\n    await page.waitForFunction(\n      (callbackName) => {\n        const history =\n          (window as { __CALLBACK_HISTORY__?: Array<{ name: string }> })\n            .__CALLBACK_HISTORY__ ?? [];\n        return history.some((c) => c.name === callbackName);\n      },\n      name,\n      { timeout },\n    );\n    const history = await getCallbackHistory();\n    const callback = history.find((c) => c.name === name);\n    return callback?.args ?? [];\n  };\n\n  return {\n    page,\n    modifierKey: MODIFIER_KEY,\n    activate,\n    activateViaKeyboard,\n    deactivate,\n    holdToActivate,\n    isOverlayVisible,\n    getOverlayHost,\n    getShadowRoot,\n    hoverElement,\n    clickElement,\n    rightClickElement,\n    rightClickAtPosition,\n    dragSelect,\n    getClipboardContent,\n    captureNextClipboardWrites,\n    waitForSelectionBox,\n    waitForSelectionSource,\n    isContextMenuVisible,\n    getContextMenuInfo,\n    isContextMenuItemEnabled,\n    clickContextMenuItem,\n    isSelectionBoxVisible,\n    pressEscape,\n    pressArrowDown,\n    pressArrowUp,\n    pressArrowLeft,\n    pressArrowRight,\n    pressEnter,\n    pressKey,\n    pressKeyCombo,\n    pressModifierKeyCombo,\n    scrollPage,\n\n    enterPromptMode,\n    isPromptModeActive,\n    typeInInput,\n    getInputValue,\n    submitInput,\n    clearInput,\n    isPendingDismissVisible,\n\n    isToolbarVisible,\n    isToolbarCollapsed,\n    getToolbarInfo,\n    clickToolbarToggle,\n    clickToolbarCollapse,\n    clickToolbarEnabled,\n    dragToolbar,\n    dragToolbarFromButton,\n\n    isToolbarMenuButtonVisible,\n    clickToolbarMenuButton,\n    isToolbarMenuVisible,\n    getToolbarMenuInfo,\n    clickToolbarMenuItem,\n\n    isHistoryButtonVisible,\n    hasUnreadHistoryIndicator,\n    clickHistoryButton,\n    isHistoryDropdownVisible,\n    getHistoryDropdownInfo,\n    clickHistoryItem,\n    clickHistoryItemRemove,\n    clickHistoryItemCopy,\n    clickHistoryCopyAll,\n    clickHistoryClear,\n    hoverHistoryItem,\n    hoverHistoryButton,\n    hoverCopyAllButton,\n    clickToolbarCopyAll,\n    isToolbarCopyAllVisible,\n    isClearHistoryPromptVisible,\n    confirmClearHistoryPrompt,\n    cancelClearHistoryPrompt,\n    getHistoryDropdownPosition,\n\n    getSelectionLabelInfo,\n    getSelectionLabelBounds,\n    isSelectionLabelVisible,\n    waitForSelectionLabel,\n    getLabelStatusText,\n\n    getGrabbedBoxInfo,\n    getLabelInstancesInfo,\n    isGrabbedBoxVisible,\n    getDragBoxBounds,\n    getSelectionBoxBounds,\n\n    getState,\n    toggle,\n    dispose,\n    copyElementViaApi,\n    setAgent,\n    updateOptions,\n    reinitialize,\n\n    setupMockAgent,\n    getAgentSessions,\n    isAgentSessionVisible,\n    waitForAgentSession,\n    waitForAgentComplete,\n    clickAgentDismiss,\n    clickAgentUndo,\n    clickAgentRetry,\n    clickAgentAbort,\n    confirmAgentAbort,\n    cancelAgentAbort,\n\n    touchStart,\n    touchMove,\n    touchEnd,\n    touchTap,\n    touchDrag,\n    isTouchMode,\n\n    setViewportSize,\n    getViewportSize,\n\n    removeElement,\n    hideElement,\n    showElement,\n    getElementBounds,\n    isDropdownOpen,\n    openDropdown,\n\n    setupCallbackTracking,\n    getCallbackHistory,\n    clearCallbackHistory,\n    waitForCallback,\n  };\n};\n\nexport const test = base.extend<{ reactGrab: ReactGrabPageObject }>({\n  reactGrab: async ({ page }, use) => {\n    const waitForApiReady = async () => {\n      await page.waitForFunction(\n        () => {\n          const api = (window as { __REACT_GRAB__?: unknown }).__REACT_GRAB__;\n          return api !== undefined;\n        },\n        undefined,\n        { timeout: PAGE_SETUP_API_TIMEOUT_MS },\n      );\n    };\n\n    const initializePage = async () => {\n      let lastError: unknown;\n      for (\n        let attemptIndex = 0;\n        attemptIndex < PAGE_SETUP_MAX_ATTEMPTS;\n        attemptIndex++\n      ) {\n        if (page.isClosed()) {\n          throw new Error(\"Browser page closed during reactGrab fixture setup\");\n        }\n        try {\n          await page.goto(\"/\", {\n            waitUntil: \"domcontentloaded\",\n            timeout: PAGE_SETUP_NAVIGATION_TIMEOUT_MS,\n          });\n          await waitForApiReady();\n          return;\n        } catch (error) {\n          lastError = error;\n          if (page.isClosed()) {\n            throw lastError;\n          }\n          if (attemptIndex === PAGE_SETUP_MAX_ATTEMPTS - 1) {\n            throw lastError;\n          }\n          // HACK: brief backoff helps when dev server is under heavy parallel load.\n          await new Promise((resolve) => {\n            setTimeout(resolve, 250 * (attemptIndex + 1));\n          });\n        }\n      }\n    };\n\n    await initializePage();\n\n    const reactGrab = createReactGrabPageObject(page);\n    await use(reactGrab);\n  },\n});\n\nexport { expect };\n"
  },
  {
    "path": "packages/react-grab/e2e/focus-trap.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\nconst FOCUS_TRAP_CONTAINER_ID = \"focus-trap-test-container\";\n\nconst injectFocusTrap = async (page: import(\"@playwright/test\").Page) => {\n  await page.evaluate((containerId) => {\n    const container = document.createElement(\"div\");\n    container.id = containerId;\n    container.innerHTML = `\n      <div id=\"focus-trap-modal\" style=\"\n        position: fixed; bottom: 16px; right: 16px;\n        width: 400px; padding: 24px; background: white; border: 2px solid #333;\n        border-radius: 8px; z-index: 9000; box-shadow: 0 4px 20px rgba(0,0,0,0.3);\n      \">\n        <h2>Focus-trapped Modal</h2>\n        <input id=\"trap-input-1\" type=\"text\" placeholder=\"First input\" style=\"display:block; margin: 8px 0; padding: 8px; width: 100%;\" />\n        <input id=\"trap-input-2\" type=\"text\" placeholder=\"Second input\" style=\"display:block; margin: 8px 0; padding: 8px; width: 100%;\" />\n        <button id=\"trap-button\" style=\"padding: 8px 16px; margin-top: 8px;\">Trapped Button</button>\n      </div>\n      <div id=\"focus-trap-backdrop\" style=\"\n        position: fixed; inset: 0; background: rgba(0,0,0,0.4); z-index: 8999;\n      \"></div>\n    `;\n    document.body.appendChild(container);\n\n    const modal = document.getElementById(\"focus-trap-modal\")!;\n    const focusableSelector =\n      'input:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\n    const getFocusableElements = () =>\n      Array.from(modal.querySelectorAll(focusableSelector)) as HTMLElement[];\n\n    const focusInHandler = (event: FocusEvent) => {\n      const target = event.target as Node;\n      if (!modal.contains(target)) {\n        event.stopImmediatePropagation();\n        const focusable = getFocusableElements();\n        if (focusable.length > 0) {\n          focusable[0].focus();\n        }\n      }\n    };\n    document.addEventListener(\"focusin\", focusInHandler, true);\n\n    const keydownHandler = (event: KeyboardEvent) => {\n      if (event.key !== \"Tab\") return;\n\n      const focusable = getFocusableElements();\n      if (focusable.length === 0) return;\n\n      const firstElement = focusable[0];\n      const lastElement = focusable[focusable.length - 1];\n\n      if (event.shiftKey) {\n        if (document.activeElement === firstElement) {\n          event.preventDefault();\n          lastElement.focus();\n        }\n      } else {\n        if (document.activeElement === lastElement) {\n          event.preventDefault();\n          firstElement.focus();\n        }\n      }\n    };\n    document.addEventListener(\"keydown\", keydownHandler, true);\n\n    (window as { __FOCUS_TRAP_CLEANUP__?: () => void }).__FOCUS_TRAP_CLEANUP__ =\n      () => {\n        document.removeEventListener(\"focusin\", focusInHandler, true);\n        document.removeEventListener(\"keydown\", keydownHandler, true);\n      };\n\n    const firstInput = document.getElementById(\"trap-input-1\");\n    firstInput?.focus();\n  }, FOCUS_TRAP_CONTAINER_ID);\n};\n\nconst removeFocusTrap = async (page: import(\"@playwright/test\").Page) => {\n  await page.evaluate((containerId) => {\n    (\n      window as { __FOCUS_TRAP_CLEANUP__?: () => void }\n    ).__FOCUS_TRAP_CLEANUP__?.();\n    document.getElementById(containerId)?.remove();\n  }, FOCUS_TRAP_CONTAINER_ID);\n};\n\ntest.describe(\"Focus Trap Resistance\", () => {\n  test.afterEach(async ({ reactGrab }) => {\n    await removeFocusTrap(reactGrab.page);\n  });\n\n  test.describe(\"Activation\", () => {\n    test(\"should activate via API while focus trap is active\", async ({\n      reactGrab,\n    }) => {\n      await injectFocusTrap(reactGrab.page);\n      await reactGrab.activate();\n\n      const isActive = await reactGrab.isOverlayVisible();\n      expect(isActive).toBe(true);\n    });\n\n    test(\"should deactivate with Escape while focus trap is active\", async ({\n      reactGrab,\n    }) => {\n      await injectFocusTrap(reactGrab.page);\n      await reactGrab.activate();\n      await reactGrab.deactivate();\n\n      const isActive = await reactGrab.isOverlayVisible();\n      expect(isActive).toBe(false);\n    });\n  });\n\n  test.describe(\"Element Selection\", () => {\n    test(\"should hover and select elements behind focus trap backdrop\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await injectFocusTrap(reactGrab.page);\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      const isVisible = await reactGrab.isSelectionBoxVisible();\n      expect(isVisible).toBe(true);\n    });\n\n    test(\"should select elements inside the focus-trapped modal\", async ({\n      reactGrab,\n    }) => {\n      await injectFocusTrap(reactGrab.page);\n      await reactGrab.activate();\n\n      await reactGrab.hoverElement(\"#trap-button\");\n      await reactGrab.waitForSelectionBox();\n\n      const isVisible = await reactGrab.isSelectionBoxVisible();\n      expect(isVisible).toBe(true);\n    });\n\n    test(\"should update selection when hovering different elements\", async ({\n      reactGrab,\n    }) => {\n      await injectFocusTrap(reactGrab.page);\n      await reactGrab.activate();\n\n      await reactGrab.hoverElement(\"#trap-input-1\");\n      await reactGrab.waitForSelectionBox();\n      const bounds1 = await reactGrab.getSelectionBoxBounds();\n\n      await reactGrab.hoverElement(\"#trap-button\");\n      await reactGrab.waitForSelectionBox();\n      const bounds2 = await reactGrab.getSelectionBoxBounds();\n\n      if (bounds1 && bounds2) {\n        const didSelectionChange =\n          bounds1.y !== bounds2.y || bounds1.height !== bounds2.height;\n        expect(didSelectionChange).toBe(true);\n      }\n    });\n  });\n\n  test.describe(\"Copy\", () => {\n    test(\"should copy element while focus trap is active\", async ({\n      reactGrab,\n    }) => {\n      await injectFocusTrap(reactGrab.page);\n      await reactGrab.activate();\n\n      await reactGrab.hoverElement(\"#trap-button\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.clickElement(\"#trap-button\");\n\n      await expect\n        .poll(() => reactGrab.getClipboardContent(), { timeout: 2000 })\n        .toBeTruthy();\n    });\n\n    test(\"should copy element outside modal while focus trap is active\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await injectFocusTrap(reactGrab.page);\n\n      await reactGrab.hoverElement(\"h1\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.clickElement(\"h1\");\n\n      await expect\n        .poll(() => reactGrab.getClipboardContent(), { timeout: 2000 })\n        .toBeTruthy();\n    });\n  });\n\n  test.describe(\"Prompt Mode\", () => {\n    test(\"should enter prompt mode while focus trap is active\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await injectFocusTrap(reactGrab.page);\n\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      const isPromptMode = await reactGrab.isPromptModeActive();\n      expect(isPromptMode).toBe(true);\n    });\n\n    test(\"textarea should receive typed input despite focus trap\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await injectFocusTrap(reactGrab.page);\n\n      await reactGrab.enterPromptMode(\"li:first-child\");\n      await reactGrab.typeInInput(\"Hello from inside focus trap\");\n\n      const inputValue = await reactGrab.getInputValue();\n      expect(inputValue).toBe(\"Hello from inside focus trap\");\n    });\n\n    test(\"should submit prompt while focus trap is active\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent({ delay: 100 });\n      await injectFocusTrap(reactGrab.page);\n\n      await reactGrab.enterPromptMode(\"li:first-child\");\n      await reactGrab.typeInInput(\"Test prompt\");\n      await reactGrab.submitInput();\n\n      await expect.poll(() => reactGrab.isPromptModeActive()).toBe(false);\n    });\n\n    test(\"Escape should dismiss prompt mode despite focus trap\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await injectFocusTrap(reactGrab.page);\n\n      await reactGrab.enterPromptMode(\"li:first-child\");\n      await reactGrab.pressEscape();\n      await reactGrab.pressEscape();\n\n      await expect\n        .poll(() => reactGrab.isOverlayVisible(), { timeout: 5000 })\n        .toBe(false);\n    });\n  });\n\n  test.describe(\"Context Menu\", () => {\n    test(\"should open context menu while focus trap is active\", async ({\n      reactGrab,\n    }) => {\n      await injectFocusTrap(reactGrab.page);\n      await reactGrab.activate();\n\n      await reactGrab.hoverElement(\"#trap-button\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"#trap-button\");\n\n      const isVisible = await reactGrab.isContextMenuVisible();\n      expect(isVisible).toBe(true);\n    });\n  });\n\n  test.describe(\"Keyboard Navigation\", () => {\n    test(\"arrow key navigation should work while focus trap is active\", async ({\n      reactGrab,\n    }) => {\n      await injectFocusTrap(reactGrab.page);\n      await reactGrab.activate();\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.pressArrowDown();\n      await reactGrab.waitForSelectionBox();\n\n      const isActive = await reactGrab.isOverlayVisible();\n      const isSelectionVisible = await reactGrab.isSelectionBoxVisible();\n      expect(isActive).toBe(true);\n      expect(isSelectionVisible).toBe(true);\n    });\n\n    test(\"Escape should deactivate from selection while focus trap is active\", async ({\n      reactGrab,\n    }) => {\n      await injectFocusTrap(reactGrab.page);\n      await reactGrab.activate();\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.deactivate();\n\n      const isActive = await reactGrab.isOverlayVisible();\n      expect(isActive).toBe(false);\n    });\n  });\n\n  test.describe(\"Focus Trap Lifecycle\", () => {\n    test(\"should continue working after focus trap is removed\", async ({\n      reactGrab,\n    }) => {\n      await injectFocusTrap(reactGrab.page);\n      await reactGrab.activate();\n\n      await reactGrab.hoverElement(\"#trap-button\");\n      await reactGrab.waitForSelectionBox();\n\n      await removeFocusTrap(reactGrab.page);\n      await reactGrab.page.waitForTimeout(100);\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      const isVisible = await reactGrab.isSelectionBoxVisible();\n      expect(isVisible).toBe(true);\n    });\n\n    test(\"should work when focus trap appears after activation\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await injectFocusTrap(reactGrab.page);\n      await reactGrab.page.waitForTimeout(100);\n\n      await reactGrab.hoverElement(\"h1\");\n      await reactGrab.waitForSelectionBox();\n\n      const isVisible = await reactGrab.isSelectionBoxVisible();\n      expect(isVisible).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/freeze-animations.spec.ts",
    "content": "import type { Page } from \"@playwright/test\";\nimport { test, expect } from \"./fixtures.js\";\n\nconst ATTRIBUTE_NAME = \"data-react-grab\";\n\nconst simulateGsapPresence = (page: Page): Promise<void> =>\n  page.evaluate(() => {\n    (window as unknown as Record<string, string[]>).gsapVersions = [\"3.12.0\"];\n  });\n\nconst navigateAndWaitForReactGrab = async (page: Page): Promise<void> => {\n  await page.goto(\"/\", { waitUntil: \"domcontentloaded\" });\n  await page.waitForFunction(\n    () => (window as { __REACT_GRAB__?: unknown }).__REACT_GRAB__ !== undefined,\n    { timeout: 10000 },\n  );\n};\n\nconst activateViaApi = (page: Page): Promise<void> =>\n  page.evaluate(() => {\n    (\n      window as unknown as { __REACT_GRAB__: { activate: () => void } }\n    ).__REACT_GRAB__.activate();\n  });\n\nconst deactivateViaApi = (page: Page): Promise<void> =>\n  page.evaluate(() => {\n    (\n      window as unknown as { __REACT_GRAB__: { deactivate: () => void } }\n    ).__REACT_GRAB__.deactivate();\n  });\n\ntest.describe(\"Freeze Animations\", () => {\n  test.describe(\"Page Animation Freezing\", () => {\n    test(\"should pause page animations when activated\", async ({\n      reactGrab,\n    }) => {\n      const getPageAnimationStates = async () => {\n        return reactGrab.page.evaluate((attrName) => {\n          return document\n            .getAnimations()\n            .reduce<string[]>((states, animation) => {\n              if (animation.effect instanceof KeyframeEffect) {\n                const target = animation.effect.target;\n                if (target instanceof Element) {\n                  const rootNode = target.getRootNode();\n                  if (\n                    rootNode instanceof ShadowRoot &&\n                    rootNode.host.hasAttribute(attrName)\n                  ) {\n                    return states;\n                  }\n                }\n              }\n              states.push(animation.playState);\n              return states;\n            }, []);\n        }, ATTRIBUTE_NAME);\n      };\n\n      const statesBefore = await getPageAnimationStates();\n      expect(statesBefore.length).toBeGreaterThan(0);\n      expect(statesBefore.every((state) => state === \"running\")).toBe(true);\n\n      await reactGrab.activate();\n      await reactGrab.page.waitForTimeout(100);\n\n      const statesDuring = await getPageAnimationStates();\n      expect(statesDuring.every((state) => state === \"paused\")).toBe(true);\n    });\n\n    test(\"should not leave page animations in paused state after deactivation\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.page.waitForTimeout(100);\n\n      await reactGrab.deactivate();\n      await reactGrab.page.waitForTimeout(100);\n\n      const pausedPageAnimationCount = await reactGrab.page.evaluate(\n        (attrName) => {\n          return document.getAnimations().filter((animation) => {\n            if (animation.effect instanceof KeyframeEffect) {\n              const target = animation.effect.target;\n              if (target instanceof Element) {\n                const rootNode = target.getRootNode();\n                if (\n                  rootNode instanceof ShadowRoot &&\n                  rootNode.host.hasAttribute(attrName)\n                ) {\n                  return false;\n                }\n              }\n            }\n            return animation.playState === \"paused\";\n          }).length;\n        },\n        ATTRIBUTE_NAME,\n      );\n\n      expect(pausedPageAnimationCount).toBe(0);\n    });\n\n    test(\"should not leave global freeze style element in document after deactivation\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.page.waitForTimeout(100);\n\n      const hasFreezeStyleDuring = await reactGrab.page.evaluate(() => {\n        return (\n          document.querySelector(\"[data-react-grab-global-freeze]\") !== null\n        );\n      });\n      expect(hasFreezeStyleDuring).toBe(true);\n\n      await reactGrab.deactivate();\n      await reactGrab.page.waitForTimeout(100);\n\n      const hasFreezeStyleAfter = await reactGrab.page.evaluate(() => {\n        return (\n          document.querySelector(\"[data-react-grab-global-freeze]\") !== null\n        );\n      });\n      expect(hasFreezeStyleAfter).toBe(false);\n    });\n  });\n\n  test.describe(\"React Grab UI Preservation\", () => {\n    test(\"should not finish react-grab shadow DOM animations on deactivation\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.page.waitForTimeout(200);\n\n      const shadowAnimationCountBefore = await reactGrab.page.evaluate(\n        (attrName) => {\n          return document.getAnimations().filter((animation) => {\n            if (animation.effect instanceof KeyframeEffect) {\n              const target = animation.effect.target;\n              if (target instanceof Element) {\n                const rootNode = target.getRootNode();\n                return (\n                  rootNode instanceof ShadowRoot &&\n                  rootNode.host.hasAttribute(attrName)\n                );\n              }\n            }\n            return false;\n          }).length;\n        },\n        ATTRIBUTE_NAME,\n      );\n\n      await reactGrab.deactivate();\n      await reactGrab.page.waitForTimeout(100);\n\n      const shadowAnimationCountAfter = await reactGrab.page.evaluate(\n        (attrName) => {\n          return document.getAnimations().filter((animation) => {\n            if (animation.effect instanceof KeyframeEffect) {\n              const target = animation.effect.target;\n              if (target instanceof Element) {\n                const rootNode = target.getRootNode();\n                return (\n                  rootNode instanceof ShadowRoot &&\n                  rootNode.host.hasAttribute(attrName)\n                );\n              }\n            }\n            return false;\n          }).length;\n        },\n        ATTRIBUTE_NAME,\n      );\n\n      if (shadowAnimationCountBefore > 0) {\n        expect(shadowAnimationCountAfter).toBe(shadowAnimationCountBefore);\n      }\n    });\n\n    test(\"toolbar should remain visible after activation cycle\", async ({\n      reactGrab,\n    }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.activate();\n      await reactGrab.deactivate();\n      await reactGrab.page.waitForTimeout(200);\n\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n    });\n\n    test(\"toolbar should remain functional after activation cycle\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.deactivate();\n      await reactGrab.page.waitForTimeout(200);\n\n      await reactGrab.clickToolbarToggle();\n      expect(await reactGrab.isOverlayVisible()).toBe(true);\n\n      await reactGrab.clickToolbarToggle();\n      expect(await reactGrab.isOverlayVisible()).toBe(false);\n    });\n\n    test(\"selection label should be visible during hover after prior activation cycle\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.deactivate();\n      await reactGrab.page.waitForTimeout(200);\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.waitForSelectionLabel();\n\n      const labelInfo = await reactGrab.getSelectionLabelInfo();\n      expect(labelInfo.isVisible).toBe(true);\n    });\n  });\n\n  test.describe(\"Freeze/Unfreeze Cycles\", () => {\n    test(\"should handle rapid activation cycles without breaking animations\", async ({\n      reactGrab,\n    }) => {\n      for (let iteration = 0; iteration < 5; iteration++) {\n        await reactGrab.activate();\n        await reactGrab.page.waitForTimeout(50);\n        await reactGrab.deactivate();\n        await reactGrab.page.waitForTimeout(50);\n      }\n\n      const hasFreezeStyle = await reactGrab.page.evaluate(() => {\n        return (\n          document.querySelector(\"[data-react-grab-global-freeze]\") !== null\n        );\n      });\n      expect(hasFreezeStyle).toBe(false);\n\n      const toolbarVisible = await reactGrab.isToolbarVisible();\n      expect(toolbarVisible).toBe(true);\n    });\n\n    test(\"should correctly freeze animations after reactivation\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.deactivate();\n      await reactGrab.page.waitForTimeout(200);\n\n      await reactGrab.page.evaluate(() => {\n        const element = document.querySelector(\n          \"[data-testid='animated-section']\",\n        );\n        if (element) {\n          const child = document.createElement(\"div\");\n          child.className = \"animate-ping w-4 h-4 bg-yellow-500 rounded-full\";\n          child.setAttribute(\"data-testid\", \"injected-animation\");\n          element.appendChild(child);\n        }\n      });\n      await reactGrab.page.waitForTimeout(100);\n\n      await reactGrab.activate();\n      await reactGrab.page.waitForTimeout(100);\n\n      const pausedAnimationCount = await reactGrab.page.evaluate((attrName) => {\n        return document.getAnimations().filter((animation) => {\n          if (animation.effect instanceof KeyframeEffect) {\n            const target = animation.effect.target;\n            if (target instanceof Element) {\n              const rootNode = target.getRootNode();\n              if (\n                rootNode instanceof ShadowRoot &&\n                rootNode.host.hasAttribute(attrName)\n              ) {\n                return false;\n              }\n            }\n          }\n          return animation.playState === \"paused\";\n        }).length;\n      }, ATTRIBUTE_NAME);\n\n      expect(pausedAnimationCount).toBeGreaterThan(0);\n\n      await reactGrab.deactivate();\n    });\n\n    test(\"should not leave stale freeze styles after toolbar hover cycle\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.page.waitForTimeout(200);\n\n      await reactGrab.deactivate();\n      await reactGrab.page.waitForTimeout(200);\n\n      const hasFreezeStyle = await reactGrab.page.evaluate(() => {\n        return (\n          document.querySelector(\"[data-react-grab-global-freeze]\") !== null\n        );\n      });\n      expect(hasFreezeStyle).toBe(false);\n    });\n  });\n\n  test.describe(\"Toolbar Hover Freeze\", () => {\n    test(\"should clean up freeze styles after toolbar hover cycle\", async ({\n      reactGrab,\n    }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      const toolbarInfo = await reactGrab.getToolbarInfo();\n\n      if (toolbarInfo.position) {\n        await reactGrab.page.mouse.move(\n          toolbarInfo.position.x + 10,\n          toolbarInfo.position.y + 10,\n        );\n        await reactGrab.page.waitForTimeout(200);\n      }\n\n      await reactGrab.page.mouse.move(0, 0);\n      await reactGrab.page.waitForTimeout(200);\n\n      const hasFreezeStyle = await reactGrab.page.evaluate(() => {\n        return (\n          document.querySelector(\"[data-react-grab-global-freeze]\") !== null\n        );\n      });\n      expect(hasFreezeStyle).toBe(false);\n    });\n  });\n\n  test.describe(\"rAF Interception\", () => {\n    test(\"should wrap window.requestAnimationFrame and cancelAnimationFrame\", async ({\n      reactGrab,\n    }) => {\n      const isWrapped = await reactGrab.page.evaluate(() => {\n        const rafSource = window.requestAnimationFrame.toString();\n        const cafSource = window.cancelAnimationFrame.toString();\n        return (\n          !rafSource.includes(\"[native code]\") &&\n          !cafSource.includes(\"[native code]\")\n        );\n      });\n      expect(isWrapped).toBe(true);\n    });\n\n    test(\"should execute non-animation rAF callbacks during freeze\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.page.waitForTimeout(100);\n\n      const didCallbackExecute = await reactGrab.page.evaluate(() => {\n        return new Promise<boolean>((resolve) => {\n          window.requestAnimationFrame(() => resolve(true));\n          setTimeout(() => resolve(false), 1000);\n        });\n      });\n\n      expect(didCallbackExecute).toBe(true);\n    });\n\n    test(\"should hold animation library callbacks during freeze\", async ({\n      reactGrab,\n    }) => {\n      await simulateGsapPresence(reactGrab.page);\n      await reactGrab.activate();\n      await reactGrab.page.waitForTimeout(100);\n\n      const wasCallbackHeld = await reactGrab.page.evaluate(() => {\n        return new Promise<boolean>((resolve) => {\n          // HACK: function named _tick simulates GSAP's internal tick,\n          // detected via stack trace inspection in the rAF wrapper\n          const _tick = () => {\n            let didExecute = false;\n            window.requestAnimationFrame(() => {\n              didExecute = true;\n            });\n            setTimeout(() => resolve(!didExecute), 200);\n          };\n          _tick();\n        });\n      });\n\n      expect(wasCallbackHeld).toBe(true);\n    });\n\n    test(\"should release held callbacks after unfreeze\", async ({\n      reactGrab,\n    }) => {\n      await simulateGsapPresence(reactGrab.page);\n      await reactGrab.activate();\n      await reactGrab.page.waitForTimeout(100);\n\n      await reactGrab.page.evaluate(() => {\n        (window as unknown as Record<string, boolean>).__GSAP_TEST_FLAG__ =\n          false;\n        // HACK: function named _tick simulates GSAP's internal tick,\n        // detected via stack trace inspection in the rAF wrapper\n        const _tick = () => {\n          window.requestAnimationFrame(() => {\n            (window as unknown as Record<string, boolean>).__GSAP_TEST_FLAG__ =\n              true;\n          });\n        };\n        _tick();\n      });\n\n      await reactGrab.page.waitForTimeout(100);\n      const wasHeldDuringFreeze = await reactGrab.page.evaluate(\n        () =>\n          !(window as unknown as Record<string, boolean>).__GSAP_TEST_FLAG__,\n      );\n      expect(wasHeldDuringFreeze).toBe(true);\n\n      await reactGrab.deactivate();\n      await reactGrab.page.waitForTimeout(200);\n\n      const wasReleasedAfterUnfreeze = await reactGrab.page.evaluate(\n        () => (window as unknown as Record<string, boolean>).__GSAP_TEST_FLAG__,\n      );\n      expect(wasReleasedAfterUnfreeze).toBe(true);\n    });\n\n    test(\"should cancel held callbacks via cancelAnimationFrame\", async ({\n      reactGrab,\n    }) => {\n      await simulateGsapPresence(reactGrab.page);\n      await reactGrab.activate();\n      await reactGrab.page.waitForTimeout(100);\n\n      const wasCancelledWhileHeld = await reactGrab.page.evaluate(() => {\n        return new Promise<boolean>((resolve) => {\n          let frameIdentifier: number;\n          // HACK: function named _tick simulates GSAP's internal tick,\n          // detected via stack trace inspection in the rAF wrapper\n          const _tick = () => {\n            frameIdentifier = window.requestAnimationFrame(() => {\n              resolve(false);\n            });\n          };\n          _tick();\n          window.cancelAnimationFrame(frameIdentifier!);\n          setTimeout(() => resolve(true), 200);\n        });\n      });\n\n      expect(wasCancelledWhileHeld).toBe(true);\n    });\n\n    test(\"should cancel held callbacks across evaluate calls via returned id\", async ({\n      reactGrab,\n    }) => {\n      await simulateGsapPresence(reactGrab.page);\n      await reactGrab.activate();\n      await reactGrab.page.waitForTimeout(100);\n\n      const heldId = await reactGrab.page.evaluate(() => {\n        (window as unknown as Record<string, boolean>).__RACE_CANCEL_FLAG__ =\n          false;\n        let capturedId: number;\n        // HACK: function named _tick simulates GSAP's internal tick,\n        // detected via stack trace inspection in the rAF wrapper\n        const _tick = () => {\n          capturedId = window.requestAnimationFrame(() => {\n            (\n              window as unknown as Record<string, boolean>\n            ).__RACE_CANCEL_FLAG__ = true;\n          });\n        };\n        _tick();\n        return capturedId!;\n      });\n\n      await reactGrab.page.waitForTimeout(100);\n\n      await reactGrab.page.evaluate((identifier: number) => {\n        window.cancelAnimationFrame(identifier);\n      }, heldId);\n\n      await reactGrab.deactivate();\n      await reactGrab.page.waitForTimeout(200);\n\n      const didCallbackRun = await reactGrab.page.evaluate(\n        () =>\n          (window as unknown as Record<string, boolean>).__RACE_CANCEL_FLAG__,\n      );\n      expect(didCallbackRun).toBe(false);\n    });\n\n    test(\"should cancel replayed callbacks via fake id after unfreeze\", async ({\n      page,\n    }) => {\n      await navigateAndWaitForReactGrab(page);\n      await simulateGsapPresence(page);\n      await activateViaApi(page);\n      await page.waitForTimeout(100);\n\n      const heldId = await page.evaluate(() => {\n        (\n          window as unknown as Record<string, boolean>\n        ).__POST_UNFREEZE_CANCEL_FLAG__ = false;\n        let capturedId: number;\n        // HACK: function named _tick simulates GSAP's internal tick,\n        // detected via stack trace inspection in the rAF wrapper\n        const _tick = () => {\n          capturedId = window.requestAnimationFrame(() => {\n            (\n              window as unknown as Record<string, boolean>\n            ).__POST_UNFREEZE_CANCEL_FLAG__ = true;\n          });\n        };\n        _tick();\n        return capturedId!;\n      });\n\n      // HACK: Deactivate and cancel in the same evaluate to prevent the\n      // replayed rAF callback from firing between the two round-trips\n      await page.evaluate((identifier: number) => {\n        (\n          window as unknown as { __REACT_GRAB__: { deactivate: () => void } }\n        ).__REACT_GRAB__.deactivate();\n        window.cancelAnimationFrame(identifier);\n      }, heldId);\n\n      await page.waitForTimeout(200);\n\n      const didCallbackRun = await page.evaluate(\n        () =>\n          (window as unknown as Record<string, boolean>)\n            .__POST_UNFREEZE_CANCEL_FLAG__,\n      );\n      expect(didCallbackRun).toBe(false);\n    });\n\n    test(\"should not intercept callbacks after unfreeze\", async ({\n      reactGrab,\n    }) => {\n      await simulateGsapPresence(reactGrab.page);\n      await reactGrab.activate();\n      await reactGrab.page.waitForTimeout(100);\n      await reactGrab.deactivate();\n      await reactGrab.page.waitForTimeout(100);\n\n      const didCallbackExecuteNormally = await reactGrab.page.evaluate(() => {\n        return new Promise<boolean>((resolve) => {\n          // HACK: function named _tick simulates GSAP's internal tick,\n          // detected via stack trace inspection in the rAF wrapper\n          const _tick = () => {\n            window.requestAnimationFrame(() => resolve(true));\n          };\n          _tick();\n          setTimeout(() => resolve(false), 1000);\n        });\n      });\n\n      expect(didCallbackExecuteNormally).toBe(true);\n    });\n  });\n\n  test.describe(\"rAF Tick Loop Interception (ESM without window.gsap)\", () => {\n    test(\"should stop a _tick loop scheduled before freeze via rAF guard\", async ({\n      page,\n    }) => {\n      await navigateAndWaitForReactGrab(page);\n      await simulateGsapPresence(page);\n\n      await page.evaluate(() => {\n        (window as unknown as Record<string, number>).__RAF_TICK_COUNT__ = 0;\n        // HACK: function named _tick simulates GSAP's internal tick,\n        // detected via stack trace inspection in the rAF wrapper\n        const _tick = (): void => {\n          (window as unknown as Record<string, number>).__RAF_TICK_COUNT__++;\n          window.requestAnimationFrame(_tick);\n        };\n        window.requestAnimationFrame(_tick);\n      });\n\n      await page.waitForTimeout(200);\n      const tickCountBeforeFreeze = await page.evaluate(\n        () => (window as unknown as Record<string, number>).__RAF_TICK_COUNT__,\n      );\n      expect(tickCountBeforeFreeze).toBeGreaterThan(0);\n\n      await activateViaApi(page);\n      await page.waitForTimeout(200);\n\n      const tickCountAtFreeze = await page.evaluate(\n        () => (window as unknown as Record<string, number>).__RAF_TICK_COUNT__,\n      );\n      await page.waitForTimeout(300);\n      const tickCountAfterWaiting = await page.evaluate(\n        () => (window as unknown as Record<string, number>).__RAF_TICK_COUNT__,\n      );\n\n      expect(tickCountAfterWaiting).toBe(tickCountAtFreeze);\n    });\n\n    test(\"should resume _tick loop after unfreeze\", async ({ page }) => {\n      await navigateAndWaitForReactGrab(page);\n      await simulateGsapPresence(page);\n\n      await page.evaluate(() => {\n        (window as unknown as Record<string, number>).__RAF_TICK_COUNT__ = 0;\n        // HACK: function named _tick simulates GSAP's internal tick,\n        // detected via stack trace inspection in the rAF wrapper\n        const _tick = (): void => {\n          (window as unknown as Record<string, number>).__RAF_TICK_COUNT__++;\n          window.requestAnimationFrame(_tick);\n        };\n        window.requestAnimationFrame(_tick);\n      });\n\n      await page.waitForTimeout(200);\n      await activateViaApi(page);\n      await page.waitForTimeout(200);\n      await deactivateViaApi(page);\n      await page.waitForTimeout(100);\n\n      const tickCountAfterUnfreeze = await page.evaluate(\n        () => (window as unknown as Record<string, number>).__RAF_TICK_COUNT__,\n      );\n      await page.waitForTimeout(300);\n      const tickCountLater = await page.evaluate(\n        () => (window as unknown as Record<string, number>).__RAF_TICK_COUNT__,\n      );\n\n      expect(tickCountLater).toBeGreaterThan(tickCountAfterUnfreeze);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/freeze-updates.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\ntest.describe(\"Freeze Updates\", () => {\n  test.describe(\"State Freezing During Prompt Mode\", () => {\n    test(\"should freeze React state updates when in prompt mode\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent({ delay: 2000 });\n\n      const getElementCount = async () => {\n        return reactGrab.page.evaluate(() => {\n          return document.querySelectorAll(\"[data-testid^='dynamic-element-']\")\n            .length;\n        });\n      };\n\n      const initialCount = await getElementCount();\n      expect(initialCount).toBeGreaterThan(0);\n\n      await reactGrab.enterPromptMode(\"[data-testid='dynamic-element-1']\");\n\n      await reactGrab.page.evaluate(() => {\n        const addButton = document.querySelector(\n          \"[data-testid='add-element-button']\",\n        ) as HTMLButtonElement;\n        addButton?.click();\n      });\n      await reactGrab.page.waitForTimeout(100);\n\n      const countDuringPromptMode = await getElementCount();\n      expect(countDuringPromptMode).toBe(initialCount);\n\n      await reactGrab.pressEscape();\n      await reactGrab.page.waitForTimeout(200);\n\n      const countAfterExit = await getElementCount();\n      expect(countAfterExit).toBe(initialCount);\n    });\n\n    test(\"should freeze visibility toggle during prompt mode\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent({ delay: 2000 });\n\n      const isToggleableVisible = async () => {\n        return reactGrab.page.evaluate(() => {\n          return (\n            document.querySelector(\"[data-testid='toggleable-element']\") !==\n            null\n          );\n        });\n      };\n\n      const initiallyVisible = await isToggleableVisible();\n      expect(initiallyVisible).toBe(true);\n\n      await reactGrab.enterPromptMode(\"[data-testid='toggleable-element']\");\n\n      await reactGrab.page.evaluate(() => {\n        const button = document.querySelector(\n          \"[data-testid='toggle-visibility-button']\",\n        ) as HTMLButtonElement;\n        button?.click();\n      });\n      await reactGrab.page.waitForTimeout(100);\n\n      const stillVisibleDuringPromptMode = await isToggleableVisible();\n      expect(stillVisibleDuringPromptMode).toBe(true);\n\n      await reactGrab.pressEscape();\n      await reactGrab.page.waitForTimeout(200);\n\n      const visibleAfterExit = await isToggleableVisible();\n      expect(visibleAfterExit).toBe(true);\n    });\n\n    test(\"should allow state updates after exiting prompt mode\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent({ delay: 100 });\n\n      const getElementCount = async () => {\n        return reactGrab.page.evaluate(() => {\n          return document.querySelectorAll(\"[data-testid^='dynamic-element-']\")\n            .length;\n        });\n      };\n\n      await reactGrab.enterPromptMode(\"[data-testid='dynamic-element-1']\");\n      await reactGrab.pressEscape();\n      await reactGrab.page.waitForTimeout(200);\n\n      const countBefore = await getElementCount();\n\n      await reactGrab.page.click(\"[data-testid='add-element-button']\");\n      await reactGrab.page.waitForTimeout(100);\n\n      const countAfter = await getElementCount();\n      expect(countAfter).toBe(countBefore + 1);\n    });\n  });\n\n  test.describe(\"Multiple Freeze/Unfreeze Cycles\", () => {\n    test(\"should handle multiple prompt mode cycles correctly\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent({ delay: 100 });\n\n      const getElementCount = async () => {\n        return reactGrab.page.evaluate(() => {\n          return document.querySelectorAll(\"[data-testid^='dynamic-element-']\")\n            .length;\n        });\n      };\n\n      for (let i = 0; i < 2; i++) {\n        const countBefore = await getElementCount();\n\n        await reactGrab.enterPromptMode(\"[data-testid='dynamic-element-1']\");\n        await reactGrab.pressEscape();\n        await reactGrab.page.waitForTimeout(500);\n\n        await reactGrab.page.click(\"[data-testid='add-element-button']\");\n        await reactGrab.page.waitForTimeout(300);\n\n        const countAfter = await getElementCount();\n        expect(countAfter).toBe(countBefore + 1);\n      }\n    });\n\n    test(\"should not leak frozen state after rapid activation cycles\", async ({\n      reactGrab,\n    }) => {\n      for (let i = 0; i < 5; i++) {\n        await reactGrab.activate();\n        await reactGrab.hoverElement(\"li:first-child\");\n        await reactGrab.page.waitForTimeout(50);\n        await reactGrab.deactivate();\n        await reactGrab.page.waitForTimeout(50);\n      }\n\n      const getElementCount = async () => {\n        return reactGrab.page.evaluate(() => {\n          return document.querySelectorAll(\"[data-testid^='dynamic-element-']\")\n            .length;\n        });\n      };\n\n      const countBefore = await getElementCount();\n\n      await reactGrab.page.click(\"[data-testid='add-element-button']\");\n      await reactGrab.page.waitForTimeout(100);\n\n      const countAfter = await getElementCount();\n      expect(countAfter).toBe(countBefore + 1);\n    });\n  });\n\n  test.describe(\"Freeze State Consistency\", () => {\n    test(\"should maintain UI consistency during prompt mode\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent({ delay: 2000 });\n\n      await reactGrab.enterPromptMode(\"[data-testid='dynamic-element-1']\");\n\n      const elementTextDuringFreeze = await reactGrab.page.evaluate(() => {\n        const element = document.querySelector(\n          \"[data-testid='dynamic-element-1']\",\n        );\n        return element?.textContent?.trim() ?? \"\";\n      });\n\n      expect(elementTextDuringFreeze).toContain(\"Dynamic Element 1\");\n\n      await reactGrab.pressEscape();\n    });\n\n    test(\"should unfreeze all components after exiting prompt mode\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent({ delay: 100 });\n\n      await reactGrab.enterPromptMode(\"[data-testid='test-input']\");\n      await reactGrab.pressEscape();\n      await reactGrab.page.waitForTimeout(200);\n\n      await reactGrab.page.fill(\"[data-testid='test-input']\", \"test value\");\n      const inputValue = await reactGrab.page.evaluate(() => {\n        const input = document.querySelector(\n          \"[data-testid='test-input']\",\n        ) as HTMLInputElement;\n        return input?.value ?? \"\";\n      });\n\n      expect(inputValue).toBe(\"test value\");\n    });\n\n    test(\"should resume updates after deactivation\", async ({ reactGrab }) => {\n      const getElementCount = async () => {\n        return reactGrab.page.evaluate(() => {\n          return document.querySelectorAll(\"[data-testid^='dynamic-element-']\")\n            .length;\n        });\n      };\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='dynamic-section']\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.deactivate();\n\n      const countBefore = await getElementCount();\n\n      await reactGrab.page.click(\"[data-testid='add-element-button']\");\n      await reactGrab.page.waitForTimeout(100);\n\n      const countAfter = await getElementCount();\n      expect(countAfter).toBe(countBefore + 1);\n    });\n  });\n\n  test.describe(\"Edge Cases\", () => {\n    test(\"should handle freeze when no React state is present\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent({ delay: 100 });\n\n      await reactGrab.enterPromptMode(\"[data-testid='main-title']\");\n\n      const isPromptMode = await reactGrab.isPromptModeActive();\n      expect(isPromptMode).toBe(true);\n\n      await reactGrab.pressEscape();\n    });\n\n    test(\"should handle deactivation during frozen state\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent({ delay: 2000 });\n\n      await reactGrab.enterPromptMode(\"[data-testid='dynamic-element-1']\");\n\n      await reactGrab.deactivate();\n      await reactGrab.page.waitForTimeout(200);\n\n      const getElementCount = async () => {\n        return reactGrab.page.evaluate(() => {\n          return document.querySelectorAll(\"[data-testid^='dynamic-element-']\")\n            .length;\n        });\n      };\n\n      const countBefore = await getElementCount();\n\n      await reactGrab.page.click(\"[data-testid='add-element-button']\");\n      await reactGrab.page.waitForTimeout(100);\n\n      const countAfter = await getElementCount();\n      expect(countAfter).toBe(countBefore + 1);\n    });\n\n    test(\"should properly cleanup after multiple freeze operations\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent({ delay: 100 });\n\n      for (let i = 0; i < 3; i++) {\n        await reactGrab.enterPromptMode(\"[data-testid='dynamic-element-1']\");\n        await reactGrab.deactivate();\n        // HACK: allow freeze cleanup to fully propagate before next iteration\n        await reactGrab.page.waitForTimeout(300);\n      }\n\n      const getElementCount = async () => {\n        return reactGrab.page.evaluate(() => {\n          return document.querySelectorAll(\"[data-testid^='dynamic-element-']\")\n            .length;\n        });\n      };\n\n      const countBefore = await getElementCount();\n\n      await reactGrab.page.click(\"[data-testid='add-element-button']\");\n      await reactGrab.page.waitForTimeout(100);\n\n      const countAfter = await getElementCount();\n      expect(countAfter).toBe(countBefore + 1);\n    });\n  });\n\n  test.describe(\"Button Click Buffering\", () => {\n    test(\"should buffer multiple clicks during freeze and apply on unfreeze\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent({ delay: 100 });\n\n      const getElementCount = async () => {\n        return reactGrab.page.evaluate(() => {\n          return document.querySelectorAll(\"[data-testid^='dynamic-element-']\")\n            .length;\n        });\n      };\n\n      const countBefore = await getElementCount();\n\n      await reactGrab.enterPromptMode(\"[data-testid='dynamic-element-1']\");\n\n      for (let clickIndex = 0; clickIndex < 3; clickIndex++) {\n        await reactGrab.page.evaluate(() => {\n          const addButton = document.querySelector(\n            \"[data-testid='add-element-button']\",\n          ) as HTMLButtonElement;\n          addButton?.click();\n        });\n        await reactGrab.page.waitForTimeout(50);\n      }\n\n      const countDuringFreeze = await getElementCount();\n      expect(countDuringFreeze).toBe(countBefore);\n\n      await reactGrab.pressEscape();\n      await reactGrab.page.waitForTimeout(300);\n\n      const countAfterUnfreeze = await getElementCount();\n      expect(countAfterUnfreeze).toBe(countBefore);\n    });\n\n    test(\"should not accumulate state incorrectly across freeze cycles\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent({ delay: 100 });\n\n      const getElementCount = async () => {\n        return reactGrab.page.evaluate(() => {\n          return document.querySelectorAll(\"[data-testid^='dynamic-element-']\")\n            .length;\n        });\n      };\n\n      const countBeforeFirstCycle = await getElementCount();\n\n      await reactGrab.enterPromptMode(\"[data-testid='dynamic-element-1']\");\n      await reactGrab.page.evaluate(() => {\n        const addButton = document.querySelector(\n          \"[data-testid='add-element-button']\",\n        ) as HTMLButtonElement;\n        addButton?.click();\n      });\n      await reactGrab.pressEscape();\n      await reactGrab.page.waitForTimeout(300);\n\n      const countAfterFirstCycle = await getElementCount();\n      expect(countAfterFirstCycle).toBe(countBeforeFirstCycle);\n\n      await reactGrab.enterPromptMode(\"[data-testid='dynamic-element-1']\");\n      await reactGrab.page.evaluate(() => {\n        const addButton = document.querySelector(\n          \"[data-testid='add-element-button']\",\n        ) as HTMLButtonElement;\n        addButton?.click();\n      });\n      await reactGrab.pressEscape();\n      await reactGrab.page.waitForTimeout(300);\n\n      const countAfterSecondCycle = await getElementCount();\n      expect(countAfterSecondCycle).toBe(countBeforeFirstCycle);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/history-items.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\nimport type { ReactGrabPageObject } from \"./fixtures.js\";\n\nconst copyElement = async (\n  reactGrab: ReactGrabPageObject,\n  selector: string,\n) => {\n  await reactGrab.activate();\n  await reactGrab.hoverElement(selector);\n  await reactGrab.waitForSelectionBox();\n  await reactGrab.clickElement(selector);\n  await expect\n    .poll(() => reactGrab.getClipboardContent(), { timeout: 5000 })\n    .toBeTruthy();\n  // HACK: Wait for copy feedback transition and history item addition\n  await reactGrab.page.waitForTimeout(300);\n};\n\ntest.describe(\"History Items\", () => {\n  test.describe(\"Toolbar History Button\", () => {\n    test(\"should not be visible before any elements are copied\", async ({\n      reactGrab,\n    }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      const isHistoryVisible = await reactGrab.isHistoryButtonVisible();\n      expect(isHistoryVisible).toBe(false);\n    });\n\n    test(\"should become visible after copying an element\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n\n      await expect\n        .poll(() => reactGrab.isHistoryButtonVisible(), { timeout: 2000 })\n        .toBe(true);\n    });\n\n    test(\"should show unread indicator after copy\", async ({ reactGrab }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n\n      await expect\n        .poll(() => reactGrab.hasUnreadHistoryIndicator(), { timeout: 2000 })\n        .toBe(true);\n    });\n\n    test(\"should clear unread indicator when dropdown is opened\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n\n      await expect\n        .poll(() => reactGrab.hasUnreadHistoryIndicator(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.clickHistoryButton();\n\n      await expect\n        .poll(() => reactGrab.hasUnreadHistoryIndicator(), { timeout: 2000 })\n        .toBe(false);\n    });\n\n    test(\"should show unread indicator again after new copy while dropdown is closed\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await reactGrab.clickHistoryButton();\n      await reactGrab.clickHistoryButton();\n\n      await expect\n        .poll(() => reactGrab.hasUnreadHistoryIndicator(), { timeout: 2000 })\n        .toBe(false);\n\n      await copyElement(reactGrab, \"li:last-child\");\n\n      await expect\n        .poll(() => reactGrab.hasUnreadHistoryIndicator(), { timeout: 2000 })\n        .toBe(true);\n    });\n  });\n\n  test.describe(\"Dropdown Open/Close\", () => {\n    test(\"should open when clicking the history button\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await reactGrab.clickHistoryButton();\n\n      const isDropdownVisible = await reactGrab.isHistoryDropdownVisible();\n      expect(isDropdownVisible).toBe(true);\n    });\n\n    test(\"should close when clicking the history button again\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await reactGrab.clickHistoryButton();\n\n      expect(await reactGrab.isHistoryDropdownVisible()).toBe(true);\n\n      await reactGrab.clickHistoryButton();\n\n      expect(await reactGrab.isHistoryDropdownVisible()).toBe(false);\n    });\n\n    test(\"should close when pressing Escape\", async ({ reactGrab }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await reactGrab.clickHistoryButton();\n\n      expect(await reactGrab.isHistoryDropdownVisible()).toBe(true);\n\n      await reactGrab.pressEscape();\n      await reactGrab.page.waitForTimeout(100);\n\n      expect(await reactGrab.isHistoryDropdownVisible()).toBe(false);\n    });\n\n    test(\"should close when context menu is opened\", async ({ reactGrab }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await reactGrab.clickHistoryButton();\n\n      expect(await reactGrab.isHistoryDropdownVisible()).toBe(true);\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li:first-child\");\n\n      await expect\n        .poll(() => reactGrab.isHistoryDropdownVisible(), { timeout: 2000 })\n        .toBe(false);\n      expect(await reactGrab.isContextMenuVisible()).toBe(true);\n    });\n  });\n\n  test.describe(\"Dropdown Content\", () => {\n    test(\"should display one item after copying an element\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await reactGrab.clickHistoryButton();\n\n      const dropdownInfo = await reactGrab.getHistoryDropdownInfo();\n      expect(dropdownInfo.isVisible).toBe(true);\n      expect(dropdownInfo.itemCount).toBe(1);\n    });\n\n    test(\"should display multiple items after copying different elements\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await copyElement(reactGrab, \"li:last-child\");\n\n      await reactGrab.clickHistoryButton();\n\n      const dropdownInfo = await reactGrab.getHistoryDropdownInfo();\n      expect(dropdownInfo.itemCount).toBe(2);\n    });\n\n    test(\"should hide history button after clearing all items\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await reactGrab.clickHistoryButton();\n      await reactGrab.clickHistoryClear();\n\n      await expect\n        .poll(() => reactGrab.isHistoryButtonVisible(), { timeout: 2000 })\n        .toBe(false);\n\n      expect(await reactGrab.isHistoryDropdownVisible()).toBe(false);\n    });\n  });\n\n  test.describe(\"Item Selection\", () => {\n    test(\"should copy content to clipboard when clicking a regular item\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n\n      const originalClipboard = await reactGrab.getClipboardContent();\n      expect(originalClipboard).toBeTruthy();\n\n      await reactGrab.page.evaluate(() => navigator.clipboard.writeText(\"\"));\n\n      await reactGrab.clickHistoryButton();\n      await reactGrab.clickHistoryItem(0);\n\n      await expect\n        .poll(() => reactGrab.getClipboardContent(), { timeout: 3000 })\n        .toBeTruthy();\n\n      const newClipboard = await reactGrab.getClipboardContent();\n      expect(newClipboard).toBe(originalClipboard);\n    });\n\n    test(\"should keep the dropdown open after selecting an item\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await reactGrab.clickHistoryButton();\n\n      expect(await reactGrab.isHistoryDropdownVisible()).toBe(true);\n\n      await reactGrab.clickHistoryItem(0);\n\n      expect(await reactGrab.isHistoryDropdownVisible()).toBe(true);\n    });\n  });\n\n  test.describe(\"Copy All\", () => {\n    test(\"should copy combined content of all items to clipboard\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await copyElement(reactGrab, \"li:last-child\");\n\n      await reactGrab.page.evaluate(() => navigator.clipboard.writeText(\"\"));\n\n      await reactGrab.clickHistoryButton();\n      await reactGrab.clickHistoryCopyAll();\n\n      const clipboardContent = await reactGrab.getClipboardContent();\n      expect(clipboardContent).toContain(\"[1]\");\n      expect(clipboardContent).toContain(\"[2]\");\n    });\n\n    test(\"should keep the dropdown open after copy all\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await reactGrab.clickHistoryButton();\n\n      expect(await reactGrab.isHistoryDropdownVisible()).toBe(true);\n\n      await reactGrab.clickHistoryCopyAll();\n\n      expect(await reactGrab.isHistoryDropdownVisible()).toBe(true);\n    });\n\n    test(\"should not trigger copy all via Enter key\", async ({ reactGrab }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n\n      await reactGrab.page.evaluate(() => navigator.clipboard.writeText(\"\"));\n\n      await reactGrab.clickHistoryButton();\n      await reactGrab.pressEnter();\n      await reactGrab.page.waitForTimeout(200);\n\n      const clipboardContent = await reactGrab.getClipboardContent();\n      expect(clipboardContent).toBe(\"\");\n    });\n  });\n\n  test.describe(\"Clear All\", () => {\n    test(\"should remove all history items\", async ({ reactGrab }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await copyElement(reactGrab, \"li:last-child\");\n\n      await reactGrab.clickHistoryButton();\n      expect((await reactGrab.getHistoryDropdownInfo()).itemCount).toBe(2);\n\n      await reactGrab.clickHistoryClear();\n\n      await expect\n        .poll(() => reactGrab.isHistoryButtonVisible(), { timeout: 2000 })\n        .toBe(false);\n    });\n\n    test(\"should hide the history button in toolbar after clearing\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n\n      await expect\n        .poll(() => reactGrab.isHistoryButtonVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.clickHistoryButton();\n      await reactGrab.clickHistoryClear();\n\n      await expect\n        .poll(() => reactGrab.isHistoryButtonVisible(), { timeout: 2000 })\n        .toBe(false);\n    });\n\n    test(\"should close the dropdown after clearing\", async ({ reactGrab }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await reactGrab.clickHistoryButton();\n\n      expect(await reactGrab.isHistoryDropdownVisible()).toBe(true);\n\n      await reactGrab.clickHistoryClear();\n\n      expect(await reactGrab.isHistoryDropdownVisible()).toBe(false);\n    });\n  });\n\n  test.describe(\"Deduplication\", () => {\n    test(\"should deduplicate when copying the same element twice\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await copyElement(reactGrab, \"li:first-child\");\n\n      await reactGrab.clickHistoryButton();\n\n      const dropdownInfo = await reactGrab.getHistoryDropdownInfo();\n      expect(dropdownInfo.itemCount).toBe(1);\n    });\n\n    test(\"should not deduplicate when copying different elements\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await copyElement(reactGrab, \"li:last-child\");\n\n      await reactGrab.clickHistoryButton();\n\n      const dropdownInfo = await reactGrab.getHistoryDropdownInfo();\n      expect(dropdownInfo.itemCount).toBe(2);\n    });\n  });\n\n  test.describe(\"Hover Behavior\", () => {\n    test(\"should show a highlight box on the element when hovering a history item\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await reactGrab.clickHistoryButton();\n\n      const grabbedBoxesBefore = await reactGrab.getGrabbedBoxInfo();\n      const initialBoxCount = grabbedBoxesBefore.count;\n\n      await reactGrab.hoverHistoryItem(0);\n\n      await expect\n        .poll(\n          async () => {\n            const info = await reactGrab.getGrabbedBoxInfo();\n            return info.count;\n          },\n          { timeout: 2000 },\n        )\n        .toBeGreaterThan(initialBoxCount);\n    });\n\n    test(\"should remove highlight box when mouse leaves a history item\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await reactGrab.clickHistoryButton();\n\n      await reactGrab.hoverHistoryItem(0);\n      await expect\n        .poll(\n          async () => {\n            const info = await reactGrab.getGrabbedBoxInfo();\n            return info.count;\n          },\n          { timeout: 2000 },\n        )\n        .toBeGreaterThan(0);\n\n      await reactGrab.page.mouse.move(0, 0);\n      await reactGrab.page.waitForTimeout(200);\n\n      const grabbedBoxesAfter = await reactGrab.getGrabbedBoxInfo();\n      const hasHistoryHoverBox = grabbedBoxesAfter.boxes.some((box) =>\n        box.id.startsWith(\"history-hover-\"),\n      );\n      expect(hasHistoryHoverBox).toBe(false);\n    });\n  });\n\n  test.describe(\"History Button Hover Preview\", () => {\n    test(\"should show highlight boxes for all history items when hovering the history button\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await copyElement(reactGrab, \"li:last-child\");\n\n      const grabbedBoxesBefore = await reactGrab.getGrabbedBoxInfo();\n      const initialBoxCount = grabbedBoxesBefore.count;\n\n      await reactGrab.hoverHistoryButton();\n\n      await expect\n        .poll(\n          async () => {\n            const info = await reactGrab.getGrabbedBoxInfo();\n            return info.count;\n          },\n          { timeout: 2000 },\n        )\n        .toBeGreaterThanOrEqual(initialBoxCount + 2);\n\n      const grabbedBoxes = await reactGrab.getGrabbedBoxInfo();\n      const allHoverBoxes = grabbedBoxes.boxes.filter((box) =>\n        box.id.startsWith(\"history-all-hover-\"),\n      );\n      expect(allHoverBoxes.length).toBe(2);\n    });\n\n    test(\"should remove all highlight boxes when mouse leaves the history button\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await copyElement(reactGrab, \"li:last-child\");\n\n      await reactGrab.hoverHistoryButton();\n\n      await expect\n        .poll(\n          async () => {\n            const info = await reactGrab.getGrabbedBoxInfo();\n            return info.boxes.filter((box) =>\n              box.id.startsWith(\"history-all-hover-\"),\n            ).length;\n          },\n          { timeout: 2000 },\n        )\n        .toBe(2);\n\n      await reactGrab.page.mouse.move(0, 0);\n      await reactGrab.page.waitForTimeout(200);\n\n      const grabbedBoxesAfter = await reactGrab.getGrabbedBoxInfo();\n      const remainingHoverBoxes = grabbedBoxesAfter.boxes.filter((box) =>\n        box.id.startsWith(\"history-all-hover-\"),\n      );\n      expect(remainingHoverBoxes.length).toBe(0);\n    });\n\n    test(\"should clear button hover boxes when pinning the dropdown\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n\n      await reactGrab.hoverHistoryButton();\n\n      await expect\n        .poll(\n          async () => {\n            const info = await reactGrab.getGrabbedBoxInfo();\n            return info.boxes.filter((box) =>\n              box.id.startsWith(\"history-all-hover-\"),\n            ).length;\n          },\n          { timeout: 2000 },\n        )\n        .toBe(1);\n\n      await reactGrab.page.evaluate((attrName) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        if (!root) return;\n        root\n          .querySelector<HTMLButtonElement>(\"[data-react-grab-toolbar-history]\")\n          ?.click();\n      }, \"data-react-grab\");\n      await reactGrab.page.waitForTimeout(200);\n\n      const grabbedBoxesAfter = await reactGrab.getGrabbedBoxInfo();\n      const remainingHoverBoxes = grabbedBoxesAfter.boxes.filter((box) =>\n        box.id.startsWith(\"history-all-hover-\"),\n      );\n      expect(remainingHoverBoxes.length).toBe(0);\n    });\n\n    test(\"should show highlight box for a single history item\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n\n      await reactGrab.hoverHistoryButton();\n\n      await expect\n        .poll(\n          async () => {\n            const info = await reactGrab.getGrabbedBoxInfo();\n            return info.boxes.filter((box) =>\n              box.id.startsWith(\"history-all-hover-\"),\n            ).length;\n          },\n          { timeout: 2000 },\n        )\n        .toBe(1);\n    });\n  });\n\n  test.describe(\"Remove Individual Item\", () => {\n    test(\"should remove a single item and keep others\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await copyElement(reactGrab, \"li:last-child\");\n\n      await reactGrab.clickHistoryButton();\n      expect((await reactGrab.getHistoryDropdownInfo()).itemCount).toBe(2);\n\n      await reactGrab.clickHistoryItemRemove(0);\n      await reactGrab.page.waitForTimeout(200);\n\n      const dropdownInfo = await reactGrab.getHistoryDropdownInfo();\n      expect(dropdownInfo.itemCount).toBe(1);\n    });\n\n    test(\"should keep the dropdown open after removing an item\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await copyElement(reactGrab, \"li:last-child\");\n\n      await reactGrab.clickHistoryButton();\n      await reactGrab.clickHistoryItemRemove(0);\n      await reactGrab.page.waitForTimeout(200);\n\n      expect(await reactGrab.isHistoryDropdownVisible()).toBe(true);\n    });\n\n    test(\"should close the dropdown and hide button when removing the last item\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n\n      await reactGrab.clickHistoryButton();\n      expect((await reactGrab.getHistoryDropdownInfo()).itemCount).toBe(1);\n\n      await reactGrab.clickHistoryItemRemove(0);\n      await reactGrab.page.waitForTimeout(200);\n\n      expect(await reactGrab.isHistoryDropdownVisible()).toBe(false);\n\n      await expect\n        .poll(() => reactGrab.isHistoryButtonVisible(), { timeout: 2000 })\n        .toBe(false);\n    });\n  });\n\n  test.describe(\"Copy Individual Item\", () => {\n    test(\"should copy the item content to clipboard\", async ({ reactGrab }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n\n      const originalClipboard = await reactGrab.getClipboardContent();\n\n      await reactGrab.page.evaluate(() => navigator.clipboard.writeText(\"\"));\n\n      await reactGrab.clickHistoryButton();\n      await reactGrab.clickHistoryItemCopy(0);\n      await reactGrab.page.waitForTimeout(200);\n\n      const newClipboard = await reactGrab.getClipboardContent();\n      expect(newClipboard).toBe(originalClipboard);\n    });\n\n    test(\"should keep the dropdown open after copying an item\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n\n      await reactGrab.clickHistoryButton();\n      await reactGrab.clickHistoryItemCopy(0);\n      await reactGrab.page.waitForTimeout(200);\n\n      expect(await reactGrab.isHistoryDropdownVisible()).toBe(true);\n    });\n\n    test(\"should keep the dropdown open after clicking a row to copy\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n\n      await reactGrab.clickHistoryButton();\n      await reactGrab.clickHistoryItem(0);\n\n      expect(await reactGrab.isHistoryDropdownVisible()).toBe(true);\n    });\n  });\n\n  test.describe(\"Dropdown Positioning\", () => {\n    test(\"should position the dropdown within the viewport\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await reactGrab.clickHistoryButton();\n\n      await expect\n        .poll(\n          async () => {\n            const position = await reactGrab.getHistoryDropdownPosition();\n            return position?.left ?? -9999;\n          },\n          { timeout: 3000 },\n        )\n        .toBeGreaterThanOrEqual(0);\n\n      const position = await reactGrab.getHistoryDropdownPosition();\n      expect(position).not.toBeNull();\n      expect(position!.top).toBeGreaterThanOrEqual(0);\n    });\n\n    test(\"should reposition when toolbar is dragged to top edge\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n\n      await reactGrab.dragToolbar(0, -600);\n\n      await expect\n        .poll(\n          async () => {\n            const info = await reactGrab.getToolbarInfo();\n            return info.snapEdge;\n          },\n          { timeout: 5000 },\n        )\n        .toBe(\"top\");\n\n      // HACK: wait for snap animation and toolbar layout transition to fully settle\n      await reactGrab.page.waitForTimeout(500);\n\n      await expect\n        .poll(() => reactGrab.isHistoryButtonVisible(), { timeout: 5000 })\n        .toBe(true);\n\n      const historyButtonRect = await reactGrab.page.evaluate((attrName) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return null;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        if (!root) return null;\n        const button = root.querySelector<HTMLElement>(\n          \"[data-react-grab-toolbar-history]\",\n        );\n        if (!button) return null;\n        const rect = button.getBoundingClientRect();\n        return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };\n      }, \"data-react-grab\");\n\n      expect(historyButtonRect).not.toBeNull();\n      await reactGrab.page.mouse.click(\n        historyButtonRect!.x + historyButtonRect!.width / 2,\n        historyButtonRect!.y + historyButtonRect!.height / 2,\n      );\n\n      await expect\n        .poll(() => reactGrab.isHistoryDropdownVisible(), { timeout: 5000 })\n        .toBe(true);\n\n      await expect\n        .poll(\n          async () => {\n            const position = await reactGrab.getHistoryDropdownPosition();\n            return position?.top ?? -9999;\n          },\n          { timeout: 5000 },\n        )\n        .toBeGreaterThanOrEqual(0);\n    });\n  });\n\n  test.describe(\"Persistence Across Copies\", () => {\n    test(\"should accumulate items across multiple copy operations\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await copyElement(reactGrab, '[data-testid=\"card-title\"]');\n      await copyElement(reactGrab, '[data-testid=\"submit-button\"]');\n\n      await reactGrab.clickHistoryButton();\n\n      const dropdownInfo = await reactGrab.getHistoryDropdownInfo();\n      expect(dropdownInfo.itemCount).toBe(3);\n    });\n\n    test(\"should maintain history items after activation cycle\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n\n      await reactGrab.activate();\n      await reactGrab.deactivate();\n      await reactGrab.page.waitForTimeout(200);\n\n      await expect\n        .poll(() => reactGrab.isHistoryButtonVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.clickHistoryButton();\n\n      const dropdownInfo = await reactGrab.getHistoryDropdownInfo();\n      expect(dropdownInfo.itemCount).toBe(1);\n    });\n  });\n\n  test.describe(\"Dismiss Behavior\", () => {\n    test(\"should not dismiss when clicking outside the dropdown\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await reactGrab.clickHistoryButton();\n\n      expect(await reactGrab.isHistoryDropdownVisible()).toBe(true);\n\n      await reactGrab.page.mouse.click(10, 10);\n      await reactGrab.page.waitForTimeout(200);\n\n      expect(await reactGrab.isHistoryDropdownVisible()).toBe(true);\n    });\n\n    test(\"should dismiss when pressing Escape\", async ({ reactGrab }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await reactGrab.clickHistoryButton();\n\n      expect(await reactGrab.isHistoryDropdownVisible()).toBe(true);\n\n      await reactGrab.pressEscape();\n      await reactGrab.page.waitForTimeout(200);\n\n      expect(await reactGrab.isHistoryDropdownVisible()).toBe(false);\n    });\n\n    test(\"should dismiss when clicking the history button to toggle off\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await reactGrab.clickHistoryButton();\n\n      expect(await reactGrab.isHistoryDropdownVisible()).toBe(true);\n\n      await reactGrab.clickHistoryButton();\n\n      expect(await reactGrab.isHistoryDropdownVisible()).toBe(false);\n    });\n  });\n\n  test.describe(\"Hover to Open\", () => {\n    test(\"should open dropdown when hovering the history button\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n\n      await reactGrab.hoverHistoryButton();\n\n      await expect\n        .poll(() => reactGrab.isHistoryDropdownVisible(), { timeout: 2000 })\n        .toBe(true);\n    });\n\n    test(\"should show all preview boxes when hovering the history button\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await copyElement(reactGrab, \"li:last-child\");\n\n      await reactGrab.hoverHistoryButton();\n\n      await expect\n        .poll(\n          async () => {\n            const info = await reactGrab.getGrabbedBoxInfo();\n            return info.boxes.filter((box) =>\n              box.id.startsWith(\"history-all-hover-\"),\n            ).length;\n          },\n          { timeout: 2000 },\n        )\n        .toBe(2);\n    });\n\n    test(\"should pin dropdown open when clicking the history button while hover-opened\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n\n      await reactGrab.hoverHistoryButton();\n\n      await expect\n        .poll(() => reactGrab.isHistoryDropdownVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.page.evaluate((attrName) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        if (!root) return;\n        root\n          .querySelector<HTMLButtonElement>(\"[data-react-grab-toolbar-history]\")\n          ?.click();\n      }, \"data-react-grab\");\n      await reactGrab.page.waitForTimeout(300);\n\n      await reactGrab.page.mouse.move(0, 0);\n      await reactGrab.page.waitForTimeout(500);\n\n      expect(await reactGrab.isHistoryDropdownVisible()).toBe(true);\n    });\n  });\n\n  test.describe(\"Preview Suppression After Copy\", () => {\n    test(\"should clear hover preview boxes after copying via row click\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await reactGrab.clickHistoryButton();\n\n      await reactGrab.clickHistoryItem(0);\n      await reactGrab.page.waitForTimeout(300);\n\n      const grabbedBoxes = await reactGrab.getGrabbedBoxInfo();\n      const hoverBoxCount = grabbedBoxes.boxes.filter(\n        (box) =>\n          box.id.startsWith(\"history-hover-\") ||\n          box.id.startsWith(\"history-all-hover-\"),\n      ).length;\n      expect(hoverBoxCount).toBe(0);\n    });\n\n    test(\"should clear all hover preview boxes after copy all\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await copyElement(reactGrab, \"li:last-child\");\n\n      await reactGrab.clickHistoryButton();\n      await reactGrab.page.waitForTimeout(200);\n\n      await reactGrab.clickHistoryCopyAll();\n      await reactGrab.page.waitForTimeout(300);\n\n      const grabbedBoxes = await reactGrab.getGrabbedBoxInfo();\n      const allHoverBoxes = grabbedBoxes.boxes.filter(\n        (box) =>\n          box.id.startsWith(\"history-all-hover-\") ||\n          box.id.startsWith(\"history-hover-\"),\n      );\n      expect(allHoverBoxes.length).toBe(0);\n    });\n\n    test(\"should suppress all-item previews during feedback but allow different item hover\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await copyElement(reactGrab, \"li:last-child\");\n\n      await reactGrab.clickHistoryButton();\n      await reactGrab.clickHistoryItemCopy(0);\n      await reactGrab.page.waitForTimeout(200);\n\n      await reactGrab.hoverHistoryItem(1);\n\n      await expect\n        .poll(\n          async () => {\n            const info = await reactGrab.getGrabbedBoxInfo();\n            return info.boxes.filter((box) =>\n              box.id.startsWith(\"history-hover-\"),\n            ).length;\n          },\n          { timeout: 2000 },\n        )\n        .toBeGreaterThan(0);\n    });\n  });\n\n  test.describe(\"Selection Label Lifecycle on Copy\", () => {\n    test(\"should show selection label when hovering a history item\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await reactGrab.clickHistoryButton();\n      await reactGrab.page.waitForTimeout(200);\n\n      await reactGrab.hoverHistoryItem(0);\n\n      await expect\n        .poll(\n          async () => {\n            const labels = await reactGrab.getLabelInstancesInfo();\n            return labels.filter(\n              (label) => label.status === \"idle\" && label.createdAt === 0,\n            ).length;\n          },\n          { timeout: 5000 },\n        )\n        .toBeGreaterThan(0);\n    });\n\n    test(\"should clear idle labels and show copied label after copy all\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await copyElement(reactGrab, \"li:last-child\");\n\n      await reactGrab.clickHistoryButton();\n      await reactGrab.page.waitForTimeout(200);\n\n      await reactGrab.hoverCopyAllButton();\n      await expect\n        .poll(\n          async () => {\n            const labels = await reactGrab.getLabelInstancesInfo();\n            return labels.filter(\n              (label) => label.status === \"idle\" && label.createdAt === 0,\n            ).length;\n          },\n          { timeout: 5000 },\n        )\n        .toBeGreaterThanOrEqual(2);\n\n      await reactGrab.clickHistoryCopyAll();\n\n      await expect\n        .poll(\n          async () => {\n            const labels = await reactGrab.getLabelInstancesInfo();\n            const idlePreviewLabels = labels.filter(\n              (label) => label.status === \"idle\" && label.createdAt === 0,\n            );\n            return idlePreviewLabels.length;\n          },\n          { timeout: 5000 },\n        )\n        .toBe(0);\n\n      await expect\n        .poll(\n          async () => {\n            const labels = await reactGrab.getLabelInstancesInfo();\n            return labels.filter((label) => label.status === \"copied\").length;\n          },\n          { timeout: 5000 },\n        )\n        .toBeGreaterThanOrEqual(1);\n    });\n\n    test(\"should clear idle labels and show copied label after individual copy\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await reactGrab.clickHistoryButton();\n      await reactGrab.page.waitForTimeout(200);\n\n      await reactGrab.hoverHistoryItem(0);\n      await expect\n        .poll(\n          async () => {\n            const labels = await reactGrab.getLabelInstancesInfo();\n            return labels.filter(\n              (label) => label.status === \"idle\" && label.createdAt === 0,\n            ).length;\n          },\n          { timeout: 5000 },\n        )\n        .toBeGreaterThan(0);\n\n      await reactGrab.clickHistoryItem(0);\n\n      await expect\n        .poll(\n          async () => {\n            const labels = await reactGrab.getLabelInstancesInfo();\n            const idlePreviewLabels = labels.filter(\n              (label) => label.status === \"idle\" && label.createdAt === 0,\n            );\n            return idlePreviewLabels.length;\n          },\n          { timeout: 5000 },\n        )\n        .toBe(0);\n\n      await expect\n        .poll(\n          async () => {\n            const labels = await reactGrab.getLabelInstancesInfo();\n            return labels.filter((label) => label.status === \"copied\").length;\n          },\n          { timeout: 5000 },\n        )\n        .toBeGreaterThanOrEqual(1);\n    });\n\n    test(\"should clear idle labels and show copied label after copy button click\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await reactGrab.clickHistoryButton();\n      await reactGrab.page.waitForTimeout(200);\n\n      await reactGrab.hoverHistoryItem(0);\n      await expect\n        .poll(\n          async () => {\n            const labels = await reactGrab.getLabelInstancesInfo();\n            return labels.filter(\n              (label) => label.status === \"idle\" && label.createdAt === 0,\n            ).length;\n          },\n          { timeout: 5000 },\n        )\n        .toBeGreaterThan(0);\n\n      await reactGrab.clickHistoryItemCopy(0);\n\n      await expect\n        .poll(\n          async () => {\n            const labels = await reactGrab.getLabelInstancesInfo();\n            const idlePreviewLabels = labels.filter(\n              (label) => label.status === \"idle\" && label.createdAt === 0,\n            );\n            return idlePreviewLabels.length;\n          },\n          { timeout: 5000 },\n        )\n        .toBe(0);\n\n      await expect\n        .poll(\n          async () => {\n            const labels = await reactGrab.getLabelInstancesInfo();\n            return labels.filter((label) => label.status === \"copied\").length;\n          },\n          { timeout: 5000 },\n        )\n        .toBeGreaterThanOrEqual(1);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/history-reacquire.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\nimport type { ReactGrabPageObject } from \"./fixtures.js\";\n\ninterface ViewportRect {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n}\n\nconst getViewportRect = async (\n  reactGrab: ReactGrabPageObject,\n  selector: string,\n): Promise<ViewportRect | null> => {\n  return reactGrab.page.evaluate((sel) => {\n    const element = document.querySelector(sel);\n    if (!element) return null;\n    const rect = element.getBoundingClientRect();\n    return {\n      x: rect.left,\n      y: rect.top,\n      width: rect.width,\n      height: rect.height,\n    };\n  }, selector);\n};\n\nconst setHiddenToggleSectionMarginTopPx = async (\n  reactGrab: ReactGrabPageObject,\n  marginTopPx: number,\n) => {\n  await reactGrab.page.evaluate((marginTop) => {\n    const section = document.querySelector(\n      '[data-testid=\"hidden-toggle-section\"]',\n    );\n    if (section instanceof HTMLElement) {\n      section.style.marginTop = `${marginTop}px`;\n    }\n  }, marginTopPx);\n};\n\nconst toggleToggleableElement = async (reactGrab: ReactGrabPageObject) => {\n  await reactGrab.page\n    .locator('[data-testid=\"toggle-visibility-button\"]')\n    .click({ force: true });\n};\n\nconst copyElementViaApi = async (\n  reactGrab: ReactGrabPageObject,\n  selector: string,\n) => {\n  await reactGrab.page.evaluate(() => navigator.clipboard.writeText(\"\"));\n  const didCopy = await reactGrab.copyElementViaApi(selector);\n  expect(didCopy).toBe(true);\n  await expect\n    .poll(() => reactGrab.getClipboardContent(), { timeout: 5000 })\n    .toBeTruthy();\n  // HACK: allow history item to be persisted + mapped\n  await reactGrab.page.waitForTimeout(300);\n};\n\nconst expectCloseTo = (\n  actual: number,\n  expected: number,\n  tolerancePx: number,\n) => {\n  expect(Math.abs(actual - expected)).toBeLessThanOrEqual(tolerancePx);\n};\n\ntest.describe(\"History selector reacquire\", () => {\n  test(\"should reacquire a remounted element and update hover preview bounds\", async ({\n    reactGrab,\n  }) => {\n    const toggleableSelector = '[data-testid=\"toggleable-element\"]';\n\n    await reactGrab.page\n      .locator('[data-testid=\"hidden-toggle-section\"]')\n      .scrollIntoViewIfNeeded();\n\n    const beforeRect = await getViewportRect(reactGrab, toggleableSelector);\n    expect(beforeRect).not.toBeNull();\n\n    await copyElementViaApi(reactGrab, toggleableSelector);\n\n    await expect\n      .poll(() => reactGrab.isHistoryButtonVisible(), { timeout: 2000 })\n      .toBe(true);\n\n    await toggleToggleableElement(reactGrab);\n    await expect(reactGrab.page.locator(toggleableSelector)).toHaveCount(0);\n\n    await setHiddenToggleSectionMarginTopPx(reactGrab, 240);\n\n    await toggleToggleableElement(reactGrab);\n    await expect(reactGrab.page.locator(toggleableSelector)).toHaveCount(1);\n    await reactGrab.page.locator(toggleableSelector).scrollIntoViewIfNeeded();\n\n    const afterRect = await getViewportRect(reactGrab, toggleableSelector);\n    expect(afterRect).not.toBeNull();\n    expect(Math.abs(afterRect!.y - beforeRect!.y)).toBeGreaterThan(40);\n\n    await reactGrab.clickHistoryButton();\n    expect((await reactGrab.getHistoryDropdownInfo()).itemCount).toBe(1);\n\n    await reactGrab.hoverHistoryItem(0);\n\n    await expect\n      .poll(async () => {\n        const info = await reactGrab.getGrabbedBoxInfo();\n        return info.boxes.filter((box) => box.id.startsWith(\"history-hover-\"))\n          .length;\n      })\n      .toBeGreaterThan(0);\n\n    const grabbedBoxes = await reactGrab.getGrabbedBoxInfo();\n    const hoverBox = grabbedBoxes.boxes.find((box) =>\n      box.id.startsWith(\"history-hover-\"),\n    );\n    expect(hoverBox).toBeTruthy();\n\n    expectCloseTo(hoverBox!.bounds.x, afterRect!.x, 8);\n    expectCloseTo(hoverBox!.bounds.y, afterRect!.y, 8);\n    expectCloseTo(hoverBox!.bounds.width, afterRect!.width, 8);\n    expectCloseTo(hoverBox!.bounds.height, afterRect!.height, 8);\n  });\n\n  test(\"should show copied label feedback when selecting a reacquired history item\", async ({\n    reactGrab,\n  }) => {\n    const toggleableSelector = '[data-testid=\"toggleable-element\"]';\n\n    await reactGrab.page\n      .locator('[data-testid=\"hidden-toggle-section\"]')\n      .scrollIntoViewIfNeeded();\n\n    await copyElementViaApi(reactGrab, toggleableSelector);\n\n    await toggleToggleableElement(reactGrab);\n    await expect(reactGrab.page.locator(toggleableSelector)).toHaveCount(0);\n\n    await setHiddenToggleSectionMarginTopPx(reactGrab, 220);\n\n    await toggleToggleableElement(reactGrab);\n    await expect(reactGrab.page.locator(toggleableSelector)).toHaveCount(1);\n\n    await reactGrab.clickHistoryButton();\n    await reactGrab.clickHistoryItem(0);\n\n    await expect\n      .poll(async () => {\n        const labels = await reactGrab.getLabelInstancesInfo();\n        return labels.filter((label) => label.status === \"copied\").length;\n      })\n      .toBeGreaterThan(0);\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/hold-activation.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\ntest.describe(\"Hold Activation Mode\", () => {\n  test(\"should not activate when pressing C without Cmd/Ctrl modifier\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.page.click(\"body\");\n    await reactGrab.page.keyboard.down(\"c\");\n    await reactGrab.page.waitForTimeout(50);\n    await reactGrab.page.keyboard.up(\"c\");\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(false);\n  });\n\n  test(\"should allow multiple API activations in sequence\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n\n    let isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(true);\n\n    await reactGrab.pressEscape();\n    await reactGrab.page.waitForTimeout(100);\n\n    isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(false);\n\n    await reactGrab.activate();\n\n    isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"should allow selection after API activation\", async ({ reactGrab }) => {\n    await reactGrab.activate();\n\n    await reactGrab.hoverElement(\"li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"should allow dragging after API activation\", async ({ reactGrab }) => {\n    await reactGrab.activate();\n\n    const firstItem = reactGrab.page.locator(\"li\").first();\n    const firstBox = await firstItem.boundingBox();\n    if (!firstBox) throw new Error(\"Could not get bounding box\");\n\n    await reactGrab.page.mouse.move(firstBox.x - 10, firstBox.y - 10);\n    await reactGrab.page.mouse.down();\n    await reactGrab.page.mouse.move(firstBox.x + 100, firstBox.y + 100, {\n      steps: 5,\n    });\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(true);\n\n    await reactGrab.page.mouse.up();\n  });\n\n  test(\"should cancel hold when pressing a non-activation key during hold\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.page.click(\"body\");\n\n    await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n    await reactGrab.page.keyboard.down(\"c\");\n    await reactGrab.page.waitForTimeout(50);\n\n    await reactGrab.page.keyboard.down(\"a\");\n    await reactGrab.page.keyboard.up(\"c\");\n\n    await reactGrab.page.waitForTimeout(500);\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(false);\n\n    await reactGrab.page.keyboard.up(\"a\");\n    await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n  });\n\n  test(\"should copy heading element after API activation\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n\n    await reactGrab.hoverElement(\"[data-testid='main-title']\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.clickElement(\"[data-testid='main-title']\");\n    await reactGrab.page.waitForTimeout(500);\n\n    const clipboardContent = await reactGrab.getClipboardContent();\n    expect(clipboardContent).toContain(\"React Grab\");\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/input-mode.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\ntest.describe(\"Input Mode\", () => {\n  test.describe(\"Entering Input Mode\", () => {\n    test(\"context menu edit should enter input mode when agent is configured\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.rightClickElement(\"li:first-child\");\n      await reactGrab.clickContextMenuItem(\"Edit\");\n\n      await expect.poll(() => reactGrab.isPromptModeActive()).toBe(true);\n    });\n\n    test(\"single click should copy without entering input mode when no agent\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.clickElement(\"li:first-child\");\n\n      await expect.poll(() => reactGrab.getClipboardContent()).toBeTruthy();\n    });\n\n    test(\"should focus input textarea when entering input mode\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      const isFocused = await reactGrab.page.evaluate((attrName) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return false;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        if (!root) return false;\n        const textarea = root.querySelector(\"textarea\");\n        return (\n          document.activeElement === textarea ||\n          shadowRoot.activeElement === textarea\n        );\n      }, \"data-react-grab\");\n\n      expect(isFocused).toBe(true);\n    });\n\n    test(\"input mode should show input textarea\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"h1\");\n\n      const hasTextarea = await reactGrab.page.evaluate((attrName) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return false;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        if (!root) return false;\n        return root.querySelector(\"textarea\") !== null;\n      }, \"data-react-grab\");\n\n      expect(hasTextarea).toBe(true);\n    });\n  });\n\n  test.describe(\"Text Input and Editing\", () => {\n    test(\"should accept text input\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Test prompt text\");\n\n      const inputValue = await reactGrab.getInputValue();\n      expect(inputValue).toBe(\"Test prompt text\");\n    });\n\n    test(\"should allow editing typed text\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Hello\");\n      await reactGrab.page.keyboard.press(\"Backspace\");\n      await reactGrab.page.keyboard.press(\"Backspace\");\n      await reactGrab.typeInInput(\"p!\");\n\n      const inputValue = await reactGrab.getInputValue();\n      expect(inputValue).toBe(\"Help!\");\n    });\n\n    test(\"should handle long text input\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      const longText =\n        \"This is a very long prompt that should be handled properly by the textarea input field and might need to scroll within the container.\";\n      await reactGrab.typeInInput(longText);\n\n      const inputValue = await reactGrab.getInputValue();\n      expect(inputValue).toBe(longText);\n    });\n\n    test(\"should handle multiline input with shift+enter\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Line 1\");\n      await reactGrab.page.keyboard.down(\"Shift\");\n      await reactGrab.page.keyboard.press(\"Enter\");\n      await reactGrab.page.keyboard.up(\"Shift\");\n      await reactGrab.typeInInput(\"Line 2\");\n\n      const inputValue = await reactGrab.getInputValue();\n      expect(inputValue).toContain(\"Line 1\");\n      expect(inputValue).toContain(\"Line 2\");\n    });\n  });\n\n  test.describe(\"Submit and Cancel\", () => {\n    test(\"Enter key should submit input\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent({ delay: 100 });\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Test prompt\");\n      await reactGrab.submitInput();\n\n      await expect.poll(() => reactGrab.isPromptModeActive()).toBe(false);\n    });\n\n    test(\"Escape should cancel input mode\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.pressEscape();\n\n      await expect.poll(() => reactGrab.isPromptModeActive()).toBe(false);\n    });\n\n    test(\"Escape in textarea should dismiss input mode directly\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      expect(await reactGrab.isPromptModeActive()).toBe(true);\n\n      await reactGrab.typeInInput(\"Some unsaved text\");\n\n      await reactGrab.pressEscape();\n\n      await expect.poll(() => reactGrab.isPromptModeActive()).toBe(false);\n    });\n\n    test(\"confirming dismiss should close input mode\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Some text\");\n      await reactGrab.pressEscape();\n      await reactGrab.pressEscape();\n\n      await expect.poll(() => reactGrab.isOverlayVisible()).toBe(false);\n    });\n\n    test(\"empty input should cancel without confirmation\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.pressEscape();\n\n      const isPendingDismiss = await reactGrab.isPendingDismissVisible();\n      expect(isPendingDismiss).toBe(false);\n    });\n  });\n\n  test.describe(\"Input Mode with Selection\", () => {\n    test(\"should freeze selection while in input mode\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.page.mouse.move(500, 500);\n\n      const isPromptMode = await reactGrab.isPromptModeActive();\n      expect(isPromptMode).toBe(true);\n    });\n  });\n\n  test.describe(\"Keyboard Shortcuts in Input Mode\", () => {\n    test(\"arrow keys should not navigate elements in input mode\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.pressArrowDown();\n\n      const isPromptMode = await reactGrab.isPromptModeActive();\n      expect(isPromptMode).toBe(true);\n    });\n\n    test(\"activation shortcut should not cancel input mode when input is focused\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n      await reactGrab.page.keyboard.press(\"c\");\n      await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n\n      await expect.poll(() => reactGrab.isPromptModeActive()).toBe(true);\n    });\n  });\n\n  test.describe(\"Input Preservation\", () => {\n    test(\"input should be cleared after dismissing input mode\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Some text\");\n      await reactGrab.pressEscape();\n\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      const inputValue = await reactGrab.getInputValue();\n      expect(inputValue).toBe(\"\");\n    });\n  });\n\n  test.describe(\"Edge Cases\", () => {\n    test(\"clicking outside should cancel input mode\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.page.mouse.click(10, 10);\n      await reactGrab.page.mouse.click(10, 10);\n\n      await expect.poll(() => reactGrab.isPromptModeActive()).toBe(false);\n    });\n\n    test(\"context menu edit maintains overlay in input mode\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      const isPromptActive = await reactGrab.isPromptModeActive();\n      expect(isPromptActive).toBe(true);\n\n      const isOverlayActive = await reactGrab.isOverlayVisible();\n      expect(isOverlayActive).toBe(true);\n    });\n\n    test(\"input mode should work after scroll\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.activate();\n      await reactGrab.scrollPage(100);\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.rightClickElement(\"li:first-child\");\n      await reactGrab.clickContextMenuItem(\"Edit\");\n\n      await expect.poll(() => reactGrab.isPromptModeActive()).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/keyboard-navigation.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\ntest.describe(\"Keyboard Navigation\", () => {\n  test(\"should navigate to next element with ArrowDown\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.page.keyboard.press(\"ArrowDown\");\n    await reactGrab.waitForSelectionBox();\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"should navigate to previous element with ArrowUp\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"li:nth-child(3)\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.page.keyboard.press(\"ArrowUp\");\n    await reactGrab.waitForSelectionBox();\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"should navigate to parent element with ArrowLeft\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.page.keyboard.press(\"ArrowLeft\");\n    await reactGrab.waitForSelectionBox();\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"should navigate to child element with ArrowRight\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"ul\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.page.keyboard.press(\"ArrowRight\");\n    await reactGrab.waitForSelectionBox();\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"should maintain activation during keyboard navigation\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.page.keyboard.press(\"ArrowDown\");\n    await reactGrab.page.keyboard.press(\"ArrowDown\");\n    await reactGrab.page.keyboard.press(\"ArrowUp\");\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"should copy element after keyboard navigation with click\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"[data-testid='todo-list'] li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.page.keyboard.press(\"ArrowDown\");\n    await reactGrab.waitForSelectionBox();\n\n    const secondItem = reactGrab.page.locator(\n      \"[data-testid='todo-list'] li:nth-child(2)\",\n    );\n    const box = await secondItem.boundingBox();\n    if (box) {\n      await reactGrab.page.mouse.click(box.x + 10, box.y + 10);\n    }\n    await reactGrab.page.waitForTimeout(500);\n\n    const clipboardContent = await reactGrab.getClipboardContent();\n    expect(clipboardContent).toBeTruthy();\n  });\n\n  test(\"should copy keyboard-selected element when clicking after mouse movement\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"[data-testid='todo-list'] li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    const initialBounds = await reactGrab.getSelectionBoxBounds();\n    expect(initialBounds).not.toBeNull();\n\n    await reactGrab.page.keyboard.press(\"ArrowUp\");\n    await reactGrab.waitForSelectionBox();\n\n    const selectionBoundsAfterArrow = await reactGrab.getSelectionBoxBounds();\n    expect(selectionBoundsAfterArrow).not.toBeNull();\n\n    await reactGrab.page.mouse.move(10, 10);\n    await reactGrab.page.waitForTimeout(50);\n\n    await reactGrab.page.mouse.click(\n      selectionBoundsAfterArrow!.x + selectionBoundsAfterArrow!.width / 2,\n      selectionBoundsAfterArrow!.y + selectionBoundsAfterArrow!.height / 2,\n    );\n    await reactGrab.page.waitForTimeout(500);\n\n    const clipboardContent = await reactGrab.getClipboardContent();\n    expect(clipboardContent).toBeTruthy();\n  });\n\n  test(\"should freeze selection when navigating with arrow keys\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.page.keyboard.press(\"ArrowDown\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.page.mouse.move(0, 0);\n    await reactGrab.page.waitForTimeout(100);\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(true);\n  });\n});\n\ntest.describe(\"Navigation History and Wrapping\", () => {\n  test(\"ArrowLeft should go back to previous element\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.pressArrowDown();\n    await reactGrab.pressArrowDown();\n\n    await reactGrab.pressArrowLeft();\n    await reactGrab.waitForSelectionBox();\n\n    const isVisible = await reactGrab.isSelectionBoxVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"multiple ArrowDown should navigate through siblings\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.pressArrowDown();\n    await reactGrab.pressArrowDown();\n    await reactGrab.pressArrowDown();\n    await reactGrab.waitForSelectionBox();\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"ArrowUp at first sibling should stay on element\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.pressArrowUp();\n    await reactGrab.waitForSelectionBox();\n\n    const isVisible = await reactGrab.isSelectionBoxVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"ArrowDown at last sibling should stay on element\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"li:last-child\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.pressArrowDown();\n    await reactGrab.waitForSelectionBox();\n\n    const isVisible = await reactGrab.isSelectionBoxVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"navigation should work on deeply nested elements\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"[data-testid='deeply-nested-text']\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.pressArrowLeft();\n    await reactGrab.waitForSelectionBox();\n\n    const isVisible = await reactGrab.isSelectionBoxVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"keyboard navigation should update selection label\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    const labelBefore = await reactGrab.getSelectionLabelInfo();\n\n    await reactGrab.pressArrowLeft();\n    await reactGrab.waitForSelectionBox();\n\n    const labelAfter = await reactGrab.getSelectionLabelInfo();\n\n    expect(labelBefore.isVisible).toBe(true);\n    expect(labelAfter.isVisible).toBe(true);\n  });\n});\n\ntest.describe(\"ArrowUp Vertical Traversal\", () => {\n  test(\"ArrowUp should reach parent element from child\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"[data-testid='todo-list'] li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    const initialLabel = await reactGrab.getSelectionLabelInfo();\n\n    await reactGrab.pressArrowUp();\n    await reactGrab.waitForSelectionBox();\n\n    const afterUpLabel = await reactGrab.getSelectionLabelInfo();\n\n    expect(initialLabel.tagName).toBe(\"li\");\n    expect(afterUpLabel.tagName).not.toBe(\"li\");\n    expect(afterUpLabel.isVisible).toBe(true);\n  });\n\n  test(\"repeated ArrowUp should not oscillate between elements\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"[data-testid='todo-list'] li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    const visitedTags: string[] = [];\n    for (let step = 0; step < 8; step++) {\n      await reactGrab.pressArrowUp();\n      await reactGrab.page.waitForTimeout(50);\n      const labelInfo = await reactGrab.getSelectionLabelInfo();\n      if (!labelInfo.isVisible) break;\n      visitedTags.push(labelInfo.tagName ?? \"unknown\");\n    }\n\n    let oscillationCount = 0;\n    for (let index = 2; index < visitedTags.length; index++) {\n      const isRepeatingTwoStepPattern =\n        visitedTags[index] === visitedTags[index - 2] &&\n        visitedTags[index] !== visitedTags[index - 1];\n      if (isRepeatingTwoStepPattern) {\n        oscillationCount++;\n      }\n    }\n    expect(oscillationCount).toBeLessThan(2);\n  });\n\n  test(\"ArrowUp bounds should never shrink\", async ({ reactGrab }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"[data-testid='todo-list'] li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    let previousBounds = await reactGrab.getSelectionBoxBounds();\n    expect(previousBounds).not.toBeNull();\n    let boundsShrunk = false;\n\n    for (let step = 0; step < 5; step++) {\n      await reactGrab.pressArrowUp();\n      await reactGrab.page.waitForTimeout(50);\n      const currentBounds = await reactGrab.getSelectionBoxBounds();\n      if (!currentBounds) break;\n\n      const previousArea = previousBounds!.width * previousBounds!.height;\n      const currentArea = currentBounds.width * currentBounds.height;\n      if (currentArea < previousArea - 1) {\n        boundsShrunk = true;\n        break;\n      }\n      previousBounds = currentBounds;\n    }\n\n    expect(boundsShrunk).toBe(false);\n  });\n\n  test(\"ArrowDown should reverse ArrowUp and maintain selection\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"[data-testid='todo-list'] li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.pressArrowUp();\n    await reactGrab.waitForSelectionBox();\n    await reactGrab.pressArrowUp();\n    await reactGrab.waitForSelectionBox();\n\n    const afterUpVisible = await reactGrab.isSelectionBoxVisible();\n    expect(afterUpVisible).toBe(true);\n\n    await reactGrab.pressArrowDown();\n    await reactGrab.waitForSelectionBox();\n    await reactGrab.pressArrowDown();\n    await reactGrab.waitForSelectionBox();\n\n    const afterDownVisible = await reactGrab.isSelectionBoxVisible();\n    expect(afterDownVisible).toBe(true);\n\n    const afterDownBounds = await reactGrab.getSelectionBoxBounds();\n    expect(afterDownBounds).not.toBeNull();\n    expect(afterDownBounds!.width).toBeGreaterThan(0);\n    expect(afterDownBounds!.height).toBeGreaterThan(0);\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/keyboard-shortcuts.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\ntest.describe(\"Keyboard Shortcuts\", () => {\n  test(\"should copy selected element when clicking\", async ({ reactGrab }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"[data-testid='todo-list'] h1\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.clickElement(\"[data-testid='todo-list'] h1\");\n    await reactGrab.page.waitForTimeout(500);\n\n    const clipboardContent = await reactGrab.getClipboardContent();\n    expect(clipboardContent).toContain(\"Todo List\");\n  });\n\n  test(\"should deactivate when pressing Escape while hovering\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.pressEscape();\n    await reactGrab.page.waitForTimeout(100);\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(false);\n  });\n\n  test(\"should not activate when pressing C without Cmd/Ctrl modifier\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.page.keyboard.down(\"c\");\n    await reactGrab.page.waitForTimeout(50);\n    await reactGrab.page.keyboard.up(\"c\");\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(false);\n  });\n\n  test(\"should copy list item when clicked\", async ({ reactGrab }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"[data-testid='todo-list'] li:nth-child(2)\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.clickElement(\"[data-testid='todo-list'] li:nth-child(2)\");\n    await reactGrab.page.waitForTimeout(500);\n\n    const clipboardContent = await reactGrab.getClipboardContent();\n    expect(clipboardContent).toContain(\"Walk the dog\");\n  });\n\n  test(\"should keep overlay active while navigating with arrow keys\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    for (let i = 0; i < 5; i++) {\n      await reactGrab.page.keyboard.press(\"ArrowDown\");\n      await reactGrab.page.waitForTimeout(50);\n    }\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"should deactivate after successful click copy in toggle mode\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.clickElement(\"li:first-child\");\n    await reactGrab.page.waitForTimeout(2000);\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/open-file.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\ntest.describe(\"Open File\", () => {\n  test.describe(\"Keyboard Shortcut\", () => {\n    test(\"Cmd+O should open file when source info available\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.page.evaluate(() => {\n        (window as { __OPEN_FILE_CALLED__?: boolean }).__OPEN_FILE_CALLED__ =\n          false;\n        const api = (\n          window as {\n            __REACT_GRAB__?: {\n              registerPlugin: (plugin: Record<string, unknown>) => void;\n            };\n          }\n        ).__REACT_GRAB__;\n        api?.registerPlugin({\n          name: \"test-open-file\",\n          hooks: {\n            onOpenFile: () => {\n              (\n                window as { __OPEN_FILE_CALLED__?: boolean }\n              ).__OPEN_FILE_CALLED__ = true;\n            },\n          },\n        });\n      });\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.waitForSelectionSource();\n\n      await expect\n        .poll(\n          async () => {\n            await reactGrab.pressKeyCombo([reactGrab.modifierKey], \"o\");\n            return reactGrab.page.evaluate(\n              () =>\n                (window as { __OPEN_FILE_CALLED__?: boolean })\n                  .__OPEN_FILE_CALLED__ ?? false,\n            );\n          },\n          { timeout: 5000, intervals: [500] },\n        )\n        .toBe(true);\n    });\n\n    test(\"Cmd+O should do nothing without onOpenFile callback\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n      await reactGrab.page.keyboard.press(\"o\");\n      await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n      await reactGrab.page.waitForTimeout(200);\n\n      const isActive = await reactGrab.isOverlayVisible();\n      expect(isActive).toBe(true);\n    });\n\n    test(\"Cmd+O without selection should be ignored\", async ({ reactGrab }) => {\n      let openFileCalled = false;\n\n      await reactGrab.page.evaluate(() => {\n        (window as { __OPEN_FILE_CALLED__?: boolean }).__OPEN_FILE_CALLED__ =\n          false;\n        const api = (\n          window as {\n            __REACT_GRAB__?: {\n              registerPlugin: (plugin: Record<string, unknown>) => void;\n            };\n          }\n        ).__REACT_GRAB__;\n        api?.registerPlugin({\n          name: \"test-open-file\",\n          hooks: {\n            onOpenFile: () => {\n              (\n                window as { __OPEN_FILE_CALLED__?: boolean }\n              ).__OPEN_FILE_CALLED__ = true;\n            },\n          },\n        });\n      });\n\n      await reactGrab.activate();\n\n      await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n      await reactGrab.page.keyboard.press(\"o\");\n      await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n      await reactGrab.page.waitForTimeout(200);\n\n      openFileCalled = await reactGrab.page.evaluate(() => {\n        return (\n          (window as { __OPEN_FILE_CALLED__?: boolean }).__OPEN_FILE_CALLED__ ??\n          false\n        );\n      });\n\n      expect(openFileCalled).toBe(false);\n    });\n  });\n\n  test.describe(\"Context Menu\", () => {\n    test(\"Open item should appear in context menu\", async ({ reactGrab }) => {\n      await reactGrab.page.evaluate(() => {\n        const api = (\n          window as {\n            __REACT_GRAB__?: {\n              registerPlugin: (plugin: Record<string, unknown>) => void;\n            };\n          }\n        ).__REACT_GRAB__;\n        api?.registerPlugin({\n          name: \"test-open-file\",\n          hooks: {\n            onOpenFile: () => {},\n          },\n        });\n      });\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.rightClickElement(\"li:first-child\");\n\n      const menuInfo = await reactGrab.getContextMenuInfo();\n      expect(menuInfo.isVisible).toBe(true);\n      expect(menuInfo.menuItems).toContain(\"Open\");\n    });\n\n    test(\"Clicking Open in context menu should trigger onOpenFile\", async ({\n      reactGrab,\n    }) => {\n      let openFileCalled = false;\n\n      await reactGrab.page.evaluate(() => {\n        (window as { __OPEN_FILE_CALLED__?: boolean }).__OPEN_FILE_CALLED__ =\n          false;\n        const api = (\n          window as {\n            __REACT_GRAB__?: {\n              registerPlugin: (plugin: Record<string, unknown>) => void;\n            };\n          }\n        ).__REACT_GRAB__;\n        api?.registerPlugin({\n          name: \"test-open-file\",\n          hooks: {\n            onOpenFile: () => {\n              (\n                window as { __OPEN_FILE_CALLED__?: boolean }\n              ).__OPEN_FILE_CALLED__ = true;\n            },\n          },\n        });\n      });\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.rightClickElement(\"li:first-child\");\n      await reactGrab.page.waitForTimeout(100);\n\n      await reactGrab.clickContextMenuItem(\"Open\");\n      await reactGrab.page.waitForTimeout(200);\n\n      openFileCalled = await reactGrab.page.evaluate(() => {\n        return (\n          (window as { __OPEN_FILE_CALLED__?: boolean }).__OPEN_FILE_CALLED__ ??\n          false\n        );\n      });\n\n      expect(openFileCalled).toBe(true);\n    });\n\n    test(\"Open should not be clickable without onOpenFile callback\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.rightClickElement(\"li:first-child\");\n      await reactGrab.page.waitForTimeout(200);\n\n      const menuInfo = await reactGrab.getContextMenuInfo();\n      expect(menuInfo.isVisible).toBe(true);\n    });\n  });\n\n  test.describe(\"onOpenFile Callback\", () => {\n    test(\"callback should receive element info\", async ({ reactGrab }) => {\n      let receivedInfo: unknown = null;\n\n      await reactGrab.page.evaluate(() => {\n        (window as { __OPEN_FILE_INFO__?: unknown }).__OPEN_FILE_INFO__ = null;\n        const api = (\n          window as {\n            __REACT_GRAB__?: {\n              registerPlugin: (plugin: Record<string, unknown>) => void;\n            };\n          }\n        ).__REACT_GRAB__;\n        api?.registerPlugin({\n          name: \"test-open-file\",\n          hooks: {\n            onOpenFile: (info: unknown) => {\n              (window as { __OPEN_FILE_INFO__?: unknown }).__OPEN_FILE_INFO__ =\n                info;\n            },\n          },\n        });\n      });\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n      await reactGrab.page.keyboard.press(\"o\");\n      await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n      await reactGrab.page.waitForTimeout(200);\n\n      receivedInfo = await reactGrab.page.evaluate(() => {\n        return (window as { __OPEN_FILE_INFO__?: unknown }).__OPEN_FILE_INFO__;\n      });\n\n      expect(receivedInfo).toBeDefined();\n    });\n\n    test(\"callback should include source info when available\", async ({\n      reactGrab,\n    }) => {\n      let receivedInfo: Record<string, unknown> | null | undefined = null;\n\n      await reactGrab.page.evaluate(() => {\n        (\n          window as { __OPEN_FILE_INFO__?: Record<string, unknown> | null }\n        ).__OPEN_FILE_INFO__ = null;\n        const api = (\n          window as {\n            __REACT_GRAB__?: {\n              registerPlugin: (plugin: Record<string, unknown>) => void;\n            };\n          }\n        ).__REACT_GRAB__;\n        api?.registerPlugin({\n          name: \"test-open-file\",\n          hooks: {\n            onOpenFile: (info: Record<string, unknown>) => {\n              (\n                window as {\n                  __OPEN_FILE_INFO__?: Record<string, unknown> | null;\n                }\n              ).__OPEN_FILE_INFO__ = info;\n            },\n          },\n        });\n      });\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n      await reactGrab.page.keyboard.press(\"o\");\n      await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n      await reactGrab.page.waitForTimeout(200);\n\n      receivedInfo = await reactGrab.page.evaluate(() => {\n        return (\n          window as { __OPEN_FILE_INFO__?: Record<string, unknown> | null }\n        ).__OPEN_FILE_INFO__;\n      });\n\n      expect(receivedInfo).toBeDefined();\n    });\n  });\n\n  test.describe(\"Tag Badge Click\", () => {\n    test(\"clicking tag badge should trigger open file\", async ({\n      reactGrab,\n    }) => {\n      let openFileCalled = false;\n\n      await reactGrab.page.evaluate(() => {\n        (window as { __OPEN_FILE_CALLED__?: boolean }).__OPEN_FILE_CALLED__ =\n          false;\n        const api = (\n          window as {\n            __REACT_GRAB__?: {\n              registerPlugin: (plugin: Record<string, unknown>) => void;\n            };\n          }\n        ).__REACT_GRAB__;\n        api?.registerPlugin({\n          name: \"test-open-file\",\n          hooks: {\n            onOpenFile: () => {\n              (\n                window as { __OPEN_FILE_CALLED__?: boolean }\n              ).__OPEN_FILE_CALLED__ = true;\n            },\n          },\n        });\n      });\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.page.evaluate((attrName) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        if (!root) return;\n\n        const spans = root.querySelectorAll(\"span\");\n        for (const span of spans) {\n          if (\n            span.textContent?.includes(\"li\") ||\n            span.textContent?.includes(\"span\")\n          ) {\n            (span as HTMLElement).click();\n            return;\n          }\n        }\n      }, \"data-react-grab\");\n\n      await reactGrab.page.waitForTimeout(200);\n\n      openFileCalled = await reactGrab.page.evaluate(() => {\n        return (\n          (window as { __OPEN_FILE_CALLED__?: boolean }).__OPEN_FILE_CALLED__ ??\n          false\n        );\n      });\n\n      expect(typeof openFileCalled).toBe(\"boolean\");\n    });\n  });\n\n  test.describe(\"Edge Cases\", () => {\n    test(\"open file should work after element change\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.page.evaluate(() => {\n        (window as { __OPEN_FILE_COUNT__?: number }).__OPEN_FILE_COUNT__ = 0;\n        const api = (\n          window as {\n            __REACT_GRAB__?: {\n              registerPlugin: (plugin: Record<string, unknown>) => void;\n            };\n          }\n        ).__REACT_GRAB__;\n        api?.registerPlugin({\n          name: \"test-open-file\",\n          hooks: {\n            onOpenFile: () => {\n              (window as { __OPEN_FILE_COUNT__?: number }).__OPEN_FILE_COUNT__ =\n                ((window as { __OPEN_FILE_COUNT__?: number })\n                  .__OPEN_FILE_COUNT__ ?? 0) + 1;\n            },\n          },\n        });\n      });\n\n      await reactGrab.activate();\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.waitForSelectionSource();\n\n      await expect\n        .poll(\n          async () => {\n            await reactGrab.pressKeyCombo([reactGrab.modifierKey], \"o\");\n            return reactGrab.page.evaluate(\n              () =>\n                (window as { __OPEN_FILE_COUNT__?: number })\n                  .__OPEN_FILE_COUNT__ ?? 0,\n            );\n          },\n          { timeout: 5000, intervals: [500] },\n        )\n        .toBeGreaterThanOrEqual(1);\n\n      await reactGrab.hoverElement(\"li:nth-child(2)\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.waitForSelectionSource();\n\n      await expect\n        .poll(\n          async () => {\n            await reactGrab.pressKeyCombo([reactGrab.modifierKey], \"o\");\n            return reactGrab.page.evaluate(\n              () =>\n                (window as { __OPEN_FILE_COUNT__?: number })\n                  .__OPEN_FILE_COUNT__ ?? 0,\n            );\n          },\n          { timeout: 5000, intervals: [500] },\n        )\n        .toBeGreaterThanOrEqual(2);\n    });\n\n    test(\"open file should work with drag-selected elements\", async ({\n      reactGrab,\n    }) => {\n      let openFileCalled = false;\n\n      await reactGrab.page.evaluate(() => {\n        (window as { __OPEN_FILE_CALLED__?: boolean }).__OPEN_FILE_CALLED__ =\n          false;\n        const api = (\n          window as {\n            __REACT_GRAB__?: {\n              registerPlugin: (plugin: Record<string, unknown>) => void;\n            };\n          }\n        ).__REACT_GRAB__;\n        api?.registerPlugin({\n          name: \"test-open-file\",\n          hooks: {\n            onOpenFile: () => {\n              (\n                window as { __OPEN_FILE_CALLED__?: boolean }\n              ).__OPEN_FILE_CALLED__ = true;\n            },\n          },\n        });\n      });\n\n      await reactGrab.activate();\n      await reactGrab.dragSelect(\"li:first-child\", \"li:nth-child(2)\");\n      await reactGrab.page.waitForTimeout(200);\n\n      await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n      await reactGrab.page.keyboard.press(\"o\");\n      await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n      await reactGrab.page.waitForTimeout(200);\n\n      openFileCalled = await reactGrab.page.evaluate(() => {\n        return (\n          (window as { __OPEN_FILE_CALLED__?: boolean }).__OPEN_FILE_CALLED__ ??\n          false\n        );\n      });\n\n      expect(typeof openFileCalled).toBe(\"boolean\");\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/overlay-filtering.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\ntest.describe(\"Overlay Filtering\", () => {\n  test.describe(\"React-grab elements should not be selectable\", () => {\n    test(\"should not select react-grab host element\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      const selectedElement = await reactGrab.page.evaluate(() => {\n        const api = (\n          window as {\n            __REACT_GRAB__?: {\n              getState: () => { targetElement: Element | null };\n            };\n          }\n        ).__REACT_GRAB__;\n        const state = api?.getState();\n        return state?.targetElement?.hasAttribute(\"data-react-grab\") ?? false;\n      });\n\n      expect(selectedElement).toBe(false);\n    });\n\n    test(\"should not select elements inside react-grab shadow DOM\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      const isInsideShadowDom = await reactGrab.page.evaluate(() => {\n        const api = (\n          window as {\n            __REACT_GRAB__?: {\n              getState: () => { targetElement: Element | null };\n            };\n          }\n        ).__REACT_GRAB__;\n        const state = api?.getState();\n        const target = state?.targetElement;\n        if (!target) return false;\n\n        const rootNode = target.getRootNode();\n        if (rootNode instanceof ShadowRoot) {\n          return rootNode.host.hasAttribute(\"data-react-grab\");\n        }\n        return false;\n      });\n\n      expect(isInsideShadowDom).toBe(false);\n    });\n\n    test(\"should select page elements through react-grab overlay\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      const tagName = await reactGrab.page.evaluate(() => {\n        const api = (\n          window as {\n            __REACT_GRAB__?: {\n              getState: () => { targetElement: Element | null };\n            };\n          }\n        ).__REACT_GRAB__;\n        const state = api?.getState();\n        return state?.targetElement?.tagName?.toLowerCase() ?? null;\n      });\n\n      expect(tagName).toBe(\"li\");\n    });\n  });\n\n  test.describe(\"Selection ignores react-grab UI components\", () => {\n    test(\"hovering over toolbar area should still select underlying element\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n\n      const toolbarInfo = await reactGrab.getToolbarInfo();\n      if (toolbarInfo.position) {\n        await reactGrab.page.mouse.move(\n          toolbarInfo.position.x + 10,\n          toolbarInfo.position.y + 10,\n        );\n        await reactGrab.page.waitForTimeout(200);\n\n        const state = await reactGrab.getState();\n        expect(state.isActive).toBe(true);\n      }\n    });\n\n    test(\"clicking through overlay should copy correct element\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='todo-list'] h1\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.clickElement(\"[data-testid='todo-list'] h1\");\n\n      await expect\n        .poll(() => reactGrab.getClipboardContent())\n        .toContain(\"Todo List\");\n    });\n\n    test(\"drag selection should work through overlay canvas\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.dragSelect(\"li:first-child\", \"li:nth-child(3)\");\n      await reactGrab.page.waitForTimeout(500);\n\n      const grabbedInfo = await reactGrab.getGrabbedBoxInfo();\n      expect(grabbedInfo.count).toBeGreaterThan(1);\n    });\n  });\n\n  test.describe(\"Shadow DOM isolation\", () => {\n    test(\"should only filter elements inside react-grab shadow DOM\", async ({\n      reactGrab,\n    }) => {\n      const shadowHostExists = await reactGrab.page.evaluate(() => {\n        const host = document.querySelector(\"[data-react-grab]\");\n        return host !== null && host.shadowRoot !== null;\n      });\n\n      expect(shadowHostExists).toBe(true);\n\n      await reactGrab.activate();\n\n      const isReactGrabHostFiltered = await reactGrab.page.evaluate(() => {\n        const host = document.querySelector(\"[data-react-grab]\");\n        if (!host) return false;\n\n        const api = (\n          window as {\n            __REACT_GRAB__?: {\n              getState: () => { targetElement: Element | null };\n            };\n          }\n        ).__REACT_GRAB__;\n        const state = api?.getState();\n        return state?.targetElement !== host;\n      });\n\n      expect(isReactGrabHostFiltered).toBe(true);\n    });\n\n    test(\"should verify react-grab host has correct attribute\", async ({\n      reactGrab,\n    }) => {\n      const hostHasAttribute = await reactGrab.page.evaluate(() => {\n        const host = document.querySelector(\"[data-react-grab]\");\n        return host?.hasAttribute(\"data-react-grab\") ?? false;\n      });\n\n      expect(hostHasAttribute).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/prompt-mode.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\ntest.describe(\"Prompt Mode\", () => {\n  test.describe(\"Entering Prompt Mode\", () => {\n    test(\"context menu edit should enter prompt mode when agent is configured\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.rightClickElement(\"li:first-child\");\n      await reactGrab.clickContextMenuItem(\"Edit\");\n\n      await expect.poll(() => reactGrab.isPromptModeActive()).toBe(true);\n    });\n\n    test(\"single click should copy without entering prompt mode when no agent\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.clickElement(\"li:first-child\");\n\n      await expect.poll(() => reactGrab.getClipboardContent()).toBeTruthy();\n    });\n\n    test(\"should focus input textarea when entering prompt mode\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      const isFocused = await reactGrab.page.evaluate((attrName) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return false;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        if (!root) return false;\n        const textarea = root.querySelector(\"textarea\");\n        return (\n          document.activeElement === textarea ||\n          shadowRoot.activeElement === textarea\n        );\n      }, \"data-react-grab\");\n\n      expect(isFocused).toBe(true);\n    });\n\n    test(\"prompt mode should show input textarea\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"h1\");\n\n      const hasTextarea = await reactGrab.page.evaluate((attrName) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return false;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        if (!root) return false;\n        return root.querySelector(\"textarea\") !== null;\n      }, \"data-react-grab\");\n\n      expect(hasTextarea).toBe(true);\n    });\n  });\n\n  test.describe(\"Prompt Mode Control\", () => {\n    test(\"API toggle should exit prompt mode\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.toggle();\n\n      await expect\n        .poll(() => reactGrab.isOverlayVisible(), { timeout: 2000 })\n        .toBe(false);\n      expect(await reactGrab.isPromptModeActive()).toBe(false);\n    });\n  });\n\n  test.describe(\"Text Input and Editing\", () => {\n    test(\"should accept text input\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Test prompt text\");\n\n      const inputValue = await reactGrab.getInputValue();\n      expect(inputValue).toBe(\"Test prompt text\");\n    });\n\n    test(\"should allow editing typed text\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Hello\");\n      await reactGrab.page.keyboard.press(\"Backspace\");\n      await reactGrab.page.keyboard.press(\"Backspace\");\n      await reactGrab.typeInInput(\"p!\");\n\n      const inputValue = await reactGrab.getInputValue();\n      expect(inputValue).toBe(\"Help!\");\n    });\n\n    test(\"should handle long text input\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      const longText =\n        \"This is a very long prompt that should be handled properly by the textarea input field and might need to scroll within the container.\";\n      await reactGrab.typeInInput(longText);\n\n      const inputValue = await reactGrab.getInputValue();\n      expect(inputValue).toBe(longText);\n    });\n\n    test(\"should handle multiline input with shift+enter\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Line 1\");\n      await reactGrab.page.keyboard.down(\"Shift\");\n      await reactGrab.page.keyboard.press(\"Enter\");\n      await reactGrab.page.keyboard.up(\"Shift\");\n      await reactGrab.typeInInput(\"Line 2\");\n\n      const inputValue = await reactGrab.getInputValue();\n      expect(inputValue).toContain(\"Line 1\");\n      expect(inputValue).toContain(\"Line 2\");\n    });\n  });\n\n  test.describe(\"Submit and Cancel\", () => {\n    test(\"Enter key should submit input\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent({ delay: 100 });\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Test prompt\");\n      await reactGrab.submitInput();\n\n      await expect.poll(() => reactGrab.isPromptModeActive()).toBe(false);\n    });\n\n    test(\"Escape should cancel prompt mode\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.pressEscape();\n\n      await expect.poll(() => reactGrab.isPromptModeActive()).toBe(false);\n    });\n\n    test(\"Escape in textarea should dismiss prompt mode directly\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      expect(await reactGrab.isPromptModeActive()).toBe(true);\n\n      await reactGrab.typeInInput(\"Some unsaved text\");\n\n      await reactGrab.pressEscape();\n\n      await expect.poll(() => reactGrab.isPromptModeActive()).toBe(false);\n    });\n\n    test(\"confirming dismiss should close prompt mode\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Some text\");\n      await reactGrab.pressEscape();\n      await reactGrab.pressEscape();\n\n      await expect.poll(() => reactGrab.isOverlayVisible()).toBe(false);\n    });\n\n    test(\"empty input should cancel without confirmation\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.pressEscape();\n\n      const isPendingDismiss = await reactGrab.isPendingDismissVisible();\n      expect(isPendingDismiss).toBe(false);\n    });\n  });\n\n  test.describe(\"Prompt Mode with Selection\", () => {\n    test(\"should freeze selection while in prompt mode\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.page.mouse.move(500, 500);\n\n      const isPromptMode = await reactGrab.isPromptModeActive();\n      expect(isPromptMode).toBe(true);\n    });\n  });\n\n  test.describe(\"Keyboard Shortcuts in Prompt Mode\", () => {\n    test(\"arrow keys should not navigate elements in prompt mode\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.pressArrowDown();\n\n      const isPromptMode = await reactGrab.isPromptModeActive();\n      expect(isPromptMode).toBe(true);\n    });\n\n    test(\"activation shortcut should not cancel prompt mode when input is focused\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.page.keyboard.down(reactGrab.modifierKey);\n      await reactGrab.page.keyboard.press(\"c\");\n      await reactGrab.page.keyboard.up(reactGrab.modifierKey);\n\n      await expect.poll(() => reactGrab.isPromptModeActive()).toBe(true);\n    });\n  });\n\n  test.describe(\"Input Preservation\", () => {\n    test(\"input should be cleared after dismissing prompt mode\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.typeInInput(\"Some text\");\n      await reactGrab.pressEscape();\n\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      const inputValue = await reactGrab.getInputValue();\n      expect(inputValue).toBe(\"\");\n    });\n  });\n\n  test.describe(\"Edge Cases\", () => {\n    test(\"clicking outside should cancel prompt mode\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      await reactGrab.page.mouse.click(10, 10);\n      await reactGrab.page.mouse.click(10, 10);\n\n      await expect.poll(() => reactGrab.isPromptModeActive()).toBe(false);\n    });\n\n    test(\"context menu edit maintains overlay in prompt mode\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.enterPromptMode(\"li:first-child\");\n\n      const isPromptActive = await reactGrab.isPromptModeActive();\n      expect(isPromptActive).toBe(true);\n\n      const isOverlayActive = await reactGrab.isOverlayVisible();\n      expect(isOverlayActive).toBe(true);\n    });\n\n    test(\"prompt mode should work after scroll\", async ({ reactGrab }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.activate();\n      await reactGrab.scrollPage(100);\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.rightClickElement(\"li:first-child\");\n      await reactGrab.clickContextMenuItem(\"Edit\");\n\n      await expect.poll(() => reactGrab.isPromptModeActive()).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/selection.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\ntest.describe(\"Element Selection\", () => {\n  test(\"should show selection box when hovering over element while active\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"li\");\n    await reactGrab.waitForSelectionBox();\n\n    const hasSelectionContent = await reactGrab.page.evaluate(() => {\n      const host = document.querySelector(\"[data-react-grab]\");\n      const shadowRoot = host?.shadowRoot;\n      if (!shadowRoot) return false;\n      const root = shadowRoot.querySelector(\"[data-react-grab]\");\n      return root !== null && root.innerHTML.length > 0;\n    });\n\n    expect(hasSelectionContent).toBe(true);\n  });\n\n  test(\"should copy element content to clipboard on click\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"li\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.clickElement(\"li\");\n    await expect.poll(() => reactGrab.getClipboardContent()).toBeTruthy();\n\n    const clipboardContent = await reactGrab.getClipboardContent();\n    expect(clipboardContent.length).toBeGreaterThan(0);\n  });\n\n  test(\"should copy heading element to clipboard\", async ({ reactGrab }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"[data-testid='todo-list'] h1\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.clickElement(\"[data-testid='todo-list'] h1\");\n    await expect\n      .poll(() => reactGrab.getClipboardContent())\n      .toContain(\"Todo List\");\n  });\n\n  test(\"should write React Grab clipboard metadata on copy\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"[data-testid='todo-list'] h1\");\n    await reactGrab.waitForSelectionBox();\n\n    const copyPayloadPromise = reactGrab.captureNextClipboardWrites();\n    await reactGrab.clickElement(\"[data-testid='todo-list'] h1\");\n    const copyPayload = await copyPayloadPromise;\n    const clipboardMetadataText = copyPayload[\"application/x-react-grab\"];\n    if (!clipboardMetadataText) {\n      throw new Error(\"Missing React Grab clipboard metadata\");\n    }\n\n    const clipboardMetadata = JSON.parse(clipboardMetadataText);\n    expect(clipboardMetadata.content).toContain(\"Todo List\");\n    expect(clipboardMetadata.entries).toHaveLength(1);\n    expect(clipboardMetadata.entries[0].content).toContain(\"Todo List\");\n  });\n\n  test(\"should highlight different elements when hovering\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n\n    await reactGrab.hoverElement(\"h1\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.hoverElement(\"li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.hoverElement(\"ul\");\n    await reactGrab.waitForSelectionBox();\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"should deactivate after successful copy in toggle mode\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"li\");\n    await reactGrab.clickElement(\"li\");\n\n    await expect\n      .poll(() => reactGrab.isOverlayVisible(), { timeout: 3000 })\n      .toBe(false);\n  });\n\n  test(\"should not show selection when inactive\", async ({ reactGrab }) => {\n    const isVisibleBefore = await reactGrab.isOverlayVisible();\n    expect(isVisibleBefore).toBe(false);\n\n    await reactGrab.hoverElement(\"li\");\n\n    const isVisibleAfter = await reactGrab.isOverlayVisible();\n    expect(isVisibleAfter).toBe(false);\n  });\n\n  test(\"should select nested elements correctly\", async ({ reactGrab }) => {\n    await reactGrab.activate();\n\n    await reactGrab.hoverElement(\"li:nth-child(3)\");\n    await reactGrab.waitForSelectionBox();\n    await reactGrab.clickElement(\"li:nth-child(3)\");\n\n    await expect.poll(() => reactGrab.getClipboardContent()).toBeTruthy();\n  });\n\n  test(\"should maintain selection target while hovering\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n\n    const listItem = reactGrab.page.locator(\"li\").first();\n    const box = await listItem.boundingBox();\n    if (!box) throw new Error(\"Could not get bounding box\");\n\n    await reactGrab.page.mouse.move(\n      box.x + box.width / 2,\n      box.y + box.height / 2,\n    );\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.page.mouse.move(\n      box.x + box.width / 2 + 5,\n      box.y + box.height / 2 + 5,\n    );\n    await reactGrab.waitForSelectionBox();\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(true);\n  });\n});\n\ntest.describe(\"Selection Bounds and Mutations\", () => {\n  test(\"selection box should update when element size changes\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    const initialBounds = await reactGrab.getSelectionBoxBounds();\n    expect(initialBounds).not.toBeNull();\n\n    await reactGrab.page.evaluate(() => {\n      const element = document.querySelector(\"li:first-child\") as HTMLElement;\n      if (element) {\n        element.style.height = \"200px\";\n      }\n    });\n\n    await expect\n      .poll(async () => {\n        const bounds = await reactGrab.getSelectionBoxBounds();\n        return bounds?.height ?? 0;\n      })\n      .toBeGreaterThan(initialBounds?.height ?? 0);\n  });\n\n  test(\"selection should handle element being hidden\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"[data-testid='toggleable-element']\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.hideElement(\"[data-testid='toggleable-element']\");\n\n    await reactGrab.hoverElement(\"li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    const isVisible = await reactGrab.isSelectionBoxVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"selection should recalculate after scroll\", async ({ reactGrab }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    const boundsBefore = await reactGrab.getSelectionBoxBounds();\n\n    await reactGrab.scrollPage(50);\n\n    if (boundsBefore) {\n      await expect\n        .poll(async () => {\n          const bounds = await reactGrab.getSelectionBoxBounds();\n          return bounds?.y;\n        })\n        .not.toBe(boundsBefore.y);\n    }\n  });\n\n  test(\"multiple selection boxes should display for drag selection\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.dragSelect(\"li:first-child\", \"li:nth-child(3)\");\n    await reactGrab.page.waitForTimeout(500);\n\n    await expect\n      .poll(async () => {\n        const info = await reactGrab.getGrabbedBoxInfo();\n        return info.count;\n      })\n      .toBeGreaterThan(1);\n  });\n\n  test(\"selection should work on deeply nested elements\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"[data-testid='deeply-nested-text']\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.clickElement(\"[data-testid='deeply-nested-text']\");\n\n    await expect\n      .poll(() => reactGrab.getClipboardContent())\n      .toContain(\"deeply nested\");\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/ssr.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { execSync } from \"node:child_process\";\nimport path from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\n\nconst DIRECTORY = path.dirname(fileURLToPath(import.meta.url));\nconst PACKAGE_DIRECTORY = path.resolve(DIRECTORY, \"..\");\n\ntest.describe(\"SSR Compatibility\", () => {\n  test(\"importing react-grab in Node.js should not throw\", () => {\n    const result = execSync(\n      `node -e \"require('./dist/index.cjs'); console.log('OK')\"`,\n      { cwd: PACKAGE_DIRECTORY, encoding: \"utf-8\" },\n    );\n    expect(result.trim()).toBe(\"OK\");\n  });\n\n  test(\"importing react-grab/core in Node.js should not throw\", () => {\n    const result = execSync(\n      `node -e \"require('./dist/core/index.cjs'); console.log('OK')\"`,\n      { cwd: PACKAGE_DIRECTORY, encoding: \"utf-8\" },\n    );\n    expect(result.trim()).toBe(\"OK\");\n  });\n\n  test(\"init() should return a noop API in Node.js\", () => {\n    const result = execSync(\n      `node -e \"const m = require('./dist/index.cjs'); const api = m.getGlobalApi(); console.log(api === null ? 'NULL' : 'NOT_NULL')\"`,\n      { cwd: PACKAGE_DIRECTORY, encoding: \"utf-8\" },\n    );\n    expect(result.trim()).toBe(\"NULL\");\n  });\n\n  test(\"init() called explicitly in Node.js should return noop API without crashing\", () => {\n    const result = execSync(\n      `node -e \"const { init } = require('./dist/core/index.cjs'); const api = init(); console.log(typeof api.activate === 'function' ? 'NOOP_API' : 'UNEXPECTED')\"`,\n      { cwd: PACKAGE_DIRECTORY, encoding: \"utf-8\" },\n    );\n    expect(result.trim()).toBe(\"NOOP_API\");\n  });\n\n  test(\"ESM import of react-grab in Node.js should not throw\", () => {\n    const esmEntryUrl = pathToFileURL(\n      path.resolve(PACKAGE_DIRECTORY, \"dist/index.js\"),\n    ).href;\n    const result = execSync(\n      `node -e \"import('${esmEntryUrl}').then(() => console.log('OK')).catch(e => { console.error(e); process.exit(1); })\"`,\n      { cwd: PACKAGE_DIRECTORY, encoding: \"utf-8\" },\n    );\n    expect(result.trim()).toBe(\"OK\");\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/theme-customization.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\ntest.describe(\"Theme Customization\", () => {\n  test.describe(\"Hue Rotation\", () => {\n    test(\"should apply hue rotation filter\", async ({ reactGrab }) => {\n      await reactGrab.updateOptions({ theme: { hue: 180 } });\n      await reactGrab.activate();\n\n      const hasFilter = await reactGrab.page.evaluate((attrName) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return false;\n        const root = shadowRoot.querySelector(`[${attrName}]`) as HTMLElement;\n        return root?.style.filter?.includes(\"hue-rotate\") ?? false;\n      }, \"data-react-grab\");\n\n      expect(hasFilter).toBe(true);\n    });\n\n    test(\"should apply correct hue rotation value\", async ({ reactGrab }) => {\n      await reactGrab.updateOptions({ theme: { hue: 90 } });\n      await reactGrab.activate();\n\n      const filterValue = await reactGrab.page.evaluate((attrName) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return null;\n        const root = shadowRoot.querySelector(`[${attrName}]`) as HTMLElement;\n        return root?.style.filter;\n      }, \"data-react-grab\");\n\n      expect(filterValue).toContain(\"hue-rotate(90deg)\");\n    });\n\n    test(\"should not apply filter when hue is 0\", async ({ reactGrab }) => {\n      await reactGrab.updateOptions({ theme: { hue: 0 } });\n      await reactGrab.activate();\n\n      const filterValue = await reactGrab.page.evaluate((attrName) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return \"\";\n        const root = shadowRoot.querySelector(`[${attrName}]`) as HTMLElement;\n        return root?.style.filter ?? \"\";\n      }, \"data-react-grab\");\n\n      expect(filterValue).toBe(\"\");\n    });\n  });\n\n  test.describe(\"Selection Box\", () => {\n    test(\"should show selection box by default\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      const isVisible = await reactGrab.isSelectionBoxVisible();\n      expect(isVisible).toBe(true);\n    });\n\n    test(\"should hide selection box when disabled\", async ({ reactGrab }) => {\n      await reactGrab.updateOptions({\n        theme: { selectionBox: { enabled: false } },\n      });\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      const bounds = await reactGrab.getSelectionBoxBounds();\n      expect(bounds).toBeNull();\n    });\n  });\n\n  test.describe(\"Drag Box\", () => {\n    test(\"should show drag box by default\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n\n      const listItem = reactGrab.page.locator(\"li\").first();\n      const box = await listItem.boundingBox();\n      if (!box) throw new Error(\"Could not get bounding box\");\n\n      await reactGrab.page.mouse.move(box.x - 20, box.y - 20);\n      await reactGrab.page.mouse.down();\n      await reactGrab.page.mouse.move(box.x + 150, box.y + 150, { steps: 10 });\n\n      const dragBounds = await reactGrab.getDragBoxBounds();\n      await reactGrab.page.mouse.up();\n\n      expect(dragBounds).toBeDefined();\n    });\n\n    test(\"should hide drag box when disabled\", async ({ reactGrab }) => {\n      await reactGrab.updateOptions({ theme: { dragBox: { enabled: false } } });\n      await reactGrab.activate();\n\n      const listItem = reactGrab.page.locator(\"li\").first();\n      const box = await listItem.boundingBox();\n      if (!box) throw new Error(\"Could not get bounding box\");\n\n      await reactGrab.page.mouse.move(box.x - 20, box.y - 20);\n      await reactGrab.page.mouse.down();\n      await reactGrab.page.mouse.move(box.x + 150, box.y + 150, { steps: 10 });\n\n      const dragBounds = await reactGrab.getDragBoxBounds();\n      await reactGrab.page.mouse.up();\n\n      expect(dragBounds).toBeNull();\n    });\n  });\n\n  test.describe(\"Grabbed Boxes\", () => {\n    test(\"should show grabbed boxes by default\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.clickElement(\"li:first-child\");\n      await reactGrab.page.waitForTimeout(200);\n\n      const info = await reactGrab.getGrabbedBoxInfo();\n      expect(info.count).toBeGreaterThan(0);\n    });\n\n    test(\"should hide grabbed boxes when disabled\", async ({ reactGrab }) => {\n      await reactGrab.updateOptions({\n        theme: { grabbedBoxes: { enabled: false } },\n      });\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.clickElement(\"li:first-child\");\n      await reactGrab.page.waitForTimeout(200);\n\n      const isVisible = await reactGrab.isGrabbedBoxVisible();\n      expect(isVisible).toBe(false);\n    });\n  });\n\n  test.describe(\"Element Label\", () => {\n    test(\"should show element label by default\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      const isVisible = await reactGrab.isSelectionLabelVisible();\n      expect(isVisible).toBe(true);\n    });\n\n    test(\"should hide element label when disabled\", async ({ reactGrab }) => {\n      await reactGrab.updateOptions({\n        theme: { elementLabel: { enabled: false } },\n      });\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      const labelInfo = await reactGrab.getSelectionLabelInfo();\n      expect(labelInfo.isVisible).toBe(false);\n    });\n  });\n\n  test.describe(\"Toolbar\", () => {\n    test(\"should show toolbar by default\", async ({ reactGrab }) => {\n      await reactGrab.page.waitForTimeout(600);\n\n      const isVisible = await reactGrab.isToolbarVisible();\n      expect(isVisible).toBe(true);\n    });\n\n    test(\"should hide toolbar when disabled\", async ({ reactGrab }) => {\n      await reactGrab.updateOptions({ theme: { toolbar: { enabled: false } } });\n      await reactGrab.page.waitForTimeout(600);\n\n      const isVisible = await reactGrab.isToolbarVisible();\n      expect(isVisible).toBe(false);\n    });\n  });\n\n  test.describe(\"Global Enable/Disable\", () => {\n    test(\"should disable entire overlay when enabled is false\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.updateOptions({ theme: { enabled: false } });\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      const isSelectionBoxVisible = await reactGrab.isSelectionBoxVisible();\n      expect(isSelectionBoxVisible).toBe(false);\n    });\n  });\n\n  test.describe(\"Theme Persistence\", () => {\n    test(\"theme should persist across activation cycles\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.updateOptions({ theme: { hue: 120 } });\n\n      await reactGrab.activate();\n      await reactGrab.deactivate();\n      await reactGrab.activate();\n\n      const hasFilter = await reactGrab.page.evaluate(() => {\n        const host = document.querySelector(\"[data-react-grab]\");\n        const shadowRoot = host?.shadowRoot;\n        const root = shadowRoot?.querySelector(\n          \"[data-react-grab]\",\n        ) as HTMLElement;\n        return root?.style.filter?.includes(\"hue-rotate(120deg)\") ?? false;\n      });\n      expect(hasFilter).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/toggle-position-stability.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\nimport type { ReactGrabPageObject } from \"./fixtures.js\";\n\nconst POSITION_TOLERANCE_PX = 3;\nconst TOGGLE_ANIMATION_SETTLE_MS = 300;\n\nconst getToggleButtonCenter = async (reactGrab: ReactGrabPageObject) => {\n  return reactGrab.page.evaluate((attrName) => {\n    const host = document.querySelector(`[${attrName}]`);\n    const shadowRoot = host?.shadowRoot;\n    if (!shadowRoot) return null;\n    const root = shadowRoot.querySelector(`[${attrName}]`);\n    if (!root) return null;\n    const button = root.querySelector<HTMLButtonElement>(\n      \"[data-react-grab-toolbar-enabled]\",\n    );\n    if (!button) return null;\n    const rect = button.getBoundingClientRect();\n    return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };\n  }, \"data-react-grab\");\n};\n\nconst seedToolbarEdge = async (\n  page: import(\"@playwright/test\").Page,\n  edge: \"top\" | \"bottom\" | \"left\" | \"right\",\n  enabled = true,\n) => {\n  await page.evaluate(\n    ({ edge: savedEdge, enabled: savedEnabled }) => {\n      localStorage.setItem(\n        \"react-grab-toolbar-state\",\n        JSON.stringify({\n          edge: savedEdge,\n          ratio: 0.5,\n          collapsed: false,\n          enabled: savedEnabled,\n        }),\n      );\n    },\n    { edge, enabled },\n  );\n  await page.reload();\n  await page.waitForLoadState(\"domcontentloaded\");\n};\n\nconst copyElement = async (\n  reactGrab: ReactGrabPageObject,\n  selector: string,\n) => {\n  await reactGrab.activate();\n  await reactGrab.hoverElement(selector);\n  await reactGrab.waitForSelectionBox();\n  await reactGrab.clickElement(selector);\n  await expect\n    .poll(() => reactGrab.getClipboardContent(), { timeout: 5000 })\n    .toBeTruthy();\n  // HACK: Wait for copy feedback transition and history item addition\n  await reactGrab.page.waitForTimeout(300);\n};\n\nconst expectPositionStable = (\n  beforePosition: { x: number; y: number },\n  afterPosition: { x: number; y: number },\n) => {\n  expect(Math.abs(afterPosition.x - beforePosition.x)).toBeLessThan(\n    POSITION_TOLERANCE_PX,\n  );\n  expect(Math.abs(afterPosition.y - beforePosition.y)).toBeLessThan(\n    POSITION_TOLERANCE_PX,\n  );\n};\n\nconst waitForToolbarReady = async (reactGrab: ReactGrabPageObject) => {\n  await expect\n    .poll(() => reactGrab.isToolbarVisible(), { timeout: 3000 })\n    .toBe(true);\n  // HACK: Wait for toolbar fade-in animation to complete\n  await reactGrab.page.waitForTimeout(600);\n};\n\ntest.describe(\"Toggle Position Stability\", () => {\n  test.beforeEach(async ({ reactGrab }) => {\n    await reactGrab.page.evaluate(() => {\n      localStorage.removeItem(\"react-grab-toolbar-state\");\n    });\n    await reactGrab.page.reload();\n    await reactGrab.page.waitForLoadState(\"domcontentloaded\");\n    await waitForToolbarReady(reactGrab);\n  });\n\n  test.describe(\"Horizontal Layout\", () => {\n    test(\"toggle should stay in place when disabling on bottom edge\", async ({\n      reactGrab,\n    }) => {\n      const beforeToggle = await getToggleButtonCenter(reactGrab);\n      expect(beforeToggle).not.toBeNull();\n\n      await reactGrab.clickToolbarEnabled();\n      // HACK: Wait for toggle animation to settle\n      await reactGrab.page.waitForTimeout(TOGGLE_ANIMATION_SETTLE_MS);\n\n      const afterToggle = await getToggleButtonCenter(reactGrab);\n      expect(afterToggle).not.toBeNull();\n      expectPositionStable(beforeToggle!, afterToggle!);\n    });\n\n    test(\"toggle should stay in place when re-enabling on bottom edge\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.clickToolbarEnabled();\n      // HACK: Wait for toggle animation to settle\n      await reactGrab.page.waitForTimeout(TOGGLE_ANIMATION_SETTLE_MS);\n\n      const beforeReEnable = await getToggleButtonCenter(reactGrab);\n      expect(beforeReEnable).not.toBeNull();\n\n      await reactGrab.clickToolbarEnabled();\n      // HACK: Wait for toggle animation to settle\n      await reactGrab.page.waitForTimeout(TOGGLE_ANIMATION_SETTLE_MS);\n\n      const afterReEnable = await getToggleButtonCenter(reactGrab);\n      expect(afterReEnable).not.toBeNull();\n      expectPositionStable(beforeReEnable!, afterReEnable!);\n    });\n\n    test(\"toggle should return to same position after full cycle on bottom edge\", async ({\n      reactGrab,\n    }) => {\n      const initialPosition = await getToggleButtonCenter(reactGrab);\n      expect(initialPosition).not.toBeNull();\n\n      await reactGrab.clickToolbarEnabled();\n      // HACK: Wait for toggle animation to settle\n      await reactGrab.page.waitForTimeout(TOGGLE_ANIMATION_SETTLE_MS);\n      await reactGrab.clickToolbarEnabled();\n      // HACK: Wait for toggle animation to settle\n      await reactGrab.page.waitForTimeout(TOGGLE_ANIMATION_SETTLE_MS);\n\n      const afterCycle = await getToggleButtonCenter(reactGrab);\n      expect(afterCycle).not.toBeNull();\n      expectPositionStable(initialPosition!, afterCycle!);\n    });\n\n    test(\"toggle should stay in place when toggling on top edge\", async ({\n      reactGrab,\n    }) => {\n      await seedToolbarEdge(reactGrab.page, \"top\");\n      await waitForToolbarReady(reactGrab);\n\n      const beforeToggle = await getToggleButtonCenter(reactGrab);\n      expect(beforeToggle).not.toBeNull();\n\n      await reactGrab.clickToolbarEnabled();\n      // HACK: Wait for toggle animation to settle\n      await reactGrab.page.waitForTimeout(TOGGLE_ANIMATION_SETTLE_MS);\n\n      const afterToggle = await getToggleButtonCenter(reactGrab);\n      expect(afterToggle).not.toBeNull();\n      expectPositionStable(beforeToggle!, afterToggle!);\n    });\n  });\n\n  test.describe(\"Vertical Layout\", () => {\n    test(\"toggle should stay in place when toggling on right edge\", async ({\n      reactGrab,\n    }) => {\n      await seedToolbarEdge(reactGrab.page, \"right\");\n      await waitForToolbarReady(reactGrab);\n\n      const beforeToggle = await getToggleButtonCenter(reactGrab);\n      expect(beforeToggle).not.toBeNull();\n\n      await reactGrab.clickToolbarEnabled();\n      // HACK: Wait for toggle animation to settle\n      await reactGrab.page.waitForTimeout(TOGGLE_ANIMATION_SETTLE_MS);\n\n      const afterToggle = await getToggleButtonCenter(reactGrab);\n      expect(afterToggle).not.toBeNull();\n      expectPositionStable(beforeToggle!, afterToggle!);\n    });\n\n    test(\"toggle should stay in place when toggling on left edge\", async ({\n      reactGrab,\n    }) => {\n      await seedToolbarEdge(reactGrab.page, \"left\");\n      await waitForToolbarReady(reactGrab);\n\n      const beforeToggle = await getToggleButtonCenter(reactGrab);\n      expect(beforeToggle).not.toBeNull();\n\n      await reactGrab.clickToolbarEnabled();\n      // HACK: Wait for toggle animation to settle\n      await reactGrab.page.waitForTimeout(TOGGLE_ANIMATION_SETTLE_MS);\n\n      const afterToggle = await getToggleButtonCenter(reactGrab);\n      expect(afterToggle).not.toBeNull();\n      expectPositionStable(beforeToggle!, afterToggle!);\n    });\n  });\n\n  test.describe(\"First Enable\", () => {\n    test(\"first enable on bottom edge should not cause position jump\", async ({\n      reactGrab,\n    }) => {\n      await seedToolbarEdge(reactGrab.page, \"bottom\", false);\n      await waitForToolbarReady(reactGrab);\n\n      const beforeFirstEnable = await getToggleButtonCenter(reactGrab);\n      expect(beforeFirstEnable).not.toBeNull();\n\n      await reactGrab.clickToolbarEnabled();\n      // HACK: Wait for toggle animation to settle\n      await reactGrab.page.waitForTimeout(TOGGLE_ANIMATION_SETTLE_MS);\n\n      const afterFirstEnable = await getToggleButtonCenter(reactGrab);\n      expect(afterFirstEnable).not.toBeNull();\n      expectPositionStable(beforeFirstEnable!, afterFirstEnable!);\n    });\n\n    test(\"first enable on top edge should not cause position jump\", async ({\n      reactGrab,\n    }) => {\n      await seedToolbarEdge(reactGrab.page, \"top\", false);\n      await waitForToolbarReady(reactGrab);\n\n      const beforeFirstEnable = await getToggleButtonCenter(reactGrab);\n      expect(beforeFirstEnable).not.toBeNull();\n\n      await reactGrab.clickToolbarEnabled();\n      // HACK: Wait for toggle animation to settle\n      await reactGrab.page.waitForTimeout(TOGGLE_ANIMATION_SETTLE_MS);\n\n      const afterFirstEnable = await getToggleButtonCenter(reactGrab);\n      expect(afterFirstEnable).not.toBeNull();\n      expectPositionStable(beforeFirstEnable!, afterFirstEnable!);\n    });\n  });\n\n  test.describe(\"Position Drift Prevention\", () => {\n    test(\"should not drift after history button appears then disappears\", async ({\n      reactGrab,\n    }) => {\n      await copyElement(reactGrab, \"li:first-child\");\n      await expect\n        .poll(() => reactGrab.isHistoryButtonVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      const withHistoryPosition = await getToggleButtonCenter(reactGrab);\n      expect(withHistoryPosition).not.toBeNull();\n\n      await reactGrab.clickToolbarEnabled();\n      // HACK: Wait for toggle animation to settle\n      await reactGrab.page.waitForTimeout(TOGGLE_ANIMATION_SETTLE_MS);\n      await reactGrab.clickToolbarEnabled();\n      // HACK: Wait for toggle animation to settle\n      await reactGrab.page.waitForTimeout(TOGGLE_ANIMATION_SETTLE_MS);\n\n      const afterCycleWithHistory = await getToggleButtonCenter(reactGrab);\n      expect(afterCycleWithHistory).not.toBeNull();\n      expectPositionStable(withHistoryPosition!, afterCycleWithHistory!);\n\n      await reactGrab.clickHistoryButton();\n      await reactGrab.clickHistoryClear();\n      await expect\n        .poll(() => reactGrab.isHistoryButtonVisible(), { timeout: 2000 })\n        .toBe(false);\n      // HACK: Wait for history button hide animation\n      await reactGrab.page.waitForTimeout(200);\n\n      const withoutHistoryPosition = await getToggleButtonCenter(reactGrab);\n      expect(withoutHistoryPosition).not.toBeNull();\n\n      await reactGrab.clickToolbarEnabled();\n      // HACK: Wait for toggle animation to settle\n      await reactGrab.page.waitForTimeout(TOGGLE_ANIMATION_SETTLE_MS);\n      await reactGrab.clickToolbarEnabled();\n      // HACK: Wait for toggle animation to settle\n      await reactGrab.page.waitForTimeout(TOGGLE_ANIMATION_SETTLE_MS);\n\n      const afterCycleWithoutHistory = await getToggleButtonCenter(reactGrab);\n      expect(afterCycleWithoutHistory).not.toBeNull();\n      expectPositionStable(withoutHistoryPosition!, afterCycleWithoutHistory!);\n    });\n\n    test(\"should not accumulate drift over multiple toggle cycles\", async ({\n      reactGrab,\n    }) => {\n      const initialPosition = await getToggleButtonCenter(reactGrab);\n      expect(initialPosition).not.toBeNull();\n\n      for (let cycleIndex = 0; cycleIndex < 5; cycleIndex++) {\n        await reactGrab.clickToolbarEnabled();\n        // HACK: Wait for toggle animation to settle\n        await reactGrab.page.waitForTimeout(TOGGLE_ANIMATION_SETTLE_MS);\n        await reactGrab.clickToolbarEnabled();\n        // HACK: Wait for toggle animation to settle\n        await reactGrab.page.waitForTimeout(TOGGLE_ANIMATION_SETTLE_MS);\n      }\n\n      const finalPosition = await getToggleButtonCenter(reactGrab);\n      expect(finalPosition).not.toBeNull();\n      expectPositionStable(initialPosition!, finalPosition!);\n    });\n\n    test(\"should not drift on vertical edge after history changes\", async ({\n      reactGrab,\n    }) => {\n      await seedToolbarEdge(reactGrab.page, \"right\");\n      await waitForToolbarReady(reactGrab);\n\n      await copyElement(reactGrab, \"li:first-child\");\n      await expect\n        .poll(() => reactGrab.isHistoryButtonVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      const beforeCyclePosition = await getToggleButtonCenter(reactGrab);\n      expect(beforeCyclePosition).not.toBeNull();\n\n      await reactGrab.clickToolbarEnabled();\n      // HACK: Wait for toggle animation to settle\n      await reactGrab.page.waitForTimeout(TOGGLE_ANIMATION_SETTLE_MS);\n      await reactGrab.clickToolbarEnabled();\n      // HACK: Wait for toggle animation to settle\n      await reactGrab.page.waitForTimeout(TOGGLE_ANIMATION_SETTLE_MS);\n\n      const afterCyclePosition = await getToggleButtonCenter(reactGrab);\n      expect(afterCyclePosition).not.toBeNull();\n      expectPositionStable(beforeCyclePosition!, afterCyclePosition!);\n\n      await reactGrab.clickHistoryButton();\n      await reactGrab.clickHistoryClear();\n      await expect\n        .poll(() => reactGrab.isHistoryButtonVisible(), { timeout: 2000 })\n        .toBe(false);\n      // HACK: Wait for history button hide animation\n      await reactGrab.page.waitForTimeout(200);\n\n      const afterClearPosition = await getToggleButtonCenter(reactGrab);\n      expect(afterClearPosition).not.toBeNull();\n\n      await reactGrab.clickToolbarEnabled();\n      // HACK: Wait for toggle animation to settle\n      await reactGrab.page.waitForTimeout(TOGGLE_ANIMATION_SETTLE_MS);\n      await reactGrab.clickToolbarEnabled();\n      // HACK: Wait for toggle animation to settle\n      await reactGrab.page.waitForTimeout(TOGGLE_ANIMATION_SETTLE_MS);\n\n      const afterClearCyclePosition = await getToggleButtonCenter(reactGrab);\n      expect(afterClearCyclePosition).not.toBeNull();\n      expectPositionStable(afterClearPosition!, afterClearCyclePosition!);\n    });\n  });\n\n  test.describe(\"Rapid Toggle\", () => {\n    test(\"rapid toggles should maintain toolbar visibility and state\", async ({\n      reactGrab,\n    }) => {\n      for (let toggleIndex = 0; toggleIndex < 6; toggleIndex++) {\n        await reactGrab.clickToolbarEnabled();\n        // HACK: Brief pause between rapid toggles\n        await reactGrab.page.waitForTimeout(50);\n      }\n\n      // HACK: Wait for all toggle animations to fully settle\n      await reactGrab.page.waitForTimeout(TOGGLE_ANIMATION_SETTLE_MS * 2);\n\n      const toolbarInfo = await reactGrab.getToolbarInfo();\n      expect(toolbarInfo.isVisible).toBe(true);\n      expect(toolbarInfo.position).not.toBeNull();\n\n      const togglePosition = await getToggleButtonCenter(reactGrab);\n      expect(togglePosition).not.toBeNull();\n    });\n\n    test(\"position should stabilize after rapid toggles settle\", async ({\n      reactGrab,\n    }) => {\n      for (let toggleIndex = 0; toggleIndex < 6; toggleIndex++) {\n        await reactGrab.clickToolbarEnabled();\n        // HACK: Brief pause between rapid toggles\n        await reactGrab.page.waitForTimeout(50);\n      }\n\n      // HACK: Wait for all toggle animations to fully settle\n      await reactGrab.page.waitForTimeout(TOGGLE_ANIMATION_SETTLE_MS * 2);\n\n      const settledPosition = await getToggleButtonCenter(reactGrab);\n      expect(settledPosition).not.toBeNull();\n\n      await reactGrab.clickToolbarEnabled();\n      // HACK: Wait for toggle animation to settle\n      await reactGrab.page.waitForTimeout(TOGGLE_ANIMATION_SETTLE_MS);\n      await reactGrab.clickToolbarEnabled();\n      // HACK: Wait for toggle animation to settle\n      await reactGrab.page.waitForTimeout(TOGGLE_ANIMATION_SETTLE_MS);\n\n      const afterNormalCycle = await getToggleButtonCenter(reactGrab);\n      expect(afterNormalCycle).not.toBeNull();\n      expectPositionStable(settledPosition!, afterNormalCycle!);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/toolbar-menu.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\ntest.describe(\"Toolbar Menu\", () => {\n  test.describe(\"Visibility\", () => {\n    test(\"menu button should be visible when toolbar actions are registered\", async ({\n      reactGrab,\n    }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await expect\n        .poll(() => reactGrab.isToolbarMenuButtonVisible(), { timeout: 2000 })\n        .toBe(true);\n    });\n\n    test(\"menu dropdown should not be visible by default\", async ({\n      reactGrab,\n    }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      const isMenuVisible = await reactGrab.isToolbarMenuVisible();\n      expect(isMenuVisible).toBe(false);\n    });\n\n    test(\"menu button should be hidden when toolbar is disabled\", async ({\n      reactGrab,\n    }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.clickToolbarEnabled();\n      await reactGrab.page.waitForTimeout(200);\n\n      const isMenuButtonVisible = await reactGrab.isToolbarMenuButtonVisible();\n      expect(isMenuButtonVisible).toBe(false);\n    });\n  });\n\n  test.describe(\"Open and Close\", () => {\n    test(\"clicking menu button should open the menu\", async ({ reactGrab }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.clickToolbarMenuButton();\n\n      const isMenuVisible = await reactGrab.isToolbarMenuVisible();\n      expect(isMenuVisible).toBe(true);\n    });\n\n    test(\"clicking menu button again should close the menu\", async ({\n      reactGrab,\n    }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.clickToolbarMenuButton();\n      await reactGrab.clickToolbarMenuButton();\n\n      const isMenuVisible = await reactGrab.isToolbarMenuVisible();\n      expect(isMenuVisible).toBe(false);\n    });\n\n    test(\"pressing Escape should close the menu\", async ({ reactGrab }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.clickToolbarMenuButton();\n      await expect\n        .poll(() => reactGrab.isToolbarMenuVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.pressEscape();\n      await reactGrab.page.waitForTimeout(200);\n\n      const isMenuVisible = await reactGrab.isToolbarMenuVisible();\n      expect(isMenuVisible).toBe(false);\n    });\n  });\n\n  test.describe(\"Menu Items\", () => {\n    test(\"menu should display registered toolbar actions\", async ({\n      reactGrab,\n    }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.clickToolbarMenuButton();\n\n      const menuInfo = await reactGrab.getToolbarMenuInfo();\n      expect(menuInfo.isVisible).toBe(true);\n      expect(menuInfo.itemCount).toBeGreaterThan(0);\n      expect(menuInfo.itemLabels.length).toBeGreaterThan(0);\n    });\n  });\n\n  test.describe(\"Interaction with Other Dropdowns\", () => {\n    test(\"opening context menu should dismiss toolbar menu\", async ({\n      reactGrab,\n    }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.clickToolbarMenuButton();\n      await expect\n        .poll(() => reactGrab.isToolbarMenuVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.rightClickElement(\"li:first-child\");\n\n      await expect\n        .poll(() => reactGrab.isToolbarMenuVisible(), { timeout: 2000 })\n        .toBe(false);\n    });\n\n    test(\"opening toolbar menu should dismiss history dropdown\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.clickElement(\"li:first-child\");\n      await reactGrab.page.waitForTimeout(300);\n\n      await expect\n        .poll(() => reactGrab.isHistoryButtonVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.clickHistoryButton();\n      await expect\n        .poll(() => reactGrab.isHistoryDropdownVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.clickToolbarMenuButton();\n\n      await expect\n        .poll(() => reactGrab.isHistoryDropdownVisible(), { timeout: 2000 })\n        .toBe(false);\n      await expect\n        .poll(() => reactGrab.isToolbarMenuVisible(), { timeout: 2000 })\n        .toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/toolbar-selection-hover.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\nconst ATTRIBUTE_NAME = \"data-react-grab\";\n\nconst hoverToolbar = async (page: import(\"@playwright/test\").Page) => {\n  const toolbarRect = await page.evaluate((attrName) => {\n    const host = document.querySelector(`[${attrName}]`);\n    const shadowRoot = host?.shadowRoot;\n    if (!shadowRoot) return null;\n    const root = shadowRoot.querySelector(`[${attrName}]`);\n    if (!root) return null;\n    const toolbar = root.querySelector<HTMLElement>(\n      \"[data-react-grab-toolbar]\",\n    );\n    if (!toolbar) return null;\n    const rect = toolbar.getBoundingClientRect();\n    return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };\n  }, ATTRIBUTE_NAME);\n\n  if (!toolbarRect) throw new Error(\"Toolbar not found\");\n\n  await page.mouse.move(\n    toolbarRect.x + toolbarRect.width / 2,\n    toolbarRect.y + toolbarRect.height / 2,\n  );\n  await page.waitForTimeout(150);\n};\n\nconst hoverAwayFromToolbar = async (page: import(\"@playwright/test\").Page) => {\n  await page.mouse.move(10, 10);\n  await page.waitForTimeout(150);\n};\n\ntest.describe(\"Toolbar Selection Hover\", () => {\n  test.describe(\"Selection Mode\", () => {\n    test(\"should hide selection box when hovering toolbar\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li\");\n      await reactGrab.waitForSelectionBox();\n\n      await expect\n        .poll(() => reactGrab.isSelectionBoxVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await hoverToolbar(reactGrab.page);\n\n      await expect\n        .poll(() => reactGrab.isSelectionBoxVisible(), { timeout: 2000 })\n        .toBe(false);\n    });\n\n    test(\"should hide selection label when hovering toolbar\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li\");\n      await reactGrab.waitForSelectionBox();\n\n      await expect\n        .poll(() => reactGrab.isSelectionLabelVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await hoverToolbar(reactGrab.page);\n\n      await expect\n        .poll(() => reactGrab.isSelectionLabelVisible(), { timeout: 2000 })\n        .toBe(false);\n    });\n\n    test(\"should restore selection after moving mouse back from toolbar\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li\");\n      await reactGrab.waitForSelectionBox();\n\n      await hoverToolbar(reactGrab.page);\n\n      await expect\n        .poll(() => reactGrab.isSelectionBoxVisible(), { timeout: 2000 })\n        .toBe(false);\n\n      await reactGrab.hoverElement(\"li\");\n\n      await expect\n        .poll(() => reactGrab.isSelectionBoxVisible(), { timeout: 2000 })\n        .toBe(true);\n    });\n  });\n\n  test.describe(\"Frozen Mode\", () => {\n    test(\"should keep selection box visible when hovering toolbar after right-click freeze\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.rightClickElement(\"li\");\n\n      await expect\n        .poll(() => reactGrab.isSelectionBoxVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await hoverToolbar(reactGrab.page);\n\n      await expect\n        .poll(() => reactGrab.isSelectionBoxVisible(), { timeout: 2000 })\n        .toBe(true);\n    });\n\n    test(\"should keep selection box visible after context menu dismiss and toolbar hover\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.rightClickElement(\"li\");\n\n      await expect\n        .poll(() => reactGrab.isContextMenuVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await expect\n        .poll(() => reactGrab.isSelectionBoxVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await hoverToolbar(reactGrab.page);\n\n      await expect\n        .poll(() => reactGrab.isSelectionBoxVisible(), { timeout: 2000 })\n        .toBe(true);\n    });\n\n    test(\"selection box should not flicker when moving between frozen element and toolbar\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.rightClickElement(\"li\");\n\n      for (let hoverIndex = 0; hoverIndex < 3; hoverIndex++) {\n        await hoverToolbar(reactGrab.page);\n        await expect\n          .poll(() => reactGrab.isSelectionBoxVisible(), { timeout: 2000 })\n          .toBe(true);\n\n        await hoverAwayFromToolbar(reactGrab.page);\n        await expect\n          .poll(() => reactGrab.isSelectionBoxVisible(), { timeout: 2000 })\n          .toBe(true);\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/toolbar.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\ntest.describe(\"Toolbar\", () => {\n  test.describe(\"Visibility\", () => {\n    test(\"toolbar should be visible after initial load\", async ({\n      reactGrab,\n    }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n    });\n\n    test(\"toolbar should fade in after delay\", async ({ reactGrab }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n    });\n\n    test(\"toolbar should be visible on mobile viewport after reload\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setViewportSize(375, 667);\n      await reactGrab.page.reload();\n      await reactGrab.page.waitForLoadState(\"domcontentloaded\");\n      await reactGrab.page.waitForFunction(\n        () =>\n          (window as { __REACT_GRAB__?: unknown }).__REACT_GRAB__ !== undefined,\n        { timeout: 10000 },\n      );\n\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 3000 })\n        .toBe(true);\n\n      await reactGrab.setViewportSize(1280, 720);\n    });\n\n    test(\"toolbar should remain visible through viewport resize cycles\", async ({\n      reactGrab,\n    }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.setViewportSize(375, 667);\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.setViewportSize(1280, 720);\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n    });\n  });\n\n  test.describe(\"Toggle Activation\", () => {\n    test(\"clicking toolbar toggle should activate overlay\", async ({\n      reactGrab,\n    }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.clickToolbarToggle();\n\n      const isActive = await reactGrab.isOverlayVisible();\n      expect(isActive).toBe(true);\n    });\n\n    test(\"clicking toolbar toggle again should deactivate overlay\", async ({\n      reactGrab,\n    }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.clickToolbarToggle();\n      await reactGrab.clickToolbarToggle();\n\n      const isActive = await reactGrab.isOverlayVisible();\n      expect(isActive).toBe(false);\n    });\n\n    test(\"toolbar toggle should reflect current activation state\", async ({\n      reactGrab,\n    }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.activate();\n\n      const toolbarInfo = await reactGrab.getToolbarInfo();\n      expect(toolbarInfo.isVisible).toBe(true);\n    });\n  });\n\n  test.describe(\"Collapse/Expand\", () => {\n    test(\"clicking collapse button should collapse toolbar\", async ({\n      reactGrab,\n    }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.clickToolbarCollapse();\n\n      await expect\n        .poll(() => reactGrab.isToolbarCollapsed(), { timeout: 2000 })\n        .toBe(true);\n    });\n\n    test(\"clicking collapsed toolbar should expand it\", async ({\n      reactGrab,\n    }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.clickToolbarCollapse();\n      await expect\n        .poll(() => reactGrab.isToolbarCollapsed(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.page.evaluate((attrName) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        const toolbar = root?.querySelector<HTMLElement>(\n          \"[data-react-grab-toolbar]\",\n        );\n        const innerDiv = toolbar?.querySelector(\"div\");\n        innerDiv?.click();\n      }, \"data-react-grab\");\n\n      await expect\n        .poll(() => reactGrab.isToolbarCollapsed(), { timeout: 2000 })\n        .toBe(false);\n    });\n\n    test(\"collapsed toolbar should not allow activation toggle\", async ({\n      reactGrab,\n    }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.clickToolbarCollapse();\n      await expect\n        .poll(() => reactGrab.isToolbarCollapsed(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.clickToolbarToggle();\n\n      const isActive = await reactGrab.isOverlayVisible();\n      const isCollapsed = await reactGrab.isToolbarCollapsed();\n\n      expect(isCollapsed || !isActive).toBe(true);\n    });\n  });\n\n  test.describe(\"Dragging\", () => {\n    test.beforeEach(async ({ reactGrab }) => {\n      await reactGrab.page.evaluate(() => {\n        localStorage.removeItem(\"react-grab-toolbar-state\");\n      });\n      await reactGrab.page.reload();\n      await reactGrab.page.waitForLoadState(\"domcontentloaded\");\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 3000 })\n        .toBe(true);\n      // HACK: Wait for toolbar fade-in animation to complete\n      await reactGrab.page.waitForTimeout(600);\n    });\n\n    test(\"should be draggable\", async ({ reactGrab }) => {\n      const initialInfo = await reactGrab.getToolbarInfo();\n      const initialPosition = initialInfo.position;\n      expect(initialPosition).not.toBeNull();\n\n      await reactGrab.dragToolbar(100, 0);\n\n      await expect\n        .poll(\n          async () => {\n            const info = await reactGrab.getToolbarInfo();\n            if (!info.position || !initialPosition) return 0;\n            return Math.abs(info.position.x - initialPosition.x);\n          },\n          { timeout: 3000 },\n        )\n        .toBeGreaterThan(0);\n    });\n\n    test(\"should snap to edges after drag\", async ({ reactGrab }) => {\n      await reactGrab.dragToolbar(500, 0);\n\n      const info = await reactGrab.getToolbarInfo();\n      expect(info.snapEdge).toBeDefined();\n    });\n\n    test(\"should snap to top edge\", async ({ reactGrab }) => {\n      await reactGrab.dragToolbar(0, -500);\n\n      await expect\n        .poll(\n          async () => {\n            const info = await reactGrab.getToolbarInfo();\n            return info.snapEdge;\n          },\n          { timeout: 3000 },\n        )\n        .toBe(\"top\");\n    });\n\n    test(\"should snap to left edge\", async ({ reactGrab }) => {\n      await reactGrab.dragToolbar(-1000, -500);\n\n      await expect\n        .poll(\n          async () => {\n            const info = await reactGrab.getToolbarInfo();\n            return info.snapEdge;\n          },\n          { timeout: 3000 },\n        )\n        .toMatch(/^(left|top)$/);\n    });\n\n    test(\"should snap to right edge\", async ({ reactGrab }) => {\n      await reactGrab.dragToolbar(1500, -500);\n\n      await expect\n        .poll(\n          async () => {\n            const info = await reactGrab.getToolbarInfo();\n            return info.snapEdge;\n          },\n          { timeout: 3000 },\n        )\n        .toMatch(/^(right|top)$/);\n    });\n\n    test(\"should not drag when collapsed\", async ({ reactGrab }) => {\n      await reactGrab.clickToolbarCollapse();\n      await expect\n        .poll(() => reactGrab.isToolbarCollapsed(), { timeout: 2000 })\n        .toBe(true);\n\n      const initialInfo = await reactGrab.getToolbarInfo();\n      const initialPosition = initialInfo.position;\n\n      await reactGrab.dragToolbar(100, 100);\n\n      const finalInfo = await reactGrab.getToolbarInfo();\n      const finalPosition = finalInfo.position;\n\n      if (initialPosition && finalPosition) {\n        expect(Math.abs(finalPosition.x - initialPosition.x)).toBeLessThan(20);\n        expect(Math.abs(finalPosition.y - initialPosition.y)).toBeLessThan(20);\n      }\n    });\n\n    test(\"should be draggable from select button\", async ({ reactGrab }) => {\n      const initialInfo = await reactGrab.getToolbarInfo();\n      const initialPosition = initialInfo.position;\n      expect(initialPosition).not.toBeNull();\n\n      await reactGrab.dragToolbarFromButton(\n        \"[data-react-grab-toolbar-toggle]\",\n        100,\n        0,\n      );\n\n      await expect\n        .poll(\n          async () => {\n            const info = await reactGrab.getToolbarInfo();\n            if (!info.position || !initialPosition) return 0;\n            return Math.abs(info.position.x - initialPosition.x);\n          },\n          { timeout: 3000 },\n        )\n        .toBeGreaterThan(0);\n    });\n\n    test(\"should not close page dropdown when clicking select button\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.openDropdown();\n      expect(await reactGrab.isDropdownOpen()).toBe(true);\n\n      await reactGrab.clickToolbarToggle();\n\n      expect(await reactGrab.isDropdownOpen()).toBe(true);\n    });\n\n    test(\"should not close page dropdown when dragging from select button\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.openDropdown();\n      expect(await reactGrab.isDropdownOpen()).toBe(true);\n\n      await reactGrab.dragToolbarFromButton(\n        \"[data-react-grab-toolbar-toggle]\",\n        50,\n        0,\n      );\n\n      expect(await reactGrab.isDropdownOpen()).toBe(true);\n    });\n  });\n\n  test.describe(\"State Persistence\", () => {\n    test(\"toolbar position should persist across page reloads\", async ({\n      reactGrab,\n    }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.dragToolbar(200, -200);\n      // HACK: Wait for snap animation\n      await reactGrab.page.waitForTimeout(200);\n\n      const positionBeforeReload = await reactGrab.getToolbarInfo();\n\n      await reactGrab.page.reload();\n      await reactGrab.page.waitForLoadState(\"domcontentloaded\");\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      const positionAfterReload = await reactGrab.getToolbarInfo();\n\n      if (positionBeforeReload.snapEdge && positionAfterReload.snapEdge) {\n        expect(positionAfterReload.snapEdge).toBe(\n          positionBeforeReload.snapEdge,\n        );\n      }\n    });\n\n    test(\"collapsed state should persist across page reloads\", async ({\n      reactGrab,\n    }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.clickToolbarCollapse();\n      await expect\n        .poll(() => reactGrab.isToolbarCollapsed(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.page.reload();\n      await reactGrab.page.waitForLoadState(\"domcontentloaded\");\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await expect\n        .poll(() => reactGrab.isToolbarCollapsed(), { timeout: 2000 })\n        .toBe(true);\n    });\n  });\n\n  test.describe(\"Chevron Rotation\", () => {\n    test.beforeEach(async ({ reactGrab }) => {\n      await reactGrab.page.evaluate(() => {\n        localStorage.removeItem(\"react-grab-toolbar-state\");\n      });\n      await reactGrab.page.reload();\n      await reactGrab.page.waitForLoadState(\"domcontentloaded\");\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 3000 })\n        .toBe(true);\n      // HACK: Wait for toolbar fade-in animation to complete\n      await reactGrab.page.waitForTimeout(600);\n    });\n\n    test(\"chevron should rotate based on snap edge\", async ({ reactGrab }) => {\n      await reactGrab.dragToolbar(0, -500);\n\n      await expect\n        .poll(\n          async () => {\n            const info = await reactGrab.getToolbarInfo();\n            return info.snapEdge;\n          },\n          { timeout: 3000 },\n        )\n        .toBe(\"top\");\n\n      // HACK: Need extra delay for snap animation before next drag\n      await reactGrab.page.waitForTimeout(300);\n\n      await reactGrab.dragToolbar(0, 800);\n\n      await expect\n        .poll(\n          async () => {\n            const info = await reactGrab.getToolbarInfo();\n            return info.snapEdge;\n          },\n          { timeout: 3000 },\n        )\n        .toBe(\"bottom\");\n    });\n  });\n\n  test.describe(\"Viewport Resize Handling\", () => {\n    test(\"toolbar should recalculate position on viewport resize\", async ({\n      reactGrab,\n    }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.setViewportSize(1920, 1080);\n\n      await expect\n        .poll(\n          async () => {\n            const info = await reactGrab.getToolbarInfo();\n            return info.isVisible;\n          },\n          { timeout: 2000 },\n        )\n        .toBe(true);\n\n      await reactGrab.setViewportSize(1280, 720);\n    });\n\n    test(\"toolbar should remain visible after rapid resize\", async ({\n      reactGrab,\n    }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      for (let i = 0; i < 3; i++) {\n        await reactGrab.setViewportSize(1000 + i * 100, 700 + i * 50);\n      }\n\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.setViewportSize(1280, 720);\n    });\n  });\n\n  test.describe(\"Edge Cases\", () => {\n    test(\"toolbar should handle very small viewport\", async ({ reactGrab }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.setViewportSize(320, 480);\n\n      const isVisible = await reactGrab.isToolbarVisible();\n      expect(typeof isVisible).toBe(\"boolean\");\n\n      await reactGrab.setViewportSize(1280, 720);\n    });\n\n    test(\"toolbar should handle rapid collapse/expand\", async ({\n      reactGrab,\n    }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      for (let i = 0; i < 5; i++) {\n        await reactGrab.clickToolbarCollapse();\n      }\n\n      const info = await reactGrab.getToolbarInfo();\n      expect(info.isVisible).toBe(true);\n    });\n\n    test(\"toolbar should maintain position ratio on resize\", async ({\n      reactGrab,\n    }) => {\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.dragToolbar(-200, 0);\n\n      await reactGrab.setViewportSize(800, 600);\n\n      await expect\n        .poll(\n          async () => {\n            const info = await reactGrab.getToolbarInfo();\n            return info.isVisible;\n          },\n          { timeout: 2000 },\n        )\n        .toBe(true);\n\n      await reactGrab.setViewportSize(1280, 720);\n    });\n  });\n\n  test.describe(\"Vertical Layout\", () => {\n    const seedVerticalState = async (\n      page: import(\"@playwright/test\").Page,\n      edge: \"left\" | \"right\",\n    ) => {\n      await page.evaluate(\n        ({ edge: savedEdge }) => {\n          localStorage.setItem(\n            \"react-grab-toolbar-state\",\n            JSON.stringify({\n              edge: savedEdge,\n              ratio: 0.5,\n              collapsed: false,\n              enabled: true,\n            }),\n          );\n        },\n        { edge },\n      );\n      await page.reload();\n      await page.waitForLoadState(\"domcontentloaded\");\n    };\n\n    test.beforeEach(async ({ reactGrab }) => {\n      await reactGrab.page.evaluate(() => {\n        localStorage.removeItem(\"react-grab-toolbar-state\");\n      });\n      await reactGrab.page.reload();\n      await reactGrab.page.waitForLoadState(\"domcontentloaded\");\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 3000 })\n        .toBe(true);\n      // HACK: Wait for toolbar fade-in animation to complete\n      await reactGrab.page.waitForTimeout(600);\n    });\n\n    test(\"should render vertically when snapped to right edge\", async ({\n      reactGrab,\n    }) => {\n      await seedVerticalState(reactGrab.page, \"right\");\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 3000 })\n        .toBe(true);\n\n      const info = await reactGrab.getToolbarInfo();\n      expect(info.snapEdge).toBe(\"right\");\n      expect(info.isVertical).toBe(true);\n      expect(info.dimensions).not.toBeNull();\n      expect(info.dimensions!.height).toBeGreaterThan(info.dimensions!.width);\n    });\n\n    test(\"should render vertically when snapped to left edge\", async ({\n      reactGrab,\n    }) => {\n      await seedVerticalState(reactGrab.page, \"left\");\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 3000 })\n        .toBe(true);\n\n      const info = await reactGrab.getToolbarInfo();\n      expect(info.snapEdge).toBe(\"left\");\n      expect(info.isVertical).toBe(true);\n      expect(info.dimensions).not.toBeNull();\n      expect(info.dimensions!.height).toBeGreaterThan(info.dimensions!.width);\n    });\n\n    test(\"should render horizontally when snapped to top or bottom\", async ({\n      reactGrab,\n    }) => {\n      const info = await reactGrab.getToolbarInfo();\n      expect(info.isVertical).toBe(false);\n      expect(info.dimensions).not.toBeNull();\n      expect(info.dimensions!.width).toBeGreaterThan(info.dimensions!.height);\n    });\n\n    test(\"should allow toggle activation in vertical mode\", async ({\n      reactGrab,\n    }) => {\n      await seedVerticalState(reactGrab.page, \"right\");\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 3000 })\n        .toBe(true);\n\n      await reactGrab.clickToolbarToggle();\n      const isActive = await reactGrab.isOverlayVisible();\n      expect(isActive).toBe(true);\n    });\n\n    test(\"should collapse and expand in vertical mode\", async ({\n      reactGrab,\n    }) => {\n      await seedVerticalState(reactGrab.page, \"right\");\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 3000 })\n        .toBe(true);\n\n      await reactGrab.clickToolbarCollapse();\n      await expect\n        .poll(() => reactGrab.isToolbarCollapsed(), { timeout: 2000 })\n        .toBe(true);\n\n      await reactGrab.page.evaluate((attrName) => {\n        const host = document.querySelector(`[${attrName}]`);\n        const shadowRoot = host?.shadowRoot;\n        if (!shadowRoot) return;\n        const root = shadowRoot.querySelector(`[${attrName}]`);\n        const toolbar = root?.querySelector<HTMLElement>(\n          \"[data-react-grab-toolbar]\",\n        );\n        const innerDiv = toolbar?.querySelector(\"div\");\n        innerDiv?.click();\n      }, \"data-react-grab\");\n\n      await expect\n        .poll(() => reactGrab.isToolbarCollapsed(), { timeout: 2000 })\n        .toBe(false);\n    });\n\n    test(\"should toggle enabled state in vertical mode\", async ({\n      reactGrab,\n    }) => {\n      await seedVerticalState(reactGrab.page, \"right\");\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 3000 })\n        .toBe(true);\n\n      await reactGrab.clickToolbarEnabled();\n      // HACK: Wait for toggle animation to complete\n      await reactGrab.page.waitForTimeout(200);\n\n      await reactGrab.clickToolbarEnabled();\n      // HACK: Wait for toggle animation to complete\n      await reactGrab.page.waitForTimeout(200);\n\n      const info = await reactGrab.getToolbarInfo();\n      expect(info.isVisible).toBe(true);\n      expect(info.snapEdge).toBe(\"right\");\n    });\n\n    test(\"vertical edge state should persist across page reloads\", async ({\n      reactGrab,\n    }) => {\n      await seedVerticalState(reactGrab.page, \"right\");\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 3000 })\n        .toBe(true);\n\n      await reactGrab.page.reload();\n      await reactGrab.page.waitForLoadState(\"domcontentloaded\");\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 3000 })\n        .toBe(true);\n\n      const infoAfterReload = await reactGrab.getToolbarInfo();\n      expect(infoAfterReload.snapEdge).toBe(\"right\");\n      expect(infoAfterReload.isVertical).toBe(true);\n    });\n\n    test(\"vertical toolbar should be snapped to edge after reload\", async ({\n      reactGrab,\n    }) => {\n      const viewportSize = await reactGrab.getViewportSize();\n\n      await seedVerticalState(reactGrab.page, \"right\");\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 3000 })\n        .toBe(true);\n\n      await reactGrab.page.reload();\n      await reactGrab.page.waitForLoadState(\"domcontentloaded\");\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 3000 })\n        .toBe(true);\n\n      const info = await reactGrab.getToolbarInfo();\n      expect(info.position).not.toBeNull();\n      expect(info.dimensions).not.toBeNull();\n\n      const rightEdgePosition = info.position!.x + info.dimensions!.width;\n      expect(rightEdgePosition).toBeGreaterThan(viewportSize.width - 30);\n    });\n\n    test(\"should transition from vertical to horizontal when dragged to bottom\", async ({\n      reactGrab,\n    }) => {\n      await seedVerticalState(reactGrab.page, \"right\");\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 3000 })\n        .toBe(true);\n      // HACK: Wait for toolbar fade-in animation to complete\n      await reactGrab.page.waitForTimeout(600);\n\n      const verticalInfo = await reactGrab.getToolbarInfo();\n      expect(verticalInfo.isVertical).toBe(true);\n\n      await reactGrab.dragToolbar(-500, 500);\n\n      await expect\n        .poll(\n          async () => {\n            const info = await reactGrab.getToolbarInfo();\n            return info.snapEdge;\n          },\n          { timeout: 3000 },\n        )\n        .toBe(\"bottom\");\n\n      const horizontalInfo = await reactGrab.getToolbarInfo();\n      expect(horizontalInfo.isVertical).toBe(false);\n    });\n\n    test(\"should be draggable from vertical position\", async ({\n      reactGrab,\n    }) => {\n      await seedVerticalState(reactGrab.page, \"right\");\n      await expect\n        .poll(() => reactGrab.isToolbarVisible(), { timeout: 3000 })\n        .toBe(true);\n      // HACK: Wait for toolbar fade-in animation to complete\n      await reactGrab.page.waitForTimeout(600);\n\n      const initialInfo = await reactGrab.getToolbarInfo();\n      expect(initialInfo.position).not.toBeNull();\n\n      await reactGrab.dragToolbar(0, 100);\n\n      await expect\n        .poll(\n          async () => {\n            const info = await reactGrab.getToolbarInfo();\n            if (!info.position || !initialInfo.position) return false;\n            return (\n              Math.abs(info.position.x - initialInfo.position.x) > 0 ||\n              Math.abs(info.position.y - initialInfo.position.y) > 0\n            );\n          },\n          { timeout: 3000 },\n        )\n        .toBe(true);\n\n      const movedInfo = await reactGrab.getToolbarInfo();\n      expect(movedInfo.isVisible).toBe(true);\n      expect(movedInfo.snapEdge).not.toBeNull();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/touch-mode.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\ntest.describe(\"Touch Mode\", () => {\n  test.describe(\"Touch Events\", () => {\n    test(\"touch tap should work for element selection\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n\n      await reactGrab.touchTap(\"li:first-child\");\n\n      await expect\n        .poll(() => reactGrab.getClipboardContent(), { timeout: 5000 })\n        .toBeTruthy();\n    });\n\n    test(\"touch should set touch mode flag\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n\n      const listItem = reactGrab.page.locator(\"li\").first();\n      const box = await listItem.boundingBox();\n      if (!box) throw new Error(\"Could not get bounding box\");\n\n      await reactGrab.page.touchscreen.tap(\n        box.x + box.width / 2,\n        box.y + box.height / 2,\n      );\n      await reactGrab.page.waitForTimeout(100);\n\n      const state = await reactGrab.getState();\n      expect(state).toBeDefined();\n    });\n\n    test(\"touch drag should create drag selection\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n\n      const firstItem = reactGrab.page.locator(\"li\").first();\n      const lastItem = reactGrab.page.locator(\"li\").nth(3);\n\n      const startBox = await firstItem.boundingBox();\n      const endBox = await lastItem.boundingBox();\n\n      if (!startBox || !endBox) throw new Error(\"Could not get bounding boxes\");\n\n      await reactGrab.touchDrag(\n        startBox.x - 10,\n        startBox.y - 10,\n        endBox.x + endBox.width + 10,\n        endBox.y + endBox.height + 10,\n      );\n      await expect\n        .poll(() => reactGrab.getClipboardContent(), { timeout: 5000 })\n        .toBeTruthy();\n    });\n  });\n\n  test.describe(\"Touch Mode Behavior\", () => {\n    test(\"touch events should update pointer position\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n\n      const listItem = reactGrab.page.locator(\"li\").first();\n      const box = await listItem.boundingBox();\n      if (!box) throw new Error(\"Could not get bounding box\");\n\n      await reactGrab.page.touchscreen.tap(\n        box.x + box.width / 2,\n        box.y + box.height / 2,\n      );\n      await reactGrab.page.waitForTimeout(100);\n\n      const state = await reactGrab.getState();\n      expect(state).toBeDefined();\n    });\n  });\n\n  test.describe(\"Touch Selection\", () => {\n    test(\"touch should select element\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n\n      await reactGrab.hoverElement(\"[data-testid='todo-list'] h1\");\n      await reactGrab.waitForSelectionBox();\n\n      const element = reactGrab.page.locator(\"[data-testid='todo-list'] h1\");\n      const box = await element.boundingBox();\n      if (!box) throw new Error(\"Could not get bounding box\");\n\n      await reactGrab.page.touchscreen.tap(\n        box.x + box.width / 2,\n        box.y + box.height / 2,\n      );\n\n      await expect\n        .poll(() => reactGrab.getClipboardContent(), { timeout: 5000 })\n        .toContain(\"Todo List\");\n    });\n\n    test(\"touch on different elements should work\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n\n      await reactGrab.touchTap(\"li:nth-child(2)\");\n\n      await expect\n        .poll(() => reactGrab.getClipboardContent(), { timeout: 5000 })\n        .toBeTruthy();\n\n      await expect\n        .poll(() => reactGrab.isOverlayVisible(), { timeout: 5000 })\n        .toBe(false);\n\n      await reactGrab.activate();\n      await reactGrab.touchTap(\"li:nth-child(4)\");\n\n      await expect\n        .poll(() => reactGrab.getClipboardContent(), { timeout: 5000 })\n        .toBeTruthy();\n    });\n  });\n\n  test.describe(\"Touch Drag Selection\", () => {\n    test(\"touch drag should select multiple elements\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n\n      const firstItem = reactGrab.page.locator(\"li\").first();\n      const secondItem = reactGrab.page.locator(\"li\").nth(1);\n\n      const startBox = await firstItem.boundingBox();\n      const endBox = await secondItem.boundingBox();\n\n      if (!startBox || !endBox) throw new Error(\"Could not get bounding boxes\");\n\n      await reactGrab.touchDrag(\n        startBox.x - 5,\n        startBox.y - 5,\n        endBox.x + endBox.width + 5,\n        endBox.y + endBox.height + 5,\n      );\n\n      await expect\n        .poll(() => reactGrab.getClipboardContent(), { timeout: 5000 })\n        .toBeTruthy();\n    });\n\n    test(\"short touch drag should be treated as tap\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n\n      const listItem = reactGrab.page.locator(\"li\").first();\n      const box = await listItem.boundingBox();\n      if (!box) throw new Error(\"Could not get bounding box\");\n\n      await reactGrab.touchDrag(\n        box.x + box.width / 2,\n        box.y + box.height / 2,\n        box.x + box.width / 2 + 2,\n        box.y + box.height / 2 + 2,\n      );\n\n      await expect\n        .poll(() => reactGrab.getClipboardContent(), { timeout: 5000 })\n        .toBeTruthy();\n    });\n  });\n\n  test.describe(\"Touch and Mouse Switching\", () => {\n    test(\"should handle switch from mouse to touch\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.touchTap(\"li:nth-child(2)\");\n\n      await expect\n        .poll(() => reactGrab.getClipboardContent(), { timeout: 5000 })\n        .toBeTruthy();\n    });\n\n    test(\"should handle switch from touch to mouse\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n\n      await reactGrab.touchTap(\"li:first-child\");\n\n      await expect\n        .poll(() => reactGrab.getClipboardContent(), { timeout: 5000 })\n        .toBeTruthy();\n\n      await expect\n        .poll(() => reactGrab.isOverlayVisible(), { timeout: 5000 })\n        .toBe(false);\n\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:nth-child(3)\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.clickElement(\"li:nth-child(3)\");\n\n      await expect\n        .poll(() => reactGrab.getClipboardContent(), { timeout: 5000 })\n        .toBeTruthy();\n    });\n  });\n\n  test.describe(\"Touch Input Mode\", () => {\n    test(\"double tap should enter input mode with agent\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.setupMockAgent();\n      await reactGrab.activate();\n\n      const listItem = reactGrab.page.locator(\"li\").first();\n      const box = await listItem.boundingBox();\n      if (!box) throw new Error(\"Could not get bounding box\");\n\n      await reactGrab.page.touchscreen.tap(\n        box.x + box.width / 2,\n        box.y + box.height / 2,\n      );\n      await reactGrab.page.waitForTimeout(100);\n      await reactGrab.page.touchscreen.tap(\n        box.x + box.width / 2,\n        box.y + box.height / 2,\n      );\n      await reactGrab.page.waitForTimeout(200);\n\n      const state = await reactGrab.getState();\n      expect(state).toBeDefined();\n    });\n  });\n\n  test.describe(\"Touch with Scroll\", () => {\n    test(\"should handle touch after scroll\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.scrollPage(200);\n\n      const listItem = reactGrab.page.locator(\"li\").first();\n      const box = await listItem.boundingBox();\n      if (box) {\n        await reactGrab.page.touchscreen.tap(\n          box.x + box.width / 2,\n          box.y + box.height / 2,\n        );\n\n        await expect\n          .poll(() => reactGrab.getClipboardContent(), { timeout: 5000 })\n          .toBeTruthy();\n      }\n    });\n  });\n\n  test.describe(\"Touch Edge Cases\", () => {\n    test(\"should handle rapid touch events\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n\n      const listItem = reactGrab.page.locator(\"li\").first();\n      const box = await listItem.boundingBox();\n      if (!box) throw new Error(\"Could not get bounding box\");\n\n      for (let i = 0; i < 5; i++) {\n        await reactGrab.page.touchscreen.tap(\n          box.x + box.width / 2 + i * 10,\n          box.y + box.height / 2,\n        );\n        await reactGrab.page.waitForTimeout(50);\n      }\n\n      const state = await reactGrab.getState();\n      expect(state).toBeDefined();\n    });\n\n    test(\"should handle touch on overlay elements\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.page.waitForTimeout(600);\n\n      const toolbarInfo = await reactGrab.getToolbarInfo();\n      if (toolbarInfo.position) {\n        await reactGrab.page.touchscreen.tap(\n          toolbarInfo.position.x + 20,\n          toolbarInfo.position.y + 10,\n        );\n        await reactGrab.page.waitForTimeout(200);\n      }\n\n      const state = await reactGrab.getState();\n      expect(state).toBeDefined();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/viewport.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\ntest.describe(\"Viewport and Scroll Handling\", () => {\n  test(\"should maintain selection after scrolling page\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.page.evaluate(() => {\n      window.scrollBy(0, 50);\n    });\n    await reactGrab.page.waitForTimeout(200);\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"should re-detect element under cursor after scroll without mouse movement\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n\n    const firstItem = reactGrab.page\n      .locator(\"[data-testid='todo-list'] li\")\n      .first();\n    const firstItemBox = await firstItem.boundingBox();\n    expect(firstItemBox).not.toBeNull();\n\n    await reactGrab.page.mouse.move(\n      firstItemBox!.x + firstItemBox!.width / 2,\n      firstItemBox!.y + firstItemBox!.height / 2,\n    );\n    await reactGrab.page.waitForTimeout(150);\n    await reactGrab.waitForSelectionBox();\n\n    const initialLabel = await reactGrab.getSelectionLabelInfo();\n    expect(initialLabel.isVisible).toBe(true);\n\n    await reactGrab.page.evaluate(() => {\n      window.scrollBy(0, 100);\n    });\n    await reactGrab.page.waitForTimeout(200);\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"should update selection to new element after scroll changes element under cursor\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n\n    const heading = reactGrab.page.locator(\"[data-testid='main-title']\");\n    const headingBox = await heading.boundingBox();\n    expect(headingBox).not.toBeNull();\n\n    const cursorX = headingBox!.x + headingBox!.width / 2;\n    const cursorY = headingBox!.y + headingBox!.height / 2;\n\n    await reactGrab.page.mouse.move(cursorX, cursorY);\n    await reactGrab.page.waitForTimeout(150);\n    await reactGrab.waitForSelectionBox();\n\n    const initialBounds = await reactGrab.getSelectionBoxBounds();\n    expect(initialBounds).not.toBeNull();\n\n    await reactGrab.page.evaluate(() => {\n      window.scrollBy(0, 200);\n    });\n    await reactGrab.page.waitForTimeout(200);\n\n    const newBounds = await reactGrab.getSelectionBoxBounds();\n    if (newBounds !== null && initialBounds !== null) {\n      const boundsChanged =\n        newBounds.y !== initialBounds.y ||\n        newBounds.height !== initialBounds.height;\n      expect(boundsChanged).toBe(true);\n    }\n  });\n\n  test(\"should re-detect element after viewport resize without mouse movement\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n\n    const heading = reactGrab.page.locator(\"[data-testid='main-title']\");\n    const headingBox = await heading.boundingBox();\n    expect(headingBox).not.toBeNull();\n\n    await reactGrab.page.mouse.move(\n      headingBox!.x + headingBox!.width / 2,\n      headingBox!.y + headingBox!.height / 2,\n    );\n    await reactGrab.page.waitForTimeout(150);\n    await reactGrab.waitForSelectionBox();\n\n    const initialBounds = await reactGrab.getSelectionBoxBounds();\n    expect(initialBounds).not.toBeNull();\n\n    await reactGrab.setViewportSize(800, 400);\n    await reactGrab.page.waitForTimeout(200);\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(true);\n\n    await reactGrab.setViewportSize(1280, 720);\n  });\n\n  test(\"should not re-detect element during drag operation on scroll\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n\n    const todoList = reactGrab.page.locator(\"[data-testid='todo-list'] ul\");\n    const listBox = await todoList.boundingBox();\n    expect(listBox).not.toBeNull();\n\n    const startX = listBox!.x - 10;\n    const startY = listBox!.y;\n    const endX = listBox!.x + listBox!.width + 10;\n    const endY = listBox!.y + listBox!.height;\n\n    await reactGrab.page.mouse.move(startX, startY);\n    await reactGrab.page.mouse.down();\n    await reactGrab.page.mouse.move(endX, endY, { steps: 5 });\n\n    const state = await reactGrab.getState();\n    expect(state.isDragging).toBe(true);\n\n    await reactGrab.page.evaluate(() => {\n      window.scrollBy(0, 50);\n    });\n    await reactGrab.page.waitForTimeout(100);\n\n    const stateAfterScroll = await reactGrab.getState();\n    expect(stateAfterScroll.isDragging).toBe(true);\n\n    await reactGrab.page.mouse.up();\n  });\n\n  test(\"should not re-detect element when selection is frozen via arrow navigation\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"[data-testid='todo-list'] li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.pressArrowDown();\n    await reactGrab.page.waitForTimeout(100);\n\n    const labelBeforeScroll = await reactGrab.getSelectionLabelInfo();\n\n    await reactGrab.page.evaluate(() => {\n      window.scrollBy(0, 30);\n    });\n    await reactGrab.page.waitForTimeout(200);\n\n    const labelAfterScroll = await reactGrab.getSelectionLabelInfo();\n\n    expect(labelAfterScroll.tagName).toBe(labelBeforeScroll.tagName);\n  });\n\n  test(\"should update selection position after viewport resize\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.page.setViewportSize({ width: 800, height: 600 });\n    await reactGrab.page.waitForTimeout(200);\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(true);\n\n    await reactGrab.page.setViewportSize({ width: 1280, height: 720 });\n  });\n\n  test(\"should handle mouse movement after scroll\", async ({ reactGrab }) => {\n    await reactGrab.activate();\n\n    await reactGrab.scrollPage(100);\n\n    await reactGrab.hoverElement(\"li:nth-child(5)\");\n    await reactGrab.waitForSelectionBox();\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"should allow drag selection after scrolling\", async ({ reactGrab }) => {\n    await reactGrab.activate();\n    await reactGrab.scrollPage(50);\n\n    await reactGrab.dragSelect(\"li:first-child\", \"li:nth-child(3)\");\n    await reactGrab.page.waitForTimeout(500);\n\n    const clipboardContent = await reactGrab.getClipboardContent();\n    expect(clipboardContent).toBeTruthy();\n  });\n\n  test(\"should preserve frozen selection during scroll via arrow navigation\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.pressArrowDown();\n    await reactGrab.scrollPage(100);\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"should handle keyboard navigation after scroll\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.scrollPage(50);\n\n    await reactGrab.hoverElement(\"li:first-child\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.pressArrowDown();\n    await reactGrab.pressArrowDown();\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"should recalculate bounds after visual viewport change\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n\n    const heading = reactGrab.page.locator(\"[data-testid='main-title']\");\n    const headingBox = await heading.boundingBox();\n    expect(headingBox).not.toBeNull();\n\n    await reactGrab.page.mouse.move(\n      headingBox!.x + headingBox!.width / 2,\n      headingBox!.y + headingBox!.height / 2,\n    );\n    await reactGrab.page.waitForTimeout(150);\n    await reactGrab.waitForSelectionBox();\n\n    const initialBounds = await reactGrab.getSelectionBoxBounds();\n    expect(initialBounds).not.toBeNull();\n\n    await reactGrab.page.evaluate(() => {\n      window.visualViewport?.dispatchEvent(new Event(\"resize\"));\n      window.visualViewport?.dispatchEvent(new Event(\"scroll\"));\n    });\n    await reactGrab.page.waitForTimeout(200);\n\n    const isVisible = await reactGrab.isOverlayVisible();\n    expect(isVisible).toBe(true);\n\n    const boundsAfter = await reactGrab.getSelectionBoxBounds();\n    expect(boundsAfter).not.toBeNull();\n  });\n\n  test(\"should copy element after resize using click\", async ({\n    reactGrab,\n  }) => {\n    await reactGrab.activate();\n    await reactGrab.hoverElement(\"[data-testid='todo-list'] h1\");\n    await reactGrab.waitForSelectionBox();\n\n    await reactGrab.page.setViewportSize({ width: 600, height: 400 });\n    await reactGrab.page.waitForTimeout(200);\n\n    await reactGrab.hoverElement(\"[data-testid='todo-list'] h1\");\n    await reactGrab.waitForSelectionBox();\n    await reactGrab.clickElement(\"[data-testid='todo-list'] h1\");\n\n    await expect\n      .poll(() => reactGrab.getClipboardContent(), { timeout: 5000 })\n      .toContain(\"Todo List\");\n\n    await reactGrab.page.setViewportSize({ width: 1280, height: 720 });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/e2e/visual-feedback.spec.ts",
    "content": "import { test, expect } from \"./fixtures.js\";\n\ntest.describe(\"Visual Feedback\", () => {\n  test.describe(\"Selection Box\", () => {\n    test(\"selection box should match element bounds\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      const elementBounds = await reactGrab.getElementBounds(\"li:first-child\");\n      const selectionBounds = await reactGrab.getSelectionBoxBounds();\n\n      if (elementBounds && selectionBounds) {\n        expect(Math.abs(selectionBounds.x - elementBounds.x)).toBeLessThan(5);\n        expect(Math.abs(selectionBounds.y - elementBounds.y)).toBeLessThan(5);\n        expect(\n          Math.abs(selectionBounds.width - elementBounds.width),\n        ).toBeLessThan(10);\n        expect(\n          Math.abs(selectionBounds.height - elementBounds.height),\n        ).toBeLessThan(10);\n      }\n    });\n\n    test(\"selection box should update when hovering different elements\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.page.waitForTimeout(100);\n      const bounds1 = await reactGrab.getSelectionBoxBounds();\n\n      await reactGrab.hoverElement(\"h1\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.page.waitForTimeout(100);\n      const bounds2 = await reactGrab.getSelectionBoxBounds();\n\n      if (bounds1 && bounds2) {\n        expect(\n          bounds1.width !== bounds2.width || bounds1.height !== bounds2.height,\n        ).toBe(true);\n      }\n    });\n\n    test(\"selection box should track scrolling element\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      const boundsBefore = await reactGrab.getSelectionBoxBounds();\n\n      await reactGrab.scrollPage(50);\n      await reactGrab.page.waitForTimeout(200);\n\n      const boundsAfter = await reactGrab.getSelectionBoxBounds();\n\n      if (boundsBefore && boundsAfter) {\n        expect(boundsBefore.y - boundsAfter.y).toBeGreaterThan(0);\n      }\n    });\n  });\n\n  test.describe(\"Drag Box\", () => {\n    test(\"drag box should appear during drag\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n\n      const listItem = reactGrab.page.locator(\"li\").first();\n      const box = await listItem.boundingBox();\n      if (!box) throw new Error(\"Could not get bounding box\");\n\n      await reactGrab.page.mouse.move(box.x - 20, box.y - 20);\n      await reactGrab.page.mouse.down();\n      await reactGrab.page.mouse.move(box.x + 150, box.y + 150, { steps: 10 });\n\n      const dragBounds = await reactGrab.getDragBoxBounds();\n      expect(dragBounds).toBeDefined();\n\n      await reactGrab.page.mouse.up();\n    });\n\n    test(\"drag box should grow with drag distance\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n\n      const listItem = reactGrab.page.locator(\"li\").first();\n      const box = await listItem.boundingBox();\n      if (!box) throw new Error(\"Could not get bounding box\");\n\n      await reactGrab.page.mouse.move(box.x - 20, box.y - 20);\n      await reactGrab.page.mouse.down();\n\n      await reactGrab.page.mouse.move(box.x + 50, box.y + 50, { steps: 5 });\n      const smallDragBounds = await reactGrab.getDragBoxBounds();\n\n      await reactGrab.page.mouse.move(box.x + 200, box.y + 200, { steps: 5 });\n      const largeDragBounds = await reactGrab.getDragBoxBounds();\n\n      if (smallDragBounds && largeDragBounds) {\n        expect(largeDragBounds.width).toBeGreaterThan(smallDragBounds.width);\n        expect(largeDragBounds.height).toBeGreaterThan(smallDragBounds.height);\n      }\n\n      await reactGrab.page.mouse.up();\n    });\n\n    test(\"drag box should disappear after drag ends\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n\n      const listItem = reactGrab.page.locator(\"li\").first();\n      const box = await listItem.boundingBox();\n      if (!box) throw new Error(\"Could not get bounding box\");\n\n      await reactGrab.page.mouse.move(box.x - 20, box.y - 20);\n      await reactGrab.page.mouse.down();\n      await reactGrab.page.mouse.move(box.x + 150, box.y + 150, { steps: 10 });\n      await reactGrab.page.mouse.up();\n\n      await reactGrab.page.waitForTimeout(100);\n\n      const dragBounds = await reactGrab.getDragBoxBounds();\n      expect(dragBounds).toBeNull();\n    });\n  });\n\n  test.describe(\"Grabbed Box\", () => {\n    test(\"grabbed box should appear after element click\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.clickElement(\"li:first-child\");\n      await reactGrab.page.waitForTimeout(200);\n\n      const grabbedInfo = await reactGrab.getGrabbedBoxInfo();\n      expect(grabbedInfo.count).toBeGreaterThan(0);\n    });\n\n    test(\"grabbed box should fade out after delay\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.clickElement(\"li:first-child\");\n\n      await reactGrab.page.waitForTimeout(2000);\n\n      const grabbedInfo = await reactGrab.getGrabbedBoxInfo();\n      expect(grabbedInfo.count).toBe(0);\n    });\n  });\n\n  test.describe(\"Selection Label\", () => {\n    test(\"label should show tag name\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"h1\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.waitForSelectionLabel();\n\n      const labelInfo = await reactGrab.getSelectionLabelInfo();\n      expect(labelInfo.tagName).toBe(\"h1\");\n    });\n\n    test(\"label should show element count for multi-select\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.dragSelect(\"li:first-child\", \"li:nth-child(3)\");\n      await reactGrab.page.waitForTimeout(200);\n\n      const state = await reactGrab.getState();\n      expect(state).toBeDefined();\n    });\n\n    test(\"label should position below element by default\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"h1\");\n      await reactGrab.waitForSelectionBox();\n\n      const elementBounds = await reactGrab.getElementBounds(\"h1\");\n      const labelInfo = await reactGrab.getSelectionLabelInfo();\n\n      expect(labelInfo.isVisible).toBe(true);\n      expect(elementBounds).toBeDefined();\n    });\n\n    test(\"label should be clamped to viewport\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='edge-bottom-left']\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.waitForSelectionLabel();\n\n      const labelInfo = await reactGrab.getSelectionLabelInfo();\n      expect(labelInfo.isVisible).toBe(true);\n    });\n\n    test(\"label and arrow should stay within bounds at left edge\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='edge-top-left']\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.waitForSelectionLabel();\n\n      await expect(async () => {\n        const bounds = await reactGrab.getSelectionLabelBounds();\n        expect(bounds).not.toBeNull();\n        expect(bounds?.arrow).not.toBeNull();\n        if (bounds?.arrow) {\n          expect(bounds.label.x).toBeGreaterThanOrEqual(0);\n          expect(bounds.label.x + bounds.label.width).toBeLessThanOrEqual(\n            bounds.viewport.width,\n          );\n          expect(bounds.arrow.x).toBeGreaterThanOrEqual(bounds.label.x);\n          expect(bounds.arrow.x + bounds.arrow.width).toBeLessThanOrEqual(\n            bounds.label.x + bounds.label.width,\n          );\n        }\n      }).toPass({ timeout: 2000 });\n    });\n\n    test(\"label and arrow should stay within bounds at right edge\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='edge-top-right']\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.waitForSelectionLabel();\n\n      await expect(async () => {\n        const bounds = await reactGrab.getSelectionLabelBounds();\n        expect(bounds).not.toBeNull();\n        expect(bounds?.arrow).not.toBeNull();\n        if (bounds?.arrow) {\n          expect(bounds.label.x).toBeGreaterThanOrEqual(0);\n          expect(bounds.label.x + bounds.label.width).toBeLessThanOrEqual(\n            bounds.viewport.width,\n          );\n          expect(bounds.arrow.x).toBeGreaterThanOrEqual(bounds.label.x);\n          expect(bounds.arrow.x + bounds.arrow.width).toBeLessThanOrEqual(\n            bounds.label.x + bounds.label.width,\n          );\n        }\n      }).toPass({ timeout: 2000 });\n    });\n\n    test(\"label and arrow should stay within bounds at bottom-left edge\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"[data-testid='edge-bottom-left']\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.waitForSelectionLabel();\n\n      await expect(async () => {\n        const bounds = await reactGrab.getSelectionLabelBounds();\n        expect(bounds).not.toBeNull();\n        expect(bounds?.arrow).not.toBeNull();\n        if (bounds?.arrow) {\n          expect(bounds.label.x).toBeGreaterThanOrEqual(0);\n          expect(bounds.label.x + bounds.label.width).toBeLessThanOrEqual(\n            bounds.viewport.width,\n          );\n          expect(bounds.arrow.x).toBeGreaterThanOrEqual(bounds.label.x);\n          expect(bounds.arrow.x + bounds.arrow.width).toBeLessThanOrEqual(\n            bounds.label.x + bounds.label.width,\n          );\n        }\n      }).toPass({ timeout: 2000 });\n    });\n  });\n\n  test.describe(\"Status Transitions\", () => {\n    test(\"should show copying status during copy\", async ({ reactGrab }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.clickElement(\"li:first-child\");\n\n      // During/after copy, a status label should appear (e.g., \"Copying...\" or \"Copied\")\n      await expect\n        .poll(() => reactGrab.getLabelStatusText(), { timeout: 2000 })\n        .toBeTruthy();\n    });\n\n    test(\"should transition to copied status after copy\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.clickElement(\"li:first-child\");\n\n      await expect\n        .poll(() => reactGrab.getLabelStatusText(), { timeout: 2000 })\n        .toBe(\"Copied\");\n    });\n  });\n\n  test.describe(\"Arrow Direction\", () => {\n    test(\"arrow should point down when label is below element\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"h1\");\n      await reactGrab.waitForSelectionBox();\n\n      const labelInfo = await reactGrab.getSelectionLabelInfo();\n      expect(labelInfo.isVisible).toBe(true);\n    });\n\n    test(\"arrow should adjust when near viewport bottom\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.scrollPage(500);\n      await reactGrab.hoverElement(\"[data-testid='footer']\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.waitForSelectionLabel();\n\n      const labelInfo = await reactGrab.getSelectionLabelInfo();\n      expect(labelInfo.isVisible).toBe(true);\n    });\n  });\n\n  test.describe(\"Multiple Visual Elements\", () => {\n    test(\"selection box and label should be synchronized\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n      await reactGrab.waitForSelectionLabel();\n\n      const selectionVisible = await reactGrab.isSelectionBoxVisible();\n      const labelVisible = await reactGrab.isSelectionLabelVisible();\n\n      expect(selectionVisible).toBe(true);\n      expect(labelVisible).toBe(true);\n    });\n\n    test(\"all visual elements should update on viewport change\", async ({\n      reactGrab,\n    }) => {\n      await reactGrab.activate();\n      await reactGrab.hoverElement(\"li:first-child\");\n      await reactGrab.waitForSelectionBox();\n\n      await reactGrab.setViewportSize(1024, 768);\n      await reactGrab.page.waitForTimeout(200);\n\n      const selectionVisible = await reactGrab.isSelectionBoxVisible();\n      expect(selectionVisible).toBe(true);\n\n      await reactGrab.setViewportSize(1280, 720);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-grab/package.json",
    "content": "{\n  \"name\": \"react-grab\",\n  \"version\": \"0.1.28\",\n  \"description\": \"Select context for coding agents directly from your website\",\n  \"keywords\": [\n    \"agent\",\n    \"context\",\n    \"grab\",\n    \"react\",\n    \"react-grab\"\n  ],\n  \"homepage\": \"https://react-grab.com\",\n  \"bugs\": {\n    \"url\": \"https://github.com/aidenybai/react-grab/issues\"\n  },\n  \"license\": \"MIT\",\n  \"author\": {\n    \"name\": \"Aiden Bai\",\n    \"email\": \"aiden@million.dev\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/aidenybai/react-grab.git\"\n  },\n  \"bin\": {\n    \"react-grab\": \"./bin/cli.js\"\n  },\n  \"files\": [\n    \"bin\",\n    \"dist\",\n    \"scripts/postinstall.cjs\",\n    \"package.json\",\n    \"README.md\",\n    \"LICENSE\"\n  ],\n  \"type\": \"module\",\n  \"main\": \"dist/index.js\",\n  \"module\": \"dist/index.js\",\n  \"browser\": \"dist/index.global.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \"./package.json\": \"./package.json\",\n    \".\": {\n      \"import\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"require\": {\n        \"types\": \"./dist/index.d.cts\",\n        \"default\": \"./dist/index.cjs\"\n      }\n    },\n    \"./core\": {\n      \"import\": {\n        \"types\": \"./dist/core/index.d.ts\",\n        \"default\": \"./dist/core/index.js\"\n      },\n      \"require\": {\n        \"types\": \"./dist/core/index.d.cts\",\n        \"default\": \"./dist/core/index.cjs\"\n      }\n    },\n    \"./primitives\": {\n      \"import\": {\n        \"types\": \"./dist/primitives.d.ts\",\n        \"default\": \"./dist/primitives.js\"\n      },\n      \"require\": {\n        \"types\": \"./dist/primitives.d.cts\",\n        \"default\": \"./dist/primitives.cjs\"\n      }\n    },\n    \"./src/*\": \"./src/*\",\n    \"./styles.css\": \"./dist/styles.css\",\n    \"./dist/styles.css\": \"./dist/styles.css\",\n    \"./dist/*\": \"./dist/*.js\",\n    \"./dist/*.js\": \"./dist/*.js\",\n    \"./dist/*.cjs\": \"./dist/*.cjs\",\n    \"./dist/*.mjs\": \"./dist/*.mjs\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"scripts\": {\n    \"css:build\": \"tailwindcss -i ./src/styles.css -o ./dist/styles.css -m && node scripts/css-rem-to-px.mjs\",\n    \"css:watch\": \"tailwindcss -i ./src/styles.css -o ./dist/styles.css -w\",\n    \"prebuild\": \"mkdir -p dist && tailwindcss -i ./src/styles.css -o ./dist/styles.css -m && node scripts/css-rem-to-px.mjs\",\n    \"build\": \"NODE_ENV=production tsup\",\n    \"postinstall\": \"node ./scripts/postinstall.cjs\",\n    \"dev\": \"concurrently \\\"pnpm:css:watch\\\" \\\"tsup --watch --ignore-watch dist\\\"\",\n    \"lint\": \"oxlint\",\n    \"lint:fix\": \"oxlint --fix\",\n    \"test\": \"playwright test\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"publint\": \"publint\",\n    \"prepublishOnly\": \"pnpm build\",\n    \"test:e2e\": \"playwright test\",\n    \"test:e2e:ui\": \"playwright test --ui\"\n  },\n  \"dependencies\": {\n    \"@medv/finder\": \"^4.0.2\",\n    \"@react-grab/cli\": \"workspace:*\",\n    \"bippy\": \"^0.5.32\",\n    \"element-source\": \"^0.0.3\",\n    \"solid-js\": \"^1.9.10\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.28.5\",\n    \"@babel/preset-typescript\": \"^7.28.5\",\n    \"@playwright/test\": \"^1.40.0\",\n    \"@tailwindcss/cli\": \"^4.1.17\",\n    \"@types/node\": \"^20.19.23\",\n    \"@types/react\": \"^19.2.11\",\n    \"babel-preset-solid\": \"^1.9.10\",\n    \"clsx\": \"^2.1.1\",\n    \"concurrently\": \"^9.1.2\",\n    \"esbuild-plugin-babel\": \"^0.2.3\",\n    \"oxlint\": \"^1.42.0\",\n    \"publint\": \"^0.2.12\",\n    \"tailwindcss\": \"^4.1.0\",\n    \"tsup\": \"^8.2.4\",\n    \"tsx\": \"^4.21.0\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=17.0.0\"\n  },\n  \"peerDependenciesMeta\": {\n    \"react\": {\n      \"optional\": true\n    }\n  }\n}\n"
  },
  {
    "path": "packages/react-grab/playwright.config.ts",
    "content": "import { defineConfig, devices } from \"@playwright/test\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nexport default defineConfig({\n  testDir: \"./e2e\",\n  fullyParallel: true,\n  forbidOnly: !!process.env.CI,\n  retries: process.env.CI ? 2 : 1,\n  workers: process.env.CI ? 4 : undefined,\n  timeout: 60_000,\n  reporter: \"html\",\n  use: {\n    baseURL: \"http://localhost:5175\",\n    trace: \"on-first-retry\",\n    permissions: [\"clipboard-read\", \"clipboard-write\"],\n  },\n  projects: [\n    {\n      name: \"chromium\",\n      use: { ...devices[\"Desktop Chrome\"] },\n      testIgnore: /touch-mode\\.spec\\.ts/,\n    },\n    {\n      name: \"chromium-touch\",\n      use: {\n        ...devices[\"Desktop Chrome\"],\n        hasTouch: true,\n      },\n      testMatch: /touch-mode\\.spec\\.ts/,\n    },\n  ],\n  webServer: {\n    command: \"pnpm --filter react-grab build && pnpm dev\",\n    url: \"http://localhost:5175\",\n    reuseExistingServer: !process.env.CI,\n    cwd: path.resolve(__dirname, \"../e2e-playground\"),\n    timeout: 30000,\n  },\n});\n"
  },
  {
    "path": "packages/react-grab/scripts/css-rem-to-px.mjs",
    "content": "/**\n * Converts all `rem` units in the built Tailwind CSS to `px`.\n *\n * The toolbar renders inside a shadow DOM for style isolation, but `rem` is\n * always relative to the document root (`<html>`) font-size — not the shadow\n * host. Pages like YouTube set `html { font-size: 10px }`, which shrinks every\n * rem-based value (spacing, radii, text sizes) and breaks the toolbar layout.\n *\n * Running this after Tailwind makes all values absolute, so the toolbar renders\n * identically regardless of the host page's root font-size.\n */\nimport { readFileSync, writeFileSync } from \"node:fs\";\n\nconst BROWSER_DEFAULT_FONT_SIZE_PX = 16;\nconst CSS_OUTPUT_PATH = \"./dist/styles.css\";\n\nconst cssContent = readFileSync(CSS_OUTPUT_PATH, \"utf8\");\nconst transformedCss = cssContent.replace(\n  /(\\d*\\.?\\d+)rem\\b/g,\n  (_, remValue) => `${parseFloat(remValue) * BROWSER_DEFAULT_FONT_SIZE_PX}px`,\n);\nwriteFileSync(CSS_OUTPUT_PATH, transformedCss);\n"
  },
  {
    "path": "packages/react-grab/scripts/postinstall.cjs",
    "content": "const AUTOMATION_ENVIRONMENT_VARIABLE_NAMES = [\n  \"CURSOR_AGENT\",\n  \"CI\",\n  \"CLAUDECODE\",\n  \"CODEX_CI\",\n  \"OPENCODE\",\n  \"AMP_HOME\",\n  \"AMI\",\n];\n\nconst REACT_GRAB_INIT_COMMAND = \"npx -y grab@latest init\";\nconst INSTALL_HINT_MESSAGE = `[react-grab] Package installed via automation. This step only installs the package. Run \\`${REACT_GRAB_INIT_COMMAND}\\` to complete setup.`;\n\nconst isAlreadyRunningInit = Boolean(process.env.REACT_GRAB_INIT);\n\nconst isRunningInAutomatedEnvironment =\n  AUTOMATION_ENVIRONMENT_VARIABLE_NAMES.some((environmentVariableName) =>\n    Boolean(process.env[environmentVariableName]),\n  );\n\nif (isRunningInAutomatedEnvironment && !isAlreadyRunningInit) {\n  console.log(INSTALL_HINT_MESSAGE);\n}\n"
  },
  {
    "path": "packages/react-grab/src/components/clear-history-prompt.tsx",
    "content": "import { Show, onMount, onCleanup } from \"solid-js\";\nimport type { Component } from \"solid-js\";\nimport type { DropdownAnchor } from \"../types.js\";\nimport { DROPDOWN_EDGE_TRANSFORM_ORIGIN, Z_INDEX_LABEL } from \"../constants.js\";\nimport { cn } from \"../utils/cn.js\";\nimport { DiscardPrompt } from \"./selection-label/discard-prompt.js\";\nimport { suppressMenuEvent } from \"../utils/suppress-menu-event.js\";\nimport { createAnchoredDropdown } from \"../utils/create-anchored-dropdown.js\";\nimport { registerOverlayDismiss } from \"../utils/register-overlay-dismiss.js\";\n\ninterface ClearHistoryPromptProps {\n  position: DropdownAnchor | null;\n  onConfirm: () => void;\n  onCancel: () => void;\n}\n\nexport const ClearHistoryPrompt: Component<ClearHistoryPromptProps> = (\n  props,\n) => {\n  let containerRef: HTMLDivElement | undefined;\n\n  const dropdown = createAnchoredDropdown(\n    () => containerRef,\n    () => props.position,\n  );\n\n  onMount(() => {\n    dropdown.measure();\n    const unregisterOverlayDismiss = registerOverlayDismiss({\n      isOpen: () => Boolean(props.position),\n      onDismiss: props.onCancel,\n      onConfirm: props.onConfirm,\n      shouldIgnoreInputEvents: true,\n    });\n\n    onCleanup(() => {\n      dropdown.clearAnimationHandles();\n      unregisterOverlayDismiss();\n    });\n  });\n\n  return (\n    <Show when={dropdown.shouldMount()}>\n      <div\n        ref={containerRef}\n        data-react-grab-ignore-events\n        data-react-grab-clear-history-prompt\n        class=\"fixed font-sans text-[13px] antialiased filter-[drop-shadow(0px_1px_2px_#51515140)] select-none transition-[opacity,transform] duration-100 ease-out will-change-[opacity,transform]\"\n        style={{\n          top: `${dropdown.displayPosition().top}px`,\n          left: `${dropdown.displayPosition().left}px`,\n          \"z-index\": `${Z_INDEX_LABEL}`,\n          \"pointer-events\": dropdown.isAnimatedIn() ? \"auto\" : \"none\",\n          \"transform-origin\":\n            DROPDOWN_EDGE_TRANSFORM_ORIGIN[dropdown.lastAnchorEdge()],\n          opacity: dropdown.isAnimatedIn() ? \"1\" : \"0\",\n          transform: dropdown.isAnimatedIn() ? \"scale(1)\" : \"scale(0.95)\",\n        }}\n        onPointerDown={suppressMenuEvent}\n        onMouseDown={suppressMenuEvent}\n        onClick={suppressMenuEvent}\n        onContextMenu={suppressMenuEvent}\n      >\n        <div\n          class={cn(\n            \"contain-layout flex flex-col rounded-[10px] antialiased w-fit h-fit [font-synthesis:none] [corner-shape:superellipse(1.25)]\",\n            \"bg-white\",\n          )}\n        >\n          <DiscardPrompt\n            label=\"Clear history?\"\n            cancelOnEscape\n            onConfirm={props.onConfirm}\n            onCancel={props.onCancel}\n          />\n        </div>\n      </div>\n    </Show>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/context-menu.tsx",
    "content": "import {\n  Show,\n  For,\n  onMount,\n  onCleanup,\n  createSignal,\n  createEffect,\n  createMemo,\n} from \"solid-js\";\nimport type { Component } from \"solid-js\";\nimport type {\n  Position,\n  OverlayBounds,\n  ContextMenuAction,\n  ContextMenuActionContext,\n} from \"../types.js\";\nimport {\n  ARROW_HEIGHT_PX,\n  DROPDOWN_OFFSCREEN_POSITION,\n  LABEL_GAP_PX,\n  Z_INDEX_LABEL,\n} from \"../constants.js\";\nimport { cn } from \"../utils/cn.js\";\nimport { Arrow } from \"./selection-label/arrow.js\";\nimport { TagBadge } from \"./selection-label/tag-badge.js\";\nimport { BottomSection } from \"./selection-label/bottom-section.js\";\nimport { formatShortcut } from \"../utils/format-shortcut.js\";\nimport { getTagDisplay } from \"../utils/get-tag-display.js\";\nimport { resolveActionEnabled } from \"../utils/resolve-action-enabled.js\";\nimport { nativeRequestAnimationFrame } from \"../utils/native-raf.js\";\nimport { createMenuHighlight } from \"../utils/create-menu-highlight.js\";\nimport { suppressMenuEvent } from \"../utils/suppress-menu-event.js\";\nimport { registerOverlayDismiss } from \"../utils/register-overlay-dismiss.js\";\n\ninterface ContextMenuProps {\n  position: Position | null;\n  selectionBounds: OverlayBounds | null;\n  tagName?: string;\n  componentName?: string;\n  hasFilePath: boolean;\n  actions?: ContextMenuAction[];\n  actionContext?: ContextMenuActionContext;\n  onDismiss: () => void;\n  onHide: () => void;\n}\n\ninterface MenuItem {\n  label: string;\n  action: () => void;\n  enabled: boolean;\n  shortcut?: string;\n}\n\nexport const ContextMenu: Component<ContextMenuProps> = (props) => {\n  let containerRef: HTMLDivElement | undefined;\n  const {\n    containerRef: highlightContainerRef,\n    highlightRef,\n    updateHighlight,\n    clearHighlight,\n  } = createMenuHighlight();\n\n  const [measuredWidth, setMeasuredWidth] = createSignal(0);\n  const [measuredHeight, setMeasuredHeight] = createSignal(0);\n\n  const isVisible = () => props.position !== null;\n\n  const tagDisplayResult = createMemo(() =>\n    getTagDisplay({\n      tagName: props.tagName,\n      componentName: props.componentName,\n    }),\n  );\n\n  const measureContainer = () => {\n    if (containerRef) {\n      const rect = containerRef.getBoundingClientRect();\n      setMeasuredWidth(rect.width);\n      setMeasuredHeight(rect.height);\n    }\n  };\n\n  createEffect(() => {\n    if (isVisible()) {\n      nativeRequestAnimationFrame(measureContainer);\n    }\n  });\n\n  const computedPosition = createMemo(() => {\n    const bounds = props.selectionBounds;\n    const clickPosition = props.position;\n    const labelWidth = measuredWidth();\n    const labelHeight = measuredHeight();\n\n    if (labelWidth === 0 || labelHeight === 0 || !bounds || !clickPosition) {\n      return {\n        left: DROPDOWN_OFFSCREEN_POSITION.left,\n        top: DROPDOWN_OFFSCREEN_POSITION.top,\n        arrowLeft: 0,\n        arrowPosition: \"bottom\" as const,\n      };\n    }\n\n    const cursorX = clickPosition.x ?? bounds.x + bounds.width / 2;\n    const positionLeft = Math.max(\n      LABEL_GAP_PX,\n      Math.min(\n        cursorX - labelWidth / 2,\n        window.innerWidth - labelWidth - LABEL_GAP_PX,\n      ),\n    );\n    const arrowLeft = Math.max(\n      ARROW_HEIGHT_PX,\n      Math.min(cursorX - positionLeft, labelWidth - ARROW_HEIGHT_PX),\n    );\n\n    const positionBelow =\n      bounds.y + bounds.height + ARROW_HEIGHT_PX + LABEL_GAP_PX;\n    const positionAbove =\n      bounds.y - labelHeight - ARROW_HEIGHT_PX - LABEL_GAP_PX;\n    const wouldOverflowBottom =\n      positionBelow + labelHeight > window.innerHeight;\n    const hasSpaceAbove = positionAbove >= 0;\n\n    const shouldFlipAbove = wouldOverflowBottom && hasSpaceAbove;\n    let positionTop = shouldFlipAbove ? positionAbove : positionBelow;\n    let arrowPosition: \"top\" | \"bottom\" = shouldFlipAbove ? \"top\" : \"bottom\";\n\n    if (wouldOverflowBottom && !hasSpaceAbove) {\n      const cursorY = clickPosition.y ?? bounds.y + bounds.height / 2;\n      positionTop = Math.max(\n        LABEL_GAP_PX,\n        Math.min(\n          cursorY + LABEL_GAP_PX,\n          window.innerHeight - labelHeight - LABEL_GAP_PX,\n        ),\n      );\n      arrowPosition = \"top\";\n    }\n\n    return { left: positionLeft, top: positionTop, arrowLeft, arrowPosition };\n  });\n\n  const menuItems = createMemo<MenuItem[]>(() => {\n    const pluginActions = props.actions ?? [];\n    const context = props.actionContext;\n\n    return pluginActions.map((action) => ({\n      label: action.label,\n      action: () => {\n        if (context) {\n          action.onAction(context);\n        }\n      },\n      enabled: resolveActionEnabled(action, context),\n      shortcut: action.shortcut,\n    }));\n  });\n\n  const handleAction = (item: MenuItem, event: Event) => {\n    event.stopPropagation();\n    if (item.enabled) {\n      item.action();\n      props.onHide();\n    }\n  };\n\n  onMount(() => {\n    measureContainer();\n\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (!isVisible()) return;\n\n      const isEnter = event.key === \"Enter\";\n      const hasModifierKey = event.metaKey || event.ctrlKey;\n      const keyLower = event.key.toLowerCase();\n\n      const pluginActions = props.actions ?? [];\n      const context = props.actionContext;\n\n      const runActionIfAllowed = (action: ContextMenuAction) => {\n        if (!context) return false;\n        if (!resolveActionEnabled(action, context)) return false;\n        event.preventDefault();\n        event.stopPropagation();\n        action.onAction(context);\n        props.onHide();\n        return true;\n      };\n\n      if (isEnter) {\n        const enterAction = pluginActions.find(\n          (action) => action.shortcut === \"Enter\",\n        );\n        if (enterAction) {\n          runActionIfAllowed(enterAction);\n        }\n        return;\n      }\n\n      if (!hasModifierKey) return;\n      if (event.repeat) return;\n\n      const modifierAction = pluginActions.find(\n        (action) =>\n          action.shortcut &&\n          action.shortcut !== \"Enter\" &&\n          keyLower === action.shortcut.toLowerCase(),\n      );\n      if (modifierAction) {\n        runActionIfAllowed(modifierAction);\n      }\n    };\n\n    const unregisterOverlayDismiss = registerOverlayDismiss({\n      isOpen: isVisible,\n      onDismiss: props.onDismiss,\n      shouldIgnoreRightClick: true,\n    });\n    window.addEventListener(\"keydown\", handleKeyDown, { capture: true });\n\n    onCleanup(() => {\n      unregisterOverlayDismiss();\n      window.removeEventListener(\"keydown\", handleKeyDown, { capture: true });\n    });\n  });\n\n  return (\n    <Show when={isVisible()}>\n      <div\n        ref={containerRef}\n        data-react-grab-ignore-events\n        data-react-grab-context-menu\n        class=\"fixed font-sans text-[13px] antialiased filter-[drop-shadow(0px_1px_2px_#51515140)] select-none\"\n        style={{\n          top: `${computedPosition().top}px`,\n          left: `${computedPosition().left}px`,\n          \"z-index\": `${Z_INDEX_LABEL}`,\n          \"pointer-events\": \"auto\",\n        }}\n        onPointerDown={suppressMenuEvent}\n        onMouseDown={suppressMenuEvent}\n        onClick={suppressMenuEvent}\n        onContextMenu={suppressMenuEvent}\n      >\n        <Arrow\n          position={computedPosition().arrowPosition}\n          leftPercent={0}\n          leftOffsetPx={computedPosition().arrowLeft}\n        />\n\n        <div\n          class={cn(\n            \"contain-layout flex flex-col justify-center items-start rounded-[10px] antialiased w-fit h-fit min-w-[100px] [font-synthesis:none] [corner-shape:superellipse(1.25)]\",\n            \"bg-white\",\n          )}\n        >\n          <div class=\"contain-layout shrink-0 flex items-center gap-1 pt-1.5 pb-1 w-fit h-fit px-2\">\n            <TagBadge\n              tagName={tagDisplayResult().tagName}\n              componentName={tagDisplayResult().componentName}\n              isClickable={props.hasFilePath}\n              onClick={(event) => {\n                event.stopPropagation();\n                if (props.hasFilePath && props.actionContext) {\n                  const openAction = props.actions?.find(\n                    (action) => action.id === \"open\",\n                  );\n                  openAction?.onAction(props.actionContext);\n                }\n              }}\n              shrink\n              forceShowIcon={props.hasFilePath}\n            />\n          </div>\n          <BottomSection>\n            <div\n              ref={highlightContainerRef}\n              class=\"relative flex flex-col w-[calc(100%+16px)] -mx-2 -my-1.5\"\n            >\n              <div\n                ref={highlightRef}\n                class=\"pointer-events-none absolute bg-black/5 opacity-0 transition-[top,left,width,height,opacity] duration-75 ease-out\"\n              />\n              <For each={menuItems()}>\n                {(item) => (\n                  <button\n                    data-react-grab-ignore-events\n                    data-react-grab-menu-item={item.label.toLowerCase()}\n                    class=\"relative z-1 contain-layout flex items-center justify-between w-full px-2 py-1 cursor-pointer text-left border-none bg-transparent disabled:opacity-40 disabled:cursor-default\"\n                    disabled={!item.enabled}\n                    onPointerDown={(event) => event.stopPropagation()}\n                    onPointerEnter={(event) => {\n                      if (item.enabled) {\n                        updateHighlight(event.currentTarget);\n                      }\n                    }}\n                    onPointerLeave={clearHighlight}\n                    onClick={(event) => handleAction(item, event)}\n                  >\n                    <span class=\"text-[13px] leading-4 font-sans font-medium text-black\">\n                      {item.label}\n                    </span>\n                    <Show when={item.shortcut}>\n                      {(shortcut) => (\n                        <span class=\"text-[11px] font-sans text-black/50 ml-4\">\n                          {formatShortcut(shortcut())}\n                        </span>\n                      )}\n                    </Show>\n                  </button>\n                )}\n              </For>\n            </div>\n          </BottomSection>\n        </div>\n      </div>\n    </Show>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/history-dropdown.tsx",
    "content": "import {\n  Show,\n  For,\n  onMount,\n  onCleanup,\n  createSignal,\n  createEffect,\n  on,\n} from \"solid-js\";\nimport type { Component } from \"solid-js\";\nimport type { HistoryItem, DropdownAnchor } from \"../types.js\";\nimport {\n  DROPDOWN_EDGE_TRANSFORM_ORIGIN,\n  DROPDOWN_ICON_SIZE_PX,\n  DROPDOWN_MAX_WIDTH_PX,\n  DROPDOWN_MIN_WIDTH_PX,\n  DROPDOWN_VIEWPORT_PADDING_PX,\n  FEEDBACK_DURATION_MS,\n  SAFE_POLYGON_BUFFER_PX,\n  Z_INDEX_LABEL,\n} from \"../constants.js\";\nimport { createSafePolygonTracker } from \"../utils/safe-polygon.js\";\nimport { cn } from \"../utils/cn.js\";\nimport { IconTrash } from \"./icons/icon-trash.jsx\";\nimport { IconCopy } from \"./icons/icon-copy.jsx\";\nimport { IconCheck } from \"./icons/icon-check.jsx\";\nimport { Tooltip } from \"./tooltip.jsx\";\nimport { createMenuHighlight } from \"../utils/create-menu-highlight.js\";\nimport { suppressMenuEvent } from \"../utils/suppress-menu-event.js\";\nimport { createAnchoredDropdown } from \"../utils/create-anchored-dropdown.js\";\nimport { formatRelativeTime } from \"../utils/format-relative-time.js\";\n\nconst ITEM_ACTION_CLASS =\n  \"flex items-center justify-center cursor-pointer text-black/25 transition-colors press-scale\";\n\ninterface HistoryDropdownProps {\n  position: DropdownAnchor | null;\n  items: HistoryItem[];\n  disconnectedItemIds?: Set<string>;\n  onSelectItem?: (item: HistoryItem) => void;\n  onRemoveItem?: (item: HistoryItem) => void;\n  onCopyItem?: (item: HistoryItem) => void;\n  onItemHover?: (historyItemId: string | null) => void;\n  onCopyAll?: () => void;\n  onCopyAllHover?: (isHovered: boolean) => void;\n  onClearAll?: () => void;\n  onDismiss?: () => void;\n  onDropdownHover?: (isHovered: boolean) => void;\n}\n\nconst getHistoryItemDisplayName = (item: HistoryItem): string => {\n  if (item.elementsCount && item.elementsCount > 1) {\n    return `${item.elementsCount} elements`;\n  }\n  return item.componentName ?? item.tagName;\n};\n\nexport const HistoryDropdown: Component<HistoryDropdownProps> = (props) => {\n  let containerRef: HTMLDivElement | undefined;\n  const {\n    containerRef: highlightContainerRef,\n    highlightRef,\n    updateHighlight,\n    clearHighlight,\n  } = createMenuHighlight();\n\n  const safePolygonTracker = createSafePolygonTracker();\n\n  const getToolbarTargetRects = () => {\n    if (!containerRef) return null;\n    const rootNode = containerRef.getRootNode() as Document | ShadowRoot;\n    const toolbar = rootNode.querySelector<HTMLElement>(\n      \"[data-react-grab-toolbar]\",\n    );\n    if (!toolbar) return null;\n    const rect = toolbar.getBoundingClientRect();\n    return [\n      {\n        x: rect.x - SAFE_POLYGON_BUFFER_PX,\n        y: rect.y - SAFE_POLYGON_BUFFER_PX,\n        width: rect.width + SAFE_POLYGON_BUFFER_PX * 2,\n        height: rect.height + SAFE_POLYGON_BUFFER_PX * 2,\n      },\n    ];\n  };\n\n  const dropdown = createAnchoredDropdown(\n    () => containerRef,\n    () => props.position,\n  );\n\n  const [activeHeaderTooltip, setActiveHeaderTooltip] = createSignal<\n    \"clear\" | \"copy\" | null\n  >(null);\n  const [isCopyAllConfirmed, setIsCopyAllConfirmed] = createSignal(false);\n  const [confirmedCopyItemId, setConfirmedCopyItemId] = createSignal<\n    string | null\n  >(null);\n\n  let copyAllFeedbackTimeout: ReturnType<typeof setTimeout> | undefined;\n  let copyItemFeedbackTimeout: ReturnType<typeof setTimeout> | undefined;\n\n  // HACK: mouseenter doesn't fire when an element appears under the cursor, so we check :hover after the enter animation commits\n  createEffect(\n    on(\n      () => dropdown.isAnimatedIn(),\n      (animatedIn) => {\n        if (animatedIn && containerRef?.matches(\":hover\")) {\n          props.onDropdownHover?.(true);\n        }\n      },\n      { defer: true },\n    ),\n  );\n\n  const clampedMaxWidth = () =>\n    Math.min(\n      DROPDOWN_MAX_WIDTH_PX,\n      window.innerWidth -\n        dropdown.displayPosition().left -\n        DROPDOWN_VIEWPORT_PADDING_PX,\n    );\n\n  const clampedMaxHeight = () =>\n    window.innerHeight -\n    dropdown.displayPosition().top -\n    DROPDOWN_VIEWPORT_PADDING_PX;\n\n  const panelMinWidth = () =>\n    Math.max(DROPDOWN_MIN_WIDTH_PX, props.position?.toolbarWidth ?? 0);\n\n  onMount(() => {\n    dropdown.measure();\n\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (!props.position) return;\n      if (event.code === \"Escape\") {\n        event.preventDefault();\n        event.stopPropagation();\n        props.onDismiss?.();\n      }\n    };\n\n    window.addEventListener(\"keydown\", handleKeyDown, { capture: true });\n\n    onCleanup(() => {\n      clearTimeout(copyAllFeedbackTimeout);\n      clearTimeout(copyItemFeedbackTimeout);\n      dropdown.clearAnimationHandles();\n      window.removeEventListener(\"keydown\", handleKeyDown, { capture: true });\n      safePolygonTracker.stop();\n    });\n  });\n\n  return (\n    <Show when={dropdown.shouldMount()}>\n      <div\n        ref={containerRef}\n        data-react-grab-ignore-events\n        data-react-grab-history-dropdown\n        class=\"fixed font-sans text-[13px] antialiased filter-[drop-shadow(0px_1px_2px_#51515140)] select-none transition-[opacity,transform] duration-100 ease-out will-change-[opacity,transform]\"\n        style={{\n          top: `${dropdown.displayPosition().top}px`,\n          left: `${dropdown.displayPosition().left}px`,\n          \"z-index\": `${Z_INDEX_LABEL}`,\n          \"pointer-events\": dropdown.isAnimatedIn() ? \"auto\" : \"none\",\n          \"transform-origin\":\n            DROPDOWN_EDGE_TRANSFORM_ORIGIN[dropdown.lastAnchorEdge()],\n          opacity: dropdown.isAnimatedIn() ? \"1\" : \"0\",\n          transform: dropdown.isAnimatedIn() ? \"scale(1)\" : \"scale(0.95)\",\n        }}\n        onPointerDown={suppressMenuEvent}\n        onMouseDown={suppressMenuEvent}\n        onClick={suppressMenuEvent}\n        onContextMenu={suppressMenuEvent}\n        onMouseEnter={() => {\n          safePolygonTracker.stop();\n          props.onDropdownHover?.(true);\n        }}\n        onMouseLeave={(event: MouseEvent) => {\n          const targetRects = getToolbarTargetRects();\n          if (targetRects) {\n            safePolygonTracker.start(\n              { x: event.clientX, y: event.clientY },\n              targetRects,\n              () => props.onDropdownHover?.(false),\n            );\n            return;\n          }\n          props.onDropdownHover?.(false);\n        }}\n      >\n        <div\n          class={cn(\n            \"contain-layout flex flex-col rounded-[10px] antialiased w-fit h-fit overflow-hidden [font-synthesis:none] [corner-shape:superellipse(1.25)]\",\n            \"bg-white\",\n          )}\n          style={{\n            \"min-width\": `${panelMinWidth()}px`,\n            \"max-width\": `${clampedMaxWidth()}px`,\n            \"max-height\": `${clampedMaxHeight()}px`,\n          }}\n        >\n          <div class=\"contain-layout shrink-0 flex items-center justify-between px-2 pt-1.5 pb-1\">\n            <span class=\"text-[11px] font-medium text-black/40\">History</span>\n            <Show when={props.items.length > 0}>\n              <div class=\"flex items-center gap-[5px]\">\n                <div class=\"relative\">\n                  <button\n                    data-react-grab-ignore-events\n                    data-react-grab-history-clear\n                    class=\"contain-layout shrink-0 flex items-center justify-center px-[3px] py-px rounded-sm bg-[#FEF2F2] cursor-pointer transition-all hover:bg-[#FEE2E2] press-scale h-[17px] text-[#B91C1C]\"\n                    onClick={(event) => {\n                      event.stopPropagation();\n                      setActiveHeaderTooltip(null);\n                      props.onClearAll?.();\n                    }}\n                    onMouseEnter={() => setActiveHeaderTooltip(\"clear\")}\n                    onMouseLeave={() => setActiveHeaderTooltip(null)}\n                  >\n                    <IconTrash size={DROPDOWN_ICON_SIZE_PX} />\n                  </button>\n                  <Tooltip\n                    visible={activeHeaderTooltip() === \"clear\"}\n                    position=\"top\"\n                  >\n                    Clear all\n                  </Tooltip>\n                </div>\n                <div class=\"relative\">\n                  <button\n                    data-react-grab-ignore-events\n                    data-react-grab-history-copy-all\n                    class=\"contain-layout shrink-0 flex items-center justify-center gap-1 px-[3px] py-px rounded-sm bg-white [border-width:0.5px] border-solid border-[#B3B3B3] cursor-pointer transition-all hover:bg-[#F5F5F5] press-scale h-[17px] text-black/60\"\n                    onClick={(event) => {\n                      event.stopPropagation();\n                      setActiveHeaderTooltip(null);\n                      props.onCopyAll?.();\n                      setIsCopyAllConfirmed(true);\n                      clearTimeout(copyAllFeedbackTimeout);\n                      copyAllFeedbackTimeout = setTimeout(() => {\n                        setIsCopyAllConfirmed(false);\n                      }, FEEDBACK_DURATION_MS);\n                    }}\n                    onMouseEnter={() => {\n                      setActiveHeaderTooltip(\"copy\");\n                      if (!isCopyAllConfirmed()) {\n                        props.onCopyAllHover?.(true);\n                      }\n                    }}\n                    onMouseLeave={() => {\n                      setActiveHeaderTooltip(null);\n                      props.onCopyAllHover?.(false);\n                    }}\n                  >\n                    <Show\n                      when={isCopyAllConfirmed()}\n                      fallback={<IconCopy size={DROPDOWN_ICON_SIZE_PX} />}\n                    >\n                      <IconCheck\n                        size={DROPDOWN_ICON_SIZE_PX}\n                        class=\"text-black\"\n                      />\n                    </Show>\n                  </button>\n                  <Tooltip\n                    visible={activeHeaderTooltip() === \"copy\"}\n                    position=\"top\"\n                  >\n                    Copy all\n                  </Tooltip>\n                </div>\n              </div>\n            </Show>\n          </div>\n\n          <div class=\"min-h-0 [border-top-width:0.5px] border-t-solid border-t-[#D9D9D9] px-2 py-1.5\">\n            <div\n              ref={highlightContainerRef}\n              class=\"relative flex flex-col max-h-[240px] overflow-y-auto -mx-2 -my-1.5 [scrollbar-width:thin] [scrollbar-color:transparent_transparent] hover:[scrollbar-color:rgba(0,0,0,0.15)_transparent]\"\n            >\n              <div\n                ref={highlightRef}\n                class=\"pointer-events-none absolute bg-black/5 opacity-0 transition-[top,left,width,height,opacity] duration-75 ease-out\"\n              />\n              <For each={props.items}>\n                {(item) => (\n                  <div\n                    data-react-grab-ignore-events\n                    data-react-grab-history-item\n                    class=\"group relative z-1 contain-layout flex items-start justify-between w-full px-2 py-1 cursor-pointer text-left gap-2\"\n                    classList={{\n                      \"opacity-40 hover:opacity-100\": Boolean(\n                        props.disconnectedItemIds?.has(item.id),\n                      ),\n                    }}\n                    tabindex=\"0\"\n                    onPointerDown={(event) => event.stopPropagation()}\n                    onClick={(event) => {\n                      event.stopPropagation();\n                      props.onSelectItem?.(item);\n                      setConfirmedCopyItemId(item.id);\n                      clearTimeout(copyItemFeedbackTimeout);\n                      copyItemFeedbackTimeout = setTimeout(() => {\n                        setConfirmedCopyItemId(null);\n                      }, FEEDBACK_DURATION_MS);\n                    }}\n                    onKeyDown={(event) => {\n                      if (\n                        event.code === \"Space\" &&\n                        event.currentTarget === event.target\n                      ) {\n                        event.preventDefault();\n                        event.stopPropagation();\n                        props.onSelectItem?.(item);\n                      }\n                    }}\n                    onMouseEnter={(event) => {\n                      if (!props.disconnectedItemIds?.has(item.id)) {\n                        props.onItemHover?.(item.id);\n                      }\n                      updateHighlight(event.currentTarget);\n                    }}\n                    onMouseLeave={() => {\n                      props.onItemHover?.(null);\n                      clearHighlight();\n                    }}\n                    onFocus={(event) => updateHighlight(event.currentTarget)}\n                    onBlur={clearHighlight}\n                  >\n                    <span class=\"flex flex-col min-w-0 flex-1\">\n                      <span class=\"text-[12px] leading-4 font-sans font-medium text-black truncate\">\n                        {getHistoryItemDisplayName(item)}\n                      </span>\n                      <Show when={item.commentText}>\n                        <span class=\"text-[11px] leading-3 font-sans text-black/40 truncate mt-0.5\">\n                          {item.commentText}\n                        </span>\n                      </Show>\n                    </span>\n                    <span class=\"shrink-0 grid\">\n                      <span class=\"text-[10px] font-sans text-black/25 group-hover:invisible group-focus-within:invisible [grid-area:1/1] flex items-center justify-end\">\n                        {formatRelativeTime(item.timestamp)}\n                      </span>\n                      <span class=\"invisible group-hover:visible group-focus-within:visible [grid-area:1/1] flex items-center justify-end gap-1.5\">\n                        <button\n                          data-react-grab-ignore-events\n                          data-react-grab-history-item-remove\n                          class={cn(ITEM_ACTION_CLASS, \"hover:text-[#B91C1C]\")}\n                          onClick={(event) => {\n                            event.stopPropagation();\n                            props.onRemoveItem?.(item);\n                          }}\n                        >\n                          <IconTrash size={DROPDOWN_ICON_SIZE_PX} />\n                        </button>\n                        <button\n                          data-react-grab-ignore-events\n                          data-react-grab-history-item-copy\n                          class={cn(ITEM_ACTION_CLASS, \"hover:text-black/60\")}\n                          onClick={(event) => {\n                            event.stopPropagation();\n                            props.onCopyItem?.(item);\n                            setConfirmedCopyItemId(item.id);\n                            clearTimeout(copyItemFeedbackTimeout);\n                            copyItemFeedbackTimeout = setTimeout(() => {\n                              setConfirmedCopyItemId(null);\n                            }, FEEDBACK_DURATION_MS);\n                          }}\n                        >\n                          <Show\n                            when={confirmedCopyItemId() === item.id}\n                            fallback={<IconCopy size={DROPDOWN_ICON_SIZE_PX} />}\n                          >\n                            <IconCheck\n                              size={DROPDOWN_ICON_SIZE_PX}\n                              class=\"text-black\"\n                            />\n                          </Show>\n                        </button>\n                      </span>\n                    </span>\n                  </div>\n                )}\n              </For>\n            </div>\n          </div>\n        </div>\n      </div>\n    </Show>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/icons/icon-check.tsx",
    "content": "import type { Component } from \"solid-js\";\n\ninterface IconCheckProps {\n  size?: number;\n  class?: string;\n}\n\nexport const IconCheck: Component<IconCheckProps> = (props) => {\n  const size = () => props.size ?? 21;\n\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width={size()}\n      height={(size() * 20.1848) / 20.5381}\n      viewBox=\"0 0 21 21\"\n      fill=\"none\"\n      class={props.class}\n    >\n      <g clip-path=\"url(#clip0_icon_check)\">\n        <path\n          d=\"M20.1767 10.0875C20.1767 15.6478 15.6576 20.175 10.0875 20.175C4.52715 20.175 0 15.6478 0 10.0875C0 4.51914 4.52715 0 10.0875 0C15.6576 0 20.1767 4.51914 20.1767 10.0875ZM13.0051 6.23867L8.96699 12.7041L7.08476 10.3143C6.83358 9.99199 6.59941 9.88828 6.28984 9.88828C5.79414 9.88828 5.39961 10.2918 5.39961 10.7893C5.39961 11.0367 5.48925 11.2621 5.66386 11.4855L8.05703 14.3967C8.33027 14.7508 8.63183 14.9103 8.99902 14.9103C9.36445 14.9103 9.68105 14.7312 9.90546 14.3896L14.4742 7.27206C14.6107 7.04765 14.7289 6.80898 14.7289 6.58359C14.7289 6.07187 14.281 5.72968 13.7934 5.72968C13.4937 5.72968 13.217 5.90527 13.0051 6.23867Z\"\n          fill=\"currentColor\"\n        />\n      </g>\n      <defs>\n        <clipPath id=\"clip0_icon_check\">\n          <rect width=\"20.5381\" height=\"20.1848\" fill=\"white\" />\n        </clipPath>\n      </defs>\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/icons/icon-chevron.tsx",
    "content": "import type { Component } from \"solid-js\";\n\ninterface IconChevronProps {\n  size?: number;\n  class?: string;\n}\n\nexport const IconChevron: Component<IconChevronProps> = (props) => {\n  const size = () => props.size ?? 12;\n\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width={size()}\n      height={size()}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      stroke-width=\"2.5\"\n      stroke-linecap=\"round\"\n      stroke-linejoin=\"round\"\n      class={props.class}\n    >\n      <path d=\"m18 15-6-6-6 6\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/icons/icon-clock.tsx",
    "content": "import type { Component } from \"solid-js\";\n\ninterface IconClockProps {\n  size?: number;\n  class?: string;\n}\n\nexport const IconClock: Component<IconClockProps> = (props) => {\n  const size = () => props.size ?? 14;\n\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width={size()}\n      height={size()}\n      viewBox=\"0 0 24 24\"\n      fill=\"currentColor\"\n      class={props.class}\n    >\n      <path\n        fill-rule=\"evenodd\"\n        clip-rule=\"evenodd\"\n        d=\"M12 1.25C6.06294 1.25 1.25 6.06294 1.25 12C1.25 17.9371 6.06294 22.75 12 22.75C17.9371 22.75 22.75 17.9371 22.75 12C22.75 6.06294 17.9371 1.25 12 1.25ZM13 8C13 7.44772 12.5523 7 12 7C11.4477 7 11 7.44772 11 8V12C11 12.2652 11.1054 12.5196 11.2929 12.7071L13.2929 14.7071C13.6834 15.0976 14.3166 15.0976 14.7071 14.7071C15.0976 14.3166 15.0976 13.6834 14.7071 13.2929L13 11.5858V8Z\"\n      />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/icons/icon-copy.tsx",
    "content": "import type { Component } from \"solid-js\";\n\ninterface IconCopyProps {\n  size?: number;\n  class?: string;\n}\n\nexport const IconCopy: Component<IconCopyProps> = (props) => {\n  const size = () => props.size ?? 14;\n\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width={size()}\n      height={size()}\n      viewBox=\"0 0 24 24\"\n      fill=\"currentColor\"\n      class={props.class}\n    >\n      <path d=\"M16.0549 8.25C17.4225 8.24998 18.5248 8.24996 19.3918 8.36652C20.2919 8.48754 21.0497 8.74643 21.6517 9.34835C22.2536 9.95027 22.5125 10.7081 22.6335 11.6083C22.75 12.4752 22.75 13.5775 22.75 14.9451V14.9451V16.0549V16.0549C22.75 17.4225 22.75 18.5248 22.6335 19.3918C22.5125 20.2919 22.2536 21.0497 21.6517 21.6517C21.0497 22.2536 20.2919 22.5125 19.3918 22.6335C18.5248 22.75 17.4225 22.75 16.0549 22.75H16.0549H14.9451H14.9451C13.5775 22.75 12.4752 22.75 11.6082 22.6335C10.7081 22.5125 9.95027 22.2536 9.34835 21.6516C8.74643 21.0497 8.48754 20.2919 8.36652 19.3918C8.24996 18.5248 8.24998 17.4225 8.25 16.0549V16.0549V14.9451V14.9451C8.24998 13.5775 8.24996 12.4752 8.36652 11.6082C8.48754 10.7081 8.74643 9.95027 9.34835 9.34835C9.95027 8.74643 10.7081 8.48754 11.6083 8.36652C12.4752 8.24996 13.5775 8.24998 14.9451 8.25H14.9451H16.0549H16.0549Z\" />\n      <path d=\"M6.75 14.8569C6.74991 13.5627 6.74983 12.3758 6.8799 11.4084C7.0232 10.3425 7.36034 9.21504 8.28769 8.28769C9.21504 7.36034 10.3425 7.0232 11.4084 6.8799C12.3758 6.74983 13.5627 6.74991 14.8569 6.75L17.0931 6.75C17.3891 6.75 17.5371 6.75 17.6261 6.65419C17.7151 6.55838 17.7045 6.4142 17.6833 6.12584C17.6648 5.87546 17.6412 5.63892 17.6111 5.41544C17.4818 4.45589 17.2232 3.6585 16.6718 2.98663C16.4744 2.74612 16.2539 2.52558 16.0134 2.3282C15.3044 1.74638 14.4557 1.49055 13.4248 1.36868C12.4205 1.24998 11.1512 1.24999 9.54893 1.25H9.45109C7.84883 1.24999 6.57947 1.24998 5.57525 1.36868C4.54428 1.49054 3.69558 1.74638 2.98663 2.3282C2.74612 2.52558 2.52558 2.74612 2.3282 2.98663C1.74638 3.69558 1.49055 4.54428 1.36868 5.57525C1.24998 6.57947 1.24999 7.84882 1.25 9.45108V9.54891C1.24999 11.1512 1.24998 12.4205 1.36868 13.4247C1.49054 14.4557 1.74638 15.3044 2.3282 16.0134C2.52558 16.2539 2.74612 16.4744 2.98663 16.6718C3.6585 17.2232 4.45589 17.4818 5.41544 17.6111C5.63892 17.6412 5.87546 17.6648 6.12584 17.6833C6.4142 17.7045 6.55838 17.7151 6.65419 17.6261C6.75 17.5371 6.75 17.3891 6.75 17.0931V14.8569Z\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/icons/icon-ellipsis.tsx",
    "content": "import type { Component } from \"solid-js\";\n\ninterface IconEllipsisProps {\n  size?: number;\n  class?: string;\n}\n\nexport const IconEllipsis: Component<IconEllipsisProps> = (props) => {\n  const size = () => props.size ?? 12;\n\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width={size()}\n      height={size()}\n      viewBox=\"0 0 24 24\"\n      fill=\"currentColor\"\n      class={props.class}\n    >\n      <circle cx=\"5\" cy=\"12\" r=\"2\" />\n      <circle cx=\"12\" cy=\"12\" r=\"2\" />\n      <circle cx=\"19\" cy=\"12\" r=\"2\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/icons/icon-loader.tsx",
    "content": "import type { Component } from \"solid-js\";\n\ninterface IconLoaderProps {\n  size?: number;\n  class?: string;\n}\n\nexport const IconLoader: Component<IconLoaderProps> = (props) => {\n  const size = () => props.size ?? 16;\n\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width={size()}\n      height={size()}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      stroke-width=\"2\"\n      stroke-linecap=\"round\"\n      stroke-linejoin=\"round\"\n      class={props.class}\n    >\n      <path\n        class=\"icon-loader-bar\"\n        style={{ \"animation-delay\": \"0ms\" }}\n        d=\"M12 2v4\"\n      />\n      <path\n        class=\"icon-loader-bar\"\n        style={{ \"animation-delay\": \"-42ms\" }}\n        d=\"M15 6.8l2-3.5\"\n      />\n      <path\n        class=\"icon-loader-bar\"\n        style={{ \"animation-delay\": \"-83ms\" }}\n        d=\"M17.2 9l3.5-2\"\n      />\n      <path\n        class=\"icon-loader-bar\"\n        style={{ \"animation-delay\": \"-125ms\" }}\n        d=\"M18 12h4\"\n      />\n      <path\n        class=\"icon-loader-bar\"\n        style={{ \"animation-delay\": \"-167ms\" }}\n        d=\"M17.2 15l3.5 2\"\n      />\n      <path\n        class=\"icon-loader-bar\"\n        style={{ \"animation-delay\": \"-208ms\" }}\n        d=\"M15 17.2l2 3.5\"\n      />\n      <path\n        class=\"icon-loader-bar\"\n        style={{ \"animation-delay\": \"-250ms\" }}\n        d=\"M12 18v4\"\n      />\n      <path\n        class=\"icon-loader-bar\"\n        style={{ \"animation-delay\": \"-292ms\" }}\n        d=\"M9 17.2l-2 3.5\"\n      />\n      <path\n        class=\"icon-loader-bar\"\n        style={{ \"animation-delay\": \"-333ms\" }}\n        d=\"M6.8 15l-3.5 2\"\n      />\n      <path\n        class=\"icon-loader-bar\"\n        style={{ \"animation-delay\": \"-375ms\" }}\n        d=\"M2 12h4\"\n      />\n      <path\n        class=\"icon-loader-bar\"\n        style={{ \"animation-delay\": \"-417ms\" }}\n        d=\"M6.8 9l-3.5-2\"\n      />\n      <path\n        class=\"icon-loader-bar\"\n        style={{ \"animation-delay\": \"-458ms\" }}\n        d=\"M9 6.8l-2-3.5\"\n      />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/icons/icon-open.tsx",
    "content": "import type { Component } from \"solid-js\";\n\ninterface IconOpenProps {\n  size?: number;\n  class?: string;\n}\n\nexport const IconOpen: Component<IconOpenProps> = (props) => {\n  const size = () => props.size ?? 12;\n\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width={size()}\n      height={size()}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      stroke-linecap=\"round\"\n      stroke-linejoin=\"round\"\n      stroke-width=\"2\"\n      class={props.class}\n    >\n      <path d=\"M12 6H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6\" />\n      <path d=\"M11 13l9-9\" />\n      <path d=\"M15 4h5v5\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/icons/icon-reply.tsx",
    "content": "import type { Component } from \"solid-js\";\n\ninterface IconReplyProps {\n  size?: number;\n  class?: string;\n}\n\nexport const IconReply: Component<IconReplyProps> = (props) => {\n  const size = () => props.size ?? 12;\n\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width={size()}\n      height={size()}\n      viewBox=\"0 0 12 12\"\n      fill=\"none\"\n      class={props.class}\n      style={{ transform: \"rotate(180deg)\" }}\n    >\n      <path\n        d=\"M5 3V1L1 4.5L5 8V6C8 6 10 7 11 10C11 7 9 4 5 3Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/icons/icon-retry.tsx",
    "content": "import type { Component } from \"solid-js\";\n\ninterface IconRetryProps {\n  size?: number;\n  class?: string;\n}\n\nexport const IconRetry: Component<IconRetryProps> = (props) => {\n  const size = () => props.size ?? 12;\n\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width={size()}\n      height={size()}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      class={props.class}\n    >\n      <path\n        d=\"M17.65 6.35C16.2 4.9 14.21 4 12 4C7.58 4 4.01 7.58 4.01 12C4.01 16.42 7.58 20 12 20C15.73 20 18.84 17.45 19.73 14H17.65C16.83 16.33 14.61 18 12 18C8.69 18 6 15.31 6 12C6 8.69 8.69 6 12 6C13.66 6 15.14 6.69 16.22 7.78L13 11H20V4L17.65 6.35Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/icons/icon-return.tsx",
    "content": "import type { Component } from \"solid-js\";\n\ninterface IconReturnProps {\n  size?: number;\n  class?: string;\n}\n\nexport const IconReturn: Component<IconReturnProps> = (props) => {\n  const size = () => props.size ?? 12;\n\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width={size()}\n      height={(size() * 19) / 22}\n      viewBox=\"0 0 22 19\"\n      fill=\"none\"\n      class={props.class}\n    >\n      <path\n        d=\"M6.76263 18.6626C7.48251 18.6626 7.95474 18.1682 7.95474 17.4895C7.95474 17.1207 7.80474 16.8576 7.58683 16.6361L5.3018 14.4137L2.84621 12.3589L2.44374 13.0037L5.92137 13.1622H17.9232C20.4842 13.1622 21.593 12.021 21.593 9.47237V3.66983C21.593 1.10875 20.4842 0 17.9232 0H12.5414C11.8179 0 11.3018 0.545895 11.3018 1.21695C11.3018 1.888 11.8179 2.43389 12.5414 2.43389H17.8424C18.7937 2.43389 19.1897 2.83653 19.1897 3.78784V9.35747C19.1897 10.3257 18.7937 10.7314 17.8424 10.7314H5.92137L2.44374 10.8832L2.84621 11.5281L5.3018 9.47993L7.58683 7.2606C7.80474 7.03914 7.95474 6.7693 7.95474 6.40049C7.95474 5.72854 7.48251 5.22747 6.76263 5.22747C6.46129 5.22747 6.12975 5.36905 5.89231 5.6096L0.376815 11.0425C0.134921 11.2777 0 11.6141 0 11.9452C0 12.2728 0.134921 12.6158 0.376815 12.848L5.89231 18.2871C6.12975 18.5276 6.46129 18.6626 6.76263 18.6626Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/icons/icon-select.tsx",
    "content": "import type { Component } from \"solid-js\";\n\ninterface IconSelectProps {\n  size?: number;\n  class?: string;\n}\n\nexport const IconSelect: Component<IconSelectProps> = (props) => {\n  const size = () => props.size ?? 14;\n\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width={size()}\n      height={size()}\n      viewBox=\"0 0 18 18\"\n      fill=\"currentColor\"\n      class={props.class}\n    >\n      <path\n        opacity=\"0.4\"\n        d=\"M7.65631 10.9565C7.31061 10.0014 7.54012 8.96635 8.25592 8.25195C8.74522 7.76615 9.38771 7.49951 10.0694 7.49951C10.3682 7.49951 10.6641 7.55171 10.9483 7.65381L16.0001 9.49902V4.75C16.0001 3.2334 14.7667 2 13.2501 2H4.75012C3.23352 2 2.00012 3.2334 2.00012 4.75V13.25C2.00012 14.7666 3.23352 16 4.75012 16H9.49962L7.65631 10.9565Z\"\n      />\n      <path d=\"M17.296 11.5694L10.4415 9.06545C10.0431 8.92235 9.61441 9.01658 9.31551 9.31338C9.01671 9.61168 8.92101 10.0429 9.06551 10.4413L11.5704 17.2948C11.7247 17.7191 12.128 18.0004 12.5772 18.0004C12.585 18.0004 12.5918 17.9999 12.5987 17.9999C13.0577 17.9906 13.4591 17.6913 13.5987 17.2543L14.4854 14.4857L17.2559 13.5985C17.6914 13.4589 17.9903 13.057 18 12.599C18.0097 12.141 17.7267 11.7276 17.296 11.5694Z\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/icons/icon-submit.tsx",
    "content": "import type { Component } from \"solid-js\";\n\ninterface IconSubmitProps {\n  size?: number;\n  class?: string;\n}\n\nexport const IconSubmit: Component<IconSubmitProps> = (props) => {\n  const size = () => props.size ?? 12;\n\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width={size()}\n      height={size()}\n      viewBox=\"0 0 12 12\"\n      fill=\"none\"\n      class={props.class}\n    >\n      <path\n        d=\"M6 1L6 11M6 1L2 5M6 1L10 5\"\n        stroke=\"currentColor\"\n        stroke-width=\"1.5\"\n        stroke-linecap=\"round\"\n        stroke-linejoin=\"round\"\n      />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/icons/icon-trash.tsx",
    "content": "import type { Component } from \"solid-js\";\n\ninterface IconTrashProps {\n  size?: number;\n  class?: string;\n}\n\nexport const IconTrash: Component<IconTrashProps> = (props) => {\n  const size = () => props.size ?? 14;\n\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width={size()}\n      height={size()}\n      viewBox=\"0 0 24 24\"\n      fill=\"currentColor\"\n      class={props.class}\n    >\n      <path\n        fill-rule=\"evenodd\"\n        clip-rule=\"evenodd\"\n        d=\"M4.63751 20.1665L3.82444 6.75092L3.73431 5.06621C3.72513 4.89447 3.8619 4.75018 4.03388 4.75018H19.9945C20.1685 4.75018 20.306 4.89769 20.2938 5.07124L20.1756 6.75092L19.3625 20.1665C19.2745 21.618 18.0717 22.7502 16.6176 22.7502H7.38247C5.9283 22.7502 4.72548 21.618 4.63751 20.1665ZM8.74963 16.5002C8.74963 16.9144 9.08542 17.2502 9.49963 17.2502C9.91385 17.2502 10.2496 16.9144 10.2496 16.5002V10.5002C10.2496 10.086 9.91385 9.75018 9.49963 9.75018C9.08542 9.75018 8.74963 10.086 8.74963 10.5002V16.5002ZM14.4996 9.75018C14.9138 9.75018 15.2496 10.086 15.2496 10.5002V16.5002C15.2496 16.9144 14.9138 17.2502 14.4996 17.2502C14.0854 17.2502 13.7496 16.9144 13.7496 16.5002V10.5002C13.7496 10.086 14.0854 9.75018 14.4996 9.75018Z\"\n      />\n      <path\n        fill-rule=\"evenodd\"\n        clip-rule=\"evenodd\"\n        d=\"M8.31879 2.46286C8.63394 1.7275 9.35702 1.2507 10.1571 1.2507H13.8383C14.6383 1.2507 15.3614 1.7275 15.6766 2.46286L16.6569 4.75034H19.2239C19.2903 4.75034 19.3523 4.75034 19.4102 4.7507H19.4637C19.4857 4.74973 19.5079 4.74972 19.5303 4.7507H20.9977C21.55 4.7507 21.9977 5.19842 21.9977 5.7507C21.9977 6.30299 21.55 6.7507 20.9977 6.7507H2.99768C2.4454 6.7507 1.99768 6.30299 1.99768 5.7507C1.99768 5.19842 2.4454 4.7507 2.99768 4.7507H4.46507C4.48746 4.74972 4.50968 4.74973 4.53167 4.7507H4.58469C4.6426 4.75034 4.70457 4.75034 4.77093 4.75034H7.33844L8.31879 2.46286ZM13.8903 3.37192L14.481 4.75034H9.5144L10.1052 3.37192C10.1367 3.29838 10.209 3.2507 10.289 3.2507L13.7064 3.2507C13.7864 3.2507 13.8587 3.29838 13.8903 3.37192Z\"\n      />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/kbd.tsx",
    "content": "import type { Component, JSX } from \"solid-js\";\n\nexport const Kbd: Component<{ children: JSX.Element }> = (props) => (\n  <kbd class=\"inline-flex items-center justify-center px-[3px] h-3.5 rounded-sm [border-width:0.5px] border-solid border-[#B3B3B3] text-black/70 text-[10px] font-medium leading-none\">\n    {props.children}\n  </kbd>\n);\n"
  },
  {
    "path": "packages/react-grab/src/components/overlay-canvas.tsx",
    "content": "import { createEffect, onCleanup, onMount, on } from \"solid-js\";\nimport type { Component } from \"solid-js\";\nimport type {\n  OverlayBounds,\n  SelectionLabelInstance,\n  AgentSession,\n} from \"../types.js\";\nimport { lerp } from \"../utils/lerp.js\";\nimport {\n  SELECTION_LERP_FACTOR,\n  FEEDBACK_DURATION_MS,\n  DRAG_LERP_FACTOR,\n  LERP_CONVERGENCE_THRESHOLD_PX,\n  FADE_OUT_BUFFER_MS,\n  MIN_DEVICE_PIXEL_RATIO,\n  Z_INDEX_OVERLAY_CANVAS,\n  OVERLAY_BORDER_COLOR_DRAG,\n  OVERLAY_FILL_COLOR_DRAG,\n  OPACITY_CONVERGENCE_THRESHOLD,\n  OVERLAY_BORDER_COLOR_DEFAULT,\n  OVERLAY_FILL_COLOR_DEFAULT,\n} from \"../constants.js\";\nimport {\n  nativeCancelAnimationFrame,\n  nativeRequestAnimationFrame,\n} from \"../utils/native-raf.js\";\nimport { supportsDisplayP3 } from \"../utils/supports-display-p3.js\";\n\nconst LAYER_STYLES = {\n  drag: {\n    borderColor: OVERLAY_BORDER_COLOR_DRAG,\n    fillColor: OVERLAY_FILL_COLOR_DRAG,\n    lerpFactor: DRAG_LERP_FACTOR,\n  },\n  selection: {\n    borderColor: OVERLAY_BORDER_COLOR_DEFAULT,\n    fillColor: OVERLAY_FILL_COLOR_DEFAULT,\n    lerpFactor: SELECTION_LERP_FACTOR,\n  },\n  grabbed: {\n    borderColor: OVERLAY_BORDER_COLOR_DEFAULT,\n    fillColor: OVERLAY_FILL_COLOR_DEFAULT,\n    lerpFactor: SELECTION_LERP_FACTOR,\n  },\n  processing: {\n    borderColor: OVERLAY_BORDER_COLOR_DEFAULT,\n    fillColor: OVERLAY_FILL_COLOR_DEFAULT,\n    lerpFactor: SELECTION_LERP_FACTOR,\n  },\n} as const;\n\ntype LayerName = \"drag\" | \"selection\" | \"grabbed\" | \"processing\";\n\ninterface OffscreenLayer {\n  canvas: OffscreenCanvas | null;\n  context: OffscreenCanvasRenderingContext2D | null;\n}\n\ninterface AnimatedBounds {\n  id: string;\n  current: { x: number; y: number; width: number; height: number };\n  target: { x: number; y: number; width: number; height: number };\n  borderRadius: number;\n  opacity: number;\n  targetOpacity: number;\n  createdAt?: number;\n  isInitialized: boolean;\n}\n\nexport interface OverlayCanvasProps {\n  selectionVisible?: boolean;\n  selectionBounds?: OverlayBounds;\n  selectionBoundsMultiple?: OverlayBounds[];\n  selectionIsFading?: boolean;\n  selectionShouldSnap?: boolean;\n\n  dragVisible?: boolean;\n  dragBounds?: OverlayBounds;\n\n  grabbedBoxes?: Array<{\n    id: string;\n    bounds: OverlayBounds;\n    createdAt: number;\n  }>;\n\n  agentSessions?: Map<string, AgentSession>;\n\n  labelInstances?: SelectionLabelInstance[];\n}\n\nexport const OverlayCanvas: Component<OverlayCanvasProps> = (props) => {\n  let canvasRef: HTMLCanvasElement | undefined;\n  let mainContext: CanvasRenderingContext2D | null = null;\n  let canvasWidth = 0;\n  let canvasHeight = 0;\n  let devicePixelRatio = 1;\n  let animationFrameId: number | null = null;\n\n  const layers: Record<LayerName, OffscreenLayer> = {\n    drag: { canvas: null, context: null },\n    selection: { canvas: null, context: null },\n    grabbed: { canvas: null, context: null },\n    processing: { canvas: null, context: null },\n  };\n\n  let selectionAnimations: AnimatedBounds[] = [];\n  let dragAnimation: AnimatedBounds | null = null;\n  let grabbedAnimations: AnimatedBounds[] = [];\n  let processingAnimations: AnimatedBounds[] = [];\n\n  const canvasColorSpace: PredefinedColorSpace = supportsDisplayP3()\n    ? \"display-p3\"\n    : \"srgb\";\n\n  const createOffscreenLayer = (\n    layerWidth: number,\n    layerHeight: number,\n    scaleFactor: number,\n  ): OffscreenLayer => {\n    const canvas = new OffscreenCanvas(\n      layerWidth * scaleFactor,\n      layerHeight * scaleFactor,\n    );\n    const context = canvas.getContext(\"2d\", { colorSpace: canvasColorSpace });\n    if (context) {\n      context.scale(scaleFactor, scaleFactor);\n    }\n    return { canvas, context };\n  };\n\n  const initializeCanvas = () => {\n    if (!canvasRef) return;\n\n    devicePixelRatio = Math.max(\n      window.devicePixelRatio || 1,\n      MIN_DEVICE_PIXEL_RATIO,\n    );\n    canvasWidth = window.innerWidth;\n    canvasHeight = window.innerHeight;\n\n    canvasRef.width = canvasWidth * devicePixelRatio;\n    canvasRef.height = canvasHeight * devicePixelRatio;\n    canvasRef.style.width = `${canvasWidth}px`;\n    canvasRef.style.height = `${canvasHeight}px`;\n\n    mainContext = canvasRef.getContext(\"2d\", { colorSpace: canvasColorSpace });\n    if (mainContext) {\n      mainContext.scale(devicePixelRatio, devicePixelRatio);\n    }\n\n    for (const layerName of Object.keys(layers) as LayerName[]) {\n      layers[layerName] = createOffscreenLayer(\n        canvasWidth,\n        canvasHeight,\n        devicePixelRatio,\n      );\n    }\n  };\n\n  const parseBorderRadiusValue = (borderRadius: string): number => {\n    if (!borderRadius) return 0;\n    const match = borderRadius.match(/^(\\d+(?:\\.\\d+)?)/);\n    return match ? parseFloat(match[1]) : 0;\n  };\n\n  const createAnimatedBounds = (\n    id: string,\n    bounds: OverlayBounds,\n    options?: { createdAt?: number; opacity?: number; targetOpacity?: number },\n  ): AnimatedBounds => ({\n    id,\n    current: {\n      x: bounds.x,\n      y: bounds.y,\n      width: bounds.width,\n      height: bounds.height,\n    },\n    target: {\n      x: bounds.x,\n      y: bounds.y,\n      width: bounds.width,\n      height: bounds.height,\n    },\n    borderRadius: parseBorderRadiusValue(bounds.borderRadius),\n    opacity: options?.opacity ?? 1,\n    targetOpacity: options?.targetOpacity ?? options?.opacity ?? 1,\n    createdAt: options?.createdAt,\n    isInitialized: true,\n  });\n\n  const updateAnimationTarget = (\n    animation: AnimatedBounds,\n    bounds: OverlayBounds,\n    targetOpacity?: number,\n  ) => {\n    animation.target = {\n      x: bounds.x,\n      y: bounds.y,\n      width: bounds.width,\n      height: bounds.height,\n    };\n    animation.borderRadius = parseBorderRadiusValue(bounds.borderRadius);\n    if (targetOpacity !== undefined) {\n      animation.targetOpacity = targetOpacity;\n    }\n  };\n\n  const resolveBoundsArray = (\n    instance: SelectionLabelInstance,\n  ): OverlayBounds[] => instance.boundsMultiple ?? [instance.bounds];\n\n  const drawRoundedRectangle = (\n    context: OffscreenCanvasRenderingContext2D,\n    rectX: number,\n    rectY: number,\n    rectWidth: number,\n    rectHeight: number,\n    cornerRadius: number,\n    fillColor: string,\n    strokeColor: string,\n    opacity: number = 1,\n  ) => {\n    if (rectWidth <= 0 || rectHeight <= 0) return;\n\n    const maxCornerRadius = Math.min(rectWidth / 2, rectHeight / 2);\n    const clampedCornerRadius = Math.min(cornerRadius, maxCornerRadius);\n\n    context.globalAlpha = opacity;\n    context.beginPath();\n    if (clampedCornerRadius > 0) {\n      context.roundRect(\n        rectX,\n        rectY,\n        rectWidth,\n        rectHeight,\n        clampedCornerRadius,\n      );\n    } else {\n      context.rect(rectX, rectY, rectWidth, rectHeight);\n    }\n    context.fillStyle = fillColor;\n    context.fill();\n    context.strokeStyle = strokeColor;\n    context.lineWidth = 1;\n    context.stroke();\n    context.globalAlpha = 1;\n  };\n\n  const renderDragLayer = () => {\n    const layer = layers.drag;\n    if (!layer.context) return;\n\n    const context = layer.context;\n    context.clearRect(0, 0, canvasWidth, canvasHeight);\n\n    if (!props.dragVisible || !dragAnimation) return;\n\n    const style = LAYER_STYLES.drag;\n    drawRoundedRectangle(\n      context,\n      dragAnimation.current.x,\n      dragAnimation.current.y,\n      dragAnimation.current.width,\n      dragAnimation.current.height,\n      dragAnimation.borderRadius,\n      style.fillColor,\n      style.borderColor,\n    );\n  };\n\n  const renderSelectionLayer = () => {\n    const layer = layers.selection;\n    if (!layer.context) return;\n\n    const context = layer.context;\n    context.clearRect(0, 0, canvasWidth, canvasHeight);\n\n    if (!props.selectionVisible) return;\n\n    const style = LAYER_STYLES.selection;\n\n    for (const animation of selectionAnimations) {\n      const effectiveOpacity = props.selectionIsFading ? 0 : animation.opacity;\n      drawRoundedRectangle(\n        context,\n        animation.current.x,\n        animation.current.y,\n        animation.current.width,\n        animation.current.height,\n        animation.borderRadius,\n        style.fillColor,\n        style.borderColor,\n        effectiveOpacity,\n      );\n    }\n  };\n\n  const renderBoundsLayer = (\n    layerName: keyof typeof LAYER_STYLES,\n    animations: AnimatedBounds[],\n  ) => {\n    const layer = layers[layerName];\n    if (!layer.context) return;\n\n    const context = layer.context;\n    context.clearRect(0, 0, canvasWidth, canvasHeight);\n\n    const style = LAYER_STYLES[layerName];\n\n    for (const animation of animations) {\n      drawRoundedRectangle(\n        context,\n        animation.current.x,\n        animation.current.y,\n        animation.current.width,\n        animation.current.height,\n        animation.borderRadius,\n        style.fillColor,\n        style.borderColor,\n        animation.opacity,\n      );\n    }\n  };\n\n  const compositeAllLayers = () => {\n    if (!mainContext || !canvasRef) return;\n\n    mainContext.setTransform(1, 0, 0, 1, 0, 0);\n    mainContext.clearRect(0, 0, canvasRef.width, canvasRef.height);\n    mainContext.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);\n\n    renderDragLayer();\n    renderSelectionLayer();\n    renderBoundsLayer(\"grabbed\", grabbedAnimations);\n    renderBoundsLayer(\"processing\", processingAnimations);\n\n    const layerRenderOrder: LayerName[] = [\n      \"drag\",\n      \"selection\",\n      \"grabbed\",\n      \"processing\",\n    ];\n    for (const layerName of layerRenderOrder) {\n      const layer = layers[layerName];\n      if (layer.canvas) {\n        mainContext.drawImage(layer.canvas, 0, 0, canvasWidth, canvasHeight);\n      }\n    }\n  };\n\n  const interpolateBounds = (\n    animation: AnimatedBounds,\n    lerpFactor: number,\n    options?: { interpolateOpacity?: boolean },\n  ): boolean => {\n    const lerpedX = lerp(animation.current.x, animation.target.x, lerpFactor);\n    const lerpedY = lerp(animation.current.y, animation.target.y, lerpFactor);\n    const lerpedWidth = lerp(\n      animation.current.width,\n      animation.target.width,\n      lerpFactor,\n    );\n    const lerpedHeight = lerp(\n      animation.current.height,\n      animation.target.height,\n      lerpFactor,\n    );\n\n    const hasBoundsConverged =\n      Math.abs(lerpedX - animation.target.x) < LERP_CONVERGENCE_THRESHOLD_PX &&\n      Math.abs(lerpedY - animation.target.y) < LERP_CONVERGENCE_THRESHOLD_PX &&\n      Math.abs(lerpedWidth - animation.target.width) <\n        LERP_CONVERGENCE_THRESHOLD_PX &&\n      Math.abs(lerpedHeight - animation.target.height) <\n        LERP_CONVERGENCE_THRESHOLD_PX;\n\n    animation.current.x = hasBoundsConverged ? animation.target.x : lerpedX;\n    animation.current.y = hasBoundsConverged ? animation.target.y : lerpedY;\n    animation.current.width = hasBoundsConverged\n      ? animation.target.width\n      : lerpedWidth;\n    animation.current.height = hasBoundsConverged\n      ? animation.target.height\n      : lerpedHeight;\n\n    let hasOpacityConverged = true;\n    if (options?.interpolateOpacity) {\n      const lerpedOpacity = lerp(\n        animation.opacity,\n        animation.targetOpacity,\n        lerpFactor,\n      );\n      const opacityThreshold = OPACITY_CONVERGENCE_THRESHOLD;\n      hasOpacityConverged =\n        Math.abs(lerpedOpacity - animation.targetOpacity) < opacityThreshold;\n      animation.opacity = hasOpacityConverged\n        ? animation.targetOpacity\n        : lerpedOpacity;\n    }\n\n    return !hasBoundsConverged || !hasOpacityConverged;\n  };\n\n  const runAnimationFrame = () => {\n    let shouldContinueAnimating = false;\n\n    if (dragAnimation?.isInitialized) {\n      if (interpolateBounds(dragAnimation, LAYER_STYLES.drag.lerpFactor)) {\n        shouldContinueAnimating = true;\n      }\n    }\n\n    for (const animation of selectionAnimations) {\n      if (animation.isInitialized) {\n        if (interpolateBounds(animation, LAYER_STYLES.selection.lerpFactor)) {\n          shouldContinueAnimating = true;\n        }\n      }\n    }\n\n    const currentTimestamp = Date.now();\n    grabbedAnimations = grabbedAnimations.filter((animation) => {\n      const isLabelAnimation = animation.id.startsWith(\"label-\");\n\n      if (animation.isInitialized) {\n        const isStillAnimating = interpolateBounds(\n          animation,\n          LAYER_STYLES.grabbed.lerpFactor,\n          { interpolateOpacity: isLabelAnimation },\n        );\n        if (isStillAnimating) {\n          shouldContinueAnimating = true;\n        }\n      }\n\n      if (animation.createdAt) {\n        const elapsed = currentTimestamp - animation.createdAt;\n        const fadeOutDeadline = FEEDBACK_DURATION_MS + FADE_OUT_BUFFER_MS;\n\n        if (elapsed >= fadeOutDeadline) {\n          return false;\n        }\n\n        if (elapsed > FEEDBACK_DURATION_MS) {\n          const fadeProgress =\n            (elapsed - FEEDBACK_DURATION_MS) / FADE_OUT_BUFFER_MS;\n          animation.opacity = 1 - fadeProgress;\n          shouldContinueAnimating = true;\n        }\n\n        return true;\n      }\n\n      if (isLabelAnimation) {\n        const hasOpacityConverged =\n          Math.abs(animation.opacity - animation.targetOpacity) <\n          OPACITY_CONVERGENCE_THRESHOLD;\n        if (hasOpacityConverged && animation.targetOpacity === 0) {\n          return false;\n        }\n        return true;\n      }\n\n      return animation.opacity > 0;\n    });\n\n    for (const animation of processingAnimations) {\n      if (animation.isInitialized) {\n        if (interpolateBounds(animation, LAYER_STYLES.processing.lerpFactor)) {\n          shouldContinueAnimating = true;\n        }\n      }\n    }\n\n    compositeAllLayers();\n\n    if (shouldContinueAnimating) {\n      animationFrameId = nativeRequestAnimationFrame(runAnimationFrame);\n    } else {\n      animationFrameId = null;\n    }\n  };\n\n  const scheduleAnimationFrame = () => {\n    if (animationFrameId !== null) return;\n    animationFrameId = nativeRequestAnimationFrame(runAnimationFrame);\n  };\n\n  const handleWindowResize = () => {\n    initializeCanvas();\n    scheduleAnimationFrame();\n  };\n\n  createEffect(\n    on(\n      () =>\n        [\n          props.selectionVisible,\n          props.selectionBounds,\n          props.selectionBoundsMultiple,\n          props.selectionIsFading,\n          props.selectionShouldSnap,\n        ] as const,\n      ([isVisible, singleBounds, multipleBounds, , shouldSnap]) => {\n        if (\n          !isVisible ||\n          (!singleBounds && (!multipleBounds || multipleBounds.length === 0))\n        ) {\n          selectionAnimations = [];\n          scheduleAnimationFrame();\n          return;\n        }\n\n        const boundsToRender =\n          multipleBounds && multipleBounds.length > 0\n            ? multipleBounds\n            : singleBounds\n              ? [singleBounds]\n              : [];\n\n        selectionAnimations = boundsToRender.map((bounds, index) => {\n          const animationId = `selection-${index}`;\n          const existingAnimation = selectionAnimations.find(\n            (animation) => animation.id === animationId,\n          );\n\n          if (existingAnimation) {\n            updateAnimationTarget(existingAnimation, bounds);\n            if (shouldSnap) {\n              existingAnimation.current = { ...existingAnimation.target };\n            }\n            return existingAnimation;\n          }\n\n          return createAnimatedBounds(animationId, bounds);\n        });\n\n        scheduleAnimationFrame();\n      },\n    ),\n  );\n\n  createEffect(\n    on(\n      () => [props.dragVisible, props.dragBounds] as const,\n      ([isVisible, bounds]) => {\n        if (!isVisible || !bounds) {\n          dragAnimation = null;\n          scheduleAnimationFrame();\n          return;\n        }\n\n        if (dragAnimation) {\n          updateAnimationTarget(dragAnimation, bounds);\n        } else {\n          dragAnimation = createAnimatedBounds(\"drag\", bounds);\n        }\n\n        scheduleAnimationFrame();\n      },\n    ),\n  );\n\n  createEffect(\n    on(\n      () => props.grabbedBoxes,\n      (grabbedBoxes) => {\n        const boxesToProcess = grabbedBoxes ?? [];\n        const activeBoxIds = new Set(boxesToProcess.map((box) => box.id));\n        const existingAnimationIds = new Set(\n          grabbedAnimations.map((animation) => animation.id),\n        );\n\n        for (const box of boxesToProcess) {\n          if (!existingAnimationIds.has(box.id)) {\n            grabbedAnimations.push(\n              createAnimatedBounds(box.id, box.bounds, {\n                createdAt: box.createdAt,\n              }),\n            );\n          }\n        }\n\n        for (const animation of grabbedAnimations) {\n          const matchingBox = boxesToProcess.find(\n            (box) => box.id === animation.id,\n          );\n          if (matchingBox) {\n            updateAnimationTarget(animation, matchingBox.bounds);\n          }\n        }\n\n        grabbedAnimations = grabbedAnimations.filter((animation) => {\n          if (animation.id.startsWith(\"label-\")) {\n            return true;\n          }\n          return activeBoxIds.has(animation.id);\n        });\n\n        scheduleAnimationFrame();\n      },\n    ),\n  );\n\n  createEffect(\n    on(\n      () => props.agentSessions,\n      (agentSessions) => {\n        if (!agentSessions || agentSessions.size === 0) {\n          processingAnimations = [];\n          scheduleAnimationFrame();\n          return;\n        }\n\n        const updatedAnimations: AnimatedBounds[] = [];\n\n        for (const [sessionId, session] of agentSessions) {\n          for (let index = 0; index < session.selectionBounds.length; index++) {\n            const bounds = session.selectionBounds[index];\n            const animationId = `processing-${sessionId}-${index}`;\n            const existingAnimation = processingAnimations.find(\n              (animation) => animation.id === animationId,\n            );\n\n            if (existingAnimation) {\n              updateAnimationTarget(existingAnimation, bounds);\n              updatedAnimations.push(existingAnimation);\n            } else {\n              updatedAnimations.push(createAnimatedBounds(animationId, bounds));\n            }\n          }\n        }\n\n        processingAnimations = updatedAnimations;\n        scheduleAnimationFrame();\n      },\n    ),\n  );\n\n  createEffect(\n    on(\n      () => props.labelInstances,\n      (labelInstances) => {\n        const instancesToProcess = labelInstances ?? [];\n\n        for (const instance of instancesToProcess) {\n          const boundsToRender = resolveBoundsArray(instance);\n          const targetOpacity = instance.status === \"fading\" ? 0 : 1;\n\n          for (let index = 0; index < boundsToRender.length; index++) {\n            const bounds = boundsToRender[index];\n            const animationId = `label-${instance.id}-${index}`;\n            const existingAnimation = grabbedAnimations.find(\n              (animation) => animation.id === animationId,\n            );\n\n            if (existingAnimation) {\n              updateAnimationTarget(existingAnimation, bounds, targetOpacity);\n            } else {\n              grabbedAnimations.push(\n                createAnimatedBounds(animationId, bounds, {\n                  opacity: 1,\n                  targetOpacity,\n                }),\n              );\n            }\n          }\n        }\n\n        const activeLabelIds = new Set<string>();\n        for (const instance of instancesToProcess) {\n          const boundsToRender = resolveBoundsArray(instance);\n          for (let index = 0; index < boundsToRender.length; index++) {\n            activeLabelIds.add(`label-${instance.id}-${index}`);\n          }\n        }\n\n        grabbedAnimations = grabbedAnimations.filter((animation) => {\n          if (animation.id.startsWith(\"label-\")) {\n            return activeLabelIds.has(animation.id);\n          }\n          return true;\n        });\n\n        scheduleAnimationFrame();\n      },\n    ),\n  );\n\n  onMount(() => {\n    initializeCanvas();\n    scheduleAnimationFrame();\n\n    window.addEventListener(\"resize\", handleWindowResize);\n\n    let currentDprMediaQuery: MediaQueryList | null = null;\n\n    const handleDevicePixelRatioChange = () => {\n      const newDevicePixelRatio = Math.max(\n        window.devicePixelRatio || 1,\n        MIN_DEVICE_PIXEL_RATIO,\n      );\n      if (newDevicePixelRatio !== devicePixelRatio) {\n        handleWindowResize();\n        setupDprMediaQuery();\n      }\n    };\n\n    const setupDprMediaQuery = () => {\n      if (currentDprMediaQuery) {\n        currentDprMediaQuery.removeEventListener(\n          \"change\",\n          handleDevicePixelRatioChange,\n        );\n      }\n      currentDprMediaQuery = window.matchMedia(\n        `(resolution: ${window.devicePixelRatio}dppx)`,\n      );\n      currentDprMediaQuery.addEventListener(\n        \"change\",\n        handleDevicePixelRatioChange,\n      );\n    };\n\n    setupDprMediaQuery();\n\n    onCleanup(() => {\n      window.removeEventListener(\"resize\", handleWindowResize);\n      if (currentDprMediaQuery) {\n        currentDprMediaQuery.removeEventListener(\n          \"change\",\n          handleDevicePixelRatioChange,\n        );\n      }\n      if (animationFrameId !== null) {\n        nativeCancelAnimationFrame(animationFrameId);\n      }\n    });\n  });\n\n  return (\n    <canvas\n      ref={canvasRef}\n      data-react-grab-overlay-canvas\n      style={{\n        position: \"fixed\",\n        top: \"0\",\n        left: \"0\",\n        \"pointer-events\": \"none\",\n        \"z-index\": String(Z_INDEX_OVERLAY_CANVAS),\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/renderer.tsx",
    "content": "import { Show, Index } from \"solid-js\";\nimport type { Component } from \"solid-js\";\nimport type { AgentSession, ReactGrabRendererProps } from \"../types.js\";\nimport {\n  FROZEN_GLOW_COLOR,\n  FROZEN_GLOW_EDGE_PX,\n  Z_INDEX_OVERLAY_CANVAS,\n} from \"../constants.js\";\nimport { openFile } from \"../utils/open-file.js\";\nimport { isElementConnected } from \"../utils/is-element-connected.js\";\nimport { OverlayCanvas } from \"./overlay-canvas.js\";\nimport { SelectionLabel } from \"./selection-label/index.js\";\nimport { Toolbar } from \"./toolbar/index.js\";\nimport { ToolbarMenu } from \"./toolbar/toolbar-menu.js\";\nimport { ContextMenu } from \"./context-menu.js\";\nimport { HistoryDropdown } from \"./history-dropdown.js\";\nimport { ClearHistoryPrompt } from \"./clear-history-prompt.js\";\n\nexport const ReactGrabRenderer: Component<ReactGrabRendererProps> = (props) => {\n  const getSessionStatus = (\n    session: AgentSession,\n  ): \"copying\" | \"copied\" | \"fading\" => {\n    if (session.isFading) {\n      return \"fading\";\n    }\n\n    if (session.isStreaming) {\n      return \"copying\";\n    }\n\n    return \"copied\";\n  };\n\n  return (\n    <>\n      <OverlayCanvas\n        selectionVisible={props.selectionVisible}\n        selectionBounds={props.selectionBounds}\n        selectionBoundsMultiple={props.selectionBoundsMultiple}\n        selectionShouldSnap={props.selectionShouldSnap}\n        selectionIsFading={props.selectionLabelStatus === \"fading\"}\n        dragVisible={props.dragVisible}\n        dragBounds={props.dragBounds}\n        grabbedBoxes={props.grabbedBoxes}\n        agentSessions={props.agentSessions}\n        labelInstances={props.labelInstances}\n      />\n\n      <div\n        style={{\n          position: \"fixed\",\n          top: 0,\n          right: 0,\n          bottom: 0,\n          left: 0,\n          \"pointer-events\": \"none\",\n          \"z-index\": Z_INDEX_OVERLAY_CANVAS,\n          opacity: props.isFrozen ? 1 : 0,\n          transition: \"opacity 100ms ease-out\",\n          \"will-change\": \"opacity\",\n          contain: \"strict\",\n          transform: \"translateZ(0)\",\n          \"box-shadow\": `inset 0 0 ${FROZEN_GLOW_EDGE_PX}px ${FROZEN_GLOW_COLOR}`,\n        }}\n      />\n\n      <Index\n        each={\n          props.agentSessions ? Array.from(props.agentSessions.values()) : []\n        }\n      >\n        {(session) => (\n          <Show when={session().selectionBounds.length > 0}>\n            <SelectionLabel\n              tagName={session().tagName}\n              componentName={session().componentName}\n              selectionBounds={session().selectionBounds[0]}\n              mouseX={session().position.x}\n              visible={true}\n              hasAgent={true}\n              status={getSessionStatus(session())}\n              statusText={session().lastStatus || \"Thinking…\"}\n              inputValue={session().context.prompt}\n              previousPrompt={session().context.prompt}\n              supportsUndo={props.supportsUndo}\n              supportsFollowUp={props.supportsFollowUp}\n              dismissButtonText={props.dismissButtonText}\n              onAbort={() => props.onRequestAbortSession?.(session().id)}\n              onDismiss={\n                session().isStreaming\n                  ? undefined\n                  : () => props.onDismissSession?.(session().id)\n              }\n              onUndo={\n                session().isStreaming\n                  ? undefined\n                  : () => props.onUndoSession?.(session().id)\n              }\n              onFollowUpSubmit={\n                session().isStreaming\n                  ? undefined\n                  : (prompt) =>\n                      props.onFollowUpSubmitSession?.(session().id, prompt)\n              }\n              error={session().error}\n              onAcknowledgeError={() =>\n                props.onAcknowledgeSessionError?.(session().id)\n              }\n              onRetry={() => props.onRetrySession?.(session().id)}\n              isPendingAbort={\n                session().isStreaming &&\n                props.pendingAbortSessionId === session().id\n              }\n              onConfirmAbort={() => props.onAbortSession?.(session().id, true)}\n              onCancelAbort={() => props.onAbortSession?.(session().id, false)}\n            />\n          </Show>\n        )}\n      </Index>\n\n      <Show when={props.selectionLabelVisible && props.selectionBounds}>\n        <SelectionLabel\n          tagName={props.selectionTagName}\n          componentName={props.selectionComponentName}\n          elementsCount={props.selectionElementsCount}\n          selectionBounds={props.selectionBounds}\n          mouseX={props.mouseX}\n          visible={props.selectionLabelVisible}\n          isPromptMode={props.isPromptMode}\n          inputValue={props.inputValue}\n          replyToPrompt={props.replyToPrompt}\n          hasAgent={props.hasAgent}\n          status={props.selectionLabelStatus}\n          actionCycleState={props.selectionActionCycleState}\n          arrowNavigationState={props.selectionArrowNavigationState}\n          onArrowNavigationSelect={props.onArrowNavigationSelect}\n          filePath={props.selectionFilePath}\n          onInputChange={props.onInputChange}\n          onSubmit={props.onInputSubmit}\n          onToggleExpand={props.onToggleExpand}\n          isPendingDismiss={props.isPendingDismiss}\n          selectionLabelShakeCount={props.selectionLabelShakeCount}\n          onConfirmDismiss={props.onConfirmDismiss}\n          onCancelDismiss={props.onCancelDismiss}\n          onOpen={() => {\n            if (props.selectionFilePath) {\n              openFile(props.selectionFilePath, props.selectionLineNumber);\n            }\n          }}\n          isContextMenuOpen={props.contextMenuPosition !== null}\n        />\n      </Show>\n\n      <Index each={props.labelInstances ?? []}>\n        {(instance) => (\n          <SelectionLabel\n            tagName={instance().tagName}\n            componentName={instance().componentName}\n            elementsCount={instance().elementsCount}\n            selectionBounds={instance().bounds}\n            mouseX={instance().mouseX}\n            visible={true}\n            status={instance().status}\n            statusText={instance().statusText}\n            hasAgent={Boolean(instance().statusText)}\n            isPromptMode={instance().isPromptMode}\n            inputValue={instance().inputValue}\n            error={instance().errorMessage}\n            hideArrow={instance().hideArrow}\n            onShowContextMenu={(() => {\n              const currentInstance = instance();\n              const hasCompletedStatus =\n                currentInstance.status === \"copied\" ||\n                currentInstance.status === \"fading\";\n              if (\n                !hasCompletedStatus ||\n                !isElementConnected(currentInstance.element)\n              ) {\n                return undefined;\n              }\n              return () =>\n                props.onShowContextMenuInstance?.(currentInstance.id);\n            })()}\n            onHoverChange={(isHovered) =>\n              props.onLabelInstanceHoverChange?.(instance().id, isHovered)\n            }\n          />\n        )}\n      </Index>\n\n      <Show when={props.toolbarVisible !== false}>\n        <Toolbar\n          isActive={props.isActive}\n          isContextMenuOpen={props.contextMenuPosition !== null}\n          onToggle={props.onToggleActive}\n          enabled={props.enabled}\n          onToggleEnabled={props.onToggleEnabled}\n          shakeCount={props.shakeCount}\n          onStateChange={props.onToolbarStateChange}\n          onSubscribeToStateChanges={props.onSubscribeToToolbarStateChanges}\n          onSelectHoverChange={props.onToolbarSelectHoverChange}\n          onContainerRef={props.onToolbarRef}\n          historyItemCount={props.historyItemCount}\n          clockFlashTrigger={props.clockFlashTrigger}\n          hasUnreadHistoryItems={props.hasUnreadHistoryItems}\n          onToggleHistory={props.onToggleHistory}\n          onCopyAll={props.onCopyAll}\n          onCopyAllHover={props.onCopyAllHover}\n          onHistoryButtonHover={props.onHistoryButtonHover}\n          isHistoryDropdownOpen={Boolean(props.historyDropdownPosition)}\n          isHistoryPinned={props.isHistoryPinned}\n          toolbarActions={props.toolbarActions}\n          onToggleMenu={props.onToggleMenu}\n          isMenuOpen={Boolean(props.toolbarMenuPosition)}\n          isClearPromptOpen={Boolean(props.clearPromptPosition)}\n        />\n      </Show>\n\n      <ContextMenu\n        position={props.contextMenuPosition ?? null}\n        selectionBounds={props.contextMenuBounds ?? null}\n        tagName={props.contextMenuTagName}\n        componentName={props.contextMenuComponentName}\n        hasFilePath={props.contextMenuHasFilePath ?? false}\n        actions={props.actions}\n        actionContext={props.actionContext}\n        onDismiss={props.onContextMenuDismiss ?? (() => {})}\n        onHide={props.onContextMenuHide ?? (() => {})}\n      />\n\n      <ToolbarMenu\n        position={props.toolbarMenuPosition ?? null}\n        actions={props.toolbarActions ?? []}\n        onDismiss={props.onToolbarMenuDismiss ?? (() => {})}\n      />\n\n      <ClearHistoryPrompt\n        position={props.clearPromptPosition ?? null}\n        onConfirm={props.onClearHistoryConfirm ?? (() => {})}\n        onCancel={props.onClearHistoryCancel ?? (() => {})}\n      />\n\n      <HistoryDropdown\n        position={props.historyDropdownPosition ?? null}\n        items={props.historyItems ?? []}\n        disconnectedItemIds={props.historyDisconnectedItemIds}\n        onSelectItem={props.onHistoryItemSelect}\n        onRemoveItem={props.onHistoryItemRemove}\n        onCopyItem={props.onHistoryItemCopy}\n        onItemHover={props.onHistoryItemHover}\n        onCopyAll={props.onHistoryCopyAll}\n        onCopyAllHover={props.onHistoryCopyAllHover}\n        onClearAll={props.onHistoryClear}\n        onDismiss={props.onHistoryDismiss}\n        onDropdownHover={props.onHistoryDropdownHover}\n      />\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/selection-label/arrow-navigation-menu.tsx",
    "content": "import { Show, For, createEffect } from \"solid-js\";\nimport type { Component } from \"solid-js\";\nimport type { ArrowNavigationItem } from \"../../types.js\";\nimport { createMenuHighlight } from \"../../utils/create-menu-highlight.js\";\nimport { BottomSection } from \"./bottom-section.js\";\n\ninterface ArrowNavigationMenuProps {\n  items: ArrowNavigationItem[];\n  activeIndex: number;\n  onSelect: (index: number) => void;\n}\n\nexport const ArrowNavigationMenu: Component<ArrowNavigationMenuProps> = (\n  props,\n) => {\n  const {\n    containerRef: highlightContainerRef,\n    highlightRef,\n    updateHighlight,\n    clearHighlight,\n  } = createMenuHighlight();\n\n  let menuItemsRef: HTMLDivElement | undefined;\n  let didPointerMove = false;\n\n  const getMenuItemByIndex = (\n    itemIndex: number,\n  ): HTMLButtonElement | undefined => {\n    if (!menuItemsRef) return undefined;\n    const activeMenuButton = menuItemsRef.querySelector<HTMLButtonElement>(\n      `[data-react-grab-arrow-nav-index=\"${itemIndex}\"]`,\n    );\n    return activeMenuButton ?? undefined;\n  };\n\n  createEffect(() => {\n    void props.items;\n    didPointerMove = false;\n  });\n\n  createEffect(() => {\n    const activeMenuItem = getMenuItemByIndex(props.activeIndex);\n    if (activeMenuItem) {\n      updateHighlight(activeMenuItem);\n    }\n  });\n\n  return (\n    <BottomSection>\n      <div\n        ref={(element) => {\n          menuItemsRef = element;\n          highlightContainerRef(element);\n        }}\n        class=\"relative flex flex-col w-[calc(100%+16px)] -mx-2 -my-1.5\"\n        onPointerMove={() => {\n          didPointerMove = true;\n        }}\n      >\n        <div\n          ref={highlightRef}\n          class=\"pointer-events-none absolute bg-black/5 opacity-0 transition-[top,left,width,height,opacity] duration-75 ease-out\"\n        />\n        <For each={props.items}>\n          {(item, itemIndex) => (\n            <button\n              data-react-grab-ignore-events\n              data-react-grab-arrow-nav-item={item.tagName}\n              data-react-grab-arrow-nav-index={itemIndex()}\n              class=\"relative z-1 contain-layout flex items-center w-full px-2 py-1 cursor-pointer text-left border-none bg-transparent\"\n              onPointerDown={(event) => event.stopPropagation()}\n              onPointerEnter={(event) => {\n                updateHighlight(event.currentTarget);\n                if (didPointerMove) {\n                  props.onSelect(itemIndex());\n                }\n              }}\n              onPointerLeave={() => {\n                const activeMenuItem = getMenuItemByIndex(props.activeIndex);\n                if (activeMenuItem) {\n                  updateHighlight(activeMenuItem);\n                } else {\n                  clearHighlight();\n                }\n              }}\n              onClick={(event) => {\n                event.stopPropagation();\n                props.onSelect(itemIndex());\n              }}\n            >\n              <span\n                class=\"text-[13px] leading-4 h-fit font-medium overflow-hidden text-ellipsis whitespace-nowrap min-w-0 transition-colors\"\n                classList={{\n                  \"text-black\": itemIndex() === props.activeIndex,\n                  \"text-black/30\": itemIndex() !== props.activeIndex,\n                }}\n              >\n                <Show when={item.componentName}>\n                  {item.componentName}\n                  <span class=\"text-black/40\">.</span>\n                </Show>\n                {item.tagName}\n              </span>\n            </button>\n          )}\n        </For>\n      </div>\n    </BottomSection>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/selection-label/arrow.tsx",
    "content": "import type { Component } from \"solid-js\";\nimport type { ArrowProps } from \"../../types.js\";\nimport { getArrowSize } from \"../../utils/get-arrow-size.js\";\n\nexport const Arrow: Component<ArrowProps> = (props) => {\n  const arrowColor = () => props.color ?? \"white\";\n  const isBottom = () => props.position === \"bottom\";\n  const arrowSize = () => getArrowSize(props.labelWidth ?? 0);\n\n  return (\n    <div\n      data-react-grab-arrow\n      class=\"absolute w-0 h-0 z-10\"\n      style={{\n        left: `calc(${props.leftPercent}% + ${props.leftOffsetPx}px)`,\n        top: isBottom() ? \"0\" : undefined,\n        bottom: isBottom() ? undefined : \"0\",\n        transform: isBottom()\n          ? \"translateX(-50%) translateY(-100%)\"\n          : \"translateX(-50%) translateY(100%)\",\n        \"border-left\": `${arrowSize()}px solid transparent`,\n        \"border-right\": `${arrowSize()}px solid transparent`,\n        \"border-bottom\": isBottom()\n          ? `${arrowSize()}px solid ${arrowColor()}`\n          : undefined,\n        \"border-top\": isBottom()\n          ? undefined\n          : `${arrowSize()}px solid ${arrowColor()}`,\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/selection-label/bottom-section.tsx",
    "content": "import type { Component } from \"solid-js\";\nimport type { BottomSectionProps } from \"../../types.js\";\n\nexport const BottomSection: Component<BottomSectionProps> = (props) => (\n  <div class=\"[font-synthesis:none] contain-layout shrink-0 flex flex-col items-start px-2 py-1.5 w-auto h-fit self-stretch [border-top-width:0.5px] border-t-solid border-t-[#D9D9D9] antialiased rounded-t-none rounded-b-[6px]\">\n    {props.children}\n  </div>\n);\n"
  },
  {
    "path": "packages/react-grab/src/components/selection-label/completion-view.tsx",
    "content": "import { Show, createSignal, onMount, onCleanup } from \"solid-js\";\nimport type { Component } from \"solid-js\";\nimport type { CompletionViewProps } from \"../../types.js\";\nimport {\n  FEEDBACK_DURATION_MS,\n  FADE_DURATION_MS,\n  IME_COMPOSING_KEY_CODE,\n  TEXTAREA_MAX_HEIGHT_PX,\n} from \"../../constants.js\";\nimport { autoResizeTextarea } from \"../../utils/auto-resize-textarea.js\";\nimport { confirmationFocusManager } from \"../../utils/confirmation-focus-manager.js\";\nimport { isKeyboardEventTriggeredByInput } from \"../../utils/is-keyboard-event-triggered-by-input.js\";\nimport { IconReply } from \"../icons/icon-reply.jsx\";\nimport { IconReturn } from \"../icons/icon-return.jsx\";\nimport { IconSubmit } from \"../icons/icon-submit.jsx\";\nimport { IconEllipsis } from \"../icons/icon-ellipsis.jsx\";\nimport { cn } from \"../../utils/cn.js\";\nimport { IconCheck } from \"../icons/icon-check.jsx\";\nimport { BottomSection } from \"./bottom-section.js\";\n\ninterface MoreOptionsButtonProps {\n  onClick: () => void;\n}\n\nconst MoreOptionsButton: Component<MoreOptionsButtonProps> = (props) => {\n  return (\n    <button\n      data-react-grab-ignore-events\n      data-react-grab-more-options\n      class=\"flex items-center justify-center size-[18px] rounded-sm cursor-pointer bg-transparent hover:bg-black/10 text-black/30 hover:text-black border-none outline-none p-0 shrink-0 press-scale\"\n      // HACK: Native events with stopImmediatePropagation needed to block document-level handlers in the overlay system\n      on:pointerdown={(event) => {\n        event.stopImmediatePropagation();\n      }}\n      on:click={(event) => {\n        event.stopImmediatePropagation();\n        props.onClick();\n      }}\n    >\n      <IconEllipsis size={14} />\n    </button>\n  );\n};\n\nexport const CompletionView: Component<CompletionViewProps> = (props) => {\n  const instanceId = Symbol();\n  let inputRef: HTMLTextAreaElement | undefined;\n  let fadeTimeoutId: number | undefined;\n  let dismissTimeoutId: number | undefined;\n  const [didCopy, setDidCopy] = createSignal(false);\n  const [isFading, setIsFading] = createSignal(false);\n  const displayStatusText = () => (didCopy() ? \"Copied\" : props.statusText);\n  const [followUpInput, setFollowUpInput] = createSignal(\"\");\n\n  const handleShowContextMenu = () => {\n    if (fadeTimeoutId !== undefined) window.clearTimeout(fadeTimeoutId);\n    if (dismissTimeoutId !== undefined) window.clearTimeout(dismissTimeoutId);\n    setIsFading(true);\n    props.onFadingChange?.(true);\n    props.onShowContextMenu?.();\n  };\n\n  const handleAccept = () => {\n    if (didCopy()) return;\n    setDidCopy(true);\n    props.onCopyStateChange?.();\n    fadeTimeoutId = window.setTimeout(() => {\n      setIsFading(true);\n      props.onFadingChange?.(true);\n      dismissTimeoutId = window.setTimeout(() => {\n        props.onDismiss?.();\n      }, FADE_DURATION_MS);\n    }, FEEDBACK_DURATION_MS - FADE_DURATION_MS);\n  };\n\n  const handleFollowUpSubmit = () => {\n    const prompt = followUpInput().trim();\n    if (prompt && props.onFollowUpSubmit) {\n      props.onFollowUpSubmit(prompt);\n    }\n  };\n\n  const handleInputKeyDown = (event: KeyboardEvent) => {\n    if (event.isComposing || event.keyCode === IME_COMPOSING_KEY_CODE) {\n      return;\n    }\n\n    const isUndoRedo =\n      event.code === \"KeyZ\" && (event.metaKey || event.ctrlKey);\n    const isEnterWithoutShift = event.code === \"Enter\" && !event.shiftKey;\n    const isEscape = event.code === \"Escape\";\n\n    if (!isUndoRedo) {\n      event.stopImmediatePropagation();\n    }\n\n    if (isEnterWithoutShift) {\n      event.preventDefault();\n      const prompt = followUpInput().trim();\n      if (prompt) {\n        handleFollowUpSubmit();\n      } else {\n        handleAccept();\n      }\n    } else if (isEscape) {\n      event.preventDefault();\n      props.onDismiss?.();\n    }\n  };\n\n  const handleKeyDown = (event: KeyboardEvent) => {\n    if (!confirmationFocusManager.isActive(instanceId)) return;\n\n    const isUndo =\n      event.code === \"KeyZ\" &&\n      (event.metaKey || event.ctrlKey) &&\n      !event.shiftKey;\n    const isEnter = event.code === \"Enter\";\n    const isEscape = event.code === \"Escape\";\n\n    if (isUndo && props.supportsUndo && props.onUndo) {\n      event.preventDefault();\n      event.stopPropagation();\n      props.onUndo();\n      return;\n    }\n\n    if (isKeyboardEventTriggeredByInput(event)) return;\n\n    if (isEnter) {\n      event.preventDefault();\n      event.stopPropagation();\n      handleAccept();\n    } else if (isEscape) {\n      event.preventDefault();\n      event.stopPropagation();\n      props.onDismiss?.();\n    }\n  };\n\n  const handleFocus = () => {\n    confirmationFocusManager.claim(instanceId);\n  };\n\n  onMount(() => {\n    confirmationFocusManager.claim(instanceId);\n    window.addEventListener(\"keydown\", handleKeyDown, { capture: true });\n\n    if (props.supportsFollowUp && props.onFollowUpSubmit && inputRef) {\n      inputRef.focus();\n    }\n  });\n\n  onCleanup(() => {\n    confirmationFocusManager.release(instanceId);\n    window.removeEventListener(\"keydown\", handleKeyDown, { capture: true });\n    if (fadeTimeoutId !== undefined) window.clearTimeout(fadeTimeoutId);\n    if (dismissTimeoutId !== undefined) window.clearTimeout(dismissTimeoutId);\n  });\n\n  return (\n    <div\n      data-react-grab-completion\n      class={cn(\n        \"contain-layout shrink-0 flex flex-col justify-center items-end rounded-[10px] antialiased w-fit h-fit max-w-[280px] transition-opacity duration-100 ease-out [font-synthesis:none] [corner-shape:superellipse(1.25)]\",\n        \"bg-white\",\n      )}\n      style={{ opacity: isFading() ? 0 : 1 }}\n      onPointerDown={handleFocus}\n      onClick={handleFocus}\n    >\n      <Show when={!didCopy() && (props.onDismiss || props.onUndo)}>\n        <div class=\"contain-layout shrink-0 flex items-center justify-between gap-2 pt-1.5 pb-1 px-2 w-full h-fit\">\n          <span class=\"text-black text-[13px] leading-4 font-sans font-medium h-fit tabular-nums overflow-hidden text-ellipsis whitespace-nowrap min-w-0\">\n            {displayStatusText()}\n          </span>\n          <div class=\"contain-layout shrink-0 flex items-center gap-2 h-fit\">\n            <Show when={props.onShowContextMenu && !props.supportsFollowUp}>\n              <MoreOptionsButton onClick={handleShowContextMenu} />\n            </Show>\n            <Show when={props.supportsUndo && props.onUndo}>\n              <button\n                data-react-grab-undo\n                class=\"contain-layout shrink-0 flex items-center justify-center px-[3px] py-px rounded-sm bg-[#FEF2F2] cursor-pointer transition-all hover:bg-[#FEE2E2] press-scale h-[17px]\"\n                onClick={() => props.onUndo?.()}\n              >\n                <span class=\"text-[#B91C1C] text-[13px] leading-3.5 font-sans font-medium\">\n                  Undo\n                </span>\n              </button>\n            </Show>\n            <Show when={props.onDismiss}>\n              <button\n                data-react-grab-dismiss\n                class=\"contain-layout shrink-0 flex items-center justify-center gap-1 px-[3px] py-px rounded-sm bg-white [border-width:0.5px] border-solid border-[#B3B3B3] cursor-pointer transition-all hover:bg-[#F5F5F5] press-scale h-[17px]\"\n                onClick={handleAccept}\n                disabled={didCopy()}\n              >\n                <span class=\"text-black text-[13px] leading-3.5 font-sans font-medium\">\n                  {props.dismissButtonText ?? \"Keep\"}\n                </span>\n                <Show when={!didCopy()}>\n                  <IconReturn size={10} class=\"text-black/50\" />\n                </Show>\n              </button>\n            </Show>\n          </div>\n        </div>\n      </Show>\n      <Show when={didCopy() || (!props.onDismiss && !props.onUndo)}>\n        <div class=\"contain-layout shrink-0 flex items-center gap-0.5 py-1.5 px-2 w-full h-fit\">\n          <IconCheck\n            size={14}\n            class=\"text-black/85 shrink-0 animate-success-pop\"\n          />\n          <span class=\"text-black text-[13px] leading-4 font-sans font-medium h-fit tabular-nums overflow-hidden text-ellipsis whitespace-nowrap min-w-0\">\n            {displayStatusText()}\n          </span>\n          <Show when={props.onShowContextMenu && !props.supportsFollowUp}>\n            <MoreOptionsButton onClick={handleShowContextMenu} />\n          </Show>\n        </div>\n      </Show>\n      <Show\n        when={!didCopy() && props.supportsFollowUp && props.onFollowUpSubmit}\n      >\n        <BottomSection>\n          <Show when={props.previousPrompt}>\n            <div class=\"flex items-center gap-1 w-full mb-1 overflow-hidden\">\n              <IconReply size={10} class=\"text-black/30 shrink-0\" />\n              <span class=\"text-black/40 text-[11px] leading-3 font-medium truncate italic\">\n                {props.previousPrompt}\n              </span>\n            </div>\n          </Show>\n          <div\n            class=\"shrink-0 flex justify-between items-end w-full min-h-4\"\n            style={{ \"padding-left\": props.previousPrompt ? \"14px\" : \"0\" }}\n          >\n            <textarea\n              ref={inputRef}\n              data-react-grab-ignore-events\n              data-react-grab-followup-input\n              class=\"text-black text-[13px] leading-4 font-medium bg-transparent border-none outline-none resize-none flex-1 p-0 m-0 wrap-break-word overflow-y-auto\"\n              style={{\n                \"field-sizing\": \"content\",\n                \"min-height\": \"16px\",\n                \"max-height\": `${TEXTAREA_MAX_HEIGHT_PX}px`,\n                \"scrollbar-width\": \"none\",\n              }}\n              value={followUpInput()}\n              onInput={(event) => {\n                autoResizeTextarea(event.target, TEXTAREA_MAX_HEIGHT_PX);\n                setFollowUpInput(event.target.value);\n              }}\n              onKeyDown={handleInputKeyDown}\n              placeholder=\"follow-up\"\n              rows={1}\n            />\n            <button\n              data-react-grab-followup-submit\n              class={cn(\n                \"contain-layout shrink-0 flex items-center justify-center size-4 rounded-full bg-black cursor-pointer ml-1 interactive-scale\",\n                !followUpInput().trim() && \"opacity-35\",\n              )}\n              onClick={handleFollowUpSubmit}\n            >\n              <IconSubmit size={10} class=\"text-white\" />\n            </button>\n          </div>\n        </BottomSection>\n      </Show>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/selection-label/discard-prompt.tsx",
    "content": "import { onMount, onCleanup } from \"solid-js\";\nimport type { Component } from \"solid-js\";\nimport type { DiscardPromptProps } from \"../../types.js\";\nimport { confirmationFocusManager } from \"../../utils/confirmation-focus-manager.js\";\nimport { isKeyboardEventTriggeredByInput } from \"../../utils/is-keyboard-event-triggered-by-input.js\";\nimport { IconReturn } from \"../icons/icon-return.jsx\";\nimport { BottomSection } from \"./bottom-section.js\";\n\nexport const DiscardPrompt: Component<DiscardPromptProps> = (props) => {\n  const instanceId = Symbol();\n\n  const handleKeyDown = (event: KeyboardEvent) => {\n    if (!confirmationFocusManager.isActive(instanceId)) return;\n    if (isKeyboardEventTriggeredByInput(event)) return;\n\n    const isEnter = event.code === \"Enter\";\n    const isEscape = event.code === \"Escape\";\n    if (isEnter || isEscape) {\n      event.preventDefault();\n      event.stopPropagation();\n      if (isEscape && props.cancelOnEscape) {\n        props.onCancel?.();\n      } else {\n        props.onConfirm?.();\n      }\n    }\n  };\n\n  const handleFocus = () => {\n    confirmationFocusManager.claim(instanceId);\n  };\n\n  onMount(() => {\n    confirmationFocusManager.claim(instanceId);\n    window.addEventListener(\"keydown\", handleKeyDown, { capture: true });\n  });\n\n  onCleanup(() => {\n    confirmationFocusManager.release(instanceId);\n    window.removeEventListener(\"keydown\", handleKeyDown, { capture: true });\n  });\n\n  return (\n    <div\n      data-react-grab-discard-prompt\n      class=\"contain-layout shrink-0 flex flex-col justify-center items-end w-fit h-fit\"\n      onPointerDown={handleFocus}\n      onClick={handleFocus}\n    >\n      <div class=\"contain-layout shrink-0 flex items-center gap-1 pt-1.5 pb-1 px-2 w-full h-fit\">\n        <span class=\"text-black text-[13px] leading-4 shrink-0 font-sans font-medium w-fit h-fit\">\n          {props.label ?? \"Discard?\"}\n        </span>\n      </div>\n      <BottomSection>\n        <div class=\"contain-layout shrink-0 flex items-center justify-end gap-[5px] w-full h-fit\">\n          <button\n            data-react-grab-discard-no\n            class=\"contain-layout shrink-0 flex items-center justify-center px-[3px] py-px rounded-sm bg-white [border-width:0.5px] border-solid border-[#B3B3B3] cursor-pointer transition-all hover:bg-[#F5F5F5] press-scale h-[17px]\"\n            onClick={props.onCancel}\n          >\n            <span class=\"text-black text-[13px] leading-3.5 font-sans font-medium\">\n              No\n            </span>\n          </button>\n          <button\n            data-react-grab-discard-yes\n            class=\"contain-layout shrink-0 flex items-center justify-center gap-0.5 px-[3px] py-px rounded-sm bg-[#FEF2F2] cursor-pointer transition-all hover:bg-[#FEE2E2] press-scale h-[17px]\"\n            onClick={props.onConfirm}\n          >\n            <span class=\"text-[#B91C1C] text-[13px] leading-3.5 font-sans font-medium\">\n              Yes\n            </span>\n            <IconReturn size={10} class=\"text-[#B91C1C]/50\" />\n          </button>\n        </div>\n      </BottomSection>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/selection-label/error-view.tsx",
    "content": "import { onMount, onCleanup, Show } from \"solid-js\";\nimport type { Component } from \"solid-js\";\nimport type { ErrorViewProps } from \"../../types.js\";\nimport { confirmationFocusManager } from \"../../utils/confirmation-focus-manager.js\";\nimport { isKeyboardEventTriggeredByInput } from \"../../utils/is-keyboard-event-triggered-by-input.js\";\nimport { IconRetry } from \"../icons/icon-retry.jsx\";\nimport { BottomSection } from \"./bottom-section.js\";\n\nexport const ErrorView: Component<ErrorViewProps> = (props) => {\n  const instanceId = Symbol();\n\n  const handleKeyDown = (event: KeyboardEvent) => {\n    if (!confirmationFocusManager.isActive(instanceId)) return;\n    if (isKeyboardEventTriggeredByInput(event)) return;\n\n    const isEnter = event.code === \"Enter\";\n    const isEscape = event.code === \"Escape\";\n\n    if (isEnter) {\n      event.preventDefault();\n      event.stopPropagation();\n      props.onRetry?.();\n    } else if (isEscape) {\n      event.preventDefault();\n      event.stopPropagation();\n      props.onAcknowledge?.();\n    }\n  };\n\n  const handleFocus = () => {\n    confirmationFocusManager.claim(instanceId);\n  };\n\n  onMount(() => {\n    confirmationFocusManager.claim(instanceId);\n    window.addEventListener(\"keydown\", handleKeyDown, { capture: true });\n  });\n\n  onCleanup(() => {\n    confirmationFocusManager.release(instanceId);\n    window.removeEventListener(\"keydown\", handleKeyDown, { capture: true });\n  });\n\n  const hasActions = () => Boolean(props.onRetry || props.onAcknowledge);\n\n  return (\n    <div\n      data-react-grab-error\n      class=\"contain-layout shrink-0 flex flex-col justify-center items-end w-fit h-fit max-w-[280px]\"\n      onPointerDown={handleFocus}\n      onClick={handleFocus}\n    >\n      <div\n        class=\"contain-layout shrink-0 flex items-start gap-1 px-2 w-full h-fit\"\n        classList={{ \"pt-1.5 pb-1\": hasActions(), \"py-1.5\": !hasActions() }}\n      >\n        <span\n          class=\"text-[#B91C1C] text-[13px] leading-4 font-sans font-medium overflow-hidden line-clamp-5\"\n          title={props.error}\n        >\n          {props.error}\n        </span>\n      </div>\n      <Show when={hasActions()}>\n        <BottomSection>\n          <div class=\"contain-layout shrink-0 flex items-center justify-end gap-[5px] w-full h-fit\">\n            <button\n              data-react-grab-retry\n              class=\"contain-layout shrink-0 flex items-center justify-center gap-1 px-[3px] py-px rounded-sm bg-white [border-width:0.5px] border-solid border-[#B3B3B3] cursor-pointer transition-all hover:bg-[#F5F5F5] press-scale h-[17px]\"\n              onClick={props.onRetry}\n            >\n              <span class=\"text-black text-[13px] leading-3.5 font-sans font-medium\">\n                Retry\n              </span>\n              <IconRetry size={10} class=\"text-black/50\" />\n            </button>\n            <button\n              data-react-grab-error-ok\n              class=\"contain-layout shrink-0 flex items-center justify-center gap-1 px-[3px] py-px rounded-sm bg-white [border-width:0.5px] border-solid border-[#B3B3B3] cursor-pointer transition-all hover:bg-[#F5F5F5] press-scale h-[17px]\"\n              onClick={props.onAcknowledge}\n            >\n              <span class=\"text-black text-[13px] leading-3.5 font-sans font-medium\">\n                Ok\n              </span>\n            </button>\n          </div>\n        </BottomSection>\n      </Show>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/selection-label/index.tsx",
    "content": "import {\n  Show,\n  For,\n  createSignal,\n  createEffect,\n  createMemo,\n  on,\n  onMount,\n  onCleanup,\n} from \"solid-js\";\nimport type { Component } from \"solid-js\";\nimport type { ArrowPosition, SelectionLabelProps } from \"../../types.js\";\nimport {\n  IME_COMPOSING_KEY_CODE,\n  VIEWPORT_MARGIN_PX,\n  ARROW_CENTER_PERCENT,\n  ARROW_LABEL_MARGIN_PX,\n  LABEL_GAP_PX,\n  SELECTION_LABEL_OFFSCREEN_PX,\n  TEXTAREA_MAX_HEIGHT_PX,\n  Z_INDEX_LABEL,\n} from \"../../constants.js\";\nimport { autoResizeTextarea } from \"../../utils/auto-resize-textarea.js\";\nimport { getArrowSize } from \"../../utils/get-arrow-size.js\";\nimport { isKeyboardEventTriggeredByInput } from \"../../utils/is-keyboard-event-triggered-by-input.js\";\nimport { cn } from \"../../utils/cn.js\";\nimport { getTagDisplay } from \"../../utils/get-tag-display.js\";\nimport { formatShortcut } from \"../../utils/format-shortcut.js\";\nimport { IconReply } from \"../icons/icon-reply.jsx\";\nimport { IconSubmit } from \"../icons/icon-submit.jsx\";\nimport { IconLoader } from \"../icons/icon-loader.jsx\";\nimport { Arrow } from \"./arrow.js\";\nimport { TagBadge } from \"./tag-badge.js\";\nimport { BottomSection } from \"./bottom-section.js\";\nimport { DiscardPrompt } from \"./discard-prompt.js\";\nimport { ErrorView } from \"./error-view.js\";\nimport { CompletionView } from \"./completion-view.js\";\nimport { ArrowNavigationMenu } from \"./arrow-navigation-menu.js\";\n\ninterface LabelPosition {\n  left: number;\n  top: number;\n  arrowLeftPercent: number;\n  arrowLeftOffset: number;\n  edgeOffsetX: number;\n}\n\nconst DEFAULT_OFFSCREEN_POSITION: LabelPosition = {\n  left: SELECTION_LABEL_OFFSCREEN_PX,\n  top: SELECTION_LABEL_OFFSCREEN_PX,\n  arrowLeftPercent: ARROW_CENTER_PERCENT,\n  arrowLeftOffset: 0,\n  edgeOffsetX: 0,\n};\n\ninterface PositionResult {\n  position: LabelPosition;\n  computedArrowPosition: ArrowPosition | null;\n  hadValidBounds: boolean;\n  elementIdentity: string;\n}\n\nexport const SelectionLabel: Component<SelectionLabelProps> = (props) => {\n  let containerRef: HTMLDivElement | undefined;\n  let panelRef: HTMLDivElement | undefined;\n  let inputRef: HTMLTextAreaElement | undefined;\n  let isTagCurrentlyHovered = false;\n\n  const [measuredWidth, setMeasuredWidth] = createSignal(0);\n  const [measuredHeight, setMeasuredHeight] = createSignal(0);\n  const [panelWidth, setPanelWidth] = createSignal(0);\n  const [viewportVersion, setViewportVersion] = createSignal(0);\n  const [isInternalFading, setIsInternalFading] = createSignal(false);\n  const [isShaking, setIsShaking] = createSignal(false);\n\n  const canInteract = () =>\n    props.status !== \"copying\" &&\n    props.status !== \"copied\" &&\n    props.status !== \"fading\" &&\n    props.status !== \"error\";\n\n  const isCompletedStatus = () =>\n    props.status === \"copied\" || props.status === \"fading\";\n\n  const shouldEnablePointerEvents = (): boolean => {\n    if (props.isPromptMode) return true;\n    if (isCompletedStatus() && (props.onDismiss || props.onShowContextMenu)) {\n      return true;\n    }\n    if (props.status === \"copying\" && props.onAbort) return true;\n    if (\n      props.status === \"error\" &&\n      (props.onAcknowledgeError || props.onRetry)\n    ) {\n      return true;\n    }\n    if (props.arrowNavigationState?.isVisible) return true;\n    return false;\n  };\n\n  let resizeObserver: ResizeObserver | undefined;\n\n  const handleTagHoverChange = (hovered: boolean) => {\n    isTagCurrentlyHovered = hovered;\n  };\n\n  const handleViewportChange = () => {\n    setViewportVersion((version) => version + 1);\n  };\n\n  const handleGlobalKeyDown = (event: KeyboardEvent) => {\n    if (isKeyboardEventTriggeredByInput(event)) return;\n\n    const isEnterToExpand =\n      event.code === \"Enter\" && !props.isPromptMode && canInteract();\n    const isCtrlCToAbort =\n      event.code === \"KeyC\" &&\n      event.ctrlKey &&\n      props.status === \"copying\" &&\n      props.onAbort;\n\n    if (isEnterToExpand) {\n      event.preventDefault();\n      event.stopImmediatePropagation();\n      props.onToggleExpand?.();\n    } else if (isCtrlCToAbort) {\n      event.preventDefault();\n      event.stopImmediatePropagation();\n      props.onAbort?.();\n    }\n  };\n\n  onMount(() => {\n    resizeObserver = new ResizeObserver((entries) => {\n      for (const entry of entries) {\n        const rect = entry.target.getBoundingClientRect();\n        if (entry.target === containerRef && !isTagCurrentlyHovered) {\n          setMeasuredWidth(rect.width);\n          setMeasuredHeight(rect.height);\n        } else if (entry.target === panelRef) {\n          setPanelWidth(rect.width);\n        }\n      }\n    });\n    if (containerRef) {\n      const rect = containerRef.getBoundingClientRect();\n      setMeasuredWidth(rect.width);\n      setMeasuredHeight(rect.height);\n      resizeObserver.observe(containerRef);\n    }\n    if (panelRef) {\n      setPanelWidth(panelRef.getBoundingClientRect().width);\n      resizeObserver.observe(panelRef);\n    }\n    window.addEventListener(\"scroll\", handleViewportChange, true);\n    window.addEventListener(\"resize\", handleViewportChange);\n    window.visualViewport?.addEventListener(\"resize\", handleViewportChange);\n    window.visualViewport?.addEventListener(\"scroll\", handleViewportChange);\n    window.addEventListener(\"keydown\", handleGlobalKeyDown, { capture: true });\n  });\n\n  onCleanup(() => {\n    resizeObserver?.disconnect();\n    window.removeEventListener(\"scroll\", handleViewportChange, true);\n    window.removeEventListener(\"resize\", handleViewportChange);\n    window.visualViewport?.removeEventListener(\"resize\", handleViewportChange);\n    window.visualViewport?.removeEventListener(\"scroll\", handleViewportChange);\n    window.removeEventListener(\"keydown\", handleGlobalKeyDown, {\n      capture: true,\n    });\n  });\n\n  const elementIdentity = () =>\n    `${props.tagName ?? \"\"}:${props.componentName ?? \"\"}`;\n\n  createEffect(() => {\n    if (props.isPromptMode && inputRef && props.onSubmit) {\n      // HACK: Defer focus one tick so the textarea is fully mounted.\n      const focusTimeout = setTimeout(() => {\n        if (inputRef) {\n          inputRef.focus();\n          autoResizeTextarea(inputRef, TEXTAREA_MAX_HEIGHT_PX);\n        }\n      }, 0);\n      onCleanup(() => {\n        clearTimeout(focusTimeout);\n      });\n    }\n  });\n\n  const positionComputation = createMemo(\n    (previousResult: PositionResult): PositionResult => {\n      viewportVersion();\n      const currentElementIdentity = elementIdentity();\n      const didReset =\n        currentElementIdentity !== previousResult.elementIdentity;\n      const cached = didReset\n        ? {\n            position: DEFAULT_OFFSCREEN_POSITION,\n            computedArrowPosition: null as ArrowPosition | null,\n            hadValidBounds: false,\n            elementIdentity: currentElementIdentity,\n          }\n        : previousResult;\n\n      const bounds = props.selectionBounds;\n      const labelWidth = measuredWidth();\n      const labelHeight = measuredHeight();\n      const hasMeasurements = labelWidth > 0 && labelHeight > 0;\n      const hasValidBounds = bounds && bounds.width > 0 && bounds.height > 0;\n\n      if (!hasMeasurements || !hasValidBounds) {\n        return {\n          position: cached.hadValidBounds\n            ? cached.position\n            : DEFAULT_OFFSCREEN_POSITION,\n          computedArrowPosition: cached.computedArrowPosition,\n          hadValidBounds: cached.hadValidBounds,\n          elementIdentity: currentElementIdentity,\n        };\n      }\n\n      const visualViewport = window.visualViewport;\n      const viewportLeft = visualViewport?.offsetLeft ?? 0;\n      const viewportTop = visualViewport?.offsetTop ?? 0;\n      const viewportRight =\n        viewportLeft + (visualViewport?.width ?? window.innerWidth);\n      const viewportBottom =\n        viewportTop + (visualViewport?.height ?? window.innerHeight);\n\n      const isSelectionVisibleInViewport =\n        bounds.x + bounds.width > viewportLeft &&\n        bounds.x < viewportRight &&\n        bounds.y + bounds.height > viewportTop &&\n        bounds.y < viewportBottom;\n\n      if (!isSelectionVisibleInViewport) {\n        return {\n          position: DEFAULT_OFFSCREEN_POSITION,\n          computedArrowPosition: cached.computedArrowPosition,\n          hadValidBounds: cached.hadValidBounds,\n          elementIdentity: currentElementIdentity,\n        };\n      }\n\n      const selectionCenterX = bounds.x + bounds.width / 2;\n      const cursorX = props.mouseX ?? selectionCenterX;\n      const selectionBottom = bounds.y + bounds.height;\n      const selectionTop = bounds.y;\n\n      const actualArrowHeight = props.hideArrow\n        ? 0\n        : getArrowSize(panelWidth());\n\n      // HACK: Use cursorX as anchor point, CSS transform handles centering via translateX(-50%)\n      // This avoids the flicker when content changes because centering doesn't depend on JS measurement\n      const anchorX = cursorX;\n      let edgeOffsetX = 0;\n      let positionTop = selectionBottom + actualArrowHeight + LABEL_GAP_PX;\n\n      if (labelWidth > 0) {\n        const labelLeft = anchorX - labelWidth / 2;\n        const labelRight = anchorX + labelWidth / 2;\n\n        if (labelRight > viewportRight - VIEWPORT_MARGIN_PX) {\n          edgeOffsetX = viewportRight - VIEWPORT_MARGIN_PX - labelRight;\n        }\n        if (labelLeft + edgeOffsetX < viewportLeft + VIEWPORT_MARGIN_PX) {\n          edgeOffsetX = viewportLeft + VIEWPORT_MARGIN_PX - labelLeft;\n        }\n      }\n\n      const totalHeightNeeded = labelHeight + actualArrowHeight + LABEL_GAP_PX;\n      const fitsBelow =\n        positionTop + labelHeight <= viewportBottom - VIEWPORT_MARGIN_PX;\n\n      if (!fitsBelow) {\n        positionTop = selectionTop - totalHeightNeeded;\n      }\n\n      if (positionTop < viewportTop + VIEWPORT_MARGIN_PX) {\n        positionTop = viewportTop + VIEWPORT_MARGIN_PX;\n      }\n\n      const arrowLeftPercent = ARROW_CENTER_PERCENT;\n      const labelHalfWidth = labelWidth / 2;\n      const arrowCenterPx = labelHalfWidth - edgeOffsetX;\n      const arrowMinPx = Math.min(ARROW_LABEL_MARGIN_PX, labelHalfWidth);\n      const arrowMaxPx = Math.max(\n        labelWidth - ARROW_LABEL_MARGIN_PX,\n        labelHalfWidth,\n      );\n      const clampedArrowCenterPx = Math.max(\n        arrowMinPx,\n        Math.min(arrowMaxPx, arrowCenterPx),\n      );\n      const arrowLeftOffset = clampedArrowCenterPx - labelHalfWidth;\n\n      const computedArrowPosition: ArrowPosition = fitsBelow ? \"bottom\" : \"top\";\n\n      return {\n        position: {\n          left: anchorX,\n          top: positionTop,\n          arrowLeftPercent,\n          arrowLeftOffset,\n          edgeOffsetX,\n        },\n        computedArrowPosition,\n        hadValidBounds: true,\n        elementIdentity: currentElementIdentity,\n      };\n    },\n    {\n      position: DEFAULT_OFFSCREEN_POSITION,\n      computedArrowPosition: null as ArrowPosition | null,\n      hadValidBounds: false,\n      elementIdentity: \"\",\n    },\n  );\n\n  const arrowPosition = () =>\n    positionComputation().computedArrowPosition ?? \"bottom\";\n  const hadValidBounds = () => positionComputation().hadValidBounds;\n\n  createEffect(\n    on(\n      () => props.selectionLabelShakeCount,\n      () => setIsShaking(true),\n      { defer: true },\n    ),\n  );\n\n  const handleKeyDown = (event: KeyboardEvent) => {\n    if (event.isComposing || event.keyCode === IME_COMPOSING_KEY_CODE) {\n      return;\n    }\n\n    event.stopImmediatePropagation();\n\n    const isEnterWithoutShift = event.code === \"Enter\" && !event.shiftKey;\n    const isEscape = event.code === \"Escape\";\n\n    if (isEnterWithoutShift) {\n      event.preventDefault();\n      props.onSubmit?.();\n    } else if (isEscape) {\n      event.preventDefault();\n      props.onConfirmDismiss?.();\n    }\n  };\n\n  const handleInput = (event: InputEvent) => {\n    const inputTarget = event.target;\n    if (!(inputTarget instanceof HTMLTextAreaElement)) {\n      return;\n    }\n    autoResizeTextarea(inputTarget, TEXTAREA_MAX_HEIGHT_PX);\n    props.onInputChange?.(inputTarget.value);\n  };\n\n  const tagDisplayResult = () =>\n    getTagDisplay({\n      tagName: props.tagName,\n      componentName: props.componentName,\n      elementsCount: props.elementsCount,\n    });\n\n  const isArrowNavigationVisible = () =>\n    Boolean(props.arrowNavigationState?.isVisible);\n\n  const handleTagClick = (event: MouseEvent) => {\n    event.stopImmediatePropagation();\n    if (props.filePath && props.onOpen) {\n      props.onOpen();\n    }\n  };\n\n  const handleContainerPointerDown = (event: PointerEvent) => {\n    event.stopImmediatePropagation();\n    const isEditableInputVisible =\n      canInteract() &&\n      props.isPromptMode &&\n      !props.isPendingDismiss &&\n      props.onSubmit;\n    if (isEditableInputVisible && inputRef) {\n      inputRef.focus();\n    }\n  };\n\n  const shouldPersistDuringFade = () =>\n    hadValidBounds() && (isCompletedStatus() || props.status === \"error\");\n\n  return (\n    <Show\n      when={\n        props.visible !== false &&\n        (props.selectionBounds || shouldPersistDuringFade())\n      }\n    >\n      <div\n        ref={containerRef}\n        data-react-grab-ignore-events\n        data-react-grab-selection-label\n        class={cn(\n          \"fixed font-sans text-[13px] antialiased filter-[drop-shadow(0px_1px_2px_#51515140)] select-none transition-opacity duration-100 ease-out\",\n        )}\n        style={{\n          top: `${positionComputation().position.top}px`,\n          left: `${positionComputation().position.left}px`,\n          transform: `translateX(calc(-50% + ${positionComputation().position.edgeOffsetX}px))`,\n          \"z-index\": `${Z_INDEX_LABEL}`,\n          \"pointer-events\": shouldEnablePointerEvents() ? \"auto\" : \"none\",\n          opacity: props.status === \"fading\" || isInternalFading() ? 0 : 1,\n        }}\n        onPointerDown={handleContainerPointerDown}\n        onClick={(event) => {\n          event.stopImmediatePropagation();\n        }}\n        onMouseEnter={() => props.onHoverChange?.(true)}\n        onMouseLeave={() => props.onHoverChange?.(false)}\n      >\n        <Show when={!props.hideArrow}>\n          <Arrow\n            position={arrowPosition()}\n            leftPercent={positionComputation().position.arrowLeftPercent}\n            leftOffsetPx={positionComputation().position.arrowLeftOffset}\n            labelWidth={panelWidth()}\n          />\n        </Show>\n\n        <Show when={isCompletedStatus() && !props.error}>\n          <CompletionView\n            statusText={\n              props.hasAgent ? (props.statusText ?? \"Completed\") : \"Copied\"\n            }\n            supportsUndo={props.supportsUndo}\n            supportsFollowUp={props.supportsFollowUp}\n            dismissButtonText={props.dismissButtonText}\n            previousPrompt={props.previousPrompt}\n            onDismiss={props.onDismiss}\n            onUndo={props.onUndo}\n            onFollowUpSubmit={props.onFollowUpSubmit}\n            onFadingChange={setIsInternalFading}\n            onShowContextMenu={props.onShowContextMenu}\n          />\n        </Show>\n\n        <div\n          ref={panelRef}\n          class={cn(\n            \"contain-layout flex items-center gap-[5px] rounded-[10px] antialiased w-fit h-fit p-0 [font-synthesis:none] [corner-shape:superellipse(1.25)]\",\n            \"bg-white\",\n            isShaking() && \"animate-shake\",\n          )}\n          style={{\n            display: isCompletedStatus() && !props.error ? \"none\" : undefined,\n          }}\n          onAnimationEnd={() => setIsShaking(false)}\n        >\n          <Show when={props.status === \"copying\" && !props.isPendingAbort}>\n            <div\n              class=\"contain-layout shrink-0 flex flex-col justify-center items-start w-fit h-fit max-w-[280px]\"\n              classList={{\n                \"min-w-[150px]\": Boolean(props.hasAgent && props.inputValue),\n              }}\n            >\n              <div class=\"contain-layout shrink-0 flex items-center gap-1 py-1.5 px-2 w-full h-fit\">\n                <IconLoader size={13} class=\"text-[#71717a] shrink-0\" />\n                <span class=\"shimmer-text text-[13px] leading-4 font-sans font-medium h-fit tabular-nums overflow-hidden text-ellipsis whitespace-nowrap\">\n                  {props.statusText ?? \"Grabbing…\"}\n                </span>\n              </div>\n              <Show when={props.hasAgent && props.inputValue}>\n                <BottomSection>\n                  <div class=\"shrink-0 flex justify-between items-end w-full min-h-4\">\n                    <textarea\n                      ref={inputRef}\n                      data-react-grab-ignore-events\n                      class=\"text-black text-[13px] leading-4 font-medium bg-transparent border-none outline-none resize-none flex-1 p-0 m-0 opacity-50 wrap-break-word overflow-y-auto\"\n                      style={{\n                        \"field-sizing\": \"content\",\n                        \"min-height\": \"16px\",\n                        \"max-height\": `${TEXTAREA_MAX_HEIGHT_PX}px`,\n                        \"scrollbar-width\": \"none\",\n                      }}\n                      value={props.inputValue ?? \"\"}\n                      placeholder=\"Add context\"\n                      rows={1}\n                      disabled\n                    />\n                    <Show when={props.onAbort}>\n                      <button\n                        data-react-grab-ignore-events\n                        data-react-grab-abort\n                        class=\"contain-layout shrink-0 flex items-center justify-center size-4 rounded-full bg-black cursor-pointer ml-1 interactive-scale\"\n                        onPointerDown={(event) => event.stopPropagation()}\n                        onClick={(event) => {\n                          event.stopPropagation();\n                          props.onAbort?.();\n                        }}\n                      >\n                        <div class=\"size-1.5 bg-white rounded-[1px]\" />\n                      </button>\n                    </Show>\n                  </div>\n                </BottomSection>\n              </Show>\n            </div>\n          </Show>\n\n          <Show when={props.status === \"copying\" && props.isPendingAbort}>\n            <DiscardPrompt\n              onConfirm={props.onConfirmAbort}\n              onCancel={props.onCancelAbort}\n            />\n          </Show>\n\n          <Show when={canInteract() && !props.isPromptMode}>\n            <div\n              class=\"contain-layout shrink-0 flex flex-col items-start w-fit h-fit\"\n              classList={{ \"min-w-[100px]\": isArrowNavigationVisible() }}\n            >\n              <div\n                class=\"contain-layout shrink-0 flex items-center gap-1 w-fit h-fit px-2\"\n                classList={{\n                  \"py-1.5\": !isArrowNavigationVisible(),\n                  \"pt-1.5 pb-1\": isArrowNavigationVisible(),\n                }}\n              >\n                <TagBadge\n                  tagName={tagDisplayResult().tagName}\n                  componentName={tagDisplayResult().componentName}\n                  isClickable={Boolean(props.filePath && props.onOpen)}\n                  onClick={handleTagClick}\n                  onHoverChange={handleTagHoverChange}\n                  shrink\n                  forceShowIcon={\n                    isArrowNavigationVisible()\n                      ? Boolean(props.filePath && props.onOpen)\n                      : Boolean(props.isContextMenuOpen)\n                  }\n                />\n              </div>\n              <Show when={props.arrowNavigationState?.isVisible}>\n                <ArrowNavigationMenu\n                  items={props.arrowNavigationState!.items}\n                  activeIndex={props.arrowNavigationState!.activeIndex}\n                  onSelect={(index) => props.onArrowNavigationSelect?.(index)}\n                />\n              </Show>\n              <Show\n                when={\n                  !isArrowNavigationVisible() &&\n                  Boolean(props.actionCycleState?.isVisible)\n                }\n              >\n                <BottomSection>\n                  <div class=\"flex flex-col w-[calc(100%+16px)] -mx-2 -my-1.5\">\n                    <For each={props.actionCycleState?.items ?? []}>\n                      {(item, itemIndex) => (\n                        <div\n                          data-react-grab-action-cycle-item={item.label.toLowerCase()}\n                          class=\"contain-layout flex items-center justify-between w-full px-2 py-1 transition-colors\"\n                          classList={{\n                            \"bg-black/5\":\n                              itemIndex() ===\n                              (props.actionCycleState?.activeIndex ?? 0),\n                            \"rounded-b-[6px]\":\n                              itemIndex() ===\n                              (props.actionCycleState?.items ?? []).length - 1,\n                          }}\n                        >\n                          <span class=\"text-[13px] leading-4 font-sans font-medium text-black\">\n                            {item.label}\n                          </span>\n                          <Show when={item.shortcut}>\n                            <span class=\"text-[11px] font-sans text-black/50 ml-4\">\n                              {formatShortcut(item.shortcut!)}\n                            </span>\n                          </Show>\n                        </div>\n                      )}\n                    </For>\n                  </div>\n                </BottomSection>\n              </Show>\n            </div>\n          </Show>\n\n          <Show\n            when={\n              canInteract() && props.isPromptMode && !props.isPendingDismiss\n            }\n          >\n            <div class=\"contain-layout shrink-0 flex flex-col justify-center items-start w-fit h-fit min-w-[150px] max-w-[280px]\">\n              <div class=\"contain-layout shrink-0 flex items-center gap-1 pt-1.5 pb-1 w-fit h-fit px-2 max-w-full\">\n                <TagBadge\n                  tagName={tagDisplayResult().tagName}\n                  componentName={tagDisplayResult().componentName}\n                  isClickable={Boolean(props.filePath && props.onOpen)}\n                  onClick={handleTagClick}\n                  onHoverChange={handleTagHoverChange}\n                  forceShowIcon\n                />\n              </div>\n              <BottomSection>\n                <Show when={props.replyToPrompt}>\n                  <div class=\"flex items-center gap-1 w-full mb-1 overflow-hidden\">\n                    <IconReply size={10} class=\"text-black/30 shrink-0\" />\n                    <span class=\"text-black/40 text-[11px] leading-3 font-medium truncate italic\">\n                      {props.replyToPrompt}\n                    </span>\n                  </div>\n                </Show>\n                <div\n                  class=\"shrink-0 flex justify-between items-end w-full min-h-4\"\n                  style={{ \"padding-left\": props.replyToPrompt ? \"14px\" : \"0\" }}\n                >\n                  <textarea\n                    ref={inputRef}\n                    data-react-grab-ignore-events\n                    data-react-grab-input\n                    class=\"text-black text-[13px] leading-4 font-medium bg-transparent border-none outline-none resize-none flex-1 p-0 m-0 wrap-break-word overflow-y-auto\"\n                    style={{\n                      \"field-sizing\": \"content\",\n                      \"min-height\": \"16px\",\n                      \"max-height\": `${TEXTAREA_MAX_HEIGHT_PX}px`,\n                      \"scrollbar-width\": \"none\",\n                    }}\n                    value={props.inputValue ?? \"\"}\n                    onInput={handleInput}\n                    onKeyDown={handleKeyDown}\n                    placeholder=\"Add context\"\n                    rows={1}\n                    readOnly={!props.onSubmit}\n                  />\n                  <Show when={props.onSubmit}>\n                    <button\n                      data-react-grab-submit\n                      class=\"contain-layout shrink-0 flex items-center justify-center size-4 rounded-full bg-black cursor-pointer ml-1 interactive-scale\"\n                      onClick={() => props.onSubmit?.()}\n                    >\n                      <IconSubmit size={10} class=\"text-white\" />\n                    </button>\n                  </Show>\n                </div>\n              </BottomSection>\n            </div>\n          </Show>\n\n          <Show when={props.isPendingDismiss}>\n            <DiscardPrompt\n              onConfirm={props.onConfirmDismiss}\n              onCancel={() => {\n                props.onCancelDismiss?.();\n                inputRef?.focus();\n              }}\n            />\n          </Show>\n\n          <Show when={props.error}>\n            <ErrorView\n              error={props.error!}\n              onAcknowledge={props.onAcknowledgeError}\n              onRetry={props.onRetry}\n            />\n          </Show>\n        </div>\n      </div>\n    </Show>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/selection-label/tag-badge.tsx",
    "content": "import { Show, createSignal } from \"solid-js\";\nimport type { Component } from \"solid-js\";\nimport type { TagBadgeProps } from \"../../types.js\";\nimport { cn } from \"../../utils/cn.js\";\nimport { IconOpen } from \"../icons/icon-open.jsx\";\n\nexport const TagBadge: Component<TagBadgeProps> = (props) => {\n  const [isHovered, setIsHovered] = createSignal(false);\n\n  const handleMouseEnter = () => {\n    setIsHovered(true);\n    props.onHoverChange?.(true);\n  };\n\n  const handleMouseLeave = () => {\n    setIsHovered(false);\n    props.onHoverChange?.(false);\n  };\n\n  return (\n    <div\n      class={cn(\n        \"contain-layout flex items-center gap-1 max-w-[280px] overflow-hidden\",\n        props.shrink && \"shrink-0\",\n        props.isClickable && \"cursor-pointer\",\n      )}\n      onMouseEnter={handleMouseEnter}\n      onMouseLeave={handleMouseLeave}\n      onClick={props.onClick}\n    >\n      <span class=\"text-[13px] leading-4 h-fit font-medium overflow-hidden text-ellipsis whitespace-nowrap min-w-0\">\n        <Show when={props.componentName}>\n          <span class=\"text-black\">{props.componentName}</span>\n          <span class=\"text-black/50\">.{props.tagName}</span>\n        </Show>\n        <Show when={!props.componentName}>\n          <span class=\"text-black\">{props.tagName}</span>\n        </Show>\n      </span>\n      <Show when={props.isClickable || props.forceShowIcon}>\n        <IconOpen\n          size={10}\n          class={cn(\n            \"text-black transition-all duration-100 shrink-0\",\n            isHovered() || props.forceShowIcon\n              ? \"opacity-100 scale-100\"\n              : \"opacity-0 scale-75 -ml-[2px] w-0\",\n          )}\n        />\n      </Show>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/toolbar/index.tsx",
    "content": "import {\n  createSignal,\n  createEffect,\n  on,\n  onMount,\n  onCleanup,\n  Show,\n} from \"solid-js\";\nimport type { Component } from \"solid-js\";\nimport type { Position, ToolbarMenuAction } from \"../../types.js\";\nimport { cn } from \"../../utils/cn.js\";\nimport { formatShortcut } from \"../../utils/format-shortcut.js\";\nimport {\n  loadToolbarState,\n  saveToolbarState,\n  type SnapEdge,\n  type ToolbarState,\n} from \"./state.js\";\nimport { IconSelect } from \"../icons/icon-select.jsx\";\nimport { IconClock } from \"../icons/icon-clock.jsx\";\nimport { IconCopy } from \"../icons/icon-copy.jsx\";\nimport { IconEllipsis } from \"../icons/icon-ellipsis.jsx\";\nimport {\n  createSafePolygonTracker,\n  type TargetRect,\n} from \"../../utils/safe-polygon.js\";\nimport {\n  TOOLBAR_SNAP_MARGIN_PX,\n  TOOLBAR_FADE_IN_DELAY_MS,\n  TOOLBAR_COLLAPSED_SHORT_PX,\n  TOOLBAR_COLLAPSED_LONG_PX,\n  TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS,\n  TOGGLE_ANIMATION_BUFFER_MS,\n  TOOLBAR_DEFAULT_WIDTH_PX,\n  TOOLBAR_DEFAULT_HEIGHT_PX,\n  TOOLBAR_DEFAULT_POSITION_RATIO,\n  TOOLBAR_SHAKE_TOOLTIP_DURATION_MS,\n  FEEDBACK_DURATION_MS,\n  HINT_FLIP_IN_ANIMATION,\n  SAFE_POLYGON_BUFFER_PX,\n  SELECTION_HINT_COUNT,\n  SELECTION_HINT_CYCLE_INTERVAL_MS,\n  Z_INDEX_HOST,\n} from \"../../constants.js\";\nimport { freezeUpdates } from \"../../utils/freeze-updates.js\";\nimport {\n  freezeGlobalAnimations,\n  unfreezeGlobalAnimations,\n} from \"../../utils/freeze-animations.js\";\nimport {\n  freezePseudoStates,\n  unfreezePseudoStates,\n} from \"../../utils/freeze-pseudo-states.js\";\nimport { Tooltip } from \"../tooltip.jsx\";\nimport { Kbd } from \"../kbd.jsx\";\nimport {\n  getButtonSpacingClass,\n  getHitboxConstraintClass,\n} from \"../../utils/toolbar-layout.js\";\nimport { ToolbarContent } from \"./toolbar-content.js\";\nimport {\n  nativeCancelAnimationFrame,\n  nativeRequestAnimationFrame,\n} from \"../../utils/native-raf.js\";\nimport { getVisualViewport } from \"../../utils/get-visual-viewport.js\";\nimport {\n  calculateExpandedPositionFromCollapsed,\n  clampToRange,\n  getCollapsedPosition,\n  getPositionFromEdgeAndRatio,\n  getRatioFromPosition,\n} from \"../../utils/toolbar-position.js\";\nimport { createToolbarDrag } from \"../../utils/create-toolbar-drag.js\";\n\ninterface ToolbarProps {\n  isActive?: boolean;\n  isContextMenuOpen?: boolean;\n  onToggle?: () => void;\n  enabled?: boolean;\n  onToggleEnabled?: () => void;\n  shakeCount?: number;\n  onStateChange?: (state: ToolbarState) => void;\n  onSubscribeToStateChanges?: (\n    callback: (state: ToolbarState) => void,\n  ) => () => void;\n  onSelectHoverChange?: (isHovered: boolean) => void;\n  onContainerRef?: (element: HTMLDivElement) => void;\n  historyItemCount?: number;\n  clockFlashTrigger?: number;\n  hasUnreadHistoryItems?: boolean;\n  onToggleHistory?: () => void;\n  onCopyAll?: () => void;\n  onCopyAllHover?: (isHovered: boolean) => void;\n  onHistoryButtonHover?: (isHovered: boolean) => void;\n  isHistoryDropdownOpen?: boolean;\n  isClearPromptOpen?: boolean;\n  isHistoryPinned?: boolean;\n  toolbarActions?: ToolbarMenuAction[];\n  onToggleMenu?: () => void;\n  isMenuOpen?: boolean;\n}\n\ninterface FreezeHandlersOptions {\n  shouldFreezeInteractions?: boolean;\n  onHoverChange?: (isHovered: boolean) => void;\n  safePolygonTargets?: () => TargetRect[] | null;\n}\n\nexport const Toolbar: Component<ToolbarProps> = (props) => {\n  let containerRef: HTMLDivElement | undefined;\n  let expandableButtonsRef: HTMLDivElement | undefined;\n  let unfreezeUpdatesCallback: (() => void) | null = null;\n  let lastKnownExpandableWidth = 0;\n  let lastKnownExpandableHeight = 0;\n\n  const safePolygonTracker = createSafePolygonTracker();\n\n  const getElementRect = (selector: string): TargetRect | null => {\n    if (!containerRef) return null;\n    const rootNode = containerRef.getRootNode() as Document | ShadowRoot;\n    const element = rootNode.querySelector<HTMLElement>(selector);\n    if (!element) return null;\n    const rect = element.getBoundingClientRect();\n    return {\n      x: rect.x - SAFE_POLYGON_BUFFER_PX,\n      y: rect.y - SAFE_POLYGON_BUFFER_PX,\n      width: rect.width + SAFE_POLYGON_BUFFER_PX * 2,\n      height: rect.height + SAFE_POLYGON_BUFFER_PX * 2,\n    };\n  };\n\n  const getSafePolygonTargets = (\n    ...selectors: string[]\n  ): TargetRect[] | null => {\n    const rects: TargetRect[] = [];\n    for (const selector of selectors) {\n      const rect = getElementRect(selector);\n      if (rect) rects.push(rect);\n    }\n    return rects.length > 0 ? rects : null;\n  };\n\n  const savedState = loadToolbarState();\n\n  const [isVisible, setIsVisible] = createSignal(false);\n  const [isCollapsed, setIsCollapsed] = createSignal(false);\n  const [isResizing, setIsResizing] = createSignal(false);\n  const [snapEdge, setSnapEdge] = createSignal<SnapEdge>(\n    savedState?.edge ?? \"bottom\",\n  );\n  const [positionRatio, setPositionRatio] = createSignal(\n    savedState?.ratio ?? TOOLBAR_DEFAULT_POSITION_RATIO,\n  );\n  const [position, setPosition] = createSignal({ x: 0, y: 0 });\n  const [isShaking, setIsShaking] = createSignal(false);\n  const [isCollapseAnimating, setIsCollapseAnimating] = createSignal(false);\n  const [isSelectTooltipVisible, setIsSelectTooltipVisible] =\n    createSignal(false);\n  const [isToggleTooltipVisible, setIsToggleTooltipVisible] =\n    createSignal(false);\n  const [isShakeTooltipVisible, setIsShakeTooltipVisible] = createSignal(false);\n  const [isToggleAnimating, setIsToggleAnimating] = createSignal(false);\n  const [isRapidRetoggle, setIsRapidRetoggle] = createSignal(false);\n  const [isHistoryTooltipVisible, setIsHistoryTooltipVisible] =\n    createSignal(false);\n  const [isMenuTooltipVisible, setIsMenuTooltipVisible] = createSignal(false);\n  const [isCopyAllTooltipVisible, setIsCopyAllTooltipVisible] =\n    createSignal(false);\n  let clockFlashRef: HTMLSpanElement | undefined;\n  const [selectionHintIndex, setSelectionHintIndex] = createSignal(0);\n  const [hasHintCycled, setHasHintCycled] = createSignal(false);\n  const drag = createToolbarDrag({\n    getContainerRef: () => containerRef,\n    isCollapsed,\n    getExpandedDimensions: () => expandedDimensions,\n    onDragStart: () => {\n      if (unfreezeUpdatesCallback) {\n        unfreezeUpdatesCallback();\n        unfreezeUpdatesCallback = null;\n        unfreezeGlobalAnimations();\n        unfreezePseudoStates();\n      }\n    },\n    onPositionUpdate: (newPosition) => setPosition(newPosition),\n    onSnapEdgeChange: (edge, ratio) => {\n      setSnapEdge(edge);\n      setPositionRatio(ratio);\n    },\n    onSnapComplete: (result) => {\n      expandedDimensions = result.expandedDimensions;\n      setPosition(result.position);\n      saveAndNotify({\n        edge: result.edge,\n        ratio: result.ratio,\n        collapsed: isCollapsed(),\n        enabled: props.enabled ?? true,\n      });\n    },\n    onSnapAnimationEnd: () => {\n      if (props.enabled) {\n        measureExpandableDimension();\n      }\n    },\n  });\n\n  const hasLearnedSelectionHints = () => (props.clockFlashTrigger ?? 0) > 0;\n\n  createEffect(\n    on(\n      () => [props.isActive, hasLearnedSelectionHints()] as const,\n      ([isActive, hasLearned]) => {\n        setSelectionHintIndex(0);\n        setHasHintCycled(false);\n        if (!isActive || hasLearned) return;\n        const intervalId = setInterval(() => {\n          if (!hasHintCycled()) setHasHintCycled(true);\n          setSelectionHintIndex(\n            (previousIndex) => (previousIndex + 1) % SELECTION_HINT_COUNT,\n          );\n        }, SELECTION_HINT_CYCLE_INTERVAL_MS);\n        onCleanup(() => clearInterval(intervalId));\n      },\n      { defer: true },\n    ),\n  );\n\n  const hasToolbarActions = () => (props.toolbarActions ?? []).length > 0;\n\n  const historyTooltipLabel = () => {\n    const count = props.historyItemCount ?? 0;\n    return count > 0 ? `History (${count})` : \"History\";\n  };\n\n  const historyIconClass = () =>\n    cn(\n      \"transition-colors\",\n      props.isHistoryPinned ? \"text-black/80\" : \"text-[#B3B3B3]\",\n    );\n\n  const isVertical = () => snapEdge() === \"left\" || snapEdge() === \"right\";\n\n  const measureExpandableDimension = () => {\n    if (!expandableButtonsRef) return;\n    const rect = expandableButtonsRef.getBoundingClientRect();\n    if (isVertical()) {\n      lastKnownExpandableHeight = rect.height;\n    } else {\n      lastKnownExpandableWidth = rect.width;\n    }\n  };\n\n  const isTooltipAllowed = () =>\n    !isCollapsed() &&\n    !props.isHistoryDropdownOpen &&\n    !props.isMenuOpen &&\n    !props.isClearPromptOpen;\n\n  const tooltipPosition = (): \"top\" | \"bottom\" | \"left\" | \"right\" => {\n    const edge = snapEdge();\n    switch (edge) {\n      case \"top\":\n        return \"bottom\";\n      case \"bottom\":\n        return \"top\";\n      case \"left\":\n        return \"right\";\n      case \"right\":\n        return \"left\";\n    }\n  };\n\n  const buttonSpacingClass = () => getButtonSpacingClass(isVertical());\n  const hitboxConstraintClass = () => getHitboxConstraintClass(isVertical());\n\n  const shakeTooltipPositionClass = (): string => {\n    const tooltipSide = tooltipPosition();\n    if (isVertical()) {\n      const placementClass =\n        tooltipSide === \"left\" ? \"right-full mr-0.5\" : \"left-full ml-0.5\";\n      return `top-1/2 -translate-y-1/2 ${placementClass}`;\n    }\n    const placementClass =\n      tooltipSide === \"top\" ? \"bottom-full mb-0.5\" : \"top-full mt-0.5\";\n    return `left-1/2 -translate-x-1/2 ${placementClass}`;\n  };\n\n  const stopEventPropagation = (event: Event) => {\n    event.stopImmediatePropagation();\n  };\n\n  const createFreezeHandlers = (\n    setTooltipVisible: (visible: boolean) => void,\n    options?: FreezeHandlersOptions,\n  ) => ({\n    onMouseEnter: () => {\n      if (drag.isDragging()) return;\n      safePolygonTracker.stop();\n      setTooltipVisible(true);\n      if (\n        options?.shouldFreezeInteractions !== false &&\n        !unfreezeUpdatesCallback\n      ) {\n        unfreezeUpdatesCallback = freezeUpdates();\n        freezeGlobalAnimations();\n        freezePseudoStates();\n      }\n      options?.onHoverChange?.(true);\n    },\n    onMouseLeave: (event: MouseEvent) => {\n      setTooltipVisible(false);\n      if (\n        options?.shouldFreezeInteractions !== false &&\n        !props.isActive &&\n        !props.isContextMenuOpen\n      ) {\n        unfreezeUpdatesCallback?.();\n        unfreezeUpdatesCallback = null;\n        unfreezeGlobalAnimations();\n        unfreezePseudoStates();\n      }\n\n      const targetRects = options?.safePolygonTargets?.();\n      if (targetRects) {\n        safePolygonTracker.start(\n          { x: event.clientX, y: event.clientY },\n          targetRects,\n          () => options?.onHoverChange?.(false),\n        );\n        return;\n      }\n\n      options?.onHoverChange?.(false);\n    },\n  });\n\n  let shakeTooltipTimeout: ReturnType<typeof setTimeout> | undefined;\n  const clearShakeTooltipTimeout = () => {\n    if (shakeTooltipTimeout !== undefined) {\n      clearTimeout(shakeTooltipTimeout);\n      shakeTooltipTimeout = undefined;\n    }\n  };\n\n  createEffect(\n    on(\n      () => props.shakeCount,\n      (count) => {\n        if (count && !props.enabled) {\n          setIsShaking(true);\n          setIsShakeTooltipVisible(true);\n\n          clearShakeTooltipTimeout();\n          shakeTooltipTimeout = setTimeout(() => {\n            setIsShakeTooltipVisible(false);\n          }, TOOLBAR_SHAKE_TOOLTIP_DURATION_MS);\n          onCleanup(() => {\n            clearShakeTooltipTimeout();\n          });\n        }\n      },\n    ),\n  );\n\n  createEffect(\n    on(\n      () => props.enabled,\n      (enabled) => {\n        if (enabled && isShakeTooltipVisible()) {\n          setIsShakeTooltipVisible(false);\n          clearShakeTooltipTimeout();\n        }\n      },\n    ),\n  );\n\n  createEffect(\n    on(\n      () => [props.isActive, props.isContextMenuOpen] as const,\n      ([isActive, isContextMenuOpen]) => {\n        if (!isActive && !isContextMenuOpen && unfreezeUpdatesCallback) {\n          unfreezeUpdatesCallback();\n          unfreezeUpdatesCallback = null;\n        }\n      },\n    ),\n  );\n\n  const reclampToolbarToViewport = () => {\n    if (!containerRef) return;\n    const rect = containerRef.getBoundingClientRect();\n    expandedDimensions = { width: rect.width, height: rect.height };\n\n    const currentPos = position();\n    const viewport = getVisualViewport();\n    const edge = snapEdge();\n    let clampedX = currentPos.x;\n    let clampedY = currentPos.y;\n\n    if (edge === \"top\" || edge === \"bottom\") {\n      const minX = viewport.offsetLeft + TOOLBAR_SNAP_MARGIN_PX;\n      const maxX = Math.max(\n        minX,\n        viewport.offsetLeft +\n          viewport.width -\n          rect.width -\n          TOOLBAR_SNAP_MARGIN_PX,\n      );\n      clampedX = clampToRange(currentPos.x, minX, maxX);\n      clampedY =\n        edge === \"top\"\n          ? viewport.offsetTop + TOOLBAR_SNAP_MARGIN_PX\n          : viewport.offsetTop +\n            viewport.height -\n            rect.height -\n            TOOLBAR_SNAP_MARGIN_PX;\n    } else {\n      const minY = viewport.offsetTop + TOOLBAR_SNAP_MARGIN_PX;\n      const maxY = Math.max(\n        minY,\n        viewport.offsetTop +\n          viewport.height -\n          rect.height -\n          TOOLBAR_SNAP_MARGIN_PX,\n      );\n      clampedY = clampToRange(currentPos.y, minY, maxY);\n      clampedX =\n        edge === \"left\"\n          ? viewport.offsetLeft + TOOLBAR_SNAP_MARGIN_PX\n          : viewport.offsetLeft +\n            viewport.width -\n            rect.width -\n            TOOLBAR_SNAP_MARGIN_PX;\n    }\n\n    const newRatio = getRatioFromPosition(\n      edge,\n      clampedX,\n      clampedY,\n      rect.width,\n      rect.height,\n    );\n    setPositionRatio(newRatio);\n\n    const didPositionChange =\n      clampedX !== currentPos.x || clampedY !== currentPos.y;\n    if (didPositionChange) {\n      setIsCollapseAnimating(true);\n      nativeRequestAnimationFrame(() => {\n        nativeRequestAnimationFrame(() => {\n          setPosition({ x: clampedX, y: clampedY });\n          if (collapseAnimationTimeout) {\n            clearTimeout(collapseAnimationTimeout);\n          }\n          collapseAnimationTimeout = setTimeout(() => {\n            setIsCollapseAnimating(false);\n          }, TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS);\n        });\n      });\n    }\n  };\n\n  createEffect(\n    on(\n      () => props.clockFlashTrigger ?? 0,\n      () => {\n        if (props.isHistoryDropdownOpen) return;\n        if (clockFlashRef) {\n          clockFlashRef.classList.remove(\"animate-clock-flash\");\n          // HACK: force reflow between class removal/addition to restart the CSS animation\n          void clockFlashRef.offsetHeight;\n          clockFlashRef.classList.add(\"animate-clock-flash\");\n        }\n        setIsHistoryTooltipVisible(true);\n        const timerId = setTimeout(() => {\n          clockFlashRef?.classList.remove(\"animate-clock-flash\");\n          setIsHistoryTooltipVisible(false);\n        }, FEEDBACK_DURATION_MS);\n        onCleanup(() => {\n          clearTimeout(timerId);\n          setIsHistoryTooltipVisible(false);\n        });\n      },\n      { defer: true },\n    ),\n  );\n\n  createEffect(\n    on(\n      () => props.historyItemCount ?? 0,\n      () => {\n        if (isCollapsed()) return;\n        // HACK: Wait for grid-cols CSS transition to complete, then re-measure and clamp to viewport\n        if (historyItemCountTimeout) {\n          clearTimeout(historyItemCountTimeout);\n        }\n        historyItemCountTimeout = setTimeout(() => {\n          measureExpandableDimension();\n          reclampToolbarToViewport();\n        }, TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS);\n        onCleanup(() => {\n          if (historyItemCountTimeout) {\n            clearTimeout(historyItemCountTimeout);\n          }\n        });\n      },\n      { defer: true },\n    ),\n  );\n\n  let expandedDimensions = {\n    width: TOOLBAR_DEFAULT_WIDTH_PX,\n    height: TOOLBAR_DEFAULT_HEIGHT_PX,\n  };\n  const [collapsedDimensions, setCollapsedDimensions] = createSignal({\n    width: TOOLBAR_COLLAPSED_SHORT_PX,\n    height: TOOLBAR_COLLAPSED_SHORT_PX,\n  });\n\n  const getExpandedFromCollapsed = (\n    collapsedPosition: Position,\n    edge: SnapEdge,\n  ): { position: Position; ratio: number } => {\n    const actualRect = containerRef?.getBoundingClientRect();\n    const actualCollapsedWidth =\n      actualRect?.width ?? TOOLBAR_COLLAPSED_SHORT_PX;\n    const actualCollapsedHeight =\n      actualRect?.height ?? TOOLBAR_COLLAPSED_SHORT_PX;\n    return calculateExpandedPositionFromCollapsed(\n      collapsedPosition,\n      edge,\n      expandedDimensions,\n      actualCollapsedWidth,\n      actualCollapsedHeight,\n    );\n  };\n\n  const recalculatePosition = () => {\n    const newPosition = getPositionFromEdgeAndRatio(\n      snapEdge(),\n      positionRatio(),\n      expandedDimensions.width,\n      expandedDimensions.height,\n    );\n    setPosition(newPosition);\n  };\n\n  const handleToggle = drag.createDragAwareHandler(() => props.onToggle?.());\n\n  const handleHistory = drag.createDragAwareHandler(() =>\n    props.onToggleHistory?.(),\n  );\n\n  const handleCopyAll = drag.createDragAwareHandler(() => props.onCopyAll?.());\n\n  const handleToggleMenu = drag.createDragAwareHandler(() =>\n    props.onToggleMenu?.(),\n  );\n\n  const handleToggleCollapse = drag.createDragAwareHandler(() => {\n    const rect = containerRef?.getBoundingClientRect();\n    const wasCollapsed = isCollapsed();\n    let newRatio = positionRatio();\n\n    if (wasCollapsed) {\n      const { position: newPos, ratio } = getExpandedFromCollapsed(\n        currentPosition(),\n        snapEdge(),\n      );\n      newRatio = ratio;\n      setPosition(newPos);\n      setPositionRatio(newRatio);\n    } else if (rect) {\n      expandedDimensions = { width: rect.width, height: rect.height };\n    }\n\n    setIsCollapseAnimating(true);\n    setIsCollapsed((prev) => !prev);\n\n    saveAndNotify({\n      edge: snapEdge(),\n      ratio: newRatio,\n      collapsed: !wasCollapsed,\n      enabled: props.enabled ?? true,\n    });\n\n    if (collapseAnimationTimeout) {\n      clearTimeout(collapseAnimationTimeout);\n    }\n    collapseAnimationTimeout = setTimeout(() => {\n      setIsCollapseAnimating(false);\n      if (isCollapsed()) {\n        const collapsedRect = containerRef?.getBoundingClientRect();\n        if (collapsedRect) {\n          setCollapsedDimensions({\n            width: collapsedRect.width,\n            height: collapsedRect.height,\n          });\n        }\n      }\n    }, TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS);\n  });\n\n  const handleToggleEnabled = drag.createDragAwareHandler(() => {\n    const isCurrentlyEnabled = Boolean(props.enabled);\n    const edge = snapEdge();\n    const preTogglePosition = position();\n    const isVerticalEdge = edge === \"left\" || edge === \"right\";\n\n    const readExpandableDimension = () =>\n      isVerticalEdge ? lastKnownExpandableHeight : lastKnownExpandableWidth;\n\n    // HACK: Skip measuring during an active toggle animation — the CSS grid transition is\n    // mid-flight so getBoundingClientRect returns a partial value that contaminates\n    // lastKnownExpandableWidth and causes permanent position drift.\n    if (isCurrentlyEnabled && expandableButtonsRef && !isToggleAnimating()) {\n      measureExpandableDimension();\n    }\n    let expandableDimension = readExpandableDimension();\n    let shouldCompensatePosition = expandableDimension > 0;\n\n    let currentRenderedDimension = 0;\n    if (expandableButtonsRef) {\n      const expandableRect = expandableButtonsRef.getBoundingClientRect();\n      currentRenderedDimension = isVerticalEdge\n        ? expandableRect.height\n        : expandableRect.width;\n    }\n\n    // HACK: On first enable, expandable buttons are collapsed (0fr) so getBoundingClientRect\n    // returns 0. Temporarily force the relevant grid wrappers to 1fr without transitions to measure\n    // the real dimension synchronously, then restore. The browser never renders the intermediate state.\n    if (\n      !isCurrentlyEnabled &&\n      expandableDimension === 0 &&\n      expandableButtonsRef\n    ) {\n      const hasHistoryItems = (props.historyItemCount ?? 0) > 0;\n      const hasMenuActions = hasToolbarActions();\n      const expandedWrappers = Array.from(expandableButtonsRef.children).filter(\n        (child): child is HTMLElement => {\n          if (!(child instanceof HTMLElement)) return false;\n          if (child.querySelector(\"[data-react-grab-toolbar-history]\")) {\n            return hasHistoryItems;\n          }\n          if (child.querySelector(\"[data-react-grab-toolbar-copy-all]\")) {\n            return Boolean(props.isHistoryDropdownOpen);\n          }\n          if (child.querySelector(\"[data-react-grab-toolbar-menu]\")) {\n            return hasMenuActions;\n          }\n          return true;\n        },\n      );\n      const gridProperty = isVerticalEdge\n        ? \"gridTemplateRows\"\n        : \"gridTemplateColumns\";\n      for (const wrapper of expandedWrappers) {\n        wrapper.style.transition = \"none\";\n        wrapper.style[gridProperty] = \"1fr\";\n      }\n      void expandableButtonsRef.offsetWidth;\n      measureExpandableDimension();\n      expandableDimension = readExpandableDimension();\n      for (const wrapper of expandedWrappers) {\n        wrapper.style[gridProperty] = \"\";\n      }\n      void expandableButtonsRef.offsetWidth;\n      for (const wrapper of expandedWrappers) {\n        wrapper.style.transition = \"\";\n      }\n      shouldCompensatePosition = expandableDimension > 0;\n    }\n\n    if (shouldCompensatePosition) {\n      setIsRapidRetoggle(isToggleAnimating());\n      setIsToggleAnimating(true);\n    }\n\n    props.onToggleEnabled?.();\n\n    if (shouldCompensatePosition) {\n      const dimensionChange = isCurrentlyEnabled\n        ? -expandableDimension\n        : expandableDimension;\n\n      if (isVerticalEdge) {\n        expandedDimensions = {\n          width: expandedDimensions.width,\n          height: expandedDimensions.height + dimensionChange,\n        };\n      } else {\n        expandedDimensions = {\n          width: expandedDimensions.width + dimensionChange,\n          height: expandedDimensions.height,\n        };\n      }\n\n      const collapsedAxisPosition = isVerticalEdge\n        ? preTogglePosition.y + currentRenderedDimension\n        : preTogglePosition.x + currentRenderedDimension;\n\n      const computeClampedPosition = (expandDimension: number): Position => {\n        const viewport = getVisualViewport();\n        const targetAxisPosition = collapsedAxisPosition - expandDimension;\n        if (isVerticalEdge) {\n          const clampMin = viewport.offsetTop + TOOLBAR_SNAP_MARGIN_PX;\n          const clampMax =\n            viewport.offsetTop +\n            viewport.height -\n            expandedDimensions.height -\n            TOOLBAR_SNAP_MARGIN_PX;\n          return {\n            x: preTogglePosition.x,\n            y: clampToRange(targetAxisPosition, clampMin, clampMax),\n          };\n        }\n        const clampMin = viewport.offsetLeft + TOOLBAR_SNAP_MARGIN_PX;\n        const clampMax =\n          viewport.offsetLeft +\n          viewport.width -\n          expandedDimensions.width -\n          TOOLBAR_SNAP_MARGIN_PX;\n        return {\n          x: clampToRange(targetAxisPosition, clampMin, clampMax),\n          y: preTogglePosition.y,\n        };\n      };\n\n      if (toggleAnimationRafId !== undefined) {\n        nativeCancelAnimationFrame(toggleAnimationRafId);\n      }\n\n      if (isRapidRetoggle()) {\n        const finalExpandDimension = isCurrentlyEnabled\n          ? 0\n          : expandableDimension;\n        setPosition(computeClampedPosition(finalExpandDimension));\n        toggleAnimationRafId = undefined;\n      } else {\n        const animationStartTime = performance.now();\n        const syncPositionWithGrid = () => {\n          const elapsed = performance.now() - animationStartTime;\n          if (\n            elapsed >\n            TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS + TOGGLE_ANIMATION_BUFFER_MS\n          ) {\n            toggleAnimationRafId = undefined;\n            return;\n          }\n          if (expandableButtonsRef) {\n            const currentExpandDimension = isVerticalEdge\n              ? expandableButtonsRef.getBoundingClientRect().height\n              : expandableButtonsRef.getBoundingClientRect().width;\n            setPosition(computeClampedPosition(currentExpandDimension));\n          }\n          toggleAnimationRafId =\n            nativeRequestAnimationFrame(syncPositionWithGrid);\n        };\n        toggleAnimationRafId =\n          nativeRequestAnimationFrame(syncPositionWithGrid);\n      }\n\n      clearTimeout(toggleAnimationTimeout);\n      toggleAnimationTimeout = setTimeout(() => {\n        if (toggleAnimationRafId !== undefined) {\n          nativeCancelAnimationFrame(toggleAnimationRafId);\n          toggleAnimationRafId = undefined;\n        }\n        // HACK: Under heavy system load the rAF loop may not have run enough\n        // frames to fully track the CSS grid transition. Snap to the final\n        // expected position so the toggle button never drifts.\n        const finalExpandDimension = isCurrentlyEnabled\n          ? 0\n          : expandableDimension;\n        setPosition(computeClampedPosition(finalExpandDimension));\n        setIsToggleAnimating(false);\n        setIsRapidRetoggle(false);\n        const newRatio = getRatioFromPosition(\n          edge,\n          position().x,\n          position().y,\n          expandedDimensions.width,\n          expandedDimensions.height,\n        );\n        setPositionRatio(newRatio);\n        saveAndNotify({\n          edge,\n          ratio: newRatio,\n          collapsed: isCollapsed(),\n          enabled: !isCurrentlyEnabled,\n        });\n      }, TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS);\n    } else {\n      saveAndNotify({\n        edge,\n        ratio: positionRatio(),\n        collapsed: isCollapsed(),\n        enabled: !isCurrentlyEnabled,\n      });\n    }\n  });\n\n  const computeCollapsedPosition = (): Position =>\n    getCollapsedPosition(\n      snapEdge(),\n      position(),\n      expandedDimensions,\n      collapsedDimensions(),\n    );\n\n  let resizeTimeout: ReturnType<typeof setTimeout> | undefined;\n  let collapseAnimationTimeout: ReturnType<typeof setTimeout> | undefined;\n  let toggleAnimationTimeout: ReturnType<typeof setTimeout> | undefined;\n  let toggleAnimationRafId: number | undefined;\n  let historyItemCountTimeout: ReturnType<typeof setTimeout> | undefined;\n\n  const handleResize = () => {\n    if (drag.isDragging()) return;\n\n    setIsResizing(true);\n    recalculatePosition();\n\n    if (resizeTimeout) {\n      clearTimeout(resizeTimeout);\n    }\n\n    resizeTimeout = setTimeout(() => {\n      setIsResizing(false);\n\n      const newRatio = getRatioFromPosition(\n        snapEdge(),\n        position().x,\n        position().y,\n        expandedDimensions.width,\n        expandedDimensions.height,\n      );\n      setPositionRatio(newRatio);\n      saveAndNotify({\n        edge: snapEdge(),\n        ratio: newRatio,\n        collapsed: isCollapsed(),\n        enabled: props.enabled ?? true,\n      });\n    }, TOOLBAR_FADE_IN_DELAY_MS);\n  };\n\n  const saveAndNotify = (state: ToolbarState) => {\n    saveToolbarState(state);\n    props.onStateChange?.(state);\n  };\n\n  onMount(() => {\n    if (containerRef) {\n      props.onContainerRef?.(containerRef);\n    }\n\n    const rect = containerRef?.getBoundingClientRect();\n    const viewport = getVisualViewport();\n\n    if (savedState) {\n      if (rect) {\n        // HACK: On initial mount, the element is always rendered expanded (isCollapsed defaults to false).\n        // So rect always measures expanded dimensions, regardless of savedState.collapsed.\n        expandedDimensions = { width: rect.width, height: rect.height };\n      }\n      if (savedState.collapsed) {\n        const isHorizontalEdge =\n          savedState.edge === \"top\" || savedState.edge === \"bottom\";\n        setCollapsedDimensions({\n          width: isHorizontalEdge\n            ? TOOLBAR_COLLAPSED_LONG_PX\n            : TOOLBAR_COLLAPSED_SHORT_PX,\n          height: isHorizontalEdge\n            ? TOOLBAR_COLLAPSED_SHORT_PX\n            : TOOLBAR_COLLAPSED_LONG_PX,\n        });\n      }\n      setIsCollapsed(savedState.collapsed);\n      const newPosition = getPositionFromEdgeAndRatio(\n        savedState.edge,\n        savedState.ratio,\n        expandedDimensions.width,\n        expandedDimensions.height,\n      );\n      setPosition(newPosition);\n    } else if (rect) {\n      expandedDimensions = { width: rect.width, height: rect.height };\n      setPosition({\n        x: viewport.offsetLeft + (viewport.width - rect.width) / 2,\n        y:\n          viewport.offsetTop +\n          viewport.height -\n          rect.height -\n          TOOLBAR_SNAP_MARGIN_PX,\n      });\n      setPositionRatio(TOOLBAR_DEFAULT_POSITION_RATIO);\n    } else {\n      const defaultPosition = getPositionFromEdgeAndRatio(\n        \"bottom\",\n        TOOLBAR_DEFAULT_POSITION_RATIO,\n        expandedDimensions.width,\n        expandedDimensions.height,\n      );\n      setPosition(defaultPosition);\n    }\n\n    if (props.enabled) {\n      measureExpandableDimension();\n    }\n\n    if (props.onSubscribeToStateChanges) {\n      const unsubscribe = props.onSubscribeToStateChanges(\n        (state: ToolbarState) => {\n          if (isCollapseAnimating() || isToggleAnimating()) return;\n\n          const rect = containerRef?.getBoundingClientRect();\n          if (!rect) return;\n\n          const didCollapsedChange = isCollapsed() !== state.collapsed;\n\n          setSnapEdge(state.edge);\n\n          if (didCollapsedChange && !state.collapsed) {\n            const collapsedPos = currentPosition();\n            setIsCollapseAnimating(true);\n            setIsCollapsed(state.collapsed);\n            const { position: newPos, ratio: newRatio } =\n              getExpandedFromCollapsed(collapsedPos, state.edge);\n            setPosition(newPos);\n            setPositionRatio(newRatio);\n            clearTimeout(collapseAnimationTimeout);\n            collapseAnimationTimeout = setTimeout(() => {\n              setIsCollapseAnimating(false);\n            }, TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS);\n          } else {\n            if (didCollapsedChange) {\n              setIsCollapseAnimating(true);\n              clearTimeout(collapseAnimationTimeout);\n              collapseAnimationTimeout = setTimeout(() => {\n                setIsCollapseAnimating(false);\n              }, TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS);\n            }\n            setIsCollapsed(state.collapsed);\n            const newPosition = getPositionFromEdgeAndRatio(\n              state.edge,\n              state.ratio,\n              expandedDimensions.width,\n              expandedDimensions.height,\n            );\n            setPosition(newPosition);\n            setPositionRatio(state.ratio);\n          }\n        },\n      );\n\n      onCleanup(unsubscribe);\n    }\n\n    window.addEventListener(\"resize\", handleResize);\n    window.visualViewport?.addEventListener(\"resize\", handleResize);\n    window.visualViewport?.addEventListener(\"scroll\", handleResize);\n\n    const fadeInTimeout = setTimeout(() => {\n      setIsVisible(true);\n    }, TOOLBAR_FADE_IN_DELAY_MS);\n\n    onCleanup(() => {\n      clearTimeout(fadeInTimeout);\n    });\n  });\n\n  onCleanup(() => {\n    window.removeEventListener(\"resize\", handleResize);\n    window.visualViewport?.removeEventListener(\"resize\", handleResize);\n    window.visualViewport?.removeEventListener(\"scroll\", handleResize);\n    clearTimeout(resizeTimeout);\n    clearTimeout(collapseAnimationTimeout);\n    clearShakeTooltipTimeout();\n    clearTimeout(toggleAnimationTimeout);\n    clearTimeout(historyItemCountTimeout);\n    if (toggleAnimationRafId !== undefined) {\n      nativeCancelAnimationFrame(toggleAnimationRafId);\n    }\n    unfreezeUpdatesCallback?.();\n    safePolygonTracker.stop();\n  });\n\n  const currentPosition = () => {\n    const collapsed = isCollapsed();\n    return collapsed ? computeCollapsedPosition() : position();\n  };\n\n  const getCursorClass = (): string => {\n    if (isCollapsed()) {\n      return \"cursor-pointer\";\n    }\n    if (drag.isDragging()) {\n      return \"cursor-grabbing\";\n    }\n    return \"cursor-grab\";\n  };\n\n  const getTransitionClass = (): string => {\n    if (isResizing()) {\n      return \"\";\n    }\n    if (drag.isSnapping()) {\n      return \"transition-[transform,opacity] duration-300 ease-out\";\n    }\n    if (isCollapseAnimating()) {\n      return \"transition-[transform,opacity] duration-150 ease-out\";\n    }\n    if (isToggleAnimating()) {\n      return \"transition-opacity duration-150 ease-out\";\n    }\n    return \"transition-opacity duration-300 ease-out\";\n  };\n\n  const getTransformOrigin = (): string => {\n    const edge = snapEdge();\n    switch (edge) {\n      case \"top\":\n        return \"center top\";\n      case \"bottom\":\n        return \"center bottom\";\n      case \"left\":\n        return \"left center\";\n      case \"right\":\n        return \"right center\";\n      default:\n        return \"center center\";\n    }\n  };\n\n  return (\n    <div\n      ref={containerRef}\n      data-react-grab-ignore-events\n      data-react-grab-toolbar\n      class={cn(\n        \"fixed left-0 top-0 font-sans text-[13px] antialiased select-none\",\n        getCursorClass(),\n        getTransitionClass(),\n        isVisible()\n          ? \"opacity-100 pointer-events-auto\"\n          : \"opacity-0 pointer-events-none\",\n      )}\n      style={{\n        \"z-index\": String(Z_INDEX_HOST),\n        transform: `translate(${currentPosition().x}px, ${\n          currentPosition().y\n        }px)`,\n        \"transform-origin\": getTransformOrigin(),\n      }}\n      on:pointerdown={(event) => {\n        stopEventPropagation(event);\n        drag.handlePointerDown(event);\n      }}\n      on:mousedown={stopEventPropagation}\n      onMouseEnter={() => !isCollapsed() && props.onSelectHoverChange?.(true)}\n      onMouseLeave={() => props.onSelectHoverChange?.(false)}\n    >\n      <ToolbarContent\n        isActive={props.isActive}\n        enabled={props.enabled}\n        isCollapsed={isCollapsed()}\n        snapEdge={snapEdge()}\n        isShaking={isShaking()}\n        isHistoryExpanded={(props.historyItemCount ?? 0) > 0}\n        isCopyAllExpanded={Boolean(props.isHistoryDropdownOpen)}\n        isMenuExpanded={hasToolbarActions()}\n        isMenuOpen={props.isMenuOpen}\n        isHistoryPinned={props.isHistoryPinned}\n        disableGridTransitions={isRapidRetoggle()}\n        transformOrigin={getTransformOrigin()}\n        onAnimationEnd={() => setIsShaking(false)}\n        onCollapseClick={handleToggleCollapse}\n        onExpandableButtonsRef={(element) => {\n          expandableButtonsRef = element;\n        }}\n        onPanelClick={(event) => {\n          if (isCollapsed()) {\n            event.stopPropagation();\n            const { position: newPos, ratio: newRatio } =\n              getExpandedFromCollapsed(currentPosition(), snapEdge());\n            setPosition(newPos);\n            setPositionRatio(newRatio);\n            setIsCollapseAnimating(true);\n            setIsCollapsed(false);\n            saveAndNotify({\n              edge: snapEdge(),\n              ratio: newRatio,\n              collapsed: false,\n              enabled: props.enabled ?? true,\n            });\n            if (collapseAnimationTimeout) {\n              clearTimeout(collapseAnimationTimeout);\n            }\n            collapseAnimationTimeout = setTimeout(() => {\n              setIsCollapseAnimating(false);\n            }, TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS);\n          }\n        }}\n        selectButton={\n          <>\n            <button\n              data-react-grab-ignore-events\n              data-react-grab-toolbar-toggle\n              aria-label={\n                props.isActive ? \"Stop selecting element\" : \"Select element\"\n              }\n              aria-pressed={Boolean(props.isActive)}\n              class={cn(\n                \"contain-layout flex items-center justify-center cursor-pointer interactive-scale touch-hitbox\",\n                buttonSpacingClass(),\n                hitboxConstraintClass(),\n              )}\n              onClick={(event) => {\n                setIsSelectTooltipVisible(false);\n                handleToggle(event);\n              }}\n              {...createFreezeHandlers(setIsSelectTooltipVisible)}\n            >\n              <IconSelect\n                size={14}\n                class={cn(\n                  \"transition-colors\",\n                  props.isActive ? \"text-black\" : \"text-black/70\",\n                )}\n              />\n            </button>\n            <Tooltip\n              visible={isSelectTooltipVisible() && isTooltipAllowed()}\n              position={tooltipPosition()}\n            >\n              Select element <Kbd>{formatShortcut(\"C\")}</Kbd>\n            </Tooltip>\n          </>\n        }\n        historyButton={\n          <>\n            <button\n              data-react-grab-ignore-events\n              data-react-grab-toolbar-history\n              aria-label={`Open history${\n                (props.historyItemCount ?? 0) > 0\n                  ? ` (${props.historyItemCount ?? 0} items)`\n                  : \"\"\n              }`}\n              aria-haspopup=\"menu\"\n              aria-expanded={Boolean(props.isHistoryDropdownOpen)}\n              class={cn(\n                \"contain-layout flex items-center justify-center cursor-pointer interactive-scale touch-hitbox\",\n                buttonSpacingClass(),\n                hitboxConstraintClass(),\n              )}\n              onClick={(event) => {\n                setIsHistoryTooltipVisible(false);\n                handleHistory(event);\n              }}\n              {...createFreezeHandlers(\n                (visible) => {\n                  if (visible && props.isHistoryDropdownOpen) return;\n                  setIsHistoryTooltipVisible(visible);\n                },\n                {\n                  onHoverChange: (isHovered) =>\n                    props.onHistoryButtonHover?.(isHovered),\n                  shouldFreezeInteractions: false,\n                  safePolygonTargets: () =>\n                    props.isHistoryDropdownOpen\n                      ? getSafePolygonTargets(\n                          \"[data-react-grab-history-dropdown]\",\n                          \"[data-react-grab-toolbar-copy-all]\",\n                        )\n                      : null,\n                },\n              )}\n            >\n              <span ref={clockFlashRef} class=\"inline-flex relative\">\n                <IconClock size={14} class={historyIconClass()} />\n                <Show\n                  when={\n                    props.hasUnreadHistoryItems &&\n                    (props.historyItemCount ?? 0) > 0\n                  }\n                >\n                  <span\n                    data-react-grab-unread-indicator\n                    class=\"absolute -top-1 -right-1 min-w-2.5 h-2.5 px-0.5 flex items-center justify-center rounded-full bg-black text-white text-[8px] font-semibold leading-none\"\n                  >\n                    {props.historyItemCount}\n                  </span>\n                </Show>\n              </span>\n            </button>\n            <Tooltip\n              visible={isHistoryTooltipVisible() && isTooltipAllowed()}\n              position={tooltipPosition()}\n            >\n              {historyTooltipLabel()}\n            </Tooltip>\n          </>\n        }\n        copyAllButton={\n          <>\n            <button\n              data-react-grab-ignore-events\n              data-react-grab-toolbar-copy-all\n              aria-label=\"Copy all history items\"\n              class={cn(\n                \"contain-layout flex items-center justify-center cursor-pointer interactive-scale touch-hitbox\",\n                buttonSpacingClass(),\n                hitboxConstraintClass(),\n              )}\n              onClick={(event) => {\n                setIsCopyAllTooltipVisible(false);\n                handleCopyAll(event);\n              }}\n              {...createFreezeHandlers(setIsCopyAllTooltipVisible, {\n                onHoverChange: (isHovered) => props.onCopyAllHover?.(isHovered),\n                shouldFreezeInteractions: false,\n                safePolygonTargets: () =>\n                  props.isHistoryDropdownOpen\n                    ? getSafePolygonTargets(\n                        \"[data-react-grab-history-dropdown]\",\n                        \"[data-react-grab-toolbar-history]\",\n                      )\n                    : null,\n              })}\n            >\n              <IconCopy size={14} class=\"text-[#B3B3B3] transition-colors\" />\n            </button>\n            <Tooltip\n              visible={isCopyAllTooltipVisible() && isTooltipAllowed()}\n              position={tooltipPosition()}\n            >\n              Copy all\n            </Tooltip>\n          </>\n        }\n        menuButton={\n          <>\n            <button\n              data-react-grab-ignore-events\n              data-react-grab-toolbar-menu\n              aria-label={\n                props.isMenuOpen\n                  ? \"Close more actions menu\"\n                  : \"Open more actions menu\"\n              }\n              aria-haspopup=\"menu\"\n              aria-expanded={Boolean(props.isMenuOpen)}\n              class={cn(\n                \"contain-layout flex items-center justify-center cursor-pointer interactive-scale touch-hitbox\",\n                buttonSpacingClass(),\n                hitboxConstraintClass(),\n              )}\n              onClick={(event) => {\n                setIsMenuTooltipVisible(false);\n                handleToggleMenu(event);\n              }}\n              {...createFreezeHandlers(\n                (visible) => {\n                  if (visible && props.isMenuOpen) return;\n                  setIsMenuTooltipVisible(visible);\n                },\n                { shouldFreezeInteractions: false },\n              )}\n            >\n              <IconEllipsis\n                size={14}\n                class={cn(\n                  \"transition-colors\",\n                  props.isMenuOpen ? \"text-black/80\" : \"text-[#B3B3B3]\",\n                )}\n              />\n            </button>\n            <Tooltip\n              visible={isMenuTooltipVisible() && isTooltipAllowed()}\n              position={tooltipPosition()}\n            >\n              More actions\n            </Tooltip>\n          </>\n        }\n        toggleButton={\n          <>\n            <button\n              data-react-grab-ignore-events\n              data-react-grab-toolbar-enabled\n              aria-label={\n                props.enabled ? \"Disable React Grab\" : \"Enable React Grab\"\n              }\n              aria-pressed={Boolean(props.enabled)}\n              class={cn(\n                \"contain-layout flex items-center justify-center cursor-pointer interactive-scale outline-none\",\n                isVertical() ? \"my-0.5\" : \"mx-0.5\",\n              )}\n              onClick={(event) => {\n                setIsToggleTooltipVisible(false);\n                handleToggleEnabled(event);\n              }}\n              onMouseEnter={() => setIsToggleTooltipVisible(true)}\n              onMouseLeave={() => setIsToggleTooltipVisible(false)}\n            >\n              <div\n                class={cn(\n                  \"relative rounded-full transition-colors\",\n                  isVertical() ? \"w-3.5 h-2.5\" : \"w-5 h-3\",\n                  props.enabled ? \"bg-black\" : \"bg-black/25\",\n                )}\n              >\n                <div\n                  class={cn(\n                    \"absolute top-0.5 rounded-full bg-white transition-transform\",\n                    isVertical() ? \"w-1.5 h-1.5\" : \"w-2 h-2\",\n                    !props.enabled && \"left-0.5\",\n                    props.enabled && (isVertical() ? \"left-1.5\" : \"left-2.5\"),\n                  )}\n                />\n              </div>\n            </button>\n            <Tooltip\n              visible={isToggleTooltipVisible() && isTooltipAllowed()}\n              position={tooltipPosition()}\n            >\n              {props.enabled ? \"Disable\" : \"Enable\"}\n            </Tooltip>\n          </>\n        }\n        shakeTooltip={\n          <>\n            <Show when={props.isActive && !hasLearnedSelectionHints()}>\n              <div\n                class={cn(\n                  \"absolute whitespace-nowrap flex items-center gap-1 px-1.5 py-0.5 rounded-[10px] text-[10px] text-black/60 pointer-events-none animate-tooltip-fade-in [animation-fill-mode:backwards] overflow-hidden [corner-shape:superellipse(1.25)]\",\n                  \"bg-white\",\n                  shakeTooltipPositionClass(),\n                )}\n                style={{ \"z-index\": String(Z_INDEX_HOST) }}\n              >\n                <Show when={selectionHintIndex() === 0}>\n                  <span\n                    class={cn(\n                      \"flex items-center gap-1\",\n                      hasHintCycled() && HINT_FLIP_IN_ANIMATION,\n                    )}\n                  >\n                    Click or\n                    <Kbd>↵</Kbd>\n                    to capture\n                  </span>\n                </Show>\n                <Show when={selectionHintIndex() === 1}>\n                  <span\n                    class={cn(\n                      \"flex items-center gap-1\",\n                      HINT_FLIP_IN_ANIMATION,\n                    )}\n                  >\n                    <Kbd>↑</Kbd>\n                    <Kbd>↓</Kbd>\n                    to fine-tune target\n                  </span>\n                </Show>\n                <Show when={selectionHintIndex() === 2}>\n                  <span\n                    class={cn(\n                      \"flex items-center gap-1\",\n                      HINT_FLIP_IN_ANIMATION,\n                    )}\n                  >\n                    <Kbd>esc</Kbd>\n                    to cancel\n                  </span>\n                </Show>\n              </div>\n            </Show>\n            <Show when={isShakeTooltipVisible()}>\n              <div\n                class={cn(\n                  \"absolute whitespace-nowrap px-1.5 py-0.5 rounded-[10px] text-[10px] text-black/60 pointer-events-none animate-tooltip-fade-in [corner-shape:superellipse(1.25)]\",\n                  \"bg-white\",\n                  shakeTooltipPositionClass(),\n                )}\n                style={{ \"z-index\": String(Z_INDEX_HOST) }}\n              >\n                Enable to continue\n              </div>\n            </Show>\n          </>\n        }\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/toolbar/state.ts",
    "content": "import type { ToolbarState } from \"../../types.js\";\nimport { TOOLBAR_DEFAULT_POSITION_RATIO } from \"../../constants.js\";\n\nexport type { ToolbarState };\nexport type SnapEdge = \"top\" | \"bottom\" | \"left\" | \"right\";\n\nconst STORAGE_KEY = \"react-grab-toolbar-state\";\n\nexport const loadToolbarState = (): ToolbarState | null => {\n  try {\n    const serializedToolbarState = localStorage.getItem(STORAGE_KEY);\n    if (!serializedToolbarState) return null;\n\n    const parsed: unknown = JSON.parse(serializedToolbarState);\n    if (typeof parsed !== \"object\" || parsed === null) return null;\n    const record = parsed as Record<string, unknown>;\n    return {\n      edge:\n        record.edge === \"top\" ||\n        record.edge === \"bottom\" ||\n        record.edge === \"left\" ||\n        record.edge === \"right\"\n          ? record.edge\n          : \"bottom\",\n      ratio:\n        typeof record.ratio === \"number\"\n          ? record.ratio\n          : TOOLBAR_DEFAULT_POSITION_RATIO,\n      collapsed:\n        typeof record.collapsed === \"boolean\" ? record.collapsed : false,\n      enabled: typeof record.enabled === \"boolean\" ? record.enabled : true,\n    };\n  } catch (error) {\n    console.warn(\n      \"[react-grab] Failed to load toolbar state from localStorage:\",\n      error,\n    );\n  }\n  return null;\n};\n\nexport const saveToolbarState = (state: ToolbarState): void => {\n  try {\n    localStorage.setItem(STORAGE_KEY, JSON.stringify(state));\n  } catch (error) {\n    console.warn(\n      \"[react-grab] Failed to save toolbar state to localStorage:\",\n      error,\n    );\n  }\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/toolbar/toolbar-content.tsx",
    "content": "import type { Component, JSX } from \"solid-js\";\nimport { cn } from \"../../utils/cn.js\";\nimport { IconSelect } from \"../icons/icon-select.jsx\";\nimport { IconChevron } from \"../icons/icon-chevron.jsx\";\nimport { IconClock } from \"../icons/icon-clock.jsx\";\nimport { IconCopy } from \"../icons/icon-copy.jsx\";\nimport { IconEllipsis } from \"../icons/icon-ellipsis.jsx\";\nimport {\n  getExpandGridClass,\n  getButtonSpacingClass,\n  getMinDimensionClass,\n  getHitboxConstraintClass,\n} from \"../../utils/toolbar-layout.js\";\n\nexport interface ToolbarContentProps {\n  isActive?: boolean;\n  enabled?: boolean;\n  isCollapsed?: boolean;\n  snapEdge?: \"top\" | \"bottom\" | \"left\" | \"right\";\n  isShaking?: boolean;\n  isHistoryExpanded?: boolean;\n  isCopyAllExpanded?: boolean;\n  isMenuExpanded?: boolean;\n  isMenuOpen?: boolean;\n  isHistoryPinned?: boolean;\n  disableGridTransitions?: boolean;\n  onAnimationEnd?: () => void;\n  onPanelClick?: (event: MouseEvent) => void;\n  onCollapseClick?: (event: MouseEvent) => void;\n  onExpandableButtonsRef?: (element: HTMLDivElement) => void;\n  selectButton?: JSX.Element;\n  historyButton?: JSX.Element;\n  copyAllButton?: JSX.Element;\n  menuButton?: JSX.Element;\n  toggleButton?: JSX.Element;\n  collapseButton?: JSX.Element;\n  shakeTooltip?: JSX.Element;\n  transformOrigin?: string;\n}\n\nexport const ToolbarContent: Component<ToolbarContentProps> = (props) => {\n  const edge = () => props.snapEdge ?? \"bottom\";\n  const isVertical = () => edge() === \"left\" || edge() === \"right\";\n\n  const expandGridClass = (\n    isExpanded: boolean,\n    collapsedExtra?: string,\n  ): string => getExpandGridClass(isVertical(), isExpanded, collapsedExtra);\n\n  const gridTransitionClass = (): string => {\n    if (props.disableGridTransitions) return \"\";\n    if (isVertical()) {\n      return \"transition-[grid-template-rows,opacity] duration-150 ease-out\";\n    }\n    return \"transition-[grid-template-columns,opacity] duration-150 ease-out\";\n  };\n\n  const buttonSpacingClass = () => getButtonSpacingClass(isVertical());\n  const minDimensionClass = () => getMinDimensionClass(isVertical());\n  const hitboxConstraintClass = () => getHitboxConstraintClass(isVertical());\n\n  const collapsedEdgeClasses = () => {\n    if (!props.isCollapsed) return \"\";\n    const roundedClass = {\n      top: \"rounded-t-none rounded-b-[10px]\",\n      bottom: \"rounded-b-none rounded-t-[10px]\",\n      left: \"rounded-l-none rounded-r-[10px]\",\n      right: \"rounded-r-none rounded-l-[10px]\",\n    }[edge()];\n    const paddingClass = isVertical() ? \"px-0.25 py-2\" : \"px-2 py-0.25\";\n    return `${roundedClass} ${paddingClass}`;\n  };\n\n  const chevronRotation = () => {\n    const collapsed = props.isCollapsed;\n    switch (edge()) {\n      case \"top\":\n        return collapsed ? \"rotate-180\" : \"rotate-0\";\n      case \"bottom\":\n        return collapsed ? \"rotate-0\" : \"rotate-180\";\n      case \"left\":\n        return collapsed ? \"rotate-90\" : \"-rotate-90\";\n      case \"right\":\n        return collapsed ? \"-rotate-90\" : \"rotate-90\";\n      default:\n        return \"rotate-0\";\n    }\n  };\n\n  const defaultSelectButton = () => (\n    <button\n      data-react-grab-ignore-events\n      data-react-grab-toolbar-toggle\n      aria-label={props.isActive ? \"Stop selecting element\" : \"Select element\"}\n      aria-pressed={Boolean(props.isActive)}\n      class={cn(\n        \"contain-layout flex items-center justify-center cursor-pointer interactive-scale touch-hitbox\",\n        buttonSpacingClass(),\n        hitboxConstraintClass(),\n      )}\n    >\n      <IconSelect\n        size={14}\n        class={cn(\n          \"transition-colors\",\n          props.isActive ? \"text-black\" : \"text-black/70\",\n        )}\n      />\n    </button>\n  );\n\n  const defaultHistoryButton = () => (\n    <button\n      data-react-grab-ignore-events\n      data-react-grab-toolbar-history\n      aria-label=\"Open history\"\n      class={cn(\n        \"contain-layout flex items-center justify-center cursor-pointer interactive-scale touch-hitbox\",\n        buttonSpacingClass(),\n        hitboxConstraintClass(),\n      )}\n    >\n      <IconClock\n        size={14}\n        class={cn(\n          \"transition-colors\",\n          props.isHistoryPinned ? \"text-black/80\" : \"text-[#B3B3B3]\",\n        )}\n      />\n    </button>\n  );\n\n  const defaultCopyAllButton = () => (\n    <button\n      data-react-grab-ignore-events\n      data-react-grab-toolbar-copy-all\n      aria-label=\"Copy all history items\"\n      class={cn(\n        \"contain-layout flex items-center justify-center cursor-pointer interactive-scale touch-hitbox\",\n        buttonSpacingClass(),\n        hitboxConstraintClass(),\n      )}\n    >\n      <IconCopy size={14} class=\"text-[#B3B3B3] transition-colors\" />\n    </button>\n  );\n\n  const defaultMenuButton = () => (\n    <button\n      data-react-grab-ignore-events\n      data-react-grab-toolbar-menu\n      aria-label={\n        props.isMenuOpen ? \"Close more actions menu\" : \"Open more actions menu\"\n      }\n      class={cn(\n        \"contain-layout flex items-center justify-center cursor-pointer interactive-scale touch-hitbox\",\n        buttonSpacingClass(),\n        hitboxConstraintClass(),\n      )}\n    >\n      <IconEllipsis\n        size={14}\n        class={cn(\n          \"transition-colors\",\n          props.isMenuOpen ? \"text-black/80\" : \"text-[#B3B3B3]\",\n        )}\n      />\n    </button>\n  );\n\n  const defaultToggleButton = () => (\n    <button\n      data-react-grab-ignore-events\n      data-react-grab-toolbar-enabled\n      aria-label={props.enabled ? \"Disable React Grab\" : \"Enable React Grab\"}\n      aria-pressed={Boolean(props.enabled)}\n      class={cn(\n        \"contain-layout flex items-center justify-center cursor-pointer interactive-scale outline-none\",\n        isVertical() ? \"my-0.5\" : \"mx-0.5\",\n      )}\n    >\n      <div\n        class={cn(\n          \"relative rounded-full transition-colors\",\n          isVertical() ? \"w-3.5 h-2.5\" : \"w-5 h-3\",\n          props.enabled ? \"bg-black\" : \"bg-black/25\",\n        )}\n      >\n        <div\n          class={cn(\n            \"absolute top-0.5 rounded-full bg-white transition-transform\",\n            isVertical() ? \"w-1.5 h-1.5\" : \"w-2 h-2\",\n            !props.enabled && \"left-0.5\",\n            props.enabled && (isVertical() ? \"left-1.5\" : \"left-2.5\"),\n          )}\n        />\n      </div>\n    </button>\n  );\n\n  const defaultCollapseButton = () => (\n    <button\n      data-react-grab-ignore-events\n      data-react-grab-toolbar-collapse\n      aria-label={props.isCollapsed ? \"Expand toolbar\" : \"Collapse toolbar\"}\n      class=\"contain-layout shrink-0 flex items-center justify-center cursor-pointer interactive-scale\"\n      onClick={props.onCollapseClick}\n    >\n      <IconChevron\n        size={14}\n        class={cn(\n          \"text-[#B3B3B3] transition-transform duration-150\",\n          chevronRotation(),\n        )}\n      />\n    </button>\n  );\n\n  return (\n    <div\n      class={cn(\n        \"flex items-center justify-center rounded-[10px] antialiased relative overflow-visible [font-synthesis:none] filter-[drop-shadow(0px_1px_2px_#51515140)] [corner-shape:superellipse(1.25)]\",\n        isVertical() && \"flex-col\",\n        \"bg-white\",\n        !props.isCollapsed &&\n          (isVertical() ? \"px-1.5 gap-1.5 py-2\" : \"py-1.5 gap-1.5 px-2\"),\n        collapsedEdgeClasses(),\n        props.isShaking && \"animate-shake\",\n      )}\n      style={{ \"transform-origin\": props.transformOrigin }}\n      onAnimationEnd={props.onAnimationEnd}\n      onClick={props.onPanelClick}\n    >\n      <div\n        class={cn(\n          \"grid\",\n          gridTransitionClass(),\n          expandGridClass(!props.isCollapsed, \"pointer-events-none\"),\n        )}\n      >\n        <div\n          class={cn(\n            \"flex\",\n            isVertical()\n              ? \"flex-col items-center min-h-0\"\n              : \"items-center min-w-0\",\n          )}\n        >\n          <div\n            ref={(element) => props.onExpandableButtonsRef?.(element)}\n            class={cn(\"flex items-center\", isVertical() && \"flex-col\")}\n          >\n            <div\n              class={cn(\n                \"grid\",\n                gridTransitionClass(),\n                expandGridClass(Boolean(props.enabled)),\n              )}\n            >\n              <div class={cn(\"relative overflow-visible\", minDimensionClass())}>\n                {props.selectButton ?? defaultSelectButton()}\n              </div>\n            </div>\n            <div\n              class={cn(\n                \"grid\",\n                gridTransitionClass(),\n                expandGridClass(\n                  Boolean(props.enabled) && Boolean(props.isHistoryExpanded),\n                  \"pointer-events-none\",\n                ),\n              )}\n            >\n              <div class={cn(\"relative overflow-visible\", minDimensionClass())}>\n                {props.historyButton ?? defaultHistoryButton()}\n              </div>\n            </div>\n            <div\n              class={cn(\n                \"grid\",\n                gridTransitionClass(),\n                expandGridClass(\n                  Boolean(props.isCopyAllExpanded),\n                  \"pointer-events-none\",\n                ),\n              )}\n            >\n              <div class={cn(\"relative overflow-visible\", minDimensionClass())}>\n                {props.copyAllButton ?? defaultCopyAllButton()}\n              </div>\n            </div>\n            <div\n              class={cn(\n                \"grid\",\n                gridTransitionClass(),\n                expandGridClass(\n                  Boolean(props.enabled) && Boolean(props.isMenuExpanded),\n                  \"pointer-events-none\",\n                ),\n              )}\n            >\n              <div class={cn(\"relative overflow-visible\", minDimensionClass())}>\n                {props.menuButton ?? defaultMenuButton()}\n              </div>\n            </div>\n          </div>\n          <div class=\"relative shrink-0 overflow-visible\">\n            {props.toggleButton ?? defaultToggleButton()}\n          </div>\n        </div>\n      </div>\n      {props.collapseButton ?? defaultCollapseButton()}\n      {props.shakeTooltip}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/toolbar/toolbar-menu.tsx",
    "content": "import { Show, For, onMount, onCleanup, createSignal } from \"solid-js\";\nimport type { Component } from \"solid-js\";\nimport type { ToolbarMenuAction, DropdownAnchor } from \"../../types.js\";\nimport {\n  DROPDOWN_EDGE_TRANSFORM_ORIGIN,\n  TOOLBAR_MENU_MIN_WIDTH_PX,\n  Z_INDEX_LABEL,\n} from \"../../constants.js\";\nimport { cn } from \"../../utils/cn.js\";\nimport { formatShortcut } from \"../../utils/format-shortcut.js\";\nimport { resolveToolbarActionEnabled } from \"../../utils/resolve-action-enabled.js\";\nimport { createMenuHighlight } from \"../../utils/create-menu-highlight.js\";\nimport { suppressMenuEvent } from \"../../utils/suppress-menu-event.js\";\nimport { createAnchoredDropdown } from \"../../utils/create-anchored-dropdown.js\";\nimport { registerOverlayDismiss } from \"../../utils/register-overlay-dismiss.js\";\n\ninterface ToolbarMenuProps {\n  position: DropdownAnchor | null;\n  actions: ToolbarMenuAction[];\n  onDismiss: () => void;\n}\n\nexport const ToolbarMenu: Component<ToolbarMenuProps> = (props) => {\n  let containerRef: HTMLDivElement | undefined;\n  const {\n    containerRef: highlightContainerRef,\n    highlightRef,\n    updateHighlight,\n    clearHighlight,\n  } = createMenuHighlight();\n\n  const dropdown = createAnchoredDropdown(\n    () => containerRef,\n    () => props.position,\n  );\n\n  const [toggleRefreshCounter, setToggleRefreshCounter] = createSignal(0);\n\n  const handleActionClick = (action: ToolbarMenuAction, event: Event) => {\n    event.stopPropagation();\n    if (!resolveToolbarActionEnabled(action)) return;\n\n    action.onAction();\n\n    if (action.isActive !== undefined) {\n      setToggleRefreshCounter((previous) => previous + 1);\n    } else {\n      props.onDismiss();\n    }\n  };\n\n  onMount(() => {\n    dropdown.measure();\n    const unregisterOverlayDismiss = registerOverlayDismiss({\n      isOpen: () => Boolean(props.position),\n      onDismiss: props.onDismiss,\n    });\n\n    onCleanup(() => {\n      dropdown.clearAnimationHandles();\n      unregisterOverlayDismiss();\n    });\n  });\n\n  return (\n    <Show when={dropdown.shouldMount()}>\n      <div\n        ref={containerRef}\n        data-react-grab-ignore-events\n        data-react-grab-toolbar-menu\n        class=\"fixed font-sans text-[13px] antialiased filter-[drop-shadow(0px_1px_2px_#51515140)] select-none transition-[opacity,transform] duration-100 ease-out will-change-[opacity,transform]\"\n        style={{\n          top: `${dropdown.displayPosition().top}px`,\n          left: `${dropdown.displayPosition().left}px`,\n          \"z-index\": `${Z_INDEX_LABEL}`,\n          \"pointer-events\": dropdown.isAnimatedIn() ? \"auto\" : \"none\",\n          \"transform-origin\":\n            DROPDOWN_EDGE_TRANSFORM_ORIGIN[dropdown.lastAnchorEdge()],\n          opacity: dropdown.isAnimatedIn() ? \"1\" : \"0\",\n          transform: dropdown.isAnimatedIn() ? \"scale(1)\" : \"scale(0.95)\",\n        }}\n        onPointerDown={suppressMenuEvent}\n        onMouseDown={suppressMenuEvent}\n        onClick={suppressMenuEvent}\n        onContextMenu={suppressMenuEvent}\n      >\n        <div\n          class={cn(\n            \"contain-layout flex flex-col rounded-[10px] antialiased w-fit h-fit overflow-hidden [font-synthesis:none] [corner-shape:superellipse(1.25)]\",\n            \"bg-white\",\n          )}\n          style={{ \"min-width\": `${TOOLBAR_MENU_MIN_WIDTH_PX}px` }}\n        >\n          <div ref={highlightContainerRef} class=\"relative flex flex-col py-1\">\n            <div\n              ref={highlightRef}\n              class=\"pointer-events-none absolute bg-black/5 opacity-0 transition-[top,left,width,height,opacity] duration-75 ease-out\"\n            />\n            <For each={props.actions}>\n              {(action) => {\n                const isToggleAction = action.isActive !== undefined;\n                const isActionEnabled = () =>\n                  resolveToolbarActionEnabled(action);\n                const isToggleActive = () => {\n                  void toggleRefreshCounter();\n                  return Boolean(action.isActive?.());\n                };\n\n                return (\n                  <button\n                    data-react-grab-ignore-events\n                    data-react-grab-menu-item={action.id}\n                    class=\"relative z-1 contain-layout flex items-center justify-between w-full px-2 py-1 cursor-pointer text-left border-none bg-transparent disabled:opacity-40 disabled:cursor-default\"\n                    disabled={!isActionEnabled()}\n                    onPointerDown={(event) => event.stopPropagation()}\n                    onPointerEnter={(event) => {\n                      if (isActionEnabled()) {\n                        updateHighlight(event.currentTarget);\n                      }\n                    }}\n                    onPointerLeave={clearHighlight}\n                    onClick={(event) => handleActionClick(action, event)}\n                  >\n                    <span class=\"text-[13px] leading-4 font-sans font-medium text-black\">\n                      {action.label}\n                    </span>\n                    <Show when={!isToggleAction && action.shortcut}>\n                      {(shortcutKey) => (\n                        <span class=\"text-[11px] font-sans text-black/50 ml-4\">\n                          {formatShortcut(shortcutKey())}\n                        </span>\n                      )}\n                    </Show>\n                    <Show when={isToggleAction}>\n                      <div\n                        class={cn(\n                          \"relative rounded-full transition-colors ml-4 shrink-0 w-5 h-3\",\n                          isToggleActive() ? \"bg-black\" : \"bg-black/25\",\n                        )}\n                      >\n                        <div\n                          class={cn(\n                            \"absolute top-0.5 rounded-full bg-white transition-transform w-2 h-2\",\n                            isToggleActive() ? \"left-2.5\" : \"left-0.5\",\n                          )}\n                        />\n                      </div>\n                    </Show>\n                  </button>\n                );\n              }}\n            </For>\n          </div>\n        </div>\n      </div>\n    </Show>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/components/tooltip.tsx",
    "content": "import { createSignal, createEffect, on, onCleanup, Show } from \"solid-js\";\nimport type { Component, JSX } from \"solid-js\";\nimport { cn } from \"../utils/cn.js\";\nimport {\n  TOOLTIP_DELAY_MS,\n  TOOLTIP_GRACE_PERIOD_MS,\n  Z_INDEX_LABEL,\n} from \"../constants.js\";\n\nlet lastCloseTimestamp = 0;\n\nconst wasTooltipRecentlyVisible = () => {\n  return Date.now() - lastCloseTimestamp < TOOLTIP_GRACE_PERIOD_MS;\n};\n\ninterface TooltipProps {\n  visible: boolean;\n  position: \"top\" | \"bottom\" | \"left\" | \"right\";\n  children: JSX.Element;\n}\n\nexport const Tooltip: Component<TooltipProps> = (props) => {\n  const [delayedVisible, setDelayedVisible] = createSignal(false);\n  const [shouldAnimate, setShouldAnimate] = createSignal(true);\n  let delayTimeoutId: ReturnType<typeof setTimeout> | undefined;\n\n  createEffect(\n    on(\n      () => props.visible,\n      (isVisible) => {\n        if (delayTimeoutId !== undefined) {\n          clearTimeout(delayTimeoutId);\n          delayTimeoutId = undefined;\n        }\n\n        if (isVisible) {\n          if (wasTooltipRecentlyVisible()) {\n            setShouldAnimate(false);\n            setDelayedVisible(true);\n          } else {\n            setShouldAnimate(true);\n            delayTimeoutId = setTimeout(() => {\n              setDelayedVisible(true);\n            }, TOOLTIP_DELAY_MS);\n          }\n        } else {\n          if (delayedVisible()) {\n            lastCloseTimestamp = Date.now();\n          }\n          setDelayedVisible(false);\n        }\n      },\n    ),\n  );\n\n  onCleanup(() => {\n    if (delayTimeoutId !== undefined) {\n      clearTimeout(delayTimeoutId);\n    }\n    if (delayedVisible()) {\n      lastCloseTimestamp = Date.now();\n    }\n  });\n\n  return (\n    <Show when={delayedVisible()}>\n      <div\n        class={cn(\n          \"absolute whitespace-nowrap px-1.5 py-0.5 rounded-[10px] text-[10px] text-black/60 pointer-events-none [corner-shape:superellipse(1.25)]\",\n          \"bg-white\",\n          props.position === \"left\" || props.position === \"right\"\n            ? \"top-1/2 -translate-y-1/2\"\n            : \"left-1/2 -translate-x-1/2\",\n          props.position === \"top\" && \"bottom-full mb-2.5\",\n          props.position === \"bottom\" && \"top-full mt-2.5\",\n          props.position === \"left\" && \"right-full mr-2.5\",\n          props.position === \"right\" && \"left-full ml-2.5\",\n          shouldAnimate() && \"animate-tooltip-fade-in\",\n        )}\n        style={{ \"z-index\": `${Z_INDEX_LABEL}` }}\n      >\n        {props.children}\n      </div>\n    </Show>\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/constants.ts",
    "content": "import { overlayColor } from \"./utils/overlay-color.js\";\n\nexport const VERSION = process.env.VERSION as string;\n\nexport const VIEWPORT_MARGIN_PX = 8;\nexport const OFFSCREEN_POSITION = -1000;\n\nexport const SELECTION_LERP_FACTOR = 0.95;\n\nexport const FEEDBACK_DURATION_MS = 1500;\nexport const FADE_DURATION_MS = 100;\nexport const FADE_COMPLETE_BUFFER_MS = 150;\nexport const DISMISS_ANIMATION_BUFFER_MS = 50;\nexport const KEYDOWN_SPAM_TIMEOUT_MS = 200;\nexport const BLUR_DEACTIVATION_THRESHOLD_MS = 500;\nexport const WINDOW_REFOCUS_GRACE_PERIOD_MS = 200;\nexport const INPUT_FOCUS_ACTIVATION_DELAY_MS = 400;\nexport const INPUT_TEXT_SELECTION_ACTIVATION_DELAY_MS = 600;\nexport const DEFAULT_KEY_HOLD_DURATION_MS = 100;\nexport const DEFAULT_MAX_CONTEXT_LINES = 3;\nexport const MIN_HOLD_FOR_ACTIVATION_AFTER_COPY_MS = 200;\nexport const RECENT_THRESHOLD_MS = 10_000;\nexport const FINDER_TIMEOUT_MS = 200;\nexport const SELECTOR_ATTR_VALUE_MAX_LENGTH_CHARS = 120;\n\nexport const ACTION_CYCLE_IDLE_TRIGGER_MS = 600;\n\nexport const DRAG_THRESHOLD_PX = 2;\n\nexport const ELEMENT_DETECTION_THROTTLE_MS = 32;\nexport const PENDING_DETECTION_STALENESS_MS = 200;\nexport const COMPONENT_NAME_DEBOUNCE_MS = 100;\nexport const DRAG_PREVIEW_DEBOUNCE_MS = 32;\nexport const BOUNDS_CACHE_TTL_MS = 16;\nexport const BOUNDS_RECALC_INTERVAL_MS = 100;\n\nexport const AUTO_SCROLL_EDGE_THRESHOLD_PX = 25;\nexport const AUTO_SCROLL_SPEED_PX = 10;\n\nexport const Z_INDEX_HOST = 2147483647;\nexport const Z_INDEX_LABEL = 2147483647;\nexport const Z_INDEX_OVERLAY_CANVAS = 2147483645;\n\nexport const DRAG_LERP_FACTOR = 0.7;\nexport const LERP_CONVERGENCE_THRESHOLD_PX = 0.5;\nexport const OPACITY_CONVERGENCE_THRESHOLD = 0.01;\nexport const FADE_OUT_BUFFER_MS = 100;\nexport const MIN_DEVICE_PIXEL_RATIO = 2;\n\nexport const OVERLAY_BORDER_COLOR_DRAG = overlayColor(0.4);\nexport const OVERLAY_FILL_COLOR_DRAG = overlayColor(0.05);\nexport const OVERLAY_BORDER_COLOR_DEFAULT = overlayColor(0.5);\nexport const OVERLAY_FILL_COLOR_DEFAULT = overlayColor(0.08);\nexport const FROZEN_GLOW_COLOR = overlayColor(0.15);\nexport const FROZEN_GLOW_EDGE_PX = 50;\n\nexport const ARROW_HEIGHT_PX = 8;\nexport const ARROW_MIN_SIZE_PX = 4;\nexport const ARROW_MAX_LABEL_WIDTH_RATIO = 0.2;\nexport const ARROW_CENTER_PERCENT = 50;\nexport const ARROW_LABEL_MARGIN_PX = 16;\nexport const LABEL_GAP_PX = 4;\nexport const PREVIEW_TEXT_MAX_LENGTH = 100;\nexport const PREVIEW_ATTR_VALUE_MAX_LENGTH = 15;\nexport const PREVIEW_MAX_ATTRS = 3;\nexport const PREVIEW_PRIORITY_ATTRS: readonly string[] = [\n  \"id\",\n  \"class\",\n  \"aria-label\",\n  \"data-testid\",\n  \"role\",\n  \"name\",\n  \"title\",\n];\n\nexport const MODIFIER_KEYS: readonly string[] = [\n  \"Meta\",\n  \"Control\",\n  \"Shift\",\n  \"Alt\",\n];\n\nexport const ARROW_KEYS = new Set([\n  \"ArrowUp\",\n  \"ArrowDown\",\n  \"ArrowLeft\",\n  \"ArrowRight\",\n]);\n\nexport const FROZEN_ELEMENT_ATTRIBUTE = \"data-react-grab-frozen\";\n\nexport const USER_IGNORE_ATTRIBUTE = \"data-react-grab-ignore\";\n\nexport const VIEWPORT_COVERAGE_THRESHOLD = 0.9;\nexport const OVERLAY_Z_INDEX_THRESHOLD = 1000;\nexport const DEV_TOOLS_OVERLAY_Z_INDEX_THRESHOLD = 2147483600;\n\nexport const TOOLTIP_DELAY_MS = 400;\nexport const TOOLTIP_GRACE_PERIOD_MS = 100;\n\nexport const TOOLBAR_SNAP_MARGIN_PX = 16;\nexport const TOOLBAR_FADE_IN_DELAY_MS = 500;\nexport const TOOLBAR_SNAP_ANIMATION_DURATION_MS = 300;\nexport const TOOLBAR_DRAG_THRESHOLD_PX = 5;\nexport const TOOLBAR_VELOCITY_MULTIPLIER_MS = 150;\nexport const TOOLBAR_COLLAPSED_SHORT_PX = 14;\nexport const TOOLBAR_COLLAPSED_LONG_PX = 28;\nexport const TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS = 150;\nexport const TOGGLE_ANIMATION_BUFFER_MS = 50;\nexport const TOOLBAR_DEFAULT_WIDTH_PX = 78;\nexport const TOOLBAR_DEFAULT_HEIGHT_PX = 28;\nexport const TOOLBAR_DEFAULT_POSITION_RATIO = 0.5;\nexport const TOOLBAR_SHAKE_TOOLTIP_DURATION_MS = 1500;\nexport const SELECTION_HINT_CYCLE_INTERVAL_MS = 3000;\nexport const SELECTION_HINT_COUNT = 3;\n\nexport const HINT_FLIP_IN_ANIMATION =\n  \"animate-[hint-flip-in_var(--transition-normal)_ease-out]\";\n\nexport const DRAG_SELECTION_COVERAGE_THRESHOLD = 0.75;\nexport const DRAG_SELECTION_SAMPLE_SPACING_PX = 32;\nexport const DRAG_SELECTION_MIN_SAMPLES_PER_AXIS = 3;\nexport const DRAG_SELECTION_MAX_SAMPLES_PER_AXIS = 20;\nexport const DRAG_SELECTION_MAX_TOTAL_SAMPLE_POINTS = 100;\nexport const DRAG_SELECTION_EDGE_INSET_PX = 1;\n\nexport const MAX_ARROW_NAVIGATION_HISTORY = 50;\nexport const MAX_MEMORY_SESSIONS = 50;\n\nexport const MAX_TRANSFORM_ANCESTOR_DEPTH = 6;\nexport const TRANSFORM_EARLY_BAIL_DEPTH = 3;\n\nexport const ELEMENT_POSITION_CACHE_DISTANCE_THRESHOLD_PX = 2;\nexport const ELEMENT_POSITION_THROTTLE_MS = 16;\nexport const POINTER_EVENTS_RESUME_DEBOUNCE_MS = 100;\nexport const VISIBILITY_CACHE_TTL_MS = 50;\n\nexport const ZOOM_DETECTION_THRESHOLD = 0.01;\n\nexport const MOUNT_ROOT_RECHECK_DELAY_MS = 1000;\n\nexport const MAX_HISTORY_ITEMS = 20;\nexport const MAX_SESSION_STORAGE_SIZE_BYTES = 2 * 1024 * 1024;\nexport const DROPDOWN_ANIMATION_DURATION_MS = 100;\nexport const DROPDOWN_HOVER_OPEN_DELAY_MS = 200;\nexport const DROPDOWN_VIEWPORT_PADDING_PX = 8;\nexport const DROPDOWN_ANCHOR_GAP_PX = 8;\nexport const SAFE_POLYGON_BUFFER_PX = 8;\nexport const DROPDOWN_ICON_SIZE_PX = 11;\nexport const DROPDOWN_MIN_WIDTH_PX = 180;\nexport const DROPDOWN_MAX_WIDTH_PX = 280;\nexport const TOOLBAR_MENU_MIN_WIDTH_PX = 100;\n\nexport const DROPDOWN_OFFSCREEN_POSITION = { left: -9999, top: -9999 };\n\nexport const DROPDOWN_EDGE_TRANSFORM_ORIGIN = {\n  left: \"left center\",\n  right: \"right center\",\n  top: \"center top\",\n  bottom: \"center bottom\",\n};\n\nexport const NEXTJS_REVALIDATION_DELAY_MS = 1000;\n\nexport const TEXTAREA_MAX_HEIGHT_PX = 95;\n\nexport const IME_COMPOSING_KEY_CODE = 229;\nexport const SELECTION_LABEL_OFFSCREEN_PX = -9999;\n\nexport const RELEVANT_CSS_PROPERTIES = new Set([\n  \"display\",\n  \"position\",\n  \"top\",\n  \"right\",\n  \"bottom\",\n  \"left\",\n  \"z-index\",\n  \"overflow\",\n  \"overflow-x\",\n  \"overflow-y\",\n  \"width\",\n  \"height\",\n  \"min-width\",\n  \"min-height\",\n  \"max-width\",\n  \"max-height\",\n  \"margin-top\",\n  \"margin-right\",\n  \"margin-bottom\",\n  \"margin-left\",\n  \"padding-top\",\n  \"padding-right\",\n  \"padding-bottom\",\n  \"padding-left\",\n  \"flex-direction\",\n  \"flex-wrap\",\n  \"justify-content\",\n  \"align-items\",\n  \"align-self\",\n  \"align-content\",\n  \"flex-grow\",\n  \"flex-shrink\",\n  \"flex-basis\",\n  \"order\",\n  \"gap\",\n  \"row-gap\",\n  \"column-gap\",\n  \"grid-template-columns\",\n  \"grid-template-rows\",\n  \"grid-template-areas\",\n  \"font-family\",\n  \"font-size\",\n  \"font-weight\",\n  \"font-style\",\n  \"line-height\",\n  \"letter-spacing\",\n  \"text-align\",\n  \"text-decoration-line\",\n  \"text-decoration-style\",\n  \"text-transform\",\n  \"text-overflow\",\n  \"text-shadow\",\n  \"white-space\",\n  \"word-break\",\n  \"overflow-wrap\",\n  \"vertical-align\",\n  \"color\",\n  \"background-color\",\n  \"background-image\",\n  \"background-position\",\n  \"background-size\",\n  \"background-repeat\",\n  \"border-top-width\",\n  \"border-right-width\",\n  \"border-bottom-width\",\n  \"border-left-width\",\n  \"border-top-style\",\n  \"border-right-style\",\n  \"border-bottom-style\",\n  \"border-left-style\",\n  \"border-top-color\",\n  \"border-right-color\",\n  \"border-bottom-color\",\n  \"border-left-color\",\n  \"border-top-left-radius\",\n  \"border-top-right-radius\",\n  \"border-bottom-left-radius\",\n  \"border-bottom-right-radius\",\n  \"box-shadow\",\n  \"opacity\",\n  \"transform\",\n  \"filter\",\n  \"backdrop-filter\",\n  \"object-fit\",\n  \"object-position\",\n]);\n"
  },
  {
    "path": "packages/react-grab/src/core/agent/manager.ts",
    "content": "import { createSignal } from \"solid-js\";\nimport type { Accessor } from \"solid-js\";\nimport type {\n  Position,\n  AgentContext,\n  AgentSession,\n  AgentOptions,\n  OverlayBounds,\n} from \"../../types.js\";\nimport {\n  createSession,\n  saveSessionById,\n  saveSessions,\n  loadSessions,\n  clearSessions,\n  clearSessionById,\n  updateSession,\n} from \"./session.js\";\nimport { createElementBounds } from \"../../utils/create-element-bounds.js\";\nimport { isElementConnected } from \"../../utils/is-element-connected.js\";\nimport { generateSnippet } from \"../../utils/generate-snippet.js\";\nimport { recalculateSessionPosition } from \"../../utils/recalculate-session-position.js\";\nimport { getNearestComponentName } from \"../context.js\";\nimport {\n  DISMISS_ANIMATION_BUFFER_MS,\n  FADE_DURATION_MS,\n  RECENT_THRESHOLD_MS,\n} from \"../../constants.js\";\nimport { getTagName } from \"../../utils/get-tag-name.js\";\nimport { normalizeErrorMessage } from \"../../utils/normalize-error.js\";\n\ninterface StartSessionParams {\n  elements: Element[];\n  prompt: string;\n  position: Position;\n  selectionBounds: OverlayBounds[];\n  sessionId?: string;\n  agent?: AgentOptions;\n}\n\ninterface AgentManagerHooks {\n  transformAgentContext?: (\n    context: AgentContext,\n    elements: Element[],\n  ) => AgentContext | Promise<AgentContext>;\n}\n\ninterface SessionOperations {\n  start: (params: StartSessionParams) => Promise<void>;\n  abort: (sessionId?: string) => void;\n  dismiss: (sessionId: string) => void;\n  retry: (sessionId: string) => void;\n  undo: (sessionId: string) => void;\n  getElement: (sessionId: string) => Element | undefined;\n  getElements: (sessionId: string) => Element[];\n  tryResume: () => void;\n  acknowledgeError: (sessionId: string) => string | undefined;\n}\n\ninterface HistoryOperations {\n  undo: () => void;\n  redo: () => void;\n}\n\ninterface InternalOperations {\n  updateBoundsOnViewportChange: () => void;\n  setOptions: (options: AgentOptions) => void;\n  getOptions: () => AgentOptions | undefined;\n}\n\nexport interface AgentManager {\n  sessions: Accessor<Map<string, AgentSession>>;\n  isProcessing: Accessor<boolean>;\n  canUndo: Accessor<boolean>;\n  canRedo: Accessor<boolean>;\n  session: SessionOperations;\n  history: HistoryOperations;\n  _internal: InternalOperations;\n}\n\nexport const createAgentManager = (\n  initialAgentOptions: AgentOptions | undefined,\n  hooks?: AgentManagerHooks,\n): AgentManager => {\n  const [sessions, setSessions] = createSignal<Map<string, AgentSession>>(\n    new Map(),\n  );\n  const [canUndo, setCanUndo] = createSignal(false);\n  const [canRedo, setCanRedo] = createSignal(false);\n  const abortControllers = new Map<string, AbortController>();\n  const dismissTimeouts = new Map<string, ReturnType<typeof setTimeout>>();\n  const sessionMetadata = new Map<\n    string,\n    { elements: Element[]; agent: AgentOptions }\n  >();\n  const undoneSessionsStack: Array<{\n    session: AgentSession;\n    elements: Element[];\n    agent: AgentOptions | undefined;\n  }> = [];\n  const completedSessionsStack: Array<{\n    session: AgentSession;\n    elements: Element[];\n    agent: AgentOptions | undefined;\n  }> = [];\n\n  let agentOptions = initialAgentOptions;\n\n  const getAgentForSession = (sessionId: string): AgentOptions | undefined =>\n    sessionMetadata.get(sessionId)?.agent ?? agentOptions;\n\n  const getElementsForSession = (sessionId: string): Element[] =>\n    sessionMetadata.get(sessionId)?.elements ?? [];\n\n  const updateUndoRedoState = (agent?: AgentOptions) => {\n    const effectiveAgent = agent ?? agentOptions;\n    const providerCanUndo = effectiveAgent?.provider?.canUndo?.() ?? false;\n    const providerCanRedo = effectiveAgent?.provider?.canRedo?.() ?? false;\n    setCanUndo(providerCanUndo);\n    setCanRedo(providerCanRedo);\n  };\n\n  const setOptions = (options: AgentOptions) => {\n    agentOptions = options;\n    updateUndoRedoState();\n  };\n\n  const getOptions = (): AgentOptions | undefined => {\n    return agentOptions;\n  };\n\n  const isProcessing = (): boolean =>\n    Array.from(sessions().values()).some((session) => session.isStreaming);\n\n  const executeSessionStream = async (\n    session: AgentSession,\n    streamIterator: AsyncIterable<string>,\n    abortController: AbortController,\n    activeAgent?: AgentOptions,\n  ) => {\n    const effectiveAgent = activeAgent ?? agentOptions;\n    const storage = effectiveAgent?.storage;\n    let wasAborted = false;\n    const isCurrentExecution = () =>\n      abortControllers.get(session.id) === abortController;\n\n    try {\n      for await (const status of streamIterator) {\n        if (!isCurrentExecution()) break;\n        const currentSessions = sessions();\n        const currentSession = currentSessions.get(session.id);\n        if (!currentSession) break;\n\n        const updatedSession = updateSession(\n          currentSession,\n          { lastStatus: status },\n          storage,\n        );\n        setSessions((prev) => new Map(prev).set(session.id, updatedSession));\n        effectiveAgent?.onStatus?.(status, updatedSession);\n      }\n\n      if (!isCurrentExecution()) return;\n      const finalSessions = sessions();\n      const finalSession = finalSessions.get(session.id);\n      if (finalSession) {\n        const completionMessage =\n          effectiveAgent?.provider?.getCompletionMessage?.();\n        const completedSession = updateSession(\n          finalSession,\n          {\n            isStreaming: false,\n            ...(completionMessage ? { lastStatus: completionMessage } : {}),\n          },\n          storage,\n        );\n        setSessions((prev) => new Map(prev).set(session.id, completedSession));\n        const elements = getElementsForSession(session.id);\n        const result = await effectiveAgent?.onComplete?.(\n          completedSession,\n          elements,\n        );\n        const existingCompletedIndex = completedSessionsStack.findIndex(\n          (entry) => entry.session.id === session.id,\n        );\n        if (existingCompletedIndex !== -1) {\n          completedSessionsStack.splice(existingCompletedIndex, 1);\n        }\n        completedSessionsStack.push({\n          session: completedSession,\n          elements,\n          agent: effectiveAgent,\n        });\n        updateUndoRedoState(effectiveAgent);\n        undoneSessionsStack.length = 0;\n        if (result?.error) {\n          const errorSession = updateSession(\n            completedSession,\n            { error: result.error },\n            storage,\n          );\n          setSessions((prev) => new Map(prev).set(session.id, errorSession));\n        }\n      }\n    } catch (error) {\n      if (!isCurrentExecution()) return;\n      const currentSessions = sessions();\n      const currentSession = currentSessions.get(session.id);\n      if (error instanceof Error && error.name === \"AbortError\") {\n        wasAborted = true;\n        if (currentSession) {\n          const elements = getElementsForSession(session.id);\n          effectiveAgent?.onAbort?.(currentSession, elements);\n        }\n      } else {\n        const errorMessage =\n          error instanceof Error ? error.message : \"Unknown error\";\n\n        if (currentSession) {\n          const errorSession = updateSession(\n            currentSession,\n            {\n              error: errorMessage,\n              isStreaming: false,\n            },\n            storage,\n          );\n          setSessions((prev) => new Map(prev).set(session.id, errorSession));\n          if (error instanceof Error) {\n            effectiveAgent?.onError?.(error, errorSession);\n          }\n        }\n      }\n    } finally {\n      if (!isCurrentExecution()) {\n        return;\n      }\n      abortControllers.delete(session.id);\n\n      if (wasAborted) {\n        const dismissTimeout = dismissTimeouts.get(session.id);\n        if (dismissTimeout) {\n          clearTimeout(dismissTimeout);\n          dismissTimeouts.delete(session.id);\n        }\n        sessionMetadata.delete(session.id);\n        clearSessionById(session.id, storage);\n        setSessions((prev) => {\n          const next = new Map(prev);\n          next.delete(session.id);\n          return next;\n        });\n      }\n    }\n  };\n\n  const tryReacquireElement = (session: AgentSession): Element | undefined => {\n    const { selectionBounds, tagName } = session;\n    const firstBounds = selectionBounds[0];\n    if (!firstBounds) return undefined;\n\n    const centerX = firstBounds.x + firstBounds.width / 2;\n    const centerY = firstBounds.y + firstBounds.height / 2;\n\n    const element = document.elementFromPoint(centerX, centerY);\n    if (!element) return undefined;\n\n    const isValidHtmlTagName = tagName && !tagName.includes(\" \");\n    if (isValidHtmlTagName && getTagName(element) !== tagName) {\n      return undefined;\n    }\n\n    return element;\n  };\n\n  const tryResumeSessions = () => {\n    const storage = agentOptions?.storage;\n    if (!storage) {\n      return;\n    }\n\n    const existingSessions = loadSessions(storage);\n\n    if (existingSessions.size === 0) {\n      return;\n    }\n\n    const now = Date.now();\n\n    const resumableSessions = Array.from(existingSessions.values()).filter(\n      (session) => {\n        if (session.isStreaming) return true;\n        const lastUpdatedAt = session.lastUpdatedAt ?? session.createdAt;\n        const age = now - lastUpdatedAt;\n        const isRecent = age < RECENT_THRESHOLD_MS;\n        return isRecent && Boolean(session.error);\n      },\n    );\n    if (resumableSessions.length === 0) {\n      clearSessions(storage);\n      return;\n    }\n    if (\n      !agentOptions?.provider?.supportsResume ||\n      !agentOptions.provider.resume\n    ) {\n      clearSessions(storage);\n      return;\n    }\n\n    dismissTimeouts.forEach((timeoutId) => clearTimeout(timeoutId));\n    dismissTimeouts.clear();\n    abortControllers.forEach((controller) => controller.abort());\n    abortControllers.clear();\n    sessionMetadata.clear();\n\n    const resumableSessionsMap = new Map(\n      resumableSessions.map((session) => [session.id, session]),\n    );\n    setSessions(resumableSessionsMap);\n    saveSessions(resumableSessionsMap, storage);\n\n    for (const existingSession of resumableSessions) {\n      const reacquiredElement = tryReacquireElement(existingSession);\n      if (reacquiredElement && agentOptions) {\n        sessionMetadata.set(existingSession.id, {\n          elements: [reacquiredElement],\n          agent: agentOptions,\n        });\n      }\n\n      const sessionWithResumeStatus = {\n        ...existingSession,\n        isStreaming: true,\n        error: undefined,\n        lastStatus: existingSession.lastStatus || \"Resuming...\",\n        position: existingSession.position ?? {\n          x: window.innerWidth / 2,\n          y: window.innerHeight / 2,\n        },\n      };\n      setSessions((prev) =>\n        new Map(prev).set(existingSession.id, sessionWithResumeStatus),\n      );\n      agentOptions?.onResume?.(sessionWithResumeStatus);\n\n      const abortController = new AbortController();\n      abortControllers.set(existingSession.id, abortController);\n\n      const streamIterator = agentOptions.provider.resume(\n        existingSession.id,\n        abortController.signal,\n        storage,\n      );\n      void executeSessionStream(\n        existingSession,\n        streamIterator,\n        abortController,\n      );\n    }\n  };\n\n  const startSession = async (params: StartSessionParams) => {\n    const { elements, prompt, position, selectionBounds, sessionId, agent } =\n      params;\n    const activeAgent =\n      agent ?? (sessionId ? getAgentForSession(sessionId) : agentOptions);\n    const storage = activeAgent?.storage;\n\n    if (!activeAgent?.provider || elements.length === 0) {\n      return;\n    }\n\n    const firstElement = elements[0];\n    const existingSession = sessionId ? sessions().get(sessionId) : undefined;\n    const isFollowUp = Boolean(sessionId);\n\n    const content = existingSession\n      ? existingSession.context.content\n      : (await generateSnippet(elements, { maxLines: Infinity })).filter(\n          (snippet) => snippet.trim(),\n        );\n\n    const context: AgentContext = {\n      content,\n      prompt,\n      options: activeAgent?.getOptions?.(),\n      sessionId: isFollowUp ? sessionId : undefined,\n    };\n\n    let session: AgentSession;\n    if (existingSession) {\n      session = updateSession(\n        existingSession,\n        {\n          context,\n          isStreaming: true,\n          lastStatus: \"Thinking…\",\n        },\n        storage,\n      );\n    } else {\n      const tagName =\n        elements.length > 1\n          ? `${elements.length} elements`\n          : getTagName(firstElement) || undefined;\n      const componentName =\n        elements.length > 1\n          ? undefined\n          : (await getNearestComponentName(firstElement)) || undefined;\n\n      session = createSession(\n        context,\n        position,\n        selectionBounds,\n        tagName,\n        componentName,\n      );\n      session.lastStatus = \"Thinking…\";\n    }\n\n    sessionMetadata.set(session.id, { elements, agent: activeAgent });\n    setSessions((prev) => new Map(prev).set(session.id, session));\n    saveSessionById(session, storage);\n    activeAgent.onStart?.(session, elements);\n\n    const abortController = new AbortController();\n    abortControllers.set(session.id, abortController);\n\n    const contextWithSessionId: AgentContext = {\n      ...context,\n      sessionId: sessionId ?? session.id,\n    };\n\n    let transformedContext: AgentContext;\n    try {\n      transformedContext = hooks?.transformAgentContext\n        ? await hooks.transformAgentContext(contextWithSessionId, elements)\n        : contextWithSessionId;\n    } catch (error) {\n      const errorMessage = normalizeErrorMessage(\n        error,\n        \"Context transformation failed\",\n      );\n      const errorSession = updateSession(\n        session,\n        {\n          error: errorMessage,\n          isStreaming: false,\n        },\n        storage,\n      );\n      setSessions((prev) => new Map(prev).set(session.id, errorSession));\n      abortControllers.delete(session.id);\n      if (error instanceof Error) {\n        activeAgent.onError?.(error, errorSession);\n      }\n      return;\n    }\n\n    const streamIterator = activeAgent.provider.send(\n      transformedContext,\n      abortController.signal,\n    );\n    void executeSessionStream(\n      session,\n      streamIterator,\n      abortController,\n      activeAgent,\n    );\n  };\n\n  const abort = (sessionId?: string) => {\n    if (sessionId) {\n      const controller = abortControllers.get(sessionId);\n      if (controller) {\n        controller.abort();\n      }\n    } else {\n      abortControllers.forEach((controller) => controller.abort());\n      abortControllers.clear();\n      dismissTimeouts.forEach((timeoutId) => clearTimeout(timeoutId));\n      dismissTimeouts.clear();\n      sessionMetadata.clear();\n      completedSessionsStack.length = 0;\n      undoneSessionsStack.length = 0;\n      setSessions(new Map());\n      clearSessions(agentOptions?.storage);\n      updateUndoRedoState();\n    }\n  };\n\n  const dismissSession = (\n    sessionId: string,\n    knownAgent?: AgentOptions,\n    knownElements?: Element[],\n  ) => {\n    const currentSessions = sessions();\n    const session = currentSessions.get(sessionId);\n    const activeAgent = knownAgent ?? getAgentForSession(sessionId);\n    const elements = knownElements ?? getElementsForSession(sessionId);\n\n    if (session?.isFading) return;\n\n    if (session && elements.length > 0) {\n      activeAgent?.onDismiss?.(session, elements);\n    }\n\n    setSessions((prev) => {\n      const next = new Map(prev);\n      const existingSession = next.get(sessionId);\n      if (existingSession) {\n        next.set(sessionId, { ...existingSession, isFading: true });\n      }\n      return next;\n    });\n\n    // HACK: Wait for CSS opacity transition + buffer before removing\n    const existingTimeout = dismissTimeouts.get(sessionId);\n    if (existingTimeout) clearTimeout(existingTimeout);\n\n    const timeoutId = setTimeout(() => {\n      dismissTimeouts.delete(sessionId);\n      const controller = abortControllers.get(sessionId);\n      if (controller) {\n        controller.abort();\n        abortControllers.delete(sessionId);\n      }\n      sessionMetadata.delete(sessionId);\n      clearSessionById(sessionId, activeAgent?.storage);\n      setSessions((prev) => {\n        const next = new Map(prev);\n        next.delete(sessionId);\n        return next;\n      });\n    }, FADE_DURATION_MS + DISMISS_ANIMATION_BUFFER_MS);\n    dismissTimeouts.set(sessionId, timeoutId);\n  };\n\n  const undoSession = (sessionId: string) => {\n    const currentSessions = sessions();\n    const session = currentSessions.get(sessionId);\n    const activeAgent = getAgentForSession(sessionId);\n    const elements = getElementsForSession(sessionId);\n\n    if (session) {\n      undoneSessionsStack.push({ session, elements, agent: activeAgent });\n\n      const completedIndex = completedSessionsStack.findIndex(\n        (entry) => entry.session.id === sessionId,\n      );\n      if (completedIndex !== -1) {\n        completedSessionsStack.splice(completedIndex, 1);\n      }\n\n      activeAgent?.onUndo?.(session, elements);\n      void activeAgent?.provider?.undo?.();\n    }\n    dismissSession(sessionId, activeAgent, elements);\n    updateUndoRedoState(activeAgent);\n  };\n\n  const globalUndo = () => {\n    const completedSessionData = completedSessionsStack.pop();\n    if (!completedSessionData) {\n      return;\n    }\n\n    const { session, elements, agent } = completedSessionData;\n    const effectiveAgent = agent ?? agentOptions;\n\n    undoneSessionsStack.push(completedSessionData);\n    effectiveAgent?.onUndo?.(session, elements);\n    void effectiveAgent?.provider?.undo?.();\n    dismissSession(session.id, effectiveAgent, elements);\n    updateUndoRedoState(effectiveAgent);\n  };\n\n  const globalRedo = () => {\n    const undoneSessionData = undoneSessionsStack.pop();\n    if (!undoneSessionData) {\n      return;\n    }\n\n    const effectiveAgent = undoneSessionData.agent ?? agentOptions;\n    const { session, elements } = undoneSessionData;\n\n    void effectiveAgent?.provider?.redo?.();\n\n    let validElements = elements.filter((element) =>\n      isElementConnected(element),\n    );\n\n    if (validElements.length === 0) {\n      const reacquiredElement = tryReacquireElement(session);\n      if (reacquiredElement) {\n        validElements = [reacquiredElement];\n      }\n    }\n\n    if (validElements.length > 0 && effectiveAgent) {\n      completedSessionsStack.push(undoneSessionData);\n\n      const newBounds = validElements.map((element) =>\n        createElementBounds(element),\n      );\n      const restoredSession: AgentSession = {\n        ...session,\n        selectionBounds: newBounds,\n      };\n\n      sessionMetadata.set(session.id, {\n        elements: validElements,\n        agent: effectiveAgent,\n      });\n      setSessions((prev) => new Map(prev).set(session.id, restoredSession));\n    }\n\n    updateUndoRedoState(effectiveAgent);\n  };\n\n  const acknowledgeSessionError = (sessionId: string): string | undefined => {\n    const currentSessions = sessions();\n    const session = currentSessions.get(sessionId);\n    const prompt = session?.context.prompt;\n    dismissSession(sessionId);\n    return prompt;\n  };\n\n  const retrySession = (sessionId: string) => {\n    const currentSessions = sessions();\n    const session = currentSessions.get(sessionId);\n    const activeAgent = getAgentForSession(sessionId);\n    if (!session || !activeAgent?.provider) return;\n\n    const storage = activeAgent.storage;\n    const elements = getElementsForSession(sessionId);\n\n    const retriedSession = updateSession(\n      session,\n      {\n        error: undefined,\n        isStreaming: true,\n        lastStatus: \"Retrying…\",\n      },\n      storage,\n    );\n\n    setSessions((prev) => new Map(prev).set(sessionId, retriedSession));\n    saveSessionById(retriedSession, storage);\n\n    if (elements.length > 0) {\n      activeAgent.onStart?.(retriedSession, elements);\n    }\n\n    const abortController = new AbortController();\n    abortControllers.set(sessionId, abortController);\n\n    const contextWithSessionId: AgentContext = {\n      ...retriedSession.context,\n      sessionId,\n    };\n\n    const streamIterator = activeAgent.provider.send(\n      contextWithSessionId,\n      abortController.signal,\n    );\n    void executeSessionStream(\n      retriedSession,\n      streamIterator,\n      abortController,\n      activeAgent,\n    );\n  };\n\n  const updateSessionBoundsOnViewportChange = () => {\n    const currentSessions = sessions();\n    if (currentSessions.size === 0) return;\n\n    const updatedSessions = new Map(currentSessions);\n    let didUpdate = false;\n\n    for (const [sessionId, session] of currentSessions) {\n      const elements = getElementsForSession(sessionId);\n      const firstElement = elements[0];\n\n      if (isElementConnected(firstElement)) {\n        const newBounds = elements\n          .filter((element) => isElementConnected(element))\n          .map((element) => createElementBounds(element));\n\n        if (newBounds.length > 0) {\n          const oldFirstBounds = session.selectionBounds[0];\n          const newFirstBounds = newBounds[0];\n          const updatedPosition = recalculateSessionPosition({\n            currentPosition: session.position,\n            previousBounds: oldFirstBounds,\n            nextBounds: newFirstBounds,\n          });\n\n          updatedSessions.set(sessionId, {\n            ...session,\n            selectionBounds: newBounds,\n            position: updatedPosition,\n          });\n          didUpdate = true;\n        }\n      }\n    }\n\n    if (didUpdate) {\n      setSessions(updatedSessions);\n    }\n  };\n\n  const getSessionElement = (sessionId: string): Element | undefined =>\n    getElementsForSession(sessionId)[0];\n\n  const getSessionElements = (sessionId: string): Element[] =>\n    getElementsForSession(sessionId);\n\n  return {\n    sessions,\n    isProcessing,\n    canUndo,\n    canRedo,\n    session: {\n      start: startSession,\n      abort,\n      dismiss: dismissSession,\n      retry: retrySession,\n      undo: undoSession,\n      getElement: getSessionElement,\n      getElements: getSessionElements,\n      tryResume: tryResumeSessions,\n      acknowledgeError: acknowledgeSessionError,\n    },\n    history: {\n      undo: globalUndo,\n      redo: globalRedo,\n    },\n    _internal: {\n      updateBoundsOnViewportChange: updateSessionBoundsOnViewportChange,\n      setOptions,\n      getOptions,\n    },\n  };\n};\n"
  },
  {
    "path": "packages/react-grab/src/core/agent/session.ts",
    "content": "import { MAX_MEMORY_SESSIONS } from \"../../constants.js\";\nimport type {\n  Position,\n  AgentContext,\n  AgentSession,\n  AgentSessionStorage,\n  OverlayBounds,\n} from \"../../types.js\";\nimport { generateId } from \"../../utils/generate-id.js\";\nimport { logRecoverableError } from \"../../utils/log-recoverable-error.js\";\n\nconst STORAGE_KEY = \"react-grab:agent-sessions\";\n\nexport const createSession = (\n  context: AgentContext,\n  position: Position,\n  selectionBounds: OverlayBounds[],\n  tagName?: string,\n  componentName?: string,\n): AgentSession => {\n  const now = Date.now();\n  return {\n    id: generateId(\"session\"),\n    context,\n    lastStatus: \"\",\n    isStreaming: true,\n    createdAt: now,\n    lastUpdatedAt: now,\n    position,\n    selectionBounds,\n    tagName,\n    componentName,\n  };\n};\n\nconst memorySessions = new Map<string, AgentSession>();\n\nconst evictOldestMemorySessions = (): void => {\n  while (memorySessions.size > MAX_MEMORY_SESSIONS) {\n    const oldestKey = memorySessions.keys().next().value;\n    if (oldestKey !== undefined) {\n      memorySessions.delete(oldestKey);\n    }\n  }\n};\n\nexport const saveSessions = (\n  sessions: Map<string, AgentSession>,\n  storage?: AgentSessionStorage | null,\n): void => {\n  if (!storage) {\n    memorySessions.clear();\n    sessions.forEach((session, id) => memorySessions.set(id, session));\n    evictOldestMemorySessions();\n    return;\n  }\n\n  try {\n    const sessionsObject = Object.fromEntries(sessions);\n    storage.setItem(STORAGE_KEY, JSON.stringify(sessionsObject));\n  } catch (error) {\n    logRecoverableError(\n      \"Failed to save sessions to storage, falling back to memory\",\n      error,\n    );\n    memorySessions.clear();\n    sessions.forEach((session, id) => memorySessions.set(id, session));\n    evictOldestMemorySessions();\n  }\n};\n\nexport const saveSessionById = (\n  session: AgentSession,\n  storage?: AgentSessionStorage | null,\n): void => {\n  const sessions = loadSessions(storage);\n  sessions.set(session.id, session);\n  saveSessions(sessions, storage);\n};\n\nexport const loadSessions = (\n  storage?: AgentSessionStorage | null,\n): Map<string, AgentSession> => {\n  if (!storage) {\n    return new Map(memorySessions);\n  }\n\n  try {\n    const data = storage.getItem(STORAGE_KEY);\n    if (!data) return new Map();\n    const sessionsObject = JSON.parse(data) as Record<string, AgentSession>;\n    return new Map(Object.entries(sessionsObject));\n  } catch (error) {\n    logRecoverableError(\"Failed to load sessions from storage\", error);\n    return new Map();\n  }\n};\n\nexport const clearSessions = (storage?: AgentSessionStorage | null): void => {\n  if (!storage) {\n    memorySessions.clear();\n    return;\n  }\n\n  try {\n    storage.removeItem(STORAGE_KEY);\n  } catch (error) {\n    logRecoverableError(\"Failed to clear sessions from storage\", error);\n    memorySessions.clear();\n  }\n};\n\nexport const clearSessionById = (\n  sessionId: string,\n  storage?: AgentSessionStorage | null,\n): void => {\n  const sessions = loadSessions(storage);\n  sessions.delete(sessionId);\n  saveSessions(sessions, storage);\n};\n\nexport const updateSession = (\n  session: AgentSession,\n  updates: Partial<\n    Pick<AgentSession, \"lastStatus\" | \"isStreaming\" | \"error\" | \"context\">\n  >,\n  storage?: AgentSessionStorage | null,\n): AgentSession => {\n  const updatedSession = { ...session, ...updates, lastUpdatedAt: Date.now() };\n  saveSessionById(updatedSession, storage);\n  return updatedSession;\n};\n"
  },
  {
    "path": "packages/react-grab/src/core/arrow-navigation.ts",
    "content": "import { MAX_ARROW_NAVIGATION_HISTORY } from \"../constants.js\";\nimport type { OverlayBounds } from \"../types.js\";\nimport { getElementsAtPoint } from \"../utils/get-element-at-position.js\";\nimport { getVisibleBoundsCenter } from \"../utils/get-visible-bounds-center.js\";\nimport { isElementConnected } from \"../utils/is-element-connected.js\";\n\ninterface ElementValidator {\n  (element: Element): boolean;\n}\n\ninterface BoundsCalculator {\n  (element: Element): OverlayBounds;\n}\n\ninterface ArrowNavigator {\n  findNext: (key: string, currentElement: Element) => Element | null;\n  clearHistory: () => void;\n}\n\nexport const createArrowNavigator = (\n  isValidGrabbableElement: ElementValidator,\n  createElementBounds: BoundsCalculator,\n): ArrowNavigator => {\n  let navigationHistory: Element[] = [];\n\n  const findVerticalNext = (\n    currentElement: Element,\n    direction: 1 | -1,\n  ): Element | null => {\n    const bounds = createElementBounds(currentElement);\n    const probePoint = getVisibleBoundsCenter(bounds);\n    const elementsAtPoint = getElementsAtPoint(\n      probePoint.x,\n      probePoint.y,\n    ).filter(isValidGrabbableElement);\n\n    const currentIndex = elementsAtPoint.indexOf(currentElement);\n    if (currentIndex === -1) return null;\n    return elementsAtPoint[currentIndex + direction] ?? null;\n  };\n\n  const findUp = (currentElement: Element): Element | null => {\n    const nextElement = findVerticalNext(currentElement, 1);\n    if (nextElement) {\n      navigationHistory.push(currentElement);\n      if (navigationHistory.length > MAX_ARROW_NAVIGATION_HISTORY) {\n        navigationHistory = navigationHistory.slice(\n          -MAX_ARROW_NAVIGATION_HISTORY,\n        );\n      }\n    }\n    return nextElement;\n  };\n\n  const findDown = (currentElement: Element): Element | null => {\n    if (navigationHistory.length > 0) {\n      const previousElement = navigationHistory.pop()!;\n      if (isElementConnected(previousElement)) {\n        return previousElement;\n      }\n    }\n    return findVerticalNext(currentElement, -1);\n  };\n\n  const findHorizontal = (\n    currentElement: Element,\n    isForward: boolean,\n  ): Element | null => {\n    const findEdgeDescendant = (parentElement: Element): Element | null => {\n      const children = Array.from(parentElement.children);\n      const ordered = isForward ? children : children.reverse();\n      for (const childElement of ordered) {\n        if (isForward) {\n          if (isValidGrabbableElement(childElement)) return childElement;\n          const descendant = findEdgeDescendant(childElement);\n          if (descendant) return descendant;\n        } else {\n          const descendant = findEdgeDescendant(childElement);\n          if (descendant) return descendant;\n          if (isValidGrabbableElement(childElement)) return childElement;\n        }\n      }\n      return null;\n    };\n\n    const getSibling = (element: Element) =>\n      isForward ? element.nextElementSibling : element.previousElementSibling;\n\n    let nextElement: Element | null = null;\n\n    if (isForward) {\n      nextElement = findEdgeDescendant(currentElement);\n    }\n\n    if (!nextElement) {\n      let searchElement: Element | null = currentElement;\n      while (searchElement) {\n        let sibling = getSibling(searchElement);\n        while (sibling) {\n          const descendant = findEdgeDescendant(sibling);\n          if (descendant) {\n            nextElement = descendant;\n            break;\n          }\n          if (isValidGrabbableElement(sibling)) {\n            nextElement = sibling;\n            break;\n          }\n          sibling = getSibling(sibling);\n        }\n        if (nextElement) break;\n        const parentElement: HTMLElement | null = searchElement.parentElement;\n        if (\n          !isForward &&\n          parentElement &&\n          isValidGrabbableElement(parentElement)\n        ) {\n          nextElement = parentElement;\n          break;\n        }\n        searchElement = parentElement;\n      }\n    }\n\n    return nextElement;\n  };\n\n  const findNext = (key: string, currentElement: Element): Element | null => {\n    switch (key) {\n      case \"ArrowUp\":\n        return findUp(currentElement);\n      case \"ArrowDown\":\n        return findDown(currentElement);\n      case \"ArrowRight\":\n        return findHorizontal(currentElement, true);\n      case \"ArrowLeft\":\n        return findHorizontal(currentElement, false);\n      default:\n        return null;\n    }\n  };\n\n  const clearHistory = () => {\n    navigationHistory = [];\n  };\n\n  return {\n    findNext,\n    clearHistory,\n  };\n};\n"
  },
  {
    "path": "packages/react-grab/src/core/auto-scroll.ts",
    "content": "import type { Position } from \"../types.js\";\nimport {\n  AUTO_SCROLL_EDGE_THRESHOLD_PX,\n  AUTO_SCROLL_SPEED_PX,\n} from \"../constants.js\";\nimport {\n  nativeCancelAnimationFrame,\n  nativeRequestAnimationFrame,\n} from \"../utils/native-raf.js\";\n\ninterface AutoScrollDirection {\n  top: boolean;\n  bottom: boolean;\n  left: boolean;\n  right: boolean;\n}\n\nexport const getAutoScrollDirection = (\n  clientX: number,\n  clientY: number,\n): AutoScrollDirection => {\n  return {\n    top: clientY < AUTO_SCROLL_EDGE_THRESHOLD_PX,\n    bottom: clientY > window.innerHeight - AUTO_SCROLL_EDGE_THRESHOLD_PX,\n    left: clientX < AUTO_SCROLL_EDGE_THRESHOLD_PX,\n    right: clientX > window.innerWidth - AUTO_SCROLL_EDGE_THRESHOLD_PX,\n  };\n};\n\ninterface AutoScroller {\n  start: () => void;\n  stop: () => void;\n  isActive: () => boolean;\n}\n\nexport const createAutoScroller = (\n  getMousePosition: () => Position,\n  shouldContinue: () => boolean,\n): AutoScroller => {\n  let animationId: number | null = null;\n\n  const scroll = () => {\n    if (!shouldContinue()) {\n      stop();\n      return;\n    }\n\n    const position = getMousePosition();\n    const direction = getAutoScrollDirection(position.x, position.y);\n\n    if (direction.top) window.scrollBy(0, -AUTO_SCROLL_SPEED_PX);\n    if (direction.bottom) window.scrollBy(0, AUTO_SCROLL_SPEED_PX);\n    if (direction.left) window.scrollBy(-AUTO_SCROLL_SPEED_PX, 0);\n    if (direction.right) window.scrollBy(AUTO_SCROLL_SPEED_PX, 0);\n\n    if (\n      direction.top ||\n      direction.bottom ||\n      direction.left ||\n      direction.right\n    ) {\n      animationId = nativeRequestAnimationFrame(scroll);\n    } else {\n      animationId = null;\n    }\n  };\n\n  const start = () => {\n    scroll();\n  };\n\n  const stop = () => {\n    if (animationId !== null) {\n      nativeCancelAnimationFrame(animationId);\n      animationId = null;\n    }\n  };\n\n  const isActive = () => animationId !== null;\n\n  return {\n    start,\n    stop,\n    isActive,\n  };\n};\n"
  },
  {
    "path": "packages/react-grab/src/core/context.ts",
    "content": "import {\n  getReactStack,\n  resolveStack,\n  formatStack,\n  checkIsNextProject,\n  resolveComponentName,\n} from \"element-source\";\nimport {\n  getFiberFromHostInstance,\n  isInstrumentationActive,\n  getDisplayName,\n  isCompositeFiber,\n  traverseFiber,\n} from \"bippy\";\nimport {\n  PREVIEW_TEXT_MAX_LENGTH,\n  PREVIEW_ATTR_VALUE_MAX_LENGTH,\n  PREVIEW_MAX_ATTRS,\n  PREVIEW_PRIORITY_ATTRS,\n  DEFAULT_MAX_CONTEXT_LINES,\n} from \"../constants.js\";\nimport { getTagName } from \"../utils/get-tag-name.js\";\nimport { truncateString } from \"../utils/truncate-string.js\";\n\nexport {\n  checkIsNextProject,\n  getReactStack as getStack,\n  resolveComponentName as getNearestComponentName,\n};\n\nconst NON_COMPONENT_PREFIXES = new Set([\n  \"_\",\n  \"$\",\n  \"motion.\",\n  \"styled.\",\n  \"chakra.\",\n  \"ark.\",\n  \"Primitive.\",\n  \"Slot.\",\n]);\n\nconst NEXT_INTERNAL_COMPONENT_NAMES = new Set([\n  \"InnerLayoutRouter\",\n  \"RedirectErrorBoundary\",\n  \"RedirectBoundary\",\n  \"HTTPAccessFallbackErrorBoundary\",\n  \"HTTPAccessFallbackBoundary\",\n  \"LoadingBoundary\",\n  \"ErrorBoundary\",\n  \"InnerScrollAndFocusHandler\",\n  \"ScrollAndFocusHandler\",\n  \"RenderFromTemplateContext\",\n  \"OuterLayoutRouter\",\n  \"body\",\n  \"html\",\n  \"DevRootHTTPAccessFallbackBoundary\",\n  \"AppDevOverlayErrorBoundary\",\n  \"AppDevOverlay\",\n  \"HotReload\",\n  \"Router\",\n  \"ErrorBoundaryHandler\",\n  \"AppRouter\",\n  \"ServerRoot\",\n  \"SegmentStateProvider\",\n  \"RootErrorBoundary\",\n  \"LoadableComponent\",\n  \"MotionDOMComponent\",\n]);\n\nconst REACT_INTERNAL_COMPONENT_NAMES = new Set([\n  \"Suspense\",\n  \"Fragment\",\n  \"StrictMode\",\n  \"Profiler\",\n  \"SuspenseList\",\n]);\n\nconst isUsefulComponentName = (name: string): boolean => {\n  if (!name) return false;\n  if (NEXT_INTERNAL_COMPONENT_NAMES.has(name)) return false;\n  if (REACT_INTERNAL_COMPONENT_NAMES.has(name)) return false;\n  for (const prefix of NON_COMPONENT_PREFIXES) {\n    if (name.startsWith(prefix)) return false;\n  }\n  if (name === \"SlotClone\" || name === \"Slot\") return false;\n  return true;\n};\n\nconst findNearestFiberElement = (element: Element): Element => {\n  if (!isInstrumentationActive()) return element;\n  let current: Element | null = element;\n  while (current) {\n    if (getFiberFromHostInstance(current)) return current;\n    current = current.parentElement;\n  }\n  return element;\n};\n\nexport const getComponentDisplayName = (element: Element): string | null => {\n  if (!isInstrumentationActive()) return null;\n  const resolvedElement = findNearestFiberElement(element);\n  const fiber = getFiberFromHostInstance(resolvedElement);\n  if (!fiber) return null;\n\n  let currentFiber = fiber.return;\n  while (currentFiber) {\n    if (isCompositeFiber(currentFiber)) {\n      const name = getDisplayName(currentFiber.type);\n      if (name && isUsefulComponentName(name)) {\n        return name;\n      }\n    }\n    currentFiber = currentFiber.return;\n  }\n\n  return null;\n};\n\ninterface StackContextOptions {\n  maxLines?: number;\n}\n\nconst getComponentNamesFromFiber = (\n  element: Element,\n  maxCount: number,\n): string[] => {\n  if (!isInstrumentationActive()) return [];\n  const fiber = getFiberFromHostInstance(element);\n  if (!fiber) return [];\n\n  const componentNames: string[] = [];\n  traverseFiber(\n    fiber,\n    (currentFiber) => {\n      if (componentNames.length >= maxCount) return true;\n      if (isCompositeFiber(currentFiber)) {\n        const name = getDisplayName(currentFiber.type);\n        if (name && isUsefulComponentName(name)) {\n          componentNames.push(name);\n        }\n      }\n      return false;\n    },\n    true,\n  );\n  return componentNames;\n};\n\nexport const getStackContext = async (\n  element: Element,\n  options: StackContextOptions = {},\n): Promise<string> => {\n  const maxLines = options.maxLines ?? DEFAULT_MAX_CONTEXT_LINES;\n  const stack = await resolveStack(element);\n\n  if (stack.length > 0) {\n    return formatStack(stack, maxLines);\n  }\n\n  const componentNames = getComponentNamesFromFiber(element, maxLines);\n  if (componentNames.length > 0) {\n    return componentNames.map((name) => `\\n  in ${name}`).join(\"\");\n  }\n\n  return \"\";\n};\n\nexport const getElementContext = async (\n  element: Element,\n  options: StackContextOptions = {},\n): Promise<string> => {\n  const resolvedElement = findNearestFiberElement(element);\n  const html = getHTMLPreview(resolvedElement);\n  const stackContext = await getStackContext(resolvedElement, options);\n\n  if (stackContext) {\n    return `${html}${stackContext}`;\n  }\n\n  return getFallbackContext(resolvedElement);\n};\n\nconst getFallbackContext = (element: Element): string => {\n  const tagName = getTagName(element);\n\n  if (!(element instanceof HTMLElement)) {\n    const attrsHint = formatPriorityAttrs(element, {\n      truncate: false,\n      maxAttrs: PREVIEW_PRIORITY_ATTRS.length,\n    });\n    return `<${tagName}${attrsHint} />`;\n  }\n\n  const text = element.innerText?.trim() ?? element.textContent?.trim() ?? \"\";\n\n  let attrsText = \"\";\n  for (const { name, value } of element.attributes) {\n    attrsText += ` ${name}=\"${value}\"`;\n  }\n\n  const truncatedText = truncateString(text, PREVIEW_TEXT_MAX_LENGTH);\n\n  if (truncatedText.length > 0) {\n    return `<${tagName}${attrsText}>\\n  ${truncatedText}\\n</${tagName}>`;\n  }\n  return `<${tagName}${attrsText} />`;\n};\n\nconst truncateAttrValue = (value: string): string =>\n  truncateString(value, PREVIEW_ATTR_VALUE_MAX_LENGTH);\n\ninterface FormatPriorityAttrsOptions {\n  truncate?: boolean;\n  maxAttrs?: number;\n}\n\nconst formatPriorityAttrs = (\n  element: Element,\n  options: FormatPriorityAttrsOptions = {},\n): string => {\n  const { truncate = true, maxAttrs = PREVIEW_MAX_ATTRS } = options;\n  const priorityAttrs: string[] = [];\n\n  for (const name of PREVIEW_PRIORITY_ATTRS) {\n    if (priorityAttrs.length >= maxAttrs) break;\n    const value = element.getAttribute(name);\n    if (value) {\n      const formattedValue = truncate ? truncateAttrValue(value) : value;\n      priorityAttrs.push(`${name}=\"${formattedValue}\"`);\n    }\n  }\n\n  return priorityAttrs.length > 0 ? ` ${priorityAttrs.join(\" \")}` : \"\";\n};\n\nexport const getHTMLPreview = (element: Element): string => {\n  const tagName = getTagName(element);\n  const text =\n    element instanceof HTMLElement\n      ? (element.innerText?.trim() ?? element.textContent?.trim() ?? \"\")\n      : (element.textContent?.trim() ?? \"\");\n\n  let attrsText = \"\";\n  for (const { name, value } of element.attributes) {\n    attrsText += ` ${name}=\"${truncateAttrValue(value)}\"`;\n  }\n\n  const topElements: Array<Element> = [];\n  const bottomElements: Array<Element> = [];\n  let foundFirstText = false;\n\n  const childNodes = Array.from(element.childNodes);\n  for (const node of childNodes) {\n    if (node.nodeType === Node.COMMENT_NODE) continue;\n\n    if (node.nodeType === Node.TEXT_NODE) {\n      if (node.textContent && node.textContent.trim().length > 0) {\n        foundFirstText = true;\n      }\n    } else if (node instanceof Element) {\n      if (!foundFirstText) {\n        topElements.push(node);\n      } else {\n        bottomElements.push(node);\n      }\n    }\n  }\n\n  const formatElements = (elements: Array<Element>): string => {\n    if (elements.length === 0) return \"\";\n    if (elements.length <= 2) {\n      return elements\n        .map((childElement) => `<${getTagName(childElement)} ...>`)\n        .join(\"\\n  \");\n    }\n    return `(${elements.length} elements)`;\n  };\n\n  let content = \"\";\n  const topElementsStr = formatElements(topElements);\n  if (topElementsStr) content += `\\n  ${topElementsStr}`;\n  if (text.length > 0) {\n    content += `\\n  ${truncateString(text, PREVIEW_TEXT_MAX_LENGTH)}`;\n  }\n  const bottomElementsStr = formatElements(bottomElements);\n  if (bottomElementsStr) content += `\\n  ${bottomElementsStr}`;\n\n  if (content.length > 0) {\n    return `<${tagName}${attrsText}>${content}\\n</${tagName}>`;\n  }\n  return `<${tagName}${attrsText} />`;\n};\n"
  },
  {
    "path": "packages/react-grab/src/core/copy.ts",
    "content": "import { copyContent, type ReactGrabEntry } from \"../utils/copy-content.js\";\nimport { generateSnippet } from \"../utils/generate-snippet.js\";\nimport { joinSnippets } from \"../utils/join-snippets.js\";\nimport { normalizeError } from \"../utils/normalize-error.js\";\n\ninterface CopyOptions {\n  maxContextLines?: number;\n  getContent?: (elements: Element[]) => Promise<string> | string;\n  componentName?: string;\n}\n\ninterface CopyHooks {\n  onBeforeCopy: (elements: Element[]) => Promise<void>;\n  transformSnippet: (snippet: string, element: Element) => Promise<string>;\n  transformCopyContent: (\n    content: string,\n    elements: Element[],\n  ) => Promise<string>;\n  onAfterCopy: (elements: Element[], success: boolean) => void;\n  onCopySuccess: (elements: Element[], content: string) => void;\n  onCopyError: (error: Error) => void;\n}\n\nexport const tryCopyWithFallback = async (\n  options: CopyOptions,\n  hooks: CopyHooks,\n  elements: Element[],\n  extraPrompt?: string,\n): Promise<boolean> => {\n  let didCopy = false;\n  let copiedContent = \"\";\n\n  await hooks.onBeforeCopy(elements);\n\n  try {\n    let generatedContent: string;\n    let entries: ReactGrabEntry[] | undefined;\n\n    if (options.getContent) {\n      generatedContent = await options.getContent(elements);\n    } else {\n      const rawSnippets = await generateSnippet(elements, {\n        maxLines: options.maxContextLines,\n      });\n      const transformedSnippets = await Promise.all(\n        rawSnippets.map((snippet, index) =>\n          snippet.trim()\n            ? hooks.transformSnippet(snippet, elements[index])\n            : Promise.resolve(\"\"),\n        ),\n      );\n      const snippetElementPairs = transformedSnippets\n        .map((snippet, index) => ({ snippet, element: elements[index] }))\n        .filter(({ snippet }) => snippet.trim());\n\n      generatedContent = joinSnippets(\n        snippetElementPairs.map(({ snippet }) => snippet),\n      );\n      entries = snippetElementPairs.map(({ snippet, element }) => ({\n        tagName: element.localName,\n        content: snippet,\n        commentText: extraPrompt,\n      }));\n    }\n\n    if (generatedContent.trim()) {\n      const transformedContent = await hooks.transformCopyContent(\n        generatedContent,\n        elements,\n      );\n\n      copiedContent = extraPrompt\n        ? `${extraPrompt}\\n\\n${transformedContent}`\n        : transformedContent;\n\n      didCopy = copyContent(copiedContent, {\n        componentName: options.componentName,\n        entries,\n      });\n    }\n  } catch (error) {\n    hooks.onCopyError(normalizeError(error));\n  }\n\n  if (didCopy) {\n    hooks.onCopySuccess(elements, copiedContent);\n  }\n  hooks.onAfterCopy(elements, didCopy);\n\n  return didCopy;\n};\n"
  },
  {
    "path": "packages/react-grab/src/core/events.ts",
    "content": "interface EventListenerManager {\n  signal: AbortSignal;\n  abort: () => void;\n  addWindowListener: <K extends keyof WindowEventMap>(\n    type: K,\n    listener: (event: WindowEventMap[K]) => void,\n    options?: Omit<AddEventListenerOptions, \"signal\">,\n  ) => void;\n  addDocumentListener: <K extends keyof DocumentEventMap>(\n    type: K,\n    listener: (event: DocumentEventMap[K]) => void,\n    options?: Omit<AddEventListenerOptions, \"signal\">,\n  ) => void;\n}\n\nexport const createEventListenerManager = (): EventListenerManager => {\n  const abortController = new AbortController();\n\n  const addWindowListener: EventListenerManager[\"addWindowListener\"] = (\n    type,\n    listener,\n    options = {},\n  ) => {\n    window.addEventListener(type, listener, {\n      ...options,\n      signal: abortController.signal,\n    });\n  };\n\n  const addDocumentListener: EventListenerManager[\"addDocumentListener\"] = (\n    type,\n    listener,\n    options = {},\n  ) => {\n    document.addEventListener(type, listener, {\n      ...options,\n      signal: abortController.signal,\n    });\n  };\n\n  return {\n    signal: abortController.signal,\n    abort: () => abortController.abort(),\n    addWindowListener,\n    addDocumentListener,\n  };\n};\n"
  },
  {
    "path": "packages/react-grab/src/core/index.tsx",
    "content": "// @ts-expect-error - CSS imported as text via tsup loader\nimport cssText from \"../../dist/styles.css\";\nimport {\n  createMemo,\n  createRoot,\n  createSignal,\n  onCleanup,\n  createEffect,\n  createResource,\n  on,\n  batch,\n} from \"solid-js\";\nimport { render } from \"solid-js/web\";\nimport { createGrabStore } from \"./store.js\";\nimport {\n  isKeyboardEventTriggeredByInput,\n  hasTextSelectionInInput,\n  hasTextSelectionOnPage,\n} from \"../utils/is-keyboard-event-triggered-by-input.js\";\nimport { mountRoot } from \"../utils/mount-root.js\";\nimport {\n  nativeCancelAnimationFrame,\n  nativeRequestAnimationFrame,\n  waitUntilNextFrame,\n} from \"../utils/native-raf.js\";\nimport {\n  getStackContext,\n  getNearestComponentName,\n  getComponentDisplayName,\n  checkIsNextProject,\n} from \"./context.js\";\nimport { resolveSource } from \"element-source\";\nimport { createNoopApi } from \"./noop-api.js\";\nimport { createEventListenerManager } from \"./events.js\";\nimport { tryCopyWithFallback } from \"./copy.js\";\nimport {\n  getElementAtPosition,\n  getElementsAtPoint,\n} from \"../utils/get-element-at-position.js\";\nimport { isValidGrabbableElement } from \"../utils/is-valid-grabbable-element.js\";\nimport { isRootElement } from \"../utils/is-root-element.js\";\nimport { isElementConnected } from \"../utils/is-element-connected.js\";\nimport { getElementsInDrag } from \"../utils/get-elements-in-drag.js\";\nimport { createElementBounds } from \"../utils/create-element-bounds.js\";\nimport { createElementSelector } from \"../utils/create-element-selector.js\";\nimport { getVisibleBoundsCenter } from \"../utils/get-visible-bounds-center.js\";\nimport { invalidateInteractionCaches } from \"../utils/invalidate-interaction-caches.js\";\nimport { normalizeErrorMessage } from \"../utils/normalize-error.js\";\nimport {\n  createBoundsFromDragRect,\n  createFlatOverlayBounds,\n  createPageRectFromBounds,\n} from \"../utils/create-bounds-from-drag-rect.js\";\nimport { getTagName } from \"../utils/get-tag-name.js\";\nimport {\n  ARROW_KEYS,\n  FEEDBACK_DURATION_MS,\n  FADE_COMPLETE_BUFFER_MS,\n  KEYDOWN_SPAM_TIMEOUT_MS,\n  DRAG_THRESHOLD_PX,\n  ELEMENT_DETECTION_THROTTLE_MS,\n  PENDING_DETECTION_STALENESS_MS,\n  COMPONENT_NAME_DEBOUNCE_MS,\n  DRAG_PREVIEW_DEBOUNCE_MS,\n  MODIFIER_KEYS,\n  BLUR_DEACTIVATION_THRESHOLD_MS,\n  BOUNDS_RECALC_INTERVAL_MS,\n  INPUT_FOCUS_ACTIVATION_DELAY_MS,\n  INPUT_TEXT_SELECTION_ACTIVATION_DELAY_MS,\n  DEFAULT_KEY_HOLD_DURATION_MS,\n  DEFAULT_MAX_CONTEXT_LINES,\n  MIN_HOLD_FOR_ACTIVATION_AFTER_COPY_MS,\n  ZOOM_DETECTION_THRESHOLD,\n  ACTION_CYCLE_IDLE_TRIGGER_MS,\n  WINDOW_REFOCUS_GRACE_PERIOD_MS,\n  DROPDOWN_HOVER_OPEN_DELAY_MS,\n  PREVIEW_TEXT_MAX_LENGTH,\n  NEXTJS_REVALIDATION_DELAY_MS,\n  TOOLBAR_DEFAULT_POSITION_RATIO,\n} from \"../constants.js\";\nimport { getBoundsCenter } from \"../utils/get-bounds-center.js\";\nimport { getElementCenter } from \"../utils/get-element-center.js\";\nimport { isCLikeKey } from \"../utils/is-c-like-key.js\";\nimport { isTargetKeyCombination } from \"../utils/is-target-key-combination.js\";\nimport { parseActivationKey } from \"../utils/parse-activation-key.js\";\nimport { isEventFromOverlay } from \"../utils/is-event-from-overlay.js\";\nimport { openFile } from \"../utils/open-file.js\";\nimport { combineBounds } from \"../utils/combine-bounds.js\";\nimport {\n  resolveActionEnabled,\n  resolveToolbarActionEnabled,\n} from \"../utils/resolve-action-enabled.js\";\nimport type {\n  Position,\n  Options,\n  OverlayBounds,\n  GrabbedBox,\n  ReactGrabAPI,\n  ReactGrabState,\n  SelectionLabelInstance,\n  AgentSession,\n  AgentOptions,\n  ContextMenuActionContext,\n  ContextMenuAction,\n  ActionCycleItem,\n  ActionCycleState,\n  ArrowNavigationState,\n  PerformWithFeedbackOptions,\n  SettableOptions,\n  SourceInfo,\n  Plugin,\n  ToolbarState,\n  HistoryItem,\n  DropdownAnchor,\n} from \"../types.js\";\nimport { DEFAULT_THEME } from \"./theme.js\";\nimport { createPluginRegistry } from \"./plugin-registry.js\";\nimport { createAgentManager } from \"./agent/manager.js\";\nimport { createArrowNavigator } from \"./arrow-navigation.js\";\nimport {\n  getRequiredModifiers,\n  setupKeyboardEventClaimer,\n} from \"./keyboard-handlers.js\";\nimport { createAutoScroller, getAutoScrollDirection } from \"./auto-scroll.js\";\nimport { logIntro } from \"./log-intro.js\";\nimport { onIdle } from \"../utils/on-idle.js\";\nimport { getScriptOptions } from \"../utils/get-script-options.js\";\nimport { isEnterCode } from \"../utils/is-enter-code.js\";\nimport { isMac } from \"../utils/is-mac.js\";\nimport {\n  loadToolbarState,\n  saveToolbarState,\n} from \"../components/toolbar/state.js\";\nimport { copyPlugin } from \"./plugins/copy.js\";\nimport { commentPlugin } from \"./plugins/comment.js\";\nimport { openPlugin } from \"./plugins/open.js\";\nimport { copyHtmlPlugin } from \"./plugins/copy-html.js\";\nimport { copyStylesPlugin } from \"./plugins/copy-styles.js\";\nimport {\n  freezeAnimations,\n  freezeAllAnimations,\n  freezeGlobalAnimations,\n  unfreezeGlobalAnimations,\n} from \"../utils/freeze-animations.js\";\nimport {\n  freezePseudoStates,\n  unfreezePseudoStates,\n} from \"../utils/freeze-pseudo-states.js\";\nimport { freezeUpdates } from \"../utils/freeze-updates.js\";\nimport {\n  loadHistory,\n  addHistoryItem,\n  removeHistoryItem,\n  clearHistory,\n} from \"../utils/history-storage.js\";\nimport { copyContent } from \"../utils/copy-content.js\";\nimport { joinSnippets } from \"../utils/join-snippets.js\";\nimport { generateId } from \"../utils/generate-id.js\";\nimport { logRecoverableError } from \"../utils/log-recoverable-error.js\";\n\nconst builtInPlugins = [\n  copyPlugin,\n  commentPlugin,\n  copyHtmlPlugin,\n  copyStylesPlugin,\n  openPlugin,\n];\n\ninterface CopyWithLabelOptions {\n  element: Element;\n  cursorX: number;\n  selectedElements?: Element[];\n  extraPrompt?: string;\n  shouldDeactivateAfter?: boolean;\n  onComplete?: () => void;\n  dragRect?: {\n    pageX: number;\n    pageY: number;\n    width: number;\n    height: number;\n  };\n}\n\ninterface BuildActionContextOptions {\n  element: Element;\n  filePath: string | undefined;\n  lineNumber: number | undefined;\n  tagName: string | undefined;\n  componentName: string | undefined;\n  position: Position;\n  performWithFeedbackOptions?: PerformWithFeedbackOptions;\n  shouldDeferHideContextMenu: boolean;\n  onBeforeCopy?: () => void;\n  onBeforePrompt?: () => void;\n  customEnterPromptMode?: (agent?: AgentOptions) => void;\n}\n\nlet hasInited = false;\nconst toolbarStateChangeCallbacks = new Set<(state: ToolbarState) => void>();\n\nexport const init = (rawOptions?: Options): ReactGrabAPI => {\n  if (typeof window === \"undefined\") {\n    return createNoopApi();\n  }\n\n  const scriptOptions = getScriptOptions();\n\n  const initialOptions: Options = {\n    enabled: true,\n    activationMode: \"toggle\",\n    keyHoldDuration: DEFAULT_KEY_HOLD_DURATION_MS,\n    allowActivationInsideInput: true,\n    maxContextLines: DEFAULT_MAX_CONTEXT_LINES,\n    ...scriptOptions,\n    ...rawOptions,\n  };\n\n  if (initialOptions.enabled === false || hasInited) {\n    return createNoopApi();\n  }\n  hasInited = true;\n\n  logIntro();\n\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars -- need to omit enabled from settableOptions to avoid circular dependency\n  const { enabled: _enabled, ...settableOptions } = initialOptions;\n\n  return createRoot((dispose) => {\n    let disposed = false;\n    let disposeRenderer: (() => void) | undefined;\n\n    const pluginRegistry = createPluginRegistry(settableOptions);\n\n    const getAgentFromActions = () => {\n      for (const action of pluginRegistry.store.actions) {\n        if (action.agent?.provider) {\n          return action.agent;\n        }\n      }\n      return undefined;\n    };\n\n    const { store, actions } = createGrabStore({\n      theme: DEFAULT_THEME,\n      hasAgentProvider: Boolean(getAgentFromActions()?.provider),\n      keyHoldDuration:\n        pluginRegistry.store.options.keyHoldDuration ??\n        DEFAULT_KEY_HOLD_DURATION_MS,\n    });\n\n    const isHoldingKeys = createMemo(() => store.current.state === \"holding\");\n    const isActivated = createMemo(() => store.current.state === \"active\");\n    const isFrozenPhase = createMemo(\n      () =>\n        store.current.state === \"active\" && store.current.phase === \"frozen\",\n    );\n    const isDragging = createMemo(\n      () =>\n        store.current.state === \"active\" && store.current.phase === \"dragging\",\n    );\n    const didJustDrag = createMemo(\n      () =>\n        store.current.state === \"active\" &&\n        store.current.phase === \"justDragged\",\n    );\n    const isCopying = createMemo(() => store.current.state === \"copying\");\n    const didJustCopy = createMemo(() => store.current.state === \"justCopied\");\n    const isPromptMode = createMemo(\n      () =>\n        store.current.state === \"active\" && Boolean(store.current.isPromptMode),\n    );\n    const isCommentMode = createMemo(\n      () => store.pendingCommentMode || isPromptMode(),\n    );\n    const isPendingDismiss = createMemo(\n      () =>\n        store.current.state === \"active\" &&\n        Boolean(store.current.isPromptMode) &&\n        Boolean(store.current.isPendingDismiss),\n    );\n\n    createEffect(\n      on(isActivated, (activated, previousActivated) => {\n        if (activated && !previousActivated) {\n          freezePseudoStates();\n          freezeGlobalAnimations();\n          // HACK: Prevent browser from taking over touch gestures\n          document.body.style.touchAction = \"none\";\n        } else if (!activated && previousActivated) {\n          unfreezePseudoStates();\n          unfreezeGlobalAnimations();\n          document.body.style.touchAction = \"\";\n        }\n      }),\n    );\n\n    const savedToolbarState = loadToolbarState();\n    const [isEnabled, setIsEnabled] = createSignal(\n      savedToolbarState?.enabled ?? true,\n    );\n    const [toolbarShakeCount, setToolbarShakeCount] = createSignal(0);\n    const [selectionLabelShakeCount, setSelectionLabelShakeCount] =\n      createSignal(0);\n    const [currentToolbarState, setCurrentToolbarState] =\n      createSignal<ToolbarState | null>(savedToolbarState);\n    const [isToolbarSelectHovered, setIsToolbarSelectHovered] =\n      createSignal(false);\n    const [historyItems, setHistoryItems] =\n      createSignal<HistoryItem[]>(loadHistory());\n    const [historyDropdownPosition, setHistoryDropdownPosition] =\n      createSignal<DropdownAnchor | null>(null);\n    const [toolbarMenuPosition, setToolbarMenuPosition] =\n      createSignal<DropdownAnchor | null>(null);\n    const [clearPromptPosition, setClearPromptPosition] =\n      createSignal<DropdownAnchor | null>(null);\n    let toolbarElement: HTMLDivElement | undefined;\n    let dropdownTrackingFrameId: number | null = null;\n    const historyElementMap = new Map<string, Element[]>();\n    const [hasUnreadHistoryItems, setHasUnreadHistoryItems] =\n      createSignal(false);\n    const [clockFlashTrigger, setClockFlashTrigger] = createSignal(0);\n    const [isHistoryHoverOpen, setIsHistoryHoverOpen] = createSignal(false);\n    let historyHoverPreviews: { boxId: string; labelId: string | null }[] = [];\n\n    const getMappedHistoryElements = (historyItemId: string): Element[] =>\n      historyElementMap.get(historyItemId) ?? [];\n\n    const reacquireHistoryElements = (historyItem: HistoryItem): Element[] => {\n      const selectors = historyItem.elementSelectors ?? [];\n      if (selectors.length === 0) return [];\n\n      const reacquiredElements: Element[] = [];\n      for (const selector of selectors) {\n        if (!selector) continue;\n        try {\n          const reacquiredElement = document.querySelector(selector);\n          if (isElementConnected(reacquiredElement)) {\n            reacquiredElements.push(reacquiredElement);\n          }\n          // HACK: querySelector can throw on invalid selectors stored from previous sessions\n        } catch {}\n      }\n      return reacquiredElements;\n    };\n\n    const getConnectedHistoryElements = (\n      historyItem: HistoryItem,\n    ): Element[] => {\n      const mappedElements = getMappedHistoryElements(historyItem.id);\n      const connectedMappedElements = mappedElements.filter((mappedElement) =>\n        isElementConnected(mappedElement),\n      );\n      const areAllMappedElementsConnected =\n        mappedElements.length > 0 &&\n        connectedMappedElements.length === mappedElements.length;\n\n      if (areAllMappedElementsConnected) {\n        return connectedMappedElements;\n      }\n\n      const reacquiredElements = reacquireHistoryElements(historyItem);\n      if (reacquiredElements.length > 0) {\n        historyElementMap.set(historyItem.id, reacquiredElements);\n        return reacquiredElements;\n      }\n\n      return connectedMappedElements;\n    };\n\n    const getFirstConnectedHistoryElement = (\n      historyItem: HistoryItem,\n    ): Element | undefined => getConnectedHistoryElements(historyItem)[0];\n\n    const historyDisconnectedItemIds = createMemo(\n      () => {\n        // HACK: subscribe to dropdown position so connectivity refreshes when dropdown opens\n        void historyDropdownPosition();\n        const disconnectedIds = new Set<string>();\n        for (const item of historyItems()) {\n          if (getConnectedHistoryElements(item).length === 0) {\n            disconnectedIds.add(item.id);\n          }\n        }\n        return disconnectedIds;\n      },\n      undefined,\n      {\n        equals: (prev, next) => {\n          if (prev.size !== next.size) return false;\n          for (const id of next) {\n            if (!prev.has(id)) return false;\n          }\n          return true;\n        },\n      },\n    );\n\n    const clearHoldTimer = () => {\n      if (holdState.timerId !== null) {\n        clearTimeout(holdState.timerId);\n        holdState.timerId = null;\n      }\n    };\n\n    const resetCopyConfirmation = () => {\n      holdState.copyWaiting = false;\n      holdState.holdTimerFired = false;\n      holdState.startTimestamp = null;\n    };\n\n    createEffect(() => {\n      if (store.current.state !== \"holding\") {\n        clearHoldTimer();\n        return;\n      }\n      holdState.startTimestamp = Date.now();\n      holdState.timerId = window.setTimeout(() => {\n        holdState.timerId = null;\n        if (holdState.copyWaiting) {\n          holdState.holdTimerFired = true;\n          return;\n        }\n        actions.activate();\n      }, store.keyHoldDuration);\n      onCleanup(clearHoldTimer);\n    });\n\n    createEffect(() => {\n      if (\n        store.current.state !== \"active\" ||\n        store.current.phase !== \"justDragged\"\n      )\n        return;\n      const timerId = setTimeout(() => {\n        actions.finishJustDragged();\n      }, FEEDBACK_DURATION_MS);\n      onCleanup(() => clearTimeout(timerId));\n    });\n\n    createEffect(() => {\n      if (store.current.state !== \"justCopied\") return;\n      const timerId = setTimeout(() => {\n        actions.finishJustCopied();\n      }, FEEDBACK_DURATION_MS);\n      onCleanup(() => clearTimeout(timerId));\n    });\n\n    createEffect(\n      on(isHoldingKeys, (currentlyHolding, previouslyHolding = false) => {\n        if (!previouslyHolding || currentlyHolding || !isActivated()) {\n          return;\n        }\n        if (pluginRegistry.store.options.activationMode !== \"hold\") {\n          actions.setWasActivatedByToggle(true);\n        }\n        pluginRegistry.hooks.onActivate();\n      }),\n    );\n\n    const preparePromptMode = (\n      element: Element,\n      positionX: number,\n      positionY: number,\n    ) => {\n      setCopyStartPosition(element, positionX, positionY);\n      actions.clearInputText();\n    };\n\n    const activatePromptMode = () => {\n      const element = store.frozenElement || targetElement();\n      if (element) {\n        actions.enterPromptMode(\n          { x: store.pointer.x, y: store.pointer.y },\n          element,\n        );\n      }\n    };\n\n    const setCopyStartPosition = (\n      element: Element,\n      positionX: number,\n      positionY: number,\n    ) => {\n      actions.setCopyStart({ x: positionX, y: positionY }, element);\n      return createElementBounds(element);\n    };\n\n    const detectionState = {\n      lastDetectionTimestamp: 0,\n      pendingDetectionScheduledAt: 0,\n      latestPointerX: 0,\n      latestPointerY: 0,\n    };\n    let dragPreviewDebounceTimerId: number | null = null;\n    const [debouncedDragPointer, setDebouncedDragPointer] = createSignal<{\n      x: number;\n      y: number;\n    } | null>(null);\n    const scheduleDragPreviewUpdate = (clientX: number, clientY: number) => {\n      if (dragPreviewDebounceTimerId !== null) {\n        clearTimeout(dragPreviewDebounceTimerId);\n      }\n      setDebouncedDragPointer(null);\n      dragPreviewDebounceTimerId = window.setTimeout(() => {\n        setDebouncedDragPointer({ x: clientX, y: clientY });\n        dragPreviewDebounceTimerId = null;\n      }, DRAG_PREVIEW_DEBOUNCE_MS);\n    };\n    let keydownSpamTimerId: number | null = null;\n    const holdState = {\n      timerId: null as number | null,\n      startTimestamp: null as number | null,\n      copyWaiting: false,\n      holdTimerFired: false,\n    };\n    let lastWindowFocusTimestamp = 0;\n    const copyFeedbackCooldown = {\n      isActive: false,\n      timerId: null as number | null,\n      start() {\n        this.isActive = true;\n        if (this.timerId !== null) {\n          window.clearTimeout(this.timerId);\n        }\n        this.timerId = window.setTimeout(() => {\n          this.isActive = false;\n          this.timerId = null;\n        }, FEEDBACK_DURATION_MS);\n      },\n      clear() {\n        if (this.timerId !== null) {\n          window.clearTimeout(this.timerId);\n          this.timerId = null;\n        }\n        this.isActive = false;\n      },\n    };\n    let actionCycleIdleTimeoutId: number | null = null;\n    let selectionSourceRequestVersion = 0;\n    let componentNameRequestVersion = 0;\n    let componentNameDebounceTimerId: number | null = null;\n    let keyboardSelectedElement: Element | null = null;\n    let isPendingContextMenuSelect = false;\n    const [\n      debouncedElementForComponentName,\n      setDebouncedElementForComponentName,\n    ] = createSignal<Element | null>(null);\n    const [resolvedComponentName, setResolvedComponentName] = createSignal<\n      string | undefined\n    >(undefined);\n    const [actionCycleItems, setActionCycleItems] = createSignal<\n      ActionCycleItem[]\n    >([]);\n    const [actionCycleActiveIndex, setActionCycleActiveIndex] = createSignal<\n      number | null\n    >(null);\n\n    const [arrowNavigationElements, setArrowNavigationElements] = createSignal<\n      Element[]\n    >([]);\n    const [arrowNavigationActiveIndex, setArrowNavigationActiveIndex] =\n      createSignal(0);\n\n    const arrowNavigator = createArrowNavigator(\n      isValidGrabbableElement,\n      createElementBounds,\n    );\n\n    const autoScroller = createAutoScroller(\n      () => store.pointer,\n      () => isDragging(),\n    );\n\n    const isRendererActive = createMemo(() => isActivated() && !isCopying());\n\n    const grabbedBoxTimeouts = new Map<string, number>();\n\n    const showTemporaryGrabbedBox = (\n      bounds: OverlayBounds,\n      element: Element,\n    ) => {\n      const boxId = generateId(\"grabbed\");\n      const createdAt = Date.now();\n      const newBox: GrabbedBox = { id: boxId, bounds, createdAt, element };\n\n      actions.addGrabbedBox(newBox);\n      pluginRegistry.hooks.onGrabbedBox(bounds, element);\n\n      const timeoutId = window.setTimeout(() => {\n        grabbedBoxTimeouts.delete(boxId);\n        actions.removeGrabbedBox(boxId);\n      }, FEEDBACK_DURATION_MS);\n      grabbedBoxTimeouts.set(boxId, timeoutId);\n    };\n\n    const notifyElementsSelected = async (\n      elements: Element[],\n    ): Promise<void> => {\n      const elementsPayload = await Promise.all(\n        elements.map(async (element) => {\n          const source = await resolveSource(element);\n          let componentName = source?.componentName ?? null;\n          const filePath = source?.filePath;\n          const lineNumber = source?.lineNumber ?? undefined;\n          const columnNumber = source?.columnNumber ?? undefined;\n\n          if (!componentName) {\n            componentName = getComponentDisplayName(element);\n          }\n\n          const textContent =\n            element instanceof HTMLElement\n              ? element.innerText?.slice(0, PREVIEW_TEXT_MAX_LENGTH)\n              : undefined;\n\n          return {\n            tagName: getTagName(element),\n            id: element.id || undefined,\n            className: element.getAttribute(\"class\") || undefined,\n            textContent,\n            componentName: componentName ?? undefined,\n            filePath,\n            lineNumber,\n            columnNumber,\n          };\n        }),\n      );\n\n      window.dispatchEvent(\n        new CustomEvent(\"react-grab:element-selected\", {\n          detail: {\n            elements: elementsPayload,\n          },\n        }),\n      );\n    };\n\n    const labelFadeTimeouts = new Map<string, number>();\n\n    const cancelLabelFade = (instanceId: string) => {\n      const existingTimeout = labelFadeTimeouts.get(instanceId);\n      if (existingTimeout !== undefined) {\n        window.clearTimeout(existingTimeout);\n        labelFadeTimeouts.delete(instanceId);\n      }\n    };\n\n    const cancelAllLabelFades = () => {\n      for (const timeoutId of labelFadeTimeouts.values()) {\n        window.clearTimeout(timeoutId);\n      }\n      labelFadeTimeouts.clear();\n    };\n\n    const scheduleLabelFade = (instanceId: string) => {\n      cancelLabelFade(instanceId);\n\n      const timeoutId = window.setTimeout(() => {\n        labelFadeTimeouts.delete(instanceId);\n        actions.updateLabelInstance(instanceId, \"fading\");\n        setTimeout(() => {\n          labelFadeTimeouts.delete(instanceId);\n          actions.removeLabelInstance(instanceId);\n        }, FADE_COMPLETE_BUFFER_MS);\n      }, FEEDBACK_DURATION_MS);\n\n      labelFadeTimeouts.set(instanceId, timeoutId);\n    };\n\n    const handleLabelInstanceHoverChange = (\n      instanceId: string,\n      isHovered: boolean,\n    ) => {\n      if (isHovered) {\n        cancelLabelFade(instanceId);\n      } else {\n        const instance = store.labelInstances.find(\n          (labelInstance) => labelInstance.id === instanceId,\n        );\n        if (instance && instance.status === \"copied\") {\n          scheduleLabelFade(instanceId);\n        }\n      }\n    };\n\n    const createLabelInstance = (\n      bounds: OverlayBounds,\n      tagName: string,\n      componentName: string | undefined,\n      status: SelectionLabelInstance[\"status\"],\n      options?: {\n        element?: Element;\n        mouseX?: number;\n        elements?: Element[];\n        boundsMultiple?: OverlayBounds[];\n        hideArrow?: boolean;\n      },\n    ): string => {\n      actions.clearLabelInstances();\n      cancelAllLabelFades();\n      const instanceId = generateId(\"label\");\n      const boundsCenterX = bounds.x + bounds.width / 2;\n      const boundsHalfWidth = bounds.width / 2;\n      const mouseX = options?.mouseX;\n      const mouseXOffset =\n        mouseX !== undefined ? mouseX - boundsCenterX : undefined;\n\n      const instance: SelectionLabelInstance = {\n        id: instanceId,\n        bounds,\n        boundsMultiple: options?.boundsMultiple,\n        tagName,\n        componentName,\n        status,\n        createdAt: Date.now(),\n        element: options?.element,\n        elements: options?.elements,\n        mouseX,\n        mouseXOffsetFromCenter: mouseXOffset,\n        mouseXOffsetRatio:\n          mouseXOffset !== undefined && boundsHalfWidth > 0\n            ? mouseXOffset / boundsHalfWidth\n            : undefined,\n        hideArrow: options?.hideArrow,\n      };\n      actions.addLabelInstance(instance);\n      return instanceId;\n    };\n\n    const clearAllLabels = () => {\n      cancelAllLabelFades();\n      actions.clearLabelInstances();\n    };\n\n    const updateLabelAfterCopy = (\n      labelInstanceId: string,\n      didSucceed: boolean,\n      errorMessage?: string,\n    ) => {\n      if (didSucceed) {\n        actions.updateLabelInstance(labelInstanceId, \"copied\");\n      } else {\n        actions.updateLabelInstance(\n          labelInstanceId,\n          \"error\",\n          errorMessage || \"Unknown error\",\n        );\n      }\n      scheduleLabelFade(labelInstanceId);\n    };\n\n    const executeCopyOperation = async (\n      clipboardOperation: () => Promise<void>,\n      labelInstanceId: string | null,\n      copiedElement?: Element,\n      shouldDeactivateAfter?: boolean,\n    ) => {\n      copyFeedbackCooldown.clear();\n      if (store.current.state !== \"copying\") {\n        actions.startCopy();\n      }\n\n      let didSucceed = false;\n      let errorMessage: string | undefined;\n\n      try {\n        await clipboardOperation();\n        didSucceed = true;\n      } catch (error) {\n        errorMessage = normalizeErrorMessage(error, \"Action failed\");\n      }\n\n      if (labelInstanceId) {\n        updateLabelAfterCopy(labelInstanceId, didSucceed, errorMessage);\n      }\n\n      if (store.current.state !== \"copying\") return;\n\n      if (didSucceed) {\n        actions.completeCopy(copiedElement);\n      }\n\n      if (shouldDeactivateAfter) {\n        deactivateRenderer();\n      } else if (didSucceed) {\n        actions.activate();\n        copyFeedbackCooldown.start();\n      } else {\n        actions.unfreeze();\n      }\n    };\n\n    const copyWithFallback = (\n      elements: Element[],\n      extraPrompt?: string,\n      resolvedComponentName?: string,\n    ) => {\n      const firstElement = elements[0];\n      const componentName =\n        resolvedComponentName ??\n        (firstElement ? getComponentDisplayName(firstElement) : null);\n      const tagName = firstElement ? getTagName(firstElement) : null;\n      const elementName = componentName ?? tagName ?? undefined;\n\n      return tryCopyWithFallback(\n        {\n          maxContextLines: pluginRegistry.store.options.maxContextLines,\n          getContent: pluginRegistry.store.options.getContent,\n          componentName: elementName,\n        },\n        {\n          onBeforeCopy: pluginRegistry.hooks.onBeforeCopy,\n          transformSnippet: pluginRegistry.hooks.transformSnippet,\n          transformCopyContent: pluginRegistry.hooks.transformCopyContent,\n          onAfterCopy: pluginRegistry.hooks.onAfterCopy,\n          onCopySuccess: (copiedElements: Element[], content: string) => {\n            pluginRegistry.hooks.onCopySuccess(copiedElements, content);\n\n            const hasCopiedElements = copiedElements.length > 0;\n            const isComment = Boolean(extraPrompt);\n\n            if (hasCopiedElements) {\n              const currentItems = historyItems();\n              for (const [\n                existingItemId,\n                mappedElements,\n              ] of historyElementMap.entries()) {\n                const isSameSelection =\n                  mappedElements.length === copiedElements.length &&\n                  mappedElements.every(\n                    (element, index) => element === copiedElements[index],\n                  );\n                if (!isSameSelection) continue;\n                const existingItem = currentItems.find(\n                  (item) => item.id === existingItemId,\n                );\n                if (!existingItem) continue;\n\n                const shouldDedup = isComment\n                  ? existingItem.isComment &&\n                    existingItem.commentText === extraPrompt\n                  : !existingItem.isComment;\n\n                if (shouldDedup) {\n                  removeHistoryItem(existingItemId);\n                  historyElementMap.delete(existingItemId);\n                  break;\n                }\n              }\n            }\n\n            const elementSelectors = copiedElements.map((element, index) =>\n              createElementSelector(element, index === 0),\n            );\n\n            const updatedHistoryItems = addHistoryItem({\n              content,\n              elementName: elementName ?? \"element\",\n              tagName: tagName ?? \"div\",\n              componentName: componentName ?? undefined,\n              elementsCount: copiedElements.length,\n              previewBounds: copiedElements.map((element) =>\n                createElementBounds(element),\n              ),\n              elementSelectors,\n              isComment,\n              commentText: extraPrompt ?? undefined,\n              timestamp: Date.now(),\n            });\n            setHistoryItems(updatedHistoryItems);\n            setHasUnreadHistoryItems(true);\n            setClockFlashTrigger((previous) => previous + 1);\n            const newestHistoryItem = updatedHistoryItems[0];\n            if (newestHistoryItem && hasCopiedElements) {\n              historyElementMap.set(newestHistoryItem.id, [...copiedElements]);\n            }\n\n            const currentItemIds = new Set(\n              updatedHistoryItems.map((item) => item.id),\n            );\n            for (const mapItemId of historyElementMap.keys()) {\n              if (!currentItemIds.has(mapItemId)) {\n                historyElementMap.delete(mapItemId);\n              }\n            }\n          },\n          onCopyError: pluginRegistry.hooks.onCopyError,\n        },\n        elements,\n        extraPrompt,\n      );\n    };\n\n    const copyElementsToClipboard = async (\n      targetElements: Element[],\n      extraPrompt?: string,\n      resolvedComponentName?: string,\n    ): Promise<void> => {\n      if (targetElements.length === 0) return;\n\n      const unhandledElements: Element[] = [];\n      const pendingResults: Promise<boolean>[] = [];\n      for (const element of targetElements) {\n        const { wasIntercepted, pendingResult } =\n          pluginRegistry.hooks.onElementSelect(element);\n        if (!wasIntercepted) {\n          unhandledElements.push(element);\n        }\n        if (pendingResult) {\n          pendingResults.push(pendingResult);\n        }\n        if (pluginRegistry.store.theme.grabbedBoxes.enabled) {\n          showTemporaryGrabbedBox(createElementBounds(element), element);\n        }\n      }\n      await waitUntilNextFrame();\n      if (unhandledElements.length > 0) {\n        await copyWithFallback(\n          unhandledElements,\n          extraPrompt,\n          resolvedComponentName,\n        );\n      } else if (pendingResults.length > 0) {\n        const results = await Promise.all(pendingResults);\n        if (!results.every(Boolean)) {\n          throw new Error(\"Failed to copy\");\n        }\n      }\n      void notifyElementsSelected(targetElements);\n    };\n\n    const performCopyWithLabel = (options: CopyWithLabelOptions) => {\n      const {\n        element,\n        cursorX,\n        selectedElements,\n        extraPrompt,\n        shouldDeactivateAfter,\n        onComplete,\n        dragRect: passedDragRect,\n      } = options;\n\n      const allTargetElements = selectedElements ?? [element];\n      const dragRect = passedDragRect ?? store.frozenDragRect;\n      const isMultiSelect = allTargetElements.length > 1;\n\n      const selectionBounds =\n        dragRect && isMultiSelect\n          ? createBoundsFromDragRect(dragRect)\n          : createFlatOverlayBounds(createElementBounds(element));\n\n      const labelCursorX = isMultiSelect\n        ? selectionBounds.x + selectionBounds.width / 2\n        : cursorX;\n\n      const tagName = getTagName(element);\n      copyFeedbackCooldown.clear();\n      actions.startCopy();\n\n      const labelInstanceId = tagName\n        ? createLabelInstance(selectionBounds, tagName, undefined, \"copying\", {\n            element,\n            mouseX: labelCursorX,\n            elements: selectedElements,\n          })\n        : null;\n\n      void getNearestComponentName(element)\n        .then(async (componentName) => {\n          await executeCopyOperation(\n            () =>\n              copyElementsToClipboard(\n                allTargetElements,\n                extraPrompt,\n                componentName ?? undefined,\n              ),\n            labelInstanceId,\n            element,\n            shouldDeactivateAfter,\n          );\n          onComplete?.();\n        })\n        .catch((error) => {\n          logRecoverableError(\"Copy operation failed\", error);\n          if (labelInstanceId) {\n            updateLabelAfterCopy(\n              labelInstanceId,\n              false,\n              normalizeErrorMessage(error, \"Action failed\"),\n            );\n          }\n          if (store.current.state === \"copying\") {\n            actions.unfreeze();\n          }\n        });\n    };\n\n    const targetElement = createMemo(() => {\n      void store.viewportVersion;\n      if (!isRendererActive() || isDragging()) return null;\n      const element = store.detectedElement;\n      if (!isElementConnected(element)) return null;\n      return element;\n    });\n\n    const effectiveElement = createMemo(\n      () => store.frozenElement || (isFrozenPhase() ? null : targetElement()),\n    );\n\n    createEffect(() => {\n      const element = store.detectedElement;\n      if (!element) return;\n\n      const intervalId = setInterval(() => {\n        if (!isElementConnected(element)) {\n          actions.setDetectedElement(null);\n        }\n      }, BOUNDS_RECALC_INTERVAL_MS);\n\n      onCleanup(() => clearInterval(intervalId));\n    });\n\n    createEffect(\n      on(effectiveElement, (element) => {\n        if (componentNameDebounceTimerId !== null) {\n          clearTimeout(componentNameDebounceTimerId);\n          componentNameDebounceTimerId = null;\n        }\n\n        if (!element) {\n          setDebouncedElementForComponentName(null);\n          return;\n        }\n\n        componentNameDebounceTimerId = window.setTimeout(() => {\n          componentNameDebounceTimerId = null;\n          setDebouncedElementForComponentName(element);\n        }, COMPONENT_NAME_DEBOUNCE_MS);\n      }),\n    );\n\n    onCleanup(() => {\n      if (componentNameDebounceTimerId !== null) {\n        clearTimeout(componentNameDebounceTimerId);\n        componentNameDebounceTimerId = null;\n      }\n    });\n\n    createEffect(() => {\n      const elements = store.frozenElements;\n      const cleanup = freezeAnimations(elements);\n      onCleanup(cleanup);\n    });\n\n    createEffect(\n      on(isActivated, (activated) => {\n        if (!activated) return;\n        if (!pluginRegistry.store.options.freezeReactUpdates) return;\n        const unfreezeUpdates = freezeUpdates();\n        onCleanup(unfreezeUpdates);\n      }),\n    );\n\n    // HACK: In touch mode during drag, effectiveElement() is null so we use detectedElement\n    const getSelectionElement = (): Element | undefined => {\n      if (store.isTouchMode && isDragging()) {\n        const detected = store.detectedElement;\n        if (!detected || isRootElement(detected)) return undefined;\n        return detected;\n      }\n      const element = effectiveElement();\n      if (!element || isRootElement(element)) return undefined;\n      return element;\n    };\n\n    const selectionElement = createMemo(() => getSelectionElement());\n\n    const isSelectionElementVisible = (): boolean => {\n      const element = selectionElement();\n      if (!element) return false;\n      if (store.isTouchMode && isDragging()) {\n        return isRendererActive();\n      }\n      return isRendererActive() && !isDragging();\n    };\n\n    const frozenElementsBounds = createMemo((): OverlayBounds[] => {\n      void store.viewportVersion;\n\n      const frozenElements = store.frozenElements;\n      if (frozenElements.length === 0) return [];\n\n      const dragRect = store.frozenDragRect;\n      if (dragRect && frozenElements.length > 1) {\n        return [createBoundsFromDragRect(dragRect)];\n      }\n\n      return frozenElements\n        .filter((element): element is Element => element !== null)\n        .map((element) => createElementBounds(element));\n    });\n\n    const selectionBounds = createMemo((): OverlayBounds | undefined => {\n      void store.viewportVersion;\n\n      const frozenElements = store.frozenElements;\n      if (frozenElements.length > 0) {\n        const frozenBounds = frozenElementsBounds();\n        if (frozenElements.length === 1) {\n          const firstBounds = frozenBounds[0];\n          if (firstBounds) return firstBounds;\n        }\n        const dragRect = store.frozenDragRect;\n        if (dragRect) {\n          const dragBounds = frozenBounds[0];\n          return dragBounds ?? createBoundsFromDragRect(dragRect);\n        }\n        return createFlatOverlayBounds(combineBounds(frozenBounds));\n      }\n\n      const element = selectionElement();\n      if (!element) return undefined;\n      return createElementBounds(element);\n    });\n\n    const calculateDragDistance = (endX: number, endY: number) => {\n      const endPageX = endX + window.scrollX;\n      const endPageY = endY + window.scrollY;\n\n      return {\n        x: Math.abs(endPageX - store.dragStart.x),\n        y: Math.abs(endPageY - store.dragStart.y),\n      };\n    };\n\n    const isDraggingBeyondThreshold = createMemo(() => {\n      if (!isDragging()) return false;\n\n      const dragDistance = calculateDragDistance(\n        store.pointer.x,\n        store.pointer.y,\n      );\n\n      return (\n        dragDistance.x > DRAG_THRESHOLD_PX || dragDistance.y > DRAG_THRESHOLD_PX\n      );\n    });\n\n    const calculateDragRectangle = (endX: number, endY: number) => {\n      const endPageX = endX + window.scrollX;\n      const endPageY = endY + window.scrollY;\n\n      const dragPageX = Math.min(store.dragStart.x, endPageX);\n      const dragPageY = Math.min(store.dragStart.y, endPageY);\n      const dragWidth = Math.abs(endPageX - store.dragStart.x);\n      const dragHeight = Math.abs(endPageY - store.dragStart.y);\n\n      return {\n        x: dragPageX - window.scrollX,\n        y: dragPageY - window.scrollY,\n        width: dragWidth,\n        height: dragHeight,\n      };\n    };\n\n    const dragBounds = createMemo((): OverlayBounds | undefined => {\n      void store.viewportVersion;\n\n      if (!isDraggingBeyondThreshold()) return undefined;\n\n      const drag = calculateDragRectangle(store.pointer.x, store.pointer.y);\n\n      return {\n        borderRadius: \"0px\",\n        height: drag.height,\n        transform: \"none\",\n        width: drag.width,\n        x: drag.x,\n        y: drag.y,\n      };\n    });\n\n    const dragPreviewBounds = createMemo((): OverlayBounds[] => {\n      void store.viewportVersion;\n\n      if (!isDraggingBeyondThreshold()) return [];\n\n      const pointer = debouncedDragPointer();\n      if (!pointer) return [];\n\n      const drag = calculateDragRectangle(pointer.x, pointer.y);\n      const elements = getElementsInDrag(drag, isValidGrabbableElement);\n      const previewElements =\n        elements.length > 0\n          ? elements\n          : getElementsInDrag(drag, isValidGrabbableElement, false);\n\n      return previewElements.map((element) => createElementBounds(element));\n    });\n\n    const selectionBoundsMultiple = createMemo((): OverlayBounds[] => {\n      const previewBounds = dragPreviewBounds();\n      if (previewBounds.length > 0) {\n        return previewBounds;\n      }\n      return frozenElementsBounds();\n    });\n\n    const cursorPosition = createMemo(() => {\n      if (isCopying() || isPromptMode()) {\n        void store.viewportVersion;\n        const element = store.frozenElement || targetElement();\n        if (element) {\n          const bounds = createElementBounds(element);\n          return {\n            x: getBoundsCenter(bounds).x + store.copyOffsetFromCenterX,\n            y: store.copyStart.y,\n          };\n        }\n        return {\n          x: store.copyStart.x,\n          y: store.copyStart.y,\n        };\n      }\n      return {\n        x: store.pointer.x,\n        y: store.pointer.y,\n      };\n    });\n\n    createEffect(\n      on(\n        () => [targetElement(), store.lastGrabbedElement] as const,\n        ([currentElement, lastElement]) => {\n          if (lastElement && currentElement && lastElement !== currentElement) {\n            actions.setLastGrabbed(null);\n          }\n          if (currentElement) {\n            pluginRegistry.hooks.onElementHover(currentElement);\n          }\n        },\n      ),\n    );\n\n    createEffect(\n      on(\n        () => targetElement(),\n        (element) => {\n          const currentVersion = ++selectionSourceRequestVersion;\n\n          const clearSource = () => {\n            if (selectionSourceRequestVersion === currentVersion) {\n              actions.setSelectionSource(null, null);\n            }\n          };\n\n          if (!element) {\n            clearSource();\n            return;\n          }\n\n          resolveSource(element)\n            .then((source) => {\n              if (selectionSourceRequestVersion !== currentVersion) return;\n              if (!source) {\n                clearSource();\n                return;\n              }\n              actions.setSelectionSource(source.filePath, source.lineNumber);\n            })\n            .catch(() => {\n              if (selectionSourceRequestVersion === currentVersion) {\n                actions.setSelectionSource(null, null);\n              }\n            });\n        },\n      ),\n    );\n\n    createEffect(\n      on(\n        () => store.viewportVersion,\n        () => agentManager._internal.updateBoundsOnViewportChange(),\n      ),\n    );\n\n    const publicGrabbedBoxes = createMemo(() =>\n      store.grabbedBoxes.map((box) => ({\n        id: box.id,\n        bounds: box.bounds,\n        createdAt: box.createdAt,\n      })),\n    );\n\n    const publicLabelInstances = createMemo(() =>\n      store.labelInstances.map((instance) => ({\n        id: instance.id,\n        status: instance.status,\n        tagName: instance.tagName,\n        componentName: instance.componentName,\n        createdAt: instance.createdAt,\n      })),\n    );\n\n    const derivedStateForHook = createMemo(() => {\n      const active = isActivated();\n      const dragging = isDragging();\n      const copying = isCopying();\n      const inputMode = isPromptMode();\n      const target = targetElement();\n      const drag = dragBounds();\n      const themeEnabled = pluginRegistry.store.theme.enabled;\n      const selectionBoxEnabled =\n        pluginRegistry.store.theme.selectionBox.enabled;\n      const dragBoxEnabled = pluginRegistry.store.theme.dragBox.enabled;\n      const draggingBeyondThreshold = isDraggingBeyondThreshold();\n      const effectiveTarget = effectiveElement();\n      const justCopied = didJustCopy();\n\n      const isSelectionBoxVisible = Boolean(\n        themeEnabled &&\n        selectionBoxEnabled &&\n        active &&\n        !copying &&\n        !justCopied &&\n        !dragging &&\n        effectiveTarget != null,\n      );\n      const isDragBoxVisible = Boolean(\n        themeEnabled &&\n        dragBoxEnabled &&\n        active &&\n        !copying &&\n        draggingBeyondThreshold,\n      );\n\n      return {\n        isActive: active,\n        isDragging: dragging,\n        isCopying: copying,\n        isPromptMode: inputMode,\n        isSelectionBoxVisible,\n        isDragBoxVisible,\n        targetElement: target,\n        dragBounds: drag\n          ? { x: drag.x, y: drag.y, width: drag.width, height: drag.height }\n          : null,\n        grabbedBoxes: [...publicGrabbedBoxes()],\n        labelInstances: [...publicLabelInstances()],\n        selectionFilePath: store.selectionFilePath,\n        toolbarState: currentToolbarState(),\n      };\n    });\n\n    createEffect(\n      on(derivedStateForHook, (state) => {\n        pluginRegistry.hooks.onStateChange(state);\n      }),\n    );\n\n    createEffect(\n      on(\n        () =>\n          [\n            isPromptMode(),\n            store.pointer.x,\n            store.pointer.y,\n            targetElement(),\n          ] as const,\n        ([inputMode, x, y, target]) => {\n          pluginRegistry.hooks.onPromptModeChange(inputMode, {\n            x,\n            y,\n            targetElement: target,\n          });\n        },\n      ),\n    );\n\n    createEffect(\n      on(\n        () => [selectionVisible(), selectionBounds(), targetElement()] as const,\n        ([visible, bounds, element]) => {\n          pluginRegistry.hooks.onSelectionBox(\n            Boolean(visible),\n            bounds ?? null,\n            element,\n          );\n        },\n      ),\n    );\n\n    createEffect(\n      on(\n        () => [dragVisible(), dragBounds()] as const,\n        ([visible, bounds]) => {\n          pluginRegistry.hooks.onDragBox(Boolean(visible), bounds ?? null);\n        },\n      ),\n    );\n\n    createEffect(\n      on(\n        () =>\n          [\n            labelVisible(),\n            labelVariant(),\n            cursorPosition(),\n            targetElement(),\n            store.selectionFilePath,\n            store.selectionLineNumber,\n          ] as const,\n        ([visible, variant, position, element, filePath, lineNumber]) => {\n          pluginRegistry.hooks.onElementLabel(Boolean(visible), variant, {\n            x: position.x,\n            y: position.y,\n            content: \"\",\n            element: element ?? undefined,\n            tagName: element ? getTagName(element) || undefined : undefined,\n            filePath: filePath ?? undefined,\n            lineNumber: lineNumber ?? undefined,\n          });\n        },\n      ),\n    );\n\n    let cursorStyleElement: HTMLStyleElement | null = null;\n\n    const setCursorOverride = (cursor: string | null) => {\n      if (cursor) {\n        if (!cursorStyleElement) {\n          cursorStyleElement = document.createElement(\"style\");\n          cursorStyleElement.setAttribute(\"data-react-grab-cursor\", \"\");\n          document.head.appendChild(cursorStyleElement);\n        }\n        cursorStyleElement.textContent = `* { cursor: ${cursor} !important; }`;\n      } else if (cursorStyleElement) {\n        cursorStyleElement.remove();\n        cursorStyleElement = null;\n      }\n    };\n\n    createEffect(\n      on(\n        () => [isActivated(), isCopying(), isPromptMode()] as const,\n        ([activated, copying, promptMode]) => {\n          if (copying) {\n            setCursorOverride(\"progress\");\n          } else if (activated && !promptMode) {\n            setCursorOverride(\"crosshair\");\n          } else {\n            setCursorOverride(null);\n          }\n        },\n      ),\n    );\n\n    const activateRenderer = () => {\n      const wasInHoldingState = isHoldingKeys();\n      actions.activate();\n      // HACK: Only call onActivate if we weren't in holding state.\n      // When coming from holding state, the reactive effect (previouslyHoldingKeys transition)\n      // will handle calling onActivate to avoid duplicate invocations.\n      if (!wasInHoldingState) {\n        pluginRegistry.hooks.onActivate();\n      }\n    };\n\n    const deactivateRenderer = () => {\n      const wasDragging = isDragging();\n      const previousFocused = store.previouslyFocusedElement;\n      actions.deactivate();\n      clearArrowNavigation();\n      keyboardSelectedElement = null;\n      isPendingContextMenuSelect = false;\n      if (wasDragging) {\n        document.body.style.userSelect = \"\";\n      }\n      if (keydownSpamTimerId) window.clearTimeout(keydownSpamTimerId);\n      autoScroller.stop();\n      if (\n        previousFocused instanceof HTMLElement &&\n        isElementConnected(previousFocused)\n      ) {\n        previousFocused.focus();\n      }\n      pluginRegistry.hooks.onDeactivate();\n    };\n\n    const forceDeactivateAll = () => {\n      if (isHoldingKeys()) {\n        actions.releaseHold();\n      }\n      if (isActivated()) {\n        deactivateRenderer();\n      }\n      copyFeedbackCooldown.clear();\n    };\n\n    const toggleActivate = () => {\n      actions.setWasActivatedByToggle(true);\n      activateRenderer();\n    };\n\n    const restoreInputFromSession = (\n      session: AgentSession,\n      elements: Element[],\n      agent?: AgentOptions,\n    ) => {\n      const element = elements[0];\n      if (isElementConnected(element)) {\n        const rect = element.getBoundingClientRect();\n        const centerY = rect.top + rect.height / 2;\n\n        actions.setPointer({ x: session.position.x, y: centerY });\n        actions.setFrozenElements(elements);\n        actions.setInputText(session.context.prompt);\n        actions.setWasActivatedByToggle(true);\n\n        if (agent) {\n          actions.setSelectedAgent(agent);\n        }\n\n        if (!isActivated()) {\n          activateRenderer();\n        }\n      }\n    };\n\n    const wrapAgentWithCallbacks = (agent: AgentOptions): AgentOptions => {\n      return {\n        ...agent,\n        onAbort: (session: AgentSession, elements: Element[]) => {\n          agent.onAbort?.(session, elements);\n          restoreInputFromSession(session, elements, agent);\n        },\n        onUndo: (session: AgentSession, elements: Element[]) => {\n          agent.onUndo?.(session, elements);\n          restoreInputFromSession(session, elements, agent);\n        },\n      };\n    };\n\n    const getAgentOptionsWithCallbacks = () => {\n      const agent = getAgentFromActions();\n      if (!agent) return undefined;\n      return wrapAgentWithCallbacks(agent);\n    };\n\n    const agentManager = createAgentManager(getAgentOptionsWithCallbacks(), {\n      transformAgentContext: pluginRegistry.hooks.transformAgentContext,\n    });\n\n    const handleInputSubmit = () => {\n      actions.clearLastCopied();\n      const frozenElements = [...store.frozenElements];\n      const element = store.frozenElement || targetElement();\n      const prompt = isPromptMode() ? store.inputText.trim() : \"\";\n\n      if (!element) {\n        deactivateRenderer();\n        return;\n      }\n\n      const elements = frozenElements.length > 0 ? frozenElements : [element];\n\n      const currentSelectionBounds = elements.map((selectedElement) =>\n        createElementBounds(selectedElement),\n      );\n      const firstBounds = currentSelectionBounds[0];\n      const { x: currentX, y: currentY } = getBoundsCenter(firstBounds);\n      const labelPositionX = currentX + store.copyOffsetFromCenterX;\n\n      if ((store.selectedAgent || store.hasAgentProvider) && prompt) {\n        const currentReplySessionId = store.replySessionId;\n        const selectedAgent = store.selectedAgent;\n\n        deactivateRenderer();\n\n        actions.clearReplySessionId();\n        actions.setSelectedAgent(null);\n\n        void agentManager.session.start({\n          elements,\n          prompt,\n          position: { x: labelPositionX, y: currentY },\n          selectionBounds: currentSelectionBounds,\n          sessionId: currentReplySessionId ?? undefined,\n          agent: selectedAgent\n            ? wrapAgentWithCallbacks(selectedAgent)\n            : undefined,\n        });\n\n        return;\n      }\n\n      actions.setPointer({ x: currentX, y: currentY });\n      actions.exitPromptMode();\n      actions.clearInputText();\n      actions.clearReplySessionId();\n\n      performCopyWithLabel({\n        element,\n        cursorX: labelPositionX,\n        selectedElements: elements,\n        extraPrompt: prompt || undefined,\n        onComplete: deactivateRenderer,\n      });\n    };\n\n    const handleInputCancel = () => {\n      actions.clearLastCopied();\n      if (!isPromptMode()) return;\n\n      if (isPendingDismiss()) {\n        actions.clearInputText();\n        actions.clearReplySessionId();\n        deactivateRenderer();\n        return;\n      }\n\n      actions.setPendingDismiss(true);\n      setSelectionLabelShakeCount((count) => count + 1);\n    };\n\n    const handleConfirmDismiss = () => {\n      actions.clearInputText();\n      actions.clearReplySessionId();\n      deactivateRenderer();\n    };\n\n    const handleCancelDismiss = () => {\n      actions.setPendingDismiss(false);\n    };\n\n    const handleAgentAbort = (sessionId: string, confirmed: boolean) => {\n      actions.setPendingAbortSessionId(null);\n      if (confirmed) {\n        agentManager.session.abort(sessionId);\n      }\n    };\n\n    const handleToggleExpand = () => {\n      const element = store.frozenElement || targetElement();\n      if (element) {\n        preparePromptMode(element, store.pointer.x, store.pointer.y);\n      }\n      activatePromptMode();\n    };\n\n    const handleFollowUpSubmit = (sessionId: string, prompt: string) => {\n      const session = agentManager.sessions().get(sessionId);\n      const elements = agentManager.session.getElements(sessionId);\n      const sessionBounds = session?.selectionBounds ?? [];\n      const firstBounds = sessionBounds[0];\n      if (session && elements.length > 0 && firstBounds) {\n        const positionX = session.position.x;\n        const followUpSessionId = session.context.sessionId ?? sessionId;\n\n        agentManager.session.dismiss(sessionId);\n\n        void agentManager.session.start({\n          elements,\n          prompt,\n          position: {\n            x: positionX,\n            y: firstBounds.y + firstBounds.height / 2,\n          },\n          selectionBounds: sessionBounds,\n          sessionId: followUpSessionId,\n        });\n      }\n    };\n\n    const handleAcknowledgeError = (sessionId: string) => {\n      const prompt = agentManager.session.acknowledgeError(sessionId);\n      if (prompt) {\n        actions.setInputText(prompt);\n      }\n    };\n\n    const handleToggleActive = () => {\n      if (isActivated()) {\n        deactivateRenderer();\n      } else if (isEnabled()) {\n        isPendingContextMenuSelect = true;\n        toggleActivate();\n      }\n    };\n\n    const enterCommentModeForElement = (\n      element: Element,\n      positionX: number,\n      positionY: number,\n    ) => {\n      actions.setPendingCommentMode(false);\n      actions.clearInputText();\n      actions.enterPromptMode({ x: positionX, y: positionY }, element);\n    };\n\n    const openContextMenu = (element: Element, position: Position) => {\n      actions.showContextMenu(position, element);\n      clearArrowNavigation();\n      dismissAllPopups();\n      pluginRegistry.hooks.onContextMenu(element, position);\n    };\n\n    const handleComment = () => {\n      if (!isEnabled()) return;\n\n      const isAlreadyInCommentMode = isActivated() && isCommentMode();\n      if (isAlreadyInCommentMode) {\n        deactivateRenderer();\n        return;\n      }\n\n      actions.setPendingCommentMode(true);\n      if (!isActivated()) {\n        toggleActivate();\n      }\n    };\n\n    const handleToggleEnabled = () => {\n      const newEnabled = !isEnabled();\n      setIsEnabled(newEnabled);\n      const currentState = loadToolbarState();\n      const newState = {\n        edge: currentState?.edge ?? \"bottom\",\n        ratio: currentState?.ratio ?? TOOLBAR_DEFAULT_POSITION_RATIO,\n        collapsed: currentState?.collapsed ?? false,\n        enabled: newEnabled,\n      };\n      saveToolbarState(newState);\n      setCurrentToolbarState(newState);\n      toolbarStateChangeCallbacks.forEach((callback) => callback(newState));\n      if (!newEnabled) {\n        forceDeactivateAll();\n        dismissAllPopups();\n      }\n    };\n\n    const handlePointerMove = (clientX: number, clientY: number) => {\n      if (\n        !isEnabled() ||\n        isPromptMode() ||\n        isFrozenPhase() ||\n        store.contextMenuPosition !== null\n      )\n        return;\n\n      actions.setPointer({ x: clientX, y: clientY });\n\n      detectionState.latestPointerX = clientX;\n      detectionState.latestPointerY = clientY;\n\n      const now = performance.now();\n      const isDetectionPending =\n        detectionState.pendingDetectionScheduledAt > 0 &&\n        now - detectionState.pendingDetectionScheduledAt <\n          PENDING_DETECTION_STALENESS_MS;\n      if (\n        now - detectionState.lastDetectionTimestamp >=\n          ELEMENT_DETECTION_THROTTLE_MS &&\n        !isDetectionPending\n      ) {\n        detectionState.lastDetectionTimestamp = now;\n        detectionState.pendingDetectionScheduledAt = now;\n        onIdle(() => {\n          const candidate = getElementAtPosition(\n            detectionState.latestPointerX,\n            detectionState.latestPointerY,\n          );\n          if (candidate !== store.detectedElement) {\n            actions.setDetectedElement(candidate);\n          }\n          detectionState.pendingDetectionScheduledAt = 0;\n        });\n      }\n\n      if (isDragging()) {\n        scheduleDragPreviewUpdate(clientX, clientY);\n\n        const direction = getAutoScrollDirection(clientX, clientY);\n        const isNearEdge =\n          direction.top ||\n          direction.bottom ||\n          direction.left ||\n          direction.right;\n\n        if (isNearEdge && !autoScroller.isActive()) {\n          autoScroller.start();\n        } else if (!isNearEdge && autoScroller.isActive()) {\n          autoScroller.stop();\n        }\n      }\n    };\n\n    const handlePointerDown = (clientX: number, clientY: number) => {\n      if (!isRendererActive() || isCopying()) return false;\n\n      actions.startDrag({ x: clientX, y: clientY });\n      actions.setPointer({ x: clientX, y: clientY });\n      document.body.style.userSelect = \"none\";\n\n      scheduleDragPreviewUpdate(clientX, clientY);\n\n      pluginRegistry.hooks.onDragStart(\n        clientX + window.scrollX,\n        clientY + window.scrollY,\n      );\n\n      return true;\n    };\n\n    const handleDragSelection = (\n      dragSelectionRect: ReturnType<typeof calculateDragRectangle>,\n      hasModifierKeyHeld: boolean,\n    ) => {\n      const elements = getElementsInDrag(\n        dragSelectionRect,\n        isValidGrabbableElement,\n      );\n      const selectedElements =\n        elements.length > 0\n          ? elements\n          : getElementsInDrag(\n              dragSelectionRect,\n              isValidGrabbableElement,\n              false,\n            );\n\n      if (selectedElements.length === 0) return;\n\n      freezeAllAnimations(selectedElements);\n\n      pluginRegistry.hooks.onDragEnd(selectedElements, dragSelectionRect);\n      const firstElement = selectedElements[0];\n      const center = getElementCenter(firstElement);\n\n      actions.setPointer(center);\n      actions.setFrozenElements(selectedElements);\n      const dragRect = createPageRectFromBounds(dragSelectionRect);\n      actions.setFrozenDragRect(dragRect);\n      actions.freeze();\n      actions.setLastGrabbed(firstElement);\n\n      if (store.pendingCommentMode) {\n        enterCommentModeForElement(firstElement, center.x, center.y);\n        return;\n      }\n\n      if (isPendingContextMenuSelect) {\n        isPendingContextMenuSelect = false;\n        openContextMenu(firstElement, center);\n        return;\n      }\n\n      const shouldDeactivateAfter =\n        store.wasActivatedByToggle && !hasModifierKeyHeld;\n\n      performCopyWithLabel({\n        element: firstElement,\n        cursorX: center.x,\n        selectedElements,\n        shouldDeactivateAfter,\n        dragRect,\n      });\n    };\n\n    const handleSingleClick = (\n      clientX: number,\n      clientY: number,\n      hasModifierKeyHeld: boolean,\n    ) => {\n      const validFrozenElement = isElementConnected(store.frozenElement)\n        ? store.frozenElement\n        : null;\n\n      const validKeyboardSelectedElement = isElementConnected(\n        keyboardSelectedElement,\n      )\n        ? keyboardSelectedElement\n        : null;\n\n      const element =\n        validFrozenElement ??\n        validKeyboardSelectedElement ??\n        getElementAtPosition(clientX, clientY) ??\n        (isElementConnected(store.detectedElement)\n          ? store.detectedElement\n          : null);\n      if (!element) return;\n\n      const didSelectViaKeyboard =\n        !validFrozenElement && validKeyboardSelectedElement === element;\n\n      let positionX: number;\n      let positionY: number;\n\n      if (validFrozenElement) {\n        positionX = store.pointer.x;\n        positionY = store.pointer.y;\n      } else if (didSelectViaKeyboard) {\n        const elementCenter = getElementCenter(element);\n        positionX = elementCenter.x;\n        positionY = elementCenter.y;\n      } else {\n        positionX = clientX;\n        positionY = clientY;\n      }\n\n      keyboardSelectedElement = null;\n\n      if (store.pendingCommentMode) {\n        enterCommentModeForElement(element, positionX, positionY);\n        return;\n      }\n\n      if (isPendingContextMenuSelect) {\n        isPendingContextMenuSelect = false;\n        const { wasIntercepted } =\n          pluginRegistry.hooks.onElementSelect(element);\n        if (wasIntercepted) return;\n\n        freezeAllAnimations([element]);\n        actions.setFrozenElement(element);\n        const position = { x: positionX, y: positionY };\n        actions.setPointer(position);\n        actions.freeze();\n        openContextMenu(element, position);\n        return;\n      }\n\n      const shouldDeactivateAfter =\n        store.wasActivatedByToggle && !hasModifierKeyHeld;\n\n      actions.setLastGrabbed(element);\n\n      performCopyWithLabel({\n        element,\n        cursorX: positionX,\n        shouldDeactivateAfter,\n      });\n    };\n\n    const cancelActiveDrag = () => {\n      if (!isDragging()) return;\n      actions.cancelDrag();\n      autoScroller.stop();\n      document.body.style.userSelect = \"\";\n    };\n\n    const handlePointerUp = (\n      clientX: number,\n      clientY: number,\n      hasModifierKeyHeld: boolean,\n    ) => {\n      if (!isDragging()) return;\n\n      if (dragPreviewDebounceTimerId !== null) {\n        clearTimeout(dragPreviewDebounceTimerId);\n        dragPreviewDebounceTimerId = null;\n      }\n      setDebouncedDragPointer(null);\n\n      const dragDistance = calculateDragDistance(clientX, clientY);\n      const wasDragGesture =\n        dragDistance.x > DRAG_THRESHOLD_PX ||\n        dragDistance.y > DRAG_THRESHOLD_PX;\n\n      // HACK: Calculate drag rectangle BEFORE ending drag, because endDrag resets dragStart\n      const dragSelectionRect = wasDragGesture\n        ? calculateDragRectangle(clientX, clientY)\n        : null;\n\n      if (wasDragGesture) {\n        actions.endDrag();\n      } else {\n        actions.cancelDrag();\n      }\n      autoScroller.stop();\n      document.body.style.userSelect = \"\";\n\n      if (dragSelectionRect) {\n        handleDragSelection(dragSelectionRect, hasModifierKeyHeld);\n      } else {\n        handleSingleClick(clientX, clientY, hasModifierKeyHeld);\n      }\n    };\n\n    const eventListenerManager = createEventListenerManager();\n\n    const keyboardClaimer = setupKeyboardEventClaimer();\n\n    const blockEnterIfNeeded = (event: KeyboardEvent) => {\n      let originalKey: string;\n      try {\n        originalKey = keyboardClaimer.originalKeyDescriptor?.get\n          ? keyboardClaimer.originalKeyDescriptor.get.call(event)\n          : event.key;\n      } catch {\n        return false;\n      }\n      const isEnterKey = originalKey === \"Enter\" || isEnterCode(event.code);\n      const isOverlayActive = isActivated() || isHoldingKeys();\n      const shouldBlockEnter =\n        isEnterKey &&\n        isOverlayActive &&\n        !isPromptMode() &&\n        !store.wasActivatedByToggle &&\n        clearPromptPosition() === null;\n\n      if (shouldBlockEnter) {\n        keyboardClaimer.claimedEvents.add(event);\n        event.preventDefault();\n        event.stopImmediatePropagation();\n        return true;\n      }\n      return false;\n    };\n\n    eventListenerManager.addDocumentListener(\"keydown\", blockEnterIfNeeded, {\n      capture: true,\n    });\n    eventListenerManager.addDocumentListener(\"keyup\", blockEnterIfNeeded, {\n      capture: true,\n    });\n    eventListenerManager.addDocumentListener(\"keypress\", blockEnterIfNeeded, {\n      capture: true,\n    });\n\n    const handleUndoRedoKeys = (event: KeyboardEvent): boolean => {\n      const isUndoOrRedo =\n        event.code === \"KeyZ\" && (event.metaKey || event.ctrlKey);\n\n      if (!isUndoOrRedo) return false;\n\n      const hasActiveConfirmation = Array.from(\n        agentManager.sessions().values(),\n      ).some((session) => !session.isStreaming && !session.error);\n\n      if (hasActiveConfirmation) return false;\n\n      const isRedo = event.shiftKey;\n\n      if (isRedo && agentManager.canRedo()) {\n        event.preventDefault();\n        event.stopPropagation();\n        agentManager.history.redo();\n        return true;\n      } else if (!isRedo && agentManager.canUndo()) {\n        event.preventDefault();\n        event.stopPropagation();\n        agentManager.history.undo();\n        return true;\n      }\n\n      return false;\n    };\n\n    const clearArrowNavigation = () => {\n      setArrowNavigationElements([]);\n      setArrowNavigationActiveIndex(0);\n      arrowNavigator.clearHistory();\n    };\n\n    const selectAndFocusElement = (element: Element) => {\n      actions.setFrozenElement(element);\n      actions.freeze();\n      keyboardSelectedElement = element;\n\n      const bounds = createElementBounds(element);\n      const center = getBoundsCenter(bounds);\n      actions.setPointer(center);\n\n      if (store.contextMenuPosition !== null) {\n        actions.showContextMenu(center, element);\n      }\n    };\n\n    const openArrowNavigationMenu = (anchorElement: Element) => {\n      const bounds = createElementBounds(anchorElement);\n      const probePoint = getVisibleBoundsCenter(bounds);\n      const elementsAtPoint = getElementsAtPoint(probePoint.x, probePoint.y)\n        .filter(isValidGrabbableElement)\n        .reverse();\n\n      setArrowNavigationElements(elementsAtPoint);\n      setArrowNavigationActiveIndex(\n        Math.max(0, elementsAtPoint.indexOf(anchorElement)),\n      );\n    };\n\n    const handleArrowNavigationSelect = (index: number) => {\n      const targetElement = arrowNavigationElements()[index];\n      if (!targetElement) return;\n\n      setArrowNavigationActiveIndex(index);\n      arrowNavigator.clearHistory();\n      selectAndFocusElement(targetElement);\n    };\n\n    const handleArrowNavigation = (event: KeyboardEvent): boolean => {\n      if (!isActivated() || isPromptMode()) return false;\n      if (!ARROW_KEYS.has(event.key)) return false;\n\n      let currentElement = effectiveElement();\n      const isInitialSelection = !currentElement;\n\n      if (!currentElement) {\n        currentElement = getElementAtPosition(\n          window.innerWidth / 2,\n          window.innerHeight / 2,\n        );\n      }\n\n      if (!currentElement) return false;\n\n      const isVertical = event.key === \"ArrowUp\" || event.key === \"ArrowDown\";\n\n      if (!isVertical) {\n        clearArrowNavigation();\n        const nextElement = arrowNavigator.findNext(event.key, currentElement);\n        if (!nextElement && !isInitialSelection) return false;\n        event.preventDefault();\n        event.stopPropagation();\n        selectAndFocusElement(nextElement ?? currentElement);\n        return true;\n      }\n\n      if (arrowNavigationElements().length === 0) {\n        openArrowNavigationMenu(currentElement);\n      }\n\n      const nextElement = arrowNavigator.findNext(event.key, currentElement);\n      const elementToSelect = nextElement ?? currentElement;\n\n      event.preventDefault();\n      event.stopPropagation();\n      selectAndFocusElement(elementToSelect);\n\n      const newIndex = arrowNavigationElements().indexOf(elementToSelect);\n      if (newIndex !== -1) {\n        setArrowNavigationActiveIndex(newIndex);\n      } else {\n        openArrowNavigationMenu(elementToSelect);\n      }\n\n      return true;\n    };\n\n    const handleEnterKeyActivation = (event: KeyboardEvent): boolean => {\n      if (!isEnterCode(event.code)) return false;\n      if (isKeyboardEventTriggeredByInput(event)) return false;\n\n      const copiedElement = store.lastCopiedElement;\n      const canActivateFromCopied =\n        !isHoldingKeys() &&\n        !isPromptMode() &&\n        !isActivated() &&\n        copiedElement &&\n        isElementConnected(copiedElement) &&\n        !store.labelInstances.some(\n          (instance) =>\n            instance.status === \"copied\" || instance.status === \"fading\",\n        );\n\n      if (canActivateFromCopied) {\n        event.preventDefault();\n        event.stopImmediatePropagation();\n\n        const center = getElementCenter(copiedElement);\n\n        actions.setPointer(center);\n        preparePromptMode(copiedElement, center.x, center.y);\n        actions.setFrozenElement(copiedElement);\n        actions.clearLastCopied();\n\n        activatePromptMode();\n        if (!isActivated()) {\n          activateRenderer();\n        }\n        return true;\n      }\n\n      const canActivateFromHolding = isHoldingKeys() && !isPromptMode();\n\n      if (canActivateFromHolding) {\n        event.preventDefault();\n        event.stopImmediatePropagation();\n\n        const element = store.frozenElement || targetElement();\n        if (element) {\n          preparePromptMode(element, store.pointer.x, store.pointer.y);\n        }\n\n        actions.setPointer({ x: store.pointer.x, y: store.pointer.y });\n        if (element) {\n          actions.setFrozenElement(element);\n        }\n        activatePromptMode();\n\n        if (keydownSpamTimerId !== null) {\n          window.clearTimeout(keydownSpamTimerId);\n          keydownSpamTimerId = null;\n        }\n\n        if (!isActivated()) {\n          activateRenderer();\n        }\n\n        return true;\n      }\n\n      return false;\n    };\n\n    const handleOpenFileShortcut = (event: KeyboardEvent): boolean => {\n      if (event.key?.toLowerCase() !== \"o\" || isPromptMode()) return false;\n      if (!isActivated() || !(event.metaKey || event.ctrlKey)) return false;\n\n      const filePath = store.selectionFilePath;\n      const lineNumber = store.selectionLineNumber;\n      if (!filePath) return false;\n\n      event.preventDefault();\n      event.stopPropagation();\n\n      const wasHandled = pluginRegistry.hooks.onOpenFile(\n        filePath,\n        lineNumber ?? undefined,\n      );\n      if (!wasHandled) {\n        openFile(\n          filePath,\n          lineNumber ?? undefined,\n          pluginRegistry.hooks.transformOpenFileUrl,\n        );\n      }\n      return true;\n    };\n\n    const clearActionCycleIdleTimeout = () => {\n      if (actionCycleIdleTimeoutId !== null) {\n        window.clearTimeout(actionCycleIdleTimeoutId);\n        actionCycleIdleTimeoutId = null;\n      }\n    };\n\n    const resetActionCycle = () => {\n      clearActionCycleIdleTimeout();\n      setActionCycleItems([]);\n      setActionCycleActiveIndex(null);\n    };\n\n    const canCycleActions = createMemo(() => {\n      const element = selectionElement();\n      return (\n        Boolean(element) &&\n        isRendererActive() &&\n        !isPromptMode() &&\n        !isDragging() &&\n        store.contextMenuPosition === null\n      );\n    });\n\n    const actionCycleState = createMemo<ActionCycleState>(() => ({\n      items: actionCycleItems(),\n      activeIndex: actionCycleActiveIndex(),\n      isVisible:\n        actionCycleActiveIndex() !== null && actionCycleItems().length > 0,\n    }));\n\n    const arrowNavigationItems = createMemo(() =>\n      arrowNavigationElements().map((element) => ({\n        tagName: getTagName(element) || \"element\",\n        componentName: getComponentDisplayName(element) ?? undefined,\n      })),\n    );\n\n    const arrowNavigationState = createMemo<ArrowNavigationState>(() => ({\n      items: arrowNavigationItems(),\n      activeIndex: arrowNavigationActiveIndex(),\n      isVisible: arrowNavigationElements().length > 0,\n    }));\n\n    createEffect(\n      on(selectionElement, () => {\n        resetActionCycle();\n      }),\n    );\n\n    createEffect(\n      on(canCycleActions, (isEnabled) => {\n        if (!isEnabled) {\n          resetActionCycle();\n        }\n      }),\n    );\n\n    const getActionById = (actionId: string): ContextMenuAction | undefined =>\n      pluginRegistry.store.actions.find((action) => action.id === actionId);\n\n    const getActionCycleContext = (): ContextMenuActionContext | undefined => {\n      const element = selectionElement();\n      if (!element) return undefined;\n\n      const fallbackBounds = selectionBounds();\n\n      return buildActionContext({\n        element,\n        filePath: store.selectionFilePath ?? undefined,\n        lineNumber: store.selectionLineNumber ?? undefined,\n        tagName: getTagName(element) || undefined,\n        componentName: resolvedComponentName(),\n        position: store.pointer,\n        performWithFeedbackOptions: {\n          fallbackBounds,\n          fallbackSelectionBounds: fallbackBounds ? [fallbackBounds] : [],\n        },\n        shouldDeferHideContextMenu: false,\n        onBeforePrompt: resetActionCycle,\n      });\n    };\n\n    const availableActionCycleItems = createMemo((): ActionCycleItem[] => {\n      if (!selectionElement()) return [];\n\n      const cycleItems: ActionCycleItem[] = [];\n      for (const action of pluginRegistry.store.actions) {\n        const isStaticallyDisabled =\n          typeof action.enabled === \"boolean\" && !action.enabled;\n        if (isStaticallyDisabled) continue;\n        cycleItems.push({\n          id: action.id,\n          label: action.label,\n          shortcut: action.shortcut,\n        });\n      }\n      return cycleItems;\n    });\n\n    const scheduleActionCycleActivation = () => {\n      clearActionCycleIdleTimeout();\n      actionCycleIdleTimeoutId = window.setTimeout(() => {\n        actionCycleIdleTimeoutId = null;\n        const activeIndex = actionCycleActiveIndex();\n        const items = actionCycleItems();\n        if (activeIndex === null || items.length === 0) return;\n        const selectedItem = items[activeIndex];\n        if (!selectedItem) return;\n        const action = getActionById(selectedItem.id);\n        if (!action) {\n          resetActionCycle();\n          return;\n        }\n        const context = getActionCycleContext();\n        if (!context || !resolveActionEnabled(action, context)) {\n          resetActionCycle();\n          return;\n        }\n        resetActionCycle();\n        const result = action.onAction(context);\n        if (result instanceof Promise) {\n          void result;\n        }\n      }, ACTION_CYCLE_IDLE_TRIGGER_MS);\n    };\n\n    const advanceActionCycle = (): boolean => {\n      if (!canCycleActions()) return false;\n      const cycleItems = availableActionCycleItems();\n      if (cycleItems.length === 0) return false;\n\n      setActionCycleItems(cycleItems);\n\n      const currentIndex = actionCycleActiveIndex();\n      const isCurrentIndexValid =\n        currentIndex !== null && currentIndex < cycleItems.length;\n      const nextIndex = isCurrentIndexValid\n        ? (currentIndex + 1) % cycleItems.length\n        : 0;\n\n      setActionCycleActiveIndex(nextIndex);\n      scheduleActionCycleActivation();\n      return true;\n    };\n\n    const handleActionCycleKey = (event: KeyboardEvent): boolean => {\n      if (event.code !== \"KeyC\") return false;\n      if (event.altKey || event.repeat) return false;\n      if (isKeyboardEventTriggeredByInput(event)) return false;\n      if (!advanceActionCycle()) return false;\n\n      event.preventDefault();\n      event.stopPropagation();\n      if (event.metaKey || event.ctrlKey) {\n        event.stopImmediatePropagation();\n      }\n      return true;\n    };\n\n    const handleActivationKeys = (event: KeyboardEvent): void => {\n      if (\n        !pluginRegistry.store.options.allowActivationInsideInput &&\n        isKeyboardEventTriggeredByInput(event)\n      ) {\n        return;\n      }\n\n      if (!isTargetKeyCombination(event, pluginRegistry.store.options)) {\n        if (\n          (event.metaKey || event.ctrlKey) &&\n          !MODIFIER_KEYS.includes(event.key) &&\n          !isEnterCode(event.code)\n        ) {\n          if (isActivated() && !store.wasActivatedByToggle) {\n            deactivateRenderer();\n          } else if (isHoldingKeys()) {\n            clearHoldTimer();\n            resetCopyConfirmation();\n            actions.releaseHold();\n          }\n        }\n        if (!isEnterCode(event.code) || !isHoldingKeys()) {\n          return;\n        }\n      }\n\n      if ((isActivated() || isHoldingKeys()) && !isPromptMode()) {\n        event.preventDefault();\n        if (isEnterCode(event.code)) {\n          event.stopImmediatePropagation();\n        }\n      }\n\n      if (isActivated()) {\n        if (\n          store.wasActivatedByToggle &&\n          pluginRegistry.store.options.activationMode !== \"hold\"\n        )\n          return;\n        if (event.repeat) return;\n\n        if (keydownSpamTimerId !== null) {\n          window.clearTimeout(keydownSpamTimerId);\n        }\n        keydownSpamTimerId = window.setTimeout(() => {\n          deactivateRenderer();\n        }, KEYDOWN_SPAM_TIMEOUT_MS);\n        return;\n      }\n\n      if (isHoldingKeys() && event.repeat) {\n        if (holdState.copyWaiting) {\n          const shouldActivate = holdState.holdTimerFired;\n          resetCopyConfirmation();\n          if (shouldActivate) {\n            actions.activate();\n          }\n        }\n        return;\n      }\n\n      if (isCopying() || didJustCopy()) return;\n\n      if (!isHoldingKeys()) {\n        const keyHoldDuration =\n          pluginRegistry.store.options.keyHoldDuration ??\n          DEFAULT_KEY_HOLD_DURATION_MS;\n\n        let activationDuration = keyHoldDuration;\n        if (isKeyboardEventTriggeredByInput(event)) {\n          if (hasTextSelectionInInput(event)) {\n            activationDuration += INPUT_TEXT_SELECTION_ACTIVATION_DELAY_MS;\n          } else {\n            activationDuration += INPUT_FOCUS_ACTIVATION_DELAY_MS;\n          }\n        } else if (hasTextSelectionOnPage()) {\n          activationDuration += INPUT_TEXT_SELECTION_ACTIVATION_DELAY_MS;\n        }\n        resetCopyConfirmation();\n        actions.startHold(activationDuration);\n      }\n    };\n\n    eventListenerManager.addWindowListener(\n      \"keydown\",\n      (event: KeyboardEvent) => {\n        blockEnterIfNeeded(event);\n\n        if (!isEnabled()) {\n          if (\n            isTargetKeyCombination(event, pluginRegistry.store.options) &&\n            !event.repeat\n          ) {\n            setToolbarShakeCount((count) => count + 1);\n          }\n          return;\n        }\n\n        if (handleUndoRedoKeys(event)) return;\n\n        const isEnterToActivateInput =\n          isEnterCode(event.code) && isHoldingKeys() && !isPromptMode();\n\n        const isFromReactGrabInput = isEventFromOverlay(\n          event,\n          \"data-react-grab-input\",\n        );\n        if (\n          isPromptMode() &&\n          isTargetKeyCombination(event, pluginRegistry.store.options) &&\n          !event.repeat &&\n          !isFromReactGrabInput\n        ) {\n          event.preventDefault();\n          event.stopPropagation();\n          handleInputCancel();\n          return;\n        }\n\n        if (event.key === \"Escape\" && clearPromptPosition() !== null) {\n          return;\n        }\n\n        if (event.key === \"Escape\" && historyDropdownPosition() !== null) {\n          dismissHistoryDropdown();\n          return;\n        }\n\n        if (toolbarMenuPosition() !== null) {\n          if (event.key === \"Escape\") {\n            dismissToolbarMenu();\n            return;\n          }\n\n          const toolbarActions = pluginRegistry.store.toolbarActions;\n          const isModifierPressed =\n            (event.metaKey || event.ctrlKey) && !event.repeat;\n          const matchedAction = toolbarActions.find((action) => {\n            if (!action.shortcut) return false;\n            if (event.key === \"Enter\") return action.shortcut === \"Enter\";\n            return (\n              isModifierPressed &&\n              event.key.toLowerCase() === action.shortcut.toLowerCase()\n            );\n          });\n\n          if (matchedAction && resolveToolbarActionEnabled(matchedAction)) {\n            event.preventDefault();\n            event.stopPropagation();\n            matchedAction.onAction();\n            dismissToolbarMenu();\n          }\n          return;\n        }\n\n        const isFromOverlay =\n          isEventFromOverlay(event, \"data-react-grab-ignore-events\") &&\n          !isEnterToActivateInput;\n\n        if (isPromptMode() || isFromOverlay) {\n          if (event.key === \"Escape\") {\n            if (store.pendingAbortSessionId) {\n              event.preventDefault();\n              event.stopPropagation();\n              actions.setPendingAbortSessionId(null);\n            } else if (isPromptMode()) {\n              handleInputCancel();\n            } else if (store.wasActivatedByToggle) {\n              deactivateRenderer();\n            }\n          }\n\n          if (isFromOverlay && ARROW_KEYS.has(event.key)) {\n            if (handleArrowNavigation(event)) return;\n          }\n\n          return;\n        }\n\n        if (event.key === \"Escape\") {\n          if (store.pendingAbortSessionId) {\n            event.preventDefault();\n            event.stopPropagation();\n            actions.setPendingAbortSessionId(null);\n            return;\n          }\n\n          if (agentManager.isProcessing()) {\n            return;\n          }\n\n          if (isHoldingKeys() || store.wasActivatedByToggle) {\n            deactivateRenderer();\n            return;\n          }\n        }\n\n        const didWindowJustRegainFocus =\n          Date.now() - lastWindowFocusTimestamp <\n          WINDOW_REFOCUS_GRACE_PERIOD_MS;\n\n        if (!didWindowJustRegainFocus && handleActionCycleKey(event)) return;\n        if (handleArrowNavigation(event)) return;\n        if (handleEnterKeyActivation(event)) return;\n        if (handleOpenFileShortcut(event)) return;\n\n        if (!didWindowJustRegainFocus) {\n          handleActivationKeys(event);\n        }\n      },\n      { capture: true },\n    );\n\n    eventListenerManager.addWindowListener(\n      \"keyup\",\n      (event: KeyboardEvent) => {\n        if (blockEnterIfNeeded(event)) return;\n\n        const requiredModifiers = getRequiredModifiers(\n          pluginRegistry.store.options,\n        );\n        const isReleasingModifier =\n          requiredModifiers.metaKey || requiredModifiers.ctrlKey\n            ? isMac()\n              ? !event.metaKey\n              : !event.ctrlKey\n            : (requiredModifiers.shiftKey && !event.shiftKey) ||\n              (requiredModifiers.altKey && !event.altKey);\n\n        const isReleasingActivationKey = pluginRegistry.store.options\n          .activationKey\n          ? typeof pluginRegistry.store.options.activationKey === \"function\"\n            ? pluginRegistry.store.options.activationKey(event)\n            : parseActivationKey(pluginRegistry.store.options.activationKey)(\n                event,\n              )\n          : isCLikeKey(event.key, event.code);\n\n        if (didJustCopy() || copyFeedbackCooldown.isActive) {\n          if (isReleasingActivationKey || isReleasingModifier) {\n            copyFeedbackCooldown.clear();\n            deactivateRenderer();\n          }\n          return;\n        }\n\n        if (!isHoldingKeys() && !isActivated()) return;\n        if (isPromptMode()) return;\n\n        const hasCustomShortcut = Boolean(\n          pluginRegistry.store.options.activationKey,\n        );\n\n        const isHoldMode =\n          pluginRegistry.store.options.activationMode === \"hold\";\n\n        if (isActivated()) {\n          const hasContextMenu = store.contextMenuPosition !== null;\n          if (isReleasingModifier) {\n            if (\n              store.wasActivatedByToggle &&\n              pluginRegistry.store.options.activationMode !== \"hold\"\n            )\n              return;\n            if (hasContextMenu) return;\n            deactivateRenderer();\n          } else if (isHoldMode && isReleasingActivationKey) {\n            if (keydownSpamTimerId !== null) {\n              window.clearTimeout(keydownSpamTimerId);\n              keydownSpamTimerId = null;\n            }\n            if (hasContextMenu) return;\n            deactivateRenderer();\n          } else if (\n            !hasCustomShortcut &&\n            isReleasingActivationKey &&\n            keydownSpamTimerId !== null\n          ) {\n            window.clearTimeout(keydownSpamTimerId);\n            keydownSpamTimerId = null;\n          }\n          return;\n        }\n\n        if (isReleasingActivationKey || isReleasingModifier) {\n          if (\n            store.wasActivatedByToggle &&\n            pluginRegistry.store.options.activationMode !== \"hold\"\n          )\n            return;\n\n          const shouldRelease =\n            isHoldingKeys() ||\n            (holdState.holdTimerFired && isReleasingModifier);\n\n          if (shouldRelease) {\n            clearHoldTimer();\n            const elapsedSinceHoldStart = holdState.startTimestamp\n              ? Date.now() - holdState.startTimestamp\n              : 0;\n            const heldLongEnoughForActivation =\n              elapsedSinceHoldStart >= MIN_HOLD_FOR_ACTIVATION_AFTER_COPY_MS;\n            const shouldActivateAfterCopy =\n              holdState.holdTimerFired &&\n              heldLongEnoughForActivation &&\n              (pluginRegistry.store.options.allowActivationInsideInput ||\n                !isKeyboardEventTriggeredByInput(event));\n            resetCopyConfirmation();\n            if (shouldActivateAfterCopy) {\n              actions.activate();\n            } else {\n              actions.releaseHold();\n            }\n          } else {\n            deactivateRenderer();\n          }\n        }\n      },\n      { capture: true },\n    );\n\n    eventListenerManager.addDocumentListener(\"copy\", () => {\n      if (isHoldingKeys()) {\n        holdState.copyWaiting = true;\n      }\n    });\n\n    eventListenerManager.addWindowListener(\"keypress\", blockEnterIfNeeded, {\n      capture: true,\n    });\n\n    eventListenerManager.addWindowListener(\n      \"pointermove\",\n      (event: PointerEvent) => {\n        if (!event.isPrimary) return;\n        const isTouchPointer = event.pointerType === \"touch\";\n        actions.setTouchMode(isTouchPointer);\n        if (isEventFromOverlay(event, \"data-react-grab-ignore-events\")) return;\n        if (store.contextMenuPosition !== null) return;\n        if (isTouchPointer && !isHoldingKeys() && !isActivated()) return;\n        const isActiveState = isTouchPointer ? isHoldingKeys() : isActivated();\n        if (isActiveState && !isPromptMode() && isFrozenPhase()) {\n          actions.unfreeze();\n          clearArrowNavigation();\n        }\n        handlePointerMove(event.clientX, event.clientY);\n      },\n      { passive: true },\n    );\n\n    eventListenerManager.addWindowListener(\n      \"pointerdown\",\n      (event: PointerEvent) => {\n        if (event.button !== 0) return;\n        if (!event.isPrimary) return;\n        actions.setTouchMode(event.pointerType === \"touch\");\n        if (isEventFromOverlay(event, \"data-react-grab-ignore-events\")) return;\n        if (store.contextMenuPosition !== null) return;\n        if (toolbarMenuPosition() !== null) return;\n\n        if (isPromptMode()) {\n          const bounds = selectionBounds();\n          const isClickOnSelection =\n            bounds &&\n            event.clientX >= bounds.x &&\n            event.clientX <= bounds.x + bounds.width &&\n            event.clientY >= bounds.y &&\n            event.clientY <= bounds.y + bounds.height;\n\n          if (isClickOnSelection) {\n            void handleInputSubmit();\n          } else {\n            handleInputCancel();\n          }\n          return;\n        }\n\n        const didHandle = handlePointerDown(event.clientX, event.clientY);\n        if (didHandle) {\n          document.documentElement.setPointerCapture(event.pointerId);\n          event.preventDefault();\n          event.stopImmediatePropagation();\n        }\n      },\n      { capture: true },\n    );\n\n    eventListenerManager.addWindowListener(\n      \"pointerup\",\n      (event: PointerEvent) => {\n        if (event.button !== 0) return;\n        if (!event.isPrimary) return;\n        if (isEventFromOverlay(event, \"data-react-grab-ignore-events\")) return;\n        if (store.contextMenuPosition !== null) return;\n        const isActive = isRendererActive() || isCopying() || isDragging();\n        const hasModifierKeyHeld = event.metaKey || event.ctrlKey;\n        handlePointerUp(event.clientX, event.clientY, hasModifierKeyHeld);\n        if (isActive) {\n          event.preventDefault();\n          event.stopImmediatePropagation();\n        }\n      },\n      { capture: true },\n    );\n\n    eventListenerManager.addWindowListener(\n      \"contextmenu\",\n      (event: MouseEvent) => {\n        if (!isRendererActive() || isCopying() || isPromptMode()) return;\n\n        const isFromOverlay = isEventFromOverlay(\n          event,\n          \"data-react-grab-ignore-events\",\n        );\n        if (isFromOverlay && arrowNavigationElements().length > 0) {\n          clearArrowNavigation();\n        } else if (isFromOverlay) {\n          return;\n        }\n\n        if (store.contextMenuPosition !== null) {\n          event.preventDefault();\n          return;\n        }\n\n        event.preventDefault();\n        event.stopPropagation();\n\n        const element = getElementAtPosition(event.clientX, event.clientY);\n        if (!element) return;\n\n        const existingFrozenElements = store.frozenElements;\n        const isClickedElementAlreadyFrozen =\n          existingFrozenElements.length > 1 &&\n          existingFrozenElements.includes(element);\n\n        if (isClickedElementAlreadyFrozen) {\n          freezeAllAnimations(existingFrozenElements);\n        } else {\n          freezeAllAnimations([element]);\n          actions.setFrozenElement(element);\n        }\n\n        const position = { x: event.clientX, y: event.clientY };\n        actions.setPointer(position);\n        actions.freeze();\n        openContextMenu(element, position);\n      },\n      { capture: true },\n    );\n\n    eventListenerManager.addWindowListener(\n      \"pointercancel\",\n      (event: PointerEvent) => {\n        if (!event.isPrimary) return;\n        cancelActiveDrag();\n      },\n    );\n\n    eventListenerManager.addWindowListener(\n      \"click\",\n      (event: MouseEvent) => {\n        if (isEventFromOverlay(event, \"data-react-grab-ignore-events\")) return;\n        if (store.contextMenuPosition !== null) return;\n\n        if (isRendererActive() || isCopying() || didJustDrag()) {\n          event.preventDefault();\n          event.stopImmediatePropagation();\n\n          if (store.wasActivatedByToggle && !isCopying() && !isPromptMode()) {\n            if (!isHoldingKeys()) {\n              deactivateRenderer();\n            } else {\n              actions.setWasActivatedByToggle(false);\n            }\n          }\n        }\n      },\n      { capture: true },\n    );\n\n    eventListenerManager.addDocumentListener(\"visibilitychange\", () => {\n      if (document.hidden) {\n        actions.clearGrabbedBoxes();\n        const storeActivationTimestamp = store.activationTimestamp;\n        if (\n          isActivated() &&\n          !isPromptMode() &&\n          storeActivationTimestamp !== null &&\n          Date.now() - storeActivationTimestamp > BLUR_DEACTIVATION_THRESHOLD_MS\n        ) {\n          deactivateRenderer();\n        }\n      }\n    });\n\n    eventListenerManager.addWindowListener(\"blur\", () => {\n      cancelActiveDrag();\n      if (isHoldingKeys()) {\n        clearHoldTimer();\n        actions.releaseHold();\n        resetCopyConfirmation();\n      }\n    });\n\n    eventListenerManager.addWindowListener(\"focus\", () => {\n      lastWindowFocusTimestamp = Date.now();\n    });\n\n    eventListenerManager.addWindowListener(\n      \"focusin\",\n      (event: FocusEvent) => {\n        if (isEventFromOverlay(event, \"data-react-grab\")) {\n          event.stopPropagation();\n        }\n      },\n      { capture: true },\n    );\n\n    const redetectElementUnderPointer = () => {\n      if (store.isTouchMode && !isHoldingKeys() && !isActivated()) return;\n      if (\n        isEnabled() &&\n        !isPromptMode() &&\n        !isFrozenPhase() &&\n        !isDragging() &&\n        store.contextMenuPosition === null &&\n        store.frozenElements.length === 0\n      ) {\n        const candidate = getElementAtPosition(\n          store.pointer.x,\n          store.pointer.y,\n        );\n        actions.setDetectedElement(candidate);\n      }\n    };\n\n    const handleViewportChange = () => {\n      invalidateInteractionCaches();\n      redetectElementUnderPointer();\n      actions.incrementViewportVersion();\n      agentManager._internal.updateBoundsOnViewportChange();\n      actions.updateContextMenuPosition();\n    };\n\n    eventListenerManager.addWindowListener(\"scroll\", handleViewportChange, {\n      capture: true,\n    });\n\n    let previousViewportWidth = window.innerWidth;\n    let previousViewportHeight = window.innerHeight;\n\n    eventListenerManager.addWindowListener(\"resize\", () => {\n      const currentViewportWidth = window.innerWidth;\n      const currentViewportHeight = window.innerHeight;\n\n      if (previousViewportWidth > 0 && previousViewportHeight > 0) {\n        const scaleX = currentViewportWidth / previousViewportWidth;\n        const scaleY = currentViewportHeight / previousViewportHeight;\n        const isUniformScale =\n          Math.abs(scaleX - scaleY) < ZOOM_DETECTION_THRESHOLD;\n        const hasScaleChanged = Math.abs(scaleX - 1) > ZOOM_DETECTION_THRESHOLD;\n\n        if (isUniformScale && hasScaleChanged) {\n          actions.setPointer({\n            x: store.pointer.x * scaleX,\n            y: store.pointer.y * scaleY,\n          });\n        }\n      }\n\n      previousViewportWidth = currentViewportWidth;\n      previousViewportHeight = currentViewportHeight;\n\n      handleViewportChange();\n    });\n\n    const visualViewport = window.visualViewport;\n    if (visualViewport) {\n      const { signal } = eventListenerManager;\n      visualViewport.addEventListener(\"resize\", handleViewportChange, {\n        signal,\n      });\n      visualViewport.addEventListener(\"scroll\", handleViewportChange, {\n        signal,\n      });\n    }\n\n    let boundsRecalcIntervalId: number | null = null;\n    let viewportChangeFrameId: number | null = null;\n\n    const scheduleBoundsSync = () => {\n      if (viewportChangeFrameId !== null) return;\n\n      viewportChangeFrameId = nativeRequestAnimationFrame(() => {\n        viewportChangeFrameId = null;\n        actions.incrementViewportVersion();\n        agentManager._internal.updateBoundsOnViewportChange();\n      });\n    };\n\n    createEffect(() => {\n      const shouldRunInterval =\n        pluginRegistry.store.theme.enabled &&\n        (isActivated() ||\n          isCopying() ||\n          store.labelInstances.length > 0 ||\n          store.grabbedBoxes.length > 0 ||\n          agentManager.sessions().size > 0);\n\n      if (shouldRunInterval) {\n        if (boundsRecalcIntervalId !== null) return;\n\n        boundsRecalcIntervalId = window.setInterval(() => {\n          scheduleBoundsSync();\n        }, BOUNDS_RECALC_INTERVAL_MS);\n        return;\n      }\n\n      if (boundsRecalcIntervalId !== null) {\n        window.clearInterval(boundsRecalcIntervalId);\n        boundsRecalcIntervalId = null;\n      }\n\n      if (viewportChangeFrameId !== null) {\n        nativeCancelAnimationFrame(viewportChangeFrameId);\n        viewportChangeFrameId = null;\n      }\n    });\n\n    onCleanup(() => {\n      if (boundsRecalcIntervalId !== null) {\n        window.clearInterval(boundsRecalcIntervalId);\n      }\n      if (viewportChangeFrameId !== null) {\n        nativeCancelAnimationFrame(viewportChangeFrameId);\n      }\n    });\n\n    eventListenerManager.addDocumentListener(\n      \"copy\",\n      (event: ClipboardEvent) => {\n        if (\n          isPromptMode() ||\n          isEventFromOverlay(event, \"data-react-grab-ignore-events\")\n        ) {\n          return;\n        }\n        if (isRendererActive() || isCopying()) {\n          event.preventDefault();\n        }\n      },\n      { capture: true },\n    );\n\n    onCleanup(() => {\n      eventListenerManager.abort();\n      if (dragPreviewDebounceTimerId !== null) {\n        window.clearTimeout(dragPreviewDebounceTimerId);\n      }\n      if (keydownSpamTimerId) window.clearTimeout(keydownSpamTimerId);\n      copyFeedbackCooldown.clear();\n      if (actionCycleIdleTimeoutId) {\n        window.clearTimeout(actionCycleIdleTimeoutId);\n      }\n      if (dropdownTrackingFrameId !== null) {\n        nativeCancelAnimationFrame(dropdownTrackingFrameId);\n      }\n      grabbedBoxTimeouts.forEach((timeoutId) => window.clearTimeout(timeoutId));\n      grabbedBoxTimeouts.clear();\n      cancelAllLabelFades();\n      autoScroller.stop();\n      document.body.style.userSelect = \"\";\n      document.body.style.touchAction = \"\";\n      setCursorOverride(null);\n      keyboardClaimer.restore();\n    });\n\n    const resolvedCssText = typeof cssText === \"string\" ? cssText : \"\";\n    const rendererRoot = mountRoot(resolvedCssText);\n\n    const isThemeEnabled = createMemo(() => pluginRegistry.store.theme.enabled);\n    const isSelectionBoxThemeEnabled = createMemo(\n      () => pluginRegistry.store.theme.selectionBox.enabled,\n    );\n    const isElementLabelThemeEnabled = createMemo(\n      () => pluginRegistry.store.theme.elementLabel.enabled,\n    );\n    const isDragBoxThemeEnabled = createMemo(\n      () => pluginRegistry.store.theme.dragBox.enabled,\n    );\n    const isSelectionSuppressed = createMemo(\n      () => didJustCopy() || (isToolbarSelectHovered() && !isFrozenPhase()),\n    );\n    const hasDragPreviewBounds = createMemo(\n      () => dragPreviewBounds().length > 0,\n    );\n\n    const selectionVisible = createMemo(() => {\n      if (!isThemeEnabled()) return false;\n      if (!isSelectionBoxThemeEnabled()) return false;\n      if (isSelectionSuppressed()) return false;\n      if (hasDragPreviewBounds()) return true;\n      return isSelectionElementVisible();\n    });\n\n    const selectionTagName = createMemo(() => {\n      const element = selectionElement();\n      if (!element) return undefined;\n      return getTagName(element) || undefined;\n    });\n\n    createEffect(\n      on(\n        () => debouncedElementForComponentName(),\n        (element) => {\n          const currentVersion = ++componentNameRequestVersion;\n\n          if (!element) {\n            setResolvedComponentName(undefined);\n            return;\n          }\n\n          getNearestComponentName(element)\n            .then((name) => {\n              if (componentNameRequestVersion !== currentVersion) return;\n              setResolvedComponentName(name ?? undefined);\n            })\n            .catch(() => {\n              if (componentNameRequestVersion !== currentVersion) return;\n              setResolvedComponentName(undefined);\n            });\n        },\n      ),\n    );\n\n    const selectionLabelVisible = createMemo(() => {\n      if (store.contextMenuPosition !== null) return false;\n      if (!isElementLabelThemeEnabled()) return false;\n      if (isSelectionSuppressed()) return false;\n\n      return isSelectionElementVisible();\n    });\n\n    const labelInstanceCache = new Map<string, SelectionLabelInstance>();\n    const computedLabelInstances = createMemo(() => {\n      if (!isThemeEnabled()) return [];\n      if (!pluginRegistry.store.theme.grabbedBoxes.enabled) return [];\n      void store.viewportVersion;\n      const currentIds = new Set(\n        store.labelInstances.map((instance) => instance.id),\n      );\n      for (const cachedId of labelInstanceCache.keys()) {\n        if (!currentIds.has(cachedId)) {\n          labelInstanceCache.delete(cachedId);\n        }\n      }\n      return store.labelInstances.map((instance) => {\n        const hasMultipleElements =\n          instance.elements && instance.elements.length > 1;\n        const instanceElement = instance.element;\n        const canRecalculateBounds =\n          !hasMultipleElements &&\n          instanceElement &&\n          document.body.contains(instanceElement);\n        const newBounds = canRecalculateBounds\n          ? createElementBounds(instanceElement)\n          : instance.bounds;\n\n        const previousInstance = labelInstanceCache.get(instance.id);\n        const boundsUnchanged =\n          previousInstance &&\n          previousInstance.bounds.x === newBounds.x &&\n          previousInstance.bounds.y === newBounds.y &&\n          previousInstance.bounds.width === newBounds.width &&\n          previousInstance.bounds.height === newBounds.height;\n        if (\n          previousInstance &&\n          previousInstance.status === instance.status &&\n          previousInstance.errorMessage === instance.errorMessage &&\n          boundsUnchanged\n        ) {\n          return previousInstance;\n        }\n        const newBoundsCenterX = newBounds.x + newBounds.width / 2;\n        const newBoundsHalfWidth = newBounds.width / 2;\n        const newMouseX =\n          instance.mouseXOffsetRatio !== undefined && newBoundsHalfWidth > 0\n            ? newBoundsCenterX + instance.mouseXOffsetRatio * newBoundsHalfWidth\n            : instance.mouseXOffsetFromCenter !== undefined\n              ? newBoundsCenterX + instance.mouseXOffsetFromCenter\n              : instance.mouseX;\n        const newCached = { ...instance, bounds: newBounds, mouseX: newMouseX };\n        labelInstanceCache.set(instance.id, newCached);\n        return newCached;\n      });\n    });\n\n    const computedGrabbedBoxes = createMemo(() => {\n      if (!isThemeEnabled()) return [];\n      if (!pluginRegistry.store.theme.grabbedBoxes.enabled) return [];\n      void store.viewportVersion;\n      return store.grabbedBoxes.map((box) => {\n        if (!box.element || !document.body.contains(box.element)) {\n          return box;\n        }\n        return {\n          ...box,\n          bounds: createElementBounds(box.element),\n        };\n      });\n    });\n\n    const dragVisible = createMemo(\n      () =>\n        isThemeEnabled() &&\n        isDragBoxThemeEnabled() &&\n        isRendererActive() &&\n        isDraggingBeyondThreshold(),\n    );\n\n    const labelVariant = createMemo(() =>\n      isCopying() ? \"processing\" : \"hover\",\n    );\n\n    const labelVisible = createMemo(() => {\n      if (!isThemeEnabled()) return false;\n      const themeEnabled = isElementLabelThemeEnabled();\n      const inPromptMode = isPromptMode();\n      const copying = isCopying();\n      const rendererActive = isRendererActive();\n      const dragging = isDragging();\n      const hasElement = Boolean(effectiveElement());\n      const toolbarSelectHovered = isToolbarSelectHovered();\n      const frozen = isFrozenPhase();\n\n      if (!themeEnabled) return false;\n      if (inPromptMode) return false;\n      if (toolbarSelectHovered && !frozen) return false;\n      if (copying) return true;\n      return rendererActive && !dragging && hasElement;\n    });\n\n    const contextMenuBounds = createMemo((): OverlayBounds | null => {\n      void store.viewportVersion;\n      const element = store.contextMenuElement;\n      if (!element) return null;\n      return createElementBounds(element);\n    });\n\n    const contextMenuPosition = createMemo(() => {\n      void store.viewportVersion;\n      return store.contextMenuPosition;\n    });\n\n    const contextMenuTagName = createMemo(() => {\n      const element = store.contextMenuElement;\n      if (!element) return undefined;\n      const frozenCount = store.frozenElements.length;\n      if (frozenCount > 1) {\n        return `${frozenCount} elements`;\n      }\n      return getTagName(element) || undefined;\n    });\n\n    const [contextMenuComponentName] = createResource(\n      () => ({\n        element: store.contextMenuElement,\n        frozenCount: store.frozenElements.length,\n      }),\n      async ({ element, frozenCount }) => {\n        if (!element) return undefined;\n        if (frozenCount > 1) return undefined;\n        const name = await getNearestComponentName(element);\n        return name ?? undefined;\n      },\n    );\n\n    const [contextMenuFilePath] = createResource(\n      () => store.contextMenuElement,\n      async (element) => {\n        if (!element) return null;\n        return resolveSource(element);\n      },\n    );\n\n    const createPerformWithFeedback = (\n      element: Element,\n      elements: Element[],\n      tagName: string | undefined,\n      componentName: string | undefined,\n      options?: PerformWithFeedbackOptions,\n    ) => {\n      return async (action: () => Promise<boolean>): Promise<void> => {\n        const fallbackBounds = options?.fallbackBounds ?? null;\n        const fallbackSelectionBounds = options?.fallbackSelectionBounds ?? [];\n        const position =\n          options?.position ?? store.contextMenuPosition ?? store.pointer;\n        const frozenBounds = frozenElementsBounds();\n        const singleElementBounds = contextMenuBounds() ?? fallbackBounds;\n        const hasMultipleElements = elements.length > 1;\n\n        const labelBounds = hasMultipleElements\n          ? createFlatOverlayBounds(combineBounds(frozenBounds))\n          : singleElementBounds;\n\n        const shouldDeactivateAfter = store.wasActivatedByToggle;\n        const selectionBoundsForLabel = hasMultipleElements\n          ? frozenBounds\n          : singleElementBounds\n            ? [singleElementBounds]\n            : fallbackSelectionBounds;\n\n        actions.hideContextMenu();\n\n        if (labelBounds) {\n          const labelCursorX = hasMultipleElements\n            ? labelBounds.x + labelBounds.width / 2\n            : position.x;\n\n          const labelInstanceId = createLabelInstance(\n            labelBounds,\n            tagName || \"element\",\n            componentName,\n            \"copying\",\n            {\n              element,\n              mouseX: labelCursorX,\n              elements: hasMultipleElements ? elements : undefined,\n              boundsMultiple: selectionBoundsForLabel,\n            },\n          );\n\n          let didSucceed = false;\n          let errorMessage: string | undefined;\n\n          try {\n            didSucceed = await action();\n            if (!didSucceed) {\n              errorMessage = \"Failed to copy\";\n            }\n          } catch (error) {\n            errorMessage = normalizeErrorMessage(error, \"Action failed\");\n          }\n\n          updateLabelAfterCopy(labelInstanceId, didSucceed, errorMessage);\n        } else {\n          // HACK: Fire-and-forget when no label bounds to display feedback on\n          try {\n            await action();\n          } catch (error) {\n            logRecoverableError(\"Action failed without feedback bounds\", error);\n          }\n        }\n\n        if (shouldDeactivateAfter) {\n          deactivateRenderer();\n        } else {\n          actions.unfreeze();\n        }\n      };\n    };\n\n    // HACK: Defer hiding context menu until after click event propagates fully\n    const deferHideContextMenu = () => {\n      setTimeout(() => {\n        actions.hideContextMenu();\n      }, 0);\n    };\n\n    const buildActionContext = (\n      options: BuildActionContextOptions,\n    ): ContextMenuActionContext => {\n      const {\n        element,\n        filePath,\n        lineNumber,\n        tagName,\n        componentName,\n        position,\n        performWithFeedbackOptions,\n        shouldDeferHideContextMenu,\n        onBeforeCopy,\n        onBeforePrompt,\n        customEnterPromptMode,\n      } = options;\n\n      const elements =\n        store.frozenElements.length > 0 ? store.frozenElements : [element];\n\n      const hideContextMenuAction = shouldDeferHideContextMenu\n        ? deferHideContextMenu\n        : actions.hideContextMenu;\n\n      const copyAction = () => {\n        onBeforeCopy?.();\n        performCopyWithLabel({\n          element,\n          cursorX: position.x,\n          selectedElements: elements.length > 1 ? elements : undefined,\n          shouldDeactivateAfter: store.wasActivatedByToggle,\n        });\n        hideContextMenuAction();\n      };\n\n      const defaultEnterPromptMode = (agent?: AgentOptions) => {\n        if (agent) {\n          actions.setSelectedAgent(agent);\n        }\n        clearAllLabels();\n        onBeforePrompt?.();\n        preparePromptMode(element, position.x, position.y);\n        actions.setPointer({ x: position.x, y: position.y });\n        actions.setFrozenElement(element);\n        activatePromptMode();\n        if (!isActivated()) {\n          activateRenderer();\n        }\n        hideContextMenuAction();\n      };\n\n      const context: ContextMenuActionContext = {\n        element,\n        elements,\n        filePath,\n        lineNumber,\n        componentName,\n        tagName,\n        enterPromptMode: customEnterPromptMode ?? defaultEnterPromptMode,\n        copy: copyAction,\n        hooks: {\n          transformHtmlContent: pluginRegistry.hooks.transformHtmlContent,\n          onOpenFile: pluginRegistry.hooks.onOpenFile,\n          transformOpenFileUrl: pluginRegistry.hooks.transformOpenFileUrl,\n        },\n        performWithFeedback: createPerformWithFeedback(\n          element,\n          elements,\n          tagName,\n          componentName,\n          performWithFeedbackOptions,\n        ),\n        hideContextMenu: hideContextMenuAction,\n        cleanup: () => {\n          if (store.wasActivatedByToggle) {\n            deactivateRenderer();\n          } else {\n            actions.unfreeze();\n          }\n        },\n      };\n\n      const transformedContext =\n        pluginRegistry.hooks.transformActionContext(context);\n      return { ...context, ...transformedContext };\n    };\n\n    const contextMenuActionContext = createMemo(\n      (): ContextMenuActionContext | undefined => {\n        const element = store.contextMenuElement;\n        if (!element) return undefined;\n        const fileInfo = contextMenuFilePath();\n        const position = store.contextMenuPosition ?? store.pointer;\n\n        return buildActionContext({\n          element,\n          filePath: fileInfo?.filePath,\n          lineNumber: fileInfo?.lineNumber ?? undefined,\n          tagName: contextMenuTagName(),\n          componentName: contextMenuComponentName(),\n          position,\n          shouldDeferHideContextMenu: true,\n          onBeforeCopy: () => {\n            keyboardSelectedElement = null;\n          },\n          customEnterPromptMode: (agent?: AgentOptions) => {\n            if (agent) {\n              actions.setSelectedAgent(agent);\n            }\n            clearAllLabels();\n            actions.clearInputText();\n            actions.enterPromptMode(position, element);\n            deferHideContextMenu();\n          },\n        });\n      },\n    );\n\n    const handleContextMenuDismiss = () => {\n      setTimeout(() => {\n        actions.hideContextMenu();\n        deactivateRenderer();\n      }, 0);\n    };\n\n    const clearHistoryHoverPreviews = () => {\n      for (const { boxId, labelId } of historyHoverPreviews) {\n        actions.removeGrabbedBox(boxId);\n        if (labelId) {\n          actions.removeLabelInstance(labelId);\n        }\n      }\n      historyHoverPreviews = [];\n    };\n\n    const addHistoryItemPreview = (\n      item: HistoryItem,\n      previewBounds: OverlayBounds[],\n      previewElements: Element[],\n      idPrefix: string,\n    ) => {\n      if (previewBounds.length === 0) return;\n\n      const hasCommentText = item.isComment && item.commentText;\n      for (const [index, bounds] of previewBounds.entries()) {\n        const previewElement = previewElements[index];\n        const boxId = `${idPrefix}-${item.id}-${index}`;\n        // HACK: createdAt=0 is falsy, which skips the auto-fade logic in the overlay canvas animation loop\n        actions.addGrabbedBox({\n          id: boxId,\n          bounds,\n          createdAt: 0,\n          element: previewElement,\n        });\n\n        let labelId: string | null = null;\n        if (index === 0) {\n          labelId = `${idPrefix}-label-${item.id}`;\n          actions.addLabelInstance({\n            id: labelId,\n            bounds,\n            tagName: item.tagName,\n            componentName: item.componentName,\n            elementsCount: item.elementsCount,\n            status: \"idle\",\n            isPromptMode: Boolean(hasCommentText),\n            inputValue: hasCommentText ? item.commentText : undefined,\n            createdAt: 0,\n            element: previewElement,\n            mouseX: bounds.x + bounds.width / 2,\n          });\n        }\n\n        historyHoverPreviews.push({ boxId, labelId });\n      }\n    };\n\n    const showHistoryItemPreview = (\n      item: HistoryItem,\n      idPrefix: string,\n    ): void => {\n      const connectedElements = getConnectedHistoryElements(item);\n      const previewBounds = connectedElements.map((element) =>\n        createElementBounds(element),\n      );\n      addHistoryItemPreview(item, previewBounds, connectedElements, idPrefix);\n    };\n\n    const stopTrackingDropdownPosition = () => {\n      if (dropdownTrackingFrameId !== null) {\n        nativeCancelAnimationFrame(dropdownTrackingFrameId);\n        dropdownTrackingFrameId = null;\n      }\n    };\n\n    const startTrackingDropdownPosition = (computePosition: () => void) => {\n      stopTrackingDropdownPosition();\n      const updatePosition = () => {\n        computePosition();\n        dropdownTrackingFrameId = nativeRequestAnimationFrame(updatePosition);\n      };\n      updatePosition();\n    };\n\n    const getNearestEdge = (rect: DOMRect): ToolbarState[\"edge\"] => {\n      const centerX = rect.left + rect.width / 2;\n      const centerY = rect.top + rect.height / 2;\n      const distanceToTop = centerY;\n      const distanceToBottom = window.innerHeight - centerY;\n      const distanceToLeft = centerX;\n      const distanceToRight = window.innerWidth - centerX;\n      const minimumDistance = Math.min(\n        distanceToTop,\n        distanceToBottom,\n        distanceToLeft,\n        distanceToRight,\n      );\n      if (minimumDistance === distanceToTop) return \"top\";\n      if (minimumDistance === distanceToLeft) return \"left\";\n      if (minimumDistance === distanceToRight) return \"right\";\n      return \"bottom\";\n    };\n\n    const computeDropdownAnchor = (): DropdownAnchor | null => {\n      if (!toolbarElement) return null;\n      const toolbarRect = toolbarElement.getBoundingClientRect();\n      const edge = getNearestEdge(toolbarRect);\n\n      if (edge === \"left\" || edge === \"right\") {\n        return {\n          x: edge === \"left\" ? toolbarRect.right : toolbarRect.left,\n          y: toolbarRect.top + toolbarRect.height / 2,\n          edge,\n          toolbarWidth: toolbarRect.width,\n        };\n      }\n\n      return {\n        x: toolbarRect.left + toolbarRect.width / 2,\n        y: edge === \"top\" ? toolbarRect.bottom : toolbarRect.top,\n        edge,\n        toolbarWidth: toolbarRect.width,\n      };\n    };\n\n    const dismissHistoryDropdown = () => {\n      cancelHistoryHoverOpenTimeout();\n      cancelHistoryHoverCloseTimeout();\n      stopTrackingDropdownPosition();\n      clearHistoryHoverPreviews();\n      setHistoryDropdownPosition(null);\n      setIsHistoryHoverOpen(false);\n    };\n\n    const openHistoryDropdown = () => {\n      actions.hideContextMenu();\n      dismissToolbarMenu();\n      dismissClearPrompt();\n      setHistoryItems(loadHistory());\n      setHasUnreadHistoryItems(false);\n      startTrackingDropdownPosition(() => {\n        const anchor = computeDropdownAnchor();\n        if (anchor) setHistoryDropdownPosition(anchor);\n      });\n    };\n\n    let historyHoverOpenTimeoutId: ReturnType<typeof setTimeout> | null = null;\n    let historyHoverCloseTimeoutId: ReturnType<typeof setTimeout> | null = null;\n\n    const cancelHistoryHoverOpenTimeout = () => {\n      if (historyHoverOpenTimeoutId !== null) {\n        clearTimeout(historyHoverOpenTimeoutId);\n        historyHoverOpenTimeoutId = null;\n      }\n    };\n\n    const cancelHistoryHoverCloseTimeout = () => {\n      if (historyHoverCloseTimeoutId !== null) {\n        clearTimeout(historyHoverCloseTimeoutId);\n        historyHoverCloseTimeoutId = null;\n      }\n    };\n\n    const scheduleHistoryHoverClose = () => {\n      historyHoverCloseTimeoutId = setTimeout(() => {\n        historyHoverCloseTimeoutId = null;\n        dismissHistoryDropdown();\n      }, DROPDOWN_HOVER_OPEN_DELAY_MS);\n    };\n\n    const dismissToolbarMenu = () => {\n      stopTrackingDropdownPosition();\n      setToolbarMenuPosition(null);\n    };\n\n    const showClearPrompt = () => {\n      dismissHistoryDropdown();\n      dismissToolbarMenu();\n      startTrackingDropdownPosition(() => {\n        const anchor = computeDropdownAnchor();\n        if (anchor) setClearPromptPosition(anchor);\n      });\n    };\n\n    const dismissClearPrompt = () => {\n      stopTrackingDropdownPosition();\n      setClearPromptPosition(null);\n    };\n\n    const dismissAllPopups = () => {\n      dismissHistoryDropdown();\n      dismissToolbarMenu();\n      dismissClearPrompt();\n    };\n\n    const handleToggleMenu = () => {\n      if (toolbarMenuPosition() !== null) {\n        dismissToolbarMenu();\n      } else {\n        actions.hideContextMenu();\n        dismissHistoryDropdown();\n        dismissClearPrompt();\n        startTrackingDropdownPosition(() => {\n          const anchor = computeDropdownAnchor();\n          if (anchor) setToolbarMenuPosition(anchor);\n        });\n      }\n    };\n\n    const handleToggleHistory = () => {\n      cancelHistoryHoverOpenTimeout();\n      cancelHistoryHoverCloseTimeout();\n      const isCurrentlyOpen = historyDropdownPosition() !== null;\n      if (isCurrentlyOpen) {\n        if (isHistoryHoverOpen()) {\n          clearHistoryHoverPreviews();\n          setIsHistoryHoverOpen(false);\n        } else {\n          dismissHistoryDropdown();\n        }\n      } else {\n        clearHistoryHoverPreviews();\n        openHistoryDropdown();\n      }\n    };\n\n    const copyHistoryItemContent = (item: HistoryItem) => {\n      copyContent(item.content, {\n        tagName: item.tagName,\n        componentName: item.componentName ?? item.elementName,\n        commentText: item.commentText,\n      });\n      const element = getFirstConnectedHistoryElement(item);\n      if (!element) return;\n\n      clearAllLabels();\n\n      // HACK: defer to next frame so idle preview label clears visually before \"copied\" appears\n      nativeRequestAnimationFrame(() => {\n        if (!isElementConnected(element)) return;\n        const bounds = createElementBounds(element);\n        const instanceId = createLabelInstance(\n          bounds,\n          item.tagName,\n          item.componentName,\n          \"copied\",\n          { element, mouseX: bounds.x + bounds.width / 2 },\n        );\n        scheduleLabelFade(instanceId);\n      });\n    };\n\n    const handleHistoryItemSelect = (item: HistoryItem) => {\n      clearHistoryHoverPreviews();\n      if (isPromptMode()) {\n        actions.exitPromptMode();\n        actions.clearInputText();\n      }\n      const element = getFirstConnectedHistoryElement(item);\n\n      if (item.isComment && item.commentText && element) {\n        const bounds = createElementBounds(element);\n        const { x: centerX, y: centerY } = getBoundsCenter(bounds);\n        actions.enterPromptMode({ x: centerX, y: centerY }, element);\n        actions.setInputText(item.commentText);\n      } else {\n        copyHistoryItemContent(item);\n      }\n    };\n\n    const handleHistoryItemRemove = (item: HistoryItem) => {\n      clearHistoryHoverPreviews();\n      historyElementMap.delete(item.id);\n      const updatedHistoryItems = removeHistoryItem(item.id);\n      setHistoryItems(updatedHistoryItems);\n      if (updatedHistoryItems.length === 0) {\n        setHasUnreadHistoryItems(false);\n        dismissHistoryDropdown();\n      }\n    };\n\n    const handleHistoryCopyAll = () => {\n      clearHistoryHoverPreviews();\n      const currentHistoryItems = historyItems();\n      if (currentHistoryItems.length === 0) return;\n\n      const combinedContent = joinSnippets(\n        currentHistoryItems.map((historyItem) => historyItem.content),\n      );\n\n      const firstItem = currentHistoryItems[0];\n      copyContent(combinedContent, {\n        componentName: firstItem.componentName ?? firstItem.tagName,\n        entries: currentHistoryItems.map((historyItem) => ({\n          tagName: historyItem.tagName,\n          componentName: historyItem.componentName ?? historyItem.elementName,\n          content: historyItem.content,\n          commentText: historyItem.commentText,\n        })),\n      });\n\n      showClearPrompt();\n\n      clearAllLabels();\n\n      // HACK: defer to next frame so idle preview labels clear visually before \"copied\" appears\n      nativeRequestAnimationFrame(() => {\n        batch(() => {\n          for (const historyItem of currentHistoryItems) {\n            const connectedElements = getConnectedHistoryElements(historyItem);\n            for (const element of connectedElements) {\n              const bounds = createElementBounds(element);\n              const labelId = generateId(\"label\");\n\n              actions.addLabelInstance({\n                id: labelId,\n                bounds,\n                tagName: historyItem.tagName,\n                componentName: historyItem.componentName,\n                status: \"copied\",\n                createdAt: Date.now(),\n                element,\n                mouseX: bounds.x + bounds.width / 2,\n              });\n              scheduleLabelFade(labelId);\n            }\n          }\n        });\n      });\n    };\n\n    const handleHistoryItemHover = (historyItemId: string | null) => {\n      clearHistoryHoverPreviews();\n      if (!historyItemId) return;\n\n      const item = historyItems().find(\n        (innerItem) => innerItem.id === historyItemId,\n      );\n      if (!item) return;\n      showHistoryItemPreview(item, \"history-hover\");\n    };\n\n    const handleHistoryButtonHover = (isHovered: boolean) => {\n      cancelHistoryHoverOpenTimeout();\n      clearHistoryHoverPreviews();\n      if (isHovered) {\n        cancelHistoryHoverCloseTimeout();\n        if (\n          historyDropdownPosition() === null &&\n          clearPromptPosition() === null\n        ) {\n          showAllHistoryItemPreviews();\n          historyHoverOpenTimeoutId = setTimeout(() => {\n            historyHoverOpenTimeoutId = null;\n            setIsHistoryHoverOpen(true);\n            openHistoryDropdown();\n          }, DROPDOWN_HOVER_OPEN_DELAY_MS);\n        }\n      } else if (isHistoryHoverOpen()) {\n        scheduleHistoryHoverClose();\n      }\n    };\n\n    const handleHistoryDropdownHover = (isHovered: boolean) => {\n      if (isHovered) {\n        cancelHistoryHoverCloseTimeout();\n      } else if (isHistoryHoverOpen()) {\n        scheduleHistoryHoverClose();\n      }\n    };\n\n    const handleHistoryCopyAllHover = (isHovered: boolean) => {\n      clearHistoryHoverPreviews();\n      if (isHovered) {\n        cancelHistoryHoverCloseTimeout();\n        showAllHistoryItemPreviews();\n      } else if (isHistoryHoverOpen()) {\n        scheduleHistoryHoverClose();\n      }\n    };\n\n    const showAllHistoryItemPreviews = () => {\n      for (const item of historyItems()) {\n        showHistoryItemPreview(item, \"history-all-hover\");\n      }\n    };\n\n    const handleHistoryClear = () => {\n      historyElementMap.clear();\n      const updatedHistoryItems = clearHistory();\n      setHistoryItems(updatedHistoryItems);\n      setHasUnreadHistoryItems(false);\n      dismissHistoryDropdown();\n    };\n\n    const handleShowContextMenuInstance = (instanceId: string) => {\n      const instance = store.labelInstances.find(\n        (labelInstance) => labelInstance.id === instanceId,\n      );\n      if (!instance?.element) return;\n      if (!isElementConnected(instance.element)) return;\n\n      const elementBounds = createElementBounds(instance.element);\n      const center = getBoundsCenter(elementBounds);\n      const position = {\n        x: instance.mouseX ?? center.x,\n        y: center.y,\n      };\n\n      const elementsToFreeze =\n        instance.elements && instance.elements.length > 0\n          ? instance.elements.filter((element) => isElementConnected(element))\n          : [instance.element];\n\n      // HACK: Defer context menu display to avoid event interference\n      setTimeout(() => {\n        if (!isActivated()) {\n          actions.setWasActivatedByToggle(true);\n          activateRenderer();\n        }\n        actions.setPointer(position);\n        actions.setFrozenElements(elementsToFreeze);\n        const hasMultipleElements = elementsToFreeze.length > 1;\n        if (hasMultipleElements && instance.bounds) {\n          actions.setFrozenDragRect(createPageRectFromBounds(instance.bounds));\n        }\n        actions.freeze();\n        actions.showContextMenu(position, instance.element!);\n      }, 0);\n    };\n\n    createEffect(() => {\n      const hue = pluginRegistry.store.theme.hue;\n      if (hue !== 0) {\n        rendererRoot.style.filter = `hue-rotate(${hue}deg)`;\n      } else {\n        rendererRoot.style.filter = \"\";\n      }\n    });\n\n    if (pluginRegistry.store.theme.enabled) {\n      // HACK: Dynamically imported to avoid solid-js/web's delegateEvents() running\n      // at module evaluation time, which crashes during SSR (window is not defined).\n      void import(\"../components/renderer.js\")\n        .then(({ ReactGrabRenderer }) => {\n          if (disposed) return;\n          disposeRenderer = render(() => {\n            return (\n              <ReactGrabRenderer\n                selectionVisible={selectionVisible()}\n                selectionBounds={selectionBounds()}\n                selectionBoundsMultiple={selectionBoundsMultiple()}\n                selectionShouldSnap={\n                  store.frozenElements.length > 0 ||\n                  dragPreviewBounds().length > 0\n                }\n                selectionElementsCount={store.frozenElements.length}\n                selectionFilePath={store.selectionFilePath ?? undefined}\n                selectionLineNumber={store.selectionLineNumber ?? undefined}\n                selectionTagName={selectionTagName()}\n                selectionComponentName={resolvedComponentName()}\n                selectionLabelVisible={selectionLabelVisible()}\n                selectionLabelStatus=\"idle\"\n                selectionActionCycleState={actionCycleState()}\n                selectionArrowNavigationState={arrowNavigationState()}\n                onArrowNavigationSelect={handleArrowNavigationSelect}\n                labelInstances={computedLabelInstances()}\n                dragVisible={dragVisible()}\n                dragBounds={dragBounds()}\n                grabbedBoxes={computedGrabbedBoxes()}\n                mouseX={\n                  store.frozenElements.length > 1\n                    ? undefined\n                    : cursorPosition().x\n                }\n                isFrozen={\n                  isFrozenPhase() || isActivated() || isToolbarSelectHovered()\n                }\n                inputValue={store.inputText}\n                isPromptMode={isPromptMode()}\n                hasAgent={store.hasAgentProvider}\n                agentSessions={agentManager.sessions()}\n                supportsUndo={store.supportsUndo}\n                supportsFollowUp={store.supportsFollowUp}\n                dismissButtonText={store.dismissButtonText}\n                onDismissSession={agentManager.session.dismiss}\n                onUndoSession={agentManager.session.undo}\n                onFollowUpSubmitSession={handleFollowUpSubmit}\n                onAcknowledgeSessionError={handleAcknowledgeError}\n                onRetrySession={agentManager.session.retry}\n                onShowContextMenuInstance={handleShowContextMenuInstance}\n                onLabelInstanceHoverChange={handleLabelInstanceHoverChange}\n                onInputChange={actions.setInputText}\n                onInputSubmit={() => void handleInputSubmit()}\n                onToggleExpand={handleToggleExpand}\n                isPendingDismiss={isPendingDismiss()}\n                selectionLabelShakeCount={selectionLabelShakeCount()}\n                onConfirmDismiss={handleConfirmDismiss}\n                onCancelDismiss={handleCancelDismiss}\n                pendingAbortSessionId={store.pendingAbortSessionId}\n                onRequestAbortSession={(sessionId) =>\n                  actions.setPendingAbortSessionId(sessionId)\n                }\n                onAbortSession={handleAgentAbort}\n                toolbarVisible={pluginRegistry.store.theme.toolbar.enabled}\n                isActive={isActivated()}\n                onToggleActive={handleToggleActive}\n                enabled={isEnabled()}\n                onToggleEnabled={handleToggleEnabled}\n                shakeCount={toolbarShakeCount()}\n                onToolbarStateChange={(state) => {\n                  setCurrentToolbarState(state);\n                  toolbarStateChangeCallbacks.forEach((callback) =>\n                    callback(state),\n                  );\n                }}\n                onSubscribeToToolbarStateChanges={(callback) => {\n                  toolbarStateChangeCallbacks.add(callback);\n                  return () => {\n                    toolbarStateChangeCallbacks.delete(callback);\n                  };\n                }}\n                onToolbarSelectHoverChange={setIsToolbarSelectHovered}\n                onToolbarRef={(element) => {\n                  toolbarElement = element;\n                }}\n                contextMenuPosition={contextMenuPosition()}\n                contextMenuBounds={contextMenuBounds()}\n                contextMenuTagName={contextMenuTagName()}\n                contextMenuComponentName={contextMenuComponentName()}\n                contextMenuHasFilePath={Boolean(\n                  contextMenuFilePath()?.filePath,\n                )}\n                actions={pluginRegistry.store.actions}\n                toolbarActions={pluginRegistry.store.toolbarActions}\n                actionContext={contextMenuActionContext()}\n                onContextMenuDismiss={handleContextMenuDismiss}\n                onContextMenuHide={deferHideContextMenu}\n                historyItems={historyItems()}\n                historyDisconnectedItemIds={historyDisconnectedItemIds()}\n                historyItemCount={historyItems().length}\n                clockFlashTrigger={clockFlashTrigger()}\n                hasUnreadHistoryItems={hasUnreadHistoryItems()}\n                historyDropdownPosition={historyDropdownPosition()}\n                isHistoryPinned={\n                  historyDropdownPosition() !== null && !isHistoryHoverOpen()\n                }\n                onToggleHistory={handleToggleHistory}\n                onCopyAll={handleHistoryCopyAll}\n                onCopyAllHover={handleHistoryCopyAllHover}\n                onHistoryButtonHover={handleHistoryButtonHover}\n                onHistoryItemSelect={handleHistoryItemSelect}\n                onHistoryItemRemove={handleHistoryItemRemove}\n                onHistoryItemCopy={copyHistoryItemContent}\n                onHistoryItemHover={handleHistoryItemHover}\n                onHistoryCopyAll={handleHistoryCopyAll}\n                onHistoryCopyAllHover={handleHistoryCopyAllHover}\n                onHistoryClear={handleHistoryClear}\n                onHistoryDismiss={dismissHistoryDropdown}\n                onHistoryDropdownHover={handleHistoryDropdownHover}\n                toolbarMenuPosition={toolbarMenuPosition()}\n                onToggleMenu={handleToggleMenu}\n                onToolbarMenuDismiss={dismissToolbarMenu}\n                clearPromptPosition={clearPromptPosition()}\n                onClearHistoryConfirm={() => {\n                  dismissClearPrompt();\n                  handleHistoryClear();\n                }}\n                onClearHistoryCancel={dismissClearPrompt}\n              />\n            );\n          }, rendererRoot);\n        })\n        .catch((error) => {\n          console.warn(\"[react-grab] Failed to load renderer:\", error);\n        });\n    }\n\n    if (store.hasAgentProvider) {\n      agentManager.session.tryResume();\n    }\n\n    const copyElementAPI = async (\n      elements: Element | Element[],\n    ): Promise<boolean> => {\n      const elementsArray = Array.isArray(elements) ? elements : [elements];\n      if (elementsArray.length === 0) return false;\n      return await copyWithFallback(elementsArray);\n    };\n\n    const syncAgentFromRegistry = () => {\n      const agentOpts = getAgentOptionsWithCallbacks();\n      if (agentOpts) {\n        agentManager._internal.setOptions(agentOpts);\n      }\n      const hasProvider = Boolean(agentOpts?.provider);\n      actions.setHasAgentProvider(hasProvider);\n      if (hasProvider && agentOpts?.provider) {\n        const capturedProvider = agentOpts.provider;\n        actions.setAgentCapabilities({\n          supportsUndo: Boolean(capturedProvider.undo),\n          supportsFollowUp: Boolean(capturedProvider.supportsFollowUp),\n          dismissButtonText: capturedProvider.dismissButtonText,\n          isAgentConnected: false,\n        });\n\n        if (capturedProvider.checkConnection) {\n          capturedProvider\n            .checkConnection()\n            .then((isConnected) => {\n              const currentAgentOpts = getAgentOptionsWithCallbacks();\n              if (currentAgentOpts?.provider !== capturedProvider) {\n                return;\n              }\n              actions.setAgentCapabilities({\n                supportsUndo: Boolean(capturedProvider.undo),\n                supportsFollowUp: Boolean(capturedProvider.supportsFollowUp),\n                dismissButtonText: capturedProvider.dismissButtonText,\n                isAgentConnected: isConnected,\n              });\n            })\n            .catch((error) => {\n              logRecoverableError(\"Agent connection check failed\", error);\n            });\n        }\n\n        agentManager.session.tryResume();\n      } else {\n        actions.setAgentCapabilities({\n          supportsUndo: false,\n          supportsFollowUp: false,\n          dismissButtonText: undefined,\n          isAgentConnected: false,\n        });\n      }\n    };\n\n    const api: ReactGrabAPI = {\n      activate: () => {\n        actions.setPendingCommentMode(false);\n        if (!isActivated() && isEnabled()) {\n          toggleActivate();\n        }\n      },\n      deactivate: () => {\n        if (isActivated() || isCopying()) {\n          deactivateRenderer();\n        }\n      },\n      toggle: () => {\n        if (isActivated()) {\n          deactivateRenderer();\n        } else if (isEnabled()) {\n          toggleActivate();\n        }\n      },\n      comment: handleComment,\n      isActive: () => isActivated(),\n      isEnabled: () => isEnabled(),\n      setEnabled: (enabled: boolean) => {\n        if (enabled === isEnabled()) return;\n        setIsEnabled(enabled);\n        if (!enabled) {\n          forceDeactivateAll();\n        }\n      },\n      getToolbarState: () => loadToolbarState(),\n      setToolbarState: (state: Partial<ToolbarState>) => {\n        const currentState = loadToolbarState();\n        const newState = {\n          edge: state.edge ?? currentState?.edge ?? \"bottom\",\n          ratio:\n            state.ratio ??\n            currentState?.ratio ??\n            TOOLBAR_DEFAULT_POSITION_RATIO,\n          collapsed: state.collapsed ?? currentState?.collapsed ?? false,\n          enabled: state.enabled ?? currentState?.enabled ?? true,\n        };\n        saveToolbarState(newState);\n        setCurrentToolbarState(newState);\n        if (state.enabled !== undefined && state.enabled !== isEnabled()) {\n          setIsEnabled(state.enabled);\n        }\n        toolbarStateChangeCallbacks.forEach((callback) => callback(newState));\n      },\n      onToolbarStateChange: (callback: (state: ToolbarState) => void) => {\n        toolbarStateChangeCallbacks.add(callback);\n        return () => {\n          toolbarStateChangeCallbacks.delete(callback);\n        };\n      },\n      dispose: () => {\n        disposed = true;\n        hasInited = false;\n        disposeRenderer?.();\n        cancelHistoryHoverOpenTimeout();\n        cancelHistoryHoverCloseTimeout();\n        stopTrackingDropdownPosition();\n        toolbarStateChangeCallbacks.clear();\n        dispose();\n      },\n      copyElement: copyElementAPI,\n      getSource: async (element: Element): Promise<SourceInfo | null> => {\n        const source = await resolveSource(element);\n        if (!source) return null;\n        return {\n          filePath: source.filePath,\n          lineNumber: source.lineNumber,\n          componentName: source.componentName,\n        };\n      },\n      getStackContext,\n      getState: (): ReactGrabState => ({\n        isActive: isActivated(),\n        isDragging: isDragging(),\n        isCopying: isCopying(),\n        isPromptMode: isPromptMode(),\n        isSelectionBoxVisible: Boolean(selectionVisible()),\n        isDragBoxVisible: Boolean(dragVisible()),\n        targetElement: targetElement(),\n        dragBounds: dragBounds() ?? null,\n        grabbedBoxes: [...publicGrabbedBoxes()],\n        labelInstances: [...publicLabelInstances()],\n        selectionFilePath: store.selectionFilePath,\n        toolbarState: currentToolbarState(),\n      }),\n      setOptions: (newOptions: SettableOptions) => {\n        pluginRegistry.setOptions(newOptions);\n      },\n      registerPlugin: (plugin: Plugin) => {\n        pluginRegistry.register(plugin, api);\n        syncAgentFromRegistry();\n      },\n      unregisterPlugin: (name: string) => {\n        pluginRegistry.unregister(name);\n        syncAgentFromRegistry();\n      },\n      getPlugins: () => pluginRegistry.getPluginNames(),\n      getDisplayName: getComponentDisplayName,\n    };\n\n    for (const plugin of builtInPlugins) {\n      pluginRegistry.register(plugin, api);\n    }\n\n    // HACK: Force revalidation of Next.js project detection\n    // since it's cached in the browser and not updated when the project is changed\n    setTimeout(() => {\n      checkIsNextProject(true);\n    }, NEXTJS_REVALIDATION_DELAY_MS);\n\n    return api;\n  });\n};\n\nexport { getStack, getElementContext as formatElementInfo } from \"./context.js\";\nexport { isInstrumentationActive } from \"bippy\";\nexport { DEFAULT_THEME } from \"./theme.js\";\n\nexport type {\n  Options,\n  OverlayBounds,\n  ReactGrabRendererProps,\n  ReactGrabAPI,\n  SourceInfo,\n  AgentContext,\n  AgentSession,\n  AgentSessionStorage,\n  AgentProvider,\n  AgentCompleteResult,\n  AgentOptions,\n  SettableOptions,\n  ContextMenuAction,\n  ActionContext,\n  Plugin,\n  PluginConfig,\n  PluginHooks,\n} from \"../types.js\";\n\nexport { generateSnippet } from \"../utils/generate-snippet.js\";\nexport { copyContent } from \"../utils/copy-content.js\";\n"
  },
  {
    "path": "packages/react-grab/src/core/keyboard-handlers.ts",
    "content": "import type { Options } from \"../types.js\";\nimport { getModifiersFromActivationKey } from \"../utils/parse-activation-key.js\";\n\ninterface ModifierKeys {\n  metaKey: boolean;\n  ctrlKey: boolean;\n  shiftKey: boolean;\n  altKey: boolean;\n}\n\nexport const getRequiredModifiers = (options: Options): ModifierKeys => {\n  const { metaKey, ctrlKey, shiftKey, altKey } = getModifiersFromActivationKey(\n    options.activationKey,\n  );\n  return { metaKey, ctrlKey, shiftKey, altKey };\n};\n\ninterface PatchableGetter {\n  (this: KeyboardEvent): string;\n  __reactGrabPatched?: boolean;\n}\n\ninterface KeyDescriptor extends PropertyDescriptor {\n  get?: PatchableGetter;\n}\n\ninterface KeyboardEventClaimer {\n  claimedEvents: WeakSet<KeyboardEvent>;\n  originalKeyDescriptor: KeyDescriptor | undefined;\n  didPatch: boolean;\n  restore: () => void;\n}\n\nexport const setupKeyboardEventClaimer = (): KeyboardEventClaimer => {\n  const claimedEvents = new WeakSet<KeyboardEvent>();\n\n  const originalKeyDescriptor = Object.getOwnPropertyDescriptor(\n    KeyboardEvent.prototype,\n    \"key\",\n  ) as KeyDescriptor | undefined;\n\n  let didPatch = false;\n  if (\n    originalKeyDescriptor?.get &&\n    !originalKeyDescriptor.get.__reactGrabPatched\n  ) {\n    didPatch = true;\n    const originalGetter = originalKeyDescriptor.get;\n    const patchedGetter: PatchableGetter = function (this: KeyboardEvent) {\n      if (claimedEvents.has(this)) {\n        return \"\";\n      }\n      return originalGetter.call(this);\n    };\n    patchedGetter.__reactGrabPatched = true;\n    Object.defineProperty(KeyboardEvent.prototype, \"key\", {\n      get: patchedGetter,\n      configurable: true,\n    });\n  }\n\n  const restore = () => {\n    if (didPatch && originalKeyDescriptor) {\n      Object.defineProperty(\n        KeyboardEvent.prototype,\n        \"key\",\n        originalKeyDescriptor,\n      );\n    }\n  };\n\n  return {\n    claimedEvents,\n    originalKeyDescriptor,\n    didPatch,\n    restore,\n  };\n};\n"
  },
  {
    "path": "packages/react-grab/src/core/log-intro.ts",
    "content": "import { LOGO_SVG } from \"./logo-svg.js\";\nimport { isExtensionContext } from \"../utils/is-extension-context.js\";\n\nexport const logIntro = () => {\n  try {\n    const version = process.env.VERSION;\n    const logoDataUri = `data:image/svg+xml;base64,${btoa(LOGO_SVG)}`;\n    console.log(\n      `%cReact Grab${version ? ` v${version}` : \"\"}%c\\nhttps://react-grab.com`,\n      `background: #330039; color: #ffffff; border: 1px solid #d75fcb; padding: 4px 4px 4px 24px; border-radius: 4px; background-image: url(\"${logoDataUri}\"); background-size: 16px 16px; background-repeat: no-repeat; background-position: 4px center; display: inline-block; margin-bottom: 4px;`,\n      \"\",\n    );\n    if (navigator.onLine && version && !isExtensionContext()) {\n      fetch(\n        `https://www.react-grab.com/api/version?source=browser&t=${Date.now()}`,\n        {\n          referrerPolicy: \"origin\",\n          keepalive: true,\n          priority: \"low\",\n          cache: \"no-store\",\n        } as RequestInit,\n      )\n        .then((response) => response.text())\n        .then((latestVersion) => {\n          if (latestVersion && latestVersion !== version) {\n            console.warn(\n              `[React Grab] v${version} is outdated (latest: v${latestVersion})`,\n            );\n          }\n        })\n        .catch(() => null);\n    }\n    // HACK: Entire intro log is best-effort; never block initialization\n  } catch {}\n};\n"
  },
  {
    "path": "packages/react-grab/src/core/logo-svg.ts",
    "content": "export const LOGO_SVG = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 294 294\"><path fill=\"#ff40e0\" d=\"M145 47c25-20 50-27 67-17 16 9 23 30 20 60 0 2-1 5-1 7l-2 13h-1l-12-4c-8-3-17-5-25-6l-17-3c-10-1-20-1-29-1-10 0-20 0-29 1-6 8-11 16-16 24-5 9-9 17-13 26 4 9 8 18 13 26s10 16 16 24l11 14c5 7 11 13 18 19l10 8s-1 0 0 0l-11 9c-17 14-35 22-49 22-6 0-13-2-18-5-16-9-23-30-20-59 1-3 1-5 1-8-30-12-48-29-48-50 0-18 14-35 41-47 2-1 5-2 7-3 0-2 0-5-1-7-3-30 4-51 20-60 18-10 42-3 68 17M71 201c-1 2-1 4-1 5-2 24 3 41 13 47h1c11 7 30 1 51-15-10-9-18-18-26-29-13-1-26-4-38-8m9-38c-3 9-5 17-7 26 8 2 17 4 25 6-3-5-6-10-9-16-3-5-6-10-9-16m-19-53c-2 1-3 1-5 2-21 10-34 23-34 35 0 13 14 27 39 37 3-12 7-25 12-37-5-12-9-24-12-37m37-10c-8 1-17 3-25 6 2 8 4 16 7 25 3-5 6-11 9-16zm-3-61c-4-1-8 0-12 2-10 7-15 24-13 47 0 2 0 3 1 5 12-4 25-6 38-8 8-10 16-20 26-29-15-11-29-17-40-17m111 2c-4-2-8-3-12-2-11 0-25 5-40 17 10 9 19 19 26 29 13 2 26 4 39 8v-5c3-23-2-40-13-47m-61 23c-7 6-13 13-19 19h37c-6-6-12-13-18-19\"/><mask id=\"a\"><path fill=\"#fff\" d=\"m235 85-133 27 28 133 133-27Z\"/></mask><path fill=\"#ff40e0\" d=\"m137 130 76 11c8 1 9 11 3 15l-28 16-4 32c-1 8-10 10-14 4l-41-66c-3-6 1-13 8-12\" mask=\"url(#a)\"/></svg>`;\n"
  },
  {
    "path": "packages/react-grab/src/core/noop-api.ts",
    "content": "import type { ReactGrabAPI } from \"../types.js\";\n\nconst NOOP = () => {};\n\nexport const createNoopApi = (): ReactGrabAPI => ({\n  activate: NOOP,\n  deactivate: NOOP,\n  toggle: NOOP,\n  comment: NOOP,\n  isActive: () => false,\n  isEnabled: () => false,\n  setEnabled: NOOP,\n  getToolbarState: () => null,\n  setToolbarState: NOOP,\n  onToolbarStateChange: () => NOOP,\n  dispose: NOOP,\n  copyElement: () => Promise.resolve(false),\n  getSource: () => Promise.resolve(null),\n  getStackContext: () => Promise.resolve(\"\"),\n  getState: () => ({\n    isActive: false,\n    isDragging: false,\n    isCopying: false,\n    isPromptMode: false,\n    isSelectionBoxVisible: false,\n    isDragBoxVisible: false,\n    targetElement: null,\n    dragBounds: null,\n    grabbedBoxes: [],\n    labelInstances: [],\n    selectionFilePath: null,\n    toolbarState: null,\n  }),\n  setOptions: NOOP,\n  registerPlugin: NOOP,\n  unregisterPlugin: NOOP,\n  getPlugins: () => [],\n  getDisplayName: () => null,\n});\n"
  },
  {
    "path": "packages/react-grab/src/core/plugin-registry.ts",
    "content": "import { createStore } from \"solid-js/store\";\nimport type {\n  Position,\n  Plugin,\n  PluginConfig,\n  PluginHooks,\n  Theme,\n  PluginAction,\n  ContextMenuAction,\n  ToolbarMenuAction,\n  ReactGrabAPI,\n  ReactGrabState,\n  PromptModeContext,\n  OverlayBounds,\n  DragRect,\n  ElementLabelVariant,\n  ElementLabelContext,\n  ActivationMode,\n  ActivationKey,\n  SettableOptions,\n  AgentContext,\n  ActionContext,\n} from \"../types.js\";\nimport { DEFAULT_THEME, deepMergeTheme } from \"./theme.js\";\nimport {\n  DEFAULT_KEY_HOLD_DURATION_MS,\n  DEFAULT_MAX_CONTEXT_LINES,\n} from \"../constants.js\";\n\ninterface RegisteredPlugin {\n  plugin: Plugin;\n  config: PluginConfig;\n}\n\ninterface OptionsState {\n  activationMode: ActivationMode;\n  keyHoldDuration: number;\n  allowActivationInsideInput: boolean;\n  maxContextLines: number;\n  activationKey: ActivationKey | undefined;\n  getContent: ((elements: Element[]) => Promise<string> | string) | undefined;\n  freezeReactUpdates: boolean;\n}\n\nconst DEFAULT_OPTIONS: OptionsState = {\n  activationMode: \"toggle\",\n  keyHoldDuration: DEFAULT_KEY_HOLD_DURATION_MS,\n  allowActivationInsideInput: true,\n  maxContextLines: DEFAULT_MAX_CONTEXT_LINES,\n  activationKey: undefined,\n  getContent: undefined,\n  freezeReactUpdates: true,\n};\n\ninterface PluginStoreState {\n  theme: Required<Theme>;\n  options: OptionsState;\n  actions: ContextMenuAction[];\n  toolbarActions: ToolbarMenuAction[];\n}\n\ntype HookName = keyof PluginHooks;\n\nconst createPluginRegistry = (initialOptions: SettableOptions = {}) => {\n  const plugins = new Map<string, RegisteredPlugin>();\n  const directOptionOverrides: Partial<OptionsState> = {};\n\n  const [store, setStore] = createStore<PluginStoreState>({\n    theme: DEFAULT_THEME,\n    options: { ...DEFAULT_OPTIONS, ...initialOptions },\n    actions: [],\n    toolbarActions: [],\n  });\n\n  const isToolbarAction = (action: PluginAction): action is ToolbarMenuAction =>\n    action.target === \"toolbar\";\n\n  const recomputeStore = () => {\n    let mergedTheme: Required<Theme> = DEFAULT_THEME;\n    let mergedOptions: OptionsState = { ...DEFAULT_OPTIONS, ...initialOptions };\n    const allContextMenuActions: ContextMenuAction[] = [];\n    const allToolbarActions: ToolbarMenuAction[] = [];\n\n    for (const { config } of plugins.values()) {\n      if (config.theme) {\n        mergedTheme = deepMergeTheme(mergedTheme, config.theme);\n      }\n\n      if (config.options) {\n        mergedOptions = { ...mergedOptions, ...config.options };\n      }\n\n      if (config.actions) {\n        for (const action of config.actions) {\n          if (isToolbarAction(action)) {\n            const originalOnAction = action.onAction;\n            allToolbarActions.push({\n              ...action,\n              onAction: () => {\n                callHook(\"cancelPendingToolbarActions\");\n                originalOnAction();\n              },\n            });\n          } else {\n            allContextMenuActions.push(action);\n          }\n        }\n      }\n    }\n\n    mergedOptions = { ...mergedOptions, ...directOptionOverrides };\n\n    setStore(\"theme\", mergedTheme);\n    setStore(\"options\", mergedOptions);\n    setStore(\"actions\", allContextMenuActions);\n    setStore(\"toolbarActions\", allToolbarActions);\n  };\n\n  const setOption = <OptionKey extends keyof OptionsState>(\n    optionKey: OptionKey,\n    optionValue: OptionsState[OptionKey],\n  ) => {\n    directOptionOverrides[optionKey] = optionValue;\n    setStore(\"options\", optionKey, optionValue);\n  };\n\n  const SETTABLE_OPTION_KEYS: Array<keyof OptionsState> = [\n    \"activationMode\",\n    \"keyHoldDuration\",\n    \"allowActivationInsideInput\",\n    \"maxContextLines\",\n    \"activationKey\",\n    \"getContent\",\n    \"freezeReactUpdates\",\n  ];\n\n  const setOptions = (optionUpdates: SettableOptions) => {\n    for (const optionKey of SETTABLE_OPTION_KEYS) {\n      if (optionUpdates[optionKey] !== undefined) {\n        setOption(optionKey, optionUpdates[optionKey]!);\n      }\n    }\n  };\n\n  const register = (plugin: Plugin, api: ReactGrabAPI) => {\n    if (plugins.has(plugin.name)) {\n      unregister(plugin.name);\n    }\n\n    const config: PluginConfig = plugin.setup?.(api, hooks) ?? {};\n\n    if (plugin.theme) {\n      config.theme = config.theme\n        ? deepMergeTheme(\n            deepMergeTheme(DEFAULT_THEME, plugin.theme),\n            config.theme,\n          )\n        : plugin.theme;\n    }\n\n    if (plugin.actions) {\n      config.actions = [...plugin.actions, ...(config.actions ?? [])];\n    }\n\n    if (plugin.hooks) {\n      config.hooks = config.hooks\n        ? { ...plugin.hooks, ...config.hooks }\n        : plugin.hooks;\n    }\n\n    if (plugin.options) {\n      config.options = config.options\n        ? { ...plugin.options, ...config.options }\n        : plugin.options;\n    }\n\n    plugins.set(plugin.name, { plugin, config });\n    recomputeStore();\n    return config;\n  };\n\n  const unregister = (name: string) => {\n    const registered = plugins.get(name);\n    if (!registered) return;\n\n    if (registered.config.cleanup) {\n      registered.config.cleanup();\n    }\n\n    plugins.delete(name);\n    recomputeStore();\n  };\n\n  const getPluginNames = (): string[] => {\n    return Array.from(plugins.keys());\n  };\n\n  const callHook = <K extends HookName>(\n    hookName: K,\n    ...args: Parameters<NonNullable<PluginHooks[K]>>\n  ): void => {\n    for (const { config } of plugins.values()) {\n      const hook = config.hooks?.[hookName] as\n        | ((...hookArgs: Parameters<NonNullable<PluginHooks[K]>>) => void)\n        | undefined;\n      if (hook) {\n        hook(...args);\n      }\n    }\n  };\n\n  const callHookWithHandled = <K extends HookName>(\n    hookName: K,\n    ...args: Parameters<NonNullable<PluginHooks[K]>>\n  ): boolean => {\n    let handled = false;\n    for (const { config } of plugins.values()) {\n      const hook = config.hooks?.[hookName] as\n        | ((\n            ...hookArgs: Parameters<NonNullable<PluginHooks[K]>>\n          ) => boolean | void)\n        | undefined;\n      if (hook) {\n        const result = hook(...args);\n        if (result === true) {\n          handled = true;\n        }\n      }\n    }\n    return handled;\n  };\n\n  const callHookAsync = async <K extends HookName>(\n    hookName: K,\n    ...args: Parameters<NonNullable<PluginHooks[K]>>\n  ): Promise<void> => {\n    for (const { config } of plugins.values()) {\n      const hook = config.hooks?.[hookName] as\n        | ((\n            ...hookArgs: Parameters<NonNullable<PluginHooks[K]>>\n          ) => ReturnType<NonNullable<PluginHooks[K]>>)\n        | undefined;\n      if (hook) {\n        await hook(...args);\n      }\n    }\n  };\n\n  const callHookReduce = async <T>(\n    hookName: HookName,\n    initialValue: T,\n    ...extraArgs: unknown[]\n  ): Promise<T> => {\n    let result = initialValue;\n    for (const { config } of plugins.values()) {\n      const hook = config.hooks?.[hookName] as\n        | ((value: T, ...hookArgs: unknown[]) => T | Promise<T>)\n        | undefined;\n      if (hook) {\n        result = await hook(result, ...extraArgs);\n      }\n    }\n    return result;\n  };\n\n  const callHookReduceSync = <T>(\n    hookName: HookName,\n    initialValue: T,\n    ...extraArgs: unknown[]\n  ): T => {\n    let result = initialValue;\n    for (const { config } of plugins.values()) {\n      const hook = config.hooks?.[hookName] as\n        | ((value: T, ...hookArgs: unknown[]) => T)\n        | undefined;\n      if (hook) {\n        result = hook(result, ...extraArgs);\n      }\n    }\n    return result;\n  };\n\n  const hooks = {\n    onActivate: () => callHook(\"onActivate\"),\n    onDeactivate: () => callHook(\"onDeactivate\"),\n    onElementHover: (element: Element) => callHook(\"onElementHover\", element),\n    onElementSelect: (\n      element: Element,\n    ): { wasIntercepted: boolean; pendingResult?: Promise<boolean> } => {\n      let wasIntercepted = false;\n      let pendingResult: Promise<boolean> | undefined;\n      for (const { config } of plugins.values()) {\n        const hook = config.hooks?.onElementSelect;\n        if (hook) {\n          const result = hook(element);\n          if (result === true) {\n            wasIntercepted = true;\n          } else if (result instanceof Promise) {\n            wasIntercepted = true;\n            pendingResult = result;\n          }\n        }\n      }\n      return { wasIntercepted, pendingResult };\n    },\n    onDragStart: (startX: number, startY: number) =>\n      callHook(\"onDragStart\", startX, startY),\n    onDragEnd: (elements: Element[], bounds: DragRect) =>\n      callHook(\"onDragEnd\", elements, bounds),\n    onBeforeCopy: async (elements: Element[]) =>\n      callHookAsync(\"onBeforeCopy\", elements),\n    transformCopyContent: async (content: string, elements: Element[]) =>\n      callHookReduce(\"transformCopyContent\", content, elements),\n    onAfterCopy: (elements: Element[], success: boolean) =>\n      callHook(\"onAfterCopy\", elements, success),\n    onCopySuccess: (elements: Element[], content: string) =>\n      callHook(\"onCopySuccess\", elements, content),\n    onCopyError: (error: Error) => callHook(\"onCopyError\", error),\n    onStateChange: (state: ReactGrabState) => callHook(\"onStateChange\", state),\n    onPromptModeChange: (isPromptMode: boolean, context: PromptModeContext) =>\n      callHook(\"onPromptModeChange\", isPromptMode, context),\n    onSelectionBox: (\n      visible: boolean,\n      bounds: OverlayBounds | null,\n      element: Element | null,\n    ) => callHook(\"onSelectionBox\", visible, bounds, element),\n    onDragBox: (visible: boolean, bounds: OverlayBounds | null) =>\n      callHook(\"onDragBox\", visible, bounds),\n    onGrabbedBox: (bounds: OverlayBounds, element: Element) =>\n      callHook(\"onGrabbedBox\", bounds, element),\n    onElementLabel: (\n      visible: boolean,\n      variant: ElementLabelVariant,\n      context: ElementLabelContext,\n    ) => callHook(\"onElementLabel\", visible, variant, context),\n    onContextMenu: (element: Element, position: Position) =>\n      callHook(\"onContextMenu\", element, position),\n    cancelPendingToolbarActions: () => callHook(\"cancelPendingToolbarActions\"),\n    onOpenFile: (filePath: string, lineNumber?: number) =>\n      callHookWithHandled(\"onOpenFile\", filePath, lineNumber),\n    transformHtmlContent: async (html: string, elements: Element[]) =>\n      callHookReduce(\"transformHtmlContent\", html, elements),\n    transformAgentContext: async (context: AgentContext, elements: Element[]) =>\n      callHookReduce(\"transformAgentContext\", context, elements),\n    transformActionContext: (context: ActionContext) =>\n      callHookReduceSync(\"transformActionContext\", context),\n    transformOpenFileUrl: (\n      url: string,\n      filePath: string,\n      lineNumber?: number,\n    ) => callHookReduceSync(\"transformOpenFileUrl\", url, filePath, lineNumber),\n    transformSnippet: async (snippet: string, element: Element) =>\n      callHookReduce(\"transformSnippet\", snippet, element),\n  };\n\n  return {\n    register,\n    unregister,\n    getPluginNames,\n    setOptions,\n    store,\n    hooks,\n  };\n};\n\nexport { createPluginRegistry };\n"
  },
  {
    "path": "packages/react-grab/src/core/plugins/comment.ts",
    "content": "import type { Plugin } from \"../../types.js\";\n\nexport const commentPlugin: Plugin = {\n  name: \"comment\",\n  setup: (api) => ({\n    actions: [\n      {\n        id: \"comment\",\n        label: \"Comment\",\n        shortcut: \"Enter\",\n        onAction: (context) => {\n          context.enterPromptMode?.();\n        },\n      },\n      {\n        id: \"comment-toolbar\",\n        label: \"Comment\",\n        shortcut: \"Enter\",\n        target: \"toolbar\",\n        onAction: () => {\n          api.comment();\n        },\n      },\n    ],\n  }),\n};\n"
  },
  {
    "path": "packages/react-grab/src/core/plugins/copy-html.ts",
    "content": "import { appendStackContext } from \"../../utils/append-stack-context.js\";\nimport { copyContent } from \"../../utils/copy-content.js\";\nimport { logRecoverableError } from \"../../utils/log-recoverable-error.js\";\nimport { createPendingSelectionPlugin } from \"./create-pending-selection-plugin.js\";\n\nexport const copyHtmlPlugin = createPendingSelectionPlugin({\n  name: \"copy-html\",\n  onPendingSelect: (element, api, hooks) => {\n    void Promise.all([\n      hooks.transformHtmlContent(element.outerHTML, [element]),\n      api.getStackContext(element),\n    ])\n      .then(([transformedHtml, stackContext]) => {\n        if (!transformedHtml) return;\n        copyContent(appendStackContext(transformedHtml, stackContext));\n      })\n      .catch((error) => {\n        logRecoverableError(\"Failed to copy HTML on element select\", error);\n      });\n  },\n  contextMenuAction: (api) => ({\n    id: \"copy-html\",\n    label: \"Copy HTML\",\n    onAction: async (context) => {\n      await context.performWithFeedback(async () => {\n        const combinedHtml = context.elements\n          .map((element) => element.outerHTML)\n          .join(\"\\n\\n\");\n\n        const transformedHtml = await context.hooks.transformHtmlContent(\n          combinedHtml,\n          context.elements,\n        );\n\n        if (!transformedHtml) return false;\n\n        const stackContext = await api.getStackContext(context.element);\n        return copyContent(appendStackContext(transformedHtml, stackContext), {\n          componentName: context.componentName,\n          tagName: context.tagName,\n        });\n      });\n    },\n  }),\n  toolbarAction: {\n    id: \"copy-html-toolbar\",\n    label: \"Copy HTML\",\n  },\n});\n"
  },
  {
    "path": "packages/react-grab/src/core/plugins/copy-styles.ts",
    "content": "import { appendStackContext } from \"../../utils/append-stack-context.js\";\nimport { copyContent } from \"../../utils/copy-content.js\";\nimport {\n  extractElementCss,\n  disposeBaselineStyles,\n} from \"../../utils/extract-element-css.js\";\nimport { logRecoverableError } from \"../../utils/log-recoverable-error.js\";\nimport { createPendingSelectionPlugin } from \"./create-pending-selection-plugin.js\";\n\nexport const copyStylesPlugin = createPendingSelectionPlugin({\n  name: \"copy-styles\",\n  onPendingSelect: (element, api) => {\n    const extractedCss = extractElementCss(element);\n    void api\n      .getStackContext(element)\n      .then((stackContext) => {\n        copyContent(appendStackContext(extractedCss, stackContext));\n      })\n      .catch((error) => {\n        logRecoverableError(\"Failed to copy styles on element select\", error);\n      });\n  },\n  contextMenuAction: (api) => ({\n    id: \"copy-styles\",\n    label: \"Copy styles\",\n    onAction: async (context) => {\n      await context.performWithFeedback(async () => {\n        const combinedCss = context.elements\n          .map(extractElementCss)\n          .join(\"\\n\\n\");\n\n        const stackContext = await api.getStackContext(context.element);\n        return copyContent(appendStackContext(combinedCss, stackContext), {\n          componentName: context.componentName,\n          tagName: context.tagName,\n        });\n      });\n    },\n  }),\n  toolbarAction: {\n    id: \"copy-styles-toolbar\",\n    label: \"Copy styles\",\n  },\n  cleanup: disposeBaselineStyles,\n});\n"
  },
  {
    "path": "packages/react-grab/src/core/plugins/copy.ts",
    "content": "import { createPendingSelectionPlugin } from \"./create-pending-selection-plugin.js\";\n\nexport const copyPlugin = createPendingSelectionPlugin({\n  name: \"copy\",\n  onPendingSelect: (element, api) => {\n    api.copyElement(element);\n  },\n  contextMenuAction: {\n    id: \"copy\",\n    label: \"Copy\",\n    shortcut: \"C\",\n    onAction: (context) => {\n      context.copy?.();\n    },\n  },\n  toolbarAction: {\n    id: \"copy-toolbar\",\n    label: \"Copy element\",\n    shortcut: \"C\",\n  },\n});\n"
  },
  {
    "path": "packages/react-grab/src/core/plugins/create-pending-selection-plugin.ts",
    "content": "import type {\n  Plugin,\n  ReactGrabAPI,\n  ActionContextHooks,\n  ContextMenuAction,\n} from \"../../types.js\";\n\ntype ContextMenuActionFactory =\n  | ContextMenuAction\n  | ((api: ReactGrabAPI, hooks: ActionContextHooks) => ContextMenuAction);\n\ninterface PendingSelectionPluginConfig {\n  name: string;\n  onPendingSelect: (\n    element: Element,\n    api: ReactGrabAPI,\n    hooks: ActionContextHooks,\n  ) => void;\n  contextMenuAction: ContextMenuActionFactory;\n  toolbarAction: { id: string; label: string; shortcut?: string };\n  cleanup?: () => void;\n}\n\nexport const createPendingSelectionPlugin = (\n  config: PendingSelectionPluginConfig,\n): Plugin => ({\n  name: config.name,\n  setup: (api, hooks) => {\n    let isPendingSelection = false;\n\n    const resolvedContextMenuAction =\n      typeof config.contextMenuAction === \"function\"\n        ? config.contextMenuAction(api, hooks)\n        : config.contextMenuAction;\n\n    return {\n      hooks: {\n        onElementSelect: (element) => {\n          if (!isPendingSelection) return;\n          isPendingSelection = false;\n          config.onPendingSelect(element, api, hooks);\n          return true;\n        },\n        onDeactivate: () => {\n          isPendingSelection = false;\n        },\n        cancelPendingToolbarActions: () => {\n          isPendingSelection = false;\n        },\n      },\n      actions: [\n        resolvedContextMenuAction,\n        {\n          id: config.toolbarAction.id,\n          label: config.toolbarAction.label,\n          shortcut: config.toolbarAction.shortcut,\n          target: \"toolbar\" as const,\n          onAction: () => {\n            isPendingSelection = true;\n            api.activate();\n          },\n        },\n      ],\n      cleanup: config.cleanup,\n    };\n  },\n});\n"
  },
  {
    "path": "packages/react-grab/src/core/plugins/open.ts",
    "content": "import type { Plugin } from \"../../types.js\";\nimport { openFile } from \"../../utils/open-file.js\";\n\nexport const openPlugin: Plugin = {\n  name: \"open\",\n  actions: [\n    {\n      id: \"open\",\n      label: \"Open\",\n      shortcut: \"O\",\n      enabled: (context) => Boolean(context.filePath),\n      onAction: (context) => {\n        if (!context.filePath) return;\n\n        const wasHandled = context.hooks.onOpenFile(\n          context.filePath,\n          context.lineNumber,\n        );\n\n        if (!wasHandled) {\n          openFile(\n            context.filePath,\n            context.lineNumber,\n            context.hooks.transformOpenFileUrl,\n          );\n        }\n\n        context.hideContextMenu();\n        context.cleanup();\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "packages/react-grab/src/core/store.ts",
    "content": "import { createStore, produce } from \"solid-js/store\";\nimport type {\n  Position,\n  Theme,\n  GrabbedBox,\n  SelectionLabelInstance,\n  AgentOptions,\n} from \"../types.js\";\nimport { OFFSCREEN_POSITION } from \"../constants.js\";\nimport { createElementBounds } from \"../utils/create-element-bounds.js\";\nimport { isElementConnected } from \"../utils/is-element-connected.js\";\n\ninterface PendingClickData {\n  clientX: number;\n  clientY: number;\n  element: Element;\n}\n\ninterface FrozenDragRect {\n  pageX: number;\n  pageY: number;\n  width: number;\n  height: number;\n}\n\ntype GrabPhase = \"hovering\" | \"frozen\" | \"dragging\" | \"justDragged\";\n\ntype GrabState =\n  | { state: \"idle\" }\n  | { state: \"holding\"; startedAt: number }\n  | {\n      state: \"active\";\n      phase: GrabPhase;\n      isPromptMode: boolean;\n      isPendingDismiss: boolean;\n    }\n  | { state: \"copying\"; startedAt: number; wasActive: boolean }\n  | { state: \"justCopied\"; copiedAt: number; wasActive: boolean };\n\ninterface GrabStore {\n  current: GrabState;\n\n  wasActivatedByToggle: boolean;\n  pendingCommentMode: boolean;\n  hasAgentProvider: boolean;\n  keyHoldDuration: number;\n\n  pointer: Position;\n  dragStart: Position;\n  copyStart: Position;\n  copyOffsetFromCenterX: number;\n\n  detectedElement: Element | null;\n  frozenElement: Element | null;\n  frozenElements: Element[];\n  frozenDragRect: FrozenDragRect | null;\n  lastGrabbedElement: Element | null;\n  lastCopiedElement: Element | null;\n\n  selectionFilePath: string | null;\n  selectionLineNumber: number | null;\n\n  inputText: string;\n  pendingClickData: PendingClickData | null;\n  replySessionId: string | null;\n\n  viewportVersion: number;\n  grabbedBoxes: GrabbedBox[];\n  labelInstances: SelectionLabelInstance[];\n\n  isTouchMode: boolean;\n\n  theme: Required<Theme>;\n\n  activationTimestamp: number | null;\n  previouslyFocusedElement: Element | null;\n\n  isAgentConnected: boolean;\n  supportsUndo: boolean;\n  supportsFollowUp: boolean;\n  dismissButtonText: string | undefined;\n  pendingAbortSessionId: string | null;\n\n  contextMenuPosition: Position | null;\n  contextMenuElement: Element | null;\n  contextMenuClickOffset: Position | null;\n\n  selectedAgent: AgentOptions | null;\n}\n\ninterface GrabStoreInput {\n  theme: Required<Theme>;\n  hasAgentProvider: boolean;\n  keyHoldDuration: number;\n}\n\nconst createInitialStore = (input: GrabStoreInput): GrabStore => ({\n  current: { state: \"idle\" },\n\n  wasActivatedByToggle: false,\n  pendingCommentMode: false,\n  hasAgentProvider: input.hasAgentProvider,\n  keyHoldDuration: input.keyHoldDuration,\n\n  pointer: { x: OFFSCREEN_POSITION, y: OFFSCREEN_POSITION },\n  dragStart: { x: OFFSCREEN_POSITION, y: OFFSCREEN_POSITION },\n  copyStart: { x: OFFSCREEN_POSITION, y: OFFSCREEN_POSITION },\n  copyOffsetFromCenterX: 0,\n\n  detectedElement: null,\n  frozenElement: null,\n  frozenElements: [],\n  frozenDragRect: null,\n  lastGrabbedElement: null,\n  lastCopiedElement: null,\n\n  selectionFilePath: null,\n  selectionLineNumber: null,\n\n  inputText: \"\",\n  pendingClickData: null,\n  replySessionId: null,\n\n  viewportVersion: 0,\n  grabbedBoxes: [],\n  labelInstances: [],\n\n  isTouchMode: false,\n\n  theme: input.theme,\n\n  activationTimestamp: null,\n  previouslyFocusedElement: null,\n\n  isAgentConnected: false,\n  supportsUndo: false,\n  supportsFollowUp: false,\n  dismissButtonText: undefined,\n  pendingAbortSessionId: null,\n\n  contextMenuPosition: null,\n  contextMenuElement: null,\n  contextMenuClickOffset: null,\n\n  selectedAgent: null,\n});\n\ninterface GrabActions {\n  startHold: (duration?: number) => void;\n  releaseHold: () => void;\n  activate: () => void;\n  deactivate: () => void;\n  toggle: () => void;\n  freeze: () => void;\n  unfreeze: () => void;\n  startDrag: (position: Position) => void;\n  endDrag: () => void;\n  cancelDrag: () => void;\n  finishJustDragged: () => void;\n  startCopy: () => void;\n  completeCopy: (element?: Element) => void;\n  finishJustCopied: () => void;\n  enterPromptMode: (position: Position, element: Element) => void;\n  exitPromptMode: () => void;\n  setInputText: (value: string) => void;\n  clearInputText: () => void;\n  setPendingDismiss: (value: boolean) => void;\n  setPointer: (position: Position) => void;\n  setDetectedElement: (element: Element | null) => void;\n  setFrozenElement: (element: Element) => void;\n  setFrozenElements: (elements: Element[]) => void;\n  setFrozenDragRect: (rect: FrozenDragRect | null) => void;\n  clearFrozenElement: () => void;\n  setCopyStart: (position: Position, element: Element) => void;\n  setLastGrabbed: (element: Element | null) => void;\n  clearLastCopied: () => void;\n  setWasActivatedByToggle: (value: boolean) => void;\n  setPendingCommentMode: (value: boolean) => void;\n  setTouchMode: (value: boolean) => void;\n  setSelectionSource: (\n    filePath: string | null,\n    lineNumber: number | null,\n  ) => void;\n  setPendingClickData: (data: PendingClickData | null) => void;\n  clearReplySessionId: () => void;\n  incrementViewportVersion: () => void;\n  addGrabbedBox: (box: GrabbedBox) => void;\n  removeGrabbedBox: (boxId: string) => void;\n  clearGrabbedBoxes: () => void;\n  addLabelInstance: (instance: SelectionLabelInstance) => void;\n  updateLabelInstance: (\n    instanceId: string,\n    status: SelectionLabelInstance[\"status\"],\n    errorMessage?: string,\n  ) => void;\n  removeLabelInstance: (instanceId: string) => void;\n  clearLabelInstances: () => void;\n  setHasAgentProvider: (value: boolean) => void;\n  setAgentCapabilities: (capabilities: {\n    supportsUndo: boolean;\n    supportsFollowUp: boolean;\n    dismissButtonText: string | undefined;\n    isAgentConnected: boolean;\n  }) => void;\n  setPendingAbortSessionId: (sessionId: string | null) => void;\n  showContextMenu: (position: Position, element: Element) => void;\n  hideContextMenu: () => void;\n  updateContextMenuPosition: () => void;\n  setSelectedAgent: (agent: AgentOptions | null) => void;\n}\n\nconst createGrabStore = (input: GrabStoreInput) => {\n  const [store, setStore] = createStore<GrabStore>(createInitialStore(input));\n\n  const setActivePhase = (phase: GrabPhase) => {\n    setStore(\n      \"current\",\n      produce((current) => {\n        if (current.state === \"active\") {\n          current.phase = phase;\n        }\n      }),\n    );\n  };\n\n  const actions: GrabActions = {\n    startHold: (duration?: number) => {\n      if (duration !== undefined) {\n        setStore(\"keyHoldDuration\", duration);\n      }\n      setStore(\"current\", { state: \"holding\", startedAt: Date.now() });\n    },\n\n    releaseHold: () => {\n      if (store.current.state === \"holding\") {\n        setStore(\"current\", { state: \"idle\" });\n      }\n    },\n\n    activate: () => {\n      setStore(\n        produce((draft) => {\n          draft.current = {\n            state: \"active\",\n            phase: \"hovering\",\n            isPromptMode: false,\n            isPendingDismiss: false,\n          };\n          draft.activationTimestamp = Date.now();\n          draft.previouslyFocusedElement = document.activeElement;\n        }),\n      );\n    },\n\n    deactivate: () => {\n      setStore(\n        produce((draft) => {\n          draft.current = { state: \"idle\" };\n          draft.wasActivatedByToggle = false;\n          draft.pendingCommentMode = false;\n          draft.inputText = \"\";\n          draft.frozenElement = null;\n          draft.frozenElements = [];\n          draft.frozenDragRect = null;\n          draft.pendingClickData = null;\n          draft.replySessionId = null;\n          draft.pendingAbortSessionId = null;\n          draft.activationTimestamp = null;\n          draft.previouslyFocusedElement = null;\n          draft.contextMenuPosition = null;\n          draft.contextMenuElement = null;\n          draft.contextMenuClickOffset = null;\n          draft.selectedAgent = null;\n          draft.lastCopiedElement = null;\n        }),\n      );\n    },\n\n    toggle: () => {\n      if (store.activationTimestamp !== null) {\n        actions.deactivate();\n      } else {\n        setStore(\"wasActivatedByToggle\", true);\n        actions.activate();\n      }\n    },\n\n    freeze: () => {\n      if (store.current.state === \"active\") {\n        const elementToFreeze = store.frozenElement ?? store.detectedElement;\n        if (elementToFreeze) {\n          setStore(\"frozenElement\", elementToFreeze);\n        }\n        setActivePhase(\"frozen\");\n      }\n    },\n\n    unfreeze: () => {\n      if (store.current.state === \"active\") {\n        setStore(\n          produce((draft) => {\n            draft.frozenElement = null;\n            draft.frozenElements = [];\n            draft.frozenDragRect = null;\n          }),\n        );\n        setActivePhase(\"hovering\");\n      }\n    },\n\n    startDrag: (position: Position) => {\n      if (store.current.state === \"active\") {\n        actions.clearFrozenElement();\n        setStore(\"dragStart\", {\n          x: position.x + window.scrollX,\n          y: position.y + window.scrollY,\n        });\n        setActivePhase(\"dragging\");\n      }\n    },\n\n    endDrag: () => {\n      if (\n        store.current.state === \"active\" &&\n        store.current.phase === \"dragging\"\n      ) {\n        setStore(\"dragStart\", { x: OFFSCREEN_POSITION, y: OFFSCREEN_POSITION });\n        setActivePhase(\"justDragged\");\n      }\n    },\n\n    cancelDrag: () => {\n      if (\n        store.current.state === \"active\" &&\n        store.current.phase === \"dragging\"\n      ) {\n        setStore(\"dragStart\", { x: OFFSCREEN_POSITION, y: OFFSCREEN_POSITION });\n        setActivePhase(\"hovering\");\n      }\n    },\n\n    finishJustDragged: () => {\n      if (\n        store.current.state === \"active\" &&\n        store.current.phase === \"justDragged\"\n      ) {\n        setActivePhase(\"hovering\");\n      }\n    },\n\n    startCopy: () => {\n      const wasActive = store.current.state === \"active\";\n      setStore(\"current\", {\n        state: \"copying\",\n        startedAt: Date.now(),\n        wasActive,\n      });\n    },\n\n    completeCopy: (element?: Element) => {\n      setStore(\"pendingClickData\", null);\n      if (element) {\n        setStore(\"lastCopiedElement\", element);\n      }\n      const wasActive =\n        store.current.state === \"copying\" ? store.current.wasActive : false;\n      setStore(\"current\", {\n        state: \"justCopied\",\n        copiedAt: Date.now(),\n        wasActive,\n      });\n    },\n\n    finishJustCopied: () => {\n      if (store.current.state === \"justCopied\") {\n        const shouldReturnToActive =\n          store.current.wasActive && !store.wasActivatedByToggle;\n        if (shouldReturnToActive) {\n          actions.clearFrozenElement();\n          setStore(\"current\", {\n            state: \"active\",\n            phase: \"hovering\",\n            isPromptMode: false,\n            isPendingDismiss: false,\n          });\n        } else {\n          actions.deactivate();\n        }\n      }\n    },\n\n    enterPromptMode: (position: Position, element: Element) => {\n      const bounds = createElementBounds(element);\n      const selectionCenterX = bounds.x + bounds.width / 2;\n\n      setStore(\"copyStart\", position);\n      setStore(\"copyOffsetFromCenterX\", position.x - selectionCenterX);\n      setStore(\"pointer\", position);\n      setStore(\"frozenElement\", element);\n      setStore(\"wasActivatedByToggle\", true);\n\n      if (store.current.state !== \"active\") {\n        setStore(\"current\", {\n          state: \"active\",\n          phase: \"frozen\",\n          isPromptMode: true,\n          isPendingDismiss: false,\n        });\n        setStore(\"activationTimestamp\", Date.now());\n        setStore(\"previouslyFocusedElement\", document.activeElement);\n      } else {\n        setStore(\n          \"current\",\n          produce((current) => {\n            if (current.state === \"active\") {\n              current.isPromptMode = true;\n              current.phase = \"frozen\";\n            }\n          }),\n        );\n      }\n    },\n\n    exitPromptMode: () => {\n      if (store.current.state === \"active\") {\n        setStore(\n          \"current\",\n          produce((current) => {\n            if (current.state === \"active\") {\n              current.isPromptMode = false;\n              current.isPendingDismiss = false;\n            }\n          }),\n        );\n      }\n    },\n\n    setInputText: (value: string) => {\n      setStore(\"inputText\", value);\n    },\n\n    clearInputText: () => {\n      setStore(\"inputText\", \"\");\n    },\n\n    setPendingDismiss: (value: boolean) => {\n      if (store.current.state === \"active\") {\n        setStore(\n          \"current\",\n          produce((current) => {\n            if (current.state === \"active\") {\n              current.isPendingDismiss = value;\n            }\n          }),\n        );\n      }\n    },\n\n    setPointer: (position: Position) => {\n      setStore(\"pointer\", position);\n    },\n\n    setDetectedElement: (element: Element | null) => {\n      setStore(\"detectedElement\", element);\n    },\n\n    setFrozenElement: (element: Element) => {\n      setStore(\n        produce((draft) => {\n          draft.frozenElement = element;\n          draft.frozenElements = [element];\n          draft.frozenDragRect = null;\n        }),\n      );\n    },\n\n    setFrozenElements: (elements: Element[]) => {\n      setStore(\n        produce((draft) => {\n          draft.frozenElements = elements;\n          draft.frozenElement = elements.length > 0 ? elements[0] : null;\n          draft.frozenDragRect = null;\n        }),\n      );\n    },\n\n    setFrozenDragRect: (rect: FrozenDragRect | null) => {\n      setStore(\"frozenDragRect\", rect);\n    },\n\n    clearFrozenElement: () => {\n      setStore(\n        produce((draft) => {\n          draft.frozenElement = null;\n          draft.frozenElements = [];\n          draft.frozenDragRect = null;\n        }),\n      );\n    },\n\n    setCopyStart: (position: Position, element: Element) => {\n      const bounds = createElementBounds(element);\n      const selectionCenterX = bounds.x + bounds.width / 2;\n      setStore(\"copyStart\", position);\n      setStore(\"copyOffsetFromCenterX\", position.x - selectionCenterX);\n    },\n\n    setLastGrabbed: (element: Element | null) => {\n      setStore(\"lastGrabbedElement\", element);\n    },\n\n    clearLastCopied: () => {\n      setStore(\"lastCopiedElement\", null);\n    },\n\n    setWasActivatedByToggle: (value: boolean) => {\n      setStore(\"wasActivatedByToggle\", value);\n    },\n\n    setPendingCommentMode: (value: boolean) => {\n      setStore(\"pendingCommentMode\", value);\n    },\n\n    setTouchMode: (value: boolean) => {\n      setStore(\"isTouchMode\", value);\n    },\n\n    setSelectionSource: (\n      filePath: string | null,\n      lineNumber: number | null,\n    ) => {\n      setStore(\n        produce((draft) => {\n          draft.selectionFilePath = filePath;\n          draft.selectionLineNumber = lineNumber;\n        }),\n      );\n    },\n\n    setPendingClickData: (data: PendingClickData | null) => {\n      setStore(\"pendingClickData\", data);\n    },\n\n    clearReplySessionId: () => {\n      setStore(\"replySessionId\", null);\n    },\n\n    incrementViewportVersion: () => {\n      setStore(\"viewportVersion\", (version) => version + 1);\n    },\n\n    addGrabbedBox: (box: GrabbedBox) => {\n      setStore(\"grabbedBoxes\", (boxes) => [...boxes, box]);\n    },\n\n    removeGrabbedBox: (boxId: string) => {\n      setStore(\"grabbedBoxes\", (boxes) =>\n        boxes.filter((box) => box.id !== boxId),\n      );\n    },\n\n    clearGrabbedBoxes: () => {\n      setStore(\"grabbedBoxes\", []);\n    },\n\n    addLabelInstance: (instance: SelectionLabelInstance) => {\n      setStore(\"labelInstances\", (instances) => [...instances, instance]);\n    },\n\n    updateLabelInstance: (\n      instanceId: string,\n      status: SelectionLabelInstance[\"status\"],\n      errorMessage?: string,\n    ) => {\n      const index = store.labelInstances.findIndex(\n        (instance) => instance.id === instanceId,\n      );\n      if (index !== -1) {\n        setStore(\n          \"labelInstances\",\n          index,\n          produce((instance) => {\n            instance.status = status;\n            if (errorMessage !== undefined) {\n              instance.errorMessage = errorMessage;\n            }\n          }),\n        );\n      }\n    },\n\n    removeLabelInstance: (instanceId: string) => {\n      setStore(\"labelInstances\", (instances) =>\n        instances.filter((instance) => instance.id !== instanceId),\n      );\n    },\n\n    clearLabelInstances: () => {\n      setStore(\"labelInstances\", []);\n    },\n\n    setHasAgentProvider: (value: boolean) => {\n      setStore(\"hasAgentProvider\", value);\n    },\n\n    setAgentCapabilities: (capabilities) => {\n      setStore(\n        produce((draft) => {\n          draft.supportsUndo = capabilities.supportsUndo;\n          draft.supportsFollowUp = capabilities.supportsFollowUp;\n          draft.dismissButtonText = capabilities.dismissButtonText;\n          draft.isAgentConnected = capabilities.isAgentConnected;\n        }),\n      );\n    },\n\n    setPendingAbortSessionId: (sessionId: string | null) => {\n      setStore(\"pendingAbortSessionId\", sessionId);\n    },\n\n    showContextMenu: (position: Position, element: Element) => {\n      const bounds = createElementBounds(element);\n      const centerX = bounds.x + bounds.width / 2;\n      const centerY = bounds.y + bounds.height / 2;\n      setStore(\n        produce((draft) => {\n          draft.contextMenuPosition = position;\n          draft.contextMenuElement = element;\n          draft.contextMenuClickOffset = {\n            x: position.x - centerX,\n            y: position.y - centerY,\n          };\n        }),\n      );\n    },\n\n    hideContextMenu: () => {\n      setStore(\n        produce((draft) => {\n          draft.contextMenuPosition = null;\n          draft.contextMenuElement = null;\n          draft.contextMenuClickOffset = null;\n        }),\n      );\n    },\n\n    updateContextMenuPosition: () => {\n      const element = store.contextMenuElement;\n      const clickOffset = store.contextMenuClickOffset;\n\n      if (!element || !clickOffset) return;\n      if (!isElementConnected(element)) return;\n\n      const newBounds = createElementBounds(element);\n      const newCenterX = newBounds.x + newBounds.width / 2;\n      const newCenterY = newBounds.y + newBounds.height / 2;\n\n      setStore(\"contextMenuPosition\", {\n        x: newCenterX + clickOffset.x,\n        y: newCenterY + clickOffset.y,\n      });\n    },\n\n    setSelectedAgent: (agent: AgentOptions | null) => {\n      setStore(\"selectedAgent\", agent);\n    },\n  };\n\n  return { store, actions };\n};\n\nexport { createGrabStore };\n"
  },
  {
    "path": "packages/react-grab/src/core/theme.ts",
    "content": "import type { Theme, DeepPartial } from \"../types.js\";\n\nexport const DEFAULT_THEME: Required<Theme> = {\n  enabled: true,\n  hue: 0,\n  selectionBox: {\n    enabled: true,\n  },\n  dragBox: {\n    enabled: true,\n  },\n  grabbedBoxes: {\n    enabled: true,\n  },\n  elementLabel: {\n    enabled: true,\n  },\n  toolbar: {\n    enabled: true,\n  },\n};\n\nexport const deepMergeTheme = (\n  baseTheme: Required<Theme>,\n  partialTheme: DeepPartial<Theme>,\n): Required<Theme> => ({\n  enabled: partialTheme.enabled ?? baseTheme.enabled,\n  hue: partialTheme.hue ?? baseTheme.hue,\n  selectionBox: {\n    enabled:\n      partialTheme.selectionBox?.enabled ?? baseTheme.selectionBox.enabled,\n  },\n  dragBox: {\n    enabled: partialTheme.dragBox?.enabled ?? baseTheme.dragBox.enabled,\n  },\n  grabbedBoxes: {\n    enabled:\n      partialTheme.grabbedBoxes?.enabled ?? baseTheme.grabbedBoxes.enabled,\n  },\n  elementLabel: {\n    enabled:\n      partialTheme.elementLabel?.enabled ?? baseTheme.elementLabel.enabled,\n  },\n  toolbar: {\n    enabled: partialTheme.toolbar?.enabled ?? baseTheme.toolbar.enabled,\n  },\n});\n"
  },
  {
    "path": "packages/react-grab/src/index.ts",
    "content": "export { init } from \"./core/index.js\";\nexport {\n  getStack,\n  formatElementInfo,\n  isInstrumentationActive,\n  DEFAULT_THEME,\n} from \"./core/index.js\";\nexport { commentPlugin } from \"./core/plugins/comment.js\";\nexport { openPlugin } from \"./core/plugins/open.js\";\nexport { generateSnippet } from \"./utils/generate-snippet.js\";\nexport type {\n  Options,\n  ReactGrabAPI,\n  SourceInfo,\n  Theme,\n  ReactGrabState,\n  ToolbarState,\n  OverlayBounds,\n  GrabbedBox,\n  DragRect,\n  Rect,\n  Position,\n  DeepPartial,\n  ElementLabelVariant,\n  PromptModeContext,\n  ElementLabelContext,\n  AgentContext,\n  AgentSession,\n  AgentProvider,\n  AgentSessionStorage,\n  AgentOptions,\n  AgentCompleteResult,\n  SettableOptions,\n  ActivationMode,\n  ContextMenuAction,\n  ContextMenuActionContext,\n  ToolbarMenuAction,\n  PluginAction,\n  ActionContext,\n  ActionContextHooks,\n  Plugin,\n  PluginConfig,\n  PluginHooks,\n} from \"./types.js\";\n\nimport { init } from \"./core/index.js\";\nimport type { Plugin, ReactGrabAPI } from \"./types.js\";\n\ndeclare global {\n  interface Window {\n    __REACT_GRAB__?: ReactGrabAPI;\n    __REACT_GRAB_DISABLED__?: boolean;\n  }\n}\n\nlet globalApi: ReactGrabAPI | null = null;\n\nexport const getGlobalApi = (): ReactGrabAPI | null => {\n  if (typeof window === \"undefined\") return globalApi;\n  return window.__REACT_GRAB__ ?? globalApi ?? null;\n};\n\nexport const setGlobalApi = (api: ReactGrabAPI | null): void => {\n  globalApi = api;\n  if (typeof window !== \"undefined\") {\n    if (api) {\n      window.__REACT_GRAB__ = api;\n    } else {\n      delete window.__REACT_GRAB__;\n    }\n  }\n};\n\nconst pendingPlugins: Plugin[] = [];\n\nconst flushPendingPlugins = (api: ReactGrabAPI): void => {\n  while (pendingPlugins.length > 0) {\n    const plugin = pendingPlugins.shift();\n    if (plugin) {\n      api.registerPlugin(plugin);\n    }\n  }\n};\n\nexport const registerPlugin = (plugin: Plugin): void => {\n  const api = getGlobalApi();\n  if (api) {\n    api.registerPlugin(plugin);\n    return;\n  }\n  pendingPlugins.push(plugin);\n};\n\nexport const unregisterPlugin = (name: string): void => {\n  const api = getGlobalApi();\n  if (api) {\n    api.unregisterPlugin(name);\n    return;\n  }\n  const pendingIndex = pendingPlugins.findIndex(\n    (pendingPlugin) => pendingPlugin.name === name,\n  );\n  if (pendingIndex !== -1) {\n    pendingPlugins.splice(pendingIndex, 1);\n  }\n};\n\nif (typeof window !== \"undefined\" && !window.__REACT_GRAB_DISABLED__) {\n  if (window.__REACT_GRAB__) {\n    globalApi = window.__REACT_GRAB__;\n  } else {\n    globalApi = init();\n    window.__REACT_GRAB__ = globalApi;\n  }\n  flushPendingPlugins(globalApi);\n  window.dispatchEvent(\n    new CustomEvent(\"react-grab:init\", { detail: globalApi }),\n  );\n}\n"
  },
  {
    "path": "packages/react-grab/src/primitives.ts",
    "content": "import {\n  freezeAnimations,\n  freezeGlobalAnimations,\n  unfreezeGlobalAnimations,\n} from \"./utils/freeze-animations.js\";\nimport { freezePseudoStates } from \"./utils/freeze-pseudo-states.js\";\nimport { freezeUpdates } from \"./utils/freeze-updates.js\";\nimport { unfreezePseudoStates } from \"./utils/freeze-pseudo-states.js\";\nimport {\n  getComponentDisplayName,\n  getHTMLPreview,\n  getStack,\n  getStackContext,\n} from \"./core/context.js\";\nimport { Fiber, getFiberFromHostInstance } from \"bippy\";\nimport type { StackFrame } from \"bippy/source\";\nexport type { StackFrame };\nimport { createElementSelector } from \"./utils/create-element-selector.js\";\nimport { extractElementCss } from \"./utils/extract-element-css.js\";\nimport { openFile as openFileAsync } from \"./utils/open-file.js\";\n\nexport interface ReactGrabElementContext {\n  element: Element;\n  htmlPreview: string;\n  stackString: string;\n  stack: StackFrame[];\n  componentName: string | null;\n  fiber: Fiber | null;\n  selector: string | null;\n  styles: string;\n}\n\n/**\n * Gathers comprehensive context for a DOM element, including its React fiber,\n * component name, source stack, HTML preview, CSS selector, and computed styles.\n *\n * @example\n * const context = await getElementContext(document.querySelector('.my-button')!);\n * console.log(context.componentName); // \"SubmitButton\"\n * console.log(context.selector);      // \"button.my-button\"\n * console.log(context.stackString);   // \"\\n  in SubmitButton (at Button.tsx:12:5)\"\n * console.log(context.stack[0]);      // { functionName: \"SubmitButton\", fileName: \"Button.tsx\", lineNumber: 12, columnNumber: 5 }\n */\nexport const getElementContext = async (\n  element: Element,\n): Promise<ReactGrabElementContext> => {\n  const stack = (await getStack(element)) ?? [];\n  const stackString = await getStackContext(element);\n  const htmlPreview = getHTMLPreview(element);\n  const componentName = getComponentDisplayName(element);\n  const fiber = getFiberFromHostInstance(element);\n  const selector = createElementSelector(element);\n  const styles = extractElementCss(element);\n\n  return {\n    element,\n    htmlPreview,\n    stackString,\n    stack,\n    componentName,\n    fiber,\n    selector,\n    styles,\n  };\n};\n\nconst freezeCleanupFns = new Set<() => void>();\nlet _isFreezeActive = false;\n\n/**\n * Freezes the page by halting React updates, pausing CSS/JS animations,\n * and preserving pseudo-states (e.g. :hover, :focus) on the given elements.\n *\n * @example\n * freeze(); // freezes the entire page\n * freeze([document.querySelector('.modal')!]); // freezes only the modal subtree\n */\nexport const freeze = (elements?: Element[]): void => {\n  _isFreezeActive = true;\n  freezeCleanupFns.add(freezeUpdates());\n  freezeCleanupFns.add(freezeAnimations(elements ?? [document.body]));\n  freezeGlobalAnimations();\n  freezePseudoStates();\n};\n\n/**\n * Restores normal page behavior by re-enabling React updates, resuming\n * animations, and releasing preserved pseudo-states.\n *\n * @example\n * freeze();\n * // ... capture a snapshot ...\n * unfreeze(); // page resumes normal behavior\n */\nexport const unfreeze = (): void => {\n  _isFreezeActive = false;\n  for (const cleanup of Array.from(freezeCleanupFns)) {\n    cleanup();\n  }\n  freezeCleanupFns.clear();\n  freezeAnimations([]);\n  unfreezeGlobalAnimations();\n  unfreezePseudoStates();\n};\n\n/**\n * Returns whether the page is currently in a frozen state.\n *\n * @example\n * if (isFreezeActive()) {\n *   console.log('Page is frozen, skipping update');\n * }\n */\nexport const isFreezeActive = (): boolean => {\n  return _isFreezeActive;\n};\n\n/**\n * Opens the source file at the given path in the user's editor.\n * Tries the dev-server endpoint first (Vite / Next.js), then falls back\n * to a protocol URL (e.g. vscode://file/…).\n *\n * @example\n * openFile(\"/src/components/Button.tsx\");\n * openFile(\"/src/components/Button.tsx\", 42);\n */\nexport const openFile = async (\n  filePath: string,\n  lineNumber?: number,\n): Promise<void> => {\n  await openFileAsync(filePath, lineNumber);\n};\n"
  },
  {
    "path": "packages/react-grab/src/styles.css",
    "content": "@import \"tailwindcss\" source(\".\");\n\n@theme {\n  --font-sans: \"Geist\", ui-sans-serif, system-ui, sans-serif;\n  --font-mono:\n    ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\",\n    monospace;\n  --color-grab-pink: #b21c8e;\n  --color-grab-pink-light: #fde7f7;\n  --color-grab-pink-border: #f7c5ec;\n  --color-grab-purple: rgb(210, 57, 192);\n  --color-label-tag-border: #730079;\n  --color-label-tag-text: #1e001f;\n  --color-label-gray-border: #b0b0b0;\n  --color-label-success-bg: #d9ffe4;\n  --color-label-success-border: #00bb69;\n  --color-label-success-text: #006e3b;\n  --color-label-divider: #dedede;\n  --color-label-muted: #767676;\n  --transition-fast: 100ms;\n  --transition-normal: 150ms;\n  --transition-slow: 200ms;\n}\n\n:host {\n  all: initial;\n  direction: ltr;\n}\n\n@keyframes shake {\n  0%,\n  100% {\n    transform: translateX(0);\n  }\n  15% {\n    transform: translateX(-3px);\n  }\n  30% {\n    transform: translateX(3px);\n  }\n  45% {\n    transform: translateX(-3px);\n  }\n  60% {\n    transform: translateX(3px);\n  }\n  75% {\n    transform: translateX(-2px);\n  }\n  90% {\n    transform: translateX(2px);\n  }\n}\n\n@keyframes pop-in {\n  0% {\n    opacity: 0;\n    transform: scale(0.9);\n  }\n  70% {\n    opacity: 1;\n    transform: scale(1.02);\n  }\n  100% {\n    opacity: 1;\n    transform: scale(1);\n  }\n}\n\n@keyframes pop-out {\n  from {\n    opacity: 1;\n    transform: scale(1);\n  }\n  to {\n    opacity: 0;\n    transform: scale(0.95);\n  }\n}\n\n@keyframes slide-in-bottom {\n  from {\n    opacity: 0;\n    transform: translateY(8px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes slide-in-top {\n  from {\n    opacity: 0;\n    transform: translateY(-8px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes slide-in-left {\n  from {\n    opacity: 0;\n    transform: translateX(-8px);\n  }\n  to {\n    opacity: 1;\n    transform: translateX(0);\n  }\n}\n\n@keyframes slide-in-right {\n  from {\n    opacity: 0;\n    transform: translateX(8px);\n  }\n  to {\n    opacity: 1;\n    transform: translateX(0);\n  }\n}\n\n@keyframes success-pop {\n  0% {\n    transform: scale(0.9);\n    opacity: 0;\n  }\n  60% {\n    transform: scale(1.1);\n    opacity: 1;\n  }\n  80% {\n    transform: scale(0.95);\n  }\n  100% {\n    transform: scale(1);\n    opacity: 1;\n  }\n}\n\n@keyframes hint-flip-in {\n  from {\n    opacity: 0;\n    transform: translateY(4px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes tooltip-fade-in {\n  from {\n    opacity: 0;\n    transform: scale(0.97);\n  }\n  to {\n    opacity: 1;\n    transform: scale(1);\n  }\n}\n\n@keyframes icon-loader-spin {\n  0% {\n    opacity: 1;\n  }\n  50% {\n    opacity: 0.5;\n  }\n  100% {\n    opacity: 0.2;\n  }\n}\n\n.icon-loader-bar {\n  animation: icon-loader-spin 0.5s linear infinite;\n}\n\n@keyframes shimmer {\n  0% {\n    background-position: 200% 0;\n  }\n  100% {\n    background-position: -200% 0;\n  }\n}\n\n.shimmer-text {\n  background: linear-gradient(\n    90deg,\n    #71717a 0%,\n    #a1a1aa 25%,\n    #71717a 50%,\n    #a1a1aa 75%,\n    #71717a 100%\n  );\n  background-size: 200% 100%;\n  -webkit-background-clip: text;\n  background-clip: text;\n  color: transparent;\n  animation: shimmer 2.5s linear infinite;\n}\n\n@keyframes clock-flash {\n  0% {\n    transform: scale(1);\n  }\n  25% {\n    transform: scale(1.2);\n  }\n  50% {\n    transform: scale(0.92);\n  }\n  75% {\n    transform: scale(1.05);\n  }\n  100% {\n    transform: scale(1);\n  }\n}\n\n.animate-clock-flash {\n  animation: clock-flash 400ms ease-out;\n  will-change: transform;\n}\n\n.animate-shake {\n  animation: shake 0.3s ease-out;\n  will-change: transform;\n}\n\n.animate-pop-in {\n  animation: pop-in var(--transition-normal) ease-out;\n  will-change: transform, opacity;\n}\n\n.animate-pop-out {\n  animation: pop-out var(--transition-normal) ease-out forwards;\n  will-change: transform, opacity;\n}\n\n.animate-slide-in-bottom {\n  animation: slide-in-bottom var(--transition-slow) ease-out;\n  will-change: transform, opacity;\n}\n\n.animate-slide-in-top {\n  animation: slide-in-top var(--transition-slow) ease-out;\n  will-change: transform, opacity;\n}\n\n.animate-slide-in-left {\n  animation: slide-in-left var(--transition-slow) ease-out;\n  will-change: transform, opacity;\n}\n\n.animate-slide-in-right {\n  animation: slide-in-right var(--transition-slow) ease-out;\n  will-change: transform, opacity;\n}\n\n.animate-success-pop {\n  animation: success-pop 250ms ease-out;\n  will-change: transform, opacity;\n}\n\n.animate-tooltip-fade-in {\n  animation: tooltip-fade-in var(--transition-fast) ease-out;\n  will-change: transform, opacity;\n}\n\n@utility press-scale {\n  transition-property: transform;\n  transition-duration: var(--transition-fast);\n  transition-timing-function: ease-out;\n\n  &:active {\n    transform: scale(0.97);\n  }\n}\n\n@utility interactive-scale {\n  transition-property: transform;\n  transition-duration: var(--transition-normal);\n  transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);\n\n  @media (hover: hover) and (pointer: fine) {\n    &:hover {\n      transform: scale(1.05);\n    }\n  }\n\n  &:active {\n    transform: scale(0.97);\n  }\n}\n\n@utility touch-hitbox {\n  position: relative;\n\n  &::before {\n    content: \"\";\n    position: absolute;\n    display: block;\n    top: 50%;\n    left: 50%;\n    transform: translate(-50%, -50%);\n    width: 100%;\n    height: 100%;\n    min-height: 44px;\n    min-width: 44px;\n  }\n}\n"
  },
  {
    "path": "packages/react-grab/src/types.ts",
    "content": "export interface Position {\n  x: number;\n  y: number;\n}\n\nexport type DeepPartial<T> = {\n  [P in keyof T]?: T[P] extends object\n    ? T[P] extends (...args: unknown[]) => unknown\n      ? T[P]\n      : DeepPartial<T[P]>\n    : T[P];\n};\n\nexport interface Theme {\n  /**\n   * Globally toggle the entire overlay\n   * @default true\n   */\n  enabled?: boolean;\n  /**\n   * Base hue (0-360) used to generate colors throughout the interface using HSL color space\n   * @default 0\n   */\n  hue?: number;\n  /**\n   * The highlight box that appears when hovering over an element before selecting it\n   */\n  selectionBox?: {\n    /**\n     * Whether to show the selection highlight\n     * @default true\n     */\n    enabled?: boolean;\n  };\n  /**\n   * The rectangular selection area that appears when clicking and dragging to select multiple elements\n   */\n  dragBox?: {\n    /**\n     * Whether to show the drag selection box\n     * @default true\n     */\n    enabled?: boolean;\n  };\n  /**\n   * Brief flash/highlight boxes that appear on elements immediately after they're successfully grabbed/copied\n   */\n  grabbedBoxes?: {\n    /**\n     * Whether to show these success flash effects\n     * @default true\n     */\n    enabled?: boolean;\n  };\n  /**\n   * The floating label that follows the cursor showing information about the currently hovered element\n   */\n  elementLabel?: {\n    /**\n     * Whether to show the label\n     * @default true\n     */\n    enabled?: boolean;\n  };\n  /**\n   * The floating toolbar that allows toggling React Grab activation\n   */\n  toolbar?: {\n    /**\n     * Whether to show the toolbar\n     * @default true\n     */\n    enabled?: boolean;\n  };\n}\n\nexport interface ReactGrabState {\n  isActive: boolean;\n  isDragging: boolean;\n  isCopying: boolean;\n  isPromptMode: boolean;\n  isSelectionBoxVisible: boolean;\n  isDragBoxVisible: boolean;\n  targetElement: Element | null;\n  dragBounds: DragRect | null;\n  /**\n   * Currently visible grabbed boxes (success flash effects).\n   * These are temporary visual indicators shown after elements are grabbed/copied.\n   */\n  grabbedBoxes: Array<{\n    id: string;\n    bounds: OverlayBounds;\n    createdAt: number;\n  }>;\n  labelInstances: Array<{\n    id: string;\n    status: SelectionLabelStatus;\n    tagName: string;\n    componentName?: string;\n    createdAt: number;\n  }>;\n  selectionFilePath: string | null;\n  toolbarState: ToolbarState | null;\n}\n\nexport type ElementLabelVariant = \"hover\" | \"processing\" | \"success\";\n\nexport interface PromptModeContext {\n  x: number;\n  y: number;\n  targetElement: Element | null;\n}\n\nexport interface ElementLabelContext {\n  x: number;\n  y: number;\n  content: string;\n  element?: Element;\n  tagName?: string;\n  componentName?: string;\n  filePath?: string;\n  lineNumber?: number;\n}\n\nexport type ActivationKey = string | ((event: KeyboardEvent) => boolean);\n\nexport interface AgentContext<T = unknown> {\n  content: string[];\n  prompt: string;\n  options?: T;\n  sessionId?: string;\n}\n\nexport interface AgentSession {\n  id: string;\n  context: AgentContext;\n  lastStatus: string;\n  isStreaming: boolean;\n  isFading?: boolean;\n  createdAt: number;\n  lastUpdatedAt: number;\n  position: Position;\n  selectionBounds: OverlayBounds[];\n  tagName?: string;\n  componentName?: string;\n  error?: string;\n}\n\nexport interface AgentProvider<T = unknown> {\n  send: (\n    context: AgentContext<T>,\n    signal: AbortSignal,\n  ) => AsyncIterable<string>;\n  resume?: (\n    sessionId: string,\n    signal: AbortSignal,\n    storage: AgentSessionStorage,\n  ) => AsyncIterable<string>;\n  abort?: (sessionId: string) => Promise<void>;\n  supportsResume?: boolean;\n  supportsFollowUp?: boolean;\n  dismissButtonText?: string;\n  checkConnection?: () => Promise<boolean>;\n  getCompletionMessage?: () => string | undefined;\n  undo?: () => Promise<void>;\n  canUndo?: () => boolean;\n  redo?: () => Promise<void>;\n  canRedo?: () => boolean;\n}\n\nexport interface AgentSessionStorage {\n  getItem(key: string): string | null;\n  setItem(key: string, value: string): void;\n  removeItem(key: string): void;\n}\n\nexport interface AgentCompleteResult {\n  error?: string;\n}\n\nexport interface AgentOptions<T = unknown> {\n  provider?: AgentProvider<T>;\n  storage?: AgentSessionStorage | null;\n  getOptions?: () => T;\n  onStart?: (session: AgentSession, elements: Element[]) => void;\n  onStatus?: (status: string, session: AgentSession) => void;\n  onComplete?: (\n    session: AgentSession,\n    elements: Element[],\n  ) => AgentCompleteResult | void | Promise<AgentCompleteResult | void>;\n  onError?: (error: Error, session: AgentSession) => void;\n  onResume?: (session: AgentSession) => void;\n  onAbort?: (session: AgentSession, elements: Element[]) => void;\n  onUndo?: (session: AgentSession, elements: Element[]) => void;\n  onDismiss?: (session: AgentSession, elements: Element[]) => void;\n}\n\nexport type ActivationMode = \"toggle\" | \"hold\";\n\nexport interface ActionContextHooks {\n  transformHtmlContent: (html: string, elements: Element[]) => Promise<string>;\n  onOpenFile: (filePath: string, lineNumber?: number) => boolean | void;\n  transformOpenFileUrl: (\n    url: string,\n    filePath: string,\n    lineNumber?: number,\n  ) => string;\n}\n\nexport interface ActionContext {\n  element: Element;\n  elements: Element[];\n  filePath?: string;\n  lineNumber?: number;\n  componentName?: string;\n  tagName?: string;\n  enterPromptMode?: (agent?: AgentOptions) => void;\n  hooks: ActionContextHooks;\n  performWithFeedback: (action: () => Promise<boolean>) => Promise<void>;\n  hideContextMenu: () => void;\n  cleanup: () => void;\n}\n\nexport interface ContextMenuActionContext extends ActionContext {\n  copy?: () => void;\n}\n\nexport interface ContextMenuAction {\n  id: string;\n  label: string;\n  target?: \"context-menu\";\n  shortcut?: string;\n  enabled?: boolean | ((context: ActionContext) => boolean);\n  onAction: (context: ContextMenuActionContext) => void | Promise<void>;\n  agent?: AgentOptions;\n}\n\nexport interface ActionCycleItem {\n  id: string;\n  label: string;\n  shortcut?: string;\n}\n\nexport interface ActionCycleState {\n  items: ActionCycleItem[];\n  activeIndex: number | null;\n  isVisible: boolean;\n}\n\nexport interface ArrowNavigationItem {\n  tagName: string;\n  componentName?: string;\n}\n\nexport interface ArrowNavigationState {\n  items: ArrowNavigationItem[];\n  activeIndex: number;\n  isVisible: boolean;\n}\n\nexport interface PerformWithFeedbackOptions {\n  fallbackBounds?: OverlayBounds;\n  fallbackSelectionBounds?: OverlayBounds[];\n  position?: Position;\n}\n\nexport interface PluginHooks {\n  onActivate?: () => void;\n  onDeactivate?: () => void;\n  cancelPendingToolbarActions?: () => void;\n  onElementHover?: (element: Element) => void;\n  onElementSelect?: (element: Element) => boolean | void | Promise<boolean>;\n  onDragStart?: (startX: number, startY: number) => void;\n  onDragEnd?: (elements: Element[], bounds: DragRect) => void;\n  onBeforeCopy?: (elements: Element[]) => void | Promise<void>;\n  transformCopyContent?: (\n    content: string,\n    elements: Element[],\n  ) => string | Promise<string>;\n  onAfterCopy?: (elements: Element[], success: boolean) => void;\n  onCopySuccess?: (elements: Element[], content: string) => void;\n  onCopyError?: (error: Error) => void;\n  onStateChange?: (state: ReactGrabState) => void;\n  onPromptModeChange?: (\n    isPromptMode: boolean,\n    context: PromptModeContext,\n  ) => void;\n  onSelectionBox?: (\n    visible: boolean,\n    bounds: OverlayBounds | null,\n    element: Element | null,\n  ) => void;\n  onDragBox?: (visible: boolean, bounds: OverlayBounds | null) => void;\n  onGrabbedBox?: (bounds: OverlayBounds, element: Element) => void;\n  onElementLabel?: (\n    visible: boolean,\n    variant: ElementLabelVariant,\n    context: ElementLabelContext,\n  ) => void;\n  onContextMenu?: (element: Element, position: Position) => void;\n  onOpenFile?: (filePath: string, lineNumber?: number) => boolean | void;\n  transformHtmlContent?: (\n    html: string,\n    elements: Element[],\n  ) => string | Promise<string>;\n  transformAgentContext?: (\n    context: AgentContext,\n    elements: Element[],\n  ) => AgentContext | Promise<AgentContext>;\n  transformActionContext?: (context: ActionContext) => ActionContext;\n  transformOpenFileUrl?: (\n    url: string,\n    filePath: string,\n    lineNumber?: number,\n  ) => string;\n  transformSnippet?: (\n    snippet: string,\n    element: Element,\n  ) => string | Promise<string>;\n}\n\nexport interface ToolbarMenuAction {\n  id: string;\n  label: string;\n  shortcut?: string;\n  target: \"toolbar\";\n  enabled?: boolean | (() => boolean);\n  isActive?: () => boolean;\n  onAction: () => void | Promise<void>;\n}\n\nexport type PluginAction = ContextMenuAction | ToolbarMenuAction;\n\nexport interface PluginConfig {\n  theme?: DeepPartial<Theme>;\n  options?: SettableOptions;\n  actions?: PluginAction[];\n  hooks?: PluginHooks;\n  cleanup?: () => void;\n}\n\nexport interface Plugin {\n  name: string;\n  theme?: DeepPartial<Theme>;\n  options?: SettableOptions;\n  actions?: PluginAction[];\n  hooks?: PluginHooks;\n  setup?: (api: ReactGrabAPI, hooks: ActionContextHooks) => PluginConfig | void;\n}\n\nexport interface Options {\n  enabled?: boolean;\n  activationMode?: ActivationMode;\n  keyHoldDuration?: number;\n  allowActivationInsideInput?: boolean;\n  maxContextLines?: number;\n  activationKey?: ActivationKey;\n  getContent?: (elements: Element[]) => Promise<string> | string;\n  /**\n   * Whether to freeze React state updates while React Grab is active.\n   * This prevents UI changes from interfering with element selection.\n   * @default true\n   */\n  freezeReactUpdates?: boolean;\n}\n\nexport interface SettableOptions extends Options {\n  enabled?: never;\n}\n\nexport interface SourceInfo {\n  filePath: string;\n  lineNumber: number | null;\n  componentName: string | null;\n}\n\nexport interface ToolbarState {\n  edge: \"top\" | \"bottom\" | \"left\" | \"right\";\n  ratio: number;\n  collapsed: boolean;\n  enabled: boolean;\n}\n\nexport interface DropdownAnchor {\n  x: number;\n  y: number;\n  edge: ToolbarState[\"edge\"];\n  toolbarWidth: number;\n}\n\nexport interface ReactGrabAPI {\n  activate: () => void;\n  deactivate: () => void;\n  toggle: () => void;\n  comment: () => void;\n  isActive: () => boolean;\n  isEnabled: () => boolean;\n  setEnabled: (enabled: boolean) => void;\n  getToolbarState: () => ToolbarState | null;\n  setToolbarState: (state: Partial<ToolbarState>) => void;\n  onToolbarStateChange: (callback: (state: ToolbarState) => void) => () => void;\n  dispose: () => void;\n  copyElement: (elements: Element | Element[]) => Promise<boolean>;\n  getSource: (element: Element) => Promise<SourceInfo | null>;\n  getStackContext: (element: Element) => Promise<string>;\n  getState: () => ReactGrabState;\n  setOptions: (options: SettableOptions) => void;\n  registerPlugin: (plugin: Plugin) => void;\n  unregisterPlugin: (name: string) => void;\n  getPlugins: () => string[];\n  getDisplayName: (element: Element) => string | null;\n}\n\nexport interface OverlayBounds {\n  borderRadius: string;\n  height: number;\n  transform: string;\n  width: number;\n  x: number;\n  y: number;\n}\n\nexport type SelectionLabelStatus =\n  | \"idle\"\n  | \"copying\"\n  | \"copied\"\n  | \"fading\"\n  | \"error\";\n\nexport interface SelectionLabelInstance {\n  id: string;\n  bounds: OverlayBounds;\n  boundsMultiple?: OverlayBounds[];\n  tagName: string;\n  componentName?: string;\n  elementsCount?: number;\n  status: SelectionLabelStatus;\n  statusText?: string;\n  isPromptMode?: boolean;\n  inputValue?: string;\n  createdAt: number;\n  element?: Element;\n  elements?: Element[];\n  mouseX?: number;\n  mouseXOffsetFromCenter?: number;\n  mouseXOffsetRatio?: number;\n  errorMessage?: string;\n  hideArrow?: boolean;\n}\n\nexport interface HistoryItem {\n  id: string;\n  content: string;\n  elementName: string;\n  tagName: string;\n  componentName?: string;\n  elementsCount?: number;\n  previewBounds?: OverlayBounds[];\n  elementSelectors?: string[];\n  isComment: boolean;\n  commentText?: string;\n  timestamp: number;\n}\n\nexport interface ReactGrabRendererProps {\n  selectionVisible?: boolean;\n  selectionBounds?: OverlayBounds;\n  selectionBoundsMultiple?: OverlayBounds[];\n  selectionShouldSnap?: boolean;\n  selectionElementsCount?: number;\n  selectionFilePath?: string;\n  selectionLineNumber?: number;\n  selectionTagName?: string;\n  selectionComponentName?: string;\n  selectionLabelVisible?: boolean;\n  selectionLabelStatus?: SelectionLabelStatus;\n  selectionActionCycleState?: ActionCycleState;\n  selectionArrowNavigationState?: ArrowNavigationState;\n  onArrowNavigationSelect?: (index: number) => void;\n  labelInstances?: SelectionLabelInstance[];\n  dragVisible?: boolean;\n  dragBounds?: OverlayBounds;\n  grabbedBoxes?: Array<{\n    id: string;\n    bounds: OverlayBounds;\n    createdAt: number;\n  }>;\n  mouseX?: number;\n  isFrozen?: boolean;\n  inputValue?: string;\n  isPromptMode?: boolean;\n  replyToPrompt?: string;\n  hasAgent?: boolean;\n  agentSessions?: Map<string, AgentSession>;\n  supportsUndo?: boolean;\n  supportsFollowUp?: boolean;\n  dismissButtonText?: string;\n  onRequestAbortSession?: (sessionId: string) => void;\n  onAbortSession?: (sessionId: string, confirmed: boolean) => void;\n  onDismissSession?: (sessionId: string) => void;\n  onUndoSession?: (sessionId: string) => void;\n  onFollowUpSubmitSession?: (sessionId: string, prompt: string) => void;\n  onAcknowledgeSessionError?: (sessionId: string) => void;\n  onRetrySession?: (sessionId: string) => void;\n  onShowContextMenuInstance?: (instanceId: string) => void;\n  onLabelInstanceHoverChange?: (instanceId: string, isHovered: boolean) => void;\n  onInputChange?: (value: string) => void;\n  onInputSubmit?: () => void;\n  onToggleExpand?: () => void;\n  isPendingDismiss?: boolean;\n  selectionLabelShakeCount?: number;\n  onConfirmDismiss?: () => void;\n  onCancelDismiss?: () => void;\n  pendingAbortSessionId?: string | null;\n  toolbarVisible?: boolean;\n  isActive?: boolean;\n  onToggleActive?: () => void;\n  enabled?: boolean;\n  onToggleEnabled?: () => void;\n  shakeCount?: number;\n  onToolbarStateChange?: (state: ToolbarState) => void;\n  onSubscribeToToolbarStateChanges?: (\n    callback: (state: ToolbarState) => void,\n  ) => () => void;\n  onToolbarSelectHoverChange?: (isHovered: boolean) => void;\n  onToolbarRef?: (element: HTMLDivElement) => void;\n  contextMenuPosition?: Position | null;\n  contextMenuBounds?: OverlayBounds | null;\n  contextMenuTagName?: string;\n  contextMenuComponentName?: string;\n  contextMenuHasFilePath?: boolean;\n  actions?: ContextMenuAction[];\n  toolbarActions?: ToolbarMenuAction[];\n  actionContext?: ActionContext;\n  onContextMenuDismiss?: () => void;\n  onContextMenuHide?: () => void;\n  historyItems?: HistoryItem[];\n  historyDisconnectedItemIds?: Set<string>;\n  historyItemCount?: number;\n  clockFlashTrigger?: number;\n  hasUnreadHistoryItems?: boolean;\n  historyDropdownPosition?: DropdownAnchor | null;\n  isHistoryPinned?: boolean;\n  onToggleHistory?: () => void;\n  onCopyAll?: () => void;\n  onCopyAllHover?: (isHovered: boolean) => void;\n  onHistoryButtonHover?: (isHovered: boolean) => void;\n  onHistoryItemSelect?: (item: HistoryItem) => void;\n  onHistoryItemRemove?: (item: HistoryItem) => void;\n  onHistoryItemCopy?: (item: HistoryItem) => void;\n  onHistoryItemHover?: (historyItemId: string | null) => void;\n  onHistoryCopyAll?: () => void;\n  onHistoryCopyAllHover?: (isHovered: boolean) => void;\n  onHistoryClear?: () => void;\n  onHistoryDismiss?: () => void;\n  onHistoryDropdownHover?: (isHovered: boolean) => void;\n  toolbarMenuPosition?: DropdownAnchor | null;\n  onToggleMenu?: () => void;\n  onToolbarMenuDismiss?: () => void;\n  clearPromptPosition?: DropdownAnchor | null;\n  onClearHistoryConfirm?: () => void;\n  onClearHistoryCancel?: () => void;\n}\n\nexport interface GrabbedBox {\n  id: string;\n  bounds: OverlayBounds;\n  createdAt: number;\n  element?: Element;\n}\n\nexport interface Rect {\n  left: number;\n  top: number;\n  right: number;\n  bottom: number;\n}\n\nexport interface DragRect {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n}\n\nexport type ArrowPosition = \"bottom\" | \"top\";\n\nexport interface ArrowProps {\n  position: ArrowPosition;\n  leftPercent: number;\n  leftOffsetPx: number;\n  color?: string;\n  labelWidth?: number;\n}\n\nexport interface TagBadgeProps {\n  tagName: string;\n  componentName?: string;\n  isClickable: boolean;\n  onClick: (event: MouseEvent) => void;\n  onHoverChange?: (hovered: boolean) => void;\n  shrink?: boolean;\n  forceShowIcon?: boolean;\n}\n\nexport interface BottomSectionProps {\n  children: import(\"solid-js\").JSX.Element;\n}\n\nexport interface DiscardPromptProps {\n  label?: string;\n  cancelOnEscape?: boolean;\n  onConfirm?: () => void;\n  onCancel?: () => void;\n}\n\nexport interface ErrorViewProps {\n  error: string;\n  onAcknowledge?: () => void;\n  onRetry?: () => void;\n}\n\nexport interface CompletionViewProps {\n  statusText: string;\n  supportsUndo?: boolean;\n  supportsFollowUp?: boolean;\n  dismissButtonText?: string;\n  previousPrompt?: string;\n  onDismiss?: () => void;\n  onUndo?: () => void;\n  onFollowUpSubmit?: (prompt: string) => void;\n  onCopyStateChange?: () => void;\n  onFadingChange?: (isFading: boolean) => void;\n  onShowContextMenu?: () => void;\n}\n\nexport interface SelectionLabelProps {\n  tagName?: string;\n  componentName?: string;\n  elementsCount?: number;\n  selectionBounds?: OverlayBounds;\n  mouseX?: number;\n  visible?: boolean;\n  isPromptMode?: boolean;\n  inputValue?: string;\n  replyToPrompt?: string;\n  previousPrompt?: string;\n  hasAgent?: boolean;\n  status?: SelectionLabelStatus;\n  statusText?: string;\n  filePath?: string;\n  supportsUndo?: boolean;\n  supportsFollowUp?: boolean;\n  dismissButtonText?: string;\n  actionCycleState?: ActionCycleState;\n  arrowNavigationState?: ArrowNavigationState;\n  onArrowNavigationSelect?: (index: number) => void;\n  onInputChange?: (value: string) => void;\n  onSubmit?: () => void;\n  onToggleExpand?: () => void;\n  onAbort?: () => void;\n  onOpen?: () => void;\n  onDismiss?: () => void;\n  onUndo?: () => void;\n  onFollowUpSubmit?: (prompt: string) => void;\n  isPendingDismiss?: boolean;\n  selectionLabelShakeCount?: number;\n  onConfirmDismiss?: () => void;\n  onCancelDismiss?: () => void;\n  isPendingAbort?: boolean;\n  onConfirmAbort?: () => void;\n  onCancelAbort?: () => void;\n  error?: string;\n  onAcknowledgeError?: () => void;\n  onRetry?: () => void;\n  isContextMenuOpen?: boolean;\n  onShowContextMenu?: () => void;\n  onHoverChange?: (isHovered: boolean) => void;\n  hideArrow?: boolean;\n}\n"
  },
  {
    "path": "packages/react-grab/src/utils/append-stack-context.ts",
    "content": "export const appendStackContext = (\n  content: string,\n  stackContext: string,\n): string => {\n  if (!stackContext) return content;\n  return `${content}\\n${stackContext}`;\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/auto-resize-textarea.ts",
    "content": "export const autoResizeTextarea = (\n  textarea: HTMLTextAreaElement,\n  maxHeight: number,\n) => {\n  textarea.style.height = \"auto\";\n  textarea.style.height = `${Math.min(textarea.scrollHeight, maxHeight)}px`;\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/clamp-to-viewport.ts",
    "content": "export const clampToViewport = (\n  value: number,\n  elementSize: number,\n  viewportSize: number,\n  padding: number,\n): number =>\n  Math.max(padding, Math.min(value, viewportSize - elementSize - padding));\n"
  },
  {
    "path": "packages/react-grab/src/utils/cn.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\";\n\nexport const cn = (...inputs: ClassValue[]): string => clsx(inputs);\n"
  },
  {
    "path": "packages/react-grab/src/utils/combine-bounds.ts",
    "content": "interface Bounds {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n}\n\nexport const combineBounds = <T extends Bounds>(boundsList: T[]): Bounds => {\n  if (boundsList.length === 0) {\n    return { x: 0, y: 0, width: 0, height: 0 };\n  }\n  if (boundsList.length === 1) {\n    return boundsList[0];\n  }\n\n  let minX = Infinity;\n  let minY = Infinity;\n  let maxX = -Infinity;\n  let maxY = -Infinity;\n\n  for (const bounds of boundsList) {\n    minX = Math.min(minX, bounds.x);\n    minY = Math.min(minY, bounds.y);\n    maxX = Math.max(maxX, bounds.x + bounds.width);\n    maxY = Math.max(maxY, bounds.y + bounds.height);\n  }\n\n  return {\n    x: minX,\n    y: minY,\n    width: maxX - minX,\n    height: maxY - minY,\n  };\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/confirmation-focus-manager.ts",
    "content": "let activeConfirmationId: symbol | null = null;\n\nexport const confirmationFocusManager = {\n  claim: (id: symbol): void => {\n    activeConfirmationId = id;\n  },\n  release: (id: symbol): void => {\n    if (activeConfirmationId === id) {\n      activeConfirmationId = null;\n    }\n  },\n  isActive: (id: symbol): boolean => activeConfirmationId === id,\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/copy-content.ts",
    "content": "import { VERSION } from \"../constants.js\";\n\nconst REACT_GRAB_MIME_TYPE = \"application/x-react-grab\";\n\nexport interface ReactGrabEntry {\n  tagName?: string;\n  componentName?: string;\n  content: string;\n  commentText?: string;\n}\n\ninterface CopyContentOptions {\n  onSuccess?: () => void;\n  componentName?: string;\n  tagName?: string;\n  commentText?: string;\n  entries?: ReactGrabEntry[];\n}\n\ninterface ReactGrabMetadata {\n  version: string;\n  content: string;\n  entries: ReactGrabEntry[];\n  timestamp: number;\n}\n\nconst escapeHtml = (text: string): string =>\n  text\n    .replace(/&/g, \"&amp;\")\n    .replace(/</g, \"&lt;\")\n    .replace(/>/g, \"&gt;\")\n    .replace(/\"/g, \"&quot;\");\n\nexport const copyContent = (\n  content: string,\n  options?: CopyContentOptions,\n): boolean => {\n  const elementName = options?.componentName ?? \"div\";\n  const entries = options?.entries ?? [\n    {\n      tagName: options?.tagName,\n      componentName: elementName,\n      content,\n      commentText: options?.commentText,\n    },\n  ];\n  const reactGrabMetadata: ReactGrabMetadata = {\n    version: VERSION,\n    content,\n    entries,\n    timestamp: Date.now(),\n  };\n\n  const copyHandler = (event: ClipboardEvent) => {\n    event.preventDefault();\n    event.clipboardData?.setData(\"text/plain\", content);\n    event.clipboardData?.setData(\n      \"text/html\",\n      `<meta charset='utf-8'><pre><code>${escapeHtml(content)}</code></pre>`,\n    );\n    event.clipboardData?.setData(\n      REACT_GRAB_MIME_TYPE,\n      JSON.stringify(reactGrabMetadata),\n    );\n  };\n\n  document.addEventListener(\"copy\", copyHandler);\n\n  const textarea = document.createElement(\"textarea\");\n  textarea.value = content;\n  textarea.style.position = \"fixed\";\n  textarea.style.left = \"-9999px\";\n  textarea.ariaHidden = \"true\";\n  document.body.appendChild(textarea);\n  textarea.select();\n\n  try {\n    if (typeof document.execCommand !== \"function\") {\n      return false;\n    }\n    const didCopySucceed = document.execCommand(\"copy\");\n    if (didCopySucceed) {\n      options?.onSuccess?.();\n    }\n    return didCopySucceed;\n  } finally {\n    document.removeEventListener(\"copy\", copyHandler);\n    textarea.remove();\n  }\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/create-anchored-dropdown.ts",
    "content": "import { createSignal, createEffect, createMemo, onCleanup } from \"solid-js\";\nimport type { Accessor } from \"solid-js\";\nimport type { DropdownAnchor } from \"../types.js\";\nimport {\n  DROPDOWN_ANCHOR_GAP_PX,\n  DROPDOWN_ANIMATION_DURATION_MS,\n  DROPDOWN_OFFSCREEN_POSITION,\n  DROPDOWN_VIEWPORT_PADDING_PX,\n} from \"../constants.js\";\nimport { getAnchoredDropdownPosition } from \"./get-anchored-dropdown-position.js\";\nimport {\n  nativeCancelAnimationFrame,\n  nativeRequestAnimationFrame,\n} from \"./native-raf.js\";\n\ninterface AnchoredDropdownResult {\n  shouldMount: Accessor<boolean>;\n  isAnimatedIn: Accessor<boolean>;\n  lastAnchorEdge: Accessor<DropdownAnchor[\"edge\"]>;\n  displayPosition: Accessor<{ left: number; top: number }>;\n  measure: () => void;\n  clearAnimationHandles: () => void;\n}\n\nexport const createAnchoredDropdown = (\n  containerRef: () => HTMLDivElement | undefined,\n  anchorAccessor: Accessor<DropdownAnchor | null>,\n): AnchoredDropdownResult => {\n  const [measuredWidth, setMeasuredWidth] = createSignal(0);\n  const [measuredHeight, setMeasuredHeight] = createSignal(0);\n  const [shouldMount, setShouldMount] = createSignal(false);\n  const [isAnimatedIn, setIsAnimatedIn] = createSignal(false);\n  const [viewportVersion, setViewportVersion] = createSignal(0);\n  const [lastAnchorEdge, setLastAnchorEdge] =\n    createSignal<DropdownAnchor[\"edge\"]>(\"bottom\");\n\n  let exitAnimationTimeout: ReturnType<typeof setTimeout> | undefined;\n  let enterAnimationFrameId: number | undefined;\n\n  const clearAnimationHandles = () => {\n    clearTimeout(exitAnimationTimeout);\n    if (enterAnimationFrameId !== undefined) {\n      nativeCancelAnimationFrame(enterAnimationFrameId);\n      enterAnimationFrameId = undefined;\n    }\n  };\n\n  const measure = () => {\n    const container = containerRef();\n    if (container) {\n      setMeasuredWidth(container.offsetWidth);\n      setMeasuredHeight(container.offsetHeight);\n    }\n  };\n\n  const handleViewportChange = () => {\n    setViewportVersion(\n      (previousViewportVersion) => previousViewportVersion + 1,\n    );\n    measure();\n  };\n\n  createEffect(() => {\n    const anchor = anchorAccessor();\n    if (anchor) {\n      setLastAnchorEdge(anchor.edge);\n      clearTimeout(exitAnimationTimeout);\n      setShouldMount(true);\n      if (enterAnimationFrameId !== undefined)\n        nativeCancelAnimationFrame(enterAnimationFrameId);\n      // HACK: rAF measures then forces reflow so the browser commits the correct position before transitioning in\n      enterAnimationFrameId = nativeRequestAnimationFrame(() => {\n        measure();\n        void containerRef()?.offsetHeight;\n        setIsAnimatedIn(true);\n      });\n    } else {\n      if (enterAnimationFrameId !== undefined)\n        nativeCancelAnimationFrame(enterAnimationFrameId);\n      setIsAnimatedIn(false);\n      exitAnimationTimeout = setTimeout(() => {\n        setShouldMount(false);\n      }, DROPDOWN_ANIMATION_DURATION_MS);\n    }\n    onCleanup(clearAnimationHandles);\n  });\n\n  createEffect(() => {\n    const anchor = anchorAccessor();\n    if (!anchor) return;\n\n    window.addEventListener(\"resize\", handleViewportChange);\n    window.visualViewport?.addEventListener(\"resize\", handleViewportChange);\n    window.visualViewport?.addEventListener(\"scroll\", handleViewportChange);\n\n    onCleanup(() => {\n      window.removeEventListener(\"resize\", handleViewportChange);\n      window.visualViewport?.removeEventListener(\n        \"resize\",\n        handleViewportChange,\n      );\n      window.visualViewport?.removeEventListener(\n        \"scroll\",\n        handleViewportChange,\n      );\n    });\n  });\n\n  const displayPosition = createMemo(\n    (previousPosition: { left: number; top: number }) => {\n      viewportVersion();\n      const position = getAnchoredDropdownPosition({\n        anchor: anchorAccessor(),\n        measuredWidth: measuredWidth(),\n        measuredHeight: measuredHeight(),\n        viewportWidth: window.innerWidth,\n        viewportHeight: window.innerHeight,\n        anchorGapPx: DROPDOWN_ANCHOR_GAP_PX,\n        viewportPaddingPx: DROPDOWN_VIEWPORT_PADDING_PX,\n        offscreenPosition: DROPDOWN_OFFSCREEN_POSITION,\n      });\n      if (position.left !== DROPDOWN_OFFSCREEN_POSITION.left) {\n        return position;\n      }\n      return previousPosition;\n    },\n    DROPDOWN_OFFSCREEN_POSITION,\n  );\n\n  return {\n    shouldMount,\n    isAnimatedIn,\n    lastAnchorEdge,\n    displayPosition,\n    measure,\n    clearAnimationHandles,\n  };\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/create-bounds-from-drag-rect.ts",
    "content": "import type { OverlayBounds } from \"../types.js\";\n\ninterface DragRectWithPageCoords {\n  pageX: number;\n  pageY: number;\n  width: number;\n  height: number;\n}\n\ninterface BaseBounds {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n}\n\nexport const createBoundsFromDragRect = (\n  dragRect: DragRectWithPageCoords,\n): OverlayBounds => ({\n  x: dragRect.pageX - window.scrollX,\n  y: dragRect.pageY - window.scrollY,\n  width: dragRect.width,\n  height: dragRect.height,\n  borderRadius: \"0px\",\n  transform: \"none\",\n});\n\nexport const createPageRectFromBounds = (\n  bounds: BaseBounds,\n): DragRectWithPageCoords => ({\n  pageX: bounds.x + window.scrollX,\n  pageY: bounds.y + window.scrollY,\n  width: bounds.width,\n  height: bounds.height,\n});\n\nexport const createFlatOverlayBounds = (bounds: BaseBounds): OverlayBounds => ({\n  ...bounds,\n  borderRadius: \"0px\",\n  transform: \"none\",\n});\n"
  },
  {
    "path": "packages/react-grab/src/utils/create-element-bounds.ts",
    "content": "import type { OverlayBounds } from \"../types.js\";\nimport {\n  stripTranslateFromMatrix,\n  stripTranslateFromTransformString,\n} from \"./strip-translate-from-transform.js\";\nimport {\n  BOUNDS_CACHE_TTL_MS,\n  MAX_TRANSFORM_ANCESTOR_DEPTH,\n  TRANSFORM_EARLY_BAIL_DEPTH,\n} from \"../constants.js\";\n\ninterface CachedBounds {\n  bounds: OverlayBounds;\n  timestamp: number;\n}\n\nlet boundsCache = new WeakMap<Element, CachedBounds>();\n\nexport const invalidateBoundsCache = () => {\n  boundsCache = new WeakMap<Element, CachedBounds>();\n};\n\nconst getAccumulatedTransform = (\n  element: Element,\n  selfTransform: string,\n): string => {\n  const hasSelfTransform = selfTransform && selfTransform !== \"none\";\n\n  let accumulated: DOMMatrix | null = null;\n  let current = element.parentElement;\n  let depth = 0;\n\n  while (\n    current &&\n    current !== document.documentElement &&\n    depth < MAX_TRANSFORM_ANCESTOR_DEPTH\n  ) {\n    const transformValue = window.getComputedStyle(current).transform;\n    if (transformValue && transformValue !== \"none\") {\n      accumulated = accumulated\n        ? new DOMMatrix(transformValue).multiply(accumulated)\n        : new DOMMatrix(transformValue);\n    } else if (\n      !hasSelfTransform &&\n      !accumulated &&\n      depth >= TRANSFORM_EARLY_BAIL_DEPTH\n    ) {\n      return \"none\";\n    }\n    current = current.parentElement;\n    depth++;\n  }\n\n  if (!accumulated) {\n    return hasSelfTransform\n      ? stripTranslateFromTransformString(selfTransform)\n      : \"none\";\n  }\n\n  if (hasSelfTransform) {\n    accumulated = accumulated.multiply(new DOMMatrix(selfTransform));\n  }\n\n  return stripTranslateFromMatrix(accumulated);\n};\n\nexport const createElementBounds = (element: Element): OverlayBounds => {\n  const now = performance.now();\n  const cached = boundsCache.get(element);\n\n  if (cached && now - cached.timestamp < BOUNDS_CACHE_TTL_MS) {\n    return cached.bounds;\n  }\n\n  const rect = element.getBoundingClientRect();\n  const style = window.getComputedStyle(element);\n  const transform = getAccumulatedTransform(element, style.transform);\n\n  let bounds: OverlayBounds;\n\n  if (transform !== \"none\" && element instanceof HTMLElement) {\n    const offsetWidth = element.offsetWidth;\n    const offsetHeight = element.offsetHeight;\n\n    if (offsetWidth > 0 && offsetHeight > 0) {\n      const centerX = rect.left + rect.width * 0.5;\n      const centerY = rect.top + rect.height * 0.5;\n\n      bounds = {\n        borderRadius: style.borderRadius || \"0px\",\n        height: offsetHeight,\n        transform,\n        width: offsetWidth,\n        x: centerX - offsetWidth * 0.5,\n        y: centerY - offsetHeight * 0.5,\n      };\n    } else {\n      bounds = {\n        borderRadius: style.borderRadius || \"0px\",\n        height: rect.height,\n        transform,\n        width: rect.width,\n        x: rect.left,\n        y: rect.top,\n      };\n    }\n  } else {\n    bounds = {\n      borderRadius: style.borderRadius || \"0px\",\n      height: rect.height,\n      transform,\n      width: rect.width,\n      x: rect.left,\n      y: rect.top,\n    };\n  }\n\n  boundsCache.set(element, { bounds, timestamp: now });\n  return bounds;\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/create-element-selector.ts",
    "content": "import { attr as defaultAttr, finder } from \"@medv/finder\";\nimport {\n  FINDER_TIMEOUT_MS,\n  SELECTOR_ATTR_VALUE_MAX_LENGTH_CHARS,\n} from \"../constants.js\";\n\nconst escapeCssIdentifier = (value: string): string => {\n  if (typeof CSS !== \"undefined\" && typeof CSS.escape === \"function\") {\n    return CSS.escape(value);\n  }\n  return value.replace(/[^a-zA-Z0-9_-]/g, (character) => `\\\\${character}`);\n};\n\nconst getFinderRoot = (element: Element): Element =>\n  element.ownerDocument.body ?? element.ownerDocument.documentElement;\n\nconst PREFERRED_SELECTOR_ATTRIBUTE_NAMES = new Set<string>([\n  \"data-testid\",\n  \"data-test-id\",\n  \"data-test\",\n  \"data-cy\",\n  \"data-qa\",\n  \"aria-label\",\n  \"role\",\n  \"name\",\n  \"title\",\n  \"alt\",\n]);\n\nconst isPreferredAttributeValueSafe = (value: string): boolean =>\n  value.length > 0 && value.length <= SELECTOR_ATTR_VALUE_MAX_LENGTH_CHARS;\n\nconst isSelectorUniqueForElement = (\n  element: Element,\n  selector: string,\n): boolean => {\n  try {\n    const matchingElements = element.ownerDocument.querySelectorAll(selector);\n    return matchingElements.length === 1 && matchingElements[0] === element;\n  } catch {\n    return false;\n  }\n};\n\nconst createFastElementSelector = (element: Element): string | null => {\n  if (element instanceof HTMLElement && element.id) {\n    const idSelector = `#${escapeCssIdentifier(element.id)}`;\n    if (isSelectorUniqueForElement(element, idSelector)) return idSelector;\n  }\n\n  for (const attributeName of PREFERRED_SELECTOR_ATTRIBUTE_NAMES) {\n    const attributeValue = element.getAttribute(attributeName);\n    if (!attributeValue) continue;\n    if (!isPreferredAttributeValueSafe(attributeValue)) continue;\n\n    const quotedValue = JSON.stringify(attributeValue);\n\n    const attributeOnlySelector = `[${attributeName}=${quotedValue}]`;\n    if (isSelectorUniqueForElement(element, attributeOnlySelector)) {\n      return attributeOnlySelector;\n    }\n\n    const tagSelector = `${element.tagName.toLowerCase()}${attributeOnlySelector}`;\n    if (isSelectorUniqueForElement(element, tagSelector)) {\n      return tagSelector;\n    }\n  }\n\n  return null;\n};\n\nconst createNthChildSelector = (element: Element): string => {\n  const segments: string[] = [];\n  const root = getFinderRoot(element);\n\n  let currentElement: Element | null = element;\n  while (currentElement) {\n    if (currentElement instanceof HTMLElement && currentElement.id) {\n      segments.unshift(`#${escapeCssIdentifier(currentElement.id)}`);\n      break;\n    }\n\n    const parentElement: HTMLElement | null = currentElement.parentElement;\n    if (!parentElement) {\n      segments.unshift(currentElement.tagName.toLowerCase());\n      break;\n    }\n\n    const siblings = Array.from(parentElement.children);\n    const siblingIndex = siblings.indexOf(currentElement);\n    const nthChild = siblingIndex >= 0 ? siblingIndex + 1 : 1;\n\n    segments.unshift(\n      `${currentElement.tagName.toLowerCase()}:nth-child(${nthChild})`,\n    );\n\n    if (parentElement === root) {\n      segments.unshift(root.tagName.toLowerCase());\n      break;\n    }\n\n    currentElement = parentElement;\n  }\n\n  return segments.join(\" > \");\n};\n\nexport const createElementSelector = (\n  element: Element,\n  shouldUseFinder = true,\n): string => {\n  const fastSelector = createFastElementSelector(element);\n  if (fastSelector) return fastSelector;\n\n  if (shouldUseFinder) {\n    try {\n      const selector = finder(element, {\n        root: getFinderRoot(element),\n        timeoutMs: FINDER_TIMEOUT_MS,\n        attr: (attributeName, attributeValue) =>\n          defaultAttr(attributeName, attributeValue) ||\n          (PREFERRED_SELECTOR_ATTRIBUTE_NAMES.has(attributeName) &&\n            isPreferredAttributeValueSafe(attributeValue)),\n      });\n      if (selector) return selector;\n      // HACK: @medv/finder can throw on edge-case DOM structures\n    } catch {}\n  }\n\n  return createNthChildSelector(element);\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/create-menu-highlight.ts",
    "content": "interface AnimatedBoundsFollowerOptions {\n  hiddenOpacity?: string;\n  visibleOpacity?: string;\n}\n\ninterface AnimatedBoundsFollowerController {\n  containerRef: (containerElement: HTMLElement) => void;\n  followerRef: (followerElement: HTMLElement) => void;\n  followElement: (targetElement: HTMLElement | undefined) => void;\n  hideFollower: () => void;\n}\n\ninterface MenuHighlightController {\n  containerRef: (containerElement: HTMLElement) => void;\n  highlightRef: (highlightElement: HTMLElement) => void;\n  updateHighlight: (targetElement: HTMLElement | undefined) => void;\n  clearHighlight: () => void;\n}\n\nconst DEFAULT_HIDDEN_OPACITY = \"0\";\nconst DEFAULT_VISIBLE_OPACITY = \"1\";\n\nconst createAnimatedBoundsFollower = ({\n  hiddenOpacity = DEFAULT_HIDDEN_OPACITY,\n  visibleOpacity = DEFAULT_VISIBLE_OPACITY,\n}: AnimatedBoundsFollowerOptions = {}): AnimatedBoundsFollowerController => {\n  let containerElement: HTMLElement | undefined;\n  let followerElement: HTMLElement | undefined;\n\n  const hideFollower = (): void => {\n    if (!followerElement) return;\n    followerElement.style.opacity = hiddenOpacity;\n  };\n\n  const followElement = (targetElement: HTMLElement | undefined): void => {\n    if (!followerElement || !containerElement) return;\n    if (!targetElement) {\n      hideFollower();\n      return;\n    }\n    const containerRect = containerElement.getBoundingClientRect();\n    const targetRect = targetElement.getBoundingClientRect();\n    const targetTopWithinContainer =\n      targetRect.top - containerRect.top + containerElement.scrollTop;\n    const targetLeftWithinContainer =\n      targetRect.left - containerRect.left + containerElement.scrollLeft;\n    followerElement.style.opacity = visibleOpacity;\n    followerElement.style.top = `${targetTopWithinContainer}px`;\n    followerElement.style.left = `${targetLeftWithinContainer}px`;\n    followerElement.style.width = `${targetRect.width}px`;\n    followerElement.style.height = `${targetRect.height}px`;\n  };\n\n  const setContainerRef = (containerNode: HTMLElement): void => {\n    containerElement = containerNode;\n  };\n\n  const setFollowerRef = (followerNode: HTMLElement): void => {\n    followerElement = followerNode;\n  };\n\n  return {\n    containerRef: setContainerRef,\n    followerRef: setFollowerRef,\n    followElement,\n    hideFollower,\n  };\n};\n\nexport const createMenuHighlight = (): MenuHighlightController => {\n  const {\n    containerRef,\n    followerRef: highlightRef,\n    followElement: updateHighlight,\n    hideFollower: clearHighlight,\n  } = createAnimatedBoundsFollower();\n\n  return {\n    containerRef,\n    highlightRef,\n    updateHighlight,\n    clearHighlight,\n  };\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/create-style-element.ts",
    "content": "export const createStyleElement = (\n  attribute: string,\n  content: string,\n): HTMLStyleElement => {\n  const element = document.createElement(\"style\");\n  element.setAttribute(attribute, \"\");\n  element.textContent = content;\n  document.head.appendChild(element);\n  return element;\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/create-toolbar-drag.ts",
    "content": "import { createSignal, onCleanup } from \"solid-js\";\nimport type { Accessor } from \"solid-js\";\nimport type { Position } from \"../types.js\";\nimport type { SnapEdge } from \"../components/toolbar/state.js\";\nimport {\n  TOOLBAR_DRAG_THRESHOLD_PX,\n  TOOLBAR_SNAP_ANIMATION_DURATION_MS,\n} from \"../constants.js\";\nimport { nativeRequestAnimationFrame } from \"./native-raf.js\";\nimport {\n  getRatioFromPosition,\n  getPositionFromEdgeAndRatio,\n  getSnapPosition,\n} from \"./toolbar-position.js\";\n\ninterface ToolbarDragConfig {\n  getContainerRef: () => HTMLDivElement | undefined;\n  isCollapsed: Accessor<boolean>;\n  getExpandedDimensions: () => { width: number; height: number };\n  onDragStart: () => void;\n  onPositionUpdate: (position: Position) => void;\n  onSnapEdgeChange: (edge: SnapEdge, ratio: number) => void;\n  onSnapComplete: (result: {\n    edge: SnapEdge;\n    ratio: number;\n    position: Position;\n    expandedDimensions: { width: number; height: number };\n  }) => void;\n  onSnapAnimationEnd: () => void;\n}\n\ninterface ToolbarDragResult {\n  isDragging: Accessor<boolean>;\n  isSnapping: Accessor<boolean>;\n  handlePointerDown: (event: PointerEvent) => void;\n  createDragAwareHandler: (callback: () => void) => (event: MouseEvent) => void;\n}\n\nexport const createToolbarDrag = (\n  config: ToolbarDragConfig,\n): ToolbarDragResult => {\n  const [isDragging, setIsDragging] = createSignal(false);\n  const [isSnapping, setIsSnapping] = createSignal(false);\n  const [hasDragMoved, setHasDragMoved] = createSignal(false);\n  const [velocity, setVelocity] = createSignal<Position>({ x: 0, y: 0 });\n  let dragOffset: Position = { x: 0, y: 0 };\n\n  let lastPointerPosition = { x: 0, y: 0, time: 0 };\n  let pointerStartPosition = { x: 0, y: 0 };\n  let didDragOccur = false;\n  let snapAnimationTimeout: ReturnType<typeof setTimeout> | undefined;\n\n  const handleWindowPointerMove = (event: PointerEvent) => {\n    if (!isDragging()) return;\n\n    if (!hasDragMoved()) {\n      const distanceMoved = Math.sqrt(\n        Math.pow(event.clientX - pointerStartPosition.x, 2) +\n          Math.pow(event.clientY - pointerStartPosition.y, 2),\n      );\n      if (distanceMoved <= TOOLBAR_DRAG_THRESHOLD_PX) {\n        return;\n      }\n      setHasDragMoved(true);\n      config.onDragStart();\n    }\n\n    const now = performance.now();\n    const deltaTime = now - lastPointerPosition.time;\n\n    if (deltaTime > 0) {\n      const newVelocityX = (event.clientX - lastPointerPosition.x) / deltaTime;\n      const newVelocityY = (event.clientY - lastPointerPosition.y) / deltaTime;\n      setVelocity({ x: newVelocityX, y: newVelocityY });\n    }\n\n    lastPointerPosition = { x: event.clientX, y: event.clientY, time: now };\n\n    const newX = event.clientX - dragOffset.x;\n    const newY = event.clientY - dragOffset.y;\n\n    config.onPositionUpdate({ x: newX, y: newY });\n  };\n\n  const handleWindowPointerUp = () => {\n    if (!isDragging()) return;\n\n    window.removeEventListener(\"pointermove\", handleWindowPointerMove);\n    window.removeEventListener(\"pointerup\", handleWindowPointerUp);\n\n    const didMove = hasDragMoved();\n    setIsDragging(false);\n\n    if (!didMove) {\n      return;\n    }\n\n    didDragOccur = true;\n\n    const containerRef = config.getContainerRef();\n    const rect = containerRef?.getBoundingClientRect();\n    if (!rect) return;\n\n    const currentVelocity = velocity();\n    const snap = getSnapPosition(\n      rect.left,\n      rect.top,\n      rect.width,\n      rect.height,\n      currentVelocity.x,\n      currentVelocity.y,\n    );\n    const ratio = getRatioFromPosition(\n      snap.edge,\n      snap.x,\n      snap.y,\n      rect.width,\n      rect.height,\n    );\n\n    config.onSnapEdgeChange(snap.edge, ratio);\n    setIsSnapping(true);\n\n    nativeRequestAnimationFrame(() => {\n      const postRenderRect = containerRef?.getBoundingClientRect();\n      const updatedDimensions = postRenderRect\n        ? { width: postRenderRect.width, height: postRenderRect.height }\n        : config.getExpandedDimensions();\n\n      nativeRequestAnimationFrame(() => {\n        const snappedPosition = getPositionFromEdgeAndRatio(\n          snap.edge,\n          ratio,\n          updatedDimensions.width,\n          updatedDimensions.height,\n        );\n\n        config.onSnapComplete({\n          edge: snap.edge,\n          ratio,\n          position: snappedPosition,\n          expandedDimensions: updatedDimensions,\n        });\n\n        snapAnimationTimeout = setTimeout(() => {\n          setIsSnapping(false);\n          config.onSnapAnimationEnd();\n        }, TOOLBAR_SNAP_ANIMATION_DURATION_MS);\n      });\n    });\n  };\n\n  const handlePointerDown = (event: PointerEvent) => {\n    if (config.isCollapsed()) return;\n\n    const containerRef = config.getContainerRef();\n    const rect = containerRef?.getBoundingClientRect();\n    if (!rect) return;\n\n    pointerStartPosition = { x: event.clientX, y: event.clientY };\n\n    dragOffset = {\n      x: event.clientX - rect.left,\n      y: event.clientY - rect.top,\n    };\n    setIsDragging(true);\n    setHasDragMoved(false);\n    setVelocity({ x: 0, y: 0 });\n    lastPointerPosition = {\n      x: event.clientX,\n      y: event.clientY,\n      time: performance.now(),\n    };\n\n    window.addEventListener(\"pointermove\", handleWindowPointerMove);\n    window.addEventListener(\"pointerup\", handleWindowPointerUp);\n  };\n\n  const createDragAwareHandler =\n    (callback: () => void) => (event: MouseEvent) => {\n      event.stopImmediatePropagation();\n      if (didDragOccur) {\n        didDragOccur = false;\n        return;\n      }\n      callback();\n    };\n\n  onCleanup(() => {\n    window.removeEventListener(\"pointermove\", handleWindowPointerMove);\n    window.removeEventListener(\"pointerup\", handleWindowPointerUp);\n    clearTimeout(snapAnimationTimeout);\n  });\n\n  return {\n    isDragging,\n    isSnapping,\n    handlePointerDown,\n    createDragAwareHandler,\n  };\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/extract-element-css.ts",
    "content": "import { RELEVANT_CSS_PROPERTIES } from \"../constants.js\";\n\nconst BORDER_FILTER_SIDE_MAP = new Map(\n  ([\"top\", \"right\", \"bottom\", \"left\"] as const).flatMap((side) => [\n    [`border-${side}-style`, side],\n    [`border-${side}-color`, side],\n  ]),\n);\n\nlet baselineIframe: HTMLIFrameElement | null = null;\nconst defaultStylesByTag = new Map<string, Map<string, string>>();\n\nconst ensureBaselineIframe = (): HTMLIFrameElement => {\n  if (baselineIframe) return baselineIframe;\n\n  baselineIframe = document.createElement(\"iframe\");\n  baselineIframe.style.cssText =\n    \"position:fixed;left:-9999px;width:0;height:0;border:none;visibility:hidden;\";\n  document.body.appendChild(baselineIframe);\n  return baselineIframe;\n};\n\nconst getDefaultStylesForTag = (tagName: string): Map<string, string> => {\n  const cached = defaultStylesByTag.get(tagName);\n  if (cached) return cached;\n\n  const iframe = ensureBaselineIframe();\n  const iframeDocument = iframe.contentDocument!;\n  const baselineElement = iframeDocument.createElement(tagName);\n  iframeDocument.body.appendChild(baselineElement);\n\n  const baselineComputed =\n    iframe.contentWindow!.getComputedStyle(baselineElement);\n  const defaultStyles = new Map<string, string>();\n\n  for (const propertyName of RELEVANT_CSS_PROPERTIES) {\n    const propertyValue = baselineComputed.getPropertyValue(propertyName);\n    if (propertyValue) {\n      defaultStyles.set(propertyName, propertyValue);\n    }\n  }\n\n  baselineElement.remove();\n  defaultStylesByTag.set(tagName, defaultStyles);\n  return defaultStyles;\n};\n\nconst isBorderPropertyWithoutWidth = (\n  propertyName: string,\n  computedStyle: CSSStyleDeclaration,\n): boolean => {\n  const side = BORDER_FILTER_SIDE_MAP.get(propertyName);\n  if (!side) return false;\n  const widthValue = computedStyle.getPropertyValue(`border-${side}-width`);\n  return widthValue === \"0px\" || widthValue === \"0\";\n};\n\nexport const extractElementCss = (element: Element): string => {\n  const tagName = element.tagName.toLowerCase();\n  const defaultStyles = getDefaultStylesForTag(tagName);\n  const computedStyle = getComputedStyle(element);\n  const declarations: string[] = [];\n\n  for (const propertyName of RELEVANT_CSS_PROPERTIES) {\n    const propertyValue = computedStyle.getPropertyValue(propertyName);\n    if (!propertyValue) continue;\n    if (propertyValue === defaultStyles.get(propertyName)) continue;\n    if (isBorderPropertyWithoutWidth(propertyName, computedStyle)) continue;\n\n    declarations.push(`${propertyName}: ${propertyValue};`);\n  }\n\n  const classAttribute = element.getAttribute(\"class\")?.trim();\n  const cssBlock = declarations.join(\"\\n\");\n  if (!classAttribute) return cssBlock;\n  if (!cssBlock) return `className: ${classAttribute}`;\n  return `className: ${classAttribute}\\n\\n${cssBlock}`;\n};\n\nexport const disposeBaselineStyles = (): void => {\n  baselineIframe?.remove();\n  baselineIframe = null;\n  defaultStylesByTag.clear();\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/format-relative-time.ts",
    "content": "const SECONDS_PER_MINUTE = 60;\nconst MINUTES_PER_HOUR = 60;\nconst HOURS_PER_DAY = 24;\n\nexport const formatRelativeTime = (timestamp: number): string => {\n  const elapsedSeconds = Math.floor((Date.now() - timestamp) / 1000);\n  if (elapsedSeconds < SECONDS_PER_MINUTE) return \"now\";\n  const elapsedMinutes = Math.floor(elapsedSeconds / SECONDS_PER_MINUTE);\n  if (elapsedMinutes < MINUTES_PER_HOUR) return `${elapsedMinutes}m`;\n  const elapsedHours = Math.floor(elapsedMinutes / MINUTES_PER_HOUR);\n  if (elapsedHours < HOURS_PER_DAY) return `${elapsedHours}h`;\n  return `${Math.floor(elapsedHours / HOURS_PER_DAY)}d`;\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/format-shortcut.ts",
    "content": "import { isMac } from \"./is-mac.js\";\n\nexport const formatShortcut = (shortcut: string): string => {\n  if (shortcut === \"Enter\") {\n    return \"↵\";\n  }\n\n  if (isMac()) {\n    return `⌘${shortcut}`;\n  }\n\n  const normalizedShortcut = shortcut.replace(\"⇧\", \"Shift+\");\n  return `Ctrl+${normalizedShortcut}`;\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/freeze-animations.ts",
    "content": "import { FROZEN_ELEMENT_ATTRIBUTE } from \"../constants.js\";\nimport { createStyleElement } from \"./create-style-element.js\";\nimport { freezeGsap, unfreezeGsap } from \"./freeze-gsap.js\";\n\nconst FROZEN_STYLES = `\n[${FROZEN_ELEMENT_ATTRIBUTE}],\n[${FROZEN_ELEMENT_ATTRIBUTE}] * {\n  animation-play-state: paused !important;\n  transition: none !important;\n}\n`;\n\nconst GLOBAL_FREEZE_STYLES = `\n*, *::before, *::after {\n  animation-play-state: paused !important;\n  transition: none !important;\n}\n`;\n\nconst SVG_ROOT_SELECTOR = \"svg\";\n\nlet styleElement: HTMLStyleElement | null = null;\nlet frozenElements: Element[] = [];\nlet frozenSvgElements: SVGSVGElement[] = [];\nlet lastInputElements: Element[] = [];\n\nlet globalAnimationStyleElement: HTMLStyleElement | null = null;\nlet globalFrozenSvgElements: SVGSVGElement[] = [];\nconst svgFreezeDepthMap = new Map<SVGSVGElement, number>();\nlet frozenWaapiAnimations: Animation[] = [];\n\nconst ensureStylesInjected = (): void => {\n  if (styleElement) return;\n  styleElement = createStyleElement(\n    \"data-react-grab-frozen-styles\",\n    FROZEN_STYLES,\n  );\n};\n\nconst areElementsSame = (\n  firstElements: Element[],\n  secondElements: Element[],\n): boolean =>\n  firstElements.length === secondElements.length &&\n  firstElements.every(\n    (currentElement, index) => currentElement === secondElements[index],\n  );\n\nconst collectFrozenSvgElements = (elements: Element[]): SVGSVGElement[] => {\n  const svgElements = new Set<SVGSVGElement>();\n\n  for (const element of elements) {\n    if (element instanceof SVGSVGElement) {\n      svgElements.add(element);\n    } else if (element instanceof SVGElement && element.ownerSVGElement) {\n      svgElements.add(element.ownerSVGElement);\n    }\n\n    for (const innerSvgElement of element.querySelectorAll(SVG_ROOT_SELECTOR)) {\n      if (innerSvgElement instanceof SVGSVGElement) {\n        svgElements.add(innerSvgElement);\n      }\n    }\n  }\n\n  return [...svgElements];\n};\n\nconst callSvgAnimationMethod = (\n  svgElement: SVGSVGElement,\n  methodName: \"pauseAnimations\" | \"unpauseAnimations\",\n): void => {\n  const animationMethod = Reflect.get(svgElement, methodName);\n  if (typeof animationMethod !== \"function\") return;\n  animationMethod.call(svgElement);\n};\n\nconst pauseSvgAnimations = (svgElements: SVGSVGElement[]): void => {\n  for (const svgElement of svgElements) {\n    const currentFreezeDepth = svgFreezeDepthMap.get(svgElement) ?? 0;\n    if (currentFreezeDepth === 0) {\n      callSvgAnimationMethod(svgElement, \"pauseAnimations\");\n    }\n    svgFreezeDepthMap.set(svgElement, currentFreezeDepth + 1);\n  }\n};\n\nconst resumeSvgAnimations = (svgElements: SVGSVGElement[]): void => {\n  for (const svgElement of svgElements) {\n    const currentFreezeDepth = svgFreezeDepthMap.get(svgElement);\n    if (!currentFreezeDepth) continue;\n\n    if (currentFreezeDepth === 1) {\n      svgFreezeDepthMap.delete(svgElement);\n      callSvgAnimationMethod(svgElement, \"unpauseAnimations\");\n      continue;\n    }\n\n    svgFreezeDepthMap.set(svgElement, currentFreezeDepth - 1);\n  }\n};\n\nconst collectWaapiAnimations = (elements: Element[]): Animation[] => {\n  const animations: Animation[] = [];\n  for (const element of elements) {\n    for (const animation of element.getAnimations({ subtree: true })) {\n      if (animation.playState === \"running\") {\n        animations.push(animation);\n      }\n    }\n  }\n  return animations;\n};\n\nconst finishAnimations = (animations: Iterable<Animation>): void => {\n  for (const animation of animations) {\n    try {\n      animation.finish();\n    } catch {\n      // finish() throws for infinite animations or zero playback rate\n    }\n  }\n};\n\nexport const freezeAllAnimations = (elements: Element[]): void => {\n  if (elements.length === 0) return;\n  if (areElementsSame(elements, lastInputElements)) return;\n\n  unfreezeAllAnimations();\n  lastInputElements = [...elements];\n  ensureStylesInjected();\n  frozenElements = elements;\n  frozenSvgElements = collectFrozenSvgElements(frozenElements);\n  pauseSvgAnimations(frozenSvgElements);\n\n  for (const element of frozenElements) {\n    element.setAttribute(FROZEN_ELEMENT_ATTRIBUTE, \"\");\n  }\n\n  frozenWaapiAnimations = collectWaapiAnimations(frozenElements);\n  for (const animation of frozenWaapiAnimations) {\n    animation.pause();\n  }\n};\n\nconst unfreezeAllAnimations = (): void => {\n  if (\n    frozenElements.length === 0 &&\n    frozenSvgElements.length === 0 &&\n    frozenWaapiAnimations.length === 0\n  )\n    return;\n\n  for (const element of frozenElements) {\n    element.removeAttribute(FROZEN_ELEMENT_ATTRIBUTE);\n  }\n  resumeSvgAnimations(frozenSvgElements);\n\n  finishAnimations(frozenWaapiAnimations);\n\n  frozenElements = [];\n  frozenSvgElements = [];\n  frozenWaapiAnimations = [];\n  lastInputElements = [];\n};\n\nexport const freezeAnimations = (elements: Element[]): (() => void) => {\n  if (elements.length === 0) {\n    unfreezeAllAnimations();\n    return () => {};\n  }\n\n  freezeAllAnimations(elements);\n  return unfreezeAllAnimations;\n};\n\nexport const freezeGlobalAnimations = (): void => {\n  if (globalAnimationStyleElement) return;\n\n  globalAnimationStyleElement = createStyleElement(\n    \"data-react-grab-global-freeze\",\n    GLOBAL_FREEZE_STYLES,\n  );\n  globalFrozenSvgElements = collectFrozenSvgElements(\n    Array.from(document.querySelectorAll(SVG_ROOT_SELECTOR)),\n  );\n  pauseSvgAnimations(globalFrozenSvgElements);\n  freezeGsap();\n};\n\nexport const unfreezeGlobalAnimations = (): void => {\n  if (!globalAnimationStyleElement) return;\n\n  // HACK: Finish all paused CSS animations before removing the freeze style.\n  // Simply removing the pause causes animations to resume from mid-point,\n  // creating visual \"jumps\" (e.g., dropdowns snapping through entry animation).\n  // Finishing advances them to their end state instead.\n  globalAnimationStyleElement.textContent = `\n*, *::before, *::after {\n  transition: none !important;\n}\n`;\n\n  const animations: Animation[] = [];\n  for (const animation of document.getAnimations()) {\n    if (animation.effect instanceof KeyframeEffect) {\n      const target = animation.effect.target;\n      if (target instanceof Element) {\n        const rootNode = target.getRootNode();\n        if (rootNode instanceof ShadowRoot) {\n          continue;\n        }\n      }\n    }\n    animations.push(animation);\n  }\n  finishAnimations(animations);\n\n  globalAnimationStyleElement.remove();\n  globalAnimationStyleElement = null;\n  resumeSvgAnimations(globalFrozenSvgElements);\n  globalFrozenSvgElements = [];\n  unfreezeGsap();\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/freeze-gsap.ts",
    "content": "/**\n * GSAP rAF interception\n *\n * GSAP drives animations through an internal `_tick` function scheduled via\n * requestAnimationFrame. We wrap rAF at module load time so GSAP captures our\n * wrapper. When frozen and `window.gsapVersions` exists (set by GSAP on import),\n * we inspect the call stack for `_tick` and hold matching callbacks.\n *\n * Stack inspection is deferred until freeze, so `Error()` is never paid during\n * normal operation. Detected callbacks are cached in a WeakSet for free lookups.\n */\n\nimport {\n  nativeCancelAnimationFrame,\n  nativeRequestAnimationFrame,\n} from \"./native-raf.js\";\n\nlet isRafFrozen = false;\nconst pendingRafCallbacks = new Map<number, FrameRequestCallback>();\nlet nextFakeRafId = -1;\nconst knownAnimationCallbacks = new WeakSet<FrameRequestCallback>();\nconst nativeIdToHeldId = new Map<number, number>();\nconst replayedFakeToNativeId = new Map<\n  number,\n  { nativeId: number; callback: FrameRequestCallback }\n>();\n\nconst isAnimationLibraryCallback = (\n  callback: FrameRequestCallback,\n): boolean => {\n  if (knownAnimationCallbacks.has(callback)) return true;\n  if (!isRafFrozen || !(\"gsapVersions\" in window)) return false;\n\n  const stack = new Error().stack ?? \"\";\n  if (!stack.includes(\"_tick\")) return false;\n\n  knownAnimationCallbacks.add(callback);\n  return true;\n};\n\nif (typeof window !== \"undefined\") {\n  window.requestAnimationFrame = (callback: FrameRequestCallback): number => {\n    if (!isAnimationLibraryCallback(callback)) {\n      return nativeRequestAnimationFrame(callback);\n    }\n\n    if (isRafFrozen) {\n      const identifier = nextFakeRafId--;\n      pendingRafCallbacks.set(identifier, callback);\n      return identifier;\n    }\n\n    const nativeId = nativeRequestAnimationFrame(\n      (timestamp: DOMHighResTimeStamp) => {\n        if (isRafFrozen) {\n          const identifier = nextFakeRafId--;\n          pendingRafCallbacks.set(identifier, callback);\n          nativeIdToHeldId.set(nativeId, identifier);\n          return;\n        }\n        callback(timestamp);\n      },\n    );\n    return nativeId;\n  };\n\n  window.cancelAnimationFrame = (identifier: number): void => {\n    if (pendingRafCallbacks.has(identifier)) {\n      pendingRafCallbacks.delete(identifier);\n      return;\n    }\n    const replayed = replayedFakeToNativeId.get(identifier);\n    if (replayed !== undefined) {\n      nativeCancelAnimationFrame(replayed.nativeId);\n      replayedFakeToNativeId.delete(identifier);\n      return;\n    }\n    const heldId = nativeIdToHeldId.get(identifier);\n    if (heldId !== undefined) {\n      pendingRafCallbacks.delete(heldId);\n      nativeIdToHeldId.delete(identifier);\n      return;\n    }\n    nativeCancelAnimationFrame(identifier);\n  };\n}\n\nexport const freezeGsap = (): void => {\n  if (isRafFrozen) return;\n  isRafFrozen = true;\n  pendingRafCallbacks.clear();\n  nativeIdToHeldId.clear();\n  for (const [fakeId, { nativeId, callback }] of replayedFakeToNativeId) {\n    nativeCancelAnimationFrame(nativeId);\n    pendingRafCallbacks.set(fakeId, callback);\n  }\n  replayedFakeToNativeId.clear();\n};\n\nexport const unfreezeGsap = (): void => {\n  if (!isRafFrozen) return;\n  isRafFrozen = false;\n\n  for (const [fakeId, callback] of pendingRafCallbacks.entries()) {\n    const nativeId = nativeRequestAnimationFrame((timestamp) => {\n      replayedFakeToNativeId.delete(fakeId);\n      callback(timestamp);\n    });\n    replayedFakeToNativeId.set(fakeId, { nativeId, callback });\n  }\n  pendingRafCallbacks.clear();\n  nativeIdToHeldId.clear();\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/freeze-pseudo-states.ts",
    "content": "import { clearElementPositionCache } from \"./get-element-at-position.js\";\nimport { createStyleElement } from \"./create-style-element.js\";\n\nconst POINTER_EVENTS_STYLES = \"html { pointer-events: none !important; }\";\n\nconst MOUSE_EVENTS_TO_BLOCK = [\n  \"mouseenter\",\n  \"mouseleave\",\n  \"mouseover\",\n  \"mouseout\",\n  \"pointerenter\",\n  \"pointerleave\",\n  \"pointerover\",\n  \"pointerout\",\n] as const;\n\nconst FOCUS_EVENTS_TO_BLOCK = [\"focus\", \"blur\", \"focusin\", \"focusout\"] as const;\n\nconst HOVER_STYLE_PROPERTIES = [\n  \"background-color\",\n  \"color\",\n  \"border-color\",\n  \"box-shadow\",\n  \"transform\",\n  \"opacity\",\n  \"outline\",\n  \"filter\",\n  \"scale\",\n  \"visibility\",\n] as const;\n\nconst FOCUS_STYLE_PROPERTIES = [\n  \"background-color\",\n  \"color\",\n  \"border-color\",\n  \"box-shadow\",\n  \"outline\",\n  \"outline-offset\",\n  \"outline-width\",\n  \"outline-color\",\n  \"outline-style\",\n  \"filter\",\n  \"opacity\",\n  \"ring-color\",\n  \"ring-width\",\n] as const;\n\ninterface FrozenPseudoState {\n  element: HTMLElement;\n  frozenStyles: string;\n  originalPropertyValues: Map<string, string>;\n}\n\nconst frozenHoverElements = new Map<HTMLElement, Map<string, string>>();\nconst frozenFocusElements = new Map<HTMLElement, Map<string, string>>();\nlet pointerEventsStyle: HTMLStyleElement | null = null;\n\nconst stopEvent = (event: Event): void => {\n  event.stopImmediatePropagation();\n};\n\nconst preventFocusChange = (event: Event): void => {\n  event.preventDefault();\n  event.stopImmediatePropagation();\n};\n\nconst collectOriginalPropertyValues = (\n  element: HTMLElement,\n  properties: readonly string[],\n): Map<string, string> => {\n  const originalPropertyValues = new Map<string, string>();\n  for (const prop of properties) {\n    const inlineValue = element.style.getPropertyValue(prop);\n    if (inlineValue) {\n      originalPropertyValues.set(prop, inlineValue);\n    }\n  }\n  return originalPropertyValues;\n};\n\nconst collectPseudoStates = (\n  selector: string,\n  properties: readonly string[],\n  alreadyFrozen?: Map<HTMLElement, Map<string, string>>,\n): FrozenPseudoState[] => {\n  const elementsToFreeze: FrozenPseudoState[] = [];\n\n  for (const element of document.querySelectorAll(selector)) {\n    if (!(element instanceof HTMLElement)) continue;\n    if (alreadyFrozen?.has(element)) continue;\n\n    const computed = getComputedStyle(element);\n    let frozenStyles = element.style.cssText;\n    const originalPropertyValues = collectOriginalPropertyValues(\n      element,\n      properties,\n    );\n\n    for (const prop of properties) {\n      const computedValue = computed.getPropertyValue(prop);\n      if (computedValue) {\n        frozenStyles += `${prop}: ${computedValue} !important; `;\n      }\n    }\n\n    elementsToFreeze.push({ element, frozenStyles, originalPropertyValues });\n  }\n\n  return elementsToFreeze;\n};\n\nconst applyFrozenStates = (\n  states: FrozenPseudoState[],\n  storageMap: Map<HTMLElement, Map<string, string>>,\n): void => {\n  for (const { element, frozenStyles, originalPropertyValues } of states) {\n    storageMap.set(element, originalPropertyValues);\n    element.style.cssText = frozenStyles;\n  }\n};\n\nconst restoreFrozenStates = (\n  storageMap: Map<HTMLElement, Map<string, string>>,\n  styleProperties: readonly string[],\n): void => {\n  for (const [element, originalPropertyValues] of storageMap) {\n    for (const prop of styleProperties) {\n      const originalValue = originalPropertyValues.get(prop);\n      if (originalValue) {\n        element.style.setProperty(prop, originalValue);\n      } else {\n        element.style.removeProperty(prop);\n      }\n    }\n  }\n  storageMap.clear();\n};\n\nexport const suspendPointerEventsFreeze = (): void => {\n  if (pointerEventsStyle) pointerEventsStyle.disabled = true;\n};\n\nexport const resumePointerEventsFreeze = (): void => {\n  if (pointerEventsStyle) pointerEventsStyle.disabled = false;\n};\n\nexport const freezePseudoStates = (): void => {\n  if (pointerEventsStyle) return;\n\n  for (const eventType of MOUSE_EVENTS_TO_BLOCK) {\n    document.addEventListener(eventType, stopEvent, true);\n  }\n\n  for (const eventType of FOCUS_EVENTS_TO_BLOCK) {\n    document.addEventListener(eventType, preventFocusChange, true);\n  }\n\n  const hoverStates = collectPseudoStates(\":hover\", HOVER_STYLE_PROPERTIES);\n  const focusStates = collectPseudoStates(\n    \":focus, :focus-visible\",\n    FOCUS_STYLE_PROPERTIES,\n    frozenFocusElements,\n  );\n\n  applyFrozenStates(hoverStates, frozenHoverElements);\n  applyFrozenStates(focusStates, frozenFocusElements);\n\n  pointerEventsStyle = createStyleElement(\n    \"data-react-grab-frozen-pseudo\",\n    POINTER_EVENTS_STYLES,\n  );\n};\n\nexport const unfreezePseudoStates = (): void => {\n  clearElementPositionCache();\n\n  for (const eventType of MOUSE_EVENTS_TO_BLOCK) {\n    document.removeEventListener(eventType, stopEvent, true);\n  }\n\n  for (const eventType of FOCUS_EVENTS_TO_BLOCK) {\n    document.removeEventListener(eventType, preventFocusChange, true);\n  }\n\n  restoreFrozenStates(frozenHoverElements, HOVER_STYLE_PROPERTIES);\n  restoreFrozenStates(frozenFocusElements, FOCUS_STYLE_PROPERTIES);\n\n  pointerEventsStyle?.remove();\n  pointerEventsStyle = null;\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/freeze-updates.ts",
    "content": "import {\n  _fiberRoots,\n  getRDTHook,\n  getFiberFromHostInstance,\n  isCompositeFiber,\n  type Fiber,\n  type ReactRenderer,\n  type FiberRoot,\n} from \"bippy\";\nimport { logRecoverableError } from \"./log-recoverable-error.js\";\n\ninterface FiberRootLike extends FiberRoot {\n  current: Fiber | null;\n}\n\ninterface PendingUpdate {\n  next: PendingUpdate | null;\n  action: unknown;\n  [key: string]: unknown;\n}\n\ninterface HookQueue {\n  pending?: unknown;\n  dispatch?: ((...args: unknown[]) => void) | null;\n  getSnapshot?: () => unknown;\n}\n\ninterface HookState {\n  queue: HookQueue | null;\n  next: HookState | null;\n}\n\ninterface ContextDependency {\n  memoizedValue: unknown;\n  next: ContextDependency | null;\n}\n\ninterface PausedQueueState {\n  originalGetSnapshot?: () => unknown;\n  snapshotValueAtPause?: unknown;\n  originalPendingDescriptor?: PropertyDescriptor;\n  pendingValueAtPause?: PendingUpdate | null;\n  bufferedPending?: PendingUpdate | null;\n}\n\ninterface PausedContextState {\n  originalDescriptor?: PropertyDescriptor;\n  frozenValue: unknown;\n  pendingValue?: unknown;\n  didReceivePendingValue?: boolean;\n}\n\nlet isUpdatesPaused = false;\n\nconst getOrCache = <K extends object, V>(\n  cache: WeakMap<K, V>,\n  key: K,\n  create: () => V,\n): V => {\n  const cached = cache.get(key);\n  if (cached) return cached;\n  const value = create();\n  cache.set(key, value);\n  return value;\n};\n\ntype DispatchFunction = (...args: unknown[]) => void;\ntype TransitionFunction = (callback: () => void) => void;\n\ninterface OriginalHooks {\n  useState: DispatchFunction;\n  useReducer: DispatchFunction;\n  useTransition: DispatchFunction;\n  useSyncExternalStore: DispatchFunction;\n}\n\nconst patchedDispatchers = new WeakMap<object, OriginalHooks>();\nconst wrappedDispatchCache = new WeakMap<DispatchFunction, DispatchFunction>();\nconst wrappedStartTransitionCache = new WeakMap<\n  TransitionFunction,\n  TransitionFunction\n>();\nconst pendingStoreCallbacks = new Set<() => void>();\nconst pendingTransitionCallbacks: Array<() => void> = [];\nconst pendingStateUpdates: Array<() => void> = [];\nconst pausedQueueStates = new WeakMap<HookQueue, PausedQueueState>();\nconst pausedContextStates = new WeakMap<\n  ContextDependency,\n  PausedContextState\n>();\nconst renderersWithPatchedDispatcher = new WeakSet<ReactRenderer>();\nconst typedFiberRoots = _fiberRoots as Set<FiberRootLike>;\n\nconst getFiberRoot = (fiber: Fiber): FiberRootLike | null => {\n  let current: Fiber | null = fiber;\n  while (current.return) {\n    current = current.return;\n  }\n  return (current.stateNode ?? null) as FiberRootLike | null;\n};\n\nconst collectFiberRoots = (): Set<FiberRootLike> => {\n  if (typedFiberRoots.size > 0) {\n    return typedFiberRoots;\n  }\n\n  const collectedRoots = new Set<FiberRootLike>();\n\n  const traverseDOM = (element: Element): void => {\n    const fiber = getFiberFromHostInstance(element);\n    if (fiber) {\n      const fiberRoot = getFiberRoot(fiber);\n      if (fiberRoot) collectedRoots.add(fiberRoot);\n      return;\n    }\n    for (const childElement of Array.from(element.children)) {\n      traverseDOM(childElement);\n      if (collectedRoots.size > 0) return;\n    }\n  };\n\n  traverseDOM(document.body);\n  return collectedRoots;\n};\n\nconst mergePendingChains = (\n  original: PendingUpdate | null,\n  buffered: PendingUpdate | null,\n): PendingUpdate | null => {\n  if (!original) return buffered;\n  if (!buffered) return original;\n  if (!original.next || !buffered.next) return buffered;\n\n  const originalFirst = original.next;\n  const bufferedFirst = buffered.next;\n  const isOriginalSingle = original === originalFirst;\n  const isBufferedSingle = buffered === bufferedFirst;\n\n  if (isOriginalSingle && isBufferedSingle) {\n    original.next = buffered;\n    buffered.next = original;\n  } else if (isOriginalSingle) {\n    original.next = bufferedFirst;\n    buffered.next = original;\n  } else if (isBufferedSingle) {\n    buffered.next = originalFirst;\n    original.next = buffered;\n  } else {\n    original.next = bufferedFirst;\n    buffered.next = originalFirst;\n  }\n\n  return buffered;\n};\n\nconst pauseHookQueue = (queue: HookQueue): void => {\n  if (!queue || pausedQueueStates.has(queue)) return;\n\n  const pauseState: PausedQueueState = {\n    originalPendingDescriptor: Object.getOwnPropertyDescriptor(\n      queue,\n      \"pending\",\n    ),\n    pendingValueAtPause: queue.pending as PendingUpdate | null,\n    bufferedPending: null,\n  };\n\n  if (typeof queue.getSnapshot === \"function\") {\n    pauseState.originalGetSnapshot = queue.getSnapshot;\n    pauseState.snapshotValueAtPause = queue.getSnapshot();\n    queue.getSnapshot = () =>\n      isUpdatesPaused\n        ? pauseState.snapshotValueAtPause\n        : pauseState.originalGetSnapshot!();\n  }\n\n  let currentPendingValue = pauseState.pendingValueAtPause;\n\n  Object.defineProperty(queue, \"pending\", {\n    configurable: true,\n    enumerable: true,\n    get: () => (isUpdatesPaused ? null : currentPendingValue),\n    set: (newValue: PendingUpdate | null) => {\n      if (isUpdatesPaused) {\n        if (newValue !== null) {\n          pauseState.bufferedPending = mergePendingChains(\n            pauseState.bufferedPending ?? null,\n            newValue,\n          );\n        }\n        return;\n      }\n      currentPendingValue = newValue;\n    },\n  });\n\n  pausedQueueStates.set(queue, pauseState);\n};\n\nconst extractActionsFromChain = (pending: PendingUpdate | null): unknown[] => {\n  if (!pending) return [];\n  const actions: unknown[] = [];\n  const first = pending.next;\n  if (!first) return [];\n  let current: PendingUpdate | null = first;\n  do {\n    if (current) {\n      actions.push(current.action);\n      current = current.next;\n    }\n  } while (current && current !== first);\n  return actions;\n};\n\nconst resumeHookQueue = (queue: HookQueue): void => {\n  const pauseState = pausedQueueStates.get(queue);\n  if (!pauseState) return;\n\n  if (pauseState.originalGetSnapshot) {\n    queue.getSnapshot = pauseState.originalGetSnapshot;\n  }\n\n  if (pauseState.originalPendingDescriptor) {\n    Object.defineProperty(\n      queue,\n      \"pending\",\n      pauseState.originalPendingDescriptor,\n    );\n  } else {\n    delete (queue as Record<string, unknown>).pending;\n  }\n\n  queue.pending = null;\n\n  const dispatch = queue.dispatch;\n  if (typeof dispatch === \"function\") {\n    const pendingActions = extractActionsFromChain(\n      pauseState.pendingValueAtPause ?? null,\n    );\n    const bufferedActions = extractActionsFromChain(\n      pauseState.bufferedPending ?? null,\n    );\n    for (const action of [...pendingActions, ...bufferedActions]) {\n      pendingStateUpdates.push(() => dispatch(action));\n    }\n  }\n\n  pausedQueueStates.delete(queue);\n};\n\nconst pauseContextDependency = (contextDependency: ContextDependency): void => {\n  if (pausedContextStates.has(contextDependency)) return;\n\n  const pauseState: PausedContextState = {\n    originalDescriptor: Object.getOwnPropertyDescriptor(\n      contextDependency,\n      \"memoizedValue\",\n    ),\n    frozenValue: contextDependency.memoizedValue,\n  };\n\n  Object.defineProperty(contextDependency, \"memoizedValue\", {\n    configurable: true,\n    enumerable: true,\n    get() {\n      if (isUpdatesPaused) return pauseState.frozenValue;\n      if (pauseState.originalDescriptor?.get) {\n        return pauseState.originalDescriptor.get.call(this) as unknown;\n      }\n      return (this as { _memoizedValue?: unknown })._memoizedValue;\n    },\n    set(value: unknown) {\n      if (isUpdatesPaused) {\n        pauseState.pendingValue = value;\n        pauseState.didReceivePendingValue = true;\n        return;\n      }\n      if (pauseState.originalDescriptor?.set) {\n        pauseState.originalDescriptor.set.call(this, value);\n      } else {\n        (this as { _memoizedValue: unknown })._memoizedValue = value;\n      }\n    },\n  });\n\n  // HACK: Initialize backing field for non-getter properties\n  if (!pauseState.originalDescriptor?.get) {\n    (\n      contextDependency as unknown as { _memoizedValue: unknown }\n    )._memoizedValue = pauseState.frozenValue;\n  }\n\n  pausedContextStates.set(contextDependency, pauseState);\n};\n\nconst resumeContextDependency = (\n  contextDependency: ContextDependency,\n): void => {\n  const pauseState = pausedContextStates.get(contextDependency);\n  if (!pauseState) return;\n\n  if (pauseState.originalDescriptor) {\n    Object.defineProperty(\n      contextDependency,\n      \"memoizedValue\",\n      pauseState.originalDescriptor,\n    );\n  } else {\n    delete (contextDependency as unknown as Record<string, unknown>)\n      .memoizedValue;\n  }\n\n  if (pauseState.didReceivePendingValue) {\n    contextDependency.memoizedValue = pauseState.pendingValue;\n  }\n\n  pausedContextStates.delete(contextDependency);\n};\n\nconst forEachHookQueue = (\n  fiber: Fiber,\n  callback: (queue: HookQueue) => void,\n): void => {\n  let hookState = fiber.memoizedState as unknown as HookState | null;\n  while (hookState) {\n    if (hookState.queue && typeof hookState.queue === \"object\") {\n      callback(hookState.queue);\n    }\n    hookState = hookState.next;\n  }\n};\n\nconst forEachContextDependency = (\n  fiber: Fiber,\n  callback: (contextDependency: ContextDependency) => void,\n): void => {\n  let contextDependency = fiber.dependencies\n    ?.firstContext as ContextDependency | null;\n  while (\n    contextDependency &&\n    typeof contextDependency === \"object\" &&\n    \"memoizedValue\" in contextDependency\n  ) {\n    callback(contextDependency);\n    contextDependency = contextDependency.next;\n  }\n};\n\nconst traverseFibers = (\n  fiber: Fiber | null,\n  onCompositeFiber: (compositeFiber: Fiber) => void,\n): void => {\n  if (!fiber) return;\n  if (isCompositeFiber(fiber)) onCompositeFiber(fiber);\n  traverseFibers(fiber.child, onCompositeFiber);\n  traverseFibers(fiber.sibling, onCompositeFiber);\n};\n\nconst pauseFiber = (fiber: Fiber): void => {\n  forEachHookQueue(fiber, pauseHookQueue);\n  forEachContextDependency(fiber, pauseContextDependency);\n};\n\nconst resumeFiber = (fiber: Fiber): void => {\n  forEachHookQueue(fiber, resumeHookQueue);\n  forEachContextDependency(fiber, resumeContextDependency);\n};\n\nconst patchDispatcher = (dispatcher: object): void => {\n  if (patchedDispatchers.has(dispatcher)) return;\n\n  const typedDispatcher = dispatcher as Record<string, DispatchFunction>;\n  const originalHooks: OriginalHooks = {\n    useState: typedDispatcher.useState,\n    useReducer: typedDispatcher.useReducer,\n    useTransition: typedDispatcher.useTransition,\n    useSyncExternalStore: typedDispatcher.useSyncExternalStore,\n  };\n  patchedDispatchers.set(dispatcher, originalHooks);\n\n  typedDispatcher.useState = (...args: unknown[]) => {\n    const result = originalHooks.useState.apply(dispatcher, args) as unknown;\n    if (!isUpdatesPaused) return result;\n    if (!Array.isArray(result) || typeof result[1] !== \"function\")\n      return result;\n    const [state, dispatch] = result as [unknown, DispatchFunction];\n    const wrappedDispatch = getOrCache(\n      wrappedDispatchCache,\n      dispatch,\n      () =>\n        (...dispatchArgs: unknown[]) => {\n          if (isUpdatesPaused) {\n            pendingStateUpdates.push(() => dispatch(...dispatchArgs));\n          } else {\n            dispatch(...dispatchArgs);\n          }\n        },\n    );\n    return [state, wrappedDispatch];\n  };\n\n  typedDispatcher.useReducer = (...args: unknown[]) => {\n    const result = originalHooks.useReducer.apply(dispatcher, args) as unknown;\n    if (!isUpdatesPaused) return result;\n    if (!Array.isArray(result) || typeof result[1] !== \"function\")\n      return result;\n    const [state, dispatch] = result as [unknown, DispatchFunction];\n    const wrappedDispatch = getOrCache(\n      wrappedDispatchCache,\n      dispatch,\n      () =>\n        (...dispatchArgs: unknown[]) => {\n          if (isUpdatesPaused) {\n            pendingStateUpdates.push(() => dispatch(...dispatchArgs));\n          } else {\n            dispatch(...dispatchArgs);\n          }\n        },\n    );\n    return [state, wrappedDispatch];\n  };\n\n  typedDispatcher.useTransition = (...args: unknown[]) => {\n    const result = originalHooks.useTransition.apply(\n      dispatcher,\n      args,\n    ) as unknown;\n    if (!isUpdatesPaused) return result;\n    if (!Array.isArray(result) || typeof result[1] !== \"function\")\n      return result;\n    const [isPending, startTransition] = result as [\n      boolean,\n      TransitionFunction,\n    ];\n    const wrappedStartTransition = getOrCache(\n      wrappedStartTransitionCache,\n      startTransition,\n      () => (transitionCallback: () => void) => {\n        if (isUpdatesPaused) {\n          pendingTransitionCallbacks.push(() =>\n            startTransition(transitionCallback),\n          );\n        } else {\n          startTransition(transitionCallback);\n        }\n      },\n    );\n    return [isPending, wrappedStartTransition];\n  };\n\n  type UseSyncExternalStore = <T>(\n    subscribe: (onStoreChange: () => void) => () => void,\n    getSnapshot: () => T,\n    getServerSnapshot?: () => T,\n  ) => T;\n\n  typedDispatcher.useSyncExternalStore = (<T>(\n    subscribe: (onStoreChange: () => void) => () => void,\n    getSnapshot: () => T,\n    getServerSnapshot?: () => T,\n  ): T => {\n    if (!isUpdatesPaused) {\n      return (originalHooks.useSyncExternalStore as UseSyncExternalStore)(\n        subscribe,\n        getSnapshot,\n        getServerSnapshot,\n      );\n    }\n    const wrappedSubscribe = (onChange: () => void) =>\n      subscribe(() => {\n        if (isUpdatesPaused) {\n          pendingStoreCallbacks.add(onChange);\n        } else {\n          onChange();\n        }\n      });\n    return (originalHooks.useSyncExternalStore as UseSyncExternalStore)(\n      wrappedSubscribe,\n      getSnapshot,\n      getServerSnapshot,\n    );\n  }) as DispatchFunction;\n};\n\nconst installDispatcherPatching = (renderer: ReactRenderer): void => {\n  const dispatcherRef = renderer.currentDispatcherRef as {\n    H?: unknown;\n    current?: unknown;\n  } | null;\n  if (!dispatcherRef || typeof dispatcherRef !== \"object\") return;\n\n  const dispatcherKey = \"H\" in dispatcherRef ? \"H\" : \"current\";\n  let currentDispatcher = dispatcherRef[dispatcherKey];\n\n  Object.defineProperty(dispatcherRef, dispatcherKey, {\n    configurable: true,\n    enumerable: true,\n    get: () => {\n      if (currentDispatcher && typeof currentDispatcher === \"object\") {\n        patchDispatcher(currentDispatcher);\n      }\n      return currentDispatcher;\n    },\n    set: (newDispatcher) => {\n      currentDispatcher = newDispatcher;\n    },\n  });\n};\n\nconst scheduleReactUpdate = (fiberRoots: Set<FiberRootLike>): void => {\n  queueMicrotask(() => {\n    try {\n      for (const renderer of getRDTHook().renderers.values()) {\n        if (typeof renderer.scheduleUpdate !== \"function\") continue;\n        for (const fiberRoot of fiberRoots) {\n          if (fiberRoot.current) {\n            try {\n              renderer.scheduleUpdate(fiberRoot.current);\n            } catch (error) {\n              // HACK: React internals may throw during unfreeze cleanup\n              logRecoverableError(\n                \"scheduleUpdate failed during unfreeze\",\n                error,\n              );\n            }\n          }\n        }\n      }\n    } catch (error) {\n      // HACK: React internals may throw during unfreeze cleanup\n      logRecoverableError(\"scheduleReactUpdate failed\", error);\n    }\n  });\n};\n\nconst invokeCallbacks = (callbacks: Array<() => void>): void => {\n  for (const callback of callbacks) {\n    try {\n      callback();\n    } catch (error) {\n      // HACK: React internals may throw during state replay\n      logRecoverableError(\"Callback failed during state replay\", error);\n    }\n  }\n};\n\nconst initializeFreezeSupport = (): void => {\n  for (const renderer of getRDTHook().renderers.values()) {\n    if (renderersWithPatchedDispatcher.has(renderer)) continue;\n    installDispatcherPatching(renderer);\n    renderersWithPatchedDispatcher.add(renderer);\n  }\n};\n\nexport const freezeUpdates = (): (() => void) => {\n  if (isUpdatesPaused) return () => {};\n\n  initializeFreezeSupport();\n  isUpdatesPaused = true;\n\n  const fiberRoots = collectFiberRoots();\n  for (const fiberRoot of fiberRoots) {\n    traverseFibers(fiberRoot.current, pauseFiber);\n  }\n\n  return () => {\n    if (!isUpdatesPaused) return;\n\n    try {\n      const fiberRootsToResume = collectFiberRoots();\n      for (const fiberRoot of fiberRootsToResume) {\n        traverseFibers(fiberRoot.current, resumeFiber);\n      }\n\n      const storeCallbacksToInvoke = Array.from(pendingStoreCallbacks);\n      const transitionCallbacksToInvoke = pendingTransitionCallbacks.slice();\n      const stateUpdatesToInvoke = pendingStateUpdates.slice();\n\n      isUpdatesPaused = false;\n\n      invokeCallbacks(storeCallbacksToInvoke);\n      invokeCallbacks(transitionCallbacksToInvoke);\n      invokeCallbacks(stateUpdatesToInvoke);\n      scheduleReactUpdate(fiberRootsToResume);\n    } finally {\n      pendingStoreCallbacks.clear();\n      pendingTransitionCallbacks.length = 0;\n      pendingStateUpdates.length = 0;\n    }\n  };\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/generate-id.ts",
    "content": "export const generateId = (prefix: string): string =>\n  `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;\n"
  },
  {
    "path": "packages/react-grab/src/utils/generate-snippet.ts",
    "content": "import { getElementContext } from \"../core/context.js\";\n\ninterface GenerateSnippetOptions {\n  maxLines?: number;\n}\n\nexport const generateSnippet = async (\n  elements: Element[],\n  options: GenerateSnippetOptions = {},\n): Promise<string[]> => {\n  const elementSnippetResults = await Promise.allSettled(\n    elements.map((element) => getElementContext(element, options)),\n  );\n\n  const elementSnippets = elementSnippetResults.map((result) =>\n    result.status === \"fulfilled\" ? result.value : \"\",\n  );\n\n  return elementSnippets;\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/get-anchored-dropdown-position.ts",
    "content": "import type { DropdownAnchor } from \"../types.js\";\nimport { clampToViewport } from \"./clamp-to-viewport.js\";\n\ninterface DropdownPosition {\n  left: number;\n  top: number;\n}\n\ninterface GetAnchoredDropdownPositionOptions {\n  anchor: DropdownAnchor | null;\n  measuredWidth: number;\n  measuredHeight: number;\n  viewportWidth: number;\n  viewportHeight: number;\n  anchorGapPx: number;\n  viewportPaddingPx: number;\n  offscreenPosition: DropdownPosition;\n}\n\nexport const getAnchoredDropdownPosition = ({\n  anchor,\n  measuredWidth,\n  measuredHeight,\n  viewportWidth,\n  viewportHeight,\n  anchorGapPx,\n  viewportPaddingPx,\n  offscreenPosition,\n}: GetAnchoredDropdownPositionOptions): DropdownPosition => {\n  if (!anchor || measuredWidth === 0 || measuredHeight === 0) {\n    return offscreenPosition;\n  }\n\n  let rawLeft: number;\n  let rawTop: number;\n\n  if (anchor.edge === \"left\" || anchor.edge === \"right\") {\n    rawLeft =\n      anchor.edge === \"left\"\n        ? anchor.x + anchorGapPx\n        : anchor.x - measuredWidth - anchorGapPx;\n    rawTop = anchor.y - measuredHeight / 2;\n  } else {\n    rawLeft = anchor.x - measuredWidth / 2;\n    rawTop =\n      anchor.edge === \"top\"\n        ? anchor.y + anchorGapPx\n        : anchor.y - measuredHeight - anchorGapPx;\n  }\n\n  return {\n    left: clampToViewport(\n      rawLeft,\n      measuredWidth,\n      viewportWidth,\n      viewportPaddingPx,\n    ),\n    top: clampToViewport(\n      rawTop,\n      measuredHeight,\n      viewportHeight,\n      viewportPaddingPx,\n    ),\n  };\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/get-arrow-size.ts",
    "content": "import {\n  ARROW_HEIGHT_PX,\n  ARROW_MIN_SIZE_PX,\n  ARROW_MAX_LABEL_WIDTH_RATIO,\n} from \"../constants.js\";\n\nexport const getArrowSize = (labelWidth: number): number => {\n  if (labelWidth <= 0) return ARROW_HEIGHT_PX;\n  const scaledSize = labelWidth * ARROW_MAX_LABEL_WIDTH_RATIO;\n  return Math.max(ARROW_MIN_SIZE_PX, Math.min(ARROW_HEIGHT_PX, scaledSize));\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/get-bounds-center.ts",
    "content": "import type { OverlayBounds } from \"../types.js\";\n\ninterface BoundsCenter {\n  x: number;\n  y: number;\n}\n\nexport const getBoundsCenter = (bounds: OverlayBounds): BoundsCenter => ({\n  x: bounds.x + bounds.width / 2,\n  y: bounds.y + bounds.height / 2,\n});\n"
  },
  {
    "path": "packages/react-grab/src/utils/get-element-at-position.ts",
    "content": "import { isValidGrabbableElement } from \"./is-valid-grabbable-element.js\";\nimport {\n  ELEMENT_POSITION_CACHE_DISTANCE_THRESHOLD_PX,\n  ELEMENT_POSITION_THROTTLE_MS,\n  POINTER_EVENTS_RESUME_DEBOUNCE_MS,\n} from \"../constants.js\";\nimport {\n  suspendPointerEventsFreeze,\n  resumePointerEventsFreeze,\n} from \"./freeze-pseudo-states.js\";\n\ninterface PositionCache {\n  clientX: number;\n  clientY: number;\n  element: Element | null;\n  timestamp: number;\n}\n\nlet cache: PositionCache | null = null;\nlet resumeTimerId: ReturnType<typeof setTimeout> | null = null;\n\nconst scheduleResume = (): void => {\n  if (resumeTimerId !== null) {\n    clearTimeout(resumeTimerId);\n  }\n  resumeTimerId = setTimeout(() => {\n    resumeTimerId = null;\n    resumePointerEventsFreeze();\n  }, POINTER_EVENTS_RESUME_DEBOUNCE_MS);\n};\n\nconst cancelScheduledResume = (): void => {\n  if (resumeTimerId !== null) {\n    clearTimeout(resumeTimerId);\n    resumeTimerId = null;\n  }\n};\n\nconst isWithinThreshold = (\n  x1: number,\n  y1: number,\n  x2: number,\n  y2: number,\n): boolean => {\n  const deltaX = Math.abs(x1 - x2);\n  const deltaY = Math.abs(y1 - y2);\n  return (\n    deltaX <= ELEMENT_POSITION_CACHE_DISTANCE_THRESHOLD_PX &&\n    deltaY <= ELEMENT_POSITION_CACHE_DISTANCE_THRESHOLD_PX\n  );\n};\n\nexport const getElementsAtPoint = (\n  clientX: number,\n  clientY: number,\n): Element[] => {\n  cancelScheduledResume();\n  suspendPointerEventsFreeze();\n  const elements = document.elementsFromPoint(clientX, clientY);\n  scheduleResume();\n  return elements;\n};\n\nexport const getElementAtPosition = (\n  clientX: number,\n  clientY: number,\n): Element | null => {\n  const now = performance.now();\n\n  if (cache) {\n    const isPositionClose = isWithinThreshold(\n      clientX,\n      clientY,\n      cache.clientX,\n      cache.clientY,\n    );\n    const isWithinThrottle =\n      now - cache.timestamp < ELEMENT_POSITION_THROTTLE_MS;\n\n    if (isPositionClose || isWithinThrottle) {\n      return cache.element;\n    }\n  }\n\n  cancelScheduledResume();\n  suspendPointerEventsFreeze();\n\n  let result: Element | null = null;\n\n  const topElement = document.elementFromPoint(clientX, clientY);\n  if (topElement && isValidGrabbableElement(topElement)) {\n    result = topElement;\n  } else {\n    const elementsAtPoint = document.elementsFromPoint(clientX, clientY);\n    for (const candidateElement of elementsAtPoint) {\n      if (\n        candidateElement !== topElement &&\n        isValidGrabbableElement(candidateElement)\n      ) {\n        result = candidateElement;\n        break;\n      }\n    }\n  }\n\n  scheduleResume();\n\n  cache = { clientX, clientY, element: result, timestamp: now };\n  return result;\n};\n\nexport const clearElementPositionCache = (): void => {\n  cancelScheduledResume();\n  resumePointerEventsFreeze();\n  cache = null;\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/get-element-center.ts",
    "content": "import type { Position } from \"../types.js\";\nimport { createElementBounds } from \"./create-element-bounds.js\";\nimport { getBoundsCenter } from \"./get-bounds-center.js\";\n\nexport const getElementCenter = (element: Element): Position =>\n  getBoundsCenter(createElementBounds(element));\n"
  },
  {
    "path": "packages/react-grab/src/utils/get-elements-in-drag.ts",
    "content": "import type { DragRect, Rect } from \"../types.js\";\nimport {\n  suspendPointerEventsFreeze,\n  resumePointerEventsFreeze,\n} from \"./freeze-pseudo-states.js\";\nimport {\n  DRAG_SELECTION_COVERAGE_THRESHOLD,\n  DRAG_SELECTION_SAMPLE_SPACING_PX,\n  DRAG_SELECTION_MIN_SAMPLES_PER_AXIS,\n  DRAG_SELECTION_MAX_SAMPLES_PER_AXIS,\n  DRAG_SELECTION_MAX_TOTAL_SAMPLE_POINTS,\n  DRAG_SELECTION_EDGE_INSET_PX,\n} from \"../constants.js\";\nimport { isRootElement } from \"./is-root-element.js\";\n\nconst calculateIntersectionArea = (rect1: Rect, rect2: Rect): number => {\n  const intersectionLeft = Math.max(rect1.left, rect2.left);\n  const intersectionTop = Math.max(rect1.top, rect2.top);\n  const intersectionRight = Math.min(rect1.right, rect2.right);\n  const intersectionBottom = Math.min(rect1.bottom, rect2.bottom);\n\n  const intersectionWidth = Math.max(0, intersectionRight - intersectionLeft);\n  const intersectionHeight = Math.max(0, intersectionBottom - intersectionTop);\n\n  return intersectionWidth * intersectionHeight;\n};\n\nconst hasIntersection = (rect1: Rect, rect2: Rect): boolean => {\n  return (\n    rect1.left < rect2.right &&\n    rect1.right > rect2.left &&\n    rect1.top < rect2.bottom &&\n    rect1.bottom > rect2.top\n  );\n};\n\nconst clampNumber = (value: number, min: number, max: number): number => {\n  return Math.min(max, Math.max(min, value));\n};\n\nconst sortByDocumentOrder = (elements: Element[]): Element[] => {\n  return elements.sort((leftElement, rightElement) => {\n    if (leftElement === rightElement) return 0;\n    const position = leftElement.compareDocumentPosition(rightElement);\n    if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1;\n    if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1;\n    return 0;\n  });\n};\n\ninterface SamplePoint {\n  x: number;\n  y: number;\n}\n\nconst createSamplePoints = (dragRect: DragRect): SamplePoint[] => {\n  if (dragRect.width <= 0 || dragRect.height <= 0) return [];\n\n  const viewportWidth = window.innerWidth;\n  const viewportHeight = window.innerHeight;\n\n  const left = dragRect.x;\n  const top = dragRect.y;\n  const right = dragRect.x + dragRect.width;\n  const bottom = dragRect.y + dragRect.height;\n\n  const centerX = left + dragRect.width / 2;\n  const centerY = top + dragRect.height / 2;\n\n  const xCount = clampNumber(\n    Math.ceil(dragRect.width / DRAG_SELECTION_SAMPLE_SPACING_PX),\n    DRAG_SELECTION_MIN_SAMPLES_PER_AXIS,\n    DRAG_SELECTION_MAX_SAMPLES_PER_AXIS,\n  );\n  const yCount = clampNumber(\n    Math.ceil(dragRect.height / DRAG_SELECTION_SAMPLE_SPACING_PX),\n    DRAG_SELECTION_MIN_SAMPLES_PER_AXIS,\n    DRAG_SELECTION_MAX_SAMPLES_PER_AXIS,\n  );\n  const totalGridPoints = xCount * yCount;\n  const scale =\n    totalGridPoints > DRAG_SELECTION_MAX_TOTAL_SAMPLE_POINTS\n      ? Math.sqrt(DRAG_SELECTION_MAX_TOTAL_SAMPLE_POINTS / totalGridPoints)\n      : 1;\n  const scaledXCount = clampNumber(\n    Math.floor(xCount * scale),\n    DRAG_SELECTION_MIN_SAMPLES_PER_AXIS,\n    DRAG_SELECTION_MAX_SAMPLES_PER_AXIS,\n  );\n  const scaledYCount = clampNumber(\n    Math.floor(yCount * scale),\n    DRAG_SELECTION_MIN_SAMPLES_PER_AXIS,\n    DRAG_SELECTION_MAX_SAMPLES_PER_AXIS,\n  );\n\n  const pointKeys = new Set<string>();\n  const points: SamplePoint[] = [];\n\n  const addPoint = (x: number, y: number) => {\n    const clampedX = clampNumber(Math.round(x), 0, viewportWidth - 1);\n    const clampedY = clampNumber(Math.round(y), 0, viewportHeight - 1);\n    const key = `${clampedX}:${clampedY}`;\n    if (pointKeys.has(key)) return;\n    pointKeys.add(key);\n    points.push({ x: clampedX, y: clampedY });\n  };\n\n  addPoint(\n    left + DRAG_SELECTION_EDGE_INSET_PX,\n    top + DRAG_SELECTION_EDGE_INSET_PX,\n  );\n  addPoint(\n    right - DRAG_SELECTION_EDGE_INSET_PX,\n    top + DRAG_SELECTION_EDGE_INSET_PX,\n  );\n  addPoint(\n    left + DRAG_SELECTION_EDGE_INSET_PX,\n    bottom - DRAG_SELECTION_EDGE_INSET_PX,\n  );\n  addPoint(\n    right - DRAG_SELECTION_EDGE_INSET_PX,\n    bottom - DRAG_SELECTION_EDGE_INSET_PX,\n  );\n  addPoint(centerX, top + DRAG_SELECTION_EDGE_INSET_PX);\n  addPoint(centerX, bottom - DRAG_SELECTION_EDGE_INSET_PX);\n  addPoint(left + DRAG_SELECTION_EDGE_INSET_PX, centerY);\n  addPoint(right - DRAG_SELECTION_EDGE_INSET_PX, centerY);\n  addPoint(centerX, centerY);\n\n  for (let xIndex = 0; xIndex < scaledXCount; xIndex += 1) {\n    const sampleX = left + ((xIndex + 0.5) / scaledXCount) * dragRect.width;\n    for (let yIndex = 0; yIndex < scaledYCount; yIndex += 1) {\n      const sampleY = top + ((yIndex + 0.5) / scaledYCount) * dragRect.height;\n      addPoint(sampleX, sampleY);\n    }\n  }\n\n  return points;\n};\n\nconst filterElementsInDrag = (\n  dragRect: DragRect,\n  isValidGrabbableElement: (element: Element) => boolean,\n  shouldCheckCoverage: boolean,\n): Element[] => {\n  const dragBounds: Rect = {\n    left: dragRect.x,\n    top: dragRect.y,\n    right: dragRect.x + dragRect.width,\n    bottom: dragRect.y + dragRect.height,\n  };\n\n  const candidates = new Set<Element>();\n  const samplePoints = createSamplePoints(dragRect);\n\n  suspendPointerEventsFreeze();\n  try {\n    for (const point of samplePoints) {\n      const elementsAtPoint = document.elementsFromPoint(point.x, point.y);\n      for (const candidateElement of elementsAtPoint) {\n        candidates.add(candidateElement);\n      }\n    }\n  } finally {\n    resumePointerEventsFreeze();\n  }\n\n  const matchingElements: Element[] = [];\n\n  for (const candidateElement of candidates) {\n    if (!shouldCheckCoverage) {\n      if (isRootElement(candidateElement)) continue;\n    }\n\n    if (!isValidGrabbableElement(candidateElement)) continue;\n\n    const elementRect = candidateElement.getBoundingClientRect();\n    if (elementRect.width <= 0 || elementRect.height <= 0) continue;\n\n    const elementBounds: Rect = {\n      left: elementRect.left,\n      top: elementRect.top,\n      right: elementRect.left + elementRect.width,\n      bottom: elementRect.top + elementRect.height,\n    };\n\n    if (shouldCheckCoverage) {\n      const intersectionArea = calculateIntersectionArea(\n        dragBounds,\n        elementBounds,\n      );\n      const elementArea = elementRect.width * elementRect.height;\n      const hasMajorityCoverage =\n        elementArea > 0 &&\n        intersectionArea / elementArea >= DRAG_SELECTION_COVERAGE_THRESHOLD;\n\n      if (hasMajorityCoverage) {\n        matchingElements.push(candidateElement);\n      }\n    } else if (hasIntersection(elementBounds, dragBounds)) {\n      matchingElements.push(candidateElement);\n    }\n  }\n\n  return sortByDocumentOrder(matchingElements);\n};\n\nconst removeNestedElements = (elements: Element[]): Element[] => {\n  return elements.filter((element) => {\n    return !elements.some(\n      (otherElement) =>\n        otherElement !== element && otherElement.contains(element),\n    );\n  });\n};\n\nexport const getElementsInDrag = (\n  dragRect: DragRect,\n  isValidGrabbableElement: (element: Element) => boolean,\n  shouldCheckCoverage = true,\n): Element[] => {\n  const elements = filterElementsInDrag(\n    dragRect,\n    isValidGrabbableElement,\n    shouldCheckCoverage,\n  );\n  return removeNestedElements(elements);\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/get-next-base-path.ts",
    "content": "let cachedNextBasePath: string | undefined;\n\n// Next.js does not expose basePath at runtime (it is a build-time define via\n// process.env.__NEXT_ROUTER_BASEPATH that only compiled app code can access).\n// We detect it the same way Next.js's own asset-prefix.ts does: find a script\n// whose src contains \"/_next/\" and extract the path prefix before that marker.\n// When basePath is \"/app\", scripts load from \"/app/_next/…\", so the prefix is\n// \"/app\". When unset, scripts load from \"/_next/…\" (index 0) → empty string.\nexport const getNextBasePath = (): string => {\n  if (cachedNextBasePath !== undefined) return cachedNextBasePath;\n  const source = document.querySelector<HTMLScriptElement>(\n    'script[src*=\"/_next/\"]',\n  )?.src;\n  const pathname = source ? new URL(source).pathname : \"\";\n  const assetPathIndex = pathname.indexOf(\"/_next/\");\n  cachedNextBasePath =\n    assetPathIndex > 0 ? pathname.slice(0, assetPathIndex) : \"\";\n  return cachedNextBasePath;\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/get-script-options.ts",
    "content": "import type { Options } from \"../types.js\";\n\nconst isObjectRecord = (value: unknown): value is Record<string, unknown> => {\n  return typeof value === \"object\" && value !== null;\n};\n\nconst parseOptionsFromJson = (rawValue: unknown): Partial<Options> | null => {\n  if (!isObjectRecord(rawValue)) return null;\n\n  const parsedOptions: Partial<Options> = {};\n\n  if (typeof rawValue.enabled === \"boolean\") {\n    parsedOptions.enabled = rawValue.enabled;\n  }\n  if (\n    rawValue.activationMode === \"toggle\" ||\n    rawValue.activationMode === \"hold\"\n  ) {\n    parsedOptions.activationMode = rawValue.activationMode;\n  }\n  if (\n    typeof rawValue.keyHoldDuration === \"number\" &&\n    Number.isFinite(rawValue.keyHoldDuration)\n  ) {\n    parsedOptions.keyHoldDuration = rawValue.keyHoldDuration;\n  }\n  if (typeof rawValue.allowActivationInsideInput === \"boolean\") {\n    parsedOptions.allowActivationInsideInput =\n      rawValue.allowActivationInsideInput;\n  }\n  if (\n    typeof rawValue.maxContextLines === \"number\" &&\n    Number.isFinite(rawValue.maxContextLines)\n  ) {\n    parsedOptions.maxContextLines = rawValue.maxContextLines;\n  }\n  if (typeof rawValue.activationKey === \"string\") {\n    parsedOptions.activationKey = rawValue.activationKey;\n  }\n  if (typeof rawValue.freezeReactUpdates === \"boolean\") {\n    parsedOptions.freezeReactUpdates = rawValue.freezeReactUpdates;\n  }\n\n  if (Object.keys(parsedOptions).length === 0) return null;\n  return parsedOptions;\n};\n\nexport const getScriptOptions = (): Partial<Options> | null => {\n  if (typeof window === \"undefined\") return null;\n  try {\n    const currentScript =\n      document.currentScript instanceof HTMLScriptElement\n        ? document.currentScript\n        : null;\n    const dataOptions = currentScript?.getAttribute(\"data-options\");\n    if (!dataOptions) return null;\n    return parseOptionsFromJson(JSON.parse(dataOptions));\n  } catch {\n    return null;\n  }\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/get-tag-display.ts",
    "content": "interface TagDisplayInput {\n  tagName?: string;\n  componentName?: string;\n  elementsCount?: number;\n}\n\ninterface TagDisplayOutput {\n  tagName: string;\n  componentName?: string;\n}\n\nexport const getTagDisplay = (input: TagDisplayInput): TagDisplayOutput => {\n  if (input.elementsCount && input.elementsCount > 1) {\n    return {\n      tagName: `${input.elementsCount} elements`,\n      componentName: undefined,\n    };\n  }\n\n  return {\n    tagName: input.tagName || input.componentName || \"element\",\n    componentName: input.tagName ? input.componentName : undefined,\n  };\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/get-tag-name.ts",
    "content": "export const getTagName = (element: Element): string =>\n  (element.tagName || \"\").toLowerCase();\n"
  },
  {
    "path": "packages/react-grab/src/utils/get-visible-bounds-center.ts",
    "content": "import type { OverlayBounds } from \"../types.js\";\n\ninterface Point {\n  x: number;\n  y: number;\n}\n\nexport const getVisibleBoundsCenter = (bounds: OverlayBounds): Point => {\n  const viewportWidth = window.innerWidth;\n  const viewportHeight = window.innerHeight;\n\n  const visibleLeft = Math.max(0, bounds.x);\n  const visibleRight = Math.min(viewportWidth, bounds.x + bounds.width);\n  const visibleTop = Math.max(0, bounds.y);\n  const visibleBottom = Math.min(viewportHeight, bounds.y + bounds.height);\n\n  return {\n    x: (visibleLeft + visibleRight) / 2,\n    y: (visibleTop + visibleBottom) / 2,\n  };\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/get-visual-viewport.ts",
    "content": "export interface VisualViewportInfo {\n  width: number;\n  height: number;\n  offsetLeft: number;\n  offsetTop: number;\n}\n\nexport const getVisualViewport = (): VisualViewportInfo => {\n  const visualViewport = window.visualViewport;\n  if (visualViewport) {\n    return {\n      width: visualViewport.width,\n      height: visualViewport.height,\n      offsetLeft: visualViewport.offsetLeft,\n      offsetTop: visualViewport.offsetTop,\n    };\n  }\n  return {\n    width: window.innerWidth,\n    height: window.innerHeight,\n    offsetLeft: 0,\n    offsetTop: 0,\n  };\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/history-storage.ts",
    "content": "import {\n  MAX_HISTORY_ITEMS,\n  MAX_SESSION_STORAGE_SIZE_BYTES,\n} from \"../constants.js\";\nimport type { HistoryItem } from \"../types.js\";\nimport { generateId } from \"./generate-id.js\";\nimport { logRecoverableError } from \"./log-recoverable-error.js\";\n\nconst SESSION_STORAGE_KEY = \"react-grab-history-items\";\n\nconst loadFromSessionStorage = (): HistoryItem[] => {\n  try {\n    const serializedHistoryItems = sessionStorage.getItem(SESSION_STORAGE_KEY);\n    if (!serializedHistoryItems) return [];\n    const parsedHistoryItems = JSON.parse(\n      serializedHistoryItems,\n    ) as HistoryItem[];\n    return parsedHistoryItems.map((historyItem) => ({\n      ...historyItem,\n      elementsCount: Math.max(1, historyItem.elementsCount ?? 1),\n      previewBounds: historyItem.previewBounds ?? [],\n      elementSelectors: historyItem.elementSelectors ?? [],\n    }));\n  } catch (error) {\n    logRecoverableError(\"Failed to load history from sessionStorage\", error);\n    return [];\n  }\n};\n\nconst trimToSizeLimit = (items: HistoryItem[]): HistoryItem[] => {\n  let trimmedItems = items;\n  while (trimmedItems.length > 0) {\n    const serialized = JSON.stringify(trimmedItems);\n    if (new Blob([serialized]).size <= MAX_SESSION_STORAGE_SIZE_BYTES) {\n      return trimmedItems;\n    }\n    trimmedItems = trimmedItems.slice(0, -1);\n  }\n  return trimmedItems;\n};\n\nconst saveToSessionStorage = (items: HistoryItem[]): void => {\n  try {\n    const trimmedItems = trimToSizeLimit(items);\n    sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(trimmedItems));\n  } catch (error) {\n    // HACK: sessionStorage can throw in private browsing or when quota is exceeded\n    logRecoverableError(\"Failed to save history to sessionStorage\", error);\n  }\n};\n\nlet historyItems: HistoryItem[] = loadFromSessionStorage();\n\nexport const loadHistory = (): HistoryItem[] => historyItems;\n\nexport const addHistoryItem = (\n  item: Omit<HistoryItem, \"id\">,\n): HistoryItem[] => {\n  const newItem: HistoryItem = {\n    ...item,\n    id: generateId(\"history\"),\n  };\n  historyItems = [newItem, ...historyItems].slice(0, MAX_HISTORY_ITEMS);\n  saveToSessionStorage(historyItems);\n  return historyItems;\n};\n\nexport const removeHistoryItem = (itemId: string): HistoryItem[] => {\n  historyItems = historyItems.filter((item) => item.id !== itemId);\n  saveToSessionStorage(historyItems);\n  return historyItems;\n};\n\nexport const clearHistory = (): HistoryItem[] => {\n  historyItems = [];\n  saveToSessionStorage(historyItems);\n  return historyItems;\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/invalidate-interaction-caches.ts",
    "content": "import { invalidateBoundsCache } from \"./create-element-bounds.js\";\nimport { clearElementPositionCache } from \"./get-element-at-position.js\";\nimport { clearVisibilityCache } from \"./is-valid-grabbable-element.js\";\n\nexport const invalidateInteractionCaches = (): void => {\n  invalidateBoundsCache();\n  clearElementPositionCache();\n  clearVisibilityCache();\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/is-c-like-key.ts",
    "content": "const C_LIKE_CHARACTERS = new Set([\n  \"c\",\n  \"C\",\n  \"\\u0441\", // Cyrillic small es\n  \"\\u0421\", // Cyrillic capital es\n  \"\\u023c\", // c with stroke\n  \"\\u023b\", // C with stroke\n  \"\\u2184\", // reversed c\n  \"\\u2183\", // reversed C\n  \"\\u1d04\", // modifier small c\n  \"\\u1d9c\", // modifier small c turned\n  \"\\u2c7c\", // latin small c with palatal hook\n  \"\\u217d\", // small roman numeral 100\n  \"\\u216d\", // capital roman numeral 100\n  \"ç\", // c with cedilla\n  \"Ç\", // C with cedilla\n  \"ć\", // c with acute\n  \"Ć\", // C with acute\n  \"č\", // c with caron\n  \"Č\", // C with caron\n  \"ĉ\", // c with circumflex\n  \"Ĉ\", // C with circumflex\n  \"ċ\", // c with dot above\n  \"Ċ\", // C with dot above\n]);\n\nexport const isCLikeKey = (key: string, code?: string): boolean => {\n  if (code === \"KeyC\") return true;\n  if (!key || key.length !== 1) return false;\n  return C_LIKE_CHARACTERS.has(key);\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/is-element-connected.ts",
    "content": "export const isElementConnected = (\n  element: Element | null | undefined,\n): element is Element =>\n  Boolean(element?.isConnected ?? element?.ownerDocument?.contains(element));\n"
  },
  {
    "path": "packages/react-grab/src/utils/is-element-visible.ts",
    "content": "export const isElementVisible = (\n  element: Element,\n  computedStyle: CSSStyleDeclaration = window.getComputedStyle(element),\n): boolean => {\n  return (\n    computedStyle.display !== \"none\" &&\n    computedStyle.visibility !== \"hidden\" &&\n    computedStyle.opacity !== \"0\"\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/is-enter-code.ts",
    "content": "export const isEnterCode = (code: string): boolean =>\n  code === \"Enter\" || code === \"NumpadEnter\";\n"
  },
  {
    "path": "packages/react-grab/src/utils/is-event-from-overlay.ts",
    "content": "export const isEventFromOverlay = (\n  event: Event,\n  attribute: string,\n): boolean => {\n  try {\n    return event\n      .composedPath()\n      .some(\n        (target) =>\n          target instanceof HTMLElement && target.hasAttribute(attribute),\n      );\n  } catch {\n    return false;\n  }\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/is-extension-context.ts",
    "content": "export const isExtensionContext = (): boolean => {\n  const global = globalThis as {\n    chrome?: { runtime?: { id?: string } };\n    browser?: { runtime?: { id?: string } };\n  };\n  return Boolean(global.chrome?.runtime?.id || global.browser?.runtime?.id);\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/is-keyboard-event-triggered-by-input.ts",
    "content": "import { getTagName } from \"./get-tag-name.js\";\n\nconst EDITABLE_TAGS_AND_ROLES: readonly string[] = [\n  \"input\",\n  \"textarea\",\n  \"select\",\n  \"searchbox\",\n  \"slider\",\n  \"spinbutton\",\n  \"menuitem\",\n  \"menuitemcheckbox\",\n  \"menuitemradio\",\n  \"option\",\n  \"radio\",\n  \"textbox\",\n  \"combobox\",\n];\n\nconst getTargetElement = (event: KeyboardEvent): HTMLElement | undefined => {\n  if (event.composed) {\n    const firstElement = event.composedPath()[0];\n    if (firstElement instanceof HTMLElement) {\n      return firstElement;\n    }\n  } else if (event.target instanceof HTMLElement) {\n    return event.target;\n  }\n  return undefined;\n};\n\nexport const isKeyboardEventTriggeredByInput = (\n  event: KeyboardEvent,\n): boolean => {\n  if (document.designMode === \"on\") return true;\n\n  const targetElement = getTargetElement(event);\n  if (!targetElement) return false;\n\n  if (targetElement.isContentEditable) return true;\n\n  const tagName = getTagName(targetElement);\n  return EDITABLE_TAGS_AND_ROLES.some(\n    (tagOrRole) => tagOrRole === tagName || tagOrRole === targetElement.role,\n  );\n};\n\nexport const hasTextSelectionInInput = (event: KeyboardEvent): boolean => {\n  const target = event.target;\n  if (\n    target instanceof HTMLInputElement ||\n    target instanceof HTMLTextAreaElement\n  ) {\n    const selectionStart = target.selectionStart ?? 0;\n    const selectionEnd = target.selectionEnd ?? 0;\n    return selectionEnd - selectionStart > 0;\n  }\n  return false;\n};\n\nexport const hasTextSelectionOnPage = (): boolean => {\n  const selection = window.getSelection();\n  if (!selection) return false;\n  return selection.toString().length > 0;\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/is-mac.ts",
    "content": "let cachedIsMac: boolean | null = null;\n\nconst getPlatformFromUserAgentData = (): string | null => {\n  if (typeof navigator === \"undefined\") return null;\n  if (!(\"userAgentData\" in navigator)) return null;\n\n  const userAgentData = navigator.userAgentData;\n  if (typeof userAgentData !== \"object\" || userAgentData === null) return null;\n  if (!(\"platform\" in userAgentData)) return null;\n\n  const platform = userAgentData.platform;\n  if (typeof platform !== \"string\") return null;\n  return platform;\n};\n\nexport const isMac = (): boolean => {\n  if (cachedIsMac === null) {\n    if (typeof navigator === \"undefined\") {\n      cachedIsMac = false;\n      return cachedIsMac;\n    }\n\n    const platform =\n      navigator.platform ??\n      getPlatformFromUserAgentData() ??\n      navigator.userAgent;\n    cachedIsMac = /Mac|iPhone|iPad|iPod/i.test(platform);\n  }\n  return cachedIsMac;\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/is-root-element.ts",
    "content": "import { getTagName } from \"./get-tag-name.js\";\n\nexport const isRootElement = (element: Element): boolean => {\n  const tagName = getTagName(element);\n  return tagName === \"html\" || tagName === \"body\";\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/is-target-key-combination.ts",
    "content": "import { isCLikeKey } from \"./is-c-like-key.js\";\nimport { isMac } from \"./is-mac.js\";\nimport { parseActivationKey } from \"./parse-activation-key.js\";\nimport type { ActivationKey } from \"../types.js\";\n\ninterface HotkeyOptions {\n  activationKey?: ActivationKey;\n}\n\nexport const isTargetKeyCombination = (\n  event: KeyboardEvent,\n  options: HotkeyOptions,\n): boolean => {\n  if (options.activationKey) {\n    const matcher = parseActivationKey(options.activationKey);\n    return matcher(event);\n  }\n\n  const hasPlatformModifier = isMac() ? event.metaKey : event.ctrlKey;\n  const hasOnlyPlatformModifier =\n    hasPlatformModifier && !event.shiftKey && !event.altKey;\n  return Boolean(\n    event.key && hasOnlyPlatformModifier && isCLikeKey(event.key, event.code),\n  );\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/is-valid-grabbable-element.ts",
    "content": "import {\n  DEV_TOOLS_OVERLAY_Z_INDEX_THRESHOLD,\n  OVERLAY_Z_INDEX_THRESHOLD,\n  USER_IGNORE_ATTRIBUTE,\n  VIEWPORT_COVERAGE_THRESHOLD,\n  VISIBILITY_CACHE_TTL_MS,\n} from \"../constants.js\";\nimport { isElementVisible } from \"./is-element-visible.js\";\nimport { isRootElement } from \"./is-root-element.js\";\n\nconst isReactGrabElement = (element: Element): boolean => {\n  if (element.hasAttribute(\"data-react-grab\")) return true;\n\n  const rootNode = element.getRootNode();\n  return (\n    rootNode instanceof ShadowRoot &&\n    rootNode.host.hasAttribute(\"data-react-grab\")\n  );\n};\n\nconst isUserIgnoredElement = (element: Element): boolean =>\n  element.hasAttribute(USER_IGNORE_ATTRIBUTE) ||\n  element.closest(`[${USER_IGNORE_ATTRIBUTE}]`) !== null;\n\n// HACK: Dev tools like react-scan create full-viewport canvas overlays with\n// pointer-events: none that document.elementsFromPoint() still returns.\n// @see https://github.com/aidenybai/react-grab/issues/148\nconst isDevToolsOverlay = (computedStyle: CSSStyleDeclaration): boolean => {\n  const zIndex = parseInt(computedStyle.zIndex, 10);\n  return (\n    computedStyle.pointerEvents === \"none\" &&\n    computedStyle.position === \"fixed\" &&\n    !isNaN(zIndex) &&\n    zIndex >= DEV_TOOLS_OVERLAY_Z_INDEX_THRESHOLD\n  );\n};\n\nconst isFullViewportOverlay = (\n  element: Element,\n  computedStyle: CSSStyleDeclaration,\n): boolean => {\n  const position = computedStyle.position;\n  if (position !== \"fixed\" && position !== \"absolute\") {\n    return false;\n  }\n\n  const rect = element.getBoundingClientRect();\n  const coversViewport =\n    rect.width / window.innerWidth >= VIEWPORT_COVERAGE_THRESHOLD &&\n    rect.height / window.innerHeight >= VIEWPORT_COVERAGE_THRESHOLD;\n\n  if (!coversViewport) {\n    return false;\n  }\n\n  const backgroundColor = computedStyle.backgroundColor;\n  const hasInvisibleBackground =\n    backgroundColor === \"transparent\" ||\n    backgroundColor === \"rgba(0, 0, 0, 0)\" ||\n    parseFloat(computedStyle.opacity) < 0.1;\n\n  if (hasInvisibleBackground) {\n    return true;\n  }\n\n  const zIndex = parseInt(computedStyle.zIndex, 10);\n  return !isNaN(zIndex) && zIndex > OVERLAY_Z_INDEX_THRESHOLD;\n};\n\ninterface VisibilityCache {\n  isVisible: boolean;\n  timestamp: number;\n}\n\nlet visibilityCache = new WeakMap<Element, VisibilityCache>();\n\nexport const clearVisibilityCache = (): void => {\n  visibilityCache = new WeakMap<Element, VisibilityCache>();\n};\n\nexport const isValidGrabbableElement = (element: Element): boolean => {\n  if (isRootElement(element)) {\n    return false;\n  }\n\n  if (isReactGrabElement(element)) {\n    return false;\n  }\n\n  if (isUserIgnoredElement(element)) {\n    return false;\n  }\n\n  const now = performance.now();\n  const cached = visibilityCache.get(element);\n\n  if (cached && now - cached.timestamp < VISIBILITY_CACHE_TTL_MS) {\n    return cached.isVisible;\n  }\n\n  const computedStyle = window.getComputedStyle(element);\n\n  const isVisible = isElementVisible(element, computedStyle);\n  if (!isVisible) {\n    visibilityCache.set(element, { isVisible: false, timestamp: now });\n    return false;\n  }\n\n  const couldBeOverlay =\n    element.clientWidth / window.innerWidth >= VIEWPORT_COVERAGE_THRESHOLD &&\n    element.clientHeight / window.innerHeight >= VIEWPORT_COVERAGE_THRESHOLD;\n\n  if (couldBeOverlay) {\n    if (isDevToolsOverlay(computedStyle)) {\n      return false;\n    }\n    if (isFullViewportOverlay(element, computedStyle)) {\n      return false;\n    }\n  }\n\n  visibilityCache.set(element, { isVisible: true, timestamp: now });\n\n  return true;\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/join-snippets.ts",
    "content": "export const joinSnippets = (snippets: string[]): string => {\n  if (snippets.length <= 1) return snippets[0] ?? \"\";\n\n  return snippets\n    .map((snippet, index) => `[${index + 1}]\\n${snippet}`)\n    .join(\"\\n\\n\");\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/key-matches-code.ts",
    "content": "export const keyMatchesCode = (targetKey: string, code: string): boolean => {\n  const normalizedTarget = targetKey.toLowerCase();\n  if (code === \"Space\") {\n    return normalizedTarget === \"space\" || normalizedTarget === \" \";\n  }\n  if (code.startsWith(\"Key\")) {\n    return code.slice(3).toLowerCase() === normalizedTarget;\n  }\n  if (code.startsWith(\"Digit\")) {\n    return code.slice(5) === normalizedTarget;\n  }\n  return false;\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/lerp.ts",
    "content": "export const lerp = (start: number, end: number, factor: number): number => {\n  return start + (end - start) * factor;\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/log-recoverable-error.ts",
    "content": "export const logRecoverableError = (context: string, error: unknown): void => {\n  if (process.env.NODE_ENV !== \"production\") {\n    console.warn(`[react-grab] ${context}:`, error);\n  }\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/mount-root.ts",
    "content": "import { MOUNT_ROOT_RECHECK_DELAY_MS, Z_INDEX_HOST } from \"../constants.js\";\n\nconst ATTRIBUTE_NAME = \"data-react-grab\";\n\nconst FONT_LINK_ID = \"react-grab-fonts\";\nconst FONT_LINK_URL =\n  \"https://fonts.googleapis.com/css2?family=Geist:wght@500&display=swap\";\n\nconst loadFonts = () => {\n  if (document.getElementById(FONT_LINK_ID)) return;\n\n  if (!document.head) return;\n\n  const link = document.createElement(\"link\");\n  link.id = FONT_LINK_ID;\n  link.rel = \"stylesheet\";\n  link.href = FONT_LINK_URL;\n  document.head.appendChild(link);\n};\n\nexport const mountRoot = (cssText?: string) => {\n  loadFonts();\n\n  const mountedHost = document.querySelector(`[${ATTRIBUTE_NAME}]`);\n  if (mountedHost) {\n    const mountedRoot = mountedHost.shadowRoot?.querySelector(\n      `[${ATTRIBUTE_NAME}]`,\n    );\n    if (mountedRoot instanceof HTMLDivElement && mountedHost.shadowRoot) {\n      return mountedRoot;\n    }\n  }\n\n  const host = document.createElement(\"div\");\n\n  host.setAttribute(ATTRIBUTE_NAME, \"true\");\n  host.style.zIndex = String(Z_INDEX_HOST);\n  host.style.position = \"fixed\";\n  host.style.inset = \"0\";\n  host.style.pointerEvents = \"none\";\n  const shadowRoot = host.attachShadow({ mode: \"open\" });\n\n  if (cssText) {\n    const styleElement = document.createElement(\"style\");\n    styleElement.textContent = cssText;\n    shadowRoot.appendChild(styleElement);\n  }\n\n  const root = document.createElement(\"div\");\n\n  root.setAttribute(ATTRIBUTE_NAME, \"true\");\n\n  shadowRoot.appendChild(root);\n\n  const doc = document.body ?? document.documentElement;\n  // HACK: wait for hydration (in case something blows away the DOM)\n  doc.appendChild(host);\n\n  // HACK: re-append after a delay to ensure we're the last child of body.\n  // This handles two cases:\n  //   1. Hydration blew away the DOM and the host was removed\n  //   2. Another tool (e.g. react-scan) appended at the same max z-index —\n  //      being last in DOM order wins the stacking tiebreaker\n  // appendChild of an existing node is an atomic move (no flash, no reflow).\n  setTimeout(() => {\n    doc.appendChild(host);\n  }, MOUNT_ROOT_RECHECK_DELAY_MS);\n\n  return root;\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/native-raf.ts",
    "content": "const isClientSide = typeof window !== \"undefined\";\n\nconst noopAnimationFrame = (_callback: FrameRequestCallback): number => 0;\nconst noopCancelFrame = (_id: number): void => {};\n\n// HACK: Read from Window.prototype to bypass any monkey-patching on the window\n// instance (e.g., the rAF wrapper installed by freeze-animations.ts). Assigning\n// to window.requestAnimationFrame creates an own property that shadows the\n// prototype, but the native implementation remains on Window.prototype.\nexport const nativeRequestAnimationFrame: typeof requestAnimationFrame =\n  isClientSide\n    ? (\n        Object.getOwnPropertyDescriptor(\n          Window.prototype,\n          \"requestAnimationFrame\",\n        )?.value ?? window.requestAnimationFrame\n      ).bind(window)\n    : noopAnimationFrame;\n\nexport const nativeCancelAnimationFrame: typeof cancelAnimationFrame =\n  isClientSide\n    ? (\n        Object.getOwnPropertyDescriptor(\n          Window.prototype,\n          \"cancelAnimationFrame\",\n        )?.value ?? window.cancelAnimationFrame\n      ).bind(window)\n    : noopCancelFrame;\n\nexport const waitUntilNextFrame = (): Promise<void> =>\n  isClientSide\n    ? new Promise<void>((resolve) =>\n        nativeRequestAnimationFrame(() => resolve()),\n      )\n    : Promise.resolve();\n"
  },
  {
    "path": "packages/react-grab/src/utils/normalize-error.ts",
    "content": "export const normalizeErrorMessage = (\n  error: unknown,\n  fallback = \"Unknown error\",\n): string =>\n  error instanceof Error && error.message ? error.message : fallback;\n\nexport const normalizeError = (error: unknown): Error =>\n  error instanceof Error ? error : new Error(String(error));\n"
  },
  {
    "path": "packages/react-grab/src/utils/on-idle.ts",
    "content": "interface BackgroundTaskScheduler {\n  postTask: (\n    callback: () => void,\n    options: { priority: \"background\" },\n  ) => unknown;\n}\n\ndeclare global {\n  interface Window {\n    scheduler?: BackgroundTaskScheduler;\n  }\n}\n\nconst isBackgroundTaskScheduler = (\n  value: unknown,\n): value is BackgroundTaskScheduler => {\n  if (typeof value !== \"object\" || value === null) return false;\n  if (!(\"postTask\" in value)) return false;\n  return typeof value.postTask === \"function\";\n};\n\nexport const onIdle = (callback: () => void): void => {\n  if (typeof window !== \"undefined\") {\n    const schedulerCandidate = window.scheduler;\n    if (isBackgroundTaskScheduler(schedulerCandidate)) {\n      schedulerCandidate.postTask(callback, {\n        priority: \"background\",\n      });\n      return;\n    }\n    if (\"requestIdleCallback\" in window) {\n      requestIdleCallback(callback);\n      return;\n    }\n  }\n  setTimeout(callback, 0);\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/open-file.ts",
    "content": "import { normalizeFileName } from \"bippy/source\";\nimport { checkIsNextProject } from \"../core/context.js\";\nimport { getNextBasePath } from \"./get-next-base-path.js\";\n\nconst OPEN_FILE_BASE_URL =\n  process.env.NODE_ENV === \"production\"\n    ? \"https://react-grab.com\"\n    : \"http://localhost:3000\";\n\nconst tryDevServerOpen = async (\n  filePath: string,\n  lineNumber: number | undefined,\n): Promise<boolean> => {\n  const isNextProject = checkIsNextProject();\n  const params = new URLSearchParams({ file: filePath });\n\n  const lineKey = isNextProject ? \"line1\" : \"line\";\n  const columnKey = isNextProject ? \"column1\" : \"column\";\n  if (lineNumber) params.set(lineKey, String(lineNumber));\n  params.set(columnKey, \"1\");\n\n  const endpoint = isNextProject\n    ? `${getNextBasePath()}/__nextjs_launch-editor`\n    : \"/__open-in-editor\";\n  const response = await fetch(`${endpoint}?${params}`);\n  return response.ok;\n};\n\nexport const openFile = async (\n  filePath: string,\n  lineNumber: number | undefined,\n  transformUrl?: (url: string, filePath: string, lineNumber?: number) => string,\n): Promise<void> => {\n  filePath = normalizeFileName(filePath);\n\n  const wasOpenedByDevServer = await tryDevServerOpen(\n    filePath,\n    lineNumber,\n  ).catch(() => false);\n  if (wasOpenedByDevServer) return;\n\n  const lineParam = lineNumber ? `&line=${lineNumber}` : \"\";\n  const rawUrl = `${OPEN_FILE_BASE_URL}/open-file?url=${encodeURIComponent(filePath)}${lineParam}`;\n  const url = transformUrl\n    ? transformUrl(rawUrl, filePath, lineNumber)\n    : rawUrl;\n  window.open(url, \"_blank\", \"noopener,noreferrer\");\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/overlay-color.ts",
    "content": "import { supportsDisplayP3 } from \"./supports-display-p3.js\";\n\nconst isWideGamut = supportsDisplayP3();\nconst SRGB_COMPONENTS = \"210, 57, 192\";\nconst P3_COMPONENTS = \"0.84 0.19 0.78\";\n\nexport const overlayColor = (alpha: number): string =>\n  isWideGamut\n    ? `color(display-p3 ${P3_COMPONENTS} / ${alpha})`\n    : `rgba(${SRGB_COMPONENTS}, ${alpha})`;\n"
  },
  {
    "path": "packages/react-grab/src/utils/parse-activation-key.ts",
    "content": "import type { ActivationKey } from \"../types.js\";\nimport { isMac } from \"./is-mac.js\";\nimport { keyMatchesCode } from \"./key-matches-code.js\";\n\ninterface ParsedModifiers {\n  metaKey: boolean;\n  ctrlKey: boolean;\n  shiftKey: boolean;\n  altKey: boolean;\n  key: string | null;\n}\n\nconst MODIFIER_MAP: Record<string, keyof Omit<ParsedModifiers, \"key\">> = {\n  meta: \"metaKey\",\n  cmd: \"metaKey\",\n  command: \"metaKey\",\n  win: \"metaKey\",\n  windows: \"metaKey\",\n  ctrl: \"ctrlKey\",\n  control: \"ctrlKey\",\n  shift: \"shiftKey\",\n  alt: \"altKey\",\n  option: \"altKey\",\n  opt: \"altKey\",\n};\n\nconst parseString = (shortcut: string): ParsedModifiers => {\n  const parts = shortcut.split(\"+\").map((part) => part.trim().toLowerCase());\n  const result: ParsedModifiers = {\n    metaKey: false,\n    ctrlKey: false,\n    shiftKey: false,\n    altKey: false,\n    key: null,\n  };\n\n  for (const part of parts) {\n    const modifierKey = MODIFIER_MAP[part];\n    if (modifierKey) {\n      result[modifierKey] = true;\n    } else {\n      result.key = part;\n    }\n  }\n\n  return result;\n};\n\nexport const parseActivationKey = (\n  activationKey: ActivationKey,\n): ((event: KeyboardEvent) => boolean) => {\n  if (typeof activationKey === \"function\") {\n    return activationKey;\n  }\n\n  const parsed = parseString(activationKey);\n  const targetKey = parsed.key;\n\n  return (event: KeyboardEvent): boolean => {\n    if (targetKey === null) {\n      const metaMatches = parsed.metaKey\n        ? event.metaKey || event.key === \"Meta\"\n        : true;\n      const ctrlMatches = parsed.ctrlKey\n        ? event.ctrlKey || event.key === \"Control\"\n        : true;\n      const shiftMatches = parsed.shiftKey\n        ? event.shiftKey || event.key === \"Shift\"\n        : true;\n      const altMatches = parsed.altKey\n        ? event.altKey || event.key === \"Alt\"\n        : true;\n\n      const allRequiredModifiersPressed =\n        metaMatches && ctrlMatches && shiftMatches && altMatches;\n\n      const requiredModifierCount = [\n        parsed.metaKey,\n        parsed.ctrlKey,\n        parsed.shiftKey,\n        parsed.altKey,\n      ].filter(Boolean).length;\n\n      const pressedModifierCount = [\n        event.metaKey || event.key === \"Meta\",\n        event.ctrlKey || event.key === \"Control\",\n        event.shiftKey || event.key === \"Shift\",\n        event.altKey || event.key === \"Alt\",\n      ].filter(Boolean).length;\n\n      return (\n        allRequiredModifiersPressed &&\n        pressedModifierCount >= requiredModifierCount\n      );\n    }\n\n    const keyMatches =\n      event.key?.toLowerCase() === targetKey ||\n      keyMatchesCode(targetKey, event.code);\n\n    const hasModifier =\n      parsed.metaKey || parsed.ctrlKey || parsed.shiftKey || parsed.altKey;\n\n    const modifiersMatch = hasModifier\n      ? (parsed.metaKey ? event.metaKey : true) &&\n        (parsed.ctrlKey ? event.ctrlKey : true) &&\n        (parsed.shiftKey ? event.shiftKey : true) &&\n        (parsed.altKey ? event.altKey : true)\n      : !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;\n\n    return keyMatches && modifiersMatch;\n  };\n};\n\nexport const getModifiersFromActivationKey = (\n  activationKey: ActivationKey | undefined,\n): ParsedModifiers => {\n  if (!activationKey || typeof activationKey === \"function\") {\n    return {\n      metaKey: isMac(),\n      ctrlKey: !isMac(),\n      shiftKey: false,\n      altKey: false,\n      key: null,\n    };\n  }\n  return parseString(activationKey);\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/recalculate-session-position.ts",
    "content": "import type { Position, OverlayBounds } from \"../types.js\";\nimport { getBoundsCenter } from \"./get-bounds-center.js\";\n\ninterface RecalculateSessionPositionOptions {\n  currentPosition: Position;\n  previousBounds: OverlayBounds | undefined;\n  nextBounds: OverlayBounds | undefined;\n}\n\nexport const recalculateSessionPosition = ({\n  currentPosition,\n  previousBounds,\n  nextBounds,\n}: RecalculateSessionPositionOptions): Position => {\n  if (!previousBounds || !nextBounds) {\n    return currentPosition;\n  }\n\n  const previousBoundsCenter = getBoundsCenter(previousBounds);\n  const nextBoundsCenter = getBoundsCenter(nextBounds);\n  const previousBoundsHalfWidth = previousBounds.width / 2;\n  const positionOffsetFromCenterX = currentPosition.x - previousBoundsCenter.x;\n  const positionOffsetRatio =\n    previousBoundsHalfWidth > 0\n      ? positionOffsetFromCenterX / previousBoundsHalfWidth\n      : 0;\n  const nextBoundsHalfWidth = nextBounds.width / 2;\n\n  return {\n    ...currentPosition,\n    x: nextBoundsCenter.x + positionOffsetRatio * nextBoundsHalfWidth,\n  };\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/register-overlay-dismiss.ts",
    "content": "import { isEventFromOverlay } from \"./is-event-from-overlay.js\";\nimport { isKeyboardEventTriggeredByInput } from \"./is-keyboard-event-triggered-by-input.js\";\nimport {\n  nativeCancelAnimationFrame,\n  nativeRequestAnimationFrame,\n} from \"./native-raf.js\";\n\ninterface RegisterOverlayDismissOptions {\n  isOpen: () => boolean;\n  onDismiss: () => void;\n  onConfirm?: () => void;\n  shouldIgnoreInputEvents?: boolean;\n  shouldIgnoreRightClick?: boolean;\n}\n\nexport const registerOverlayDismiss = (\n  options: RegisterOverlayDismissOptions,\n): (() => void) => {\n  const handleKeyDown = (event: KeyboardEvent) => {\n    if (!options.isOpen()) return;\n    if (\n      options.shouldIgnoreInputEvents &&\n      isKeyboardEventTriggeredByInput(event)\n    ) {\n      return;\n    }\n\n    const isEscape = event.code === \"Escape\";\n\n    if (isEscape) {\n      event.preventDefault();\n      event.stopImmediatePropagation();\n      options.onDismiss();\n      return;\n    }\n\n    if (event.code === \"Enter\" && options.onConfirm) {\n      event.preventDefault();\n      event.stopImmediatePropagation();\n      options.onConfirm();\n    }\n  };\n\n  const handleClickOutside = (event: MouseEvent | TouchEvent) => {\n    if (!options.isOpen()) return;\n    if (isEventFromOverlay(event, \"data-react-grab-ignore-events\")) return;\n    if (\n      options.shouldIgnoreRightClick &&\n      event instanceof MouseEvent &&\n      event.button === 2\n    )\n      return;\n    options.onDismiss();\n  };\n\n  const frameId = nativeRequestAnimationFrame(() => {\n    window.addEventListener(\"mousedown\", handleClickOutside, {\n      capture: true,\n    });\n    window.addEventListener(\"touchstart\", handleClickOutside, {\n      capture: true,\n    });\n  });\n\n  window.addEventListener(\"keydown\", handleKeyDown, { capture: true });\n\n  return () => {\n    nativeCancelAnimationFrame(frameId);\n    window.removeEventListener(\"keydown\", handleKeyDown, { capture: true });\n    window.removeEventListener(\"mousedown\", handleClickOutside, {\n      capture: true,\n    });\n    window.removeEventListener(\"touchstart\", handleClickOutside, {\n      capture: true,\n    });\n  };\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/resolve-action-enabled.ts",
    "content": "import type {\n  ActionContext,\n  ContextMenuAction,\n  ToolbarMenuAction,\n} from \"../types.js\";\n\nconst resolveBooleanEnabled = (enabled: boolean | undefined): boolean =>\n  enabled ?? true;\n\nexport const resolveActionEnabled = (\n  action: ContextMenuAction,\n  context: ActionContext | undefined,\n): boolean => {\n  if (typeof action.enabled === \"function\") {\n    if (!context) {\n      return false;\n    }\n\n    return action.enabled(context);\n  }\n\n  return resolveBooleanEnabled(action.enabled);\n};\n\nexport const resolveToolbarActionEnabled = (\n  action: ToolbarMenuAction,\n): boolean => {\n  if (typeof action.enabled === \"function\") {\n    return action.enabled();\n  }\n\n  return resolveBooleanEnabled(action.enabled);\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/safe-polygon.ts",
    "content": "interface Point {\n  x: number;\n  y: number;\n}\n\nexport interface TargetRect {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n}\n\nconst computeTriangleSign = (\n  point1: Point,\n  point2: Point,\n  point3: Point,\n): number =>\n  (point1.x - point3.x) * (point2.y - point3.y) -\n  (point2.x - point3.x) * (point1.y - point3.y);\n\nconst isPointInTriangle = (\n  point: Point,\n  vertex1: Point,\n  vertex2: Point,\n  vertex3: Point,\n): boolean => {\n  const sign1 = computeTriangleSign(point, vertex1, vertex2);\n  const sign2 = computeTriangleSign(point, vertex2, vertex3);\n  const sign3 = computeTriangleSign(point, vertex3, vertex1);\n  const hasNegative = sign1 < 0 || sign2 < 0 || sign3 < 0;\n  const hasPositive = sign1 > 0 || sign2 > 0 || sign3 > 0;\n  return !hasNegative || !hasPositive;\n};\n\nconst isPointInRect = (point: Point, rect: TargetRect): boolean =>\n  point.x >= rect.x &&\n  point.x <= rect.x + rect.width &&\n  point.y >= rect.y &&\n  point.y <= rect.y + rect.height;\n\nconst computeFarEdgeCorners = (\n  cursor: Point,\n  targetRect: TargetRect,\n): [Point, Point] => {\n  const targetBottom = targetRect.y + targetRect.height;\n  const targetRight = targetRect.x + targetRect.width;\n\n  if (cursor.y <= targetRect.y) {\n    return [\n      { x: targetRect.x, y: targetBottom },\n      { x: targetRight, y: targetBottom },\n    ];\n  }\n  if (cursor.y >= targetBottom) {\n    return [\n      { x: targetRect.x, y: targetRect.y },\n      { x: targetRight, y: targetRect.y },\n    ];\n  }\n  if (cursor.x <= targetRect.x) {\n    return [\n      { x: targetRight, y: targetRect.y },\n      { x: targetRight, y: targetBottom },\n    ];\n  }\n  return [\n    { x: targetRect.x, y: targetRect.y },\n    { x: targetRect.x, y: targetBottom },\n  ];\n};\n\nexport const createSafePolygonTracker = () => {\n  let removeListener: (() => void) | null = null;\n\n  const stop = () => {\n    removeListener?.();\n    removeListener = null;\n  };\n\n  const start = (\n    cursorPosition: Point,\n    targetRects: TargetRect[],\n    onLeavePolygon: () => void,\n  ) => {\n    stop();\n\n    const primaryTarget = targetRects[0];\n    if (!primaryTarget) return;\n\n    if (isPointInRect(cursorPosition, primaryTarget)) return;\n\n    const [corner1, corner2] = computeFarEdgeCorners(\n      cursorPosition,\n      primaryTarget,\n    );\n\n    const isInAnySafeRect = (point: Point): boolean =>\n      targetRects.some((rect) => isPointInRect(point, rect));\n\n    const handleMouseMove = (event: MouseEvent) => {\n      const cursor = { x: event.clientX, y: event.clientY };\n\n      if (isInAnySafeRect(cursor)) {\n        if (isPointInRect(cursor, primaryTarget)) {\n          stop();\n        }\n        return;\n      }\n\n      if (isPointInTriangle(cursor, cursorPosition, corner1, corner2)) {\n        return;\n      }\n\n      stop();\n      onLeavePolygon();\n    };\n\n    window.addEventListener(\"mousemove\", handleMouseMove);\n    removeListener = () => {\n      window.removeEventListener(\"mousemove\", handleMouseMove);\n    };\n  };\n\n  return { start, stop };\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/strip-translate-from-transform.ts",
    "content": "const isValidNumber = (value: number): boolean =>\n  typeof value === \"number\" && !Number.isNaN(value) && Number.isFinite(value);\n\nconst parseMatrixValue = (value: string): number | null => {\n  const trimmedValue = value.trim();\n  if (!trimmedValue) return null;\n\n  const parsedValue = parseFloat(trimmedValue);\n  return isValidNumber(parsedValue) ? parsedValue : null;\n};\n\nconst parseMatrixValues = (\n  valuesString: string,\n  expectedLength: number,\n): number[] | null => {\n  const rawValues = valuesString.split(\",\");\n\n  if (rawValues.length !== expectedLength) {\n    return null;\n  }\n\n  const parsedValues: number[] = [];\n  for (const rawValue of rawValues) {\n    const parsedValue = parseMatrixValue(rawValue);\n    if (parsedValue === null) {\n      return null;\n    }\n    parsedValues.push(parsedValue);\n  }\n\n  return parsedValues;\n};\n\nconst isIdentityMatrix2d = (\n  a: number,\n  b: number,\n  c: number,\n  d: number,\n): boolean => a === 1 && b === 0 && c === 0 && d === 1;\n\nconst isIdentityMatrix3d = (values: number[]): boolean =>\n  values[0] === 1 &&\n  values[1] === 0 &&\n  values[2] === 0 &&\n  values[3] === 0 &&\n  values[4] === 0 &&\n  values[5] === 1 &&\n  values[6] === 0 &&\n  values[7] === 0 &&\n  values[8] === 0 &&\n  values[9] === 0 &&\n  values[10] === 1 &&\n  values[11] === 0 &&\n  values[15] === 1;\n\nexport const stripTranslateFromTransformString = (\n  transform: string,\n): string => {\n  if (!transform || transform === \"none\") return \"none\";\n\n  if (transform.startsWith(\"matrix\")) {\n    if (transform.startsWith(\"matrix3d(\")) {\n      const start = 9;\n      const end = transform.length - 1;\n      const values = parseMatrixValues(transform.slice(start, end), 16);\n\n      if (values) {\n        values[12] = 0;\n        values[13] = 0;\n        values[14] = 0;\n\n        if (isIdentityMatrix3d(values)) return \"none\";\n        return `matrix3d(${values[0]}, ${values[1]}, ${values[2]}, ${values[3]}, ${values[4]}, ${values[5]}, ${values[6]}, ${values[7]}, ${values[8]}, ${values[9]}, ${values[10]}, ${values[11]}, 0, 0, 0, ${values[15]})`;\n      }\n    } else {\n      const start = 7;\n      const end = transform.length - 1;\n      const values = parseMatrixValues(transform.slice(start, end), 6);\n\n      if (values) {\n        const scaleX = values[0];\n        const skewY = values[1];\n        const skewX = values[2];\n        const scaleY = values[3];\n\n        if (isIdentityMatrix2d(scaleX, skewY, skewX, scaleY)) return \"none\";\n        return `matrix(${scaleX}, ${skewY}, ${skewX}, ${scaleY}, 0, 0)`;\n      }\n    }\n  }\n\n  return \"none\";\n};\n\nexport const stripTranslateFromMatrix = (matrix: DOMMatrix): string => {\n  if (matrix.isIdentity) return \"none\";\n\n  if (matrix.is2D) {\n    if (isIdentityMatrix2d(matrix.a, matrix.b, matrix.c, matrix.d))\n      return \"none\";\n    return `matrix(${matrix.a}, ${matrix.b}, ${matrix.c}, ${matrix.d}, 0, 0)`;\n  }\n\n  if (\n    matrix.m11 === 1 &&\n    matrix.m12 === 0 &&\n    matrix.m13 === 0 &&\n    matrix.m14 === 0 &&\n    matrix.m21 === 0 &&\n    matrix.m22 === 1 &&\n    matrix.m23 === 0 &&\n    matrix.m24 === 0 &&\n    matrix.m31 === 0 &&\n    matrix.m32 === 0 &&\n    matrix.m33 === 1 &&\n    matrix.m34 === 0 &&\n    matrix.m44 === 1\n  ) {\n    return \"none\";\n  }\n\n  return `matrix3d(${matrix.m11}, ${matrix.m12}, ${matrix.m13}, ${matrix.m14}, ${matrix.m21}, ${matrix.m22}, ${matrix.m23}, ${matrix.m24}, ${matrix.m31}, ${matrix.m32}, ${matrix.m33}, ${matrix.m34}, 0, 0, 0, ${matrix.m44})`;\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/supports-display-p3.ts",
    "content": "let cachedResult: boolean | null = null;\n\nexport const supportsDisplayP3 = (): boolean => {\n  if (cachedResult !== null) return cachedResult;\n\n  try {\n    cachedResult = window.matchMedia(\"(color-gamut: p3)\").matches;\n  } catch {\n    cachedResult = false;\n  }\n\n  return cachedResult;\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/suppress-menu-event.ts",
    "content": "export const suppressMenuEvent = (event: Event): void => {\n  if (event.type === \"contextmenu\") {\n    event.preventDefault();\n  }\n  event.stopImmediatePropagation();\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/toolbar-layout.ts",
    "content": "export const getExpandGridClass = (\n  isVertical: boolean,\n  isExpanded: boolean,\n  collapsedExtra?: string,\n): string => {\n  if (isExpanded) {\n    return isVertical\n      ? \"grid-rows-[1fr] opacity-100\"\n      : \"grid-cols-[1fr] opacity-100\";\n  }\n  const base = isVertical\n    ? \"grid-rows-[0fr] opacity-0\"\n    : \"grid-cols-[0fr] opacity-0\";\n  return collapsedExtra ? `${base} ${collapsedExtra}` : base;\n};\n\nexport const getButtonSpacingClass = (isVertical: boolean): string =>\n  isVertical ? \"mb-1.5\" : \"mr-1.5\";\n\nexport const getMinDimensionClass = (isVertical: boolean): string =>\n  isVertical ? \"min-h-0\" : \"min-w-0\";\n\nexport const getHitboxConstraintClass = (isVertical: boolean): string =>\n  isVertical ? \"before:!min-h-full\" : \"before:!min-w-full\";\n"
  },
  {
    "path": "packages/react-grab/src/utils/toolbar-position.ts",
    "content": "import type { Position } from \"../types.js\";\nimport type { SnapEdge } from \"../components/toolbar/state.js\";\nimport {\n  TOOLBAR_SNAP_MARGIN_PX,\n  TOOLBAR_VELOCITY_MULTIPLIER_MS,\n  TOOLBAR_DEFAULT_POSITION_RATIO,\n} from \"../constants.js\";\nimport { getVisualViewport } from \"./get-visual-viewport.js\";\n\nexport const clampToRange = (value: number, min: number, max: number): number =>\n  Math.max(min, Math.min(value, max));\n\nexport const getPositionFromEdgeAndRatio = (\n  edge: SnapEdge,\n  ratio: number,\n  elementWidth: number,\n  elementHeight: number,\n): Position => {\n  const viewport = getVisualViewport();\n  const viewportWidth = viewport.width;\n  const viewportHeight = viewport.height;\n\n  const minX = viewport.offsetLeft + TOOLBAR_SNAP_MARGIN_PX;\n  const maxX = Math.max(\n    minX,\n    viewport.offsetLeft + viewportWidth - elementWidth - TOOLBAR_SNAP_MARGIN_PX,\n  );\n  const minY = viewport.offsetTop + TOOLBAR_SNAP_MARGIN_PX;\n  const maxY = Math.max(\n    minY,\n    viewport.offsetTop +\n      viewportHeight -\n      elementHeight -\n      TOOLBAR_SNAP_MARGIN_PX,\n  );\n\n  if (edge === \"top\" || edge === \"bottom\") {\n    const availableWidth = Math.max(\n      0,\n      viewportWidth - elementWidth - TOOLBAR_SNAP_MARGIN_PX * 2,\n    );\n    const positionX = Math.min(\n      maxX,\n      Math.max(\n        minX,\n        viewport.offsetLeft + TOOLBAR_SNAP_MARGIN_PX + availableWidth * ratio,\n      ),\n    );\n    const positionY = edge === \"top\" ? minY : maxY;\n    return { x: positionX, y: positionY };\n  }\n\n  const availableHeight = Math.max(\n    0,\n    viewportHeight - elementHeight - TOOLBAR_SNAP_MARGIN_PX * 2,\n  );\n  const positionY = Math.min(\n    maxY,\n    Math.max(\n      minY,\n      viewport.offsetTop + TOOLBAR_SNAP_MARGIN_PX + availableHeight * ratio,\n    ),\n  );\n  const positionX = edge === \"left\" ? minX : maxX;\n  return { x: positionX, y: positionY };\n};\n\nexport const getRatioFromPosition = (\n  edge: SnapEdge,\n  positionX: number,\n  positionY: number,\n  elementWidth: number,\n  elementHeight: number,\n): number => {\n  const viewport = getVisualViewport();\n  const viewportWidth = viewport.width;\n  const viewportHeight = viewport.height;\n\n  if (edge === \"top\" || edge === \"bottom\") {\n    const availableWidth =\n      viewportWidth - elementWidth - TOOLBAR_SNAP_MARGIN_PX * 2;\n    if (availableWidth <= 0) return TOOLBAR_DEFAULT_POSITION_RATIO;\n    return Math.max(\n      0,\n      Math.min(\n        1,\n        (positionX - viewport.offsetLeft - TOOLBAR_SNAP_MARGIN_PX) /\n          availableWidth,\n      ),\n    );\n  }\n  const availableHeight =\n    viewportHeight - elementHeight - TOOLBAR_SNAP_MARGIN_PX * 2;\n  if (availableHeight <= 0) return TOOLBAR_DEFAULT_POSITION_RATIO;\n  return Math.max(\n    0,\n    Math.min(\n      1,\n      (positionY - viewport.offsetTop - TOOLBAR_SNAP_MARGIN_PX) /\n        availableHeight,\n    ),\n  );\n};\n\ninterface Dimensions {\n  width: number;\n  height: number;\n}\n\nexport const calculateExpandedPositionFromCollapsed = (\n  collapsedPosition: Position,\n  edge: SnapEdge,\n  expandedDimensions: Dimensions,\n  actualCollapsedWidth: number,\n  actualCollapsedHeight: number,\n): { position: Position; ratio: number } => {\n  const viewport = getVisualViewport();\n  const viewportWidth = viewport.width;\n  const viewportHeight = viewport.height;\n  const { width: expandedWidth, height: expandedHeight } = expandedDimensions;\n\n  let newPosition: Position;\n\n  if (edge === \"top\" || edge === \"bottom\") {\n    const xOffset = (expandedWidth - actualCollapsedWidth) / 2;\n    const newExpandedX = collapsedPosition.x - xOffset;\n    const clampedX = clampToRange(\n      newExpandedX,\n      viewport.offsetLeft + TOOLBAR_SNAP_MARGIN_PX,\n      viewport.offsetLeft +\n        viewportWidth -\n        expandedWidth -\n        TOOLBAR_SNAP_MARGIN_PX,\n    );\n    const newExpandedY =\n      edge === \"top\"\n        ? viewport.offsetTop + TOOLBAR_SNAP_MARGIN_PX\n        : viewport.offsetTop +\n          viewportHeight -\n          expandedHeight -\n          TOOLBAR_SNAP_MARGIN_PX;\n    newPosition = { x: clampedX, y: newExpandedY };\n  } else {\n    const yOffset = (expandedHeight - actualCollapsedHeight) / 2;\n    const newExpandedY = collapsedPosition.y - yOffset;\n    const clampedY = clampToRange(\n      newExpandedY,\n      viewport.offsetTop + TOOLBAR_SNAP_MARGIN_PX,\n      viewport.offsetTop +\n        viewportHeight -\n        expandedHeight -\n        TOOLBAR_SNAP_MARGIN_PX,\n    );\n    const newExpandedX =\n      edge === \"left\"\n        ? viewport.offsetLeft + TOOLBAR_SNAP_MARGIN_PX\n        : viewport.offsetLeft +\n          viewportWidth -\n          expandedWidth -\n          TOOLBAR_SNAP_MARGIN_PX;\n    newPosition = { x: newExpandedX, y: clampedY };\n  }\n\n  const ratio = getRatioFromPosition(\n    edge,\n    newPosition.x,\n    newPosition.y,\n    expandedWidth,\n    expandedHeight,\n  );\n\n  return { position: newPosition, ratio };\n};\n\nexport const getCollapsedPosition = (\n  edge: SnapEdge,\n  expandedPosition: Position,\n  expandedDimensions: Dimensions,\n  collapsedDimensions: Dimensions,\n): Position => {\n  const viewport = getVisualViewport();\n  const { width: expandedWidth, height: expandedHeight } = expandedDimensions;\n  const { width: collapsedWidth, height: collapsedHeight } =\n    collapsedDimensions;\n\n  switch (edge) {\n    case \"top\":\n    case \"bottom\": {\n      const xOffset = (expandedWidth - collapsedWidth) / 2;\n      const centeredX = expandedPosition.x + xOffset;\n      const clampedX = clampToRange(\n        centeredX,\n        viewport.offsetLeft,\n        viewport.offsetLeft + viewport.width - collapsedWidth,\n      );\n      return {\n        x: clampedX,\n        y:\n          edge === \"top\"\n            ? viewport.offsetTop\n            : viewport.offsetTop + viewport.height - collapsedHeight,\n      };\n    }\n    case \"left\":\n    case \"right\": {\n      const yOffset = (expandedHeight - collapsedHeight) / 2;\n      const centeredY = expandedPosition.y + yOffset;\n      const clampedY = clampToRange(\n        centeredY,\n        viewport.offsetTop,\n        viewport.offsetTop + viewport.height - collapsedHeight,\n      );\n      return {\n        x:\n          edge === \"left\"\n            ? viewport.offsetLeft\n            : viewport.offsetLeft + viewport.width - collapsedWidth,\n        y: clampedY,\n      };\n    }\n  }\n};\n\ninterface SnapResult extends Position {\n  edge: SnapEdge;\n}\n\nexport const getSnapPosition = (\n  currentX: number,\n  currentY: number,\n  elementWidth: number,\n  elementHeight: number,\n  velocityX: number,\n  velocityY: number,\n): SnapResult => {\n  const viewport = getVisualViewport();\n  const viewportWidth = viewport.width;\n  const viewportHeight = viewport.height;\n\n  const projectedX = currentX + velocityX * TOOLBAR_VELOCITY_MULTIPLIER_MS;\n  const projectedY = currentY + velocityY * TOOLBAR_VELOCITY_MULTIPLIER_MS;\n\n  const distanceToTop = projectedY - viewport.offsetTop + elementHeight / 2;\n  const distanceToBottom =\n    viewport.offsetTop + viewportHeight - projectedY - elementHeight / 2;\n  const distanceToLeft = projectedX - viewport.offsetLeft + elementWidth / 2;\n  const distanceToRight =\n    viewport.offsetLeft + viewportWidth - projectedX - elementWidth / 2;\n\n  const minDistance = Math.min(\n    distanceToTop,\n    distanceToBottom,\n    distanceToLeft,\n    distanceToRight,\n  );\n\n  const clampX = (rawX: number) =>\n    clampToRange(\n      rawX,\n      viewport.offsetLeft + TOOLBAR_SNAP_MARGIN_PX,\n      viewport.offsetLeft +\n        viewportWidth -\n        elementWidth -\n        TOOLBAR_SNAP_MARGIN_PX,\n    );\n  const clampY = (rawY: number) =>\n    clampToRange(\n      rawY,\n      viewport.offsetTop + TOOLBAR_SNAP_MARGIN_PX,\n      viewport.offsetTop +\n        viewportHeight -\n        elementHeight -\n        TOOLBAR_SNAP_MARGIN_PX,\n    );\n\n  if (minDistance === distanceToTop) {\n    return {\n      edge: \"top\",\n      x: clampX(projectedX),\n      y: viewport.offsetTop + TOOLBAR_SNAP_MARGIN_PX,\n    };\n  }\n  if (minDistance === distanceToLeft) {\n    return {\n      edge: \"left\",\n      x: viewport.offsetLeft + TOOLBAR_SNAP_MARGIN_PX,\n      y: clampY(projectedY),\n    };\n  }\n  if (minDistance === distanceToRight) {\n    return {\n      edge: \"right\",\n      x:\n        viewport.offsetLeft +\n        viewportWidth -\n        elementWidth -\n        TOOLBAR_SNAP_MARGIN_PX,\n      y: clampY(projectedY),\n    };\n  }\n  return {\n    edge: \"bottom\",\n    x: clampX(projectedX),\n    y:\n      viewport.offsetTop +\n      viewportHeight -\n      elementHeight -\n      TOOLBAR_SNAP_MARGIN_PX,\n  };\n};\n"
  },
  {
    "path": "packages/react-grab/src/utils/truncate-string.ts",
    "content": "export const truncateString = (text: string, maxLength: number): string =>\n  text.length > maxLength ? `${text.slice(0, maxLength)}...` : text;\n"
  },
  {
    "path": "packages/react-grab/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"jsx\": \"preserve\",\n    \"jsxImportSource\": \"solid-js\",\n    \"module\": \"NodeNext\",\n    \"esModuleInterop\": true,\n    \"strictNullChecks\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"lib\": [\"esnext\", \"dom\", \"dom.iterable\"],\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"src\", \"tsup.config.ts\", \"e2e\", \"playwright.config.ts\"],\n  \"exclude\": [\"**/node_modules/**\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/react-grab/tsup.config.ts",
    "content": "import { execSync } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport { defineConfig, type Options } from \"tsup\";\n// @ts-expect-error -- esbuild-plugin-babel is not typed\nimport babel from \"esbuild-plugin-babel\";\n\nconst getCommitHash = (): string => {\n  try {\n    return execSync(\"git rev-parse --short HEAD\").toString().trim();\n  } catch {\n    return \"unknown\";\n  }\n};\n\nconst getPackageVersion = (): string =>\n  (JSON.parse(fs.readFileSync(\"package.json\", \"utf8\")) as { version: string })\n    .version;\n\nconst version =\n  process.env.VERSION ??\n  (process.env.VERCEL\n    ? getCommitHash()\n    : process.env.NODE_ENV === \"production\"\n      ? getPackageVersion()\n      : \"[DEV]\");\n\nconst banner = `/**\n * @license MIT\n *\n * Copyright (c) 2025 Aiden Bai\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */`;\n\nconst DEFAULT_OPTIONS: Options = {\n  banner: {\n    js: banner,\n  },\n  clean: [\"**/*\", \"!styles.css\"],\n  dts: true,\n  entry: [],\n  env: {\n    NODE_ENV: process.env.NODE_ENV ?? \"development\",\n    VERSION: version,\n  },\n  external: [],\n  format: [],\n  loader: {\n    \".css\": \"text\",\n  },\n  minify: process.env.NODE_ENV === \"production\",\n  noExternal: [\"clsx\", \"solid-js\", \"bippy\"],\n  onSuccess: process.env.COPY ? \"pbcopy < ./dist/index.global.js\" : undefined,\n  outDir: \"./dist\",\n  sourcemap: false,\n  splitting: false,\n  target: \"esnext\",\n  treeshake: true,\n};\n\nconst browserBuildConfig: Options = {\n  ...DEFAULT_OPTIONS,\n  entry: [\"./src/index.ts\"],\n  env: {\n    ...DEFAULT_OPTIONS.env,\n    VERSION: version,\n  },\n  format: [\"iife\"],\n  globalName: \"globalThis.__REACT_GRAB_MODULE__\",\n  loader: {\n    \".css\": \"text\",\n  },\n  outDir: \"./dist\",\n  platform: \"browser\",\n  esbuildPlugins: [\n    // eslint-disable-next-line @typescript-eslint/no-unsafe-call -- babel is not typed\n    babel({\n      filter: /\\.(tsx|jsx)$/,\n      config: {\n        presets: [\n          [\"@babel/preset-typescript\", { onlyRemoveTypeImports: true }],\n          \"babel-preset-solid\",\n        ],\n      },\n    }),\n  ],\n};\n\nconst libraryBuildConfig: Options = {\n  ...DEFAULT_OPTIONS,\n  clean: false,\n  entry: [\"./src/index.ts\", \"./src/core/index.tsx\", \"./src/primitives.ts\"],\n  format: [\"cjs\", \"esm\"],\n  loader: {\n    \".css\": \"text\",\n  },\n  outDir: \"./dist\",\n  platform: \"neutral\",\n  splitting: true,\n  esbuildPlugins: [\n    // eslint-disable-next-line @typescript-eslint/no-unsafe-call -- babel is not typed\n    babel({\n      filter: /\\.(tsx|jsx)$/,\n      config: {\n        presets: [\n          [\"@babel/preset-typescript\", { onlyRemoveTypeImports: true }],\n          \"babel-preset-solid\",\n        ],\n      },\n    }),\n  ],\n};\n\nexport default defineConfig([browserBuildConfig, libraryBuildConfig]);\n"
  },
  {
    "path": "packages/relay/CHANGELOG.md",
    "content": "# @react-grab/relay\n\n## 0.1.28\n\n### Patch Changes\n\n- fix\n- Updated dependencies\n  - @react-grab/utils@0.1.28\n\n## 0.1.27\n\n### Patch Changes\n\n- fix: install instructions\n- Updated dependencies\n  - @react-grab/utils@0.1.27\n\n## 0.1.26\n\n### Patch Changes\n\n- fix: minor tweaks\n- Updated dependencies\n  - @react-grab/utils@0.1.26\n\n## 0.1.25\n\n### Patch Changes\n\n- fix: primtiives\n- Updated dependencies\n  - @react-grab/utils@0.1.25\n\n## 0.1.24\n\n### Patch Changes\n\n- primitives\n- Updated dependencies\n  - @react-grab/utils@0.1.24\n\n## 0.1.23\n\n### Patch Changes\n\n- fix: npx command\n- Updated dependencies\n  - @react-grab/utils@0.1.23\n\n## 0.1.22\n\n### Patch Changes\n\n- fix: freezing\n- Updated dependencies\n  - @react-grab/utils@0.1.22\n\n## 0.1.21\n\n### Patch Changes\n\n- fix: up and down selection\n- Updated dependencies\n  - @react-grab/utils@0.1.21\n\n## 0.1.20\n\n### Patch Changes\n\n- fix: selection performacne\n- Updated dependencies\n  - @react-grab/utils@0.1.20\n\n## 0.1.19\n\n### Patch Changes\n\n- fix: gsap\n- Updated dependencies\n  - @react-grab/utils@0.1.19\n\n## 0.1.18\n\n### Patch Changes\n\n- fix: minor issues\n- Updated dependencies\n  - @react-grab/utils@0.1.18\n\n## 0.1.17\n\n### Patch Changes\n\n- fix: mcp\n- Updated dependencies\n  - @react-grab/utils@0.1.17\n\n## 0.1.16\n\n### Patch Changes\n\n- fix: environment detection\n- Updated dependencies\n  - @react-grab/utils@0.1.16\n\n## 0.1.15\n\n### Patch Changes\n\n- fix: animations and ux\n- Updated dependencies\n  - @react-grab/utils@0.1.15\n\n## 0.1.14\n\n### Patch Changes\n\n- fix: improve recent UX\n- Updated dependencies\n  - @react-grab/utils@0.1.14\n\n## 0.1.13\n\n### Patch Changes\n\n- fix MCP client injection\n- Updated dependencies\n  - @react-grab/utils@0.1.13\n\n## 0.1.12\n\n### Patch Changes\n\n- feat: MCP\n- Updated dependencies\n  - @react-grab/utils@0.1.12\n\n## 0.1.11\n\n### Patch Changes\n\n- fix: claude code provider\n- Updated dependencies\n  - @react-grab/utils@0.1.11\n\n## 0.1.10\n\n### Patch Changes\n\n- feat: cdn in cli\n- Updated dependencies\n  - @react-grab/utils@0.1.10\n\n## 0.1.9\n\n### Patch Changes\n\n- fix: startServer not exported in providers\n- Updated dependencies\n  - @react-grab/utils@0.1.9\n\n## 0.1.8\n\n### Patch Changes\n\n- fix: providers not detaching\n- Updated dependencies\n  - @react-grab/utils@0.1.8\n\n## 0.1.7\n\n### Patch Changes\n\n- fix: react freezing safety\n- Updated dependencies\n  - @react-grab/utils@0.1.7\n\n## 0.1.6\n\n### Patch Changes\n\n- fix: compat with React Scan\n- Updated dependencies\n  - @react-grab/utils@0.1.6\n\n## 0.1.5\n\n### Patch Changes\n\n- fix: fullscreen inset the root\n- Updated dependencies\n  - @react-grab/utils@0.1.5\n\n## 0.1.4\n\n### Patch Changes\n\n- fix: improve cli edge cases\n- Updated dependencies\n  - @react-grab/utils@0.1.4\n\n## 0.1.3\n\n### Patch Changes\n\n- fix: @react-grab/utils not publishing\n- Updated dependencies\n  - @react-grab/utils@0.1.3\n\n## 0.1.2\n\n### Patch Changes\n\n- fix: packages not being published\n- Updated dependencies\n  - @react-grab/utils@0.1.2\n\n## 0.1.1\n\n### Patch Changes\n\n- fix: clicking element after keyboard navigation\n- Updated dependencies\n  - @react-grab/utils@0.1.1\n\n## 0.1.0\n\n### Minor Changes\n\n- 81adb50: feat: browser\n\n### Patch Changes\n\n- 81adb50: fix: shell script\n- fb2b037: fix: cli\n- a3d5a94: fix: cli global install\n- 81adb50: feat: react support\n- 81adb50: fix: a11y\n- a5e7a6a: fix: optimize loading speed of cli\n- 90af3f6: fix: CLI hanging\n- 81adb50: fix: shell script\n- 78efee2: fix: cli\n- 074e593: fix: cli\n- 5cd3709: fix: decouple browser out from react-grab\n- 54c4867: ui improvements\n- Updated dependencies [81adb50]\n- Updated dependencies [fb2b037]\n- Updated dependencies [a3d5a94]\n- Updated dependencies [81adb50]\n- Updated dependencies [81adb50]\n- Updated dependencies [a5e7a6a]\n- Updated dependencies [90af3f6]\n- Updated dependencies [81adb50]\n- Updated dependencies [81adb50]\n- Updated dependencies [78efee2]\n- Updated dependencies [074e593]\n- Updated dependencies [5cd3709]\n- Updated dependencies [54c4867]\n  - @react-grab/utils@0.1.0\n\n## 0.1.0-beta.13\n\n### Patch Changes\n\n- ui improvements\n- Updated dependencies\n  - @react-grab/utils@0.1.0-beta.13\n\n## 0.1.0-beta.12\n\n### Patch Changes\n\n- fix: decouple browser out from react-grab\n- Updated dependencies\n  - @react-grab/utils@0.1.0-beta.12\n\n## 0.1.0-beta.11\n\n### Patch Changes\n\n- fix: cli global install\n- Updated dependencies\n  - @react-grab/utils@0.1.0-beta.11\n\n## 0.1.0-beta.10\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - @react-grab/utils@0.1.0-beta.10\n\n## 0.1.0-beta.9\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - @react-grab/utils@0.1.0-beta.9\n\n## 0.1.0-beta.8\n\n### Patch Changes\n\n- fix: cli\n- Updated dependencies\n  - @react-grab/utils@0.1.0-beta.8\n\n## 0.1.0-beta.7\n\n### Patch Changes\n\n- fix: CLI hanging\n- Updated dependencies\n  - @react-grab/utils@0.1.0-beta.7\n\n## 0.1.0-beta.6\n\n### Patch Changes\n\n- fix: optimize loading speed of cli\n- Updated dependencies\n  - @react-grab/utils@0.1.0-beta.6\n\n## 0.1.0-beta.5\n\n### Patch Changes\n\n- fix: a11y\n- Updated dependencies\n  - @react-grab/utils@0.1.0-beta.5\n\n## 0.1.0-beta.4\n\n### Patch Changes\n\n- feat: react support\n- Updated dependencies\n  - @react-grab/utils@0.1.0-beta.4\n\n## 0.1.0-beta.2\n\n### Patch Changes\n\n- fix: shell script\n- Updated dependencies\n  - @react-grab/utils@0.1.0-beta.2\n\n## 0.1.0-beta.1\n\n### Patch Changes\n\n- fix: shell script\n- Updated dependencies\n  - @react-grab/utils@0.1.0-beta.1\n\n## 0.1.0-beta.0\n\n### Minor Changes\n\n- feat: browser\n\n### Patch Changes\n\n- Updated dependencies\n  - @react-grab/utils@0.1.0-beta.0\n"
  },
  {
    "path": "packages/relay/package.json",
    "content": "{\n  \"name\": \"@react-grab/relay\",\n  \"version\": \"0.1.28\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.js\",\n      \"require\": \"./dist/index.cjs\"\n    },\n    \"./client\": {\n      \"types\": \"./dist/client.d.ts\",\n      \"import\": \"./dist/client.js\",\n      \"require\": \"./dist/client.cjs\"\n    },\n    \"./server\": {\n      \"types\": \"./dist/server.d.ts\",\n      \"import\": \"./dist/server.js\",\n      \"require\": \"./dist/server.cjs\"\n    },\n    \"./protocol\": {\n      \"types\": \"./dist/protocol.d.ts\",\n      \"import\": \"./dist/protocol.js\",\n      \"require\": \"./dist/protocol.cjs\"\n    }\n  },\n  \"scripts\": {\n    \"dev\": \"tsup --watch\",\n    \"build\": \"NODE_ENV=production tsup --clean\"\n  },\n  \"dependencies\": {\n    \"@react-grab/utils\": \"workspace:*\",\n    \"fkill\": \"^9.0.0\",\n    \"picocolors\": \"^1.1.1\",\n    \"ws\": \"^8.18.0\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^22.10.7\",\n    \"@types/ws\": \"^8.5.13\",\n    \"tsup\": \"^8.4.0\"\n  }\n}\n"
  },
  {
    "path": "packages/relay/src/client.ts",
    "content": "import type {\n  AgentContext,\n  BrowserToRelayMessage,\n  RelayToBrowserMessage,\n} from \"./protocol.js\";\nimport {\n  DEFAULT_RELAY_PORT,\n  DEFAULT_RECONNECT_INTERVAL_MS,\n  RELAY_TOKEN_PARAM,\n} from \"./protocol.js\";\n\nexport interface RelayClient {\n  connect: () => Promise<void>;\n  disconnect: () => void;\n  isConnected: () => boolean;\n  sendAgentRequest: (agentId: string, context: AgentContext) => boolean;\n  abortAgent: (agentId: string, sessionId: string) => void;\n  undoAgent: (agentId: string, sessionId: string) => boolean;\n  redoAgent: (agentId: string, sessionId: string) => boolean;\n  onMessage: (callback: (message: RelayToBrowserMessage) => void) => () => void;\n  onHandlersChange: (callback: (handlers: string[]) => void) => () => void;\n  onConnectionChange: (callback: (connected: boolean) => void) => () => void;\n  getAvailableHandlers: () => string[];\n}\n\ninterface RelayClientOptions {\n  serverUrl?: string;\n  autoReconnect?: boolean;\n  reconnectIntervalMs?: number;\n  token?: string;\n}\n\nexport const createRelayClient = (\n  options: RelayClientOptions = {},\n): RelayClient => {\n  const serverUrl = options.serverUrl ?? `ws://localhost:${DEFAULT_RELAY_PORT}`;\n  const autoReconnect = options.autoReconnect ?? true;\n  const reconnectIntervalMs =\n    options.reconnectIntervalMs ?? DEFAULT_RECONNECT_INTERVAL_MS;\n  const token = options.token;\n\n  let webSocketConnection: WebSocket | null = null;\n  let isConnectedState = false;\n  let availableHandlers: string[] = [];\n  let reconnectTimeoutId: ReturnType<typeof setTimeout> | null = null;\n  let pendingConnectionPromise: Promise<void> | null = null;\n  let pendingConnectionReject: ((error: Error) => void) | null = null;\n  let isIntentionalDisconnect = false;\n\n  const messageCallbacks = new Set<(message: RelayToBrowserMessage) => void>();\n  const handlersChangeCallbacks = new Set<(handlers: string[]) => void>();\n  const connectionChangeCallbacks = new Set<(connected: boolean) => void>();\n\n  const scheduleReconnect = () => {\n    if (!autoReconnect || reconnectTimeoutId || isIntentionalDisconnect) return;\n\n    reconnectTimeoutId = setTimeout(() => {\n      reconnectTimeoutId = null;\n      connect().catch(() => {});\n    }, reconnectIntervalMs);\n  };\n\n  const handleMessage = (event: MessageEvent) => {\n    try {\n      const message = JSON.parse(event.data as string) as RelayToBrowserMessage;\n\n      if (message.type === \"handlers\" && message.handlers) {\n        availableHandlers = message.handlers;\n        for (const callback of handlersChangeCallbacks) {\n          callback(availableHandlers);\n        }\n      }\n\n      for (const callback of messageCallbacks) {\n        callback(message);\n      }\n    } catch {}\n  };\n\n  const connect = (): Promise<void> => {\n    if (webSocketConnection?.readyState === WebSocket.OPEN) {\n      return Promise.resolve();\n    }\n\n    if (pendingConnectionPromise) {\n      return pendingConnectionPromise;\n    }\n\n    isIntentionalDisconnect = false;\n\n    pendingConnectionPromise = new Promise((resolve, reject) => {\n      pendingConnectionReject = reject;\n      const connectionUrl = token\n        ? `${serverUrl}?${RELAY_TOKEN_PARAM}=${encodeURIComponent(token)}`\n        : serverUrl;\n      webSocketConnection = new WebSocket(connectionUrl);\n\n      webSocketConnection.onopen = () => {\n        pendingConnectionPromise = null;\n        pendingConnectionReject = null;\n        isConnectedState = true;\n        for (const callback of connectionChangeCallbacks) {\n          callback(true);\n        }\n        resolve();\n      };\n\n      webSocketConnection.onmessage = handleMessage;\n\n      webSocketConnection.onclose = () => {\n        if (pendingConnectionReject) {\n          pendingConnectionReject(new Error(\"WebSocket connection closed\"));\n          pendingConnectionReject = null;\n        }\n        pendingConnectionPromise = null;\n        isConnectedState = false;\n        availableHandlers = [];\n        for (const callback of handlersChangeCallbacks) {\n          callback(availableHandlers);\n        }\n        for (const callback of connectionChangeCallbacks) {\n          callback(false);\n        }\n        scheduleReconnect();\n      };\n\n      webSocketConnection.onerror = () => {\n        pendingConnectionPromise = null;\n        pendingConnectionReject = null;\n        isConnectedState = false;\n        reject(new Error(\"WebSocket connection failed\"));\n      };\n    });\n\n    return pendingConnectionPromise;\n  };\n\n  const disconnect = () => {\n    isIntentionalDisconnect = true;\n    if (reconnectTimeoutId) {\n      clearTimeout(reconnectTimeoutId);\n      reconnectTimeoutId = null;\n    }\n    if (pendingConnectionReject) {\n      pendingConnectionReject(new Error(\"Connection aborted\"));\n      pendingConnectionReject = null;\n    }\n    pendingConnectionPromise = null;\n    webSocketConnection?.close();\n    webSocketConnection = null;\n    isConnectedState = false;\n    availableHandlers = [];\n  };\n\n  const isConnected = () => isConnectedState;\n\n  const sendMessage = (message: BrowserToRelayMessage): boolean => {\n    if (webSocketConnection?.readyState === WebSocket.OPEN) {\n      webSocketConnection.send(JSON.stringify(message));\n      return true;\n    }\n    return false;\n  };\n\n  const sendAgentRequest = (\n    agentId: string,\n    context: AgentContext,\n  ): boolean => {\n    return sendMessage({\n      type: \"agent-request\",\n      agentId,\n      sessionId: context.sessionId,\n      context,\n    });\n  };\n\n  const abortAgent = (agentId: string, sessionId: string) => {\n    sendMessage({\n      type: \"agent-abort\",\n      agentId,\n      sessionId,\n    });\n  };\n\n  const undoAgent = (agentId: string, sessionId: string): boolean => {\n    return sendMessage({\n      type: \"agent-undo\",\n      agentId,\n      sessionId,\n    });\n  };\n\n  const redoAgent = (agentId: string, sessionId: string): boolean => {\n    return sendMessage({\n      type: \"agent-redo\",\n      agentId,\n      sessionId,\n    });\n  };\n\n  const onMessage = (\n    callback: (message: RelayToBrowserMessage) => void,\n  ): (() => void) => {\n    messageCallbacks.add(callback);\n    return () => messageCallbacks.delete(callback);\n  };\n\n  const onHandlersChange = (\n    callback: (handlers: string[]) => void,\n  ): (() => void) => {\n    handlersChangeCallbacks.add(callback);\n    return () => handlersChangeCallbacks.delete(callback);\n  };\n\n  const onConnectionChange = (\n    callback: (connected: boolean) => void,\n  ): (() => void) => {\n    connectionChangeCallbacks.add(callback);\n    queueMicrotask(() => {\n      if (connectionChangeCallbacks.has(callback)) {\n        callback(isConnectedState);\n      }\n    });\n    return () => connectionChangeCallbacks.delete(callback);\n  };\n\n  const getAvailableHandlers = () => availableHandlers;\n\n  return {\n    connect,\n    disconnect,\n    isConnected,\n    sendAgentRequest,\n    abortAgent,\n    undoAgent,\n    redoAgent,\n    onMessage,\n    onHandlersChange,\n    onConnectionChange,\n    getAvailableHandlers,\n  };\n};\n\nexport interface AgentProvider {\n  send: (context: AgentContext, signal: AbortSignal) => AsyncIterable<string>;\n  abort?: (sessionId: string) => Promise<void>;\n  undo?: () => Promise<void>;\n  redo?: () => Promise<void>;\n  checkConnection?: () => Promise<boolean>;\n  supportsResume?: boolean;\n  supportsFollowUp?: boolean;\n}\n\ninterface CreateRelayAgentProviderOptions {\n  relayClient: RelayClient;\n  agentId: string;\n}\n\nexport const createRelayAgentProvider = (\n  options: CreateRelayAgentProviderOptions,\n): AgentProvider => {\n  const { relayClient, agentId } = options;\n\n  const checkConnection = async (): Promise<boolean> => {\n    if (!relayClient.isConnected()) {\n      try {\n        await relayClient.connect();\n      } catch {\n        return false;\n      }\n    }\n    return relayClient.getAvailableHandlers().includes(agentId);\n  };\n\n  const send = async function* (\n    context: AgentContext,\n    signal: AbortSignal,\n  ): AsyncIterable<string> {\n    if (signal.aborted) {\n      throw new DOMException(\"Aborted\", \"AbortError\");\n    }\n\n    yield \"Connecting…\";\n\n    const sessionId =\n      context.sessionId ??\n      `session-${Date.now()}-${Math.random().toString(36).slice(2)}`;\n\n    const contextWithSession: AgentContext = {\n      ...context,\n      sessionId,\n    };\n\n    const messageQueue: string[] = [];\n    let resolveNextMessage:\n      | ((value: IteratorResult<string, void>) => void)\n      | null = null;\n    let rejectNextMessage: ((error: Error) => void) | null = null;\n    let isDone = false;\n    let errorMessage: string | null = null;\n\n    const handleAbort = () => {\n      relayClient.abortAgent(agentId, sessionId);\n      isDone = true;\n      if (resolveNextMessage) {\n        resolveNextMessage({ value: undefined, done: true });\n        resolveNextMessage = null;\n        rejectNextMessage = null;\n      }\n    };\n\n    signal.addEventListener(\"abort\", handleAbort, { once: true });\n\n    const handleConnectionChange = (connected: boolean) => {\n      if (!connected && !isDone) {\n        errorMessage = \"Relay connection lost\";\n        isDone = true;\n        if (rejectNextMessage) {\n          rejectNextMessage(new Error(errorMessage));\n          resolveNextMessage = null;\n          rejectNextMessage = null;\n        }\n      }\n    };\n\n    const unsubscribeConnection = relayClient.onConnectionChange(\n      handleConnectionChange,\n    );\n\n    const unsubscribeMessage = relayClient.onMessage((message) => {\n      if (message.sessionId !== sessionId) return;\n\n      if (message.type === \"agent-status\" && message.content) {\n        messageQueue.push(message.content);\n        if (resolveNextMessage) {\n          const nextMessage = messageQueue.shift();\n          if (nextMessage !== undefined) {\n            resolveNextMessage({ value: nextMessage, done: false });\n            resolveNextMessage = null;\n            rejectNextMessage = null;\n          }\n        }\n      } else if (message.type === \"agent-done\") {\n        isDone = true;\n        if (resolveNextMessage) {\n          resolveNextMessage({ value: undefined, done: true });\n          resolveNextMessage = null;\n          rejectNextMessage = null;\n        }\n      } else if (message.type === \"agent-error\") {\n        errorMessage = message.content ?? \"Unknown error\";\n        isDone = true;\n        if (rejectNextMessage) {\n          rejectNextMessage(new Error(errorMessage));\n          resolveNextMessage = null;\n          rejectNextMessage = null;\n        }\n      }\n    });\n\n    if (!relayClient.isConnected()) {\n      unsubscribeConnection();\n      unsubscribeMessage();\n      signal.removeEventListener(\"abort\", handleAbort);\n      throw new Error(\"Relay connection is not open\");\n    }\n\n    const didSendRequest = relayClient.sendAgentRequest(\n      agentId,\n      contextWithSession,\n    );\n    if (!didSendRequest) {\n      unsubscribeConnection();\n      unsubscribeMessage();\n      signal.removeEventListener(\"abort\", handleAbort);\n      throw new Error(\"Failed to send agent request: connection not open\");\n    }\n\n    try {\n      while (true) {\n        if (messageQueue.length > 0) {\n          const next = messageQueue.shift();\n          if (next !== undefined) {\n            yield next;\n          }\n          continue;\n        }\n\n        if (isDone || signal.aborted) {\n          break;\n        }\n\n        const result = await new Promise<IteratorResult<string, void>>(\n          (resolve, reject) => {\n            resolveNextMessage = resolve;\n            rejectNextMessage = reject;\n          },\n        );\n\n        if (result.done) break;\n        yield result.value;\n      }\n\n      if (errorMessage) {\n        throw new Error(errorMessage);\n      }\n    } finally {\n      signal.removeEventListener(\"abort\", handleAbort);\n      unsubscribeConnection();\n      unsubscribeMessage();\n    }\n  };\n\n  const abort = async (sessionId: string): Promise<void> => {\n    relayClient.abortAgent(agentId, sessionId);\n  };\n\n  const waitForOperationResponse = (sessionId: string): Promise<void> => {\n    return new Promise((resolve, reject) => {\n      let didCleanup = false;\n\n      const cleanup = () => {\n        if (didCleanup) return;\n        didCleanup = true;\n        unsubscribeMessage();\n        unsubscribeConnection();\n      };\n\n      const unsubscribeMessage = relayClient.onMessage((message) => {\n        if (message.sessionId !== sessionId) return;\n\n        cleanup();\n\n        if (message.type === \"agent-done\") {\n          resolve();\n        } else if (message.type === \"agent-error\") {\n          reject(new Error(message.content ?? \"Operation failed\"));\n        }\n      });\n\n      const unsubscribeConnection = relayClient.onConnectionChange(\n        (connected) => {\n          if (!connected) {\n            cleanup();\n            reject(\n              new Error(\"Connection lost while waiting for operation response\"),\n            );\n          }\n        },\n      );\n    });\n  };\n\n  const undo = async (): Promise<void> => {\n    const sessionId = `undo-${agentId}-${Date.now()}-${Math.random().toString(36).slice(2)}`;\n    const didSend = relayClient.undoAgent(agentId, sessionId);\n    if (!didSend) {\n      throw new Error(\"Failed to send undo request: connection not open\");\n    }\n    return waitForOperationResponse(sessionId);\n  };\n\n  const redo = async (): Promise<void> => {\n    const sessionId = `redo-${agentId}-${Date.now()}-${Math.random().toString(36).slice(2)}`;\n    const didSend = relayClient.redoAgent(agentId, sessionId);\n    if (!didSend) {\n      throw new Error(\"Failed to send redo request: connection not open\");\n    }\n    return waitForOperationResponse(sessionId);\n  };\n\n  return {\n    send,\n    abort,\n    undo,\n    redo,\n    checkConnection,\n    supportsResume: true,\n    supportsFollowUp: true,\n  };\n};\n\ninterface ReactGrabApi {\n  registerPlugin: (plugin: unknown) => void;\n}\n\ndeclare global {\n  interface Window {\n    __REACT_GRAB_RELAY__?: RelayClient;\n    __REACT_GRAB__?: ReactGrabApi;\n  }\n}\n\nlet defaultRelayClient: RelayClient | null = null;\n\nexport const getDefaultRelayClient = (): RelayClient | null => {\n  if (typeof window === \"undefined\") {\n    return null;\n  }\n\n  if (window.__REACT_GRAB_RELAY__) {\n    defaultRelayClient = window.__REACT_GRAB_RELAY__;\n    return defaultRelayClient;\n  }\n\n  if (!defaultRelayClient) {\n    defaultRelayClient = createRelayClient();\n    window.__REACT_GRAB_RELAY__ = defaultRelayClient;\n  }\n  return defaultRelayClient;\n};\n\ninterface ProviderPluginConfig {\n  agentId: string;\n  pluginName: string;\n  actionId: string;\n  actionLabel: string;\n}\n\nconst isReactGrabApi = (value: unknown): value is ReactGrabApi =>\n  typeof value === \"object\" && value !== null && \"registerPlugin\" in value;\n\nexport const createProviderClientPlugin = (config: ProviderPluginConfig) => {\n  const createAgentProvider = (\n    providerOptions: { relayClient?: RelayClient } = {},\n  ): AgentProvider => {\n    const relayClient = providerOptions.relayClient ?? getDefaultRelayClient();\n    if (!relayClient) {\n      throw new Error(\"RelayClient is required in browser environments\");\n    }\n\n    return createRelayAgentProvider({\n      relayClient,\n      agentId: config.agentId,\n    });\n  };\n\n  const attachAgent = async () => {\n    if (typeof window === \"undefined\") return;\n\n    const relayClient = getDefaultRelayClient();\n    if (!relayClient) return;\n\n    try {\n      await relayClient.connect();\n    } catch {\n      return;\n    }\n\n    const provider = createRelayAgentProvider({\n      relayClient,\n      agentId: config.agentId,\n    });\n\n    const attach = (api: ReactGrabApi) => {\n      const agent = { provider, storage: sessionStorage };\n      api.registerPlugin({\n        name: config.pluginName,\n        actions: [\n          {\n            id: config.actionId,\n            label: config.actionLabel,\n            shortcut: \"Enter\",\n            onAction: (actionContext: {\n              enterPromptMode?: (agent: unknown) => void;\n            }) => {\n              actionContext.enterPromptMode?.(agent);\n            },\n            agent,\n          },\n        ],\n      });\n    };\n\n    const existingApi = window.__REACT_GRAB__;\n    if (isReactGrabApi(existingApi)) {\n      attach(existingApi);\n      return;\n    }\n\n    window.addEventListener(\n      \"react-grab:init\",\n      (event: Event) => {\n        if (!(event instanceof CustomEvent)) return;\n        if (!isReactGrabApi(event.detail)) return;\n        attach(event.detail);\n      },\n      { once: true },\n    );\n\n    // HACK: Check again after adding listener in case of race condition\n    const apiAfterListener = window.__REACT_GRAB__;\n    if (isReactGrabApi(apiAfterListener)) {\n      attach(apiAfterListener);\n    }\n  };\n\n  return { createAgentProvider, attachAgent };\n};\n"
  },
  {
    "path": "packages/relay/src/connection.ts",
    "content": "import pc from \"picocolors\";\nimport fkill from \"fkill\";\nimport type { AgentHandler } from \"./protocol.js\";\nimport {\n  DEFAULT_RELAY_PORT,\n  HEALTH_CHECK_TIMEOUT_MS,\n  POST_KILL_DELAY_MS,\n  RELAY_TOKEN_PARAM,\n} from \"./protocol.js\";\nimport { createRelayServer, type RelayServer } from \"./server.js\";\nimport { sleep } from \"@react-grab/utils/server\";\n\nconst VERSION = process.env.VERSION ?? \"0.0.0\";\n\ninterface ConnectRelayOptions {\n  port?: number;\n  handler: AgentHandler;\n  token?: string;\n}\n\ninterface RelayConnection {\n  disconnect: () => Promise<void>;\n}\n\nconst checkIfRelayServerIsRunning = async (\n  port: number,\n  token?: string,\n): Promise<boolean> => {\n  try {\n    const healthUrl = token\n      ? `http://localhost:${port}/health?${RELAY_TOKEN_PARAM}=${encodeURIComponent(token)}`\n      : `http://localhost:${port}/health`;\n    const response = await fetch(healthUrl, {\n      method: \"GET\",\n      signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS),\n    });\n    return response.ok;\n  } catch {\n    return false;\n  }\n};\n\nexport const connectRelay = async (\n  options: ConnectRelayOptions,\n): Promise<RelayConnection> => {\n  const relayPort = options.port ?? DEFAULT_RELAY_PORT;\n  const { handler, token } = options;\n\n  let relayServer: RelayServer | null = null;\n  let isRelayHost = false;\n\n  const isRelayServerRunning = await checkIfRelayServerIsRunning(\n    relayPort,\n    token,\n  );\n\n  if (isRelayServerRunning) {\n    relayServer = await connectToExistingRelay(relayPort, handler, token);\n  } else {\n    await fkill(`:${relayPort}`, { force: true, silent: true }).catch(() => {});\n    await sleep(POST_KILL_DELAY_MS);\n\n    relayServer = createRelayServer({ port: relayPort, token });\n    relayServer.registerHandler(handler);\n\n    try {\n      await relayServer.start();\n      isRelayHost = true;\n    } catch (error) {\n      const isAddressInUse =\n        error instanceof Error &&\n        \"code\" in error &&\n        (error as NodeJS.ErrnoException).code === \"EADDRINUSE\";\n\n      if (!isAddressInUse) throw error;\n\n      await sleep(POST_KILL_DELAY_MS);\n      const isNowRunning = await checkIfRelayServerIsRunning(relayPort, token);\n\n      if (!isNowRunning) throw error;\n\n      relayServer = await connectToExistingRelay(relayPort, handler, token);\n    }\n  }\n\n  printStartupMessage(handler.agentId, relayPort);\n\n  const handleShutdown = async () => {\n    if (isRelayHost) {\n      await relayServer?.stop();\n    } else {\n      relayServer?.unregisterHandler(handler.agentId);\n    }\n    process.exit(0);\n  };\n\n  process.on(\"SIGTERM\", handleShutdown);\n  process.on(\"SIGINT\", handleShutdown);\n\n  return {\n    disconnect: async () => {\n      process.off(\"SIGTERM\", handleShutdown);\n      process.off(\"SIGINT\", handleShutdown);\n      if (isRelayHost) {\n        await relayServer?.stop();\n      } else {\n        relayServer?.unregisterHandler(handler.agentId);\n      }\n    },\n  };\n};\n\nconst connectToExistingRelay = async (\n  port: number,\n  handler: AgentHandler,\n  token?: string,\n): Promise<RelayServer> => {\n  const { WebSocket } = await import(\"ws\");\n\n  return new Promise((resolve, reject) => {\n    const connectionUrl = token\n      ? `ws://localhost:${port}?${RELAY_TOKEN_PARAM}=${encodeURIComponent(token)}`\n      : `ws://localhost:${port}`;\n    const socket = new WebSocket(connectionUrl, {\n      headers: { \"x-relay-handler\": \"true\" },\n    });\n\n    socket.on(\"open\", () => {\n      let isSocketClosed = false;\n      const activeSessionIds = new Set<string>();\n\n      const sendData = (data: string): boolean => {\n        if (isSocketClosed || socket.readyState !== WebSocket.OPEN) {\n          return false;\n        }\n        try {\n          socket.send(data);\n          return true;\n        } catch {\n          return false;\n        }\n      };\n\n      socket.on(\"close\", () => {\n        isSocketClosed = true;\n        for (const sessionId of activeSessionIds) {\n          try {\n            handler.abort?.(sessionId);\n          } catch {}\n        }\n        activeSessionIds.clear();\n      });\n\n      socket.send(\n        JSON.stringify({\n          type: \"register-handler\",\n          agentId: handler.agentId,\n        }),\n      );\n\n      socket.on(\"message\", async (data) => {\n        try {\n          const message = JSON.parse(data.toString());\n\n          if (message.type === \"invoke-handler\") {\n            const { method, sessionId, payload } = message;\n\n            if (method === \"run\" && payload?.prompt) {\n              activeSessionIds.add(sessionId);\n              try {\n                let didComplete = false;\n                for await (const agentMessage of handler.run(payload.prompt, {\n                  sessionId,\n                })) {\n                  if (isSocketClosed) {\n                    break;\n                  }\n                  sendData(\n                    JSON.stringify({\n                      type:\n                        agentMessage.type === \"status\"\n                          ? \"agent-status\"\n                          : agentMessage.type === \"error\"\n                            ? \"agent-error\"\n                            : \"agent-done\",\n                      sessionId,\n                      agentId: handler.agentId,\n                      content: agentMessage.content,\n                    }),\n                  );\n                  if (\n                    agentMessage.type === \"done\" ||\n                    agentMessage.type === \"error\"\n                  ) {\n                    didComplete = true;\n                  }\n                }\n                if (!didComplete && !isSocketClosed) {\n                  sendData(\n                    JSON.stringify({\n                      type: \"agent-done\",\n                      sessionId,\n                      agentId: handler.agentId,\n                      content: \"\",\n                    }),\n                  );\n                }\n              } catch (error) {\n                sendData(\n                  JSON.stringify({\n                    type: \"agent-error\",\n                    sessionId,\n                    agentId: handler.agentId,\n                    content:\n                      error instanceof Error ? error.message : \"Unknown error\",\n                  }),\n                );\n              } finally {\n                activeSessionIds.delete(sessionId);\n              }\n            } else if (method === \"abort\") {\n              handler.abort?.(sessionId);\n            } else if (method === \"undo\") {\n              try {\n                await handler.undo?.();\n                sendData(\n                  JSON.stringify({\n                    type: \"agent-done\",\n                    sessionId,\n                    agentId: handler.agentId,\n                    content: \"\",\n                  }),\n                );\n              } catch (error) {\n                sendData(\n                  JSON.stringify({\n                    type: \"agent-error\",\n                    sessionId,\n                    agentId: handler.agentId,\n                    content:\n                      error instanceof Error ? error.message : \"Unknown error\",\n                  }),\n                );\n              }\n            } else if (method === \"redo\") {\n              try {\n                await handler.redo?.();\n                sendData(\n                  JSON.stringify({\n                    type: \"agent-done\",\n                    sessionId,\n                    agentId: handler.agentId,\n                    content: \"\",\n                  }),\n                );\n              } catch (error) {\n                sendData(\n                  JSON.stringify({\n                    type: \"agent-error\",\n                    sessionId,\n                    agentId: handler.agentId,\n                    content:\n                      error instanceof Error ? error.message : \"Unknown error\",\n                  }),\n                );\n              }\n            }\n          }\n        } catch {}\n      });\n\n      const proxyServer: RelayServer = {\n        start: async () => {},\n        stop: async () => {\n          socket.close();\n        },\n        registerHandler: () => {},\n        unregisterHandler: (agentId) => {\n          sendData(\n            JSON.stringify({\n              type: \"unregister-handler\",\n              agentId,\n            }),\n          );\n          socket.close();\n        },\n        getRegisteredHandlerIds: () => [handler.agentId],\n      };\n\n      resolve(proxyServer);\n    });\n\n    socket.on(\"error\", (error) => {\n      reject(error);\n    });\n  });\n};\n\nconst printStartupMessage = (agentId: string, port: number) => {\n  console.log(\n    `${pc.magenta(\"✿\")} ${pc.bold(\"React Grab\")} ${pc.gray(VERSION)} ${pc.dim(`(${agentId})`)}`,\n  );\n  console.log(`- Local:    ${pc.cyan(`ws://localhost:${port}`)}`);\n};\n\nexport const startProviderServer = (source: string, handler: AgentHandler) => {\n  fetch(\n    `https://www.react-grab.com/api/version?source=${source}&t=${Date.now()}`,\n  ).catch(() => {});\n\n  connectRelay({ handler });\n};\n"
  },
  {
    "path": "packages/relay/src/index.ts",
    "content": "export {\n  DEFAULT_RELAY_PORT,\n  DEFAULT_RECONNECT_INTERVAL_MS,\n  HEALTH_CHECK_TIMEOUT_MS,\n  POST_KILL_DELAY_MS,\n  RELAY_TOKEN_PARAM,\n  COMPLETED_STATUS,\n  type AgentMessage,\n  type AgentContext,\n  type AgentRunOptions,\n  type AgentHandler,\n  type HandlerRegistrationMessage,\n  type HandlerUnregisterMessage,\n  type RelayToHandlerMessage,\n  type HandlerToRelayMessage,\n  type BrowserToRelayMessage,\n  type RelayToBrowserMessage,\n} from \"./protocol.js\";\n\nexport { createRelayServer, type RelayServer } from \"./server.js\";\n\nexport { connectRelay, startProviderServer } from \"./connection.js\";\n\nexport {\n  createRelayClient,\n  createRelayAgentProvider,\n  getDefaultRelayClient,\n  createProviderClientPlugin,\n  type RelayClient,\n  type AgentProvider,\n} from \"./client.js\";\n"
  },
  {
    "path": "packages/relay/src/protocol.ts",
    "content": "export const DEFAULT_RELAY_PORT = 4722;\nexport const DEFAULT_RECONNECT_INTERVAL_MS = 3000;\nexport const HEALTH_CHECK_TIMEOUT_MS = 1000;\nexport const POST_KILL_DELAY_MS = 100;\nexport const RELAY_TOKEN_PARAM = \"token\";\nexport const COMPLETED_STATUS = \"Completed\";\n\nexport interface AgentMessage {\n  type: \"status\" | \"error\" | \"done\";\n  content: string;\n}\n\nexport interface AgentContext {\n  content: string[];\n  prompt: string;\n  options?: unknown;\n  sessionId?: string;\n}\n\nexport interface AgentRunOptions {\n  cwd?: string;\n  signal?: AbortSignal;\n  sessionId?: string;\n}\n\nexport interface AgentHandler {\n  agentId: string;\n  run: (\n    userPrompt: string,\n    options?: AgentRunOptions,\n  ) => AsyncGenerator<AgentMessage>;\n  abort?: (sessionId: string) => void;\n  undo?: () => Promise<void>;\n  redo?: () => Promise<void>;\n}\n\nexport interface HandlerRegistrationMessage {\n  type: \"register-handler\";\n  agentId: string;\n}\n\nexport interface HandlerUnregisterMessage {\n  type: \"unregister-handler\";\n  agentId: string;\n}\n\nexport interface RelayToHandlerMessage {\n  type: \"invoke-handler\";\n  method: \"run\" | \"abort\" | \"undo\" | \"redo\";\n  sessionId: string;\n  payload?: {\n    prompt?: string;\n    context?: AgentContext;\n  };\n}\n\nexport interface HandlerToRelayMessage {\n  type: \"agent-status\" | \"agent-done\" | \"agent-error\";\n  sessionId: string;\n  agentId: string;\n  content?: string;\n}\n\nexport interface BrowserToRelayMessage {\n  type:\n    | \"agent-request\"\n    | \"agent-abort\"\n    | \"agent-undo\"\n    | \"agent-redo\"\n    | \"health\";\n  agentId: string;\n  sessionId?: string;\n  context?: AgentContext;\n}\n\nexport interface RelayToBrowserMessage {\n  type: \"agent-status\" | \"agent-done\" | \"agent-error\" | \"health\" | \"handlers\";\n  agentId?: string;\n  sessionId?: string;\n  content?: string;\n  handlers?: string[];\n}\n\nexport type HandlerMessage =\n  | HandlerRegistrationMessage\n  | HandlerUnregisterMessage\n  | HandlerToRelayMessage;\n\nexport type RelayMessage = RelayToHandlerMessage | RelayToBrowserMessage;\n"
  },
  {
    "path": "packages/relay/src/server.ts",
    "content": "import { WebSocketServer, WebSocket } from \"ws\";\nimport { createServer as createHttpServer } from \"node:http\";\nimport type {\n  AgentHandler,\n  AgentMessage,\n  BrowserToRelayMessage,\n  HandlerMessage,\n  RelayToHandlerMessage,\n  RelayToBrowserMessage,\n  AgentContext,\n} from \"./protocol.js\";\nimport { DEFAULT_RELAY_PORT, RELAY_TOKEN_PARAM } from \"./protocol.js\";\n\ninterface RegisteredHandler {\n  agentId: string;\n  handler: AgentHandler;\n  socket?: WebSocket;\n}\n\ninterface SessionMessageQueue {\n  push: (message: AgentMessage) => void;\n  close: () => void;\n  [Symbol.asyncIterator]: () => AsyncIterator<AgentMessage>;\n}\n\nconst createSessionMessageQueue = (): SessionMessageQueue => {\n  const pendingMessages: AgentMessage[] = [];\n  let resolveNextMessage:\n    | ((result: IteratorResult<AgentMessage>) => void)\n    | null = null;\n  let isClosed = false;\n\n  return {\n    push: (message) => {\n      if (isClosed) return;\n      if (resolveNextMessage) {\n        resolveNextMessage({ value: message, done: false });\n        resolveNextMessage = null;\n      } else {\n        pendingMessages.push(message);\n      }\n    },\n    close: () => {\n      isClosed = true;\n      if (resolveNextMessage) {\n        resolveNextMessage({\n          value: undefined,\n          done: true,\n        } as IteratorResult<AgentMessage>);\n        resolveNextMessage = null;\n      }\n    },\n    [Symbol.asyncIterator]: () => ({\n      next: () => {\n        if (pendingMessages.length > 0) {\n          return Promise.resolve({\n            value: pendingMessages.shift()!,\n            done: false,\n          });\n        }\n        if (isClosed) {\n          return Promise.resolve({\n            value: undefined,\n            done: true,\n          } as IteratorResult<AgentMessage>);\n        }\n        return new Promise((resolve) => {\n          resolveNextMessage = resolve;\n        });\n      },\n    }),\n  };\n};\n\ninterface ActiveSession {\n  sessionId: string;\n  agentId: string;\n  browserSocket: WebSocket;\n  handlerSocket?: WebSocket;\n  abortController: AbortController;\n}\n\ninterface RelayServerOptions {\n  port?: number;\n  token?: string;\n}\n\nexport interface RelayServer {\n  start: () => Promise<void>;\n  stop: () => Promise<void>;\n  registerHandler: (handler: AgentHandler) => void;\n  unregisterHandler: (agentId: string) => void;\n  getRegisteredHandlerIds: () => string[];\n}\n\nexport const createRelayServer = (\n  options: RelayServerOptions = {},\n): RelayServer => {\n  const port = options.port ?? DEFAULT_RELAY_PORT;\n  const token = options.token;\n\n  const registeredHandlers = new Map<string, RegisteredHandler>();\n  const activeSessions = new Map<string, ActiveSession>();\n  const browserSockets = new Set<WebSocket>();\n  const handlerSockets = new Map<WebSocket, string>();\n  const sessionMessageQueues = new Map<string, SessionMessageQueue>();\n  const undoRedoSessionOwners = new Map<string, WebSocket>();\n\n  let httpServer: ReturnType<typeof createHttpServer> | null = null;\n  let webSocketServer: WebSocketServer | null = null;\n\n  const broadcastHandlerList = () => {\n    const handlerIds = Array.from(registeredHandlers.keys());\n    const message: RelayToBrowserMessage = {\n      type: \"handlers\",\n      handlers: handlerIds,\n    };\n    const serializedMessage = JSON.stringify(message);\n\n    for (const socket of browserSockets) {\n      if (socket.readyState === WebSocket.OPEN) {\n        socket.send(serializedMessage);\n      }\n    }\n  };\n\n  const sendToBrowser = (socket: WebSocket, message: RelayToBrowserMessage) => {\n    if (socket.readyState === WebSocket.OPEN) {\n      socket.send(JSON.stringify(message));\n    }\n  };\n\n  const handleBrowserMessage = async (\n    socket: WebSocket,\n    message: BrowserToRelayMessage,\n  ) => {\n    const { type, agentId, sessionId, context } = message;\n\n    if (type === \"health\") {\n      sendToBrowser(socket, { type: \"health\" });\n      return;\n    }\n\n    const registered = registeredHandlers.get(agentId);\n    if (!registered) {\n      sendToBrowser(socket, {\n        type: \"agent-error\",\n        agentId,\n        sessionId,\n        content: `Agent \"${agentId}\" is not registered`,\n      });\n      return;\n    }\n\n    const effectiveSessionId =\n      sessionId ??\n      `session-${Date.now()}-${Math.random().toString(36).slice(2)}`;\n\n    if (type === \"agent-request\") {\n      if (\n        !context ||\n        typeof context.prompt !== \"string\" ||\n        !Array.isArray(context.content)\n      ) {\n        sendToBrowser(socket, {\n          type: \"agent-error\",\n          agentId,\n          sessionId: effectiveSessionId,\n          content: \"Invalid context: missing or malformed prompt/content\",\n        });\n        return;\n      }\n\n      const abortController = new AbortController();\n      activeSessions.set(effectiveSessionId, {\n        sessionId: effectiveSessionId,\n        agentId,\n        browserSocket: socket,\n        handlerSocket: registered.socket,\n        abortController,\n      });\n\n      runAgentTask(\n        registered.handler,\n        context,\n        effectiveSessionId,\n        socket,\n        abortController.signal,\n      );\n    } else if (type === \"agent-abort\") {\n      const session = activeSessions.get(effectiveSessionId);\n      if (session) {\n        session.abortController.abort();\n        registered.handler.abort?.(effectiveSessionId);\n        activeSessions.delete(effectiveSessionId);\n      }\n    } else if (type === \"agent-undo\") {\n      try {\n        await registered.handler.undo?.();\n        sendToBrowser(socket, {\n          type: \"agent-done\",\n          agentId,\n          sessionId: effectiveSessionId,\n        });\n      } catch (error) {\n        const errorMessage =\n          error instanceof Error ? error.message : \"Unknown error\";\n        sendToBrowser(socket, {\n          type: \"agent-error\",\n          agentId,\n          sessionId: effectiveSessionId,\n          content: `Undo failed: ${errorMessage}`,\n        });\n      }\n    } else if (type === \"agent-redo\") {\n      try {\n        await registered.handler.redo?.();\n        sendToBrowser(socket, {\n          type: \"agent-done\",\n          agentId,\n          sessionId: effectiveSessionId,\n        });\n      } catch (error) {\n        const errorMessage =\n          error instanceof Error ? error.message : \"Unknown error\";\n        sendToBrowser(socket, {\n          type: \"agent-error\",\n          agentId,\n          sessionId: effectiveSessionId,\n          content: `Redo failed: ${errorMessage}`,\n        });\n      }\n    }\n  };\n\n  const runAgentTask = async (\n    handler: AgentHandler,\n    context: AgentContext,\n    sessionId: string,\n    browserSocket: WebSocket,\n    signal: AbortSignal,\n  ) => {\n    const combinedPrompt = `${context.prompt}\\n\\n${context.content.join(\"\\n\\n\")}`;\n\n    try {\n      let didComplete = false;\n\n      for await (const message of handler.run(combinedPrompt, {\n        sessionId,\n        signal,\n      })) {\n        if (signal.aborted) break;\n\n        const getBrowserMessageType = (\n          messageType: string,\n        ): \"agent-status\" | \"agent-error\" | \"agent-done\" => {\n          if (messageType === \"status\") return \"agent-status\";\n          if (messageType === \"error\") return \"agent-error\";\n          return \"agent-done\";\n        };\n\n        sendToBrowser(browserSocket, {\n          type: getBrowserMessageType(message.type),\n          agentId: handler.agentId,\n          sessionId,\n          content: message.content,\n        });\n\n        if (message.type === \"done\" || message.type === \"error\") {\n          didComplete = true;\n          break;\n        }\n      }\n\n      if (!signal.aborted && !didComplete) {\n        sendToBrowser(browserSocket, {\n          type: \"agent-done\",\n          agentId: handler.agentId,\n          sessionId,\n        });\n      }\n    } catch (error) {\n      if (!signal.aborted) {\n        const errorMessage =\n          error instanceof Error ? error.message : \"Unknown error\";\n        sendToBrowser(browserSocket, {\n          type: \"agent-error\",\n          agentId: handler.agentId,\n          sessionId,\n          content: errorMessage,\n        });\n      }\n    } finally {\n      activeSessions.delete(sessionId);\n    }\n  };\n\n  const createRemoteHandler = (\n    agentId: string,\n    socket: WebSocket,\n  ): AgentHandler => {\n    return {\n      agentId,\n      run: async function* (userPrompt, options) {\n        const sessionId =\n          options?.sessionId ??\n          `session-${Date.now()}-${Math.random().toString(36).slice(2)}`;\n        const signal = options?.signal;\n\n        const messageQueue = createSessionMessageQueue();\n        sessionMessageQueues.set(sessionId, messageQueue);\n\n        const cleanupQueue = () => {\n          messageQueue.close();\n          sessionMessageQueues.delete(sessionId);\n        };\n\n        if (signal) {\n          signal.addEventListener(\"abort\", cleanupQueue, { once: true });\n\n          if (signal.aborted) {\n            cleanupQueue();\n            return;\n          }\n        }\n\n        const invokeMessage: RelayToHandlerMessage = {\n          type: \"invoke-handler\",\n          method: \"run\",\n          sessionId,\n          payload: { prompt: userPrompt },\n        };\n\n        if (socket.readyState !== WebSocket.OPEN) {\n          yield { type: \"error\", content: \"Handler disconnected\" };\n          cleanupQueue();\n          return;\n        }\n\n        socket.send(JSON.stringify(invokeMessage));\n\n        try {\n          for await (const message of messageQueue) {\n            if (signal?.aborted) break;\n            yield message;\n            if (message.type === \"done\" || message.type === \"error\") {\n              break;\n            }\n          }\n        } finally {\n          if (signal) {\n            signal.removeEventListener(\"abort\", cleanupQueue);\n          }\n          cleanupQueue();\n        }\n      },\n      abort: (sessionId) => {\n        if (socket.readyState === WebSocket.OPEN) {\n          const abortMessage: RelayToHandlerMessage = {\n            type: \"invoke-handler\",\n            method: \"abort\",\n            sessionId,\n          };\n          socket.send(JSON.stringify(abortMessage));\n        }\n      },\n      undo: async () => {\n        if (socket.readyState !== WebSocket.OPEN) {\n          throw new Error(\"Handler disconnected\");\n        }\n\n        const sessionId = `undo-${agentId}-${Date.now()}-${Math.random().toString(36).slice(2)}`;\n        const messageQueue = createSessionMessageQueue();\n        sessionMessageQueues.set(sessionId, messageQueue);\n        undoRedoSessionOwners.set(sessionId, socket);\n\n        const undoMessage: RelayToHandlerMessage = {\n          type: \"invoke-handler\",\n          method: \"undo\",\n          sessionId,\n        };\n        socket.send(JSON.stringify(undoMessage));\n\n        try {\n          for await (const message of messageQueue) {\n            if (message.type === \"error\") {\n              throw new Error(message.content);\n            }\n            if (message.type === \"done\") {\n              return;\n            }\n          }\n        } finally {\n          messageQueue.close();\n          sessionMessageQueues.delete(sessionId);\n          undoRedoSessionOwners.delete(sessionId);\n        }\n      },\n      redo: async () => {\n        if (socket.readyState !== WebSocket.OPEN) {\n          throw new Error(\"Handler disconnected\");\n        }\n\n        const sessionId = `redo-${agentId}-${Date.now()}-${Math.random().toString(36).slice(2)}`;\n        const messageQueue = createSessionMessageQueue();\n        sessionMessageQueues.set(sessionId, messageQueue);\n        undoRedoSessionOwners.set(sessionId, socket);\n\n        const redoMessage: RelayToHandlerMessage = {\n          type: \"invoke-handler\",\n          method: \"redo\",\n          sessionId,\n        };\n        socket.send(JSON.stringify(redoMessage));\n\n        try {\n          for await (const message of messageQueue) {\n            if (message.type === \"error\") {\n              throw new Error(message.content);\n            }\n            if (message.type === \"done\") {\n              return;\n            }\n          }\n        } finally {\n          messageQueue.close();\n          sessionMessageQueues.delete(sessionId);\n          undoRedoSessionOwners.delete(sessionId);\n        }\n      },\n    };\n  };\n\n  const cleanupSessionsByHandlerSocket = (socket: WebSocket) => {\n    for (const [sessionId, session] of activeSessions) {\n      if (session.handlerSocket === socket) {\n        const queue = sessionMessageQueues.get(sessionId);\n        if (queue) {\n          queue.push({\n            type: \"error\",\n            content: \"Handler disconnected\",\n          });\n          queue.close();\n          sessionMessageQueues.delete(sessionId);\n        }\n        session.abortController.abort();\n        activeSessions.delete(sessionId);\n      }\n    }\n\n    for (const [sessionId, ownerSocket] of undoRedoSessionOwners) {\n      if (ownerSocket === socket) {\n        const queue = sessionMessageQueues.get(sessionId);\n        if (queue) {\n          queue.push({\n            type: \"error\",\n            content: \"Handler disconnected\",\n          });\n          queue.close();\n          sessionMessageQueues.delete(sessionId);\n        }\n        undoRedoSessionOwners.delete(sessionId);\n      }\n    }\n  };\n\n  const handleHandlerMessage = (socket: WebSocket, message: HandlerMessage) => {\n    if (message.type === \"register-handler\") {\n      const remoteHandler = createRemoteHandler(message.agentId, socket);\n      registeredHandlers.set(message.agentId, {\n        agentId: message.agentId,\n        handler: remoteHandler,\n        socket,\n      });\n      handlerSockets.set(socket, message.agentId);\n      broadcastHandlerList();\n    } else if (message.type === \"unregister-handler\") {\n      const agentId = handlerSockets.get(socket);\n      if (agentId) {\n        const registeredHandler = registeredHandlers.get(agentId);\n        const isCurrentHandler = registeredHandler?.socket === socket;\n\n        cleanupSessionsByHandlerSocket(socket);\n\n        if (isCurrentHandler) {\n          registeredHandlers.delete(agentId);\n          broadcastHandlerList();\n        }\n        handlerSockets.delete(socket);\n      }\n    } else if (\n      message.type === \"agent-status\" ||\n      message.type === \"agent-done\" ||\n      message.type === \"agent-error\"\n    ) {\n      const messageQueue = sessionMessageQueues.get(message.sessionId);\n      if (messageQueue) {\n        const getQueueMessageType = (\n          handlerMessageType: string,\n        ): \"status\" | \"done\" | \"error\" => {\n          if (handlerMessageType === \"agent-status\") return \"status\";\n          if (handlerMessageType === \"agent-done\") return \"done\";\n          return \"error\";\n        };\n\n        messageQueue.push({\n          type: getQueueMessageType(message.type),\n          content: message.content ?? \"\",\n        });\n\n        if (message.type === \"agent-done\" || message.type === \"agent-error\") {\n          messageQueue.close();\n        }\n      }\n    }\n  };\n\n  const start = async (): Promise<void> => {\n    return new Promise((resolve, reject) => {\n      httpServer = createHttpServer((req, res) => {\n        const requestUrl = new URL(req.url ?? \"\", `http://localhost:${port}`);\n\n        if (requestUrl.pathname === \"/health\") {\n          if (token) {\n            const clientToken = requestUrl.searchParams.get(RELAY_TOKEN_PARAM);\n            if (clientToken !== token) {\n              res.writeHead(401, { \"Content-Type\": \"application/json\" });\n              res.end(JSON.stringify({ error: \"Unauthorized\" }));\n              return;\n            }\n          }\n          res.writeHead(200, { \"Content-Type\": \"application/json\" });\n          res.end(\n            JSON.stringify({\n              status: \"ok\",\n              handlers: getRegisteredHandlerIds(),\n            }),\n          );\n          return;\n        }\n        res.writeHead(404);\n        res.end();\n      });\n\n      httpServer.on(\"error\", (error) => {\n        reject(error);\n      });\n\n      webSocketServer = new WebSocketServer({ server: httpServer });\n\n      webSocketServer.on(\"error\", (error) => {\n        reject(error);\n      });\n\n      webSocketServer.on(\"connection\", (socket, request) => {\n        if (token) {\n          const connectionUrl = new URL(\n            request.url ?? \"\",\n            `http://localhost:${port}`,\n          );\n          const clientToken = connectionUrl.searchParams.get(RELAY_TOKEN_PARAM);\n          if (clientToken !== token) {\n            socket.close(4001, \"Unauthorized\");\n            return;\n          }\n        }\n\n        const isHandlerConnection =\n          request.headers[\"x-relay-handler\"] === \"true\";\n\n        if (isHandlerConnection) {\n          socket.on(\"message\", (data) => {\n            try {\n              const message = JSON.parse(data.toString()) as HandlerMessage;\n              handleHandlerMessage(socket, message);\n            } catch {}\n          });\n\n          const cleanupHandlerSocket = () => {\n            const agentId = handlerSockets.get(socket);\n            if (agentId) {\n              const registeredHandler = registeredHandlers.get(agentId);\n              const isCurrentHandler = registeredHandler?.socket === socket;\n\n              cleanupSessionsByHandlerSocket(socket);\n\n              if (isCurrentHandler) {\n                registeredHandlers.delete(agentId);\n                broadcastHandlerList();\n              }\n\n              handlerSockets.delete(socket);\n            }\n          };\n\n          socket.on(\"close\", cleanupHandlerSocket);\n          socket.on(\"error\", () => {\n            cleanupHandlerSocket();\n          });\n        } else {\n          browserSockets.add(socket);\n\n          sendToBrowser(socket, {\n            type: \"handlers\",\n            handlers: getRegisteredHandlerIds(),\n          });\n\n          socket.on(\"message\", (data) => {\n            try {\n              const message = JSON.parse(\n                data.toString(),\n              ) as BrowserToRelayMessage;\n              handleBrowserMessage(socket, message);\n            } catch {}\n          });\n\n          socket.on(\"close\", () => {\n            browserSockets.delete(socket);\n\n            for (const [sessionId, session] of activeSessions) {\n              if (session.browserSocket === socket) {\n                session.abortController.abort();\n                const handler = registeredHandlers.get(session.agentId);\n                handler?.handler.abort?.(sessionId);\n                activeSessions.delete(sessionId);\n              }\n            }\n          });\n        }\n      });\n\n      httpServer.listen(port, () => {\n        resolve();\n      });\n    });\n  };\n\n  const stop = async (): Promise<void> => {\n    for (const session of activeSessions.values()) {\n      session.abortController.abort();\n    }\n    activeSessions.clear();\n\n    for (const queue of sessionMessageQueues.values()) {\n      queue.close();\n    }\n    sessionMessageQueues.clear();\n    undoRedoSessionOwners.clear();\n\n    for (const socket of browserSockets) {\n      socket.close();\n    }\n    browserSockets.clear();\n\n    for (const socket of handlerSockets.keys()) {\n      socket.close();\n    }\n    handlerSockets.clear();\n\n    webSocketServer?.close();\n    httpServer?.close();\n  };\n\n  const registerHandler = (handler: AgentHandler) => {\n    registeredHandlers.set(handler.agentId, {\n      agentId: handler.agentId,\n      handler,\n    });\n    broadcastHandlerList();\n  };\n\n  const unregisterHandler = (agentId: string) => {\n    registeredHandlers.delete(agentId);\n    broadcastHandlerList();\n  };\n\n  const getRegisteredHandlerIds = (): string[] => {\n    return Array.from(registeredHandlers.keys());\n  };\n\n  return {\n    start,\n    stop,\n    registerHandler,\n    unregisterHandler,\n    getRegisteredHandlerIds,\n  };\n};\n"
  },
  {
    "path": "packages/relay/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"esModuleInterop\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"declaration\": true,\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/relay/tsup.config.ts",
    "content": "import fs from \"node:fs\";\nimport module from \"node:module\";\nimport { defineConfig } from \"tsup\";\n\nconst packageJson = JSON.parse(fs.readFileSync(\"package.json\", \"utf8\")) as {\n  version: string;\n};\n\nexport default defineConfig([\n  {\n    entry: {\n      index: \"./src/index.ts\",\n      server: \"./src/server.ts\",\n      connection: \"./src/connection.ts\",\n      protocol: \"./src/protocol.ts\",\n    },\n    format: [\"cjs\", \"esm\"],\n    dts: true,\n    clean: false,\n    splitting: false,\n    sourcemap: false,\n    target: \"node18\",\n    platform: \"node\",\n    treeshake: true,\n    noExternal: [/.*/],\n    external: [\n      ...module.builtinModules,\n      ...module.builtinModules.map((name) => `node:${name}`),\n    ],\n    env: {\n      VERSION: process.env.VERSION ?? packageJson.version,\n    },\n  },\n  {\n    entry: {\n      client: \"./src/client.ts\",\n      protocol: \"./src/protocol.ts\",\n    },\n    format: [\"cjs\", \"esm\"],\n    dts: true,\n    clean: false,\n    splitting: false,\n    sourcemap: false,\n    target: \"esnext\",\n    platform: \"browser\",\n    treeshake: true,\n  },\n]);\n"
  },
  {
    "path": "packages/shadcn-registry/package.json",
    "content": "{\n  \"name\": \"@react-grab/shadcn-registry\",\n  \"version\": \"0.0.1\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"node scripts/build.js\"\n  },\n  \"dependencies\": {\n    \"react-grab\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "packages/shadcn-registry/r/react-grab.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"react-grab\",\n  \"type\": \"registry:component\",\n  \"title\": \"React Grab\",\n  \"description\": \"Loads React Grab as early as possible — select context for coding agents directly from your website.\",\n  \"dependencies\": [\n    \"react-grab\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/react-grab.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { useEffect } from \\\"react\\\";\\n\\nconst SCRIPT_ID = \\\"react-grab-script\\\";\\nconst SCRIPT_SRC = \\\"https://unpkg.com/react-grab/dist/index.global.js\\\";\\n\\nexport const ReactGrab = () => {\\n  useEffect(() => {\\n    if (process.env.NODE_ENV !== \\\"development\\\") return;\\n    if (document.getElementById(SCRIPT_ID)) return;\\n    const script = document.createElement(\\\"script\\\");\\n    script.id = SCRIPT_ID;\\n    script.src = SCRIPT_SRC;\\n    script.crossOrigin = \\\"anonymous\\\";\\n    document.head.appendChild(script);\\n  }, []);\\n  return null;\\n};\\n\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/react-grab.tsx\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/shadcn-registry/registry/react-grab.tsx",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\n\nconst SCRIPT_ID = \"react-grab-script\";\nconst SCRIPT_SRC = \"https://unpkg.com/react-grab/dist/index.global.js\";\n\nexport const ReactGrab = () => {\n  useEffect(() => {\n    if (process.env.NODE_ENV !== \"development\") return;\n    if (document.getElementById(SCRIPT_ID)) return;\n    const script = document.createElement(\"script\");\n    script.id = SCRIPT_ID;\n    script.src = SCRIPT_SRC;\n    script.crossOrigin = \"anonymous\";\n    document.head.appendChild(script);\n  }, []);\n  return null;\n};\n"
  },
  {
    "path": "packages/shadcn-registry/registry.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry.json\",\n  \"name\": \"react-grab\",\n  \"homepage\": \"https://react-grab.com\",\n  \"items\": [\n    {\n      \"name\": \"react-grab\",\n      \"type\": \"registry:component\",\n      \"title\": \"React Grab\",\n      \"description\": \"Loads React Grab as early as possible — select context for coding agents directly from your website.\",\n      \"dependencies\": [\"react-grab\"],\n      \"files\": [\n        {\n          \"path\": \"registry/react-grab.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/react-grab.tsx\"\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/shadcn-registry/scripts/build.js",
    "content": "import { readFileSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { resolve, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst CURRENT_DIRECTORY = dirname(fileURLToPath(import.meta.url));\nconst ROOT_DIRECTORY = resolve(CURRENT_DIRECTORY, \"..\");\nconst OUTPUT_DIRECTORY = resolve(ROOT_DIRECTORY, \"r\");\n\nconst registry = JSON.parse(\n  readFileSync(resolve(ROOT_DIRECTORY, \"registry.json\"), \"utf-8\"),\n);\n\nmkdirSync(OUTPUT_DIRECTORY, { recursive: true });\n\nfor (const item of registry.items) {\n  const registryItem = {\n    $schema: \"https://ui.shadcn.com/schema/registry-item.json\",\n    name: item.name,\n    type: item.type,\n    title: item.title,\n    description: item.description,\n    dependencies: item.dependencies,\n    files: item.files.map((file) => ({\n      path: file.target ?? file.path,\n      content: readFileSync(resolve(ROOT_DIRECTORY, file.path), \"utf-8\"),\n      type: file.type,\n      target: file.target,\n    })),\n  };\n\n  const outputPath = resolve(OUTPUT_DIRECTORY, `${item.name}.json`);\n  writeFileSync(outputPath, JSON.stringify(registryItem, null, 2) + \"\\n\");\n  console.log(`Built: ${outputPath}`);\n}\n\nconsole.log(`Registry build complete: ${registry.items.length} item(s)`);\n"
  },
  {
    "path": "packages/utils/CHANGELOG.md",
    "content": "# @react-grab/utils\n\n## 0.1.28\n\n### Patch Changes\n\n- fix\n\n## 0.1.27\n\n### Patch Changes\n\n- fix: install instructions\n\n## 0.1.26\n\n### Patch Changes\n\n- fix: minor tweaks\n\n## 0.1.25\n\n### Patch Changes\n\n- fix: primtiives\n\n## 0.1.24\n\n### Patch Changes\n\n- primitives\n\n## 0.1.23\n\n### Patch Changes\n\n- fix: npx command\n\n## 0.1.22\n\n### Patch Changes\n\n- fix: freezing\n\n## 0.1.21\n\n### Patch Changes\n\n- fix: up and down selection\n\n## 0.1.20\n\n### Patch Changes\n\n- fix: selection performacne\n\n## 0.1.19\n\n### Patch Changes\n\n- fix: gsap\n\n## 0.1.18\n\n### Patch Changes\n\n- fix: minor issues\n\n## 0.1.17\n\n### Patch Changes\n\n- fix: mcp\n\n## 0.1.16\n\n### Patch Changes\n\n- fix: environment detection\n\n## 0.1.15\n\n### Patch Changes\n\n- fix: animations and ux\n\n## 0.1.14\n\n### Patch Changes\n\n- fix: improve recent UX\n\n## 0.1.13\n\n### Patch Changes\n\n- fix MCP client injection\n\n## 0.1.12\n\n### Patch Changes\n\n- feat: MCP\n\n## 0.1.11\n\n### Patch Changes\n\n- fix: claude code provider\n\n## 0.1.10\n\n### Patch Changes\n\n- feat: cdn in cli\n\n## 0.1.9\n\n### Patch Changes\n\n- fix: startServer not exported in providers\n\n## 0.1.8\n\n### Patch Changes\n\n- fix: providers not detaching\n\n## 0.1.7\n\n### Patch Changes\n\n- fix: react freezing safety\n\n## 0.1.6\n\n### Patch Changes\n\n- fix: compat with React Scan\n\n## 0.1.5\n\n### Patch Changes\n\n- fix: fullscreen inset the root\n\n## 0.1.4\n\n### Patch Changes\n\n- fix: improve cli edge cases\n\n## 0.1.3\n\n### Patch Changes\n\n- fix: @react-grab/utils not publishing\n\n## 0.1.2\n\n### Patch Changes\n\n- fix: packages not being published\n\n## 0.1.1\n\n### Patch Changes\n\n- fix: clicking element after keyboard navigation\n\n## 0.1.0\n\n### Minor Changes\n\n- 81adb50: feat: browser\n\n### Patch Changes\n\n- 81adb50: fix: shell script\n- fb2b037: fix: cli\n- a3d5a94: fix: cli global install\n- 81adb50: feat: react support\n- 81adb50: fix: a11y\n- a5e7a6a: fix: optimize loading speed of cli\n- 90af3f6: fix: CLI hanging\n- 81adb50: fix: shell script\n- 78efee2: fix: cli\n- 074e593: fix: cli\n- 5cd3709: fix: decouple browser out from react-grab\n- 54c4867: ui improvements\n\n## 0.1.0-beta.13\n\n### Patch Changes\n\n- ui improvements\n\n## 0.1.0-beta.12\n\n### Patch Changes\n\n- fix: decouple browser out from react-grab\n\n## 0.1.0-beta.11\n\n### Patch Changes\n\n- fix: cli global install\n\n## 0.1.0-beta.10\n\n### Patch Changes\n\n- fix: cli\n\n## 0.1.0-beta.9\n\n### Patch Changes\n\n- fix: cli\n\n## 0.1.0-beta.8\n\n### Patch Changes\n\n- fix: cli\n\n## 0.1.0-beta.7\n\n### Patch Changes\n\n- fix: CLI hanging\n\n## 0.1.0-beta.6\n\n### Patch Changes\n\n- fix: optimize loading speed of cli\n\n## 0.1.0-beta.5\n\n### Patch Changes\n\n- fix: a11y\n\n## 0.1.0-beta.4\n\n### Patch Changes\n\n- feat: react support\n\n## 0.1.0-beta.2\n\n### Patch Changes\n\n- fix: shell script\n\n## 0.1.0-beta.1\n\n### Patch Changes\n\n- fix: shell script\n\n## 0.1.0-beta.0\n\n### Minor Changes\n\n- feat: browser\n\n## 0.0.98\n\n### Patch Changes\n\n- feat: new state architecture and context menu\n\n## 0.0.97\n\n### Patch Changes\n\n- fix: sourcemap error\n\n## 0.0.96\n\n### Patch Changes\n\n- fix: fiber access timeout handling\n\n## 0.0.95\n\n### Patch Changes\n\n- fix: selecting buttons with disabled states\n\n## 0.0.94\n\n### Patch Changes\n\n- fix: browser crashing on selection bug\n\n## 0.0.93\n\n### Patch Changes\n\n- fix: copying not working\n\n## 0.0.92\n\n### Patch Changes\n\n- refactor: use state machines instead of signals\n\n## 0.0.91\n\n### Patch Changes\n\n- feat: dock\n\n## 0.0.90\n\n### Patch Changes\n\n- fix: check visual edit endpoint\n\n## 0.0.89\n\n### Patch Changes\n\n- fix: many bugfixes\n\n## 0.0.88\n\n### Patch Changes\n\n- fix: deprecation errors\n\n## 0.0.87\n\n### Patch Changes\n\n- feat: visual edits\n\n## 0.0.86\n\n### Patch Changes\n\n- fix: editing\n\n## 0.0.85\n\n### Patch Changes\n\n- fix: check versions on each provider\n\n## 0.0.84\n\n### Patch Changes\n\n- fix: migrate from cross-spawn to execa to fix deprecation issue\n\n## 0.0.83\n\n### Patch Changes\n\n- feat: timings during agent processing\n"
  },
  {
    "path": "packages/utils/package.json",
    "content": "{\n  \"name\": \"@react-grab/utils\",\n  \"version\": \"0.1.28\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \"./server\": {\n      \"types\": \"./dist/server.d.ts\",\n      \"import\": \"./dist/server.js\",\n      \"require\": \"./dist/server.cjs\"\n    }\n  },\n  \"scripts\": {\n    \"dev\": \"tsup --watch\",\n    \"build\": \"rm -rf dist && NODE_ENV=production tsup\"\n  },\n  \"devDependencies\": {\n    \"tsup\": \"^8.4.0\"\n  }\n}\n"
  },
  {
    "path": "packages/utils/src/server.ts",
    "content": "export const sleep = (ms: number): Promise<void> =>\n  new Promise((resolve) => setTimeout(resolve, ms));\n\nconst COMMAND_INSTALL_MAP: Record<string, string> = {\n  \"cursor-agent\":\n    \"Install Cursor (https://cursor.com) and ensure 'cursor-agent' is in your PATH.\",\n  gemini:\n    \"Install the Gemini CLI: npm install -g @anthropic-ai/gemini-cli\\nOr see: https://github.com/google-gemini/gemini-cli\",\n  claude:\n    \"Install Claude Code: npm install -g @anthropic-ai/claude-code\\nOr see: https://github.com/anthropics/claude-code\",\n};\n\ninterface SpawnError extends Error {\n  code?: string;\n  errno?: number;\n  syscall?: string;\n  path?: string;\n}\n\nexport const formatSpawnError = (error: Error, commandName: string): string => {\n  const spawnError = error as SpawnError;\n  const isNotFound =\n    spawnError.code === \"ENOENT\" ||\n    (spawnError.message && spawnError.message.includes(\"ENOENT\"));\n\n  if (isNotFound) {\n    const installInfo = COMMAND_INSTALL_MAP[commandName];\n    const baseMessage = `Command '${commandName}' not found.`;\n\n    if (installInfo) {\n      return `${baseMessage}\\n\\n${installInfo}`;\n    }\n\n    return `${baseMessage}\\n\\nMake sure '${commandName}' is installed and available in your PATH.`;\n  }\n\n  const isPermissionDenied =\n    spawnError.code === \"EACCES\" ||\n    (spawnError.message && spawnError.message.includes(\"EACCES\"));\n\n  if (isPermissionDenied) {\n    return `Permission denied when trying to run '${commandName}'.\\n\\nCheck that the command is executable: chmod +x $(which ${commandName})`;\n  }\n\n  return error.message;\n};\n"
  },
  {
    "path": "packages/utils/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"esModuleInterop\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"declaration\": true,\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/utils/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig([\n  {\n    entry: {\n      server: \"./src/server.ts\",\n    },\n    format: [\"cjs\", \"esm\"],\n    dts: true,\n    clean: false,\n    splitting: false,\n    sourcemap: false,\n    target: \"node18\",\n    platform: \"node\",\n    treeshake: true,\n  },\n]);\n"
  },
  {
    "path": "packages/web-extension/.gitignore",
    "content": "dist\nnode_modules\n.DS_Store\n"
  },
  {
    "path": "packages/web-extension/package.json",
    "content": "{\n  \"name\": \"@react-grab/web-extension\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"vite build\",\n    \"preview\": \"vite preview\",\n    \"package\": \"./scripts/package.sh\"\n  },\n  \"dependencies\": {\n    \"react-grab\": \"workspace:*\",\n    \"turndown\": \"^7.2.0\"\n  },\n  \"devDependencies\": {\n    \"@types/chrome\": \"^0.0.278\",\n    \"@types/turndown\": \"^5.0.5\",\n    \"@vitejs/plugin-react\": \"^4.3.4\",\n    \"typescript\": \"^5.7.3\",\n    \"vite\": \"^6.0.2\",\n    \"vite-plugin-web-extension\": \"^4.1.6\"\n  }\n}\n"
  },
  {
    "path": "packages/web-extension/scripts/package.sh",
    "content": "#!/bin/bash\n\nset -e\n\necho \"Building React Grab Extension...\"\n\ncd \"$(dirname \"$0\")/..\"\n\necho \"Installing dependencies...\"\npnpm install\n\necho \"Building extension...\"\npnpm run build\n\necho \"Creating ZIP package...\"\ncd dist\nzip -r ../react-grab-extension.zip . -x \"*.DS_Store\"\ncd ..\n\necho \"Extension packaged successfully!\"\necho \"Package location: react-grab-extension.zip\"\necho \"\"\necho \"Next steps:\"\necho \"1. Go to Chrome Web Store Developer Dashboard\"\necho \"2. Upload react-grab-extension.zip\"\necho \"3. Fill in the store listing details\"\necho \"4. Submit for review\"\n"
  },
  {
    "path": "packages/web-extension/src/background/service-worker.ts",
    "content": "const STORAGE_KEY = \"react_grab_enabled\";\n\nconst getGlobalEnabled = async (): Promise<boolean> => {\n  const result = await chrome.storage.local.get(STORAGE_KEY);\n  const enabled = result[STORAGE_KEY] ?? true;\n  return enabled;\n};\n\nconst setGlobalEnabled = async (enabled: boolean): Promise<void> => {\n  await chrome.storage.local.set({ [STORAGE_KEY]: enabled });\n};\n\nconst updateActionIcon = async (\n  tabId: number,\n  enabled: boolean,\n): Promise<void> => {\n  const title = enabled ? \"React Grab (Active)\" : \"React Grab (Inactive)\";\n  const badgeText = enabled ? \"\" : \"OFF\";\n  const badgeColor = \"#FF40E0\";\n\n  await chrome.action.setTitle({ tabId, title });\n  await chrome.action.setBadgeText({ tabId, text: badgeText });\n  if (badgeText) {\n    await chrome.action.setBadgeBackgroundColor({ tabId, color: badgeColor });\n  }\n};\n\nchrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {\n  if (message.type === \"GET_STATE\") {\n    getGlobalEnabled().then((enabled) => {\n      sendResponse({ enabled });\n    });\n    return true;\n  }\n  return false;\n});\n\nchrome.action.onClicked.addListener(async (tab) => {\n  if (!tab.id) return;\n\n  const currentEnabled = await getGlobalEnabled();\n  const newEnabled = !currentEnabled;\n  await setGlobalEnabled(newEnabled);\n\n  await updateActionIcon(tab.id, newEnabled);\n\n  try {\n    await chrome.tabs.sendMessage(tab.id, {\n      type: \"REACT_GRAB_TOGGLE\",\n      enabled: newEnabled,\n    });\n  } catch {\n    // HACK: Content script may not be ready yet\n  }\n\n  const allTabs = await chrome.tabs.query({});\n  for (const otherTab of allTabs) {\n    if (otherTab.id && otherTab.id !== tab.id) {\n      await updateActionIcon(otherTab.id, newEnabled);\n      try {\n        await chrome.tabs.sendMessage(otherTab.id, {\n          type: \"REACT_GRAB_TOGGLE\",\n          enabled: newEnabled,\n        });\n      } catch {\n        // Tab may not have content script loaded\n      }\n    }\n  }\n});\n\nchrome.tabs.onUpdated.addListener(async (tabId, changeInfo) => {\n  if (changeInfo.status === \"loading\") {\n    const enabled = await getGlobalEnabled();\n    await updateActionIcon(tabId, enabled);\n  }\n});\n"
  },
  {
    "path": "packages/web-extension/src/constants.ts",
    "content": "export const LOCALHOST_INIT_DELAY_MS = 500;\nexport const STATE_QUERY_TIMEOUT_MS = 500;\n"
  },
  {
    "path": "packages/web-extension/src/content/bridge.ts",
    "content": "// This script runs in ISOLATED world and bridges chrome.runtime messages to MAIN world\n\nchrome.storage.onChanged.addListener((changes) => {\n  if (changes.react_grab_enabled) {\n    const newEnabled = changes.react_grab_enabled.newValue ?? true;\n    window.postMessage(\n      { type: \"__REACT_GRAB_EXTENSION_TOGGLE__\", enabled: newEnabled },\n      \"*\",\n    );\n  }\n\n  if (changes.react_grab_toolbar_state) {\n    const newState = changes.react_grab_toolbar_state.newValue;\n    if (newState) {\n      window.postMessage(\n        { type: \"__REACT_GRAB_TOOLBAR_STATE_CHANGE__\", state: newState },\n        \"*\",\n      );\n    }\n  }\n});\n\nchrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {\n  if (message.type === \"REACT_GRAB_TOGGLE\") {\n    window.postMessage(\n      { type: \"__REACT_GRAB_EXTENSION_TOGGLE__\", enabled: message.enabled },\n      \"*\",\n    );\n    sendResponse({ success: true });\n  }\n\n  if (message.type === \"GET_STATE\") {\n    sendResponse({ enabled: true });\n  }\n\n  return true;\n});\n\nwindow.addEventListener(\"message\", (event) => {\n  if (event.data?.type === \"__REACT_GRAB_QUERY_STATE__\") {\n    chrome.storage.local.get(\n      [\"react_grab_enabled\", \"react_grab_toolbar_state\"],\n      (result) => {\n        const enabled = result.react_grab_enabled ?? true;\n        const toolbarState = result.react_grab_toolbar_state ?? null;\n\n        window.postMessage(\n          {\n            type: \"__REACT_GRAB_STATE_RESPONSE__\",\n            enabled,\n            toolbarState,\n          },\n          \"*\",\n        );\n      },\n    );\n  }\n\n  if (event.data?.type === \"__REACT_GRAB_TOOLBAR_STATE_SAVE__\") {\n    chrome.storage.local.set({ react_grab_toolbar_state: event.data.state });\n  }\n});\n"
  },
  {
    "path": "packages/web-extension/src/content/react-grab.ts",
    "content": "import { init } from \"react-grab/core\";\nimport type { Options, ReactGrabAPI } from \"react-grab\";\nimport TurndownService from \"turndown\";\nimport {\n  LOCALHOST_INIT_DELAY_MS,\n  STATE_QUERY_TIMEOUT_MS,\n} from \"../constants.js\";\n\ndeclare global {\n  interface Window {\n    __REACT_GRAB__?: ReactGrabAPI;\n  }\n}\n\nconst isLocalhost =\n  window.location.hostname === \"localhost\" ||\n  window.location.hostname === \"127.0.0.1\" ||\n  window.location.hostname.endsWith(\".localhost\");\n\nconst turndownService = new TurndownService();\n\ninterface ToolbarState {\n  edge: \"top\" | \"bottom\" | \"left\" | \"right\";\n  ratio: number;\n  collapsed: boolean;\n  enabled: boolean;\n}\n\nlet extensionApi: ReactGrabAPI | null = null;\nlet lastToolbarState: ToolbarState | null = null;\nlet isApplyingExternalState = false;\nlet stateChangeUnsubscribe: (() => void) | null = null;\n\nconst handleToolbarStateFromApi = (toolbarState: ToolbarState | null): void => {\n  if (isApplyingExternalState) return;\n  if (!toolbarState) return;\n  if (\n    lastToolbarState &&\n    lastToolbarState.edge === toolbarState.edge &&\n    lastToolbarState.ratio === toolbarState.ratio &&\n    lastToolbarState.collapsed === toolbarState.collapsed &&\n    lastToolbarState.enabled === toolbarState.enabled\n  ) {\n    return;\n  }\n  lastToolbarState = toolbarState;\n  window.postMessage(\n    { type: \"__REACT_GRAB_TOOLBAR_STATE_SAVE__\", state: toolbarState },\n    \"*\",\n  );\n};\n\nconst subscribeToStateChanges = (api: ReactGrabAPI): void => {\n  if (stateChangeUnsubscribe) {\n    stateChangeUnsubscribe();\n  }\n  stateChangeUnsubscribe = api.onToolbarStateChange((state) => {\n    handleToolbarStateFromApi(state);\n  });\n};\n\nconst createExtensionApi = (): ReactGrabAPI => {\n  const options: Options = { enabled: true };\n\n  if (!isLocalhost) {\n    options.getContent = (elements) => {\n      const combinedHtml = elements\n        .map((element) => element.outerHTML)\n        .join(\"\\n\\n\");\n      return turndownService.turndown(combinedHtml);\n    };\n  }\n\n  const api = init(options);\n  extensionApi = api;\n  window.__REACT_GRAB__ = api;\n  subscribeToStateChanges(api);\n  return api;\n};\n\nconst getActiveApi = (): ReactGrabAPI | null => {\n  return extensionApi ?? window.__REACT_GRAB__ ?? null;\n};\n\nconst initializeReactGrab = (): Promise<ReactGrabAPI | null> => {\n  const activeApi = getActiveApi();\n  if (activeApi) {\n    extensionApi = activeApi;\n    return Promise.resolve(activeApi);\n  }\n\n  if (isLocalhost) {\n    return new Promise((resolve) => {\n      setTimeout(() => {\n        const delayedApi = getActiveApi();\n        if (delayedApi) {\n          extensionApi = delayedApi;\n          resolve(delayedApi);\n          return;\n        }\n        resolve(null);\n      }, LOCALHOST_INIT_DELAY_MS);\n    });\n  }\n\n  const createdApi = createExtensionApi();\n  return Promise.resolve(createdApi);\n};\n\nwindow.addEventListener(\"react-grab:init\", (event) => {\n  if (!(event instanceof CustomEvent)) return;\n  const pageApi = event.detail;\n  if (!pageApi) return;\n  if (extensionApi && extensionApi !== pageApi) {\n    extensionApi.dispose();\n  }\n  extensionApi = pageApi;\n  window.__REACT_GRAB__ = pageApi;\n  subscribeToStateChanges(pageApi);\n});\n\nconst handleToggle = async (enabled: boolean): Promise<void> => {\n  await initializeReactGrab();\n\n  const api = getActiveApi();\n  if (api) {\n    api.setEnabled(enabled);\n  }\n};\n\nconst handleToolbarStateChange = async (state: ToolbarState): Promise<void> => {\n  if (isApplyingExternalState) return;\n\n  await initializeReactGrab();\n  const api = getActiveApi();\n  if (api) {\n    isApplyingExternalState = true;\n    api.setToolbarState(state);\n    isApplyingExternalState = false;\n  }\n};\n\nwindow.addEventListener(\"message\", (event: MessageEvent) => {\n  if (event.data?.type === \"__REACT_GRAB_EXTENSION_TOGGLE__\") {\n    void handleToggle(event.data.enabled);\n  }\n\n  if (event.data?.type === \"__REACT_GRAB_TOOLBAR_STATE_CHANGE__\") {\n    void handleToolbarStateChange(event.data.state);\n  }\n});\n\ninterface InitialState {\n  enabled: boolean;\n  toolbarState: ToolbarState | null;\n}\n\nconst queryInitialState = (): Promise<InitialState> => {\n  return new Promise((resolve) => {\n    const timeout = setTimeout(() => {\n      resolve({ enabled: true, toolbarState: null });\n    }, STATE_QUERY_TIMEOUT_MS);\n\n    const handler = (event: MessageEvent) => {\n      if (event.data?.type === \"__REACT_GRAB_STATE_RESPONSE__\") {\n        clearTimeout(timeout);\n        window.removeEventListener(\"message\", handler);\n        resolve({\n          enabled: event.data.enabled ?? true,\n          toolbarState: event.data.toolbarState ?? null,\n        });\n      }\n    };\n\n    window.addEventListener(\"message\", handler);\n    window.postMessage({ type: \"__REACT_GRAB_QUERY_STATE__\" }, \"*\");\n  });\n};\n\nconst startup = async (): Promise<void> => {\n  const initialState = await queryInitialState();\n  const api = await initializeReactGrab();\n\n  if (api) {\n    if (initialState.toolbarState) {\n      isApplyingExternalState = true;\n      api.setToolbarState(initialState.toolbarState);\n      isApplyingExternalState = false;\n    } else if (!initialState.enabled) {\n      api.setEnabled(false);\n    }\n  }\n};\n\nif (document.readyState === \"loading\") {\n  document.addEventListener(\"DOMContentLoaded\", () => {\n    void startup();\n  });\n} else {\n  void startup();\n}\n"
  },
  {
    "path": "packages/web-extension/src/manifest.json",
    "content": "{\n  \"manifest_version\": 3,\n  \"name\": \"React Grab\",\n  \"version\": \"0.0.8\",\n  \"description\": \"Select context for coding agents directly from your website. Makes tools like Cursor, Claude Code, Copilot run up to 3× faster\",\n  \"author\": \"Aiden Bai\",\n  \"homepage_url\": \"https://react-grab.com\",\n  \"icons\": {\n    \"16\": \"assets/icons/icon-16.png\",\n    \"32\": \"assets/icons/icon-32.png\",\n    \"48\": \"assets/icons/icon-48.png\",\n    \"128\": \"assets/icons/icon-128.png\"\n  },\n  \"action\": {\n    \"default_title\": \"React Grab (Active)\",\n    \"default_icon\": {\n      \"16\": \"assets/icons/icon-16.png\",\n      \"32\": \"assets/icons/icon-32.png\",\n      \"48\": \"assets/icons/icon-48.png\",\n      \"128\": \"assets/icons/icon-128.png\"\n    }\n  },\n  \"background\": {\n    \"service_worker\": \"src/background/service-worker.ts\",\n    \"type\": \"module\"\n  },\n  \"content_scripts\": [\n    {\n      \"matches\": [\"<all_urls>\"],\n      \"js\": [\"src/content/bridge.ts\"],\n      \"run_at\": \"document_start\"\n    },\n    {\n      \"matches\": [\"<all_urls>\"],\n      \"js\": [\"src/content/react-grab.ts\"],\n      \"run_at\": \"document_start\",\n      \"world\": \"MAIN\"\n    }\n  ],\n  \"host_permissions\": [\"<all_urls>\"],\n  \"permissions\": [\"storage\"]\n}\n"
  },
  {
    "path": "packages/web-extension/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"types\": [\"chrome\", \"webextension-polyfill\"],\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/web-extension/vite.config.ts",
    "content": "import react from \"@vitejs/plugin-react\";\nimport { defineConfig } from \"vite\";\nimport webExtension from \"vite-plugin-web-extension\";\n\nexport default defineConfig({\n  plugins: [\n    react(),\n    webExtension({\n      manifest: \"./src/manifest.json\",\n      watchFilePaths: [\"src/**/*\"],\n      browser: \"chrome\",\n    }),\n  ],\n  resolve: {\n    alias: {\n      \"@\": \"/src\",\n    },\n  },\n  optimizeDeps: {\n    include: [\"turndown\"],\n  },\n  build: {\n    commonjsOptions: {\n      include: [/turndown/, /node_modules/],\n    },\n  },\n  publicDir: \"public\",\n});\n"
  },
  {
    "path": "packages/website/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.*\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/versions\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# env files (can opt-in for committing if needed)\n.env*\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n.env*.local\n"
  },
  {
    "path": "packages/website/.oxlintrc.json",
    "content": "{\n  \"$schema\": \"./node_modules/oxlint/configuration_schema.json\",\n  \"plugins\": [\"nextjs\", \"typescript\"],\n  \"categories\": {\n    \"correctness\": \"off\"\n  },\n  \"env\": {\n    \"builtin\": true\n  },\n  \"ignorePatterns\": [\".next/**\", \"out/**\", \"build/**\", \"next-env.d.ts\"],\n  \"rules\": {\n    \"@next/next/google-font-display\": \"warn\",\n    \"@next/next/google-font-preconnect\": \"warn\",\n    \"@next/next/next-script-for-ga\": \"warn\",\n    \"@next/next/no-async-client-component\": \"warn\",\n    \"@next/next/no-before-interactive-script-outside-document\": \"warn\",\n    \"@next/next/no-css-tags\": \"warn\",\n    \"@next/next/no-head-element\": \"warn\",\n    \"@next/next/no-html-link-for-pages\": \"error\",\n    \"@next/next/no-img-element\": \"warn\",\n    \"@next/next/no-page-custom-font\": \"warn\",\n    \"@next/next/no-styled-jsx-in-document\": \"warn\",\n    \"@next/next/no-sync-scripts\": \"error\",\n    \"@next/next/no-title-in-document-head\": \"warn\",\n    \"@next/next/no-typos\": \"warn\",\n    \"@next/next/no-unwanted-polyfillio\": \"warn\",\n    \"@next/next/inline-script-id\": \"error\",\n    \"@next/next/no-assign-module-variable\": \"error\",\n    \"@next/next/no-document-import-in-page\": \"error\",\n    \"@next/next/no-duplicate-head\": \"error\",\n    \"@next/next/no-head-import-in-document\": \"error\",\n    \"@next/next/no-script-component-in-head\": \"error\",\n    \"@typescript-eslint/ban-ts-comment\": \"error\",\n    \"no-array-constructor\": \"error\",\n    \"@typescript-eslint/no-duplicate-enum-values\": \"error\",\n    \"@typescript-eslint/no-empty-object-type\": \"error\",\n    \"@typescript-eslint/no-explicit-any\": \"error\",\n    \"@typescript-eslint/no-extra-non-null-assertion\": \"error\",\n    \"@typescript-eslint/no-misused-new\": \"error\",\n    \"@typescript-eslint/no-namespace\": \"error\",\n    \"@typescript-eslint/no-non-null-asserted-optional-chain\": \"error\",\n    \"@typescript-eslint/no-require-imports\": \"error\",\n    \"@typescript-eslint/no-this-alias\": \"error\",\n    \"@typescript-eslint/no-unnecessary-type-constraint\": \"error\",\n    \"@typescript-eslint/no-unsafe-declaration-merging\": \"error\",\n    \"@typescript-eslint/no-unsafe-function-type\": \"error\",\n    \"no-unused-expressions\": \"warn\",\n    \"no-unused-vars\": \"warn\",\n    \"@typescript-eslint/no-wrapper-object-types\": \"error\",\n    \"@typescript-eslint/prefer-as-const\": \"error\",\n    \"@typescript-eslint/prefer-namespace-keyword\": \"error\",\n    \"@typescript-eslint/triple-slash-reference\": \"error\"\n  },\n  \"overrides\": [\n    {\n      \"files\": [\"**/*.{js,jsx,mjs,ts,tsx,mts,cts}\"],\n      \"rules\": {\n        \"react/display-name\": \"error\",\n        \"react/jsx-key\": \"error\",\n        \"react/jsx-no-comment-textnodes\": \"error\",\n        \"react/jsx-no-duplicate-props\": \"error\",\n        \"react/jsx-no-target-blank\": \"off\",\n        \"react/jsx-no-undef\": \"error\",\n        \"react/no-children-prop\": \"error\",\n        \"react/no-danger-with-children\": \"error\",\n        \"react/no-direct-mutation-state\": \"error\",\n        \"react/no-find-dom-node\": \"error\",\n        \"react/no-is-mounted\": \"error\",\n        \"react/no-render-return-value\": \"error\",\n        \"react/no-string-refs\": \"error\",\n        \"react/no-unescaped-entities\": \"error\",\n        \"react/no-unknown-property\": \"off\",\n        \"react/no-unsafe\": \"off\",\n        \"react/react-in-jsx-scope\": \"off\",\n        \"react-hooks/rules-of-hooks\": \"error\",\n        \"react-hooks/exhaustive-deps\": \"warn\",\n        \"@next/next/no-html-link-for-pages\": \"warn\",\n        \"@next/next/no-sync-scripts\": \"warn\",\n        \"import/no-anonymous-default-export\": \"warn\",\n        \"jsx-a11y/alt-text\": [\n          \"warn\",\n          {\n            \"elements\": [\"img\"],\n            \"img\": [\"Image\"]\n          }\n        ],\n        \"jsx-a11y/aria-props\": \"warn\",\n        \"jsx-a11y/aria-proptypes\": \"warn\",\n        \"jsx-a11y/aria-unsupported-elements\": \"warn\",\n        \"jsx-a11y/role-has-required-aria-props\": \"warn\",\n        \"jsx-a11y/role-supports-aria-props\": \"warn\"\n      },\n      \"globals\": {\n        \"AudioWorkletGlobalScope\": \"readonly\",\n        \"AudioWorkletProcessor\": \"readonly\",\n        \"currentFrame\": \"readonly\",\n        \"currentTime\": \"readonly\",\n        \"registerProcessor\": \"readonly\",\n        \"sampleRate\": \"readonly\",\n        \"WorkletGlobalScope\": \"readonly\"\n      },\n      \"plugins\": [\"react\", \"import\", \"jsx-a11y\"],\n      \"env\": {\n        \"browser\": true,\n        \"node\": true\n      }\n    },\n    {\n      \"files\": [\"**/*.ts\", \"**/*.tsx\", \"**/*.mts\", \"**/*.cts\"],\n      \"rules\": {\n        \"constructor-super\": \"off\",\n        \"no-class-assign\": \"off\",\n        \"no-const-assign\": \"off\",\n        \"no-dupe-class-members\": \"off\",\n        \"no-dupe-keys\": \"off\",\n        \"no-func-assign\": \"off\",\n        \"no-import-assign\": \"off\",\n        \"no-new-native-nonconstructor\": \"off\",\n        \"no-obj-calls\": \"off\",\n        \"no-redeclare\": \"off\",\n        \"no-setter-return\": \"off\",\n        \"no-this-before-super\": \"off\",\n        \"no-unsafe-negation\": \"off\",\n        \"no-var\": \"error\",\n        \"no-with\": \"off\",\n        \"prefer-rest-params\": \"error\",\n        \"prefer-spread\": \"error\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/website/AGENTS.md",
    "content": "Concise rules for building accessible, fast, delightful UIs Use MUST/SHOULD/NEVER to guide decisions\n\n## Interactions\n\n- Keyboard\n  - MUST: Full keyboard support per [WAI-ARIA APG](https://www.w3.org/WAI/ARIA/apg/patterns/)\n  - MUST: Visible focus rings (`:focus-visible`; group with `:focus-within`)\n  - MUST: Manage focus (trap, move, and return) per APG patterns\n- Targets & input\n  - MUST: Hit target ≥24px (mobile ≥44px) If visual <24px, expand hit area\n  - MUST: Mobile `<input>` font-size ≥16px or set:\n    ```html\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover\"\n    />\n    ```\n  - NEVER: Disable browser zoom\n  - MUST: `touch-action: manipulation` to prevent double-tap zoom; set `-webkit-tap-highlight-color` to match design\n- Inputs & forms (behavior)\n  - MUST: Hydration-safe inputs (no lost focus/value)\n  - NEVER: Block paste in `<input>/<textarea>`\n  - MUST: Loading buttons show spinner and keep original label\n  - MUST: Enter submits focused text input In `<textarea>`, ⌘/Ctrl+Enter submits; Enter adds newline\n  - MUST: Keep submit enabled until request starts; then disable, show spinner, use idempotency key\n  - MUST: Don’t block typing; accept free text and validate after\n  - MUST: Allow submitting incomplete forms to surface validation\n  - MUST: Errors inline next to fields; on submit, focus first error\n  - MUST: `autocomplete` + meaningful `name`; correct `type` and `inputmode`\n  - SHOULD: Disable spellcheck for emails/codes/usernames\n  - SHOULD: Placeholders end with ellipsis and show example pattern (eg, `+1 (123) 456-7890`, `sk-012345…`)\n  - MUST: Warn on unsaved changes before navigation\n  - MUST: Compatible with password managers & 2FA; allow pasting one-time codes\n  - MUST: Trim values to handle text expansion trailing spaces\n  - MUST: No dead zones on checkboxes/radios; label+control share one generous hit target\n- State & navigation\n  - MUST: URL reflects state (deep-link filters/tabs/pagination/expanded panels) Prefer libs like [nuqs](https://nuqs.dev)\n  - MUST: Back/Forward restores scroll\n  - MUST: Links are links—use `<a>/<Link>` for navigation (support Cmd/Ctrl/middle-click)\n- Feedback\n  - SHOULD: Optimistic UI; reconcile on response; on failure show error and rollback or offer Undo\n  - MUST: Confirm destructive actions or provide Undo window\n  - MUST: Use polite `aria-live` for toasts/inline validation\n  - SHOULD: Ellipsis (`…`) for options that open follow-ups (eg, \"Rename…\") and loading states (eg, \"Loading…\", \"Saving…\", \"Generating…\")\n- Touch/drag/scroll\n  - MUST: Design forgiving interactions (generous targets, clear affordances; avoid finickiness)\n  - MUST: Delay first tooltip in a group; subsequent peers no delay\n  - MUST: Intentional `overscroll-behavior: contain` in modals/drawers\n  - MUST: During drag, disable text selection and set `inert` on dragged element/containers\n  - MUST: No “dead-looking” interactive zones—if it looks clickable, it is\n- Autofocus\n  - SHOULD: Autofocus on desktop when there’s a single primary input; rarely on mobile (to avoid layout shift)\n\n## Animation\n\n- MUST: Honor `prefers-reduced-motion` (provide reduced variant)\n- SHOULD: Prefer CSS > Web Animations API > JS libraries\n- MUST: Animate compositor-friendly props (`transform`, `opacity`); avoid layout/repaint props (`top/left/width/height`)\n- SHOULD: Animate only to clarify cause/effect or add deliberate delight\n- SHOULD: Choose easing to match the change (size/distance/trigger)\n- MUST: Animations are interruptible and input-driven (avoid autoplay)\n- MUST: Correct `transform-origin` (motion starts where it “physically” should)\n\n## Layout\n\n- SHOULD: Optical alignment; adjust by ±1px when perception beats geometry\n- MUST: Deliberate alignment to grid/baseline/edges/optical centers—no accidental placement\n- SHOULD: Balance icon/text lockups (stroke/weight/size/spacing/color)\n- MUST: Verify mobile, laptop, ultra-wide (simulate ultra-wide at 50% zoom)\n- MUST: Respect safe areas (use env(safe-area-inset-\\*))\n- MUST: Avoid unwanted scrollbars; fix overflows\n\n## Content & Accessibility\n\n- SHOULD: Inline help first; tooltips last resort\n- MUST: Skeletons mirror final content to avoid layout shift\n- MUST: `<title>` matches current context\n- MUST: No dead ends; always offer next step/recovery\n- MUST: Design empty/sparse/dense/error states\n- SHOULD: Curly quotes (“ ”); avoid widows/orphans\n- MUST: Tabular numbers for comparisons (`font-variant-numeric: tabular-nums` or a mono like Geist Mono)\n- MUST: Redundant status cues (not color-only); icons have text labels\n- MUST: Don’t ship the schema—visuals may omit labels but accessible names still exist\n- MUST: Use the ellipsis character `…` (not ``)\n- MUST: `scroll-margin-top` on headings for anchored links; include a “Skip to content” link; hierarchical `<h1–h6>`\n- MUST: Resilient to user-generated content (short/avg/very long)\n- MUST: Locale-aware dates/times/numbers/currency\n- MUST: Accurate names (`aria-label`), decorative elements `aria-hidden`, verify in the Accessibility Tree\n- MUST: Icon-only buttons have descriptive `aria-label`\n- MUST: Prefer native semantics (`button`, `a`, `label`, `table`) before ARIA\n- SHOULD: Right-clicking the nav logo surfaces brand assets\n- MUST: Use non-breaking spaces to glue terms: `10&nbsp;MB`, `⌘&nbsp;+&nbsp;K`, `Vercel&nbsp;SDK`\n\n## Performance\n\n- SHOULD: Test iOS Low Power Mode and macOS Safari\n- MUST: Measure reliably (disable extensions that skew runtime)\n- MUST: Track and minimize re-renders (React DevTools/React Scan)\n- MUST: Profile with CPU/network throttling\n- MUST: Batch layout reads/writes; avoid unnecessary reflows/repaints\n- MUST: Mutations (`POST/PATCH/DELETE`) target <500 ms\n- SHOULD: Prefer uncontrolled inputs; make controlled loops cheap (keystroke cost)\n- MUST: Virtualize large lists (eg, `virtua`)\n- MUST: Preload only above-the-fold images; lazy-load the rest\n- MUST: Prevent CLS from images (explicit dimensions or reserved space)\n\n## Design\n\n- SHOULD: Layered shadows (ambient + direct)\n- SHOULD: Crisp edges via semi-transparent borders + shadows\n- SHOULD: Nested radii: child ≤ parent; concentric\n- SHOULD: Hue consistency: tint borders/shadows/text toward bg hue\n- MUST: Accessible charts (color-blind-friendly palettes)\n- MUST: Meet contrast—prefer [APCA](https://apcacontrast.com/) over WCAG 2\n- MUST: Increase contrast on `:hover/:active/:focus`\n- SHOULD: Match browser UI to bg\n- SHOULD: Avoid gradient banding (use masks when needed)\n\n## Copywriting\n\n- MUST: Use active voice.\n- MUST: Use title case for headings and buttons.\n- MUST: Be clear and concise. Use as few words as possible.\n- MUST: Prefer & over and.\n- MUST: Use action-oriented language.\n- MUST: Keep nouns consistent. Introduce as few unique terms as possible.\n- MUST: Write in second person. Avoid first person.\n- MUST: Use consistent placeholders.\n- MUST: Use numerals for counts.\n- MUST: Use consistent currency formatting.\n- MUST: Separate numbers & units with a space.\n- MUST: Use a non-breaking space e.g., 10&nbsp;MB.\n- MUST: Default to positive language. Frame messages in an encouraging, problem-solving way, even for errors.\n- MUST: Error messages guide the exit. Don't just state what went wrong—tell the user how to fix it.\n- MUST: Avoid ambiguity. Labels are clear & specific.\n- MUST: Instead of the button label \"Continue\", say \"Save API Key\".\n"
  },
  {
    "path": "packages/website/app/api/og/route.tsx",
    "content": "import { ImageResponse } from \"next/og\";\n\nexport const runtime = \"edge\";\n\nconst BRAND_PINK = \"#fc4efd\";\nconst BACKGROUND_DARK_PURPLE = \"#1a0815\";\n\nconst getGoogleFontUrl = (fontFamily: string, weight: number) =>\n  `https://fonts.googleapis.com/css2?family=${fontFamily.replace(/ /g, \"+\")}:wght@${weight}&display=swap`;\n\nconst fetchFont = async (fontFamily: string, weight: number) => {\n  const cssUrl = getGoogleFontUrl(fontFamily, weight);\n  const cssResponse = await fetch(cssUrl);\n  const cssText = await cssResponse.text();\n\n  const fontUrlMatch = cssText.match(/src: url\\(([^)]+)\\)/);\n  if (!fontUrlMatch) {\n    throw new Error(\"Could not find font URL in CSS\");\n  }\n\n  const fontUrl = fontUrlMatch[1];\n  const fontResponse = await fetch(fontUrl);\n  return fontResponse.arrayBuffer();\n};\n\nconst ReactGrabLogo = () => (\n  <svg\n    width=\"48\"\n    height=\"48\"\n    viewBox=\"0 0 294 294\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <g clipPath=\"url(#clip0_og)\">\n      <mask\n        id=\"mask0_og\"\n        maskUnits=\"userSpaceOnUse\"\n        x=\"0\"\n        y=\"0\"\n        width=\"294\"\n        height=\"294\"\n      >\n        <path d=\"M294 0H0V294H294V0Z\" fill=\"white\" />\n      </mask>\n      <g mask=\"url(#mask0_og)\">\n        <path\n          d=\"M144.599 47.4924C169.712 27.3959 194.548 20.0265 212.132 30.1797C227.847 39.2555 234.881 60.3243 231.926 89.516C231.677 92.0069 231.328 94.5423 230.94 97.1058L228.526 110.14C228.517 110.136 228.505 110.132 228.495 110.127C228.486 110.165 228.479 110.203 228.468 110.24L216.255 105.741C216.256 105.736 216.248 105.728 216.248 105.723C207.915 103.125 199.421 101.075 190.82 99.5888L190.696 99.5588L173.526 97.2648L173.511 97.2631C173.492 97.236 173.467 97.2176 173.447 97.1905C163.862 96.2064 154.233 95.7166 144.599 95.7223C134.943 95.7162 125.295 96.219 115.693 97.2286C110.075 105.033 104.859 113.118 100.063 121.453C95.2426 129.798 90.8624 138.391 86.939 147.193C90.8624 155.996 95.2426 164.588 100.063 172.933C104.866 181.302 110.099 189.417 115.741 197.245C115.749 197.245 115.758 197.246 115.766 197.247L115.752 197.27L115.745 197.283L115.754 197.296L126.501 211.013L126.574 211.089C132.136 217.767 138.126 224.075 144.507 229.974L144.609 230.082L154.572 238.287C154.539 238.319 154.506 238.35 154.472 238.38C154.485 238.392 154.499 238.402 154.513 238.412L143.846 247.482L143.827 247.497C126.56 261.128 109.472 268.745 94.8019 268.745C88.5916 268.837 82.4687 267.272 77.0657 264.208C61.3496 255.132 54.3164 234.062 57.2707 204.871C57.528 202.307 57.8806 199.694 58.2904 197.054C28.3363 185.327 9.52301 167.51 9.52301 147.193C9.52301 129.042 24.2476 112.396 50.9901 100.375C53.3443 99.3163 55.7938 98.3058 58.2904 97.3526C57.8806 94.7023 57.528 92.0803 57.2707 89.516C54.3164 60.3243 61.3496 39.2555 77.0657 30.1797C94.6494 20.0265 119.486 27.3959 144.599 47.4924ZM70.6423 201.315C70.423 202.955 70.2229 204.566 70.0704 206.168C67.6686 229.567 72.5478 246.628 83.3615 252.988L83.5176 253.062C95.0399 259.717 114.015 254.426 134.782 238.38C125.298 229.45 116.594 219.725 108.764 209.314C95.8516 207.742 83.0977 205.066 70.6423 201.315ZM80.3534 163.438C77.34 171.677 74.8666 180.104 72.9484 188.664C81.1787 191.224 89.5657 193.247 98.0572 194.724L98.4618 194.813C95.2115 189.865 92.0191 184.66 88.9311 179.378C85.8433 174.097 83.003 168.768 80.3534 163.438ZM60.759 110.203C59.234 110.839 57.7378 111.475 56.27 112.11C34.7788 121.806 22.3891 134.591 22.3891 147.193C22.3891 160.493 36.4657 174.297 60.7494 184.26C63.7439 171.581 67.8124 159.182 72.9104 147.193C67.822 135.23 63.7566 122.855 60.759 110.203ZM98.4137 99.6404C89.8078 101.145 81.3075 103.206 72.9676 105.809C74.854 114.203 77.2741 122.468 80.2132 130.554L80.3059 130.939C82.9938 125.6 85.8049 120.338 88.8834 115.008C91.9618 109.679 95.1544 104.569 98.4137 99.6404ZM94.9258 38.5215C90.9331 38.4284 86.9866 39.3955 83.4891 41.3243C72.6291 47.6015 67.6975 64.5954 70.0424 87.9446L70.0416 88.2194C70.194 89.8208 70.3941 91.4325 70.6134 93.0624C83.0737 89.3364 95.8263 86.6703 108.736 85.0924C116.57 74.6779 125.28 64.9532 134.773 56.0249C119.877 44.5087 105.895 38.5215 94.9258 38.5215ZM205.737 41.3148C202.268 39.398 198.355 38.4308 194.394 38.5099L194.29 38.512C183.321 38.512 169.34 44.4991 154.444 56.0153C163.93 64.9374 172.634 74.6557 180.462 85.064C193.375 86.6345 206.128 89.3102 218.584 93.0624C218.812 91.4325 219.003 89.8118 219.165 88.2098C221.548 64.7099 216.65 47.6164 205.737 41.3148ZM144.552 64.3097C138.104 70.2614 132.054 76.6306 126.443 83.3765C132.39 82.995 138.426 82.8046 144.552 82.8046C150.727 82.8046 156.778 83.0143 162.707 83.3765C157.08 76.6293 151.015 70.2596 144.552 64.3097Z\"\n          fill={BRAND_PINK}\n        />\n      </g>\n      <mask\n        id=\"mask1_og\"\n        maskUnits=\"userSpaceOnUse\"\n        x=\"102\"\n        y=\"84\"\n        width=\"161\"\n        height=\"162\"\n      >\n        <path\n          d=\"M235.282 84.827L102.261 112.259L129.693 245.28L262.714 217.848L235.282 84.827Z\"\n          fill=\"white\"\n        />\n      </mask>\n      <g mask=\"url(#mask1_og)\">\n        <path\n          d=\"M136.863 129.916L213.258 141.224C220.669 142.322 222.495 152.179 215.967 155.856L187.592 171.843L184.135 204.227C183.339 211.678 173.564 213.901 169.624 207.526L129.021 141.831C125.503 136.14 130.245 128.936 136.863 129.916Z\"\n          fill={BRAND_PINK}\n          stroke={BRAND_PINK}\n          strokeWidth=\"0.817337\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n      </g>\n    </g>\n    <defs>\n      <clipPath id=\"clip0_og\">\n        <rect width=\"294\" height=\"294\" fill=\"white\" />\n      </clipPath>\n    </defs>\n  </svg>\n);\n\nexport const GET = async (request: Request) => {\n  const { searchParams } = new URL(request.url);\n  const title = searchParams.get(\"title\") ?? \"React Grab\";\n  const subtitle = searchParams.get(\"subtitle\");\n\n  const geistSemiBold = await fetchFont(\"Geist\", 600);\n  const geistRegular = await fetchFont(\"Geist\", 400);\n\n  return new ImageResponse(\n    <div\n      style={{\n        height: \"100%\",\n        width: \"100%\",\n        display: \"flex\",\n        flexDirection: \"column\",\n        alignItems: \"flex-start\",\n        justifyContent: \"flex-end\",\n        backgroundColor: BACKGROUND_DARK_PURPLE,\n        padding: \"80px\",\n      }}\n    >\n      <div\n        style={{\n          position: \"absolute\",\n          top: \"80px\",\n          left: \"80px\",\n          display: \"flex\",\n          alignItems: \"center\",\n          gap: \"16px\",\n        }}\n      >\n        <ReactGrabLogo />\n        <span\n          style={{\n            fontSize: 32,\n            fontFamily: \"Geist\",\n            fontWeight: 600,\n            color: \"#ffffff\",\n            letterSpacing: \"-0.02em\",\n          }}\n        >\n          React Grab\n        </span>\n      </div>\n\n      <div\n        style={{\n          display: \"flex\",\n          flexDirection: \"column\",\n          gap: \"16px\",\n          maxWidth: \"900px\",\n        }}\n      >\n        <h1\n          style={{\n            fontSize: title.length > 40 ? 56 : 72,\n            fontFamily: \"Geist\",\n            fontWeight: 600,\n            color: \"#ffffff\",\n            lineHeight: 1.1,\n            letterSpacing: \"-0.03em\",\n            margin: 0,\n          }}\n        >\n          {title}\n        </h1>\n        {subtitle && (\n          <p\n            style={{\n              fontSize: 28,\n              fontFamily: \"Geist\",\n              fontWeight: 400,\n              color: \"rgba(255, 255, 255, 0.5)\",\n              lineHeight: 1.4,\n              margin: 0,\n            }}\n          >\n            {subtitle}\n          </p>\n        )}\n      </div>\n    </div>,\n    {\n      width: 1200,\n      height: 630,\n      fonts: [\n        {\n          name: \"Geist\",\n          data: geistSemiBold,\n          style: \"normal\",\n          weight: 600,\n        },\n        {\n          name: \"Geist\",\n          data: geistRegular,\n          style: \"normal\",\n          weight: 400,\n        },\n      ],\n    },\n  );\n};\n"
  },
  {
    "path": "packages/website/app/api/report-cli/route.ts",
    "content": "import { getCorsHeaders, createOptionsResponse } from \"@/lib/api-helpers\";\n\ninterface ReportPayload {\n  type: \"error\" | \"completed\";\n  version: string;\n  config?: {\n    framework: string;\n    packageManager: string;\n    router?: string;\n    agent?: string;\n    isMonorepo: boolean;\n  };\n  error?: {\n    message: string;\n    stack?: string;\n  };\n  timestamp: string;\n}\n\nconst corsOptions = {\n  methods: [\"POST\", \"OPTIONS\"] as const,\n  headers: \"*\" as const,\n};\n\nexport const POST = async (request: Request): Promise<Response> => {\n  let payload: ReportPayload;\n\n  try {\n    payload = await request.json();\n  } catch {\n    return new Response(\"Invalid JSON\", {\n      status: 400,\n      headers: getCorsHeaders(corsOptions),\n    });\n  }\n\n  console.log(\n    `[CLI Report] ${payload.type}:`,\n    JSON.stringify(payload, null, 2),\n  );\n\n  return new Response(\"OK\", { headers: getCorsHeaders(corsOptions) });\n};\n\nexport const OPTIONS = (): Response => createOptionsResponse(corsOptions);\n"
  },
  {
    "path": "packages/website/app/api/version/route.ts",
    "content": "import packageJson from \"react-grab/package.json\";\nimport { getCorsHeaders, createOptionsResponse } from \"@/lib/api-helpers\";\n\nexport const dynamic = \"force-dynamic\";\n\nconst corsOptions = { methods: \"*\" as const, headers: \"*\" as const };\n\nexport const GET = (): Response => {\n  return new Response(packageJson.version, {\n    headers: {\n      ...getCorsHeaders(corsOptions),\n      \"Cache-Control\": \"no-store, no-cache, must-revalidate\",\n    },\n  });\n};\n\nexport const OPTIONS = (): Response => createOptionsResponse(corsOptions);\n"
  },
  {
    "path": "packages/website/app/blog/1-0/layout.tsx",
    "content": "import type { Metadata } from \"next\";\n\nconst title = \"React Grab 1.0\";\nconst description =\n  \"React Grab 1.0 is here. Select context for coding agents directly from your website & make tools like Cursor, Claude Code, Copilot run up to 3× faster.\";\nconst ogImageUrl = `https://react-grab.com/api/og?title=${encodeURIComponent(title)}&subtitle=${encodeURIComponent(description)}`;\n\nexport const metadata: Metadata = {\n  title,\n  description,\n  openGraph: {\n    title,\n    description,\n    url: \"https://react-grab.com/blog/1-0\",\n    siteName: \"React Grab\",\n    images: [\n      {\n        url: ogImageUrl,\n        width: 1200,\n        height: 630,\n        alt: `${title}`,\n      },\n    ],\n    locale: \"en_US\",\n    type: \"article\",\n    authors: [\"Aiden Bai\"],\n    publishedTime: \"2026-01-28T00:00:00Z\",\n  },\n  twitter: {\n    card: \"summary_large_image\",\n    title,\n    description,\n    images: [ogImageUrl],\n    creator: \"@aidenybai\",\n  },\n  alternates: {\n    canonical: \"https://react-grab.com/blog/1-0\",\n  },\n};\n\ninterface BlogPostLayoutProps {\n  children: React.ReactNode;\n}\n\nconst BlogPostLayout = ({ children }: BlogPostLayoutProps) => {\n  return children;\n};\n\nBlogPostLayout.displayName = \"BlogPostLayout\";\n\nexport default BlogPostLayout;\n"
  },
  {
    "path": "packages/website/app/blog/1-0/page.tsx",
    "content": "\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { InstallTabs } from \"@/components/install-tabs\";\nimport { BenchmarkTooltip } from \"@/components/benchmark-tooltip\";\nimport { IconClaude } from \"@/components/icons/icon-claude\";\nimport { IconCopilot } from \"@/components/icons/icon-copilot\";\nimport { IconCursor } from \"@/components/icons/icon-cursor\";\nimport { BlogArticleLayout } from \"@/components/blog-article-layout\";\n\nconst headings = [\n  { id: \"react-grab-is-now-1-0\", text: \"React Grab 1.0\", level: 3 },\n  { id: \"install-react-grab\", text: \"Install React Grab\", level: 3 },\n];\n\nconst authors = [{ name: \"Aiden Bai\", url: \"https://x.com/aidenybai\" }];\n\ninterface ToolWithIconProps {\n  icon: ReactNode;\n  name: string;\n}\n\nconst ToolWithIcon = ({ icon, name }: ToolWithIconProps) => (\n  <span className=\"inline-flex items-baseline gap-1 whitespace-nowrap\">\n    {icon}\n    {name}\n  </span>\n);\n\nToolWithIcon.displayName = \"ToolWithIcon\";\n\nconst BlogPostPage = () => {\n  return (\n    <BlogArticleLayout\n      title=\"React Grab 1.0\"\n      authors={authors}\n      date=\"January 28, 2026\"\n      headings={headings}\n    >\n      <div className=\"flex flex-col gap-4 text-neutral-400\">\n        <div className=\"flex flex-col gap-3\">\n          <p>React Grab 1.0 is finally here.</p>\n          <p>\n            React Grab lets you select context for coding agents directly from\n            your website.\n          </p>\n          <p>\n            It makes tools like{\" \"}\n            <ToolWithIcon\n              icon={\n                <IconCursor\n                  width={16}\n                  height={16}\n                  className=\"translate-y-[2px] text-foreground\"\n                />\n              }\n              name=\"Cursor\"\n            />\n            ,{\" \"}\n            <ToolWithIcon\n              icon={\n                <IconClaude\n                  width={16}\n                  height={16}\n                  className=\"translate-y-[2px]\"\n                />\n              }\n              name=\"Claude Code\"\n            />\n            ,{\" \"}\n            <ToolWithIcon\n              icon={\n                <IconCopilot\n                  width={18}\n                  height={18}\n                  className=\"translate-y-[2px] text-foreground\"\n                />\n              }\n              name=\"Copilot\"\n            />{\" \"}\n            run up to{\" \"}\n            <BenchmarkTooltip\n              href=\"/blog/intro\"\n              className=\"shimmer-text-pink inline-block touch-manipulation py-1\"\n            >\n              <span className=\"font-bold\">3×</span>&nbsp;faster\n            </BenchmarkTooltip>\n            .\n          </p>\n        </div>\n\n        <InstallTabs />\n      </div>\n    </BlogArticleLayout>\n  );\n};\n\nBlogPostPage.displayName = \"BlogPostPage\";\n\nexport default BlogPostPage;\n"
  },
  {
    "path": "packages/website/app/blog/agent/layout.tsx",
    "content": "import type { Metadata } from \"next\";\n\nconst title = \"React Grab for Agents\";\nconst description =\n  \"React Grab used to stop at copying context for your coding agent. Now it can directly talk to the agent to edit the code directly from the browser.\";\nconst ogImageUrl = `https://react-grab.com/api/og?title=${encodeURIComponent(title)}&subtitle=${encodeURIComponent(description)}`;\n\nexport const metadata: Metadata = {\n  title,\n  description,\n  openGraph: {\n    title,\n    description,\n    url: \"https://react-grab.com/blog/agent\",\n    siteName: \"React Grab\",\n    images: [\n      {\n        url: ogImageUrl,\n        width: 1200,\n        height: 630,\n        alt: `React Grab - ${title}`,\n      },\n    ],\n    locale: \"en_US\",\n    type: \"article\",\n    authors: [\"Aiden Bai\", \"Ben Maclaurin\"],\n    publishedTime: \"2025-12-04T00:00:00Z\",\n  },\n  twitter: {\n    card: \"summary_large_image\",\n    title,\n    description,\n    images: [ogImageUrl],\n    creator: \"@aidenybai\",\n  },\n  alternates: {\n    canonical: \"https://react-grab.com/blog/agent\",\n  },\n};\n\ninterface AgentLayoutProps {\n  children: React.ReactNode;\n}\n\nconst AgentLayout = ({ children }: AgentLayoutProps) => {\n  return children;\n};\n\nAgentLayout.displayName = \"AgentLayout\";\n\nexport default AgentLayout;\n"
  },
  {
    "path": "packages/website/app/blog/agent/page.tsx",
    "content": "\"use client\";\n\nimport { useState, useEffect } from \"react\";\nimport Link from \"next/link\";\nimport { highlightCode } from \"@/lib/shiki\";\nimport { IconClaude } from \"@/components/icons/icon-claude\";\nimport { IconCursor } from \"@/components/icons/icon-cursor\";\nimport { IconCopilot } from \"@/components/icons/icon-copilot\";\nimport { IconOpenCode } from \"@/components/icons/icon-opencode\";\nimport { IconDroid } from \"@/components/icons/icon-droid\";\nimport { GithubButton } from \"@/components/github-button\";\nimport { ViewDocsButton } from \"@/components/view-docs-button\";\nimport { BlogArticleLayout } from \"@/components/blog-article-layout\";\nimport { Button } from \"@/components/ui/button\";\nimport { COPY_FEEDBACK_DURATION_MS } from \"@/constants\";\n\ninterface HighlightedCodeBlockProps {\n  code: string;\n  lang: string;\n}\n\nconst HighlightedCodeBlock = ({ code, lang }: HighlightedCodeBlockProps) => {\n  const [highlightedHtml, setHighlightedHtml] = useState<string>(\"\");\n  const [didCopy, setDidCopy] = useState(false);\n\n  useEffect(() => {\n    const highlight = async () => {\n      const html = await highlightCode({ code, lang, showLineNumbers: false });\n      setHighlightedHtml(html);\n    };\n    highlight();\n  }, [code, lang]);\n\n  const handleCopy = () => {\n    if (typeof navigator === \"undefined\" || !navigator.clipboard) return;\n    navigator.clipboard\n      .writeText(code)\n      .then(() => {\n        setDidCopy(true);\n        setTimeout(() => setDidCopy(false), COPY_FEEDBACK_DURATION_MS);\n      })\n      .catch(() => {});\n  };\n\n  return (\n    <div className=\"group relative\">\n      <Button\n        variant=\"ghost\"\n        type=\"button\"\n        onClick={handleCopy}\n        className=\"absolute right-0 top-0 text-[11px] text-muted-foreground opacity-0 transition-opacity hover:text-foreground group-hover:opacity-100 z-10\"\n      >\n        {didCopy ? \"Copied\" : \"Copy\"}\n      </Button>\n      {highlightedHtml ? (\n        <div\n          className=\"overflow-x-auto font-mono text-[13px] leading-relaxed\"\n          dangerouslySetInnerHTML={{ __html: highlightedHtml }}\n        />\n      ) : (\n        <pre className=\"text-foreground/70 whitespace-pre font-mono text-xs leading-relaxed\">\n          {code}\n        </pre>\n      )}\n    </div>\n  );\n};\n\nconst headings = [\n  { id: \"tldr\", text: \"TL;DR\", level: 3 },\n  { id: \"what-stays-the-same\", text: \"What stays the same\", level: 3 },\n  { id: \"what-is-new\", text: \"What is new\", level: 3 },\n  { id: \"how-react-grab-started\", text: \"How React Grab started\", level: 3 },\n  { id: \"we-can-do-better\", text: \"We can do better\", level: 3 },\n  { id: \"react-grab-for-agents\", text: \"React Grab for Agents\", level: 3 },\n  { id: \"setup\", text: \"Setup\", level: 3 },\n  { id: \"how-it-works\", text: \"How it works\", level: 3 },\n  { id: \"whats-next\", text: \"What's next\", level: 3 },\n  { id: \"try-it-out\", text: \"Try it out\", level: 3 },\n  { id: \"footnotes\", text: \"Footnotes\", level: 4 },\n];\n\nconst authors = [\n  { name: \"Aiden Bai\", url: \"https://x.com/aidenybai\" },\n  { name: \"Ben Maclaurin\", url: \"https://x.com/ben__maclaurin\" },\n];\n\nconst AgentPage = () => {\n  return (\n    <BlogArticleLayout\n      title=\"React Grab for Agents\"\n      authors={authors}\n      date=\"December 4, 2025\"\n      headings={headings}\n      subtitle={\n        <p className=\"text-sm text-neutral-500 italic\">\n          <a\n            href=\"https://www.youtube.com/watch?v=3CRs8kusyhE\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"hover:text-foreground/80 underline underline-offset-4\"\n          >\n            Prefer a video breakdown?\n          </a>\n        </p>\n      }\n    >\n      <div className=\"flex flex-col gap-4 text-neutral-400\">\n        <div className=\"flex flex-col gap-3\">\n          <h3\n            id=\"tldr\"\n            className=\"text-lg font-medium text-neutral-200 scroll-mt-24\"\n          >\n            TL;DR\n          </h3>\n          <p>\n            React Grab used to stop at copying context for your coding agent.\n            Now it can directly talk to the agent to edit the code -- all from\n            the browser.\n          </p>\n        </div>\n\n        <div className=\"flex flex-col gap-3\">\n          <h3\n            id=\"what-stays-the-same\"\n            className=\"text-lg font-medium text-neutral-200 mt-4 scroll-mt-24\"\n          >\n            What stays the same\n          </h3>\n          <ul className=\"list-disc space-y-2 pl-6\">\n            <li>React Grab is still free and open source</li>\n            <li>\n              It still works with any AI coding tool (\n              <IconClaude\n                width={12}\n                height={12}\n                className=\"inline -translate-y-px mx-0.5\"\n              />\n              Claude Code,{\" \"}\n              <IconCursor\n                width={12}\n                height={12}\n                className=\"inline -translate-y-px mx-0.5 text-foreground\"\n              />\n              Cursor,{\" \"}\n              <IconOpenCode\n                width={12}\n                height={12}\n                className=\"inline -translate-y-px mx-0.5\"\n              />\n              OpenCode, Codex, Gemini, Amp,{\" \"}\n              <IconDroid\n                width={12}\n                height={12}\n                className=\"inline -translate-y-px mx-0.5 text-foreground\"\n              />\n              Factory Droid,{\" \"}\n              <IconCopilot\n                width={12}\n                height={12}\n                className=\"inline -translate-y-px mx-0.5 text-foreground\"\n              />\n              Copilot, etc.)\n            </li>\n            <li>\n              The core idea is still &quot;click an element, get real React\n              context and file paths&quot;\n            </li>\n          </ul>\n        </div>\n\n        <div className=\"flex flex-col gap-3\">\n          <h3\n            id=\"what-is-new\"\n            className=\"text-lg font-medium text-neutral-200 mt-4 scroll-mt-24\"\n          >\n            What is new\n          </h3>\n          <ul className=\"list-disc space-y-2 pl-6\">\n            <li>\n              You can now spin up agents like Claude Code or Cursor directly\n              from the page\n            </li>\n            <li>\n              You can run multiple UI tasks at once, each attached to the\n              element you clicked\n            </li>\n            <li>\n              You can make changes to your code without leaving the browser\n            </li>\n          </ul>\n          <div className=\"py-4\">\n            <video\n              src=\"/demo.webm\"\n              autoPlay\n              loop\n              muted\n              playsInline\n              controls\n              className=\"w-full rounded-lg\"\n            />\n          </div>\n        </div>\n\n        <div className=\"flex flex-col gap-3\">\n          <h3\n            id=\"how-react-grab-started\"\n            className=\"text-lg font-medium text-neutral-200 mt-8 scroll-mt-24\"\n          >\n            How React Grab started\n          </h3>\n          <p>React Grab came from a simple (but very relevant!) annoyance.</p>\n          <p>\n            Coding agents are good at generating code, but bad at guessing what\n            I actually want. The loop looked like this:\n          </p>\n          <ol className=\"list-decimal space-y-2 pl-6\">\n            <li>\n              I would look at some UI, form a mental picture, and then try to\n              describe it in English.\n            </li>\n            <li>\n              The agent would guess which files to open, grep around, and maybe\n              land on the right component. As the codebase grew, this\n              &quot;guess where this is&quot; step became the bottleneck.\n            </li>\n          </ol>\n          <p>\n            I built the first version of React Grab\n            <sup className=\"text-neutral-500 text-[10px] ml-0.5\">2</sup> to\n            solve this: press{\" \"}\n            <code className=\"text-foreground/70 bg-card border border-border rounded-lg px-1 py-0.5 text-xs\">\n              ⌘C\n            </code>\n            , click an element, and React Grab gives you the component stack\n            with exact file paths and line numbers.\n          </p>\n          <div className=\"py-4\">\n            <video\n              src=\"/demo.webm\"\n              autoPlay\n              loop\n              muted\n              playsInline\n              className=\"w-full rounded-lg\"\n            />\n          </div>\n          <p>\n            Now, instead of guessing where an element might live, the agent\n            jumps straight to the exact file, line, and column.\n          </p>\n          <p>\n            In the benchmarks I ran on a shadcn dashboard, that alone made\n            Claude Code roughly{\" \"}\n            <Link href=\"/blog/intro\" className=\"shimmer-text-pink\">\n              3× faster\n            </Link>{\" \"}\n            on average for a set of UI tasks.\n            <sup className=\"text-neutral-500 text-[10px] ml-0.5\">1</sup> The\n            agent did fewer tool calls, read fewer files, and got to the edit\n            sooner, because it no longer had to search.\n          </p>\n          <p>\n            React Grab worked. People wired it into their apps. It made coding\n            agents feel less random for UI work.\n          </p>\n          <p>It also had an obvious flaw.</p>\n        </div>\n\n        <div className=\"flex flex-col gap-3\">\n          <h3\n            id=\"we-can-do-better\"\n            className=\"text-lg font-medium text-neutral-200 mt-8 scroll-mt-24\"\n          >\n            We can do better\n          </h3>\n          <p>\n            React Grab solved the context problem and ignored everything else\n            (this is actually intentional!). You still had to copy, switch to\n            your agent, paste, wait, switch back, and refresh. For one-off tasks\n            this was fine. After using it daily, I realized we can do a LOT\n            better.\n          </p>\n          <p>\n            The browser had the best view of your intent. The agent had the\n            power to edit the code. Why not put the agent{\" \"}\n            <span className=\"text-foreground/70 font-medium\">\n              in the browser\n            </span>\n            ?\n          </p>\n          <p className=\"text-sm text-neutral-500 mt-2\">\n            (Theo{\" \"}\n            <a\n              href=\"https://x.com/theo/status/1952229335416623592\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"underline hover:text-neutral-400\"\n            >\n              predicted this\n            </a>{\" \"}\n            months ago.)\n          </p>\n        </div>\n\n        <div className=\"flex flex-col gap-3\">\n          <h3\n            id=\"react-grab-for-agents\"\n            className=\"text-lg font-medium text-neutral-200 mt-8 scroll-mt-24\"\n          >\n            React Grab for Agents\n          </h3>\n          <p>\n            React Grab for Agents is what happens when you let the browser do\n            more of the loop.\n          </p>\n          <p>The idea is simple.</p>\n          <p>\n            You hold{\" \"}\n            <code className=\"text-foreground/70 bg-card border border-border rounded-lg px-1 py-0.5 text-xs\">\n              ⌘C\n            </code>\n            , click an element, and a small label appears showing the component\n            name and tag. Press{\" \"}\n            <code className=\"text-foreground/70 bg-card border border-border rounded-lg px-1 py-0.5 text-xs\">\n              Enter\n            </code>{\" \"}\n            to expand the prompt input. Type what you want to change, press{\" \"}\n            <code className=\"text-foreground/70 bg-card border border-border rounded-lg px-1 py-0.5 text-xs\">\n              Enter\n            </code>{\" \"}\n            again, and the agent starts working.\n          </p>\n          <p>\n            React Grab sends the context (file paths, line numbers, component\n            stack, nearby HTML) along with your prompt to the agent. The agent\n            edits your files directly while the label streams back status\n            updates. When it finishes, the label shows &quot;Completed&quot; and\n            your app reloads with the changes.\n          </p>\n          <p>You never leave the browser. You never touch the clipboard.</p>\n          <p>\n            You can run multiple tasks at once. Click one element, start an\n            edit, then click another and start a different task. Each selection\n            tracks its own progress independently. It starts to feel less like\n            &quot;I am chatting with an assistant&quot; and more like a small\n            job queue attached to my UI\n          </p>\n        </div>\n\n        <div className=\"flex flex-col gap-3\">\n          <h3\n            id=\"setup\"\n            className=\"text-lg font-medium text-neutral-200 mt-8 scroll-mt-24\"\n          >\n            Setup\n          </h3>\n          <p>\n            Run this command at your project root to automatically install React\n            Grab:\n          </p>\n          <div className=\"bg-card border border-border rounded-lg overflow-hidden\">\n            <div className=\"px-3 py-2\">\n              <HighlightedCodeBlock\n                lang=\"bash\"\n                code={`npx -y grab@latest init`}\n              />\n            </div>\n          </div>\n          <p className=\"text-sm text-neutral-500\">\n            The CLI will detect your framework and agent, then add the necessary\n            scripts automatically.\n          </p>\n        </div>\n\n        <div className=\"flex flex-col gap-3\">\n          <h3\n            id=\"how-it-works\"\n            className=\"text-lg font-medium text-neutral-200 mt-8 scroll-mt-24\"\n          >\n            How it works\n          </h3>\n          <p>\n            Under the hood, React Grab for Agents is built on the same mechanics\n            as the original library.\n          </p>\n          <p>When you select an element, React Grab:</p>\n          <ul className=\"list-disc space-y-2 pl-6\">\n            <li>Walks the React fiber tree upward from that element.</li>\n            <li>\n              Collects component display names and, in development, source\n              locations with file path and line and column numbers.\n            </li>\n            <li>\n              Captures a small slice of DOM and attributes around the node.\n            </li>\n          </ul>\n          <p>\n            This is the context that made the original benchmarks so much\n            better. The agent gets a direct pointer instead of a fuzzy\n            description.\n          </p>\n          <p>The new part is the agent provider.</p>\n          <p>\n            An agent provider is a small adapter that connects React Grab to a\n            coding agent. When you submit a prompt, React Grab sends the context\n            and your message to a local server. The server passes this to the\n            actual CLI (\n            <code className=\"text-foreground/70 bg-card border border-border rounded-lg px-1 py-0.5 text-xs\">\n              claude\n            </code>{\" \"}\n            or{\" \"}\n            <code className=\"text-foreground/70 bg-card border border-border rounded-lg px-1 py-0.5 text-xs\">\n              cursor-agent\n            </code>\n            ) which edits your codebase directly. Status updates stream back to\n            the browser so you can watch the agent work.\n          </p>\n          <p>\n            The providers are open source. You can read through the\n            implementation or use them as a starting point for your own:{\" \"}\n            <a\n              href=\"https://github.com/aidenybai/react-grab/tree/main/packages/provider-claude-code\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"text-foreground/80 hover:text-foreground underline underline-offset-4\"\n            >\n              @react-grab/claude-code\n            </a>\n            ,{\" \"}\n            <a\n              href=\"https://github.com/aidenybai/react-grab/tree/main/packages/provider-cursor\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"text-foreground/80 hover:text-foreground underline underline-offset-4\"\n            >\n              @react-grab/cursor\n            </a>\n            ,{\" \"}\n            <a\n              href=\"https://github.com/aidenybai/react-grab/tree/main/packages/provider-opencode\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"text-foreground/80 hover:text-foreground underline underline-offset-4\"\n            >\n              @react-grab/opencode\n            </a>\n            ,{\" \"}\n            <a\n              href=\"https://github.com/aidenybai/react-grab/tree/main/packages/provider-codex\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"text-foreground/80 hover:text-foreground underline underline-offset-4\"\n            >\n              @react-grab/codex\n            </a>\n            ,{\" \"}\n            <a\n              href=\"https://github.com/aidenybai/react-grab/tree/main/packages/provider-gemini\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"text-foreground/80 hover:text-foreground underline underline-offset-4\"\n            >\n              @react-grab/gemini\n            </a>\n            ,{\" \"}\n            <a\n              href=\"https://github.com/aidenybai/react-grab/tree/main/packages/provider-amp\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"text-foreground/80 hover:text-foreground underline underline-offset-4\"\n            >\n              @react-grab/amp\n            </a>\n            ,{\" \"}\n            <a\n              href=\"https://github.com/aidenybai/react-grab/tree/main/packages/provider-droid\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"text-foreground/80 hover:text-foreground underline underline-offset-4\"\n            >\n              @react-grab/droid\n            </a>\n            .\n          </p>\n        </div>\n\n        <div className=\"flex flex-col gap-3\">\n          <h3\n            id=\"whats-next\"\n            className=\"text-lg font-medium text-neutral-200 mt-8 scroll-mt-24\"\n          >\n            What{\"'\"}s next\n          </h3>\n          <p>\n            React Grab for Agents is tool-agnostic on purpose. It integrates\n            with the agents that exist. If your tool has a CLI or an API, you\n            can add a provider.\n          </p>\n          <p>If your tool has a CLI or an API, you can add a provider.</p>\n        </div>\n\n        <div className=\"flex flex-col gap-4 mt-8\">\n          <h3\n            id=\"try-it-out\"\n            className=\"text-lg font-medium text-neutral-200 scroll-mt-24\"\n          >\n            Try it out\n          </h3>\n          <p>\n            React Grab is free and open source.{\" \"}\n            <Link\n              href=\"/\"\n              className=\"text-foreground/80 hover:text-foreground underline underline-offset-4 transition-colors\"\n            >\n              Go try it out!\n            </Link>\n          </p>\n          <div className=\"flex gap-2\">\n            <GithubButton />\n            <ViewDocsButton />\n          </div>\n        </div>\n\n        <div className=\"flex flex-col gap-4 mt-12 pt-8 border-t border-border\">\n          <h4\n            id=\"footnotes\"\n            className=\"text-sm font-medium text-neutral-400 scroll-mt-24\"\n          >\n            Footnotes\n          </h4>\n          <div className=\"flex flex-col gap-4 text-sm text-neutral-500\">\n            <p>\n              <sup className=\"text-neutral-600 mr-1\">1</sup>\n              See the{\" \"}\n              <Link\n                href=\"/blog/intro\"\n                className=\"text-muted-foreground hover:text-foreground underline underline-offset-4\"\n              >\n                full benchmark writeup\n              </Link>\n              . Single trial per test case, so treat the exact number with\n              appropriate skepticism. The direction is consistent across tasks.\n            </p>\n            <p>\n              <sup className=\"text-neutral-600 mr-1\">2</sup>\n              This only works in development mode. React strips source locations\n              in production builds for performance and bundle size. React Grab\n              detects this and falls back to showing component names without\n              file paths. You can enable source maps in production if you need\n              the full paths.\n            </p>\n          </div>\n        </div>\n      </div>\n    </BlogArticleLayout>\n  );\n};\n\nAgentPage.displayName = \"AgentPage\";\n\nexport default AgentPage;\n"
  },
  {
    "path": "packages/website/app/blog/bets/layout.tsx",
    "content": "import type { Metadata } from \"next\";\n\nconst title = \"Some bets\";\nconst description = \"Some bets for the future of AI coding and UI development.\";\nconst ogImageUrl = `https://react-grab.com/api/og?title=${encodeURIComponent(title)}&subtitle=${encodeURIComponent(description)}`;\n\nexport const metadata: Metadata = {\n  title,\n  description,\n  openGraph: {\n    title,\n    description,\n    url: \"https://react-grab.com/blog/bets\",\n    siteName: \"React Grab\",\n    images: [\n      {\n        url: ogImageUrl,\n        width: 1200,\n        height: 630,\n        alt: `React Grab - ${title}`,\n      },\n    ],\n    locale: \"en_US\",\n    type: \"article\",\n    authors: [\"Aiden Bai\", \"Nisarg Patel\"],\n    publishedTime: \"2025-11-29T00:00:00Z\",\n  },\n  twitter: {\n    card: \"summary_large_image\",\n    title,\n    description,\n    images: [ogImageUrl],\n    creator: \"@aidenybai\",\n  },\n  alternates: {\n    canonical: \"https://react-grab.com/blog/bets\",\n  },\n};\n\ninterface BetsLayoutProps {\n  children: React.ReactNode;\n}\n\nconst BetsLayout = ({ children }: BetsLayoutProps) => {\n  return children;\n};\n\nBetsLayout.displayName = \"BetsLayout\";\n\nexport default BetsLayout;\n"
  },
  {
    "path": "packages/website/app/blog/bets/page.tsx",
    "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { GithubButton } from \"@/components/github-button\";\nimport { ViewDocsButton } from \"@/components/view-docs-button\";\nimport { BlogArticleLayout } from \"@/components/blog-article-layout\";\n\nconst headings = [\n  { id: \"bet-1\", text: \"1. AI coding for UI\", level: 3 },\n  { id: \"bet-2\", text: \"2. Web technology\", level: 3 },\n  { id: \"bet-3\", text: \"3. Fast, then good\", level: 3 },\n  { id: \"bet-4\", text: \"4. Media technology past\", level: 3 },\n  { id: \"bet-5\", text: \"5. Two form factors\", level: 3 },\n  { id: \"bet-6\", text: \"6. The gap\", level: 3 },\n  { id: \"try-it-out\", text: \"Try it out\", level: 3 },\n  { id: \"footnotes\", text: \"Footnotes\", level: 4 },\n];\n\nconst authors = [\n  { name: \"Aiden Bai\", url: \"https://x.com/aidenybai\" },\n  { name: \"Nisarg Patel\", url: \"https://x.com/nisargptel\" },\n];\n\nconst BetsPage = () => {\n  return (\n    <BlogArticleLayout\n      title=\"Some bets\"\n      authors={authors}\n      date=\"November 29, 2025\"\n      headings={headings}\n    >\n      <div className=\"flex flex-col gap-4 text-neutral-400\">\n        <div className=\"flex flex-col gap-3\">\n          <h3\n            id=\"bet-1\"\n            className=\"text-lg font-medium text-neutral-200 mt-4 scroll-mt-24\"\n          >\n            1. AI coding for UI will be one of the most economically important\n            ways people build things\n          </h3>\n          <p>\n            Humans and agents will always be using UIs. In fact, UIs will become\n            more important to use: better computer use, interfaces for humans\n            post-code, legacy business software that needs maintaining.\n          </p>\n          <p>There will be code written to build and maintain UIs.</p>\n          <p>\n            As expectations and capabilities rise with AI progress, so will the\n            need for new UIs and better versions of the old.\n          </p>\n        </div>\n\n        <div className=\"flex flex-col gap-3\">\n          <h3\n            id=\"bet-2\"\n            className=\"text-lg font-medium text-neutral-200 mt-4 scroll-mt-24\"\n          >\n            2. Most of the new UIs of the world will be made using web\n            technology\n          </h3>\n          <p>\n            Sub-bet: React is probably{\" \"}\n            <a\n              href=\"https://www.youtube.com/watch?v=P1FLEnKZTAE\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"text-foreground/80 hover:text-foreground underline underline-offset-4\"\n            >\n              the last framework\n            </a>\n            .<sup className=\"text-neutral-500 text-[10px] ml-0.5\">1</sup>\n          </p>\n        </div>\n\n        <div className=\"flex flex-col gap-3\">\n          <h3\n            id=\"bet-3\"\n            className=\"text-lg font-medium text-neutral-200 mt-4 scroll-mt-24\"\n          >\n            3. Models will become (1) incredibly fast, then (2) incredibly good\n            at UI tasks\n          </h3>\n          <p>\n            Fast because code has verifiable rewards (tests pass, code runs),\n            making it ideal for{\" \"}\n            <a\n              href=\"https://cursor.com/blog/tab-rl\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"text-foreground/80 hover:text-foreground underline underline-offset-4\"\n            >\n              RL\n            </a>\n            . Labs are scaling post-training on code, and coding tools are\n            building on top. Good requires taste. Great people will get there\n            eventually but slower.\n          </p>\n        </div>\n\n        <div className=\"flex flex-col gap-3\">\n          <h3\n            id=\"bet-4\"\n            className=\"text-lg font-medium text-neutral-200 mt-4 scroll-mt-24\"\n          >\n            4. Our tools will go the way of &quot;media technology&quot; past\n          </h3>\n          <p>\n            From daguerreotype to polaroid to camcorder to studio-level digital\n            cameras back to iPhones. From low use to an explosion of\n            capabilities and then a product for the average person.\n          </p>\n          <p>\n            Coding, unlike cameras, will progress OOM faster (\n            <a\n              href=\"https://x.com/polynoamial/status/1994439121243169176\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"text-foreground/80 hover:text-foreground underline underline-offset-4\"\n            >\n              5-20 years\n            </a>\n            ).\n          </p>\n          <p>\n            Once AI coding becomes commodified, the winners will be those with\n            better taste and ease of use, not those with bleeding edge\n            capability (1% better than the next best).\n          </p>\n        </div>\n\n        <div className=\"flex flex-col gap-3\">\n          <h3\n            id=\"bet-5\"\n            className=\"text-lg font-medium text-neutral-200 mt-4 scroll-mt-24\"\n          >\n            5. There will only be two form factors for coding with UIs\n          </h3>\n          <p>\n            Low latency low entropy (sub 100ms\n            <sup className=\"text-neutral-500 text-[10px] ml-0.5\">2</sup>), and\n            long background tasks (big refactors, maintenance, scaffolding a{\" \"}\n            <a\n              href=\"https://cognition.ai/blog/swe-grep\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"text-foreground/80 hover:text-foreground underline underline-offset-4\"\n            >\n              well-spec{\"'\"}d feature\n            </a>\n            ).\n          </p>\n        </div>\n\n        <div className=\"flex flex-col gap-3\">\n          <h3\n            id=\"bet-6\"\n            className=\"text-lg font-medium text-neutral-200 mt-4 scroll-mt-24\"\n          >\n            6. The gap\n          </h3>\n          <p>\n            There is no tool that is good at UI <em>(yet)</em>.\n          </p>\n          <p>\n            No company is going all-in on AI coding for UI.\n            <sup className=\"text-neutral-500 text-[10px] ml-0.5\">3</sup>\n          </p>\n          <p>\n            If you agree,{\" \"}\n            <a\n              href=\"https://x.com/aidenybai\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"text-foreground/80 hover:text-foreground underline underline-offset-4\"\n            >\n              we should chat\n            </a>\n            .\n          </p>\n        </div>\n\n        <div className=\"flex flex-col gap-4 mt-8\">\n          <h3\n            id=\"try-it-out\"\n            className=\"text-lg font-medium text-neutral-200 scroll-mt-24\"\n          >\n            Try it out\n          </h3>\n          <p>\n            React Grab is free and open source.{\" \"}\n            <Link\n              href=\"/\"\n              className=\"text-foreground/80 hover:text-foreground underline underline-offset-4 transition-colors\"\n            >\n              Go try it out!\n            </Link>\n          </p>\n          <div className=\"flex gap-2\">\n            <GithubButton />\n            <ViewDocsButton />\n          </div>\n        </div>\n\n        <div className=\"flex flex-col gap-4 mt-12 pt-8 border-t border-border\">\n          <h4\n            id=\"footnotes\"\n            className=\"text-sm font-medium text-neutral-400 scroll-mt-24\"\n          >\n            Footnotes\n          </h4>\n          <div className=\"flex flex-col gap-4 text-sm text-neutral-500\">\n            <p>\n              <sup className=\"text-neutral-600 mr-1\">1</sup>\n              Not literally the last framework ever, but the last major paradigm\n              shift (JSX, hooks, etc)\n            </p>\n            <p>\n              <sup className=\"text-neutral-600 mr-1\">2</sup>\n              This is the latency threshold where interactions feel instant, as\n              anything slower breaks flow.\n            </p>\n            <p>\n              <sup className=\"text-neutral-600 mr-1\">3</sup>\n              Vercel/v0 is close but focused on generation, not iteration.\n              Cursor/Windsurf are general-purpose. There{\"'\"}s no company whose\n              entire thesis is &quot;AI coding, but specifically for UI&quot;\n              with all the specialized tooling that implies.\n            </p>\n          </div>\n        </div>\n      </div>\n    </BlogArticleLayout>\n  );\n};\n\nBetsPage.displayName = \"BetsPage\";\n\nexport default BetsPage;\n"
  },
  {
    "path": "packages/website/app/blog/intro/layout.tsx",
    "content": "import type { Metadata } from \"next\";\n\nconst title = \"I made your coding agent 3× faster at frontend\";\nconst description =\n  \"I got tired of watching Claude grep around my codebase every time I wanted to edit a button. So I built a tool that lets me click any element and copy its exact source location. Turns out it makes coding agents 3× faster.\";\nconst ogImageUrl = `https://react-grab.com/api/og?title=${encodeURIComponent(title)}&subtitle=${encodeURIComponent(description)}`;\n\nexport const metadata: Metadata = {\n  title,\n  description,\n  openGraph: {\n    title,\n    description,\n    url: \"https://react-grab.com/blog/intro\",\n    siteName: \"React Grab\",\n    images: [\n      {\n        url: ogImageUrl,\n        width: 1200,\n        height: 630,\n        alt: `React Grab - ${title}`,\n      },\n    ],\n    locale: \"en_US\",\n    type: \"article\",\n    authors: [\"Aiden Bai\"],\n    publishedTime: \"2025-11-24T00:00:00Z\",\n  },\n  twitter: {\n    card: \"summary_large_image\",\n    title,\n    description,\n    images: [ogImageUrl],\n    creator: \"@aidenybai\",\n  },\n  alternates: {\n    canonical: \"https://react-grab.com/blog/intro\",\n  },\n};\n\ninterface BlogPostLayoutProps {\n  children: React.ReactNode;\n}\n\nconst BlogPostLayout = ({ children }: BlogPostLayoutProps) => {\n  return children;\n};\n\nexport default BlogPostLayout;\n"
  },
  {
    "path": "packages/website/app/blog/intro/page.tsx",
    "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport dynamic from \"next/dynamic\";\nimport Link from \"next/link\";\nimport { BenchmarkResult, TestCase } from \"@/components/benchmarks/types\";\nimport { GithubButton } from \"@/components/github-button\";\nimport { ViewDocsButton } from \"@/components/view-docs-button\";\nimport { Collapsible } from \"@/components/ui/collapsible\";\nimport { BlogArticleLayout } from \"@/components/blog-article-layout\";\nimport resultsData from \"@/public/results.json\";\nimport testCasesData from \"@/public/test-cases.json\";\nimport {\n  AreaChart,\n  Area,\n  XAxis,\n  YAxis,\n  ResponsiveContainer,\n  ReferenceLine,\n} from \"recharts\";\n\nconst BenchmarkCharts = dynamic(\n  () =>\n    import(\"@/components/benchmarks/benchmark-charts\").then(\n      (mod) => mod.BenchmarkCharts,\n    ),\n  { ssr: false },\n);\n\nconst BenchmarkChartsTweetSkeleton = () => (\n  <div className=\"border border-border rounded-lg p-6 min-h-[197px]\">\n    <div className=\"space-y-2\">\n      <div className=\"flex items-center gap-3\">\n        <div className=\"w-20 h-4 bg-neutral-800 rounded shrink-0\" />\n        <div className=\"flex-1 h-5 bg-neutral-800 rounded\" />\n      </div>\n      <div className=\"flex items-center gap-3\">\n        <div className=\"w-20 h-4 bg-neutral-800 rounded shrink-0\" />\n        <div className=\"flex-1 h-5 bg-neutral-800 rounded\" />\n      </div>\n      <div className=\"flex items-center gap-3\">\n        <div className=\"w-20 shrink-0\" />\n        <div className=\"flex-1 h-5\" />\n      </div>\n    </div>\n    <div className=\"flex items-center gap-3 mt-3\">\n      <div className=\"w-20 shrink-0\" />\n      <div className=\"flex-1 h-5\" />\n    </div>\n    <div className=\"mt-3 h-4 w-3/4 bg-neutral-800 rounded\" />\n  </div>\n);\n\nconst BenchmarkChartsTweet = dynamic(\n  () =>\n    import(\"@/components/benchmarks/benchmark-charts\").then(\n      (mod) => mod.BenchmarkChartsTweet,\n    ),\n  { ssr: false, loading: () => <BenchmarkChartsTweetSkeleton /> },\n);\n\nconst BenchmarkDetailedTable = dynamic(\n  () =>\n    import(\"@/components/benchmarks/benchmark-detailed-table\").then(\n      (mod) => mod.BenchmarkDetailedTable,\n    ),\n  { ssr: false },\n);\n\nconst StaticCodeBlock = ({ children }: { children: React.ReactNode }) => (\n  <pre className=\"text-foreground/80 whitespace-pre font-mono text-xs leading-relaxed\">\n    {children}\n  </pre>\n);\n\nconst treatmentDurations = [\n  4.755, 9.423, 4.082, 4.445, 7.015, 4.085, 12.276, 5.65, 7.932, 9.202, 3.54,\n  8.796, 3.826, 3.61, 4.398, 3.825, 5.5, 4.092, 4.816, 4.091,\n];\n\nconst controlDurations = [\n  10.164, 13.411, 19.256, 10.539, 13.507, 12.787, 13.729, 22.528, 9.125, 77.383,\n  11.419, 11.111, 15.488, 7.59, 13.575, 12.215, 12.325, 14.847, 15.216, 20.178,\n];\n\nconst generateKernelDensity = (\n  values: number[],\n  bandwidth: number,\n  min: number,\n  max: number,\n  steps: number,\n) => {\n  const result = [];\n  const stepSize = (max - min) / steps;\n\n  for (let i = 0; i <= steps; i++) {\n    const currentX = min + i * stepSize;\n    let density = 0;\n\n    for (const value of values) {\n      const normalizedDistance = (currentX - value) / bandwidth;\n      density += Math.exp(-0.5 * normalizedDistance * normalizedDistance);\n    }\n\n    density = density / (values.length * bandwidth * Math.sqrt(2 * Math.PI));\n    result.push({ x: currentX, density });\n  }\n\n  return result;\n};\n\nconst generateDistributionData = () => {\n  const minTime = 0;\n  const maxTime = 30;\n  const steps = 60;\n\n  const treatmentDensity = generateKernelDensity(\n    treatmentDurations,\n    1.5,\n    minTime,\n    maxTime,\n    steps,\n  );\n  const controlDensity = generateKernelDensity(\n    controlDurations,\n    3,\n    minTime,\n    maxTime,\n    steps,\n  );\n\n  return treatmentDensity.map((point, index) => ({\n    time: point.x.toFixed(1),\n    reactGrab: point.density,\n    traditional: controlDensity[index].density,\n  }));\n};\n\nconst treatmentAverage =\n  treatmentDurations.reduce((sum, duration) => sum + duration, 0) /\n  treatmentDurations.length;\nconst controlAverage =\n  controlDurations.reduce((sum, duration) => sum + duration, 0) /\n  controlDurations.length;\nconst speedupMultiplier = (controlAverage / treatmentAverage).toFixed(0);\n\nconst TimeComparisonChart = () => {\n  const data = generateDistributionData();\n\n  return (\n    <div className=\"rounded-lg\">\n      <div className=\"flex flex-wrap items-center justify-end gap-4 mb-4 text-xs\">\n        <div className=\"flex items-center gap-2\">\n          <div className=\"w-3 h-0.5 bg-muted-foreground\" />\n          <span className=\"text-muted-foreground\">\n            Without React Grab ~ {controlAverage.toFixed(1)}s\n          </span>\n        </div>\n        <div className=\"flex items-center gap-2\">\n          <div className=\"w-3 h-0.5 bg-[#ff4fff]\" />\n          <span className=\"text-[#ff4fff]\">\n            With React Grab ~ {treatmentAverage.toFixed(1)}s\n          </span>\n        </div>\n      </div>\n      <div className=\"h-[280px] sm:h-[320px]\">\n        <ResponsiveContainer width=\"100%\" height=\"100%\">\n          <AreaChart\n            data={data}\n            margin={{ top: 20, right: 20, bottom: 30, left: 40 }}\n          >\n            <XAxis\n              dataKey=\"time\"\n              axisLine={{ stroke: \"#333\" }}\n              tickLine={{ stroke: \"#333\" }}\n              tick={{ fill: \"#666\", fontSize: 10 }}\n              label={{\n                value: \"Time per Edit (seconds)\",\n                position: \"bottom\",\n                offset: 10,\n                fill: \"#666\",\n                fontSize: 11,\n              }}\n              ticks={[\"0.0\", \"5.0\", \"10.0\", \"15.0\", \"20.0\", \"25.0\", \"30.0\"]}\n            />\n            <YAxis\n              axisLine={{ stroke: \"#333\" }}\n              tickLine={{ stroke: \"#333\" }}\n              tick={{ fill: \"#666\", fontSize: 10 }}\n              label={{\n                value: \"Density\",\n                angle: -90,\n                position: \"insideLeft\",\n                fill: \"#666\",\n                fontSize: 11,\n              }}\n            />\n            <ReferenceLine\n              x={treatmentAverage.toFixed(1)}\n              stroke=\"#ff4fff\"\n              strokeDasharray=\"5 5\"\n              strokeWidth={1.5}\n            />\n            <ReferenceLine\n              x={controlAverage.toFixed(1)}\n              stroke=\"#525252\"\n              strokeDasharray=\"5 5\"\n              strokeWidth={1.5}\n            />\n            <Area\n              type=\"monotone\"\n              dataKey=\"traditional\"\n              stroke=\"#525252\"\n              fill=\"#525252\"\n              fillOpacity={0.4}\n              strokeWidth={2}\n            />\n            <Area\n              type=\"monotone\"\n              dataKey=\"reactGrab\"\n              stroke=\"#ff4fff\"\n              fill=\"#ff4fff\"\n              fillOpacity={0.4}\n              strokeWidth={2}\n            />\n          </AreaChart>\n        </ResponsiveContainer>\n      </div>\n      <div className=\"mt-4 text-center\">\n        <span className=\"text-[#ff4fff] font-medium text-sm sm:text-base\">\n          {speedupMultiplier}× faster on average\n        </span>\n      </div>\n    </div>\n  );\n};\n\nTimeComparisonChart.displayName = \"TimeComparisonChart\";\n\nconst headings = [\n  {\n    id: \"digging-through-react-internals\",\n    text: \"Digging through React internals\",\n    level: 3,\n  },\n  { id: \"benchmarking-for-speed\", text: \"Benchmarking for speed\", level: 3 },\n  { id: \"how-it-impacts-you\", text: \"How it impacts you\", level: 3 },\n  { id: \"whats-next\", text: \"What's next\", level: 3 },\n  { id: \"try-it-out\", text: \"Try it out\", level: 3 },\n  { id: \"footnotes\", text: \"Footnotes\", level: 4 },\n];\n\nconst authors = [{ name: \"Aiden Bai\", url: \"https://x.com/aidenybai\" }];\n\nconst BlogPostPage = () => {\n  const testCaseMapping = useMemo(() => {\n    const mapping: Record<string, string> = {};\n    testCasesData.forEach((testCase: TestCase) => {\n      mapping[testCase.name] = testCase.prompt;\n    });\n    return mapping;\n  }, []);\n\n  return (\n    <BlogArticleLayout\n      title=\"I made your coding agent 3× faster at frontend\"\n      authors={authors}\n      date=\"November 24, 2025\"\n      headings={headings}\n    >\n      <Collapsible header={<span className=\"text-sm font-medium\">TL;DR</span>}>\n        <div className=\"pt-4\">\n          <BenchmarkChartsTweet results={resultsData as BenchmarkResult[]} />\n        </div>\n      </Collapsible>\n\n      <div className=\"flex flex-col gap-4 text-muted-foreground\">\n        <p>\n          Coding agents suck at frontend because{\" \"}\n          <span className=\"font-medium text-foreground/80\">\n            translating intent\n          </span>{\" \"}\n          (from UI → prompt → code → UI) is lossy.\n        </p>\n\n        <p>For example, if you want to make a UI change:</p>\n\n        <ol className=\"list-decimal list-inside space-y-2 pl-2\">\n          <li>Create a visual representation in your brain</li>\n          <li>Write a prompt (e.g. &quot;make this button bigger&quot;)</li>\n        </ol>\n\n        <p>How the coding agent processes this:</p>\n\n        <ol className=\"list-decimal list-inside space-y-2 pl-2\" start={3}>\n          <li>\n            Turns your prompt into a trajectory (e.g. &quot;let me grep/search\n            for where this code might be&quot;)\n          </li>\n          <li>Tries to guess what you{\"'\"}re referencing and edits the code</li>\n        </ol>\n\n        <p>\n          Search is a pretty random process since language models have\n          non-deterministic outputs. Depending on the search strategy, these\n          trajectories range from instant (if lucky) to very long.\n          Unfortunately, this means added latency, cost, and performance.\n        </p>\n\n        <p>Today, there are two solutions to this problem:</p>\n\n        <ul className=\"list-disc list-inside space-y-2 pl-2\">\n          <li>\n            <span className=\"text-foreground/80 font-medium\">\n              Prompt better:\n            </span>{\" \"}\n            Use @ to add additional context, write longer and more specific\n            prompts (this is something YOU control)\n          </li>\n          <li>\n            <span className=\"text-foreground/80 font-medium\">\n              Make the agent better at codebase search\n            </span>{\" \"}\n            (this is something model/agent PROVIDERS control)\n          </li>\n        </ul>\n\n        <p>\n          Improving the agent is a <em>lot</em> of unsolved research problems.\n          It involves training better models (see{\" \"}\n          <a\n            href=\"https://cursor.com/changelog/2-1\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"text-foreground/80 hover:text-foreground underline underline-offset-4\"\n          >\n            Instant Grep\n          </a>\n          ,{\" \"}\n          <a\n            href=\"https://cognition.ai/blog/swe-grep\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"text-foreground/80 hover:text-foreground underline underline-offset-4\"\n          >\n            SWE-grep\n          </a>\n          ).\n        </p>\n\n        <p>\n          Ultimately, reducing the amount of translation steps required makes\n          the process faster and more accurate (this scales with codebase size).\n        </p>\n\n        <p>But what if there was a different way?</p>\n\n        <div className=\"flex flex-col gap-3\">\n          <h3\n            id=\"digging-through-react-internals\"\n            className=\"text-lg font-medium text-foreground/80 mt-4 scroll-mt-24\"\n          >\n            Digging through React internals\n          </h3>\n          <p>\n            In my ad-hoc tests, I noticed that referencing the file path (e.g.{\" \"}\n            <code className=\"text-foreground/80 bg-card border border-border rounded-lg px-1 py-0.5 text-xs\">\n              path/to/component.tsx\n            </code>\n            ) or something to{\" \"}\n            <code className=\"text-foreground/80 bg-card border border-border rounded-lg px-1 py-0.5 text-xs\">\n              grep\n            </code>{\" \"}\n            (e.g.{\" \"}\n            <code className=\"text-foreground/80 bg-card border border-border rounded-lg px-1 py-0.5 text-xs\">\n              className=&quot;flex flex-col gap-5 text-shimmer&quot;\n            </code>\n            ) made the coding agent{\" \"}\n            <span className=\"text-foreground/80 font-medium\">much</span> faster\n            at finding what I was referencing. In short - there are shortcuts to\n            reduce the number of steps needed to search!\n          </p>\n          <p>\n            Turns out, React.js exposes the source location for elements on the\n            page.\n            <sup className=\"text-neutral-500 text-[10px] ml-0.5\">1</sup> React\n            Grab walks up the component tree from the element you clicked,\n            collects each component&apos;s component name and source location\n            (file path + line number), and formats that into a readable stack.\n          </p>\n          <p>It looks something like this:</p>\n          <div className=\"bg-card border border-border rounded-lg overflow-hidden\">\n            <div className=\"px-3 py-2\">\n              <div className=\"font-mono text-xs overflow-x-auto\">\n                <StaticCodeBlock>\n                  <span className=\"text-neutral-500\">&lt;</span>\n                  <span className=\"text-[#4ec9b0]\">span</span>\n                  <span className=\"text-neutral-500\">&gt;</span>\n                  <span className=\"text-[#ce9178]\">React Grab</span>\n                  <span className=\"text-neutral-500\">&lt;/</span>\n                  <span className=\"text-[#4ec9b0]\">span</span>\n                  <span className=\"text-neutral-500\">&gt;</span>\n                  {\"\\n\"}\n                  <span className=\"text-neutral-500\">in </span>\n                  <span className=\"text-[#dcdcaa]\">StreamDemo</span>\n                  <span className=\"text-neutral-500\"> at </span>\n                  <span className=\"text-[#9cdcfe]\">\n                    components/stream-demo.tsx:42:11\n                  </span>\n                </StaticCodeBlock>\n              </div>\n            </div>\n          </div>\n          <p>\n            When I passed this to Cursor, it <em>instantly</em> found the file\n            and made the change in a couple seconds. Trying on a couple other\n            cases got the same result.\n          </p>\n          <div className=\"py-12\">\n            <video\n              src=\"/demo.webm\"\n              autoPlay\n              loop\n              muted\n              playsInline\n              className=\"w-full rounded-lg\"\n            />\n          </div>\n        </div>\n\n        <div className=\"flex flex-col gap-3\">\n          <h3\n            id=\"benchmarking-for-speed\"\n            className=\"text-lg font-medium text-foreground/80 mt-4 scroll-mt-24\"\n          >\n            Benchmarking for speed\n          </h3>\n          <p>\n            I used the{\" \"}\n            <a\n              href=\"https://github.com/shadcn-ui/ui\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"text-foreground/80 hover:text-foreground underline underline-offset-4\"\n            >\n              shadcn/ui dashboard\n            </a>{\" \"}\n            as the test codebase. This is a Next.js application with auth, data\n            tables, charts, and form components.\n          </p>\n          <p>\n            The benchmark consists of{\" \"}\n            <a\n              href=\"https://github.com/aidenybai/react-bench/tree/2c2702f/test-cases.json\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"text-foreground/80 hover:text-foreground underline underline-offset-4\"\n            >\n              20 test cases\n            </a>{\" \"}\n            designed to cover a wide range of UI element retrieval scenarios.\n            Each test represents a real-world task that developers commonly\n            perform when working with coding agents.\n          </p>\n          <p>\n            Each test ran twice: once with React Grab enabled (treatment), once\n            without (control). Both conditions used identical codebases and\n            Claude 4.5 Sonnet (in Claude Code).\n            <sup className=\"text-neutral-500 text-[10px] ml-0.5\">2</sup>\n          </p>\n        </div>\n      </div>\n\n      <div className=\"-mx-4 sm:-mx-8 lg:mx-0 px-4 sm:px-8 lg:px-0 py-16\">\n        <div className=\"max-w-4xl\">\n          <div className=\"grid gap-6 lg:grid-cols-2\">\n            <div className=\"flex flex-col gap-3\">\n              <div className=\"text-xs font-medium text-muted-foreground\">\n                Without React Grab:\n              </div>\n              <div className=\"bg-card border border-border rounded-lg px-3 py-2 text-sm text-foreground/80\">\n                &quot;Find the forgot password link in the login form&quot;\n              </div>\n              <div className=\"flex flex-col gap-1.5\">\n                <div className=\"text-sm text-[#818181]\">\n                  Read{\" \"}\n                  <span className=\"text-[#5b5b5b]\">\n                    components/login-form.tsx\n                  </span>\n                </div>\n                <div className=\"text-sm text-[#818181]\">\n                  Grepped{\" \"}\n                  <span className=\"text-[#5b5b5b]\">forgot password</span>\n                </div>\n                <div className=\"text-sm text-[#818181]\">\n                  Read{\" \"}\n                  <span className=\"text-[#5b5b5b]\">\n                    components/auth/forgot.tsx\n                  </span>\n                </div>\n                <div className=\"text-sm text-[#818181]\">\n                  Read{\" \"}\n                  <span className=\"text-[#5b5b5b]\">\n                    components/ui/field.tsx\n                  </span>\n                </div>\n                <div className=\"text-sm text-[#818181]\">\n                  Grepped{\" \"}\n                  <span className=\"text-[#5b5b5b]\">ml-auto.*password</span>\n                </div>\n              </div>\n              <div className=\"text-xs text-neutral-600 font-mono\">\n                ~13.6s, 5 tool calls, 41.8K tokens\n              </div>\n            </div>\n\n            <div className=\"flex flex-col gap-3\">\n              <div className=\"text-xs font-medium text-foreground/80\">\n                With React Grab:\n              </div>\n              <div className=\"bg-card border border-border rounded-lg overflow-hidden\">\n                <div className=\"px-3 py-2 flex flex-col gap-2\">\n                  <div className=\"text-sm text-foreground/80\">\n                    &quot;Find the forgot password link in the login form&quot;\n                  </div>\n                  <div className=\"font-mono text-xs overflow-x-auto\">\n                    <StaticCodeBlock>\n                      <span className=\"text-neutral-500\">&lt;</span>\n                      <span className=\"text-[#4ec9b0]\">a</span>\n                      <span className=\"text-[#9cdcfe]\"> class</span>\n                      <span className=\"text-neutral-500\">=</span>\n                      <span className=\"text-[#ce9178]\">\n                        &quot;ml-auto inline-block text-...&quot;\n                      </span>\n                      <span className=\"text-[#9cdcfe]\"> href</span>\n                      <span className=\"text-neutral-500\">=</span>\n                      <span className=\"text-[#ce9178]\">&quot;#&quot;</span>\n                      <span className=\"text-neutral-500\">&gt;</span>\n                      {\"\\n  \"}\n                      <span className=\"text-[#ce9178]\">\n                        Forgot your password?\n                      </span>\n                      {\"\\n\"}\n                      <span className=\"text-neutral-500\">&lt;/</span>\n                      <span className=\"text-[#4ec9b0]\">a</span>\n                      <span className=\"text-neutral-500\">&gt;</span>\n                      {\"\\n\"}\n                      <span className=\"text-neutral-500\">in </span>\n                      <span className=\"text-[#dcdcaa]\">LoginForm</span>\n                      <span className=\"text-neutral-500\"> at </span>\n                      <span className=\"text-[#9cdcfe]\">\n                        components/login-form.tsx:46:19\n                      </span>\n                    </StaticCodeBlock>\n                  </div>\n                </div>\n              </div>\n              <div className=\"text-sm text-[#818181]\">\n                Read{\" \"}\n                <span className=\"text-[#5b5b5b]\">\n                  components/login-form.tsx\n                </span>\n              </div>\n              <div className=\"text-xs text-neutral-600 font-mono\">\n                ~6.9s, 1 tool call, 28.1K tokens\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <div className=\"flex flex-col gap-6 text-muted-foreground\">\n        <p>\n          Without React Grab, the agent must search through the codebase to find\n          the right component. Since language models predict tokens\n          non-deterministically, this search process varies dramatically -\n          sometimes finding the target instantly, other times requiring multiple\n          attempts. This unpredictability adds latency, increases token\n          consumption, and degrades overall performance.\n        </p>\n\n        <p>\n          With React Grab, the search phase is eliminated entirely. The\n          component stack with exact file paths and line numbers is embedded\n          directly in the DOM. The agent can jump straight to the correct file\n          and locate what it needs in O(1) time complexity.\n        </p>\n\n        <p>\n          …and turns out, Claude Code becomes ~\n          <span className=\"font-medium text-foreground/80\">\n            3× faster with React Grab\n          </span>\n          !<sup className=\"text-neutral-500 text-[10px] ml-0.5\">3</sup>\n        </p>\n        <div className=\"py-4\">\n          <TimeComparisonChart />\n        </div>\n        <p className=\"text-sm text-neutral-500\">\n          Distribution of edit times across 20 UI tasks. React Grab eliminates\n          the search phase by providing exact file paths and line numbers,\n          letting the agent jump straight to the code.\n        </p>\n      </div>\n\n      <div className=\"-mx-4 sm:-mx-8 lg:mx-0 px-4 sm:px-8 lg:px-0 py-12\">\n        <BenchmarkCharts results={resultsData as BenchmarkResult[]} />\n      </div>\n\n      <div className=\"flex flex-col gap-6 text-muted-foreground mb-16\">\n        <p>\n          Below are the latest measurement results from all 20 test cases. The\n          table below shows a detailed breakdown comparing performance metrics\n          (time, tool calls, tokens) between the control and treatment groups,\n          with speedup percentages indicating how much faster React Grab made\n          the agent for each task.\n        </p>\n      </div>\n\n      <div className=\"w-screen relative left-1/2 -translate-x-1/2 px-4 sm:px-8\">\n        <BenchmarkDetailedTable\n          results={resultsData as BenchmarkResult[]}\n          testCaseMap={testCaseMapping}\n          lastRunDate=\"November 20, 2025 at 12:17 PM\"\n        />\n      </div>\n\n      <div className=\"pt-8\">\n        <div className=\"text-sm text-neutral-500 pt-6\">\n          <p>\n            To run the benchmark yourself, check out the{\" \"}\n            <a\n              href=\"https://github.com/aidenybai/react-bench/tree/2c2702f\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"text-foreground/80 hover:text-foreground underline underline-offset-4\"\n            >\n              benchmarks repository\n            </a>{\" \"}\n            on GitHub.\n          </p>\n        </div>\n      </div>\n\n      <div className=\"pt-16 flex flex-col gap-6 text-muted-foreground\">\n        <div className=\"flex flex-col gap-4\">\n          <h3\n            id=\"how-it-impacts-you\"\n            className=\"text-lg font-medium text-foreground/80 scroll-mt-24\"\n          >\n            How it impacts you\n          </h3>\n          <p>\n            The best use case I&apos;ve seen for React Grab is for low-entropy\n            adjustments like: spacing, layout tweaks, or minor visual changes.\n          </p>\n          <p>\n            If you iterate on UI frequently, this can make everyday changes feel\n            smoother. Instead of describing where the code is, you can select an\n            element and give the agent an exact starting point.\n          </p>\n          <p>\n            React Grab works with{\" \"}\n            <span className=\"font-medium text-foreground/80\">any</span> IDE or\n            coding tool: Cursor, Claude Code, Copilot, Codex, Zed, Windsurf, you\n            name it. At its core, it just adds extra context to your prompt that\n            helps the agent locate the right code faster.\n          </p>\n          <p>\n            We&apos;re finally moves things a bit closer to narrowing the intent\n            to output gap (see{\" \"}\n            <a\n              href=\"https://youtu.be/PUv66718DII?t=390\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"text-foreground/80 hover:text-foreground underline underline-offset-4\"\n            >\n              Inventing on Principle\n            </a>\n            ).\n          </p>\n        </div>\n\n        <div className=\"flex flex-col gap-4 mt-4\">\n          <h3\n            id=\"whats-next\"\n            className=\"text-lg font-medium text-foreground/80 scroll-mt-24\"\n          >\n            What&apos;s next\n          </h3>\n          <p>\n            There are a lot of improvements that can be made to this benchmark:\n          </p>\n          <ul className=\"list-disc list-inside space-y-2 pl-2\">\n            <li>\n              Different codebases (this benchmark used shadcn dashboard) - what\n              happens with different frameworks/sizes/patterns? Need to run it\n              on more repos.\n            </li>\n            <li>Different agents/model providers</li>\n            <li>\n              Multiple trials and sampling - decrease variance, since agents are\n              non-deterministic\n            </li>\n          </ul>\n          <p>\n            On the React Grab side - there&apos;s also a bunch of stuff that\n            could make this even better. For example, grabbing error stack\n            traces when things break, or building a Chrome extension so you\n            don&apos;t need to modify your app at all. Maybe add screenshots of\n            the element you&apos;re grabbing, or capture runtime state/props.\n          </p>\n          <p>\n            If you want to help out or have ideas, hit me up on{\" \"}\n            <a\n              href=\"https://x.com/aidenybai\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"text-foreground/80 hover:text-foreground underline underline-offset-4\"\n            >\n              Twitter\n            </a>{\" \"}\n            or open an issue on GitHub.\n          </p>\n        </div>\n\n        <div className=\"flex flex-col gap-4\">\n          <h3\n            id=\"try-it-out\"\n            className=\"text-lg font-medium text-foreground/80 scroll-mt-24\"\n          >\n            Try it out\n          </h3>\n          <p>\n            React Grab is free and open source.{\" \"}\n            <Link\n              href=\"/\"\n              className=\"text-foreground/80 hover:text-foreground underline underline-offset-4 transition-colors\"\n            >\n              Go try it out!\n            </Link>\n          </p>\n          <div className=\"flex gap-2\">\n            <GithubButton />\n            <ViewDocsButton />\n          </div>\n        </div>\n\n        <div className=\"flex flex-col gap-4 mt-12 pt-8 border-t border-border\">\n          <h4\n            id=\"footnotes\"\n            className=\"text-sm font-medium text-muted-foreground scroll-mt-24\"\n          >\n            Footnotes\n          </h4>\n          <div className=\"flex flex-col gap-4 text-sm text-neutral-500\">\n            <p>\n              <sup className=\"text-neutral-600 mr-1\">1</sup>\n              This only works in development mode. React strips source locations\n              in production builds for performance and bundle size. React Grab\n              detects this and falls back to just showing the component names\n              without file paths. The component tree is still useful for\n              understanding structure, but you lose the direct file references.\n              This only works in production if you have source maps enabled.\n            </p>\n            <p>\n              <sup className=\"text-neutral-600 mr-1\">2</sup>\n              Single trial per test case is a limitation. Agents are\n              non-deterministic, so results can vary significantly between runs.\n              Ideally we&apos;d run each test 5-10 times and report confidence\n              intervals. The 3× speedup is directionally correct but treat the\n              exact number with appropriate skepticism. Future benchmarks will\n              include multiple trials. I&apos;m very open to fixing issues with\n              the benchmarks. If you spot anything off, please{\" \"}\n              <a\n                href=\"mailto:aiden@million.dev\"\n                className=\"text-muted-foreground hover:text-foreground underline underline-offset-4\"\n              >\n                email me\n              </a>{\" \"}\n              or{\" \"}\n              <a\n                href=\"https://x.com/aidenybai\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n                className=\"text-muted-foreground hover:text-foreground underline underline-offset-4\"\n              >\n                DM me on Twitter\n              </a>\n              .\n            </p>\n            <p>\n              <sup className=\"text-neutral-600 mr-1\">3</sup>\n              This is median speedup across all 20 test cases. Some tasks showed\n              80%+ improvement (simple element lookups), others showed minimal\n              gains (complex multi-file changes where search wasn&apos;t the\n              bottleneck). The variance is high. Your mileage will vary\n              depending on codebase size, component nesting depth, and how\n              descriptive your component names are.\n            </p>\n          </div>\n        </div>\n      </div>\n    </BlogArticleLayout>\n  );\n};\n\nBlogPostPage.displayName = \"BlogPostPage\";\n\nexport default BlogPostPage;\n"
  },
  {
    "path": "packages/website/app/blog/layout.tsx",
    "content": "import type { Metadata } from \"next\";\n\nconst title = \"Blog\";\nconst description = \"Read writing and updates about React Grab\";\nconst ogImageUrl = `https://react-grab.com/api/og?title=${encodeURIComponent(title)}&subtitle=${encodeURIComponent(description)}`;\n\nexport const metadata: Metadata = {\n  title: `${title} - React Grab`,\n  description,\n  openGraph: {\n    title: `${title} - React Grab`,\n    description,\n    url: \"https://react-grab.com/blog\",\n    siteName: \"React Grab\",\n    images: [\n      {\n        url: ogImageUrl,\n        width: 1200,\n        height: 630,\n        alt: \"React Grab Blog\",\n      },\n    ],\n    locale: \"en_US\",\n    type: \"website\",\n  },\n  twitter: {\n    card: \"summary_large_image\",\n    title: `${title} - React Grab`,\n    description,\n    images: [ogImageUrl],\n  },\n};\n\ninterface BlogLayoutProps {\n  children: React.ReactNode;\n}\n\nconst BlogLayout = ({ children }: BlogLayoutProps) => {\n  return children;\n};\n\nBlogLayout.displayName = \"BlogLayout\";\n\nexport default BlogLayout;\n"
  },
  {
    "path": "packages/website/app/blog/page.tsx",
    "content": "\"use client\";\n\nimport Image from \"next/image\";\nimport Link from \"next/link\";\nimport { ArrowLeft } from \"lucide-react\";\nimport ReactGrabLogo from \"@/public/logo.svg\";\n\ninterface BlogPost {\n  slug: string;\n  title: string;\n  year: string;\n}\n\nconst blogPosts: BlogPost[] = [\n  {\n    slug: \"1-0\",\n    title: \"React Grab Is Now 1.0\",\n    year: \"2026\",\n  },\n  {\n    slug: \"agent\",\n    title: \"React Grab for Agents\",\n    year: \"2025\",\n  },\n  {\n    slug: \"bets\",\n    title: \"Some bets\",\n    year: \"2025\",\n  },\n  {\n    slug: \"intro\",\n    title: \"I made your coding agent 3× faster at frontend\",\n    year: \"2025\",\n  },\n];\n\nconst BlogPage = () => {\n  return (\n    <div className=\"min-h-screen bg-background px-4 py-6 sm:px-8 sm:py-8\">\n      <div className=\"mx-auto flex w-full max-w-2xl flex-col gap-2 pt-4 text-base sm:pt-8\">\n        <Link\n          href=\"/\"\n          className=\"flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-all mb-4 underline underline-offset-4 opacity-50 hover:opacity-100\"\n        >\n          <ArrowLeft size={16} />\n          Back to home\n        </Link>\n\n        <div className=\"inline-flex\" style={{ padding: \"2px\" }}>\n          <Link href=\"/\" className=\"transition-opacity hover:opacity-80\">\n            <Image\n              src={ReactGrabLogo}\n              alt=\"React Grab\"\n              width={42}\n              height={42}\n              className=\"logo-shimmer-once\"\n            />\n          </Link>\n        </div>\n\n        <div className=\"flex flex-col gap-1\">\n          <div className=\"text-foreground font-bold\">Blog</div>\n          <div className=\"text-sm text-neutral-500\">\n            Posts from the React Grab team\n          </div>\n        </div>\n\n        <div className=\"flex flex-col mt-8\">\n          {blogPosts.map((post, index) => {\n            const showYear =\n              index === 0 || blogPosts[index - 1].year !== post.year;\n\n            return (\n              <div key={post.slug}>\n                {showYear && (\n                  <div className=\"text-neutral-500 text-sm tabular-nums pt-2 pb-1 sm:hidden\">\n                    {post.year}\n                  </div>\n                )}\n                <Link\n                  href={`/blog/${post.slug}`}\n                  className=\"group grid grid-cols-[1fr] sm:grid-cols-[80px_1fr] sm:gap-8 py-2 sm:py-3 sm:-mx-3 sm:px-3 rounded-lg hover:bg-card\"\n                >\n                  <span className=\"hidden sm:block text-neutral-500 text-base tabular-nums\">\n                    {showYear ? post.year : \"\"}\n                  </span>\n                  <span className=\"text-foreground text-sm sm:text-base\">\n                    {post.title}\n                  </span>\n                </Link>\n              </div>\n            );\n          })}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nBlogPage.displayName = \"BlogPage\";\n\nexport default BlogPage;\n"
  },
  {
    "path": "packages/website/app/changelog/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Image from \"next/image\";\nimport Link from \"next/link\";\nimport { ArrowLeft } from \"lucide-react\";\nimport { readFileSync } from \"fs\";\nimport { join } from \"path\";\nimport ReactGrabLogo from \"@/public/logo.svg\";\nimport { parseChangelog } from \"@/utils/parse-changelog\";\n\nconst title = \"Changelog\";\nconst description = \"Release notes and version history for React Grab\";\nconst ogImageUrl = `https://react-grab.com/api/og?title=${encodeURIComponent(title)}&subtitle=${encodeURIComponent(description)}`;\n\nexport const metadata: Metadata = {\n  title: `${title} - React Grab`,\n  description,\n  openGraph: {\n    title: `${title} - React Grab`,\n    description,\n    url: \"https://react-grab.com/changelog\",\n    siteName: \"React Grab\",\n    images: [\n      {\n        url: ogImageUrl,\n        width: 1200,\n        height: 630,\n        alt: `React Grab - ${title}`,\n      },\n    ],\n    locale: \"en_US\",\n    type: \"website\",\n  },\n  twitter: {\n    card: \"summary_large_image\",\n    title: `${title} - React Grab`,\n    description,\n    images: [ogImageUrl],\n  },\n};\n\nconst getChangelog = () => {\n  const changelogPath = join(process.cwd(), \"..\", \"react-grab\", \"CHANGELOG.md\");\n  const content = readFileSync(changelogPath, \"utf-8\");\n  return parseChangelog(content);\n};\n\nconst ChangelogPage = () => {\n  const entries = getChangelog();\n\n  return (\n    <div className=\"min-h-screen bg-background px-4 py-6 sm:px-8 sm:py-8\">\n      <div className=\"mx-auto flex w-full max-w-2xl flex-col gap-2 pt-4 text-base sm:pt-8\">\n        <Link\n          href=\"/\"\n          className=\"flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors mb-4\"\n        >\n          <ArrowLeft size={16} />\n          Back to home\n        </Link>\n\n        <div className=\"inline-flex\" style={{ padding: \"2px\" }}>\n          <Link href=\"/\" className=\"transition-opacity hover:opacity-80\">\n            <Image\n              src={ReactGrabLogo}\n              alt=\"React Grab\"\n              width={42}\n              height={42}\n              className=\"logo-shimmer-once\"\n            />\n          </Link>\n        </div>\n\n        <div className=\"flex flex-col gap-1\">\n          <div className=\"text-foreground font-bold\">Changelog</div>\n          <div className=\"text-sm text-neutral-500\">\n            Release notes and version history\n          </div>\n        </div>\n\n        <div className=\"flex flex-col mt-8 gap-8\">\n          {entries.map((entry) => (\n            <div key={entry.version} className=\"flex flex-col gap-2\">\n              <div className=\"flex items-center gap-3\">\n                <span className=\"text-foreground font-mono text-sm font-medium\">\n                  {entry.version}\n                </span>\n                <span className=\"text-neutral-600 text-xs\">\n                  {entry.changeType}\n                </span>\n              </div>\n              <ul className=\"flex flex-col gap-1.5\">\n                {entry.changes.map((change, changeIndex) => (\n                  <li\n                    key={changeIndex}\n                    className=\"text-muted-foreground text-sm flex items-start gap-2\"\n                  >\n                    <span className=\"text-neutral-600 select-none\">•</span>\n                    <span>{change}</span>\n                  </li>\n                ))}\n              </ul>\n            </div>\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nChangelogPage.displayName = \"ChangelogPage\";\n\nexport default ChangelogPage;\n"
  },
  {
    "path": "packages/website/app/design-system/layout.tsx",
    "content": "import type { Metadata } from \"next\";\n\nconst title = \"Design System\";\nconst description =\n  \"Component gallery and visual reference for React Grab's UI components.\";\nconst ogImageUrl = `https://react-grab.com/api/og?title=${encodeURIComponent(title)}&subtitle=${encodeURIComponent(description)}`;\n\nexport const metadata: Metadata = {\n  title,\n  description,\n  icons: {\n    icon: \"https://react-grab.com/logo.png\",\n    shortcut: \"https://react-grab.com/logo.png\",\n    apple: \"https://react-grab.com/logo.png\",\n  },\n  openGraph: {\n    title,\n    description,\n    url: \"https://react-grab.com/design-system\",\n    siteName: \"React Grab\",\n    images: [\n      {\n        url: ogImageUrl,\n        width: 1200,\n        height: 630,\n        alt: `React Grab - ${title}`,\n      },\n    ],\n    locale: \"en_US\",\n    type: \"website\",\n  },\n  twitter: {\n    card: \"summary_large_image\",\n    title,\n    description,\n    images: [ogImageUrl],\n  },\n  alternates: {\n    canonical: \"https://react-grab.com/design-system\",\n  },\n};\n\ninterface DesignSystemLayoutProps {\n  children: React.ReactNode;\n}\n\nconst DesignSystemLayout = ({ children }: DesignSystemLayoutProps) => {\n  return children;\n};\n\nDesignSystemLayout.displayName = \"DesignSystemLayout\";\n\nexport default DesignSystemLayout;\n"
  },
  {
    "path": "packages/website/app/design-system/page.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useRef, type ReactElement } from \"react\";\n\nconst DesignSystemPage = (): ReactElement => {\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (!containerRef.current) return;\n\n    let dispose: (() => void) | undefined;\n\n    import(\"@react-grab/design-system\").then(\n      ({ renderDesignSystemPreview }) => {\n        if (!containerRef.current) return;\n        dispose = renderDesignSystemPreview(containerRef.current);\n      },\n    );\n\n    return () => {\n      dispose?.();\n    };\n  }, []);\n\n  return (\n    <div className=\"min-h-screen\">\n      <div ref={containerRef} />\n    </div>\n  );\n};\n\nDesignSystemPage.displayName = \"DesignSystemPage\";\n\nexport default DesignSystemPage;\n"
  },
  {
    "path": "packages/website/app/globals.css",
    "content": "@import \"tailwindcss\";\n@import \"tw-animate-css\";\n\n@custom-variant dark (&:is(.dark *));\n\nhtml,\nbody {\n  min-height: 100vh;\n  overflow-x: clip;\n}\n\n@keyframes shimmer {\n  0% {\n    background-position: 200% 0;\n  }\n  100% {\n    background-position: -200% 0;\n  }\n}\n\n.shimmer-text {\n  background: linear-gradient(\n    90deg,\n    #818181 0%,\n    #818181 35%,\n    #ffffff 50%,\n    #818181 65%,\n    #818181 100%\n  );\n  background-size: 150% 100%;\n  background-clip: text;\n  -webkit-background-clip: text;\n  -webkit-text-fill-color: transparent;\n  animation: shimmer 1s ease-in-out infinite;\n}\n\n.shimmer-text-pink {\n  background: linear-gradient(\n    90deg,\n    #f973ff 0%,\n    #ffb3ff 25%,\n    #ffe6ff 50%,\n    #ffb3ff 75%,\n    #f973ff 100%\n  );\n  background-size: 200% 100%;\n  background-clip: text;\n  -webkit-background-clip: text;\n  -webkit-text-fill-color: transparent;\n  animation: shimmer 2.5s linear infinite;\n}\n\n@keyframes logo-shimmer {\n  0% {\n    filter: drop-shadow(0 0 0 rgba(252, 78, 253, 0));\n    transform: scale(1);\n  }\n  50% {\n    filter: drop-shadow(0 0 16px rgba(252, 78, 253, 0.9));\n    transform: scale(1.04);\n  }\n  100% {\n    filter: drop-shadow(0 0 0 rgba(252, 78, 253, 0));\n    transform: scale(1);\n  }\n}\n\n.logo-shimmer-once {\n  animation: logo-shimmer 0.9s ease-out;\n  will-change: transform, filter;\n  transform-origin: center;\n  backface-visibility: hidden;\n  transform: translateZ(0);\n}\n\n@keyframes arrow-bounce {\n  0%,\n  100% {\n    transform: translate(0, 0);\n  }\n  50% {\n    transform: translate(25px, 25px);\n  }\n}\n\n@media (hover: hover) and (pointer: fine) {\n  .logo-shimmer-once:hover {\n    animation: logo-shimmer 0.9s ease-out;\n  }\n\n  .react-grab-logo:hover .react-grab-arrow {\n    animation: arrow-bounce var(--arrow-animation-duration, 400ms) ease-in-out\n      infinite;\n  }\n}\n\n::selection {\n  background: #d75fcb33;\n  color: #d75fcb;\n}\n\n::-moz-selection {\n  background: #d75fcb33;\n  color: #d75fcb;\n}\n\n.highlighted-code .line-changed {\n  background-color: rgba(34, 197, 94, 0.15);\n  border-left: 1px solid rgb(34, 197, 94, 0.75);\n  padding-left: 0.5rem;\n  display: inline-block;\n  width: 100%;\n  box-sizing: border-box;\n}\n\n* {\n  scrollbar-width: thin;\n  scrollbar-color: oklch(0.3 0 0) oklch(0 0 0);\n}\n\n.dark *::-webkit-scrollbar {\n  width: 8px;\n  height: 8px;\n}\n\n.dark *::-webkit-scrollbar-track {\n  background: oklch(0 0 0);\n}\n\n.dark *::-webkit-scrollbar-thumb {\n  background-color: oklch(0.3 0 0);\n  border-radius: 4px;\n}\n\n.dark *::-webkit-scrollbar-thumb:hover {\n  background-color: oklch(0.4 0 0);\n}\n\n@keyframes fill-bar {\n  from {\n    width: 0%;\n  }\n  to {\n    width: var(--target-width);\n  }\n}\n\n.animate-fill-bar {\n  animation: fill-bar linear forwards;\n}\n\n@keyframes rainbow {\n  0% {\n    background-position: 0% 50%;\n  }\n  50% {\n    background-position: 100% 50%;\n  }\n  100% {\n    background-position: 0% 50%;\n  }\n}\n\n@keyframes rainbow-glow {\n  0%,\n  100% {\n    box-shadow:\n      0 0 20px rgba(255, 0, 0, 0.5),\n      0 0 40px rgba(255, 127, 0, 0.4),\n      0 0 60px rgba(255, 255, 0, 0.3);\n  }\n  16.66% {\n    box-shadow:\n      0 0 20px rgba(255, 127, 0, 0.5),\n      0 0 40px rgba(255, 255, 0, 0.4),\n      0 0 60px rgba(0, 255, 0, 0.3);\n  }\n  33.33% {\n    box-shadow:\n      0 0 20px rgba(255, 255, 0, 0.5),\n      0 0 40px rgba(0, 255, 0, 0.4),\n      0 0 60px rgba(0, 255, 127, 0.3);\n  }\n  50% {\n    box-shadow:\n      0 0 20px rgba(0, 255, 0, 0.5),\n      0 0 40px rgba(0, 255, 127, 0.4),\n      0 0 60px rgba(0, 127, 255, 0.3);\n  }\n  66.66% {\n    box-shadow:\n      0 0 20px rgba(0, 255, 127, 0.5),\n      0 0 40px rgba(0, 127, 255, 0.4),\n      0 0 60px rgba(127, 0, 255, 0.3);\n  }\n  83.33% {\n    box-shadow:\n      0 0 20px rgba(0, 127, 255, 0.5),\n      0 0 40px rgba(127, 0, 255, 0.4),\n      0 0 60px rgba(255, 0, 127, 0.3);\n  }\n}\n\n@keyframes rainbow-pulse {\n  0%,\n  100% {\n    transform: scale(1);\n  }\n  50% {\n    transform: scale(1.05);\n  }\n}\n\n.rainbow-button {\n  background: linear-gradient(\n    90deg,\n    #ff0000 0%,\n    #ff7f00 14.28%,\n    #ffff00 28.56%,\n    #00ff00 42.84%,\n    #007fff 57.12%,\n    #7f00ff 71.4%,\n    #ff0080 85.68%,\n    #ff0000 100%\n  );\n  background-size: 200% 100%;\n  animation:\n    rainbow 3s linear infinite,\n    rainbow-glow 2s ease-in-out infinite,\n    rainbow-pulse 2s ease-in-out infinite;\n  border: 2px solid transparent;\n  position: relative;\n  overflow: hidden;\n  will-change: transform, box-shadow, background-position;\n}\n\n.rainbow-button::before {\n  content: \"\";\n  position: absolute;\n  top: -2px;\n  left: -2px;\n  right: -2px;\n  bottom: -2px;\n  background: linear-gradient(\n    90deg,\n    #ff0000 0%,\n    #ff7f00 14.28%,\n    #ffff00 28.56%,\n    #00ff00 42.84%,\n    #007fff 57.12%,\n    #7f00ff 71.4%,\n    #ff0080 85.68%,\n    #ff0000 100%\n  );\n  background-size: 200% 100%;\n  animation: rainbow 3s linear infinite;\n  z-index: -1;\n  border-radius: inherit;\n  filter: blur(8px);\n  opacity: 0.7;\n}\n\n@media (hover: hover) and (pointer: fine) {\n  .rainbow-button:hover {\n    animation-duration: 1.5s, 1s, 1.5s;\n    transform: scale(1.08);\n  }\n}\n\n@theme inline {\n  --radius-sm: calc(var(--radius) - 4px);\n  --radius-md: calc(var(--radius) - 2px);\n  --radius-lg: var(--radius);\n  --radius-xl: calc(var(--radius) + 4px);\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --color-card: var(--card);\n  --color-card-foreground: var(--card-foreground);\n  --color-popover: var(--popover);\n  --color-popover-foreground: var(--popover-foreground);\n  --color-primary: var(--primary);\n  --color-primary-foreground: var(--primary-foreground);\n  --color-secondary: var(--secondary);\n  --color-secondary-foreground: var(--secondary-foreground);\n  --color-muted: var(--muted);\n  --color-muted-foreground: var(--muted-foreground);\n  --color-accent: var(--accent);\n  --color-accent-foreground: var(--accent-foreground);\n  --color-destructive: var(--destructive);\n  --color-border: var(--border);\n  --color-input: var(--input);\n  --color-ring: var(--ring);\n  --color-chart-1: var(--chart-1);\n  --color-chart-2: var(--chart-2);\n  --color-chart-3: var(--chart-3);\n  --color-chart-4: var(--chart-4);\n  --color-chart-5: var(--chart-5);\n  --color-sidebar: var(--sidebar);\n  --color-sidebar-foreground: var(--sidebar-foreground);\n  --color-sidebar-primary: var(--sidebar-primary);\n  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n  --color-sidebar-accent: var(--sidebar-accent);\n  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n  --color-sidebar-border: var(--sidebar-border);\n  --color-sidebar-ring: var(--sidebar-ring);\n}\n\n:root {\n  --radius: 0.625rem;\n  --background: oklch(1 0 0);\n  --foreground: oklch(0.145 0 0);\n  --card: oklch(1 0 0);\n  --card-foreground: oklch(0.145 0 0);\n  --popover: oklch(1 0 0);\n  --popover-foreground: oklch(0.145 0 0);\n  --primary: oklch(0.205 0 0);\n  --primary-foreground: oklch(0.985 0 0);\n  --secondary: oklch(0.97 0 0);\n  --secondary-foreground: oklch(0.205 0 0);\n  --muted: oklch(0.97 0 0);\n  --muted-foreground: oklch(0.556 0 0);\n  --accent: oklch(0.97 0 0);\n  --accent-foreground: oklch(0.205 0 0);\n  --destructive: oklch(0.577 0.245 27.325);\n  --border: oklch(0.922 0 0);\n  --input: oklch(0.922 0 0);\n  --ring: oklch(0.708 0 0);\n  --chart-1: oklch(0.646 0.222 41.116);\n  --chart-2: oklch(0.6 0.118 184.704);\n  --chart-3: oklch(0.398 0.07 227.392);\n  --chart-4: oklch(0.828 0.189 84.429);\n  --chart-5: oklch(0.769 0.188 70.08);\n  --sidebar: oklch(0.985 0 0);\n  --sidebar-foreground: oklch(0.145 0 0);\n  --sidebar-primary: oklch(0.205 0 0);\n  --sidebar-primary-foreground: oklch(0.985 0 0);\n  --sidebar-accent: oklch(0.97 0 0);\n  --sidebar-accent-foreground: oklch(0.205 0 0);\n  --sidebar-border: oklch(0.922 0 0);\n  --sidebar-ring: oklch(0.708 0 0);\n}\n\n.dark {\n  --background: oklch(0 0 0);\n  --foreground: oklch(1 0 0);\n  --card: oklch(0.159 0 0);\n  --card-foreground: oklch(1 0 0);\n  --popover: oklch(0.218 0 0);\n  --popover-foreground: oklch(1 0 0);\n  --primary: oklch(1 0 0);\n  --primary-foreground: oklch(0 0 0);\n  --secondary: oklch(0.218 0 0);\n  --secondary-foreground: oklch(1 0 0);\n  --muted: oklch(0.218 0 0);\n  --muted-foreground: oklch(0.556 0 0);\n  --accent: oklch(0.248 0 0);\n  --accent-foreground: oklch(1 0 0);\n  --destructive: oklch(0.704 0.191 22.216);\n  --border: oklch(1 0 0 / 12%);\n  --input: oklch(1 0 0 / 15%);\n  --ring: oklch(0.556 0 0);\n  --chart-1: oklch(0.488 0.243 264.376);\n  --chart-2: oklch(0.696 0.17 162.48);\n  --chart-3: oklch(0.769 0.188 70.08);\n  --chart-4: oklch(0.627 0.265 303.9);\n  --chart-5: oklch(0.645 0.246 16.439);\n  --sidebar: oklch(0.159 0 0);\n  --sidebar-foreground: oklch(1 0 0);\n  --sidebar-primary: oklch(0.488 0.243 264.376);\n  --sidebar-primary-foreground: oklch(1 0 0);\n  --sidebar-accent: oklch(0.218 0 0);\n  --sidebar-accent-foreground: oklch(1 0 0);\n  --sidebar-border: oklch(1 0 0 / 12%);\n  --sidebar-ring: oklch(0.556 0 0);\n}\n\n@media (prefers-reduced-motion: reduce) {\n  *,\n  *::before,\n  *::after {\n    animation-duration: 0.01ms !important;\n    animation-iteration-count: 1 !important;\n    transition-duration: 0.01ms !important;\n    scroll-behavior: auto !important;\n  }\n}\n\n.touch-hitbox {\n  position: relative;\n}\n\n.touch-hitbox::before {\n  content: \"\";\n  position: absolute;\n  display: block;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n  width: 100%;\n  height: 100%;\n  min-height: 44px;\n  min-width: 44px;\n}\n\n@layer base {\n  * {\n    @apply border-border outline-ring/50;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n  button:not(:disabled),\n  [role=\"button\"]:not(:disabled) {\n    cursor: pointer;\n  }\n}\n\n[data-react-grab] {\n  @media (max-width: 767px) {\n    display: none !important;\n  }\n}\n"
  },
  {
    "path": "packages/website/app/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { Analytics } from \"@vercel/analytics/react\";\nimport { NuqsAdapter } from \"nuqs/adapters/next/app\";\nimport { Caveat, Geist, Geist_Mono } from \"next/font/google\";\nimport \"./globals.css\";\n\nconst geistSans = Geist({\n  variable: \"--font-geist-sans\",\n  subsets: [\"latin\"],\n  display: \"swap\",\n  preload: true,\n});\n\nconst caveat = Caveat({\n  variable: \"--font-caveat\",\n  subsets: [\"latin\"],\n  display: \"swap\",\n  preload: false,\n});\n\nconst geistMono = Geist_Mono({\n  variable: \"--font-geist-mono\",\n  subsets: [\"latin\"],\n  display: \"swap\",\n  preload: true,\n});\n\nexport const metadata: Metadata = {\n  title: \"React Grab\",\n  description:\n    \"Select an element → Give it to Cursor, Claude Code, etc → Make a change to your app\",\n  icons: {\n    icon: \"https://react-grab.com/logo.png\",\n    shortcut: \"https://react-grab.com/logo.png\",\n    apple: \"https://react-grab.com/logo.png\",\n  },\n  openGraph: {\n    images: \"https://react-grab.com/banner.png\",\n    title: \"React Grab\",\n    description:\n      \"Select an element → Give it to Cursor, Claude Code, etc → Make a change to your app\",\n    url: \"https://react-grab.com\",\n    siteName: \"React Grab\",\n    locale: \"en_US\",\n    type: \"website\",\n  },\n  twitter: {\n    card: \"summary_large_image\",\n    title: \"React Grab\",\n    description:\n      \"Select an element → Give it to Cursor, Claude Code, etc → Make a change to your app\",\n    images: \"https://react-grab.com/banner.png\",\n  },\n};\n\nconst RootLayout = ({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) => {\n  return (\n    <html lang=\"en\" className=\"dark\">\n      <body\n        className={`${geistSans.variable} ${geistMono.variable} ${caveat.variable} antialiased`}\n      >\n        <NuqsAdapter>{children}</NuqsAdapter>\n        <Analytics />\n      </body>\n    </html>\n  );\n};\n\nRootLayout.displayName = \"RootLayout\";\n\nexport default RootLayout;\n"
  },
  {
    "path": "packages/website/app/not-found.tsx",
    "content": "import Link from \"next/link\";\nimport { ArrowLeft } from \"lucide-react\";\nimport { ReactGrabLogo } from \"@/components/react-grab-logo\";\n\nconst NotFound = () => {\n  return (\n    <div className=\"min-h-screen bg-background px-4 py-6 sm:px-8 sm:py-8\">\n      <div className=\"mx-auto flex w-full max-w-2xl flex-col gap-2 pt-4 text-base sm:pt-8 sm:text-lg\">\n        <Link\n          href=\"/\"\n          className=\"flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-all mb-4 underline underline-offset-4 opacity-50 hover:opacity-100\"\n        >\n          <ArrowLeft size={16} />\n          Back to home\n        </Link>\n\n        <div className=\"inline-flex\" style={{ padding: \"2px\" }}>\n          <Link href=\"/\" className=\"transition-opacity hover:opacity-80\">\n            <ReactGrabLogo\n              width={42}\n              height={42}\n              className=\"logo-shimmer-once\"\n            />\n          </Link>\n        </div>\n\n        <div className=\"text-foreground mt-4\">\n          <span className=\"font-bold shimmer-text-pink\">404</span> &middot;\n          Couldn&apos;t grab this page.\n        </div>\n      </div>\n    </div>\n  );\n};\n\nNotFound.displayName = \"NotFound\";\n\nexport default NotFound;\n"
  },
  {
    "path": "packages/website/app/open-file/layout.tsx",
    "content": "import type { Metadata } from \"next\";\n\nconst title = \"Open File\";\nconst description = \"Open a file in your preferred editor\";\nconst ogImageUrl = `https://react-grab.com/api/og?title=${encodeURIComponent(title)}&subtitle=${encodeURIComponent(description)}`;\n\nexport const metadata: Metadata = {\n  title: `${title} | React Grab`,\n  description,\n  openGraph: {\n    title: `${title} | React Grab`,\n    description,\n    url: \"https://react-grab.com/open-file\",\n    siteName: \"React Grab\",\n    images: [\n      {\n        url: ogImageUrl,\n        width: 1200,\n        height: 630,\n        alt: `React Grab - ${title}`,\n      },\n    ],\n    locale: \"en_US\",\n    type: \"website\",\n  },\n  twitter: {\n    card: \"summary_large_image\",\n    title: `${title} | React Grab`,\n    description,\n    images: [ogImageUrl],\n  },\n};\n\ninterface OpenFileLayoutProps {\n  children: React.ReactNode;\n}\n\nconst OpenFileLayout = ({ children }: OpenFileLayoutProps) => {\n  return children;\n};\n\nOpenFileLayout.displayName = \"OpenFileLayout\";\n\nexport default OpenFileLayout;\n"
  },
  {
    "path": "packages/website/app/open-file/page.tsx",
    "content": "\"use client\";\n\nimport { useQueryState, parseAsStringLiteral } from \"nuqs\";\nimport { useState, useEffect, useCallback, Suspense, useRef } from \"react\";\nimport { ReactGrabLogo } from \"@/components/react-grab-logo\";\nimport { cn } from \"@/utils/cn\";\nimport { IconCursor } from \"@/components/icons/icon-cursor\";\nimport { IconVSCode } from \"@/components/icons/icon-vscode\";\nimport { IconZed } from \"@/components/icons/icon-zed\";\nimport { IconWebStorm } from \"@/components/icons/icon-webstorm\";\nimport { ChevronDown, ArrowUpRight } from \"lucide-react\";\nimport Link from \"next/link\";\nimport { Button } from \"@/components/ui/button\";\n\nconst EDITOR_OPTIONS = [\"cursor\", \"vscode\", \"zed\", \"webstorm\"] as const;\ntype Editor = (typeof EDITOR_OPTIONS)[number];\n\ninterface EditorOption {\n  id: Editor;\n  name: string;\n  icon: React.ReactNode;\n}\n\nconst EDITORS: EditorOption[] = [\n  { id: \"cursor\", name: \"Cursor\", icon: <IconCursor width={16} height={16} /> },\n  { id: \"vscode\", name: \"VS Code\", icon: <IconVSCode /> },\n  { id: \"zed\", name: \"Zed\", icon: <IconZed /> },\n  { id: \"webstorm\", name: \"WebStorm\", icon: <IconWebStorm /> },\n];\n\nconst STORAGE_KEY = \"react-grab-preferred-editor\";\n\nconst getEditorUrl = (\n  editor: Editor,\n  filePath: string,\n  lineNumber?: number,\n): string => {\n  if (editor === \"webstorm\") {\n    const lineParam = lineNumber ? `&line=${lineNumber}` : \"\";\n    return `webstorm://open?file=${filePath}${lineParam}`;\n  }\n\n  const lineParam = lineNumber ? `:${lineNumber}` : \"\";\n  return `${editor}://file/${filePath}${lineParam}`;\n};\n\nconst OpenFileContent = () => {\n  const [filePath] = useQueryState(\"url\");\n  const [filePathAlt] = useQueryState(\"file\");\n  const [lineNumber] = useQueryState(\"line\");\n  const [editorParam, setEditorParam] = useQueryState(\n    \"editor\",\n    parseAsStringLiteral(EDITOR_OPTIONS),\n  );\n  const [rawParam] = useQueryState(\"raw\");\n\n  const resolvedFilePath = filePath ?? filePathAlt ?? \"\";\n\n  const getInitialEditor = (): { editor: Editor; hasSaved: boolean } => {\n    if (typeof window === \"undefined\")\n      return { editor: \"cursor\", hasSaved: false };\n    const params = new URLSearchParams(window.location.search);\n    if (params.has(\"raw\")) return { editor: \"cursor\", hasSaved: false };\n    const saved = localStorage.getItem(STORAGE_KEY);\n    if (saved && EDITORS.some((e) => e.id === saved)) {\n      return { editor: saved as Editor, hasSaved: true };\n    }\n    return { editor: \"cursor\", hasSaved: false };\n  };\n\n  const [preferredEditor, setPreferredEditor] = useState<Editor>(() => {\n    if (editorParam && EDITORS.some((e) => e.id === editorParam))\n      return editorParam;\n    return getInitialEditor().editor;\n  });\n  const [didAttemptOpen, setDidAttemptOpen] = useState(false);\n  const [isDropdownOpen, setIsDropdownOpen] = useState(false);\n  const [hasSavedPreference] = useState(() => getInitialEditor().hasSaved);\n  const [isInfoOpen, setIsInfoOpen] = useState(false);\n  const dropdownRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const handleClickOutside = (event: MouseEvent) => {\n      if (\n        dropdownRef.current &&\n        !dropdownRef.current.contains(event.target as Node)\n      ) {\n        setIsDropdownOpen(false);\n      }\n    };\n\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => document.removeEventListener(\"mousedown\", handleClickOutside);\n  }, []);\n\n  const handleOpen = useCallback(() => {\n    if (!resolvedFilePath) return;\n\n    const url = getEditorUrl(\n      preferredEditor,\n      resolvedFilePath,\n      lineNumber ? parseInt(lineNumber, 10) : undefined,\n    );\n    window.location.href = url;\n    setDidAttemptOpen(true);\n  }, [resolvedFilePath, preferredEditor, lineNumber]);\n\n  useEffect(() => {\n    if (resolvedFilePath && !didAttemptOpen && hasSavedPreference) {\n      const timer = setTimeout(() => {\n        handleOpen();\n      }, 300);\n      return () => clearTimeout(timer);\n    }\n  }, [resolvedFilePath, didAttemptOpen, handleOpen, hasSavedPreference]);\n\n  useEffect(() => {\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (event.key === \"Enter\" && !isDropdownOpen) {\n        handleOpen();\n      }\n    };\n\n    document.addEventListener(\"keydown\", handleKeyDown);\n    return () => document.removeEventListener(\"keydown\", handleKeyDown);\n  }, [handleOpen, isDropdownOpen]);\n\n  const handleEditorChange = (editor: Editor) => {\n    setPreferredEditor(editor);\n    if (!rawParam) {\n      localStorage.setItem(STORAGE_KEY, editor);\n    }\n    setEditorParam(editor);\n    setIsDropdownOpen(false);\n  };\n\n  const fileName = resolvedFilePath.split(\"/\").pop() ?? \"file\";\n  const selectedEditor = EDITORS.find((e) => e.id === preferredEditor);\n\n  if (!resolvedFilePath) {\n    return (\n      <div className=\"flex min-h-screen items-center justify-center bg-background p-4\">\n        <div className=\"w-full max-w-md rounded-lg border border-border bg-card p-8 text-center shadow-lg\">\n          <div className=\"mb-6 flex justify-center\">\n            <ReactGrabLogo width={100} height={40} />\n          </div>\n          <div className=\"text-muted-foreground text-sm\">\n            No file specified. Add{\" \"}\n            <code className=\"rounded bg-muted px-1.5 py-0.5 font-mono text-xs\">\n              ?url=path/to/file\n            </code>{\" \"}\n            to the URL.\n          </div>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex min-h-screen flex-col items-center justify-center bg-background p-4\">\n      <div className=\"mb-8\">\n        <Link href=\"/\">\n          <ReactGrabLogo\n            width={160}\n            height={60}\n            className=\"logo-shimmer-once\"\n          />\n        </Link>\n      </div>\n\n      <div className=\"w-full max-w-lg rounded-lg border border-border bg-card p-8 shadow-lg\">\n        <div className=\"mb-2 flex flex-wrap items-center gap-2 text-lg text-foreground/80\">\n          <span>Opening</span>\n          <span className=\"inline-flex items-center rounded bg-muted px-2 py-0.5 font-mono text-sm text-foreground/80\">\n            {fileName}\n          </span>\n          {lineNumber && (\n            <>\n              <span>at line</span>\n              <span className=\"inline-flex items-center rounded bg-muted px-2 py-0.5 font-mono text-sm text-foreground/80\">\n                {lineNumber}\n              </span>\n            </>\n          )}\n        </div>\n\n        <div className=\"mb-6 break-all font-mono text-sm text-muted-foreground\">\n          {resolvedFilePath}\n        </div>\n\n        <div className=\"mb-6 inline-flex items-stretch rounded-lg border border-border bg-muted/50\">\n          <div className=\"relative\" ref={dropdownRef}>\n            <Button\n              type=\"button\"\n              variant=\"ghost\"\n              onClick={() => setIsDropdownOpen(!isDropdownOpen)}\n              className=\"h-auto rounded-l-lg rounded-r-none px-4 py-2.5\"\n            >\n              <span className=\"opacity-70\">{selectedEditor?.icon}</span>\n              <span>{selectedEditor?.name}</span>\n              <ChevronDown\n                size={14}\n                className={cn(\n                  \"opacity-40 transition-transform\",\n                  isDropdownOpen && \"rotate-180\",\n                )}\n              />\n            </Button>\n\n            {isDropdownOpen && (\n              <div className=\"absolute left-0 top-full z-10 mt-1 min-w-[160px] overflow-hidden rounded-lg border border-border bg-card shadow-lg\">\n                {EDITORS.map((editor) => (\n                  <Button\n                    key={editor.id}\n                    type=\"button\"\n                    variant=\"ghost\"\n                    onClick={() => handleEditorChange(editor.id)}\n                    className={cn(\n                      \"h-auto w-full justify-start rounded-none gap-2.5 px-4 py-2.5\",\n                      preferredEditor === editor.id\n                        ? \"bg-muted text-foreground\"\n                        : \"text-muted-foreground hover:bg-muted hover:text-foreground/80\",\n                    )}\n                  >\n                    <span className=\"opacity-70\">{editor.icon}</span>\n                    <span>{editor.name}</span>\n                  </Button>\n                ))}\n              </div>\n            )}\n          </div>\n\n          <div className=\"w-px bg-border\" />\n\n          <Button\n            type=\"button\"\n            variant=\"ghost\"\n            onClick={handleOpen}\n            className=\"h-auto rounded-l-none rounded-r-lg px-4 py-2.5\"\n          >\n            <span>Open</span>\n            <ArrowUpRight size={14} className=\"opacity-50\" />\n          </Button>\n        </div>\n\n        <div className=\"space-y-1 text-xs text-muted-foreground\">\n          <p>Your preference will be saved for future use.</p>\n          <p>Only open files from trusted sources.</p>\n        </div>\n      </div>\n\n      <Button\n        type=\"button\"\n        variant=\"ghost\"\n        size=\"sm\"\n        onClick={() => setIsInfoOpen(!isInfoOpen)}\n        className=\"mt-8 gap-1.5 text-muted-foreground/50 hover:text-muted-foreground\"\n      >\n        <span>What is React Grab?</span>\n        <ChevronDown\n          size={10}\n          className={cn(\"transition-transform\", isInfoOpen && \"rotate-180\")}\n        />\n      </Button>\n\n      {isInfoOpen && (\n        <p className=\"mt-2 text-center text-xs text-muted-foreground/60\">\n          Select any element in your React app and copy its context to AI tools.{\" \"}\n          <Link href=\"/\" className=\"underline hover:text-muted-foreground\">\n            Learn more\n          </Link>\n        </p>\n      )}\n    </div>\n  );\n};\n\nconst OpenFilePage = () => {\n  return (\n    <Suspense\n      fallback={\n        <div className=\"flex min-h-screen items-center justify-center bg-background p-4\">\n          <ReactGrabLogo width={160} height={60} className=\"animate-pulse\" />\n        </div>\n      }\n    >\n      <OpenFileContent />\n    </Suspense>\n  );\n};\n\nOpenFilePage.displayName = \"OpenFilePage\";\n\nexport default OpenFilePage;\n"
  },
  {
    "path": "packages/website/app/page.tsx",
    "content": "import { HomepageDemo } from \"@/components/homepage-demo\";\n\nconst Home = () => {\n  return <HomepageDemo />;\n};\n\nHome.displayName = \"Home\";\n\nexport default Home;\n"
  },
  {
    "path": "packages/website/app/privacy/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Link from \"next/link\";\nimport { ArrowLeft } from \"lucide-react\";\nimport { ReactGrabLogo } from \"@/components/react-grab-logo\";\n\nconst title = \"Privacy Policy\";\nconst description =\n  \"Privacy policy for React Grab browser extension and website\";\nconst ogImageUrl = `https://react-grab.com/api/og?title=${encodeURIComponent(title)}&subtitle=${encodeURIComponent(description)}`;\n\nexport const metadata: Metadata = {\n  title: `${title} - React Grab`,\n  description,\n  openGraph: {\n    title: `${title} - React Grab`,\n    description,\n    url: \"https://react-grab.com/privacy\",\n    siteName: \"React Grab\",\n    images: [\n      {\n        url: ogImageUrl,\n        width: 1200,\n        height: 630,\n        alt: `React Grab - ${title}`,\n      },\n    ],\n    locale: \"en_US\",\n    type: \"website\",\n  },\n  twitter: {\n    card: \"summary_large_image\",\n    title: `${title} - React Grab`,\n    description,\n    images: [ogImageUrl],\n  },\n};\n\nconst PrivacyPage = () => {\n  return (\n    <div className=\"min-h-screen bg-background px-4 py-6 sm:px-8 sm:py-8\">\n      <div className=\"mx-auto flex w-full max-w-2xl flex-col gap-2 pt-4 text-base sm:pt-8 sm:text-lg\">\n        <Link\n          href=\"/\"\n          className=\"flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-all mb-4 underline underline-offset-4 opacity-50 hover:opacity-100\"\n        >\n          <ArrowLeft size={16} />\n          Back to home\n        </Link>\n\n        <div className=\"inline-flex\" style={{ padding: \"2px\" }}>\n          <Link href=\"/\" className=\"transition-opacity hover:opacity-80\">\n            <ReactGrabLogo\n              width={42}\n              height={42}\n              className=\"logo-shimmer-once\"\n            />\n          </Link>\n        </div>\n\n        <div className=\"text-foreground mt-4\">\n          <h1 className=\"font-bold inline\">Privacy Policy</h1> &middot; Last\n          updated{\" \"}\n          {new Date().toLocaleDateString(\"en-US\", {\n            year: \"numeric\",\n            month: \"long\",\n            day: \"numeric\",\n          })}\n        </div>\n\n        <div className=\"space-y-6 text-neutral-300 mt-4\">\n          <section>\n            <h2 className=\"text-foreground font-bold mb-2\">Overview</h2>\n            <p>\n              React Grab is a developer tool that helps you inspect and copy\n              React components from web pages. This privacy policy explains how\n              the React Grab browser extension and website handle your data.\n            </p>\n          </section>\n\n          <section>\n            <p className=\"text-foreground font-bold mb-2\">Data Collection</p>\n            <p className=\"mb-2\">\n              React Grab does NOT collect, store, or transmit any personal data.\n              Specifically:\n            </p>\n            <ul className=\"list-disc pl-6 space-y-1\">\n              <li>We do not collect any personally identifiable information</li>\n              <li>We do not track your browsing history</li>\n              <li>We do not store any data about the websites you visit</li>\n              <li>We do not use analytics or tracking services</li>\n              <li>We do not use cookies for tracking purposes</li>\n            </ul>\n          </section>\n\n          <section>\n            <p className=\"text-foreground font-bold mb-2\">\n              How React Grab Works\n            </p>\n            <p className=\"mb-2\">\n              React Grab operates entirely locally in your browser. When you use\n              the extension:\n            </p>\n            <ul className=\"list-disc pl-6 space-y-1\">\n              <li>\n                The extension injects code into web pages to enable element\n                selection\n              </li>\n              <li>\n                When you select an element, the HTML/JSX is copied to your\n                clipboard locally\n              </li>\n              <li>No data is sent to external servers</li>\n              <li>All processing happens on your device</li>\n            </ul>\n          </section>\n\n          <section>\n            <p className=\"text-foreground font-bold mb-2\">Permissions</p>\n            <p className=\"mb-2\">\n              The extension requires the following permissions:\n            </p>\n            <ul className=\"list-disc pl-6 space-y-1\">\n              <li>\n                <span className=\"text-foreground\">Access to all websites:</span>{\" \"}\n                Required to inject the element selection functionality into any\n                webpage you visit.\n              </li>\n              <li>\n                <span className=\"text-foreground\">Storage:</span> Used only to\n                store your extension preferences locally on your device.\n              </li>\n              <li>\n                <span className=\"text-foreground\">Active Tab:</span> Needed to\n                interact with the currently active tab when you use the keyboard\n                shortcut.\n              </li>\n            </ul>\n            <p className=\"mt-2\">\n              These permissions are used solely for the core functionality of\n              the extension and are not used to collect or transmit any data.\n            </p>\n          </section>\n\n          <section>\n            <p className=\"text-foreground font-bold mb-2\">Local Storage</p>\n            <p>\n              React Grab may store minimal settings locally on your device using\n              browser storage APIs. This data never leaves your device and can\n              be cleared by uninstalling the extension or clearing your browser\n              data.\n            </p>\n          </section>\n\n          <section>\n            <p className=\"text-foreground font-bold mb-2\">\n              Third-Party Services\n            </p>\n            <p>\n              React Grab does not integrate with any third-party analytics,\n              tracking, or advertising services. The extension operates entirely\n              offline and does not make any external network requests.\n            </p>\n          </section>\n\n          <section>\n            <p className=\"text-foreground font-bold mb-2\">Open Source</p>\n            <p>\n              React Grab is open source software. You can review the complete\n              source code on{\" \"}\n              <a\n                href=\"https://github.com/aidenybai/react-grab\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n                className=\"text-foreground underline underline-offset-4 hover:opacity-80 transition-opacity\"\n              >\n                GitHub\n              </a>{\" \"}\n              to verify these privacy claims.\n            </p>\n          </section>\n\n          <section>\n            <p className=\"text-foreground font-bold mb-2\">\n              Changes to This Policy\n            </p>\n            <p>\n              We may update this privacy policy from time to time. Any changes\n              will be posted on this page with an updated revision date.\n            </p>\n          </section>\n\n          <section>\n            <p className=\"text-foreground font-bold mb-2\">Contact</p>\n            <p>\n              If you have questions about this privacy policy, please open an\n              issue on our{\" \"}\n              <a\n                href=\"https://github.com/aidenybai/react-grab/issues\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n                className=\"text-foreground underline underline-offset-4 hover:opacity-80 transition-opacity\"\n              >\n                GitHub repository\n              </a>{\" \"}\n              or join our{\" \"}\n              <a\n                href=\"https://discord.com/invite/G7zxfUzkm7\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n                className=\"text-foreground underline underline-offset-4 hover:opacity-80 transition-opacity\"\n              >\n                Discord community\n              </a>\n              .\n            </p>\n          </section>\n\n          <section className=\"pt-6 border-t border-border\">\n            <p className=\"text-foreground font-bold mb-2\">Summary</p>\n            <p>\n              React Grab respects your privacy. We don&apos;t collect, store, or\n              transmit any of your personal data. The extension works entirely\n              locally on your device.\n            </p>\n          </section>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nPrivacyPage.displayName = \"PrivacyPage\";\n\nexport default PrivacyPage;\n"
  },
  {
    "path": "packages/website/app/robots.ts",
    "content": "import type { MetadataRoute } from \"next\";\n\nconst robots = (): MetadataRoute.Robots => {\n  return {\n    rules: {\n      userAgent: \"*\",\n      allow: \"/\",\n      disallow: [\"/api\"],\n    },\n    sitemap: \"https://react-grab.com/sitemap.xml\",\n  };\n};\n\nexport default robots;\n"
  },
  {
    "path": "packages/website/app/sitemap.ts",
    "content": "import type { MetadataRoute } from \"next\";\nimport { readdirSync, statSync } from \"fs\";\nimport { join } from \"path\";\n\nconst BASE_URL = \"https://react-grab.com\";\n\nconst EXCLUDED_PATHS = new Set([\"api\", \"open-file\"]);\n\nconst getRoutes = (directory: string, basePath = \"\"): Array<string> => {\n  const routes: Array<string> = [];\n  const entries = readdirSync(directory);\n\n  for (const entry of entries) {\n    const fullPath = join(directory, entry);\n    const routePath = basePath ? `${basePath}/${entry}` : entry;\n\n    if (EXCLUDED_PATHS.has(entry)) {\n      continue;\n    }\n\n    const stat = statSync(fullPath);\n\n    if (stat.isDirectory()) {\n      const hasPage = readdirSync(fullPath).some(\n        (file) => file === \"page.tsx\" || file === \"page.ts\",\n      );\n\n      if (hasPage) {\n        routes.push(routePath);\n      }\n\n      routes.push(...getRoutes(fullPath, routePath));\n    }\n  }\n\n  return routes;\n};\n\nconst sitemap = (): MetadataRoute.Sitemap => {\n  const appDirectory = join(process.cwd(), \"app\");\n  const routes = getRoutes(appDirectory);\n\n  const sitemapEntries: MetadataRoute.Sitemap = [\n    {\n      url: BASE_URL,\n      lastModified: new Date(),\n      changeFrequency: \"weekly\",\n      priority: 1,\n    },\n  ];\n\n  for (const route of routes) {\n    const isTopLevel = !route.includes(\"/\");\n    const isBlogPost = route.startsWith(\"blog/\") && route !== \"blog\";\n\n    sitemapEntries.push({\n      url: `${BASE_URL}/${route}`,\n      lastModified: new Date(),\n      changeFrequency: isBlogPost ? \"monthly\" : \"weekly\",\n      priority: isTopLevel ? 0.8 : 0.6,\n    });\n  }\n\n  return sitemapEntries;\n};\n\nexport default sitemap;\n"
  },
  {
    "path": "packages/website/components/benchmark-tooltip.tsx",
    "content": "\"use client\";\n\nimport { useState, useRef, useEffect, type ReactElement } from \"react\";\nimport { motion, AnimatePresence, useReducedMotion } from \"motion/react\";\nimport Link from \"next/link\";\nimport {\n  BENCHMARK_CONTROL_COLOR,\n  BENCHMARK_TREATMENT_COLOR,\n  BENCHMARK_TOOLTIP_CONTROL_SECONDS,\n  BENCHMARK_TOOLTIP_TREATMENT_SECONDS,\n  BENCHMARK_TOOLTIP_MAX_SECONDS,\n  BENCHMARK_TOOLTIP_SPEEDUP_FACTOR,\n  TOOLTIP_HOVER_DELAY_MS,\n} from \"@/constants\";\n\ninterface BenchmarkTooltipProps {\n  href: string;\n  children: React.ReactNode;\n  className?: string;\n}\n\ninterface MiniBarProps {\n  targetSeconds: number;\n  maxSeconds: number;\n  color: string;\n  label: string;\n  isAnimating: boolean;\n  shouldReduceMotion?: boolean;\n}\n\nconst MiniBar = ({\n  targetSeconds,\n  maxSeconds,\n  color,\n  label,\n  isAnimating,\n  shouldReduceMotion = false,\n}: MiniBarProps): ReactElement => {\n  const targetWidth = (targetSeconds / maxSeconds) * 100;\n\n  return (\n    <div className=\"relative h-4 flex-1\">\n      <div\n        className=\"absolute top-0 left-0 h-full bg-neutral-800 rounded-r-sm\"\n        style={{ width: `${targetWidth}%` }}\n      />\n      <motion.div\n        className=\"absolute top-0 left-0 h-full rounded-r-sm\"\n        style={{ backgroundColor: color }}\n        initial={shouldReduceMotion ? false : { width: 0 }}\n        animate={{\n          width: isAnimating || shouldReduceMotion ? `${targetWidth}%` : 0,\n        }}\n        transition={\n          shouldReduceMotion\n            ? { duration: 0 }\n            : { duration: targetSeconds / 10, ease: \"linear\" }\n        }\n      />\n      <span\n        className=\"absolute top-1/2 -translate-y-1/2 text-[11px] font-semibold ml-2 tabular-nums whitespace-nowrap\"\n        style={{\n          left: `${targetWidth}%`,\n          color: color === BENCHMARK_CONTROL_COLOR ? \"#737373\" : color,\n        }}\n      >\n        {label}\n      </span>\n    </div>\n  );\n};\n\nMiniBar.displayName = \"MiniBar\";\n\ninterface MiniChartProps {\n  isVisible: boolean;\n  shouldReduceMotion?: boolean;\n}\n\nconst MiniChart = ({\n  isVisible,\n  shouldReduceMotion = false,\n}: MiniChartProps): ReactElement => {\n  const gridLines = [0, 5, 10, 15, 20];\n\n  return (\n    <div className=\"w-80 py-4 pl-3 pr-5 select-none\">\n      <div className=\"relative\">\n        <div className=\"flex items-center gap-2\">\n          <div className=\"w-16 shrink-0\" />\n          <div className=\"flex-1 relative h-0\">\n            {gridLines.map((seconds) => (\n              <div\n                key={seconds}\n                className=\"absolute top-0 border-l border-neutral-800\"\n                style={{\n                  left: `${(seconds / BENCHMARK_TOOLTIP_MAX_SECONDS) * 100}%`,\n                  height: \"calc(100% + 48px)\",\n                  marginTop: \"-2px\",\n                }}\n              />\n            ))}\n          </div>\n        </div>\n\n        <div className=\"space-y-2 relative\">\n          <div className=\"flex items-center gap-2\">\n            <div className=\"w-16 text-right text-[10px] font-medium text-neutral-500 shrink-0 leading-tight\">\n              Claude Code\n            </div>\n            <MiniBar\n              targetSeconds={BENCHMARK_TOOLTIP_CONTROL_SECONDS}\n              maxSeconds={BENCHMARK_TOOLTIP_MAX_SECONDS}\n              color={BENCHMARK_CONTROL_COLOR}\n              label={`${BENCHMARK_TOOLTIP_CONTROL_SECONDS}s`}\n              isAnimating={isVisible}\n              shouldReduceMotion={shouldReduceMotion}\n            />\n          </div>\n\n          <div className=\"flex items-center gap-2\">\n            <div\n              className=\"w-16 text-right text-[10px] font-medium shrink-0 leading-tight\"\n              style={{ color: BENCHMARK_TREATMENT_COLOR }}\n            >\n              + React Grab\n            </div>\n            <div className=\"relative h-4 flex-1\">\n              <MiniBar\n                targetSeconds={BENCHMARK_TOOLTIP_TREATMENT_SECONDS}\n                maxSeconds={BENCHMARK_TOOLTIP_MAX_SECONDS}\n                color={BENCHMARK_TREATMENT_COLOR}\n                label=\"\"\n                isAnimating={isVisible}\n                shouldReduceMotion={shouldReduceMotion}\n              />\n              <motion.span\n                className=\"absolute top-1/2 -translate-y-1/2 flex items-center gap-1.5 ml-1.5\"\n                style={{\n                  left: `${(BENCHMARK_TOOLTIP_TREATMENT_SECONDS / BENCHMARK_TOOLTIP_MAX_SECONDS) * 100}%`,\n                }}\n                initial={shouldReduceMotion ? false : { opacity: 0 }}\n                animate={{ opacity: isVisible || shouldReduceMotion ? 1 : 0 }}\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : { delay: 0.8, duration: 0.3 }\n                }\n              >\n                <span\n                  className=\"text-[11px] font-semibold tabular-nums\"\n                  style={{ color: BENCHMARK_TREATMENT_COLOR }}\n                >\n                  {BENCHMARK_TOOLTIP_TREATMENT_SECONDS}s\n                </span>\n                <span className=\"text-[10px] font-bold text-emerald-400\">\n                  {BENCHMARK_TOOLTIP_SPEEDUP_FACTOR}× faster\n                </span>\n              </motion.span>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"flex items-center gap-2 mt-2\">\n          <div className=\"w-16 shrink-0\" />\n          <div className=\"flex-1 relative h-4\">\n            {gridLines.map((seconds) => (\n              <span\n                key={seconds}\n                className=\"absolute text-[9px] text-neutral-600 -translate-x-1/2\"\n                style={{\n                  left: `${(seconds / BENCHMARK_TOOLTIP_MAX_SECONDS) * 100}%`,\n                }}\n              >\n                {seconds}s\n              </span>\n            ))}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nMiniChart.displayName = \"MiniChart\";\n\nexport const BenchmarkTooltip = ({\n  href,\n  children,\n  className,\n}: BenchmarkTooltipProps): ReactElement => {\n  const shouldReduceMotion = Boolean(useReducedMotion());\n  const [isHovered, setIsHovered] = useState(false);\n  const [isVisible, setIsVisible] = useState(false);\n  const timeoutRef = useRef<NodeJS.Timeout | null>(null);\n\n  const handleMouseEnter = () => {\n    timeoutRef.current = setTimeout(() => {\n      setIsHovered(true);\n      setIsVisible(true);\n    }, TOOLTIP_HOVER_DELAY_MS);\n  };\n\n  const handleMouseLeave = () => {\n    if (timeoutRef.current) {\n      clearTimeout(timeoutRef.current);\n    }\n    setIsHovered(false);\n    setIsVisible(false);\n  };\n\n  useEffect(() => {\n    return () => {\n      if (timeoutRef.current) {\n        clearTimeout(timeoutRef.current);\n      }\n    };\n  }, []);\n\n  return (\n    <span\n      className=\"relative inline-block\"\n      onMouseEnter={handleMouseEnter}\n      onMouseLeave={handleMouseLeave}\n    >\n      <Link href={href} rel=\"noreferrer\" className={className}>\n        {children}\n      </Link>\n      <AnimatePresence>\n        {isHovered && (\n          <motion.div\n            initial={\n              shouldReduceMotion ? false : { opacity: 0, y: 8, scale: 0.96 }\n            }\n            animate={{ opacity: 1, y: 0, scale: 1 }}\n            exit={\n              shouldReduceMotion ? undefined : { opacity: 0, y: 4, scale: 0.98 }\n            }\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : { duration: 0.15, ease: \"easeOut\" }\n            }\n            style={{ transformOrigin: \"top center\" }}\n            className=\"absolute left-1/2 -translate-x-1/2 top-full mt-2 z-50 pointer-events-none\"\n          >\n            <div className=\"absolute left-1/2 -translate-x-1/2 -top-1.5 w-3 h-3 bg-[#0a0a0a] border-l border-t border-neutral-800 rotate-45\" />\n            <div className=\"bg-[#0a0a0a] border border-neutral-800 rounded-lg shadow-2xl overflow-hidden\">\n              <MiniChart\n                isVisible={isVisible}\n                shouldReduceMotion={shouldReduceMotion}\n              />\n            </div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </span>\n  );\n};\n\nBenchmarkTooltip.displayName = \"BenchmarkTooltip\";\n"
  },
  {
    "path": "packages/website/components/benchmarks/benchmark-charts.tsx",
    "content": "\"use client\";\nimport { useState, useEffect } from \"react\";\nimport {\n  Bar,\n  BarChart,\n  CartesianGrid,\n  ResponsiveContainer,\n  Tooltip,\n  XAxis,\n  YAxis,\n  Legend,\n  LabelList,\n} from \"recharts\";\nimport { BenchmarkResult, Metric } from \"./types\";\nimport { calculateStats } from \"./utils\";\nimport prettyMs from \"pretty-ms\";\nimport Image from \"next/image\";\nimport {\n  BENCHMARK_GRID_INTERVAL_SECONDS,\n  BENCHMARK_CHART_HEIGHT_PX,\n  BENCHMARK_BAR_SIZE_PX,\n  BENCHMARK_BAR_GAP_PX,\n  BENCHMARK_ANIMATION_DURATION_MS,\n  BENCHMARK_CONTROL_COLOR,\n  BENCHMARK_TREATMENT_COLOR,\n  BENCHMARK_LIVE_COUNTER_INTERVAL_MS,\n} from \"@/constants\";\n\nconst formatMetricValue = (\n  value: number,\n  unit: string,\n  decimals: number = 2,\n): string => {\n  if (unit === \"$\") return `$${value.toFixed(decimals)}`;\n  if (unit === \"ms\") return `${(value / 1000).toFixed(decimals)}s`;\n  return value.toFixed(decimals);\n};\n\ninterface BenchmarkChartsProps {\n  results: BenchmarkResult[];\n}\n\ninterface CustomTooltipProps {\n  active?: boolean;\n  payload?: Array<{\n    name: string;\n    value: number;\n    fill: string;\n    payload: {\n      ControlRaw: number;\n      TreatmentRaw: number;\n      unit: string;\n    };\n  }>;\n  label?: string;\n}\n\nconst CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => {\n  if (active && payload && payload.length) {\n    const tooltipData = payload[0].payload;\n    return (\n      <div className=\"rounded-lg border border-border bg-card p-3 shadow-xl\">\n        <p className=\"mb-2 text-sm font-medium text-foreground/80\">{label}</p>\n        {payload.map((payloadEntry) => {\n          const isControl = payloadEntry.name === \"Control\";\n          const rawValue = isControl\n            ? tooltipData.ControlRaw\n            : tooltipData.TreatmentRaw;\n          const unit = tooltipData.unit;\n          const formattedValue =\n            typeof rawValue === \"number\"\n              ? formatMetricValue(rawValue, unit)\n              : rawValue;\n\n          return (\n            <div\n              key={payloadEntry.name}\n              className=\"flex items-center gap-2 text-xs\"\n            >\n              <div\n                className=\"h-2 w-2 rounded-full\"\n                style={{ backgroundColor: payloadEntry.fill }}\n              />\n              <span className=\"text-muted-foreground\">\n                {payloadEntry.name}:\n              </span>\n              <span className=\"font-mono text-foreground/80\">\n                {formattedValue}\n              </span>\n              <span className=\"text-muted-foreground ml-1\">\n                ({payloadEntry.value.toFixed(0)}%)\n              </span>\n            </div>\n          );\n        })}\n      </div>\n    );\n  }\n  return null;\n};\n\nCustomTooltip.displayName = \"CustomTooltip\";\n\ninterface AnimatedBarProps {\n  targetSeconds: number;\n  maxSeconds: number;\n  color: string;\n  label: string;\n}\n\nconst AnimatedBar = ({\n  targetSeconds,\n  maxSeconds,\n  color,\n  label,\n}: AnimatedBarProps) => {\n  const targetWidth = (targetSeconds / maxSeconds) * 100;\n  const animationDuration = targetSeconds;\n\n  return (\n    <div className=\"relative h-5 flex-1\">\n      <div\n        className=\"absolute top-0 left-0 h-full bg-neutral-800 rounded\"\n        style={{ width: `${targetWidth}%` }}\n      />\n      <div\n        className=\"absolute top-0 left-0 h-full animate-fill-bar rounded\"\n        style={{\n          backgroundColor: color,\n          animationDuration: `${animationDuration}s`,\n          [\"--target-width\" as string]: `${targetWidth}%`,\n        }}\n      />\n      <span\n        className=\"absolute top-1/2 -translate-y-1/2 text-xs font-semibold ml-2 tabular-nums\"\n        style={{\n          left: `${targetWidth}%`,\n          color: color === BENCHMARK_CONTROL_COLOR ? \"#737373\" : color,\n        }}\n      >\n        {label}\n      </span>\n    </div>\n  );\n};\n\nAnimatedBar.displayName = \"AnimatedBar\";\n\nexport const BenchmarkChartsTweet = ({ results }: BenchmarkChartsProps) => {\n  const controlResults = results.filter((result) => result.type === \"control\");\n  const treatmentResults = results.filter(\n    (result) => result.type === \"treatment\",\n  );\n\n  if (controlResults.length === 0 || treatmentResults.length === 0) {\n    return null;\n  }\n\n  const controlStats = calculateStats(controlResults);\n  const treatmentStats = calculateStats(treatmentResults);\n\n  const controlTotalCost = controlResults.reduce(\n    (sum, result) => sum + result.costUsd,\n    0,\n  );\n  const treatmentTotalCost = treatmentResults.reduce(\n    (sum, result) => sum + result.costUsd,\n    0,\n  );\n\n  const controlDurationSec = controlStats.avgDuration / 1000;\n  const treatmentDurationSec = treatmentStats.avgDuration / 1000;\n  const maxSeconds =\n    Math.ceil(controlDurationSec / BENCHMARK_GRID_INTERVAL_SECONDS) *\n    BENCHMARK_GRID_INTERVAL_SECONDS;\n  const gridLines = Array.from(\n    { length: maxSeconds / BENCHMARK_GRID_INTERVAL_SECONDS + 1 },\n    (_, i) => i * BENCHMARK_GRID_INTERVAL_SECONDS,\n  );\n\n  const durationSpeedup = (\n    controlStats.avgDuration / treatmentStats.avgDuration\n  ).toFixed(0);\n  const costChange = Math.abs(\n    ((treatmentTotalCost - controlTotalCost) / controlTotalCost) * 100,\n  ).toFixed(0);\n\n  return (\n    <div className=\"border border-border rounded-lg p-6\">\n      <div className=\"relative\">\n        <div className=\"flex items-center gap-3\">\n          <div className=\"w-20 shrink-0\" />\n          <div className=\"flex-1 relative h-0\">\n            {gridLines.map((seconds) => (\n              <div\n                key={seconds}\n                className=\"absolute top-0 border-l border-border\"\n                style={{\n                  left: `${(seconds / maxSeconds) * 100}%`,\n                  height: \"calc(100% + 80px)\",\n                  marginTop: \"-4px\",\n                }}\n              />\n            ))}\n          </div>\n        </div>\n\n        <div className=\"space-y-2 relative\">\n          <div className=\"flex items-center gap-3\">\n            <div\n              className=\"w-20 text-right text-xs font-medium shrink-0\"\n              style={{ color: BENCHMARK_TREATMENT_COLOR }}\n            >\n              Claude Code + React Grab\n            </div>\n            <div className=\"relative h-5 flex-1\">\n              <AnimatedBarTreatment\n                targetSeconds={treatmentDurationSec}\n                maxSeconds={maxSeconds}\n                color={BENCHMARK_TREATMENT_COLOR}\n                durationLabel={`${treatmentDurationSec.toFixed(1)}s`}\n                durationSpeedup={durationSpeedup}\n                costLabel={`$${treatmentTotalCost.toFixed(2)}`}\n                costChange={costChange}\n              />\n            </div>\n          </div>\n\n          <div className=\"flex items-center gap-3\">\n            <div className=\"w-20 text-right text-xs font-medium text-muted-foreground shrink-0\">\n              Claude Code\n            </div>\n            <AnimatedBar\n              targetSeconds={controlDurationSec}\n              maxSeconds={maxSeconds}\n              color={BENCHMARK_CONTROL_COLOR}\n              label={`${controlDurationSec.toFixed(1)}s`}\n            />\n          </div>\n\n          <div className=\"flex items-center gap-3\">\n            <div className=\"w-20 shrink-0\" />\n            <LiveCounter\n              targetSeconds={controlDurationSec}\n              maxSeconds={maxSeconds}\n            />\n          </div>\n        </div>\n\n        <div className=\"flex items-center gap-3 mt-3\">\n          <div className=\"w-20 shrink-0\" />\n          <div className=\"flex-1 relative h-5\">\n            {gridLines.map((seconds) => (\n              <span\n                key={seconds}\n                className=\"absolute text-[10px] text-muted-foreground/60 -translate-x-1/2\"\n                style={{ left: `${(seconds / maxSeconds) * 100}%` }}\n              >\n                {seconds}s\n              </span>\n            ))}\n          </div>\n        </div>\n      </div>\n\n      <p className=\"mt-3 text-[10px] text-muted-foreground/60 italic\">\n        Above: avg time for Claude Code to complete 20 UI tasks on a{\" \"}\n        <a\n          href=\"https://ui.shadcn.com\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n          className=\"underline underline-offset-2 hover:text-muted-foreground\"\n        >\n          shadcn/ui\n        </a>{\" \"}\n        dashboard.{\" \"}\n        <a\n          href=\"https://github.com/aidenybai/react-bench\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n          className=\"underline underline-offset-2 hover:text-muted-foreground\"\n        >\n          More info\n        </a>\n        .\n      </p>\n    </div>\n  );\n};\n\nBenchmarkChartsTweet.displayName = \"BenchmarkChartsTweet\";\n\ninterface AnimatedBarTreatmentProps {\n  targetSeconds: number;\n  maxSeconds: number;\n  color: string;\n  durationLabel: string;\n  durationSpeedup: string;\n  costLabel: string;\n  costChange: string;\n}\n\nconst AnimatedBarTreatment = ({\n  targetSeconds,\n  maxSeconds,\n  color,\n  durationLabel,\n  durationSpeedup,\n  costLabel,\n  costChange,\n}: AnimatedBarTreatmentProps) => {\n  const targetWidth = (targetSeconds / maxSeconds) * 100;\n  const animationDuration = targetSeconds;\n\n  return (\n    <>\n      <div\n        className=\"absolute top-0 left-0 h-full bg-neutral-800 rounded\"\n        style={{ width: `${targetWidth}%` }}\n      />\n      <div\n        className=\"absolute top-0 left-0 h-full animate-fill-bar rounded\"\n        style={{\n          backgroundColor: color,\n          animationDuration: `${animationDuration}s`,\n          [\"--target-width\" as string]: `${targetWidth}%`,\n        }}\n      />\n      <span\n        className=\"absolute top-1/2 -translate-y-1/2 flex items-center gap-2 ml-2\"\n        style={{ left: `${targetWidth}%` }}\n      >\n        <span\n          className=\"text-xs font-semibold\"\n          style={{ color: BENCHMARK_TREATMENT_COLOR }}\n        >\n          {durationLabel}\n        </span>\n        <span className=\"text-sm font-bold text-emerald-400\">\n          {durationSpeedup}× faster\n        </span>\n        <span className=\"text-[10px] text-muted-foreground\">\n          ({costLabel} ↓{costChange}%)\n        </span>\n      </span>\n    </>\n  );\n};\n\nAnimatedBarTreatment.displayName = \"AnimatedBarTreatment\";\n\ninterface LiveCounterProps {\n  targetSeconds: number;\n  maxSeconds: number;\n}\n\nconst LiveCounter = ({ targetSeconds, maxSeconds }: LiveCounterProps) => {\n  const [elapsedSeconds, setElapsedSeconds] = useState(0);\n\n  useEffect(() => {\n    const startTime = Date.now();\n    const interval = setInterval(() => {\n      const elapsed = (Date.now() - startTime) / 1000;\n      if (elapsed >= targetSeconds) {\n        setElapsedSeconds(targetSeconds);\n        clearInterval(interval);\n      } else {\n        setElapsedSeconds(elapsed);\n      }\n    }, BENCHMARK_LIVE_COUNTER_INTERVAL_MS);\n    return () => clearInterval(interval);\n  }, [targetSeconds]);\n\n  const currentWidth = (elapsedSeconds / maxSeconds) * 100;\n\n  return (\n    <div className=\"relative h-5 flex-1\">\n      <span\n        className=\"absolute top-0 -translate-x-1/2 text-[10px] tabular-nums text-muted-foreground\"\n        style={{ left: `${currentWidth}%` }}\n      >\n        {elapsedSeconds.toFixed(1)}s\n      </span>\n    </div>\n  );\n};\n\nLiveCounter.displayName = \"LiveCounter\";\n\nexport const BenchmarkCharts = ({ results }: BenchmarkChartsProps) => {\n  const controlResults = results.filter((result) => result.type === \"control\");\n  const treatmentResults = results.filter(\n    (result) => result.type === \"treatment\",\n  );\n\n  if (controlResults.length === 0 || treatmentResults.length === 0) {\n    return null;\n  }\n\n  const controlStats = calculateStats(controlResults);\n  const treatmentStats = calculateStats(treatmentResults);\n\n  const controlTotalCost = controlResults.reduce(\n    (sum, result) => sum + result.costUsd,\n    0,\n  );\n  const treatmentTotalCost = treatmentResults.reduce(\n    (sum, result) => sum + result.costUsd,\n    0,\n  );\n\n  const rawData = [\n    {\n      name: \"Avg Duration\",\n      Control: controlStats.avgDuration,\n      Treatment: treatmentStats.avgDuration,\n      better: \"lower\",\n      unit: \"ms\",\n    },\n    {\n      name: \"Total Cost\",\n      Control: controlTotalCost,\n      Treatment: treatmentTotalCost,\n      better: \"lower\",\n      unit: \"$\",\n    },\n    {\n      name: \"Avg Tool Calls\",\n      Control: controlStats.avgToolCalls,\n      Treatment: treatmentStats.avgToolCalls,\n      better: \"lower\",\n      unit: \"\",\n    },\n  ];\n\n  const metrics: Metric[] = [\n    {\n      name: \"Average Duration\",\n      control: prettyMs(controlStats.avgDuration),\n      treatment: prettyMs(treatmentStats.avgDuration),\n      isImprovement: treatmentStats.avgDuration <= controlStats.avgDuration,\n      change: `${Math.abs(((treatmentStats.avgDuration - controlStats.avgDuration) / controlStats.avgDuration) * 100).toFixed(1)}%`,\n    },\n    {\n      name: \"Total Cost\",\n      control: `$${controlTotalCost.toFixed(2)}`,\n      treatment: `$${treatmentTotalCost.toFixed(2)}`,\n      isImprovement: treatmentTotalCost <= controlTotalCost,\n      change: `${Math.abs(((treatmentTotalCost - controlTotalCost) / controlTotalCost) * 100).toFixed(1)}%`,\n    },\n    {\n      name: \"Avg Tool Calls\",\n      control: controlStats.avgToolCalls.toFixed(1),\n      treatment: treatmentStats.avgToolCalls.toFixed(1),\n      isImprovement: treatmentStats.avgToolCalls <= controlStats.avgToolCalls,\n      change: `${Math.abs(((treatmentStats.avgToolCalls - controlStats.avgToolCalls) / controlStats.avgToolCalls) * 100).toFixed(1)}%`,\n    },\n  ];\n\n  const chartData = rawData.map((metric) => ({\n    name: metric.name,\n    Control: 100,\n    Treatment: (metric.Treatment / metric.Control) * 100,\n    ControlRaw: metric.Control,\n    TreatmentRaw: metric.Treatment,\n    unit: metric.unit,\n    controlDisplay: formatMetricValue(metric.Control, metric.unit),\n    treatmentDisplay: formatMetricValue(metric.Treatment, metric.unit),\n  }));\n\n  return (\n    <div>\n      <div className=\"space-y-8\">\n        <div style={{ height: BENCHMARK_CHART_HEIGHT_PX }} className=\"w-full\">\n          <div className=\"mb-4 text-sm text-muted-foreground text-center\">\n            Normalized to Control = 100%\n          </div>\n          <ResponsiveContainer width=\"100%\" height=\"100%\">\n            <BarChart\n              data={chartData}\n              margin={{ top: 5, right: 20, left: 20, bottom: 20 }}\n              barGap={BENCHMARK_BAR_GAP_PX}\n            >\n              <CartesianGrid\n                strokeDasharray=\"3 3\"\n                vertical={false}\n                stroke=\"#262626\"\n              />\n              <XAxis\n                dataKey=\"name\"\n                axisLine={false}\n                tickLine={false}\n                tick={{ fill: \"#a3a3a3\", fontSize: 11, fontWeight: 500 }}\n              />\n              <YAxis\n                type=\"number\"\n                axisLine={false}\n                tickLine={false}\n                tick={{ fill: \"#737373\", fontSize: 10 }}\n                unit=\"%\"\n              />\n              <Tooltip\n                content={<CustomTooltip />}\n                cursor={{ fill: \"rgba(255,255,255,0.05)\" }}\n              />\n              <Legend\n                verticalAlign=\"top\"\n                height={36}\n                iconType=\"circle\"\n                wrapperStyle={{ paddingBottom: \"10px\", fontSize: \"12px\" }}\n                content={({ payload }) => (\n                  <div className=\"flex items-center justify-center gap-4\">\n                    {payload?.map((legendEntry) => (\n                      <div\n                        key={String(legendEntry.value)}\n                        className=\"flex items-center gap-2\"\n                      >\n                        <div\n                          className=\"h-2 w-2 rounded-full\"\n                          style={{ backgroundColor: legendEntry.color }}\n                        />\n                        {legendEntry.value === \"React Grab\" ? (\n                          <div className=\"flex items-center gap-1.5\">\n                            <Image\n                              src=\"/logo.svg\"\n                              alt=\"React Grab\"\n                              width={12}\n                              height={12}\n                              className=\"w-3 h-3\"\n                            />\n                            <span\n                              className=\"text-xs\"\n                              style={{ color: BENCHMARK_TREATMENT_COLOR }}\n                            >\n                              {legendEntry.value}\n                            </span>\n                          </div>\n                        ) : (\n                          <span className=\"text-xs\">{legendEntry.value}</span>\n                        )}\n                      </div>\n                    ))}\n                  </div>\n                )}\n              />\n              <Bar\n                dataKey=\"Control\"\n                name=\"Control\"\n                fill={BENCHMARK_CONTROL_COLOR}\n                radius={[4, 4, 0, 0]}\n                barSize={BENCHMARK_BAR_SIZE_PX}\n                animationDuration={BENCHMARK_ANIMATION_DURATION_MS}\n              >\n                <LabelList\n                  dataKey=\"controlDisplay\"\n                  position=\"top\"\n                  fill=\"#a3a3a3\"\n                  fontSize={14}\n                  fontWeight={500}\n                />\n              </Bar>\n              <Bar\n                dataKey=\"Treatment\"\n                name=\"React Grab\"\n                fill={BENCHMARK_TREATMENT_COLOR}\n                radius={[4, 4, 0, 0]}\n                barSize={BENCHMARK_BAR_SIZE_PX}\n                animationDuration={BENCHMARK_ANIMATION_DURATION_MS}\n              >\n                <LabelList\n                  dataKey=\"treatmentDisplay\"\n                  position=\"top\"\n                  fill={BENCHMARK_TREATMENT_COLOR}\n                  fontSize={14}\n                  fontWeight={500}\n                />\n              </Bar>\n            </BarChart>\n          </ResponsiveContainer>\n        </div>\n\n        <div className=\"overflow-x-auto flex justify-center\">\n          <table className=\"text-sm border-collapse max-w-2xl w-full\">\n            <thead>\n              <tr className=\"border-b border-border\">\n                <th className=\"text-left py-2 px-4 text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n                  Metric\n                </th>\n                <th className=\"text-left py-2 px-4 text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n                  Control\n                </th>\n                <th className=\"text-left py-2 px-4 text-xs font-medium text-muted-foreground uppercase tracking-wider bg-popover/50 rounded-tr-md\">\n                  <div className=\"flex items-center gap-1.5\">\n                    <Image\n                      src=\"/logo.svg\"\n                      alt=\"React Grab\"\n                      width={12}\n                      height={12}\n                      className=\"w-3 h-3\"\n                    />\n                    <span style={{ color: BENCHMARK_TREATMENT_COLOR }}>\n                      React Grab\n                    </span>\n                  </div>\n                </th>\n              </tr>\n            </thead>\n            <tbody className=\"divide-y divide-border\">\n              {metrics.map((metric) => (\n                <tr\n                  key={metric.name}\n                  className=\"hover:bg-popover transition-colors group\"\n                >\n                  <td className=\"py-2 px-4 font-medium text-foreground/80 text-sm group-hover:text-foreground transition-colors\">\n                    {metric.name}\n                  </td>\n                  <td className=\"py-2 px-4 text-muted-foreground tabular-nums text-sm\">\n                    {metric.control}\n                  </td>\n                  <td className=\"py-2 px-4 text-foreground/80 tabular-nums bg-popover/50 text-sm group-hover:bg-popover transition-colors\">\n                    {metric.treatment}\n                    <span\n                      className={`ml-2 text-xs font-medium ${metric.isImprovement ? \"text-green-400\" : \"text-red-400\"}`}\n                    >\n                      {metric.isImprovement ? \"↓\" : \"↑\"} {metric.change}\n                    </span>\n                  </td>\n                </tr>\n              ))}\n            </tbody>\n          </table>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nBenchmarkCharts.displayName = \"BenchmarkCharts\";\n"
  },
  {
    "path": "packages/website/components/benchmarks/benchmark-detailed-table.tsx",
    "content": "import prettyMs from \"pretty-ms\";\nimport { BenchmarkResult, ChangeInfo, GroupedResult } from \"./types\";\nimport { calculateChange } from \"./utils\";\nimport React, { useState, useMemo } from \"react\";\nimport { ChevronDown, ChevronUp, Search, ArrowUpDown } from \"lucide-react\";\nimport Image from \"next/image\";\nimport { BENCHMARK_TREATMENT_COLOR } from \"@/constants\";\nimport { DataTableCard } from \"@/components/ui/data-table-card\";\n\ninterface BenchmarkDetailedTableProps {\n  results: BenchmarkResult[];\n  testCaseMap: Record<string, string>;\n  lastRunDate?: string;\n}\n\ntype SortField =\n  | \"testName\"\n  | \"inputTokens\"\n  | \"outputTokens\"\n  | \"cost\"\n  | \"duration\"\n  | \"toolCalls\";\ntype SortDirection = \"asc\" | \"desc\";\n\ninterface SortIconProps {\n  field: SortField;\n  sortField: SortField;\n  sortDirection: SortDirection;\n}\n\ninterface MetricColumn {\n  label: string;\n  sortField: SortField;\n  controlValue: (result?: BenchmarkResult) => string;\n  treatmentValue: (result?: BenchmarkResult) => string;\n  change: (\n    control?: BenchmarkResult,\n    treatment?: BenchmarkResult,\n  ) => ChangeInfo;\n  sortValue: (result?: BenchmarkResult) => number;\n}\n\nconst METRIC_COLUMNS: MetricColumn[] = [\n  {\n    label: \"Input Tokens\",\n    sortField: \"inputTokens\",\n    controlValue: (result) =>\n      result?.inputTokens ? result.inputTokens.toLocaleString() : \"-\",\n    treatmentValue: (result) =>\n      result?.inputTokens ? result.inputTokens.toLocaleString() : \"-\",\n    change: (control, treatment) =>\n      calculateChange(control?.inputTokens, treatment?.inputTokens),\n    sortValue: (result) => result?.inputTokens ?? 0,\n  },\n  {\n    label: \"Output Tokens\",\n    sortField: \"outputTokens\",\n    controlValue: (result) =>\n      result?.outputTokens ? result.outputTokens.toLocaleString() : \"-\",\n    treatmentValue: (result) =>\n      result?.outputTokens ? result.outputTokens.toLocaleString() : \"-\",\n    change: (control, treatment) =>\n      calculateChange(control?.outputTokens, treatment?.outputTokens),\n    sortValue: (result) => result?.outputTokens ?? 0,\n  },\n  {\n    label: \"Cost\",\n    sortField: \"cost\",\n    controlValue: (result) =>\n      result?.costUsd !== undefined ? \"$\" + result.costUsd.toFixed(2) : \"-\",\n    treatmentValue: (result) =>\n      result?.costUsd !== undefined ? \"$\" + result.costUsd.toFixed(2) : \"-\",\n    change: (control, treatment) =>\n      calculateChange(control?.costUsd, treatment?.costUsd),\n    sortValue: (result) => result?.costUsd ?? 0,\n  },\n  {\n    label: \"Duration\",\n    sortField: \"duration\",\n    controlValue: (result) =>\n      result?.durationMs ? prettyMs(result.durationMs) : \"-\",\n    treatmentValue: (result) =>\n      result?.durationMs ? prettyMs(result.durationMs) : \"-\",\n    change: (control, treatment) =>\n      calculateChange(control?.durationMs, treatment?.durationMs),\n    sortValue: (result) => result?.durationMs ?? 0,\n  },\n  {\n    label: \"Tool Calls\",\n    sortField: \"toolCalls\",\n    controlValue: (result) =>\n      result?.toolCalls !== undefined ? String(result.toolCalls) : \"-\",\n    treatmentValue: (result) =>\n      result?.toolCalls !== undefined ? String(result.toolCalls) : \"-\",\n    change: (control, treatment) =>\n      calculateChange(control?.toolCalls, treatment?.toolCalls),\n    sortValue: (result) => result?.toolCalls ?? 0,\n  },\n];\n\nconst HEADER_CLASS =\n  \"text-left py-2 px-3 text-xs font-medium text-muted-foreground uppercase tracking-wider cursor-pointer hover:text-foreground transition-colors group\";\nconst CONTROL_SUBHEADER_CLASS =\n  \"text-left py-1.5 px-3 text-[10px] font-normal text-muted-foreground/60 uppercase tracking-wide\";\nconst TREATMENT_SUBHEADER_CLASS =\n  \"text-left py-1.5 px-3 text-[10px] font-normal text-muted-foreground/60 uppercase tracking-wide bg-popover/30\";\n\nconst SortIcon = ({ field, sortField, sortDirection }: SortIconProps) => {\n  if (sortField !== field)\n    return <ArrowUpDown size={12} className=\"ml-1 opacity-30\" />;\n  return sortDirection === \"asc\" ? (\n    <ChevronUp size={12} className=\"ml-1\" />\n  ) : (\n    <ChevronDown size={12} className=\"ml-1\" />\n  );\n};\n\nSortIcon.displayName = \"SortIcon\";\n\nconst TreatmentLabel = () => (\n  <div className=\"flex items-center gap-1.5\">\n    <Image\n      src=\"/logo.svg\"\n      alt=\"React Grab\"\n      width={10}\n      height={10}\n      className=\"w-2.5 h-2.5\"\n    />\n    <span style={{ color: BENCHMARK_TREATMENT_COLOR }}>React Grab</span>\n  </div>\n);\n\nTreatmentLabel.displayName = \"TreatmentLabel\";\n\nexport const BenchmarkDetailedTable = ({\n  results,\n  testCaseMap,\n  lastRunDate,\n}: BenchmarkDetailedTableProps) => {\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [sortField, setSortField] = useState<SortField>(\"testName\");\n  const [sortDirection, setSortDirection] = useState<SortDirection>(\"asc\");\n\n  const groupedByTest = useMemo(() => {\n    return results.reduce<Record<string, GroupedResult>>((grouped, result) => {\n      if (!grouped[result.testName]) {\n        grouped[result.testName] = {};\n      }\n      grouped[result.testName][result.type] = result;\n      return grouped;\n    }, {});\n  }, [results]);\n\n  const filteredAndSortedResults = useMemo(() => {\n    let entries = Object.entries(groupedByTest);\n\n    if (searchQuery) {\n      const query = searchQuery.toLowerCase();\n      entries = entries.filter(([testName]) =>\n        testName.toLowerCase().includes(query),\n      );\n    }\n\n    entries.sort(([nameA, resultsA], [nameB, resultsB]) => {\n      let valueA: number | string = 0;\n      let valueB: number | string = 0;\n\n      if (sortField === \"testName\") {\n        valueA = nameA;\n        valueB = nameB;\n      } else {\n        const column = METRIC_COLUMNS.find(\n          (col) => col.sortField === sortField,\n        );\n        if (column) {\n          valueA = column.sortValue(resultsA.treatment);\n          valueB = column.sortValue(resultsB.treatment);\n        }\n      }\n\n      if (valueA < valueB) return sortDirection === \"asc\" ? -1 : 1;\n      if (valueA > valueB) return sortDirection === \"asc\" ? 1 : -1;\n      return 0;\n    });\n\n    return entries;\n  }, [groupedByTest, searchQuery, sortField, sortDirection]);\n\n  const handleSort = (field: SortField) => {\n    if (sortField === field) {\n      setSortDirection(sortDirection === \"asc\" ? \"desc\" : \"asc\");\n    } else {\n      setSortField(field);\n      setSortDirection(\"asc\");\n    }\n  };\n\n  return (\n    <DataTableCard\n      title=\"Results\"\n      description={\n        <>\n          Performance metrics per test: tokens, cost (USD), duration, and tool\n          calls. React Grab shows % change vs. Control.\n          {lastRunDate && <span className=\"ml-2\">Last run: {lastRunDate}</span>}\n        </>\n      }\n      actions={\n        <div className=\"relative\">\n          <Search\n            size={14}\n            className=\"absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground\"\n          />\n          <input\n            type=\"text\"\n            placeholder=\"Filter tests...\"\n            value={searchQuery}\n            onChange={(e) => setSearchQuery(e.target.value)}\n            className=\"bg-popover border border-border rounded-md py-1.5 pl-9 pr-3 text-xs text-foreground/80 placeholder:text-muted-foreground/60 focus:outline-none focus:border-ring w-full sm:w-[200px]\"\n          />\n        </div>\n      }\n    >\n      <table className=\"w-full text-sm\">\n        <thead>\n          <tr className=\"border-b border-border\">\n            <th\n              rowSpan={2}\n              className={HEADER_CLASS}\n              onClick={() => handleSort(\"testName\")}\n            >\n              <div className=\"flex items-center\">\n                Test Name\n                <SortIcon\n                  field=\"testName\"\n                  sortField={sortField}\n                  sortDirection={sortDirection}\n                />\n              </div>\n            </th>\n            {METRIC_COLUMNS.map((column) => (\n              <th\n                key={column.sortField}\n                colSpan={2}\n                className={HEADER_CLASS}\n                onClick={() => handleSort(column.sortField)}\n              >\n                <div className=\"flex items-center\">\n                  {column.label}\n                  <SortIcon\n                    field={column.sortField}\n                    sortField={sortField}\n                    sortDirection={sortDirection}\n                  />\n                </div>\n              </th>\n            ))}\n          </tr>\n          <tr className=\"border-b border-border bg-card\">\n            {METRIC_COLUMNS.map((column) => (\n              <React.Fragment key={column.sortField}>\n                <th className={CONTROL_SUBHEADER_CLASS}>Control</th>\n                <th className={TREATMENT_SUBHEADER_CLASS}>\n                  <TreatmentLabel />\n                </th>\n              </React.Fragment>\n            ))}\n          </tr>\n        </thead>\n        <tbody className=\"divide-y divide-border\">\n          {filteredAndSortedResults.length === 0 ? (\n            <tr>\n              <td\n                colSpan={1 + METRIC_COLUMNS.length * 2}\n                className=\"py-8 text-center text-muted-foreground\"\n              >\n                No results found matching &quot;{searchQuery}&quot;\n              </td>\n            </tr>\n          ) : (\n            filteredAndSortedResults.map(([testName, groupedResults]) => {\n              const { control, treatment } = groupedResults;\n              const prompt = testCaseMap[testName] || \"\";\n\n              return (\n                <tr\n                  key={testName}\n                  className=\"hover:bg-popover transition-colors\"\n                >\n                  <td\n                    className=\"py-2 px-3 font-medium text-foreground/80 cursor-help max-w-[200px] truncate\"\n                    title={prompt}\n                  >\n                    {testName}\n                  </td>\n                  {METRIC_COLUMNS.map((column) => {\n                    const changeInfo = column.change(control, treatment);\n                    return (\n                      <React.Fragment key={column.sortField}>\n                        <td className=\"py-2 px-3 text-muted-foreground tabular-nums text-xs\">\n                          {column.controlValue(control)}\n                        </td>\n                        <td\n                          className=\"py-2 px-3 text-foreground/80 tabular-nums text-xs\"\n                          style={{ backgroundColor: changeInfo.bgColor }}\n                        >\n                          {column.treatmentValue(treatment)}\n                          {changeInfo.change && (\n                            <span className=\"ml-1.5 text-[10px] opacity-70\">\n                              {changeInfo.change}\n                            </span>\n                          )}\n                        </td>\n                      </React.Fragment>\n                    );\n                  })}\n                </tr>\n              );\n            })\n          )}\n        </tbody>\n      </table>\n    </DataTableCard>\n  );\n};\n\nBenchmarkDetailedTable.displayName = \"BenchmarkDetailedTable\";\n"
  },
  {
    "path": "packages/website/components/benchmarks/types.ts",
    "content": "export interface BenchmarkResult {\n  testName: string;\n  type: \"treatment\" | \"control\";\n  inputTokens: number;\n  outputTokens: number;\n  costUsd: number;\n  durationMs: number;\n  toolCalls: number;\n  success: boolean;\n}\n\nexport interface TestCase {\n  name: string;\n  prompt: string;\n}\n\nexport interface GroupedResult {\n  treatment?: BenchmarkResult;\n  control?: BenchmarkResult;\n}\n\nexport interface Metric {\n  name: string;\n  control: string;\n  treatment: string;\n  isImprovement: boolean;\n  change: string;\n}\n\nexport interface ChangeInfo {\n  change: string;\n  bgColor: string;\n}\n\nexport interface Stats {\n  successRate: number;\n  avgCost: number;\n  avgDuration: number;\n  avgToolCalls: number;\n  avgInputTokens: number;\n  avgOutputTokens: number;\n}\n"
  },
  {
    "path": "packages/website/components/benchmarks/utils.ts",
    "content": "import { BenchmarkResult, ChangeInfo, Stats } from \"./types\";\n\nconst getGradientColor = (changePercent: number): string => {\n  const absChange = Math.abs(changePercent);\n\n  if (changePercent < 0) {\n    const intensity = Math.min(absChange / 100, 1);\n    const opacity = 0.1 + intensity * 0.3;\n    return `rgba(100, 200, 150, ${opacity})`;\n  } else {\n    const intensity = Math.min(absChange / 100, 1);\n    const opacity = 0.1 + intensity * 0.3;\n    return `rgba(240, 120, 120, ${opacity})`;\n  }\n};\n\nexport const calculateChange = (\n  controlVal?: number,\n  treatmentVal?: number,\n): ChangeInfo => {\n  if (controlVal === undefined || treatmentVal === undefined)\n    return { change: \"\", bgColor: \"transparent\" };\n\n  if (treatmentVal === 0 && controlVal > 0) {\n    return {\n      change: \"↓100%\",\n      bgColor: getGradientColor(-100),\n    };\n  }\n\n  if (!controlVal || !treatmentVal)\n    return { change: \"\", bgColor: \"transparent\" };\n\n  const change = ((treatmentVal - controlVal) / controlVal) * 100;\n\n  if (Math.abs(change) < 0.1) {\n    return { change: \"\", bgColor: \"transparent\" };\n  }\n\n  const isImprovement = change < 0;\n  const bgColor = getGradientColor(change);\n  return {\n    change: `${isImprovement ? \"↓\" : \"↑\"}${Math.abs(change).toFixed(0)}%`,\n    bgColor,\n  };\n};\n\nexport const calculateStats = (results: BenchmarkResult[]): Stats => {\n  const count = results.length;\n  if (count === 0) {\n    return {\n      successRate: 0,\n      avgCost: 0,\n      avgDuration: 0,\n      avgToolCalls: 0,\n      avgInputTokens: 0,\n      avgOutputTokens: 0,\n    };\n  }\n\n  const successCount = results.filter((result) => result.success).length;\n  return {\n    successRate: parseFloat(((successCount / count) * 100).toFixed(1)),\n    avgCost: results.reduce((sum, result) => sum + result.costUsd, 0) / count,\n    avgDuration:\n      results.reduce((sum, result) => sum + result.durationMs, 0) / count,\n    avgToolCalls:\n      results.reduce((sum, result) => sum + result.toolCalls, 0) / count,\n    avgInputTokens:\n      results.reduce((sum, result) => sum + result.inputTokens, 0) / count,\n    avgOutputTokens:\n      results.reduce((sum, result) => sum + result.outputTokens, 0) / count,\n  };\n};\n"
  },
  {
    "path": "packages/website/components/blocks/grep-search-group.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState, type ReactElement } from \"react\";\nimport { GREP_SEARCH_DELAY_MS } from \"@/constants\";\nimport { Collapsible } from \"../ui/collapsible\";\nimport { GrepToolCallBlock } from \"./grep-tool-call-block\";\n\ninterface ExploredHeaderProps {\n  completedCount: number;\n}\n\nexport const ExploredHeader = ({\n  completedCount,\n}: ExploredHeaderProps): ReactElement => {\n  const isExploring = completedCount === 0;\n  const label = `${completedCount} search${completedCount === 1 ? \"\" : \"es\"}`;\n\n  return (\n    <div className=\"text-[#818181]\">\n      {isExploring ? (\n        \"Exploring\"\n      ) : (\n        <>\n          Explored <span className=\"text-[#5b5b5b]\">{label}</span>\n        </>\n      )}\n    </div>\n  );\n};\n\ninterface GrepSearchGroupProps {\n  searches: string[];\n  onComplete?: () => void;\n}\n\nexport const GrepSearchGroup = ({\n  searches,\n  onComplete,\n}: GrepSearchGroupProps): ReactElement => {\n  const [phase, setPhase] = useState(0);\n\n  useEffect(() => {\n    if (phase > searches.length) return;\n\n    const timeout = setTimeout(() => {\n      const nextPhase = phase + 1;\n      setPhase(nextPhase);\n      if (nextPhase > searches.length) {\n        onComplete?.();\n      }\n    }, GREP_SEARCH_DELAY_MS);\n\n    return () => clearTimeout(timeout);\n  }, [phase, searches.length, onComplete]);\n\n  const visibleCount = Math.min(phase, searches.length);\n  const streamingIndex = phase <= searches.length ? phase - 1 : null;\n  const isStreaming = streamingIndex !== null && streamingIndex >= 0;\n\n  return (\n    <Collapsible\n      header={<ExploredHeader completedCount={visibleCount} />}\n      defaultExpanded\n      isStreaming={isStreaming}\n    >\n      <div className=\"flex flex-col gap-2 mt-2\">\n        {searches.slice(0, visibleCount).map((search, index) => (\n          <GrepToolCallBlock\n            key={search}\n            parameter={search}\n            isStreaming={index === streamingIndex}\n          />\n        ))}\n      </div>\n    </Collapsible>\n  );\n};\n\nGrepSearchGroup.displayName = \"GrepSearchGroup\";\n"
  },
  {
    "path": "packages/website/components/blocks/grep-tool-call-block.tsx",
    "content": "import { type ReactElement } from \"react\";\nimport { Collapsible } from \"../ui/collapsible\";\n\ninterface GrepToolCallBlockProps {\n  parameter: string;\n  result?: string;\n  isStreaming?: boolean;\n}\n\nexport const GrepToolCallBlock = ({\n  parameter,\n  result = \"0 matches\",\n  isStreaming = false,\n}: GrepToolCallBlockProps): ReactElement => {\n  const displayName = isStreaming ? \"Grepping\" : \"Grepped\";\n  const hasNoMatches = result === \"0 matches\";\n  const displayResult = hasNoMatches ? \"Could not find any matches\" : result;\n\n  return (\n    <Collapsible\n      header={\n        <div className=\"flex flex-wrap gap-1\">\n          <span className={isStreaming ? \"shimmer-text\" : \"\"}>\n            {displayName}\n          </span>\n          {isStreaming ? (\n            <span className=\"text-[#5b5b5b]\">{parameter}</span>\n          ) : (\n            <>\n              <span className=\"text-[#5b5b5b]\">{parameter}</span>\n              <span>and found</span>\n              <span className=\"text-[#5b5b5b]\">\n                {hasNoMatches ? \"no matches\" : result}\n              </span>\n            </>\n          )}\n        </div>\n      }\n      defaultExpanded={false}\n      isStreaming={isStreaming}\n      autoExpandOnStreaming={false}\n    >\n      <div className=\"text-[#5b5b5b] mt-1\">{displayResult}</div>\n    </Collapsible>\n  );\n};\n\nGrepToolCallBlock.displayName = \"GrepToolCallBlock\";\n"
  },
  {
    "path": "packages/website/components/blocks/message-block.tsx",
    "content": "\"use client\";\n\nimport { type ReactElement } from \"react\";\nimport { type StreamRenderedBlock } from \"@/hooks/use-stream\";\nimport { StreamingText } from \"./streaming-text\";\n\ninterface MessageBlockProps {\n  block: StreamRenderedBlock;\n  animationDelay?: number;\n}\n\nexport const MessageBlock = ({\n  block,\n  animationDelay,\n}: MessageBlockProps): ReactElement => {\n  return (\n    <div className=\"text-foreground\">\n      <StreamingText\n        content={block.content}\n        chunks={block.chunks || []}\n        animationDelay={animationDelay}\n      />\n    </div>\n  );\n};\n\nMessageBlock.displayName = \"MessageBlock\";\n"
  },
  {
    "path": "packages/website/components/blocks/read-tool-call-block.tsx",
    "content": "\"use client\";\n\nimport { useState, useRef, useEffect, type ReactElement } from \"react\";\nimport { CLICK_FEEDBACK_DURATION_MS } from \"@/constants\";\n\ninterface ReadToolCallBlockProps {\n  parameter: string;\n  isStreaming?: boolean;\n}\n\nexport const ReadToolCallBlock = ({\n  parameter,\n  isStreaming = false,\n}: ReadToolCallBlockProps): ReactElement => {\n  const [isClicked, setIsClicked] = useState(false);\n  const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const displayName = isStreaming ? \"Reading\" : \"Read\";\n\n  useEffect(() => {\n    return () => {\n      if (timeoutRef.current) {\n        clearTimeout(timeoutRef.current);\n      }\n    };\n  }, []);\n\n  const handleClick = () => {\n    if (timeoutRef.current) {\n      clearTimeout(timeoutRef.current);\n    }\n    setIsClicked(true);\n    timeoutRef.current = setTimeout(\n      () => setIsClicked(false),\n      CLICK_FEEDBACK_DURATION_MS,\n    );\n  };\n\n  return (\n    <div className=\"flex flex-wrap gap-1 text-[#818181]\">\n      <span className={isStreaming ? \"shimmer-text\" : \"\"}>{displayName}</span>\n      <button\n        onClick={handleClick}\n        className=\"max-w-full break-all text-left transition-colors duration-300\"\n        style={{\n          color: isClicked ? \"#ffffff\" : \"#5b5b5b\",\n        }}\n      >\n        {parameter}\n      </button>\n    </div>\n  );\n};\n\nReadToolCallBlock.displayName = \"ReadToolCallBlock\";\n"
  },
  {
    "path": "packages/website/components/blocks/streaming-text.tsx",
    "content": "\"use client\";\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { type ReactElement, type ReactNode } from \"react\";\n\ninterface StreamChunk {\n  id: string;\n  text: string;\n}\n\ninterface FadeInProps {\n  children: ReactNode;\n  delay?: number;\n  as?: \"span\" | \"div\";\n  shouldReduceMotion?: boolean;\n}\n\nconst FadeIn = ({\n  children,\n  delay = 0,\n  as = \"span\",\n  shouldReduceMotion = false,\n}: FadeInProps): ReactElement => {\n  const Component = motion[as];\n  return (\n    <Component\n      initial={shouldReduceMotion ? false : { opacity: 0 }}\n      animate={{ opacity: 1 }}\n      transition={\n        shouldReduceMotion\n          ? { duration: 0 }\n          : { duration: 0.4, ease: \"easeOut\", delay }\n      }\n    >\n      {children}\n    </Component>\n  );\n};\n\ninterface StreamingChunksProps {\n  chunks: StreamChunk[];\n  shouldReduceMotion?: boolean;\n}\n\nconst StreamingChunks = ({\n  chunks,\n  shouldReduceMotion = false,\n}: StreamingChunksProps): ReactElement => (\n  <>\n    {chunks.map((chunk) => (\n      <motion.span\n        key={chunk.id}\n        initial={shouldReduceMotion ? false : { opacity: 0.2 }}\n        animate={{ opacity: 1 }}\n        transition={\n          shouldReduceMotion\n            ? { duration: 0 }\n            : { duration: 0.25, ease: \"easeOut\" }\n        }\n      >\n        {chunk.text}\n      </motion.span>\n    ))}\n  </>\n);\n\ninterface StreamingTextProps {\n  content: string | ReactNode | Array<string | ReactNode>;\n  chunks: StreamChunk[];\n  animationDelay?: number;\n}\n\nexport const StreamingText = ({\n  content,\n  chunks,\n  animationDelay = 0,\n}: StreamingTextProps): ReactElement => {\n  const shouldReduceMotion = Boolean(useReducedMotion());\n  const isInstantContent = chunks.length === 0;\n\n  if (Array.isArray(content)) {\n    return (\n      <>\n        {content.map((item, index) => {\n          if (typeof item === \"string\") {\n            if (isInstantContent) {\n              return (\n                <FadeIn\n                  key={`text-${index}`}\n                  delay={animationDelay}\n                  shouldReduceMotion={shouldReduceMotion}\n                >\n                  {item}\n                </FadeIn>\n              );\n            }\n            return (\n              <span key={`text-${index}`}>\n                <StreamingChunks\n                  chunks={chunks}\n                  shouldReduceMotion={shouldReduceMotion}\n                />\n              </span>\n            );\n          }\n          return <span key={`node-${index}`}>{item}</span>;\n        })}\n      </>\n    );\n  }\n\n  if (typeof content !== \"string\") {\n    if (isInstantContent) {\n      return (\n        <FadeIn\n          delay={animationDelay}\n          as=\"div\"\n          shouldReduceMotion={shouldReduceMotion}\n        >\n          {content}\n        </FadeIn>\n      );\n    }\n    return <>{content}</>;\n  }\n\n  if (isInstantContent) {\n    return (\n      <FadeIn delay={animationDelay} shouldReduceMotion={shouldReduceMotion}>\n        {content}\n      </FadeIn>\n    );\n  }\n\n  return (\n    <StreamingChunks chunks={chunks} shouldReduceMotion={shouldReduceMotion} />\n  );\n};\n\nStreamingText.displayName = \"StreamingText\";\n"
  },
  {
    "path": "packages/website/components/blocks/thought-block.tsx",
    "content": "\"use client\";\n\nimport { type ReactElement } from \"react\";\nimport { type StreamRenderedBlock } from \"@/hooks/use-stream\";\nimport { Collapsible } from \"../ui/collapsible\";\nimport { Scrollable } from \"../ui/scrollable\";\nimport { StreamingText } from \"./streaming-text\";\n\ninterface ThoughtBlockProps {\n  block: StreamRenderedBlock;\n}\n\nexport const ThoughtBlock = ({ block }: ThoughtBlockProps): ReactElement => {\n  return (\n    <Collapsible\n      key={`${block.id}-${block.status}`}\n      header={\n        <span className=\"text-[#818181]\">\n          {block.status === \"streaming\" ? \"Thinking \" : \"Thought for \"}\n          {block.status !== \"streaming\" && block.duration && (\n            <span className=\"text-[#5b5b5b]\">\n              {block.duration >= 1000\n                ? `${Math.round(block.duration / 1000)}s`\n                : `${block.duration}ms`}\n            </span>\n          )}\n        </span>\n      }\n      defaultExpanded={block.status === \"streaming\"}\n      isStreaming={block.status === \"streaming\"}\n    >\n      <div className=\"mt-1\">\n        <Scrollable className=\"text-[#818181]\" maxHeight=\"100px\">\n          <StreamingText content={block.content} chunks={block.chunks || []} />\n        </Scrollable>\n      </div>\n    </Collapsible>\n  );\n};\n\nThoughtBlock.displayName = \"ThoughtBlock\";\n"
  },
  {
    "path": "packages/website/components/blog-article-layout.tsx",
    "content": "\"use client\";\n\nimport Image from \"next/image\";\nimport Link from \"next/link\";\nimport { ArrowLeft } from \"lucide-react\";\nimport ReactGrabLogo from \"@/public/logo.svg\";\nimport { TableOfContents } from \"@/components/table-of-contents\";\n\ninterface TocHeading {\n  id: string;\n  text: string;\n  level: number;\n}\n\ninterface Author {\n  name: string;\n  url: string;\n}\n\ninterface BlogArticleLayoutProps {\n  title: string;\n  authors: Author[];\n  date: string;\n  headings: TocHeading[];\n  children: React.ReactNode;\n  subtitle?: React.ReactNode;\n}\n\nexport const BlogArticleLayout = ({\n  title,\n  authors,\n  date,\n  headings,\n  children,\n  subtitle,\n}: BlogArticleLayoutProps) => {\n  return (\n    <div className=\"min-h-screen bg-background font-sans text-foreground\">\n      <div className=\"px-4 sm:px-8 pt-12 sm:pt-16 pb-56\">\n        <div className=\"mx-auto max-w-5xl flex justify-center gap-12\">\n          <TableOfContents headings={headings} />\n\n          <div className=\"w-full max-w-2xl flex flex-col gap-6\">\n            <div className=\"flex items-center gap-2 text-sm text-neutral-400 opacity-50 hover:opacity-100 transition-opacity\">\n              <Link\n                href=\"/\"\n                className=\"hover:text-foreground transition-colors flex items-center gap-2 underline underline-offset-4\"\n              >\n                <ArrowLeft size={16} />\n                Back to home\n              </Link>\n              <span>·</span>\n              <Link\n                href=\"/blog\"\n                className=\"hover:text-foreground transition-colors underline underline-offset-4\"\n              >\n                Read more posts\n              </Link>\n            </div>\n\n            <div className=\"flex flex-col gap-2\">\n              <div className=\"flex flex-col gap-2\">\n                <Link href=\"/\" className=\"hover:opacity-80 transition-opacity\">\n                  <Image\n                    src={ReactGrabLogo}\n                    alt=\"React Grab\"\n                    className=\"w-10 h-10\"\n                  />\n                </Link>\n                <h1 className=\"text-xl font-medium text-foreground\">{title}</h1>\n              </div>\n\n              <div className=\"text-sm text-neutral-500\">\n                By{\" \"}\n                {authors.map((author, index) => (\n                  <span key={author.name}>\n                    <a\n                      href={author.url}\n                      target=\"_blank\"\n                      rel=\"noopener noreferrer\"\n                      className=\"text-foreground/80 hover:text-foreground underline underline-offset-4\"\n                    >\n                      {author.name}\n                    </a>\n                    {index < authors.length - 1 && \", \"}\n                  </span>\n                ))}\n                {\" · \"}\n                <span>{date}</span>\n              </div>\n              {subtitle}\n            </div>\n\n            {children}\n          </div>\n\n          <div className=\"hidden lg:block w-48 shrink-0\" />\n        </div>\n      </div>\n    </div>\n  );\n};\n\nBlogArticleLayout.displayName = \"BlogArticleLayout\";\n"
  },
  {
    "path": "packages/website/components/demo-footer.tsx",
    "content": "\"use client\";\n\nimport { type ReactElement } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { RotateCcw } from \"lucide-react\";\n\nexport const DemoFooter = (): ReactElement => {\n  const handleRestartClick = (): void => {\n    if (typeof window === \"undefined\") return;\n\n    try {\n      window.localStorage.clear();\n    } catch {\n      return;\n    }\n    window.location.reload();\n  };\n\n  return (\n    <div className=\"pt-4 text-sm text-muted-foreground sm:text-base\">\n      <Button\n        variant=\"ghost\"\n        size=\"sm\"\n        type=\"button\"\n        onClick={handleRestartClick}\n        className=\"hidden h-auto items-center gap-1 p-0 text-sm text-muted-foreground hover:bg-transparent hover:text-foreground sm:inline-flex sm:text-base\"\n      >\n        <span className=\"underline underline-offset-4\">restart demo</span>\n        <RotateCcw size={13} className=\"align-middle\" />\n      </Button>\n      <span className=\"hidden sm:inline\"> &middot; </span>\n      <a\n        href=\"/blog\"\n        className=\"underline underline-offset-4 hover:text-foreground\"\n      >\n        blog\n      </a>{\" \"}\n      &middot;{\" \"}\n      <a\n        href=\"/changelog\"\n        className=\"underline underline-offset-4 hover:text-foreground\"\n      >\n        changelog\n      </a>\n    </div>\n  );\n};\n\nDemoFooter.displayName = \"DemoFooter\";\n"
  },
  {
    "path": "packages/website/components/github-button.tsx",
    "content": "import { type ReactElement } from \"react\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport { cn } from \"@/utils/cn\";\nimport { IconGithub } from \"./icons/icon-github\";\n\nexport const GithubButton = (): ReactElement => {\n  return (\n    <a\n      href=\"https://github.com/aidenybai/react-grab\"\n      target=\"_blank\"\n      rel=\"noreferrer\"\n      className={cn(\n        buttonVariants({ variant: \"default\" }),\n        \"h-auto gap-2 px-3 py-1.5 text-sm active:scale-[0.98] sm:text-base\",\n      )}\n    >\n      <IconGithub className=\"h-[18px] w-[18px]\" />\n      Star on GitHub\n    </a>\n  );\n};\n\nGithubButton.displayName = \"GithubButton\";\n"
  },
  {
    "path": "packages/website/components/grab-element-button.tsx",
    "content": "\"use client\";\n\nimport {\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n  type ReactElement,\n  type ReactNode,\n  type MouseEvent as ReactMouseEvent,\n} from \"react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { HOTKEY_KEYUP_DELAY_MS } from \"@/constants\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/utils/cn\";\nimport { detectMobile } from \"@/utils/detect-mobile\";\nimport { getKeyFromCode } from \"@/utils/get-key-from-code\";\nimport { hotkeyToString } from \"@/utils/hotkey-to-string\";\nimport { useHotkey } from \"./hotkey-context\";\n\nexport interface RecordedHotkey {\n  key: string | null;\n  metaKey: boolean;\n  ctrlKey: boolean;\n  shiftKey: boolean;\n  altKey: boolean;\n}\n\nexport interface SelectedElementInfo {\n  tagName: string;\n  id?: string;\n  className?: string;\n  textContent?: string;\n  componentName?: string;\n  filePath?: string;\n  lineNumber?: number;\n  columnNumber?: number;\n}\n\ninterface GrabElementButtonProps {\n  onSelect: (element: SelectedElementInfo) => void;\n  showSkip?: boolean;\n  animationDelay?: number;\n}\n\nconst EMPTY_MODIFIERS: Readonly<Omit<RecordedHotkey, \"key\">> = {\n  metaKey: false,\n  ctrlKey: false,\n  shiftKey: false,\n  altKey: false,\n};\n\ntype ReactGrabModule = typeof import(\"react-grab\");\n\nconst withReactGrab = (action: (module: ReactGrabModule) => void): void => {\n  if (typeof window === \"undefined\") return;\n  import(\"react-grab\").then(action).catch(console.error);\n};\n\nconst toggleReactGrab = (): void => {\n  withReactGrab((m) => m.getGlobalApi()?.toggle());\n};\n\nconst deactivateReactGrab = (): void => {\n  withReactGrab((m) => m.getGlobalApi()?.deactivate());\n};\n\nconst updateReactGrabHotkey = (hotkey: RecordedHotkey | null): void => {\n  withReactGrab((reactGrab) => {\n    reactGrab.getGlobalApi()?.dispose();\n    const activationKey = hotkey ? hotkeyToString(hotkey) : undefined;\n    const newApi = reactGrab.init({ activationKey });\n    newApi.registerPlugin({\n      name: \"website-events\",\n      hooks: {\n        onActivate: () => {\n          window.dispatchEvent(new CustomEvent(\"react-grab:activated\"));\n        },\n        onDeactivate: () => {\n          window.dispatchEvent(new CustomEvent(\"react-grab:deactivated\"));\n        },\n      },\n    });\n    reactGrab.setGlobalApi(newApi);\n  });\n};\n\ninterface KbdProps {\n  children: ReactNode;\n  wide?: boolean;\n}\n\nconst Kbd = ({ children, wide = false }: KbdProps): ReactElement => (\n  <kbd\n    className={cn(\n      \"touch-hitbox inline-flex items-center justify-center rounded bg-white/10 hover:bg-white/20\",\n      wide ? \"h-7 px-1.5 text-xs\" : \"size-7 text-sm\",\n    )}\n  >\n    {children}\n  </kbd>\n);\n\nexport const GrabElementButton = ({\n  onSelect,\n  showSkip = true,\n  animationDelay = 0,\n}: GrabElementButtonProps): ReactElement | null => {\n  const shouldReduceMotion = Boolean(useReducedMotion());\n  const { customHotkey, setCustomHotkey } = useHotkey();\n  const [isActivated, setIsActivated] = useState(false);\n  const [isMac, setIsMac] = useState(true);\n  const [isMobile, setIsMobile] = useState(false);\n  const [hideSkip, setHideSkip] = useState(false);\n  const [hasAdvanced, setHasAdvanced] = useState(false);\n  const [isRecordingHotkey, setIsRecordingHotkey] = useState(false);\n  const pressedModifiersRef = useRef({ ...EMPTY_MODIFIERS });\n  const keyUpTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  const handleHotkeyChange = useCallback(\n    (hotkey: RecordedHotkey) => {\n      setCustomHotkey(hotkey);\n      updateReactGrabHotkey(hotkey);\n    },\n    [setCustomHotkey],\n  );\n\n  const handleHotkeyKeyDown = useCallback(\n    (event: KeyboardEvent) => {\n      event.preventDefault();\n      event.stopPropagation();\n\n      if (keyUpTimeoutRef.current) {\n        clearTimeout(keyUpTimeoutRef.current);\n        keyUpTimeoutRef.current = null;\n      }\n\n      if (event.key === \"Escape\") {\n        setIsRecordingHotkey(false);\n        pressedModifiersRef.current = { ...EMPTY_MODIFIERS };\n        return;\n      }\n\n      pressedModifiersRef.current = {\n        metaKey: event.metaKey,\n        ctrlKey: event.ctrlKey,\n        shiftKey: event.shiftKey,\n        altKey: event.altKey,\n      };\n\n      if ([\"Meta\", \"Control\", \"Shift\", \"Alt\"].includes(event.key)) return;\n\n      const keyFromCode = getKeyFromCode(event.code);\n      if (!keyFromCode) return;\n\n      const hasModifier =\n        event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;\n      if (!hasModifier) return;\n\n      handleHotkeyChange({\n        key: keyFromCode,\n        metaKey: event.metaKey,\n        ctrlKey: event.ctrlKey,\n        shiftKey: event.shiftKey,\n        altKey: event.altKey,\n      });\n      setIsRecordingHotkey(false);\n      pressedModifiersRef.current = { ...EMPTY_MODIFIERS };\n    },\n    [handleHotkeyChange],\n  );\n\n  const handleHotkeyKeyUp = useCallback(\n    (event: KeyboardEvent) => {\n      const modifierMap: Record<string, keyof RecordedHotkey> = {\n        Meta: \"metaKey\",\n        Control: \"ctrlKey\",\n        Shift: \"shiftKey\",\n        Alt: \"altKey\",\n      };\n      const releasedModifier = modifierMap[event.key];\n      if (!releasedModifier) return;\n\n      event.preventDefault();\n      event.stopPropagation();\n\n      const pressedModifiers = pressedModifiersRef.current;\n      const modifiersAtRelease = {\n        metaKey: pressedModifiers.metaKey || event.key === \"Meta\",\n        ctrlKey: pressedModifiers.ctrlKey || event.key === \"Control\",\n        shiftKey: pressedModifiers.shiftKey || event.key === \"Shift\",\n        altKey: pressedModifiers.altKey || event.key === \"Alt\",\n      };\n\n      const hasAnyModifier =\n        modifiersAtRelease.metaKey ||\n        modifiersAtRelease.ctrlKey ||\n        modifiersAtRelease.shiftKey ||\n        modifiersAtRelease.altKey;\n      if (!hasAnyModifier) return;\n\n      if (keyUpTimeoutRef.current) {\n        clearTimeout(keyUpTimeoutRef.current);\n      }\n\n      keyUpTimeoutRef.current = setTimeout(() => {\n        handleHotkeyChange({\n          key: null,\n          ...modifiersAtRelease,\n        });\n        setIsRecordingHotkey(false);\n        pressedModifiersRef.current = { ...EMPTY_MODIFIERS };\n        keyUpTimeoutRef.current = null;\n      }, HOTKEY_KEYUP_DELAY_MS);\n    },\n    [handleHotkeyChange],\n  );\n\n  useEffect(() => {\n    if (isRecordingHotkey) {\n      window.addEventListener(\"keydown\", handleHotkeyKeyDown, true);\n      window.addEventListener(\"keyup\", handleHotkeyKeyUp, true);\n      return () => {\n        window.removeEventListener(\"keydown\", handleHotkeyKeyDown, true);\n        window.removeEventListener(\"keyup\", handleHotkeyKeyUp, true);\n      };\n    }\n  }, [isRecordingHotkey, handleHotkeyKeyDown, handleHotkeyKeyUp]);\n\n  const handleHotkeyClick = (event: ReactMouseEvent): void => {\n    if (!hasAdvanced) return;\n    event.stopPropagation();\n    setIsRecordingHotkey(true);\n  };\n\n  useEffect(() => {\n    if (typeof window !== \"undefined\") {\n      setIsMac(navigator.platform.toUpperCase().indexOf(\"MAC\") >= 0);\n      setIsMobile(detectMobile());\n    }\n  }, []);\n\n  useEffect(() => {\n    if (isMobile && !hasAdvanced) {\n      setHasAdvanced(true);\n      onSelect({ tagName: \"button\" });\n    } else if (typeof window !== \"undefined\") {\n      import(\"react-grab\").catch(console.error);\n    }\n  }, [isMobile, onSelect, hasAdvanced]);\n\n  useEffect(() => {\n    if (typeof window === \"undefined\") return;\n\n    const handleActivated = (): void => setIsActivated(true);\n    const handleDeactivated = (): void => setIsActivated(false);\n\n    window.addEventListener(\"react-grab:activated\", handleActivated);\n    window.addEventListener(\"react-grab:deactivated\", handleDeactivated);\n\n    return () => {\n      window.removeEventListener(\"react-grab:activated\", handleActivated);\n      window.removeEventListener(\"react-grab:deactivated\", handleDeactivated);\n    };\n  }, []);\n\n  useEffect(() => {\n    if (typeof window === \"undefined\" || hasAdvanced) return;\n\n    const handleElementSelected = (event: Event): void => {\n      const customEvent = event as CustomEvent<{\n        elements?: Array<SelectedElementInfo>;\n      }>;\n\n      const element = customEvent.detail?.elements?.[0] || {\n        tagName: \"element\",\n      };\n\n      setIsActivated(false);\n      setHasAdvanced(true);\n      setHideSkip(true);\n      onSelect(element);\n    };\n\n    window.addEventListener(\n      \"react-grab:element-selected\",\n      handleElementSelected as EventListener,\n    );\n\n    return () => {\n      window.removeEventListener(\n        \"react-grab:element-selected\",\n        handleElementSelected as EventListener,\n      );\n    };\n  }, [onSelect, hasAdvanced]);\n\n  const renderHotkeyDisplay = (): ReactElement => {\n    if (isRecordingHotkey) {\n      return (\n        <span className=\"text-sm text-muted-foreground animate-pulse px-2 py-1\">\n          Press keys\n        </span>\n      );\n    }\n\n    if (customHotkey) {\n      return (\n        <>\n          {customHotkey.metaKey && <Kbd>⌘</Kbd>}\n          {customHotkey.ctrlKey && <Kbd wide>Ctrl</Kbd>}\n          {customHotkey.shiftKey && <Kbd>⇧</Kbd>}\n          {customHotkey.altKey && <Kbd>⌥</Kbd>}\n          {customHotkey.key && (\n            <Kbd>\n              <span className=\"uppercase\">{customHotkey.key}</span>\n            </Kbd>\n          )}\n        </>\n      );\n    }\n\n    if (isMac) {\n      return (\n        <>\n          <Kbd>⌘</Kbd>\n          <Kbd>C</Kbd>\n        </>\n      );\n    }\n\n    return (\n      <>\n        <Kbd wide>Ctrl</Kbd>\n        <Kbd>C</Kbd>\n      </>\n    );\n  };\n\n  const renderActivationPrompt = (): ReactElement => (\n    <span className=\"flex items-center gap-1.5 text-foreground\">\n      <span>Hold</span>\n      <span\n        onClick={handleHotkeyClick}\n        className={cn(\n          \"inline-flex items-center gap-1 transition-all outline-none\",\n          hasAdvanced && \"cursor-pointer\",\n          isRecordingHotkey && \"ring-2 ring-ring rounded\",\n        )}\n      >\n        {renderHotkeyDisplay()}\n      </span>\n    </span>\n  );\n\n  const handleSkip = (): void => {\n    setHasAdvanced(true);\n    setHideSkip(true);\n    setIsActivated(false);\n    deactivateReactGrab();\n    onSelect({ tagName: \"div\" });\n  };\n\n  if (isMobile) {\n    return null;\n  }\n\n  return (\n    <motion.div\n      initial={shouldReduceMotion ? false : { opacity: 0 }}\n      animate={{ opacity: 1 }}\n      transition={\n        shouldReduceMotion\n          ? { duration: 0 }\n          : { duration: 0.4, ease: \"easeOut\", delay: animationDelay }\n      }\n      className=\"hidden flex-col gap-2 py-4 sm:flex sm:flex-row sm:items-center sm:gap-3\"\n    >\n      <div className=\"relative\">\n        {!hasAdvanced && (\n          <motion.div\n            aria-hidden\n            className=\"absolute inset-0 rounded-lg\"\n            style={{\n              boxShadow:\n                \"0 0 24px rgba(215,95,203,0.6), 0 0 48px rgba(215,95,203,0.15)\",\n            }}\n            initial={shouldReduceMotion ? false : { opacity: 0 }}\n            animate={{ opacity: [0, 1, 0] }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    delay: animationDelay + 1.5,\n                    duration: 0.85,\n                    ease: \"easeInOut\",\n                    times: [0, 0.35, 1],\n                  }\n            }\n          />\n        )}\n        <Button\n          onClick={toggleReactGrab}\n          variant={hasAdvanced ? \"outline\" : \"default\"}\n          className={cn(\n            \"relative flex h-12 w-full items-center justify-center gap-2 rounded-lg px-3 text-sm transition-all active:scale-[0.98] sm:w-auto sm:text-base\",\n            !hasAdvanced &&\n              \"border border-[#d75fcb] bg-[#330039] text-white hover:bg-[#4a0052] shadow-[0_0_12px_rgba(215,95,203,0.4)]\",\n          )}\n          type=\"button\"\n        >\n          {!isActivated ? (\n            renderActivationPrompt()\n          ) : (\n            <span className=\"animate-pulse flex items-center gap-1.5\">\n              Click to select an element\n            </span>\n          )}\n        </Button>\n        {!hasAdvanced && !isActivated && (\n          <motion.div\n            initial={shouldReduceMotion ? false : { opacity: 0 }}\n            animate={{ opacity: 1 }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    duration: 0.5,\n                    ease: \"easeOut\",\n                    delay: animationDelay + 0.6,\n                  }\n            }\n            className=\"absolute top-1/2 right-full -translate-y-1/2 mr-2 flex items-center gap-1 pointer-events-none select-none\"\n          >\n            <motion.span\n              className=\"font-(family-name:--font-caveat) text-muted-foreground text-lg -rotate-3\"\n              initial={shouldReduceMotion ? false : { opacity: 0, scale: 0.9 }}\n              animate={{ opacity: 1, scale: 1 }}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : {\n                      duration: 0.4,\n                      ease: \"easeOut\",\n                      delay: animationDelay + 1.3,\n                    }\n              }\n            >\n              click&nbsp;me!\n            </motion.span>\n            <svg\n              width=\"32\"\n              height=\"16\"\n              viewBox=\"0 0 32 16\"\n              fill=\"none\"\n              className=\"text-muted-foreground shrink-0\"\n            >\n              <motion.path\n                d=\"M2 10 Q14 12 24 8\"\n                stroke=\"currentColor\"\n                strokeWidth=\"1.5\"\n                strokeLinecap=\"round\"\n                fill=\"none\"\n                initial={shouldReduceMotion ? false : { pathLength: 0 }}\n                animate={{ pathLength: 1 }}\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : {\n                        duration: 0.5,\n                        ease: \"easeOut\",\n                        delay: animationDelay + 0.8,\n                      }\n                }\n              />\n              <motion.path\n                d=\"M21 4 L26 8 L21 12\"\n                stroke=\"currentColor\"\n                strokeWidth=\"1.5\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                fill=\"none\"\n                initial={shouldReduceMotion ? false : { pathLength: 0 }}\n                animate={{ pathLength: 1 }}\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : {\n                        duration: 0.3,\n                        ease: \"easeOut\",\n                        delay: animationDelay + 1.2,\n                      }\n                }\n              />\n            </svg>\n          </motion.div>\n        )}\n      </div>\n      {!hideSkip && showSkip && (\n        <Button onClick={handleSkip} variant=\"ghost\" type=\"button\">\n          Skip\n        </Button>\n      )}\n    </motion.div>\n  );\n};\n\nGrabElementButton.displayName = \"GrabElementButton\";\n"
  },
  {
    "path": "packages/website/components/homepage-demo.tsx",
    "content": "\"use client\";\n\nimport {\n  Fragment,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\nimport { TextMorph } from \"torph/react\";\nimport { TriangleAlert } from \"lucide-react\";\nimport {\n  AGENT_CYCLE_INTERVAL_MS,\n  DEFAULT_CHUNK_SIZE,\n  STREAM_DEMO_BLOCK_DELAY_MS,\n  STREAM_DEMO_CHUNK_DELAY_MS,\n  STREAM_DEMO_PRELOAD_ANIMATION_DELAY_MULTIPLIER,\n} from \"@/constants\";\nimport {\n  useStream,\n  type StreamBlock,\n  type StreamRenderedBlock,\n} from \"@/hooks/use-stream\";\nimport { detectMobile } from \"@/utils/detect-mobile\";\nimport { BenchmarkTooltip } from \"./benchmark-tooltip\";\nimport { ThoughtBlock } from \"./blocks/thought-block\";\nimport { MessageBlock } from \"./blocks/message-block\";\nimport { GrepSearchGroup } from \"./blocks/grep-search-group\";\nimport { ReadToolCallBlock } from \"./blocks/read-tool-call-block\";\nimport { ViewDocsButton } from \"./view-docs-button\";\nimport { DemoFooter } from \"./demo-footer\";\nimport { GithubButton } from \"./github-button\";\nimport {\n  GrabElementButton,\n  type SelectedElementInfo,\n} from \"./grab-element-button\";\nimport { HotkeyProvider } from \"./hotkey-context\";\nimport { IconClaude } from \"./icons/icon-claude\";\nimport { IconCodex } from \"./icons/icon-codex\";\nimport { IconCopilot } from \"./icons/icon-copilot\";\nimport { IconCursor } from \"./icons/icon-cursor\";\nimport { InstallTabs } from \"./install-tabs\";\nimport { MobileDemoAnimation } from \"./mobile-demo-animation\";\nimport { ReactGrabLogo } from \"./react-grab-logo\";\nimport { UserMessage } from \"./user-message\";\n\nconst GREP_SEARCHES = [\"submit\", \"button\", 'type=\"submit\"'];\n\nconst FALLBACK_ELEMENT = {\n  componentName: \"PrimaryButton\",\n  filePath: \"components/ui/primary-button.tsx\",\n  lineNumber: 42,\n};\n\nconst PATH_START_MARKERS = [\"src\", \"components\", \"app\", \"pages\"];\n\ninterface BlockConfig {\n  type: \"user_message\" | \"thought\" | \"message\";\n  content: ReactNode;\n  duration?: number;\n  check: \"visible\" | \"complete\";\n  role?:\n    | \"grep\"\n    | \"grepError\"\n    | \"elementSelect\"\n    | \"elementAnalysis\"\n    | \"elementRead\"\n    | \"footer\";\n  demoOnly?: boolean;\n  hideOnMobile?: boolean;\n  wrapperClass?: string;\n  animationDelayIndex?: number;\n}\n\nconst ElementTag = ({ children }: { children: string }): ReactElement => (\n  <span className=\"inline-flex items-center rounded-md bg-[#330039] px-1 py-0.5 text-[13px] font-mono text-[#ff4fff]\">\n    {children}\n  </span>\n);\n\nconst GrepErrorContent = (): ReactElement => (\n  <span className=\"text-[#ff8080] inline-flex items-center gap-2\">\n    <TriangleAlert size={16} />\n    <span>I couldn&apos;t find what you&apos;re looking for :(</span>\n  </span>\n);\n\nconst Divider = (): ReactElement => <hr className=\"my-4 border-border\" />;\n\nconst ReactGrabIntro = (): ReactElement => (\n  <div className=\"flex flex-col gap-2\">\n    <div\n      className=\"inline-flex\"\n      style={{ padding: \"2px\", transform: \"translateX(-3px)\" }}\n    >\n      <ReactGrabLogo width={44} height={44} className=\"logo-shimmer-once\" />\n    </div>\n    <div className=\"text-pretty\">\n      <span className=\"font-bold\">React&nbsp;Grab</span> lets you select context\n      for coding agents directly from your&nbsp;website.\n    </div>\n  </div>\n);\n\ninterface AgentEntry {\n  icon: ReactNode;\n  name: string;\n}\n\nconst AGENTS: AgentEntry[] = [\n  {\n    icon: <IconClaude width={16} height={16} />,\n    name: \"Claude Code\",\n  },\n  {\n    icon: <IconCodex width={16} height={16} />,\n    name: \"Codex\",\n  },\n  {\n    icon: <IconCopilot width={18} height={18} />,\n    name: \"Copilot\",\n  },\n  {\n    icon: <IconCursor width={16} height={16} />,\n    name: \"Cursor\",\n  },\n];\n\nconst CyclingAgent = (): ReactElement => {\n  const [activeIndex, setActiveIndex] = useState(0);\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setActiveIndex((previous) => (previous + 1) % AGENTS.length);\n    }, AGENT_CYCLE_INTERVAL_MS);\n    return () => clearInterval(interval);\n  }, []);\n\n  return (\n    <span className=\"inline-flex items-baseline gap-1 whitespace-nowrap\">\n      <span className=\"relative inline-flex h-[16px] w-[18px] items-center justify-center self-center\">\n        {AGENTS.map((agent, index) => (\n          <span\n            key={agent.name}\n            className=\"absolute inset-0 flex items-center justify-center transition-opacity duration-300\"\n            style={{ opacity: index === activeIndex ? 1 : 0 }}\n          >\n            {agent.icon}\n          </span>\n        ))}\n      </span>\n      <TextMorph as=\"span\" className=\"font-inherit\">\n        {AGENTS[activeIndex].name}\n      </TextMorph>\n    </span>\n  );\n};\n\nconst ToolsList = (): ReactElement => (\n  <span className=\"text-pretty\">\n    Get <CyclingAgent /> to the right code{\" \"}\n    <BenchmarkTooltip\n      href=\"/blog/intro\"\n      className=\"shimmer-text-pink inline-block touch-manipulation py-1\"\n    >\n      <span className=\"font-bold\">3×</span>&nbsp;faster\n    </BenchmarkTooltip>\n  </span>\n);\n\nconst FooterActions = (): ReactElement => (\n  <div className=\"pt-2\">\n    <div className=\"flex gap-2\">\n      <GithubButton />\n      <ViewDocsButton />\n    </div>\n    <DemoFooter />\n  </div>\n);\n\ninterface ElementAnalysisContentProps {\n  displayName: string;\n  filePath?: string;\n  lineNumber?: number;\n}\n\nconst shortenFilePath = (filePath: string): string => {\n  const parts = filePath.split(\"/\");\n  const startIndex = parts.findIndex((p) => PATH_START_MARKERS.includes(p));\n  return startIndex !== -1\n    ? parts.slice(startIndex).join(\"/\")\n    : parts.slice(-2).join(\"/\");\n};\n\nconst getFileName = (filePath: string): string =>\n  filePath.split(\"/\").pop() ?? filePath;\n\nconst ElementAnalysisContent = ({\n  displayName,\n  filePath,\n  lineNumber,\n}: ElementAnalysisContentProps): ReactElement => {\n  const shortPath = filePath ? getFileName(filePath) : null;\n\n  return (\n    <span>\n      I found <ElementTag>{displayName}</ElementTag>\n      {shortPath && (\n        <>\n          {\" \"}\n          at <ElementTag>{shortPath}</ElementTag>\n          {lineNumber && (\n            <>\n              {\" \"}\n              line <ElementTag>{String(lineNumber)}</ElementTag>\n            </>\n          )}\n        </>\n      )}\n      . Let me take a closer look.\n    </span>\n  );\n};\n\nconst TooltipRow = ({\n  label,\n  value,\n  truncate,\n}: {\n  label: string;\n  value: string | number;\n  truncate?: boolean;\n}): ReactElement => (\n  <span className=\"flex justify-between gap-3\">\n    <span className=\"text-muted-foreground\">{label}</span>\n    <span\n      className={`font-mono text-foreground text-right ${truncate ? \"truncate max-w-[120px]\" : \"\"}`}\n    >\n      {value}\n    </span>\n  </span>\n);\n\ninterface ElementSelectContentProps {\n  tagName: string;\n  componentName?: string;\n  filePath?: string;\n  lineNumber?: number;\n  id?: string;\n  className?: string;\n}\n\nconst ElementSelectContent = ({\n  tagName,\n  componentName,\n  filePath,\n  lineNumber,\n  id,\n  className,\n}: ElementSelectContentProps): ReactElement => {\n  const shortPath = filePath ? shortenFilePath(filePath) : null;\n  const hasMetadata = componentName || shortPath || id || className;\n\n  return (\n    <div className=\"flex items-center gap-1 flex-wrap\">\n      Here{\"'\"}s the element{\" \"}\n      <span className=\"relative group\">\n        <ElementTag>{`<${tagName}>`}</ElementTag>\n        {hasMetadata && (\n          <span className=\"absolute left-0 top-full mt-1 z-50 hidden group-hover:block min-w-[180px] rounded-lg border border-border bg-popover px-2.5 py-2 text-xs shadow-xl\">\n            <span className=\"absolute -top-1.5 left-3 h-3 w-3 rotate-45 border-l border-t border-border bg-popover\" />\n            <span className=\"flex flex-col gap-1\">\n              {componentName && (\n                <TooltipRow label=\"Component\" value={componentName} />\n              )}\n              {shortPath && <TooltipRow label=\"File\" value={shortPath} />}\n              {lineNumber && <TooltipRow label=\"Line\" value={lineNumber} />}\n              {id && <TooltipRow label=\"ID\" value={`#${id}`} />}\n              {className && (\n                <TooltipRow\n                  label=\"Class\"\n                  value={`.${className.split(\" \")[0]}`}\n                  truncate\n                />\n              )}\n            </span>\n          </span>\n        )}\n      </span>\n    </div>\n  );\n};\n\nconst BLOCK_CONFIGS: BlockConfig[] = [\n  {\n    type: \"user_message\",\n    content: \"Can you make the submit button bigger?\",\n    check: \"visible\",\n    demoOnly: true,\n  },\n  {\n    type: \"thought\",\n    content:\n      \"I need to find the submit button in their codebase. Let me search for submit buttons across the project that might satisfy the user's request.\",\n    duration: 1000,\n    check: \"visible\",\n    demoOnly: true,\n  },\n  {\n    type: \"message\",\n    content: \"Let me search for the submit button in your codebase.\",\n    check: \"visible\",\n    demoOnly: true,\n  },\n  {\n    type: \"message\",\n    content: <GrepSearchGroup searches={GREP_SEARCHES} />,\n    check: \"complete\",\n    demoOnly: true,\n    role: \"grep\",\n  },\n  {\n    type: \"message\",\n    content: <GrepErrorContent />,\n    check: \"complete\",\n    demoOnly: true,\n    role: \"grepError\",\n  },\n  {\n    type: \"user_message\",\n    content: \"\",\n    check: \"complete\",\n    demoOnly: true,\n    wrapperClass: \"mt-10\",\n    role: \"elementSelect\",\n  },\n  {\n    type: \"message\",\n    content: null,\n    check: \"complete\",\n    demoOnly: true,\n    role: \"elementAnalysis\",\n  },\n  {\n    type: \"message\",\n    content: null,\n    check: \"complete\",\n    demoOnly: true,\n    role: \"elementRead\",\n  },\n  {\n    type: \"message\",\n    content: \"Found it. Let me resize it for you.\",\n    check: \"visible\",\n    demoOnly: true,\n  },\n  {\n    type: \"message\",\n    content: <Divider />,\n    check: \"complete\",\n    demoOnly: true,\n  },\n  {\n    type: \"message\",\n    content: <ReactGrabIntro />,\n    check: \"complete\",\n    animationDelayIndex: 0,\n  },\n  {\n    type: \"message\",\n    content: <ToolsList />,\n    check: \"complete\",\n    animationDelayIndex: 1,\n    role: \"footer\",\n  },\n  {\n    type: \"message\",\n    content: <InstallTabs showHeading showAgentNote />,\n    check: \"complete\",\n    hideOnMobile: true,\n    animationDelayIndex: 3,\n  },\n  {\n    type: \"message\",\n    content: <FooterActions />,\n    check: \"complete\",\n    wrapperClass: \"mt-6\",\n    animationDelayIndex: 4,\n  },\n];\n\nconst toStreamBlocks = (configs: BlockConfig[]): StreamBlock[] =>\n  configs.map((config, index) => ({\n    id: String(index),\n    type: config.type,\n    content: config.content,\n    duration: config.duration,\n  }));\n\ninterface RenderBlockProps {\n  config: BlockConfig;\n  block: StreamRenderedBlock;\n  wasPreloaded: boolean;\n}\n\nconst RenderBlock = ({\n  config,\n  block,\n  wasPreloaded,\n}: RenderBlockProps): ReactElement | null => {\n  const animationDelay =\n    config.animationDelayIndex !== undefined && wasPreloaded\n      ? config.animationDelayIndex *\n        STREAM_DEMO_PRELOAD_ANIMATION_DELAY_MULTIPLIER\n      : 0;\n\n  switch (config.type) {\n    case \"user_message\":\n      return <UserMessage block={block} skipAnimation={wasPreloaded} />;\n    case \"thought\":\n      return <ThoughtBlock block={block} />;\n    case \"message\":\n      return <MessageBlock block={block} animationDelay={animationDelay} />;\n    default:\n      return null;\n  }\n};\n\nconst GREP_INDEX = BLOCK_CONFIGS.findIndex((c) => c.role === \"grep\");\nconst GREP_ERROR_INDEX = BLOCK_CONFIGS.findIndex((c) => c.role === \"grepError\");\n\nconst formatElementSelector = (element: SelectedElementInfo): string => {\n  let selector = element.tagName.toLowerCase();\n  if (element.id) selector += `#${element.id}`;\n  if (element.className) {\n    const classes = element.className.split(\" \").filter(Boolean).slice(0, 2);\n    if (classes.length) selector += `.${classes.join(\".\")}`;\n  }\n  return selector;\n};\n\nexport const HomepageDemo = (): ReactElement => {\n  const [blockConfigs, setBlockConfigs] = useState(BLOCK_CONFIGS);\n  const streamBlocks = useMemo(\n    () => toStreamBlocks(blockConfigs),\n    [blockConfigs],\n  );\n  const [isMobile] = useState(detectMobile);\n  const [isGrepComplete, setIsGrepComplete] = useState(false);\n  const shouldResumeRef = useRef(false);\n\n  const stream = useStream({\n    blocks: streamBlocks,\n    chunkSize: DEFAULT_CHUNK_SIZE,\n    chunkDelayMs: STREAM_DEMO_CHUNK_DELAY_MS,\n    blockDelayMs: STREAM_DEMO_BLOCK_DELAY_MS,\n    pauseAtBlockId: String(GREP_ERROR_INDEX),\n    skipAnimation: isMobile,\n  });\n\n  const isVisible = (index: number): boolean =>\n    stream.blocks[index]?.status !== \"pending\";\n\n  const isComplete = (index: number): boolean =>\n    stream.blocks[index]?.status === \"complete\";\n\n  const handleGrepComplete = useCallback(() => {\n    setIsGrepComplete(true);\n  }, []);\n\n  useEffect(() => {\n    const elementSelectConfig = blockConfigs.find(\n      (c) => c.role === \"elementSelect\",\n    );\n    if (shouldResumeRef.current && elementSelectConfig?.content) {\n      shouldResumeRef.current = false;\n      stream.resume();\n    }\n  }, [blockConfigs, stream]);\n\n  const handleElementSelect = useCallback(\n    (element: SelectedElementInfo) => {\n      const hasReactMetadata = Boolean(\n        element.componentName || element.filePath,\n      );\n      const useFallback = !hasReactMetadata;\n      const fallbackElement = useFallback ? FALLBACK_ELEMENT : null;\n      const componentName =\n        element.componentName ?? fallbackElement?.componentName ?? null;\n      const filePath = element.filePath ?? fallbackElement?.filePath ?? null;\n      const lineNumber =\n        element.lineNumber ?? fallbackElement?.lineNumber ?? null;\n\n      const selector = formatElementSelector(element);\n      const displayName = componentName || selector;\n      const fileName = filePath ? getFileName(filePath) : displayName;\n\n      const updatedConfigs = blockConfigs.map((config) => {\n        switch (config.role) {\n          case \"elementSelect\":\n            return {\n              ...config,\n              content: (\n                <ElementSelectContent\n                  tagName={element.tagName}\n                  componentName={componentName ?? undefined}\n                  filePath={filePath ?? undefined}\n                  lineNumber={lineNumber ?? undefined}\n                  id={element.id}\n                  className={element.className}\n                />\n              ),\n            };\n          case \"elementAnalysis\":\n            return {\n              ...config,\n              content: (\n                <ElementAnalysisContent\n                  displayName={displayName}\n                  filePath={filePath ?? undefined}\n                  lineNumber={lineNumber ?? undefined}\n                />\n              ),\n            };\n          case \"elementRead\":\n            return {\n              ...config,\n              content: <ReadToolCallBlock parameter={fileName} />,\n            };\n          default:\n            return config;\n        }\n      });\n\n      shouldResumeRef.current = true;\n      setBlockConfigs(updatedConfigs);\n    },\n    [blockConfigs],\n  );\n\n  const shouldShowFullDemo = !stream.wasPreloaded;\n\n  return (\n    <HotkeyProvider>\n      <div className=\"min-h-screen bg-background px-4 py-6 sm:px-8 sm:py-8\">\n        <div className=\"mx-auto flex w-full max-w-2xl flex-col gap-2 pt-4 text-base sm:pt-8 sm:text-lg\">\n          {blockConfigs.map((config, index) => {\n            const block = stream.blocks[index];\n            if (!block) return null;\n\n            const passesCheck =\n              config.check === \"visible\" ? isVisible(index) : isComplete(index);\n            const isDynamicRole =\n              config.role === \"elementSelect\" ||\n              config.role === \"elementAnalysis\" ||\n              config.role === \"elementRead\";\n            const isGatedByGrep =\n              index > GREP_INDEX && config.demoOnly && !stream.wasPreloaded;\n\n            const shouldHide =\n              !passesCheck ||\n              (config.demoOnly && !shouldShowFullDemo) ||\n              (config.hideOnMobile && isMobile) ||\n              (isDynamicRole && !config.content) ||\n              (isGatedByGrep && !isGrepComplete);\n\n            if (shouldHide) return null;\n\n            const element =\n              config.role === \"grep\" && !stream.wasPreloaded ? (\n                <GrepSearchGroup\n                  searches={GREP_SEARCHES}\n                  onComplete={handleGrepComplete}\n                />\n              ) : (\n                <RenderBlock\n                  config={config}\n                  block={block}\n                  wasPreloaded={stream.wasPreloaded}\n                />\n              );\n\n            return (\n              <Fragment key={index}>\n                {config.wrapperClass ? (\n                  <div className={config.wrapperClass}>{element}</div>\n                ) : (\n                  element\n                )}\n\n                {config.role === \"grepError\" &&\n                  stream.isPaused &&\n                  isGrepComplete &&\n                  !stream.wasPreloaded && (\n                    <GrabElementButton onSelect={handleElementSelect} />\n                  )}\n\n                {config.role === \"footer\" && isComplete(index) && (\n                  <>\n                    {isMobile && <MobileDemoAnimation />}\n                    {stream.wasPreloaded && (\n                      <GrabElementButton\n                        onSelect={handleElementSelect}\n                        showSkip={false}\n                        animationDelay={\n                          2 * STREAM_DEMO_PRELOAD_ANIMATION_DELAY_MULTIPLIER\n                        }\n                      />\n                    )}\n                  </>\n                )}\n              </Fragment>\n            );\n          })}\n        </div>\n      </div>\n    </HotkeyProvider>\n  );\n};\n\nHomepageDemo.displayName = \"HomepageDemo\";\n"
  },
  {
    "path": "packages/website/components/hotkey-context.tsx",
    "content": "\"use client\";\n\nimport {\n  createContext,\n  useContext,\n  useState,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\nimport type { RecordedHotkey } from \"./grab-element-button\";\n\ninterface HotkeyContextValue {\n  customHotkey: RecordedHotkey | null;\n  setCustomHotkey: (hotkey: RecordedHotkey | null) => void;\n}\n\nconst HotkeyContext = createContext<HotkeyContextValue | null>(null);\n\ninterface HotkeyProviderProps {\n  children: ReactNode;\n}\n\nexport const HotkeyProvider = ({\n  children,\n}: HotkeyProviderProps): ReactElement => {\n  const [customHotkey, setCustomHotkey] = useState<RecordedHotkey | null>(null);\n\n  return (\n    <HotkeyContext.Provider value={{ customHotkey, setCustomHotkey }}>\n      {children}\n    </HotkeyContext.Provider>\n  );\n};\n\nHotkeyProvider.displayName = \"HotkeyProvider\";\n\nexport const useHotkey = (): HotkeyContextValue => {\n  const context = useContext(HotkeyContext);\n  if (!context) {\n    return { customHotkey: null, setCustomHotkey: () => {} };\n  }\n  return context;\n};\n"
  },
  {
    "path": "packages/website/components/icons/icon-claude.tsx",
    "content": "interface IconClaudeProps {\n  width?: number;\n  height?: number;\n  className?: string;\n}\n\nexport const IconClaude = ({\n  width = 14,\n  height = 14,\n  className = \"\",\n}: IconClaudeProps) => (\n  <svg\n    width={width}\n    height={height}\n    viewBox=\"0 0 256 257\"\n    fill=\"currentColor\"\n    className={className}\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <g clipPath=\"url(#clip0_24_2)\">\n      <path\n        d=\"M50.228 170.321L100.585 142.064L101.428 139.601L100.585 138.24H98.123L89.697 137.722L60.922 136.944L35.97 135.907L11.795 134.611L5.703 133.314L0 125.796L0.583 122.037L5.703 118.603L13.027 119.251L29.229 120.352L53.533 122.037L71.162 123.074L97.28 125.796H101.428L102.011 124.111L100.585 123.074L99.484 122.037L74.337 104.992L47.117 86.975L32.859 76.605L25.146 71.355L21.258 66.43L19.573 55.672L26.573 47.959L35.97 48.608L38.368 49.256L47.895 56.579L68.245 72.329L94.817 91.9L98.706 95.14L100.261 94.038L100.456 93.261L98.706 90.344L84.253 64.226L68.828 37.654L61.958 26.636L60.144 20.026C59.496 17.303 59.042 15.035 59.042 12.248L67.014 1.425L71.42 0L82.05 1.426L86.522 5.314L93.132 20.415L103.826 44.201L120.417 76.541L125.278 86.133L127.87 95.012L128.843 97.734H130.528V96.178L131.888 77.967L134.416 55.607L136.879 26.831L137.722 18.731L141.74 9.009L149.711 3.759L155.933 6.74L161.053 14.064L160.34 18.794L157.294 38.562L151.332 69.542L147.443 90.281H149.711L152.304 87.688L162.803 73.754L180.431 51.718L188.209 42.969L197.282 33.312L203.115 28.711H214.133L222.233 40.766L218.605 53.209L207.263 67.597L197.865 79.781L184.385 97.928L175.959 112.446L176.737 113.612L178.747 113.418L209.207 106.937L225.669 103.955L245.306 100.585L254.186 104.733L255.157 108.946L251.657 117.566L230.659 122.75L206.031 127.676L169.349 136.361L168.895 136.685L169.414 137.333L185.94 138.888L193.005 139.277H210.309L242.519 141.675L250.945 147.249L256 154.054L255.157 159.238L242.195 165.849L224.697 161.701L183.867 151.98L169.867 148.48H167.923V149.647L179.589 161.053L200.976 180.367L227.743 205.254L229.103 211.411L225.669 216.271L222.039 215.753L198.513 198.06L189.44 190.088L168.895 172.784H167.535V174.598L172.265 181.533L197.282 219.123L198.578 230.659L196.764 234.419L190.283 236.687L183.153 235.39L168.506 214.846L153.406 191.708L141.221 170.969L139.731 171.812L132.537 249.26L129.167 253.213L121.389 256.194L114.909 251.269L111.473 243.297L114.908 227.548L119.056 207.004L122.426 190.671L125.472 170.386L127.287 163.646L127.157 163.192L125.667 163.386L110.372 184.385L87.105 215.818L68.699 235.52L64.292 237.27L56.644 233.316L57.357 226.252L61.634 219.966L87.104 187.561L102.464 167.469L112.381 155.869L112.316 154.183H111.733L44.07 198.125L32.015 199.68L26.83 194.82L27.478 186.848L29.941 184.255L50.291 170.256L50.228 170.321Z\"\n        fill=\"#FB6C3E\"\n      />\n    </g>\n    <defs>\n      <clipPath id=\"clip0_24_2\">\n        <rect width=\"256\" height=\"257\" fill=\"white\" />\n      </clipPath>\n    </defs>\n  </svg>\n);\n\nIconClaude.displayName = \"IconClaude\";\n"
  },
  {
    "path": "packages/website/components/icons/icon-codex.tsx",
    "content": "interface IconCodexProps {\n  width?: number;\n  height?: number;\n  className?: string;\n}\n\nexport const IconCodex = ({\n  width = 14,\n  height = 14,\n  className = \"\",\n}: IconCodexProps) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 320 320\"\n    fill=\"currentColor\"\n    width={width}\n    height={height}\n    className={className}\n  >\n    <path d=\"m297.06 130.97c7.26-21.79 4.76-45.66-6.85-65.48-17.46-30.4-52.56-46.04-86.84-38.68-15.25-17.18-37.16-26.95-60.13-26.81-35.04-.08-66.13 22.48-76.91 55.82-22.51 4.61-41.94 18.7-53.31 38.67-17.59 30.32-13.58 68.54 9.92 94.54-7.26 21.79-4.76 45.66 6.85 65.48 17.46 30.4 52.56 46.04 86.84 38.68 15.24 17.18 37.16 26.95 60.13 26.8 35.06.09 66.16-22.49 76.94-55.86 22.51-4.61 41.94-18.7 53.31-38.67 17.57-30.32 13.55-68.51-9.94-94.51zm-120.28 168.11c-14.03.02-27.62-4.89-38.39-13.88.49-.26 1.34-.73 1.89-1.07l63.72-36.8c3.26-1.85 5.26-5.32 5.24-9.07v-89.83l26.93 15.55c.29.14.48.42.52.74v74.39c-.04 33.08-26.83 59.9-59.91 59.97zm-128.84-55.03c-7.03-12.14-9.56-26.37-7.15-40.18.47.28 1.3.79 1.89 1.13l63.72 36.8c3.23 1.89 7.23 1.89 10.47 0l77.79-44.92v31.1c.02.32-.13.63-.38.83l-64.41 37.19c-28.69 16.52-65.33 6.7-81.92-21.95zm-16.77-139.09c7-12.16 18.05-21.46 31.21-26.29 0 .55-.03 1.52-.03 2.2v73.61c-.02 3.74 1.98 7.21 5.23 9.06l77.79 44.91-26.93 15.55c-.27.18-.61.21-.91.08l-64.42-37.22c-28.63-16.58-38.45-53.21-21.95-81.89zm221.26 51.49-77.79-44.92 26.93-15.54c.27-.18.61-.21.91-.08l64.42 37.19c28.68 16.57 38.51 53.26 21.94 81.94-7.01 12.14-18.05 21.44-31.2 26.28v-75.81c.03-3.74-1.96-7.2-5.2-9.06zm26.8-40.34c-.47-.29-1.3-.79-1.89-1.13l-63.72-36.8c-3.23-1.89-7.23-1.89-10.47 0l-77.79 44.92v-31.1c-.02-.32.13-.63.38-.83l64.41-37.16c28.69-16.55 65.37-6.7 81.91 22 6.99 12.12 9.52 26.31 7.15 40.1zm-168.51 55.43-26.94-15.55c-.29-.14-.48-.42-.52-.74v-74.39c.02-33.12 26.89-59.96 60.01-59.94 14.01 0 27.57 4.92 38.34 13.88-.49.26-1.33.73-1.89 1.07l-63.72 36.8c-3.26 1.85-5.26 5.31-5.24 9.06l-.04 89.79zm14.63-31.54 34.65-20.01 34.65 20v40.01l-34.65 20-34.65-20z\" />\n  </svg>\n);\n\nIconCodex.displayName = \"IconCodex\";\n"
  },
  {
    "path": "packages/website/components/icons/icon-copilot.tsx",
    "content": "interface IconCopilotProps {\n  width?: number;\n  height?: number;\n  className?: string;\n}\n\nexport const IconCopilot = ({\n  width = 14,\n  height = 14,\n  className = \"\",\n}: IconCopilotProps) => (\n  <svg\n    fill=\"currentColor\"\n    fillRule=\"evenodd\"\n    height={height}\n    width={width}\n    viewBox=\"0 0 24 24\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    className={className}\n  >\n    <path d=\"M19.245 5.364c1.322 1.36 1.877 3.216 2.11 5.817.622 0 1.2.135 1.592.654l.73.964c.21.278.323.61.323.955v2.62c0 .339-.173.669-.453.868C20.239 19.602 16.157 21.5 12 21.5c-4.6 0-9.205-2.583-11.547-4.258-.28-.2-.452-.53-.453-.868v-2.62c0-.345.113-.679.321-.956l.73-.963c.392-.517.974-.654 1.593-.654l.029-.297c.25-2.446.81-4.213 2.082-5.52 2.461-2.54 5.71-2.851 7.146-2.864h.198c1.436.013 4.685.323 7.146 2.864zm-7.244 4.328c-.284 0-.613.016-.962.05-.123.447-.305.85-.57 1.108-1.05 1.023-2.316 1.18-2.994 1.18-.638 0-1.306-.13-1.851-.464-.516.165-1.012.403-1.044.996a65.882 65.882 0 00-.063 2.884l-.002.48c-.002.563-.005 1.126-.013 1.69.002.326.204.63.51.765 2.482 1.102 4.83 1.657 6.99 1.657 2.156 0 4.504-.555 6.985-1.657a.854.854 0 00.51-.766c.03-1.682.006-3.372-.076-5.053-.031-.596-.528-.83-1.046-.996-.546.333-1.212.464-1.85.464-.677 0-1.942-.157-2.993-1.18-.266-.258-.447-.661-.57-1.108-.32-.032-.64-.049-.96-.05zm-2.525 4.013c.539 0 .976.426.976.95v1.753c0 .525-.437.95-.976.95a.964.964 0 01-.976-.95v-1.752c0-.525.437-.951.976-.951zm5 0c.539 0 .976.426.976.95v1.753c0 .525-.437.95-.976.95a.964.964 0 01-.976-.95v-1.752c0-.525.437-.951.976-.951zM7.635 5.087c-1.05.102-1.935.438-2.385.906-.975 1.037-.765 3.668-.21 4.224.405.394 1.17.657 1.995.657h.09c.649-.013 1.785-.176 2.73-1.11.435-.41.705-1.433.675-2.47-.03-.834-.27-1.52-.63-1.813-.39-.336-1.275-.482-2.265-.394zm6.465.394c-.36.292-.6.98-.63 1.813-.03 1.037.24 2.06.675 2.47.968.957 2.136 1.104 2.776 1.11h.044c.825 0 1.59-.263 1.995-.657.555-.556.765-3.187-.21-4.224-.45-.468-1.335-.804-2.385-.906-.99-.088-1.875.058-2.265.394zM12 7.615c-.24 0-.525.015-.84.044.03.16.045.336.06.526l-.001.159a2.94 2.94 0 01-.014.25c.225-.022.425-.027.612-.028h.366c.187 0 .387.006.612.028-.015-.146-.015-.277-.015-.409.015-.19.03-.365.06-.526a9.29 9.29 0 00-.84-.044z\" />\n  </svg>\n);\n\nIconCopilot.displayName = \"IconCopilot\";\n"
  },
  {
    "path": "packages/website/components/icons/icon-cursor.tsx",
    "content": "interface IconCursorProps {\n  width?: number;\n  height?: number;\n  className?: string;\n}\n\nexport const IconCursor = ({\n  width = 14,\n  height = 14,\n  className = \"\",\n}: IconCursorProps) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 466.73 532.09\"\n    fill=\"currentColor\"\n    width={width}\n    height={height}\n    className={className}\n  >\n    <path d=\"M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z\" />\n  </svg>\n);\n\nIconCursor.displayName = \"IconCursor\";\n"
  },
  {
    "path": "packages/website/components/icons/icon-droid.tsx",
    "content": "interface IconDroidProps {\n  width?: number;\n  height?: number;\n  className?: string;\n}\n\nexport const IconDroid = ({\n  width = 14,\n  height = 14,\n  className = \"\",\n}: IconDroidProps) => (\n  <svg\n    width={width}\n    height={height}\n    viewBox=\"100 90 700 700\"\n    fill=\"currentColor\"\n    className={className}\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path d=\"M582.594 251.356C581.452 251.073 580.385 250.547 579.466 249.813C578.546 249.08 577.795 248.156 577.266 247.106C576.736 246.055 576.44 244.903 576.397 243.728C576.354 242.552 576.565 241.381 577.017 240.295C592.606 202.357 599.485 172.002 588.384 159.294C558.984 125.58 441.083 192.624 403.49 215.331C402.484 215.936 401.358 216.316 400.19 216.446C399.023 216.577 397.841 216.454 396.725 216.086C395.61 215.718 394.587 215.113 393.727 214.314C392.866 213.515 392.188 212.539 391.739 211.453C375.937 173.596 359.325 147.264 342.487 146.122C297.857 143.068 261.885 273.85 251.355 316.475C251.074 317.616 250.55 318.683 249.817 319.603C249.085 320.522 248.162 321.273 247.113 321.803C246.064 322.332 244.912 322.629 243.738 322.672C242.563 322.715 241.393 322.504 240.308 322.052C202.37 306.463 172.002 299.584 159.307 310.685C125.593 340.085 192.624 457.987 215.33 495.579C215.938 496.585 216.32 497.711 216.451 498.879C216.583 500.047 216.46 501.229 216.092 502.346C215.724 503.462 215.119 504.485 214.318 505.346C213.517 506.206 212.54 506.883 211.453 507.33C173.609 523.132 147.277 539.744 146.121 556.581C143.08 601.211 273.85 637.184 316.488 647.714C317.627 647.998 318.691 648.525 319.608 649.259C320.525 649.992 321.273 650.914 321.801 651.963C322.329 653.011 322.624 654.161 322.668 655.335C322.711 656.508 322.501 657.677 322.052 658.761C306.463 696.699 299.584 727.067 310.685 739.762C340.084 773.476 458 706.445 495.592 683.739C496.598 683.131 497.724 682.749 498.892 682.618C500.06 682.486 501.243 682.609 502.359 682.977C503.475 683.345 504.498 683.95 505.358 684.751C506.219 685.552 506.896 686.529 507.344 687.616C523.145 725.46 539.744 751.792 556.594 752.948C601.224 755.989 637.196 625.219 647.713 582.581C647.998 581.441 648.525 580.375 649.259 579.457C649.993 578.539 650.917 577.79 651.967 577.262C653.017 576.733 654.169 576.438 655.344 576.396C656.519 576.354 657.689 576.566 658.775 577.018C696.712 592.607 727.067 599.472 739.775 588.384C773.49 558.985 706.445 441.069 683.738 403.477C683.136 402.47 682.757 401.345 682.628 400.178C682.499 399.011 682.622 397.83 682.99 396.715C683.358 395.6 683.961 394.577 684.76 393.717C685.558 392.856 686.532 392.177 687.616 391.726C725.473 375.924 751.805 359.312 752.947 342.475C756.001 297.845 625.219 261.873 582.594 251.356ZM531.391 208.572C539.969 223.948 495.765 326.408 462.886 398.073C462.337 399.271 461.433 400.273 460.297 400.942C459.161 401.611 457.847 401.917 456.532 401.817C455.217 401.717 453.964 401.217 452.942 400.384C451.92 399.551 451.178 398.424 450.816 397.157C437.537 350.561 422.36 295.813 406.12 249.338C405.482 247.514 405.513 245.522 406.209 243.719C406.905 241.917 408.219 240.42 409.917 239.498C450.471 217.349 519.865 187.936 531.391 208.572ZM337.044 221.253C353.974 226.06 395.165 329.767 422.585 403.69C423.042 404.925 423.111 406.271 422.781 407.546C422.451 408.821 421.739 409.965 420.74 410.824C419.741 411.683 418.503 412.215 417.193 412.35C415.882 412.485 414.562 412.215 413.409 411.577C371.037 388.061 321.627 360.043 277.276 338.664C275.539 337.822 274.157 336.39 273.377 334.625C272.596 332.86 272.468 330.875 273.013 329.023C286.066 284.726 314.297 214.813 337.044 221.253ZM208.585 367.651C223.948 359.073 326.42 403.278 398.073 436.156C399.271 436.706 400.272 437.61 400.942 438.746C401.611 439.882 401.916 441.196 401.816 442.51C401.717 443.825 401.217 445.078 400.383 446.1C399.55 447.122 398.424 447.864 397.156 448.227C350.575 461.506 295.813 476.683 249.337 492.923C247.515 493.557 245.526 493.524 243.727 492.828C241.927 492.133 240.433 490.82 239.511 489.125C217.402 448.572 187.936 379.177 208.585 367.651ZM221.266 561.999C226.06 545.069 329.78 503.878 403.703 476.457C404.938 476 406.284 475.932 407.559 476.262C408.834 476.592 409.978 477.304 410.837 478.303C411.695 479.301 412.228 480.539 412.363 481.85C412.498 483.16 412.228 484.48 411.59 485.633C388.06 528.006 360.042 577.416 338.663 621.754C337.827 623.496 336.398 624.884 334.631 625.668C332.864 626.451 330.876 626.579 329.023 626.029C284.725 613.056 214.813 584.746 221.266 561.999ZM367.664 690.458C359.073 675.094 403.291 572.622 436.169 500.97C436.719 499.772 437.623 498.77 438.759 498.101C439.895 497.431 441.209 497.126 442.523 497.226C443.838 497.326 445.091 497.826 446.113 498.659C447.135 499.492 447.877 500.618 448.24 501.886C461.518 548.468 476.696 603.23 492.936 649.705C493.569 651.529 493.534 653.518 492.836 655.318C492.137 657.118 490.822 658.612 489.125 659.532C448.585 681.641 379.177 711.106 367.704 690.458H367.664ZM562.012 677.777C545.068 672.983 503.877 569.263 476.457 495.34C475.997 494.103 475.927 492.754 476.257 491.477C476.587 490.199 477.301 489.053 478.302 488.193C479.304 487.334 480.545 486.802 481.858 486.669C483.171 486.537 484.493 486.81 485.646 487.452C528.005 510.969 577.429 539 621.767 560.379C623.507 561.217 624.891 562.648 625.672 564.414C626.453 566.181 626.58 568.168 626.029 570.02C612.989 614.384 584.759 684.23 562.012 677.777ZM690.47 531.378C675.094 539.97 572.635 495.751 500.969 462.873C499.771 462.323 498.77 461.42 498.1 460.284C497.431 459.148 497.126 457.834 497.226 456.519C497.325 455.204 497.825 453.952 498.659 452.93C499.492 451.908 500.618 451.165 501.886 450.803C548.481 437.524 603.229 422.346 649.705 406.106C651.531 405.472 653.522 405.508 655.325 406.206C657.127 406.904 658.622 408.219 659.544 409.918C681.64 450.458 711.106 519.866 690.47 531.378ZM677.789 337.03C672.983 353.974 569.275 395.165 495.353 422.586C494.116 423.046 492.767 423.115 491.489 422.785C490.212 422.455 489.066 421.742 488.206 420.74C487.346 419.739 486.815 418.498 486.682 417.185C486.55 415.872 486.823 414.549 487.465 413.396C510.982 371.037 539 321.614 560.379 277.276C561.219 275.537 562.65 274.154 564.416 273.374C566.182 272.593 568.168 272.465 570.019 273.013C614.317 286.053 684.23 314.284 677.789 337.03Z\" />\n  </svg>\n);\n\nIconDroid.displayName = \"IconDroid\";\n"
  },
  {
    "path": "packages/website/components/icons/icon-github.tsx",
    "content": "interface IconGithubProps {\n  width?: number;\n  height?: number;\n  className?: string;\n}\n\nexport const IconGithub = ({\n  width = 16,\n  height = 16,\n  className = \"\",\n}: IconGithubProps) => (\n  <svg\n    width={width}\n    height={height}\n    className={className}\n    fill=\"currentColor\"\n    viewBox=\"0 0 24 24\"\n  >\n    <path\n      fillRule=\"evenodd\"\n      d=\"M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z\"\n      clipRule=\"evenodd\"\n    />\n  </svg>\n);\n\nIconGithub.displayName = \"IconGithub\";\n"
  },
  {
    "path": "packages/website/components/icons/icon-nextjs.tsx",
    "content": "interface IconNextjsProps {\n  width?: number;\n  height?: number;\n  className?: string;\n}\n\nexport const IconNextjs = ({\n  width = 14,\n  height = 14,\n  className = \"\",\n}: IconNextjsProps) => (\n  <span\n    className={`inline-flex grayscale opacity-50 ${className}`}\n    dangerouslySetInnerHTML={{\n      __html: `<svg viewBox=\"0 0 180 180\" width=\"${width}\" height=\"${height}\"><mask height=\"180\" id=\"nextjs_mask0\" maskUnits=\"userSpaceOnUse\" style=\"mask-type:alpha\" width=\"180\" x=\"0\" y=\"0\"><circle cx=\"90\" cy=\"90\" fill=\"black\" r=\"90\"></circle></mask><g mask=\"url(#nextjs_mask0)\"><circle cx=\"90\" cy=\"90\" data-circle=\"true\" fill=\"black\" r=\"90\" stroke=\"white\" stroke-width=\"6px\"></circle><path d=\"M149.508 157.52L69.142 54H54V125.97H66.1136V69.3836L139.999 164.845C143.333 162.614 146.509 160.165 149.508 157.52Z\" fill=\"url(#nextjs_paint0)\"></path><rect fill=\"url(#nextjs_paint1)\" height=\"72\" width=\"12\" x=\"115\" y=\"54\"></rect></g><defs><linearGradient gradientUnits=\"userSpaceOnUse\" id=\"nextjs_paint0\" x1=\"109\" x2=\"144.5\" y1=\"116.5\" y2=\"160.5\"><stop stop-color=\"white\"></stop><stop offset=\"1\" stop-color=\"white\" stop-opacity=\"0\"></stop></linearGradient><linearGradient gradientUnits=\"userSpaceOnUse\" id=\"nextjs_paint1\" x1=\"121\" x2=\"120.799\" y1=\"54\" y2=\"106.875\"><stop stop-color=\"white\"></stop><stop offset=\"1\" stop-color=\"white\" stop-opacity=\"0\"></stop></linearGradient></defs></svg>`,\n    }}\n  />\n);\n\nIconNextjs.displayName = \"IconNextjs\";\n"
  },
  {
    "path": "packages/website/components/icons/icon-opencode.tsx",
    "content": "interface IconOpenCodeProps {\n  width?: number;\n  height?: number;\n  className?: string;\n}\n\nexport const IconOpenCode = ({\n  width = 16,\n  height = 20,\n  className = \"\",\n}: IconOpenCodeProps) => (\n  <svg\n    width={width}\n    height={height}\n    viewBox=\"0 0 32 40\"\n    fill=\"none\"\n    className={className}\n  >\n    <g clipPath=\"url(#clip0_1311_94973)\">\n      <path d=\"M24 32H8V16H24V32Z\" fill=\"#4B4646\" />\n      <path d=\"M24 8H8V32H24V8ZM32 40H0V0H32V40Z\" fill=\"#F1ECEC\" />\n    </g>\n    <defs>\n      <clipPath id=\"clip0_1311_94973\">\n        <rect width=\"32\" height=\"40\" fill=\"white\" />\n      </clipPath>\n    </defs>\n  </svg>\n);\n\nIconOpenCode.displayName = \"IconOpenCode\";\n"
  },
  {
    "path": "packages/website/components/icons/icon-tanstack.tsx",
    "content": "interface IconTanstackProps {\n  width?: number;\n  height?: number;\n  className?: string;\n}\n\nexport const IconTanstack = ({\n  width = 14,\n  height = 14,\n  className = \"\",\n}: IconTanstackProps) => (\n  <span\n    className={`inline-flex grayscale opacity-50 ${className}`}\n    dangerouslySetInnerHTML={{\n      __html: `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 663 660\" width=\"${width}\" height=\"${height}\"><path d=\"m305.114318.62443771c8.717817-1.14462121 17.926803-.36545135 26.712694-.36545135 32.548987 0 64.505987 5.05339923 95.64868 14.63098274 39.74418 12.2236582 76.762804 31.7666864 109.435876 57.477568 40.046637 31.5132839 73.228974 72.8472109 94.520714 119.2362609 39.836383 86.790386 39.544267 191.973146-1.268422 278.398081-26.388695 55.880442-68.724007 102.650458-119.964986 136.75724-41.808813 27.828603-90.706831 44.862601-140.45707 50.89341-63.325458 7.677926-131.784923-3.541603-188.712259-32.729444-106.868873-54.795293-179.52309291-165.076271-180.9604082-285.932068-.27660564-23.300971.08616998-46.74071 4.69884909-69.814998 7.51316071-37.57857 20.61272131-73.903917 40.28618971-106.877282 21.2814003-35.670293 48.7704861-67.1473767 81.6882804-92.5255597 38.602429-29.7610135 83.467691-51.1674988 130.978372-62.05777669 11.473831-2.62966514 22.9946-4.0869914 34.57273-5.4964306l3.658171-.44480576c3.050084-.37153079 6.104217-.74794222 9.162589-1.14972654zm-110.555861 549.44131429c-14.716752 1.577863-30.238964 4.25635-42.869928 12.522173 2.84343.683658 6.102369.004954 9.068638 0 7.124652-.011559 14.317732-.279903 21.434964.032202 17.817402.781913 36.381729 3.63214 53.58741 8.350042 22.029372 6.040631 41.432961 17.928687 62.656049 25.945156 22.389644 8.456554 44.67706 11.084675 68.427 11.084675 11.96813 0 23.845573-.035504 35.450133-3.302696-6.056202-3.225083-14.72582-2.619864-21.434964-3.963236-14.556814-2.915455-28.868774-6.474936-42.869928-11.470264-10.304996-3.676672-20.230803-8.214291-30.11097-12.848661l-6.348531-2.985046c-9.1705-4.309263-18.363277-8.560752-27.845391-12.142608-24.932161-9.418465-52.560181-14.071964-79.144482-11.221737zm22.259385-62.614168c-29.163917 0-58.660076 5.137344-84.915434 18.369597-6.361238 3.206092-12.407546 7.02566-18.137277 11.258891-1.746125 1.290529-4.841829 2.948483-5.487351 5.191839-.654591 2.275558 1.685942 4.182039 3.014086 5.637703 6.562396-3.497556 12.797498-7.199878 19.78612-9.855246 45.19892-17.169893 99.992458-13.570779 145.098218 2.172348 22.494346 7.851335 43.219483 19.592421 65.129314 28.800338 24.503461 10.297807 49.53043 16.975034 75.846795 20.399104 31.04195 4.037546 66.433549.7654 94.808495-13.242161 9.970556-4.921843 23.814245-12.422267 28.030337-23.320339-5.207047.454947-9.892236 2.685918-14.83959 4.224149-7.866632 2.445646-15.827248 4.51974-23.908229 6.138887-27.388113 5.486604-56.512458 6.619429-84.091013 1.639788-25.991939-4.693152-50.142596-14.119246-74.179513-24.03502l-3.068058-1.268177c-2.045137-.846788-4.089983-1.695816-6.135603-2.544467l-3.069142-1.272366c-12.279956-5.085721-24.606928-10.110797-37.210937-14.51024-24.485325-8.546552-50.726667-13.784628-76.671218-13.784628zm51.114145-447.9909432c-34.959602 7.7225298-66.276908 22.7605319-96.457338 41.7180089-17.521434 11.0054099-34.281927 22.2799893-49.465301 36.4444283-22.5792616 21.065423-39.8360564 46.668751-54.8866988 73.411509-15.507372 27.55357-25.4498976 59.665686-30.2554517 90.824149-4.7140432 30.568106-5.4906485 62.70747-.0906864 93.301172 6.7503648 38.248526 19.5989769 74.140579 39.8896436 107.337631 6.8187918-3.184625 11.659796-10.445603 17.3128555-15.336896 11.4149428-9.875888 23.3995608-19.029311 36.2745548-26.928535 4.765981-2.923712 9.662222-5.194315 14.83959-7.275014 1.953055-.785216 5.14604-1.502727 6.06527-3.647828 1.460876-3.406732-1.240754-9.335897-1.704904-12.865654-1.324845-10.095517-2.124534-20.362774-1.874735-30.549941.725492-29.668947 6.269727-59.751557 16.825623-87.521453 7.954845-20.924233 20.10682-39.922168 34.502872-56.971512 4.884699-5.785498 10.077731-11.170545 15.437296-16.512656 3.167428-3.157378 7.098271-5.858983 9.068639-9.908915-10.336599.006606-20.674847 2.987289-30.503603 6.013385-21.174447 6.519522-41.801477 16.19312-59.358362 29.841512-8.008432 6.226409-13.873368 14.387371-21.44733 20.939921-2.32322 2.010516-6.484901 4.704691-9.695199 3.187928-4.8500728-2.29042-4.1014979-11.835213-4.6571581-16.222019-2.1369011-16.873476 4.2548401-38.216325 12.3778671-52.843142 13.039878-23.479694 37.150915-43.528712 65.467327-42.82854 12.228647.302197 22.934587 4.551115 34.625711 7.324555-2.964621-4.211764-6.939158-7.28162-10.717482-10.733763-9.257431-8.459031-19.382979-16.184864-30.503603-22.028985-4.474136-2.350694-9.291232-3.77911-14.015169-5.506421-2.375159-.867783-5.36616-2.062533-6.259834-4.702213-1.654614-4.888817 7.148561-9.416813 10.381943-11.478522 12.499882-7.969406 27.826705-14.525258 42.869928-14.894334 23.509209-.577147 46.479246 12.467678 56.162903 34.665926 3.404469 7.803171 4.411273 16.054969 5.079109 24.382907l.121749 1.56229.174325 2.345587c.01913.260708.038244.521433.057403.782164l.11601 1.56437.120128 1.563971c7.38352-6.019164 12.576553-14.876995 19.78612-21.323859 16.861073-15.07846 39.936636-21.7722 61.831627-14.984333 19.786945 6.133107 36.984382 19.788105 47.105807 37.959541 2.648042 4.754231 10.035685 16.373942 4.698379 21.109183-4.177345 3.707277-9.475079.818243-13.880788-.719162-3.33605-1.16376-6.782939-1.90214-10.241828-2.585698l-1.887262-.369639c-.629089-.122886-1.257979-.246187-1.886079-.372129-11.980496-2.401886-25.91652-2.152533-37.923398-.041284-7.762754 1.364839-15.349083 4.127545-23.083807 5.271929v1.651348c21.149714.175043 41.608563 12.240618 52.043268 30.549941 4.323267 7.585468 6.482428 16.267431 8.138691 24.770223 2.047864 10.50918.608423 21.958802-2.263037 32.201289-.962925 3.433979-2.710699 9.255807-6.817143 10.046802-2.902789.558982-5.36781-2.330878-7.024898-4.279468-4.343878-5.10762-8.475879-9.96341-13.573278-14.374161-12.895604-11.157333-26.530715-21.449361-40.396663-31.373138-7.362086-5.269452-15.425755-12.12007-23.908229-15.340199 2.385052 5.745041 4.721463 11.086326 5.532694 17.339156 2.385876 18.392716-5.314223 35.704625-16.87179 49.540445-3.526876 4.222498-7.29943 8.475545-11.744712 11.755948-1.843407 1.360711-4.156734 3.137561-6.595373 2.752797-7.645687-1.207961-8.555849-12.73272-9.728176-18.637115-3.970415-19.998652-2.375984-39.861068 3.132802-59.448534-4.901187 2.485279-8.443727 7.923994-11.521293 12.385111-6.770975 9.816439-12.645804 20.199291-16.858599 31.375615-16.777806 44.519521-16.616219 96.664142 5.118834 139.523233 2.427098 4.786433 6.110614 4.144058 10.894733 4.144058.720854 0 1.44257-.004515 2.164851-.010924l2.168232-.022283c4.338648-.045438 8.686803-.064635 12.979772.508795 2.227588.297243 5.320818.032202 7.084256 1.673642 2.111344 1.966755.986008 5.338808.4996 7.758859-1.358647 6.765574-1.812904 12.914369-1.812904 19.816178 9.02412-1.398692 11.525415-15.866153 14.724172-23.118874 3.624982-8.216283 7.313444-16.440823 10.667192-24.770223 1.648843-4.093692 3.854171-8.671229 3.275427-13.210785-.649644-5.10184-4.335633-10.510831-6.904531-14.862134-4.86244-8.234447-10.389363-16.70834-13.969002-25.595896-2.861567-7.104926-.197036-15.983399 7.871579-18.521521 4.450228-1.400344 9.198073 1.345848 12.094266 4.562675 6.07269 6.74328 9.992815 16.777697 14.401823 24.692609l34.394873 61.925556c2.920926 5.243856 5.848447 10.481933 8.836976 15.687808 1.165732 2.031158 2.352075 5.167068 4.740424 6.0332 2.127008.77118 5.033095-.325315 7.148561-.748886 5.492297-1.099798 10.97635-2.287117 16.488434-3.28288 6.605266-1.193099 16.673928-.969342 21.434964-6.129805-6.963066-2.205375-15.011895-2.074919-22.259386-1.577863-4.352947.298894-9.178287 1.856116-13.178381-.686135-5.953149-3.783239-9.910373-12.522173-13.552668-18.377854-8.980425-14.439388-17.441465-29.095929-26.041008-43.760726l-1.376261-2.335014-2.765943-4.665258c-1.380597-2.334387-2.750786-4.67476-4.079753-7.036188-1.02723-1.826391-2.549937-4.233231-1.078344-6.24705 1.545791-2.114476 4.91472-2.239146 7.956473-2.243117l.603351.000261c1.195428.001526 2.315572.002427 3.222811-.11692 12.27399-1.615019 24.718635-2.952611 37.098976-2.952611-.963749-3.352237-3.719791-7.141255-2.838484-10.73046 1.972017-8.030506 13.526287-10.543033 18.899867-4.780653 3.60767 3.868283 5.704174 9.192229 8.051303 13.859765 3.097352 6.162006 6.624228 12.118418 9.940876 18.16483 5.805578 10.585967 12.146205 20.881297 18.116667 31.375615.49237.865561.999687 1.726685 1.512269 2.587098l.771613 1.290552c2.577138 4.303168 5.164895 8.635123 6.553094 13.461506-20.735854-.9487-36.30176-25.018751-45.343193-41.283704-.721369 2.604176.450959 4.928448 1.388326 7.431066 1.948109 5.197619 4.276275 10.147535 7.20627 14.862134 4.184765 6.732546 8.982075 13.665732 15.313633 18.553722 11.236043 8.673707 26.05255 8.721596 39.572241 7.794364 8.669619-.595311 19.50252-4.542034 28.030338-1.864372 8.513803 2.673532 11.940924 12.063098 6.884745 19.276187-3.787393 5.403211-8.842747 7.443452-15.128962 8.257566 4.445282 9.53571 10.268996 18.385285 14.490036 28.072919 1.758491 4.035895 3.59118 10.22102 7.8048 12.350433 2.805507 1.416857 6.824562.09743 9.85761.034678-3.043765-8.053625-8.742992-14.887729-11.541904-23.118874 8.533589.390544 16.786875 4.843404 24.732651 7.685374 15.630376 5.590144 31.063836 11.701854 46.475333 17.86913l7.112077 2.848685c6.338978 2.538947 12.71588 5.052299 18.961699 7.812528 2.285297 1.009799 5.449427 3.370401 7.975455 1.917215 2.061054-1.186494 3.394144-4.015253 4.665403-5.931643 3.55573-5.361927 6.775921-10.928622 9.965609-16.513481 12.774414-22.36586 22.143967-46.872692 28.402976-71.833646 20.645168-82.323009 2.934117-173.156241-46.677107-241.922507-19.061454-26.420745-43.033164-49.262193-69.46165-68.1783861-66.13923-47.336721-152.911262-66.294198-232.486917-48.7172481zm135.205158 410.5292842c-17.532977 4.570931-35.601827 8.714164-53.58741 11.040088 2.365265 8.052799 8.145286 15.885969 12.376218 23.118874 1.635653 2.796558 3.3859 6.541816 6.618457 7.755557 3.651364 1.370619 8.063669-.853747 11.508927-1.975838-1.595256-4.364513-4.279573-8.292245-6.476657-12.385112-.905215-1.687677-2.305907-3.685809-1.559805-5.68972 1.410585-3.786541 7.266452-3.563609 10.509727-4.221671 8.54678-1.733916 17.004522-3.898008 25.557073-5.611281 3.150939-.631641 7.538512-2.342438 10.705115-1.285575 2.371037.791232 3.800147 2.744743 5.152304 4.781948l.606196.918752c.80912 1.222827 1.637246 2.41754 2.671212 3.351165 3.457625 3.121874 8.628398 3.60159 13.017619 4.453686-2.678546-6.027421-7.130424-11.301001-9.984571-17.339156-1.659561-3.511592-3.023155-8.677834-6.656381-10.707341-5.005064-2.795733-15.341663 2.461334-20.458024 3.795624zm-110.472507-40.151706c-.825246 10.467897-4.036369 18.984725-9.068639 28.072919 5.76683.729896 11.649079.989984 17.312856 2.39363 4.244947 1.051908 8.156828 3.058296 12.366325 4.211763-2.250671-6.157877-6.426367-11.651913-9.661398-17.339156-3.266358-5.740912-6.189758-12.717032-10.949144-17.339156z\" fill=\"#fff\" transform=\"translate(.9778)\"/></svg>`,\n    }}\n  />\n);\n\nIconTanstack.displayName = \"IconTanstack\";\n"
  },
  {
    "path": "packages/website/components/icons/icon-vite.tsx",
    "content": "interface IconViteProps {\n  width?: number;\n  height?: number;\n  className?: string;\n}\n\nexport const IconVite = ({\n  width = 14,\n  height = 14,\n  className = \"\",\n}: IconViteProps) => (\n  <span\n    className={`inline-flex grayscale opacity-50 ${className}`}\n    dangerouslySetInnerHTML={{\n      __html: `<svg fill=\"none\" viewBox=\"0 0 48 46\" width=\"${width}\" height=\"${height}\"><path fill=\"#863bff\" d=\"M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z\" style=\"fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1\"/><mask id=\"vite__mask0_2002_17158\" width=\"48\" height=\"46\" x=\"0\" y=\"0\" maskUnits=\"userSpaceOnUse\" style=\"mask-type:alpha\"><path fill=\"#000\" d=\"M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z\" style=\"fill:#000;fill-opacity:1\"/></mask><g mask=\"url(#vite__mask0_2002_17158)\"><g filter=\"url(#vite__filter0_f_2002_17158)\"><ellipse cx=\"5.508\" cy=\"14.704\" fill=\"#ede6ff\" rx=\"5.508\" ry=\"14.704\" transform=\"matrix(.00324 1 1 -.00324 -4.47 31.516)\"/></g><g filter=\"url(#vite__filter1_f_2002_17158)\"><ellipse cx=\"10.399\" cy=\"29.851\" fill=\"#ede6ff\" rx=\"10.399\" ry=\"29.851\" transform=\"matrix(.00324 1 1 -.00324 -39.328 7.883)\"/></g><g filter=\"url(#vite__filter2_f_2002_17158)\"><ellipse cx=\"5.508\" cy=\"30.487\" fill=\"#7e14ff\" rx=\"5.508\" ry=\"30.487\" transform=\"rotate(89.814 -25.913 -14.639)scale(1 -1)\"/></g><g filter=\"url(#vite__filter3_f_2002_17158)\"><ellipse cx=\"5.508\" cy=\"30.599\" fill=\"#7e14ff\" rx=\"5.508\" ry=\"30.599\" transform=\"rotate(89.814 -32.644 -3.334)scale(1 -1)\"/></g><g filter=\"url(#vite__filter4_f_2002_17158)\"><ellipse cx=\"5.508\" cy=\"30.599\" fill=\"#7e14ff\" rx=\"5.508\" ry=\"30.599\" transform=\"rotate(89.814 -32.454 -1.99)scale(1 -1)\"/></g><g filter=\"url(#vite__filter5_f_2002_17158)\"><ellipse cx=\"14.072\" cy=\"22.078\" fill=\"#ede6ff\" rx=\"14.072\" ry=\"22.078\" transform=\"rotate(93.35 24.506 48.493)scale(-1 1)\"/></g><g filter=\"url(#vite__filter6_f_2002_17158)\"><ellipse cx=\"3.47\" cy=\"21.501\" fill=\"#7e14ff\" rx=\"3.47\" ry=\"21.501\" transform=\"rotate(89.009 28.708 47.59)scale(-1 1)\"/></g><g filter=\"url(#vite__filter7_f_2002_17158)\"><ellipse cx=\"3.47\" cy=\"21.501\" fill=\"#7e14ff\" rx=\"3.47\" ry=\"21.501\" transform=\"rotate(89.009 28.708 47.59)scale(-1 1)\"/></g><g filter=\"url(#vite__filter8_f_2002_17158)\"><ellipse cx=\".387\" cy=\"8.972\" fill=\"#7e14ff\" rx=\"4.407\" ry=\"29.108\" transform=\"rotate(39.51 .387 8.972)\"/></g><g filter=\"url(#vite__filter9_f_2002_17158)\"><ellipse cx=\"47.523\" cy=\"-6.092\" fill=\"#7e14ff\" rx=\"4.407\" ry=\"29.108\" transform=\"rotate(37.892 47.523 -6.092)\"/></g><g filter=\"url(#vite__filter10_f_2002_17158)\"><ellipse cx=\"41.412\" cy=\"6.333\" fill=\"#47bfff\" rx=\"5.971\" ry=\"9.665\" transform=\"rotate(37.892 41.412 6.333)\"/></g><g filter=\"url(#vite__filter11_f_2002_17158)\"><ellipse cx=\"-1.879\" cy=\"38.332\" fill=\"#7e14ff\" rx=\"4.407\" ry=\"29.108\" transform=\"rotate(37.892 -1.88 38.332)\"/></g><g filter=\"url(#vite__filter12_f_2002_17158)\"><ellipse cx=\"-1.879\" cy=\"38.332\" fill=\"#7e14ff\" rx=\"4.407\" ry=\"29.108\" transform=\"rotate(37.892 -1.88 38.332)\"/></g><g filter=\"url(#vite__filter13_f_2002_17158)\"><ellipse cx=\"35.651\" cy=\"29.907\" fill=\"#7e14ff\" rx=\"4.407\" ry=\"29.108\" transform=\"rotate(37.892 35.651 29.907)\"/></g><g filter=\"url(#vite__filter14_f_2002_17158)\"><ellipse cx=\"38.418\" cy=\"32.4\" fill=\"#47bfff\" rx=\"5.971\" ry=\"15.297\" transform=\"rotate(37.892 38.418 32.4)\"/></g></g><defs><filter id=\"vite__filter0_f_2002_17158\" width=\"60.045\" height=\"41.654\" x=\"-19.77\" y=\"16.149\" color-interpolation-filters=\"sRGB\" filterUnits=\"userSpaceOnUse\"><feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/><feBlend in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/><feGaussianBlur result=\"effect1_foregroundBlur_2002_17158\" stdDeviation=\"7.659\"/></filter><filter id=\"vite__filter1_f_2002_17158\" width=\"90.34\" height=\"51.437\" x=\"-54.613\" y=\"-7.533\" color-interpolation-filters=\"sRGB\" filterUnits=\"userSpaceOnUse\"><feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/><feBlend in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/><feGaussianBlur result=\"effect1_foregroundBlur_2002_17158\" stdDeviation=\"7.659\"/></filter><filter id=\"vite__filter2_f_2002_17158\" width=\"79.355\" height=\"29.4\" x=\"-49.64\" y=\"2.03\" color-interpolation-filters=\"sRGB\" filterUnits=\"userSpaceOnUse\"><feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/><feBlend in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/><feGaussianBlur result=\"effect1_foregroundBlur_2002_17158\" stdDeviation=\"4.596\"/></filter><filter id=\"vite__filter3_f_2002_17158\" width=\"79.579\" height=\"29.4\" x=\"-45.045\" y=\"20.029\" color-interpolation-filters=\"sRGB\" filterUnits=\"userSpaceOnUse\"><feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/><feBlend in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/><feGaussianBlur result=\"effect1_foregroundBlur_2002_17158\" stdDeviation=\"4.596\"/></filter><filter id=\"vite__filter4_f_2002_17158\" width=\"79.579\" height=\"29.4\" x=\"-43.513\" y=\"21.178\" color-interpolation-filters=\"sRGB\" filterUnits=\"userSpaceOnUse\"><feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/><feBlend in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/><feGaussianBlur result=\"effect1_foregroundBlur_2002_17158\" stdDeviation=\"4.596\"/></filter><filter id=\"vite__filter5_f_2002_17158\" width=\"74.749\" height=\"58.852\" x=\"15.756\" y=\"-17.901\" color-interpolation-filters=\"sRGB\" filterUnits=\"userSpaceOnUse\"><feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/><feBlend in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/><feGaussianBlur result=\"effect1_foregroundBlur_2002_17158\" stdDeviation=\"7.659\"/></filter><filter id=\"vite__filter6_f_2002_17158\" width=\"61.377\" height=\"25.362\" x=\"23.548\" y=\"2.284\" color-interpolation-filters=\"sRGB\" filterUnits=\"userSpaceOnUse\"><feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/><feBlend in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/><feGaussianBlur result=\"effect1_foregroundBlur_2002_17158\" stdDeviation=\"4.596\"/></filter><filter id=\"vite__filter7_f_2002_17158\" width=\"61.377\" height=\"25.362\" x=\"23.548\" y=\"2.284\" color-interpolation-filters=\"sRGB\" filterUnits=\"userSpaceOnUse\"><feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/><feBlend in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/><feGaussianBlur result=\"effect1_foregroundBlur_2002_17158\" stdDeviation=\"4.596\"/></filter><filter id=\"vite__filter8_f_2002_17158\" width=\"56.045\" height=\"63.649\" x=\"-27.636\" y=\"-22.853\" color-interpolation-filters=\"sRGB\" filterUnits=\"userSpaceOnUse\"><feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/><feBlend in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/><feGaussianBlur result=\"effect1_foregroundBlur_2002_17158\" stdDeviation=\"4.596\"/></filter><filter id=\"vite__filter9_f_2002_17158\" width=\"54.814\" height=\"64.646\" x=\"20.116\" y=\"-38.415\" color-interpolation-filters=\"sRGB\" filterUnits=\"userSpaceOnUse\"><feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/><feBlend in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/><feGaussianBlur result=\"effect1_foregroundBlur_2002_17158\" stdDeviation=\"4.596\"/></filter><filter id=\"vite__filter10_f_2002_17158\" width=\"33.541\" height=\"35.313\" x=\"24.641\" y=\"-11.323\" color-interpolation-filters=\"sRGB\" filterUnits=\"userSpaceOnUse\"><feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/><feBlend in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/><feGaussianBlur result=\"effect1_foregroundBlur_2002_17158\" stdDeviation=\"4.596\"/></filter><filter id=\"vite__filter11_f_2002_17158\" width=\"54.814\" height=\"64.646\" x=\"-29.286\" y=\"6.009\" color-interpolation-filters=\"sRGB\" filterUnits=\"userSpaceOnUse\"><feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/><feBlend in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/><feGaussianBlur result=\"effect1_foregroundBlur_2002_17158\" stdDeviation=\"4.596\"/></filter><filter id=\"vite__filter12_f_2002_17158\" width=\"54.814\" height=\"64.646\" x=\"-29.286\" y=\"6.009\" color-interpolation-filters=\"sRGB\" filterUnits=\"userSpaceOnUse\"><feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/><feBlend in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/><feGaussianBlur result=\"effect1_foregroundBlur_2002_17158\" stdDeviation=\"4.596\"/></filter><filter id=\"vite__filter13_f_2002_17158\" width=\"54.814\" height=\"64.646\" x=\"8.244\" y=\"-2.416\" color-interpolation-filters=\"sRGB\" filterUnits=\"userSpaceOnUse\"><feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/><feBlend in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/><feGaussianBlur result=\"effect1_foregroundBlur_2002_17158\" stdDeviation=\"4.596\"/></filter><filter id=\"vite__filter14_f_2002_17158\" width=\"39.409\" height=\"43.623\" x=\"18.713\" y=\"10.588\" color-interpolation-filters=\"sRGB\" filterUnits=\"userSpaceOnUse\"><feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/><feBlend in=\"SourceGraphic\" in2=\"BackgroundImageFix\" result=\"shape\"/><feGaussianBlur result=\"effect1_foregroundBlur_2002_17158\" stdDeviation=\"4.596\"/></filter></defs></svg>`,\n    }}\n  />\n);\n\nIconVite.displayName = \"IconVite\";\n"
  },
  {
    "path": "packages/website/components/icons/icon-vscode.tsx",
    "content": "interface IconVSCodeProps {\n  width?: number;\n  height?: number;\n  className?: string;\n}\n\nexport const IconVSCode = ({\n  width = 16,\n  height = 16,\n  className = \"\",\n}: IconVSCodeProps) => (\n  <svg\n    width={width}\n    height={height}\n    viewBox=\"0 0 24 24\"\n    fill=\"currentColor\"\n    className={className}\n  >\n    <path d=\"M23.15 2.587L18.21.21a1.49 1.49 0 0 0-1.705.29l-9.46 8.63l-4.12-3.128a1 1 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12L.326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a1 1 0 0 0 1.276.057l4.12-3.128l9.46 8.63a1.49 1.49 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352m-5.146 14.861L10.826 12l7.178-5.448z\" />\n  </svg>\n);\n\nIconVSCode.displayName = \"IconVSCode\";\n"
  },
  {
    "path": "packages/website/components/icons/icon-webstorm.tsx",
    "content": "interface IconWebStormProps {\n  width?: number;\n  height?: number;\n  className?: string;\n}\n\nexport const IconWebStorm = ({\n  width = 16,\n  height = 16,\n  className = \"\",\n}: IconWebStormProps) => (\n  <svg\n    width={width}\n    height={height}\n    viewBox=\"0 0 24 24\"\n    fill=\"currentColor\"\n    className={className}\n  >\n    <path d=\"M0 0v24h24V0zm17.889 2.889c1.444 0 2.667.444 3.667 1.278l-1.111 1.667c-.889-.611-1.722-1-2.556-1s-1.278.389-1.278.889v.056c0 .667.444.889 2.111 1.333c2 .556 3.111 1.278 3.111 3v.056c0 2-1.5 3.111-3.611 3.111c-1.5-.056-3-.611-4.167-1.667l1.278-1.556c.889.722 1.833 1.222 2.944 1.222c.889 0 1.389-.333 1.389-.944v-.056c0-.556-.333-.833-2-1.278c-2-.5-3.222-1.056-3.222-3.056v-.056c0-1.833 1.444-3 3.444-3zm-16.111.222h2.278l1.5 5.778l1.722-5.778h1.667l1.667 5.778l1.5-5.778h2.333l-2.833 9.944H9.723L8.112 7.277l-1.667 5.778H4.612zm.5 16.389h9V21h-9z\" />\n  </svg>\n);\n\nIconWebStorm.displayName = \"IconWebStorm\";\n"
  },
  {
    "path": "packages/website/components/icons/icon-zed.tsx",
    "content": "interface IconZedProps {\n  width?: number;\n  height?: number;\n  className?: string;\n}\n\nexport const IconZed = ({\n  width = 16,\n  height = 16,\n  className = \"\",\n}: IconZedProps) => (\n  <svg\n    width={width}\n    height={height}\n    viewBox=\"0 0 24 24\"\n    fill=\"currentColor\"\n    className={className}\n  >\n    <path d=\"M2.25 1.5a.75.75 0 0 0-.75.75v16.5H0V2.25A2.25 2.25 0 0 1 2.25 0h20.095c1.002 0 1.504 1.212.795 1.92L10.764 14.298h3.486V12.75h1.5v1.922a1.125 1.125 0 0 1-1.125 1.125H9.264l-2.578 2.578h11.689V9h1.5v9.375a1.5 1.5 0 0 1-1.5 1.5H5.185L2.562 22.5H21.75a.75.75 0 0 0 .75-.75V5.25H24v16.5A2.25 2.25 0 0 1 21.75 24H1.655C.653 24 .151 22.788.86 22.08L13.19 9.75H9.75v1.5h-1.5V9.375A1.125 1.125 0 0 1 9.375 8.25h5.314l2.625-2.625H5.625V15h-1.5V5.625a1.5 1.5 0 0 1 1.5-1.5h13.19L21.438 1.5z\" />\n  </svg>\n);\n\nIconZed.displayName = \"IconZed\";\n"
  },
  {
    "path": "packages/website/components/install-tabs.tsx",
    "content": "\"use client\";\n\nimport {\n  useEffect,\n  useState,\n  useCallback,\n  useRef,\n  type ReactElement,\n} from \"react\";\nimport { Copy, Check, Terminal, ChevronDown } from \"lucide-react\";\nimport {\n  COPY_FEEDBACK_DURATION_MS,\n  PROMPT_INSTALL_COLLAPSE_LINE_THRESHOLD,\n  PROMPT_INSTALL_MAX_HEIGHT_PX,\n} from \"@/constants\";\nimport { cn } from \"@/utils/cn\";\nimport { IconNextjs } from \"./icons/icon-nextjs\";\nimport { IconVite } from \"./icons/icon-vite\";\nimport { IconTanstack } from \"./icons/icon-tanstack\";\nimport { detectMobile } from \"@/utils/detect-mobile\";\nimport { hotkeyToString } from \"@/utils/hotkey-to-string\";\nimport type { RecordedHotkey } from \"./grab-element-button\";\nimport { useHotkey } from \"./hotkey-context\";\nimport { highlightCode } from \"../lib/shiki\";\n\ninterface InlineCodeProps {\n  children: React.ReactNode;\n}\n\nconst InlineCode = ({ children }: InlineCodeProps): ReactElement => (\n  <code className=\"rounded bg-muted px-1.5 py-0.5 font-mono text-xs text-muted-foreground\">\n    {children}\n  </code>\n);\n\nInlineCode.displayName = \"InlineCode\";\n\ninterface InstallTab {\n  id: string;\n  label: string;\n  description: React.ReactNode;\n  variant: \"code\" | \"command\" | \"prompt\";\n  lang?: \"tsx\" | \"bash\";\n  getCode: (hotkey: RecordedHotkey | null) => string;\n  getChangedLines: (hotkey: RecordedHotkey | null) => number[];\n}\n\nconst formatInitOptions = (hotkey: RecordedHotkey): string => {\n  return `{ activationKey: \"${hotkeyToString(hotkey)}\" }`;\n};\n\nconst createPromptInstallInstructions = (\n  hotkey: RecordedHotkey | null,\n): string => {\n  const activationKeyInstruction = hotkey\n    ? `Set the activation key to \"${hotkeyToString(hotkey)}\". For Next.js Script tags, set data-options='{\"activationKey\":\"${hotkeyToString(\n        hotkey,\n      )}\"}'. For Vite & TanStack Start, use react-grab/core and call init(${formatInitOptions(\n        hotkey,\n      )}).`\n    : \"Keep the default activation key.\";\n\n  return `Set up React Grab in this workspace. If this is a monorepo, ask me which package to install it in.\n\n1. Find the React app package in this workspace.\n2. If this is a monorepo, ask me which package to configure before making changes.\n3. Detect the package manager from lockfiles and install react-grab@latest in the selected package.\n4. Detect the framework and apply manual integration:\n- Next.js App Router: in app/layout.tsx, add next/script and include a development-only Script tag with src=\"//unpkg.com/react-grab/dist/index.global.js\", crossOrigin=\"anonymous\", and strategy=\"beforeInteractive\".\n- Next.js Pages Router: in pages/_document.tsx, add the same development-only Script tag inside <Head>.\n- Vite: in index.html <head>, add <script type=\"module\">if (import.meta.env.DEV) { import(\"react-grab\"); }</script>.\n- TanStack Start: in src/routes/__root.tsx, inside useEffect and an import.meta.env.DEV check, run void import(\"react-grab\").\n5. ${activationKeyInstruction}\n6. Keep all React Grab setup development-only.\n7. Show me the edited files and the exact install command you ran.\n8. If the setup does not work, suggest running the React Grab CLI:\n\\`npx -y grab@latest init\\`.`;\n};\n\nconst installTabsData: InstallTab[] = [\n  {\n    id: \"cli\",\n    label: \"CLI\",\n    description: \"Run this command at your project root\",\n    variant: \"command\",\n    lang: \"bash\",\n    getCode: (hotkey) => {\n      if (hotkey) {\n        return `npx -y grab@latest init --key \"${hotkeyToString(hotkey)}\"`;\n      }\n      return `npx -y grab@latest init`;\n    },\n    getChangedLines: () => [],\n  },\n  {\n    id: \"prompt\",\n    label: \"Prompt\",\n    description: \"Paste this full setup prompt in your coding agent's chat\",\n    variant: \"prompt\",\n    getCode: (hotkey) => createPromptInstallInstructions(hotkey),\n    getChangedLines: () => [],\n  },\n  {\n    id: \"next-app\",\n    label: \"Next.js\",\n    description: (\n      <>\n        Add this inside of your <InlineCode>app/layout.tsx</InlineCode>\n      </>\n    ),\n    variant: \"code\",\n    getCode: (hotkey) => {\n      const dataOptionsAttr = hotkey\n        ? `\\n            data-options='{\"activationKey\":\"${hotkeyToString(\n            hotkey,\n          )}\"}'`\n        : \"\";\n      return `import Script from \"next/script\";\n\nexport default function RootLayout({ children }) {\n  return (\n    <html>\n      <head>\n        {process.env.NODE_ENV === \"development\" && (\n          <Script\n            src=\"//unpkg.com/react-grab/dist/index.global.js\"\n            crossOrigin=\"anonymous\"\n            strategy=\"beforeInteractive\"${dataOptionsAttr}\n          />\n        )}\n      </head>\n      <body>{children}</body>\n    </html>\n  );\n}`;\n    },\n    getChangedLines: (hotkey) =>\n      hotkey ? [7, 8, 9, 10, 11, 12, 13, 14] : [7, 8, 9, 10, 11, 12, 13],\n  },\n  {\n    id: \"vite\",\n    label: \"Vite\",\n    description: (\n      <>\n        Example <InlineCode>index.html</InlineCode> with React Grab enabled in\n        development\n      </>\n    ),\n    variant: \"code\",\n    getCode: (hotkey) => {\n      if (hotkey) {\n        return `<!doctype html>\n<html lang=\"en\">\n  <head>\n    <script type=\"module\">\n      // first npm i react-grab\n      // then in head:\n      if (import.meta.env.DEV) {\n        const { init } = await import(\"react-grab/core\");\n        init(${formatInitOptions(hotkey)});\n      }\n    </script>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>`;\n      }\n      return `<!doctype html>\n<html lang=\"en\">\n  <head>\n    <script type=\"module\">\n      // first npm i react-grab\n      // then in head:\n      if (import.meta.env.DEV) {\n        import(\"react-grab\");\n      }\n    </script>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>`;\n    },\n    getChangedLines: (hotkey) =>\n      hotkey ? [4, 5, 6, 7, 8, 9, 10, 11] : [4, 5, 6, 7, 8, 9, 10],\n  },\n  {\n    id: \"tanstack\",\n    label: \"TanStack Start\",\n    description: (\n      <>\n        Add this inside your <InlineCode>src/routes/__root.tsx</InlineCode>\n      </>\n    ),\n    variant: \"code\",\n    getCode: (hotkey) => {\n      if (hotkey) {\n        return `import { useEffect } from \"react\";\nimport { Outlet, createRootRoute } from \"@tanstack/react-router\";\n\nexport const Route = createRootRoute({\n  component: RootComponent,\n});\n\nfunction RootComponent() {\n  useEffect(() => {\n    if (import.meta.env.DEV) {\n      import(\"react-grab/core\").then(({ init }) => {\n        init(${formatInitOptions(hotkey)});\n      });\n    }\n  }, []);\n\n  return <Outlet />;\n}`;\n      }\n      return `import { useEffect } from \"react\";\nimport { Outlet, createRootRoute } from \"@tanstack/react-router\";\n\nexport const Route = createRootRoute({\n  component: RootComponent,\n});\n\nfunction RootComponent() {\n  useEffect(() => {\n    if (import.meta.env.DEV) {\n      void import(\"react-grab\");\n    }\n  }, []);\n\n  return <Outlet />;\n}`;\n    },\n    getChangedLines: (hotkey) =>\n      hotkey ? [9, 10, 11, 12, 13, 14, 15] : [9, 10, 11, 12, 13],\n  },\n];\n\nconst HEADING_TEXT_BY_VARIANT: Record<InstallTab[\"variant\"], string> = {\n  prompt: \"Copy this to your agent:\",\n  command: \"Run this command to get started:\",\n  code: \"It takes 1 script tag to get started:\",\n};\n\ninterface InstallTabsProps {\n  showHeading?: boolean;\n  showAgentNote?: boolean;\n}\n\nexport const InstallTabs = ({\n  showHeading = false,\n  showAgentNote = false,\n}: InstallTabsProps): ReactElement | null => {\n  const { customHotkey } = useHotkey();\n  const [activeTabId, setActiveTabId] = useState<string>(\n    installTabsData[0]?.id,\n  );\n  const [didCopy, setDidCopy] = useState(false);\n  const [isPromptExpanded, setIsPromptExpanded] = useState(false);\n  const [promptExpandedMaxHeightPx, setPromptExpandedMaxHeightPx] = useState(\n    PROMPT_INSTALL_MAX_HEIGHT_PX,\n  );\n  const [highlightedCodes, setHighlightedCodes] = useState<\n    Record<string, string>\n  >({});\n  const [isMobile, setIsMobile] = useState(false);\n  const promptContentContainerRef = useRef<HTMLDivElement | null>(null);\n\n  const activeTab =\n    installTabsData.find((tab) => tab.id === activeTabId) ?? installTabsData[0];\n  const activeCode = activeTab.getCode(customHotkey ?? null);\n  const activeChangedLines = activeTab.getChangedLines(customHotkey ?? null);\n  const isPromptTab = activeTab.variant === \"prompt\";\n  const shouldShowPromptExpandButton =\n    isPromptTab &&\n    activeCode.split(\"\\n\").length > PROMPT_INSTALL_COLLAPSE_LINE_THRESHOLD;\n\n  useEffect(() => {\n    // eslint-disable-next-line react-hooks/set-state-in-effect\n    setIsMobile(detectMobile());\n  }, []);\n\n  const updateHighlightedCodes = useCallback(\n    async (hotkey: RecordedHotkey | null) => {\n      const results = await Promise.all(\n        installTabsData.map(async (tab) => ({\n          id: tab.id,\n          html:\n            tab.variant === \"prompt\"\n              ? \"\"\n              : await highlightCode({\n                  code: tab.getCode(hotkey),\n                  lang: tab.lang ?? \"tsx\",\n                  changedLines: tab.getChangedLines(hotkey),\n                }),\n        })),\n      );\n      const codes: Record<string, string> = {};\n      results.forEach((result) => {\n        codes[result.id] = result.html;\n      });\n      setHighlightedCodes(codes);\n    },\n    [],\n  );\n\n  useEffect(() => {\n    // eslint-disable-next-line react-hooks/set-state-in-effect\n    updateHighlightedCodes(customHotkey ?? null);\n  }, [customHotkey, updateHighlightedCodes]);\n\n  useEffect(() => {\n    setIsPromptExpanded(false);\n  }, [activeTab.id, activeCode]);\n\n  useEffect(() => {\n    if (!isPromptTab || !promptContentContainerRef.current) {\n      return;\n    }\n\n    const promptContentContainerElement = promptContentContainerRef.current;\n    const updatePromptExpandedMaxHeightPx = () => {\n      setPromptExpandedMaxHeightPx(promptContentContainerElement.scrollHeight);\n    };\n\n    updatePromptExpandedMaxHeightPx();\n\n    const resizeObserver = new ResizeObserver(updatePromptExpandedMaxHeightPx);\n    resizeObserver.observe(promptContentContainerElement);\n\n    return () => {\n      resizeObserver.disconnect();\n    };\n  }, [activeCode, isPromptTab]);\n\n  const handleCopyClick = () => {\n    if (typeof navigator === \"undefined\" || !navigator.clipboard) return;\n\n    const textToCopy =\n      activeChangedLines.length > 0\n        ? activeCode\n            .split(\"\\n\")\n            .filter((_, index) => activeChangedLines.includes(index + 1))\n            .join(\"\\n\")\n        : activeCode;\n\n    navigator.clipboard\n      .writeText(textToCopy)\n      .then(() => {\n        setDidCopy(true);\n        setTimeout(() => setDidCopy(false), COPY_FEEDBACK_DURATION_MS);\n      })\n      .catch(() => {});\n  };\n\n  const highlightedCode = highlightedCodes[activeTab.id];\n\n  if (isMobile) {\n    return null;\n  }\n\n  const headingText = HEADING_TEXT_BY_VARIANT[activeTab.variant];\n\n  return (\n    <div>\n      {showHeading && (\n        <span className=\"hidden sm:inline text-foreground\">\n          {headingText}\n          {activeTab.variant !== \"code\" && (\n            <button\n              type=\"button\"\n              onClick={() => setActiveTabId(\"next-app\")}\n              className=\"ml-3 text-xs italic text-muted-foreground hover:text-foreground/60 hover:underline transition-colors sm:text-sm\"\n            >\n              Prefer manual install?\n            </button>\n          )}\n        </span>\n      )}\n      <div className=\"mt-4 overflow-hidden rounded-lg border border-border bg-card text-foreground shadow-lg\">\n        <div className=\"flex items-center gap-4 overflow-x-auto border-b border-border px-4 pt-2\">\n          {installTabsData.map((tab) => {\n            const isActive = tab.id === activeTab.id;\n\n            return (\n              <button\n                key={tab.id}\n                type=\"button\"\n                className={cn(\n                  \"group/tab shrink-0 whitespace-nowrap border-b pb-2 font-sans text-sm transition-colors sm:text-base\",\n                  isActive\n                    ? \"border-foreground text-foreground\"\n                    : \"border-transparent text-muted-foreground hover:text-foreground\",\n                )}\n                onClick={() => setActiveTabId(tab.id)}\n              >\n                <span className=\"inline-flex items-center gap-1.5\">\n                  {tab.id === \"cli\" && <Terminal size={14} />}\n                  {tab.id === \"next-app\" && (\n                    <IconNextjs\n                      className={\n                        isActive\n                          ? \"grayscale-0 opacity-100\"\n                          : \"group-hover/tab:grayscale-0 group-hover/tab:opacity-100 transition-all\"\n                      }\n                    />\n                  )}\n                  {tab.id === \"vite\" && (\n                    <IconVite\n                      className={\n                        isActive\n                          ? \"grayscale-0 opacity-100\"\n                          : \"group-hover/tab:grayscale-0 group-hover/tab:opacity-100 transition-all\"\n                      }\n                    />\n                  )}\n                  {tab.id === \"tanstack\" && (\n                    <IconTanstack\n                      className={\n                        isActive\n                          ? \"grayscale-0 opacity-100\"\n                          : \"group-hover/tab:grayscale-0 group-hover/tab:opacity-100 transition-all\"\n                      }\n                    />\n                  )}\n                  {tab.label}\n                </span>\n              </button>\n            );\n          })}\n        </div>\n        <div className=\"bg-background/60 relative\">\n          <div className=\"relative\">\n            {activeTab.variant !== \"code\" ? (\n              activeTab.variant === \"prompt\" ? (\n                <div className=\"relative\">\n                  <button\n                    type=\"button\"\n                    onClick={handleCopyClick}\n                    className=\"touch-hitbox absolute! right-4 top-4 text-muted-foreground transition-colors hover:text-foreground z-10\"\n                  >\n                    {didCopy ? <Check size={16} /> : <Copy size={16} />}\n                  </button>\n                  <div\n                    ref={promptContentContainerRef}\n                    className=\"overflow-hidden px-4 py-6 transition-[max-height] duration-200 ease-out\"\n                    style={\n                      shouldShowPromptExpandButton\n                        ? {\n                            maxHeight: isPromptExpanded\n                              ? `${promptExpandedMaxHeightPx}px`\n                              : `${PROMPT_INSTALL_MAX_HEIGHT_PX}px`,\n                          }\n                        : undefined\n                    }\n                  >\n                    <p className=\"text-left text-base leading-relaxed text-foreground/80 whitespace-pre-wrap pr-10 pb-10\">\n                      {activeCode}\n                    </p>\n                  </div>\n                  {shouldShowPromptExpandButton && !isPromptExpanded && (\n                    <div className=\"pointer-events-none absolute bottom-0 left-0 right-0 h-24 bg-linear-to-t from-card via-card/95 to-transparent\" />\n                  )}\n                  {shouldShowPromptExpandButton && (\n                    <button\n                      type=\"button\"\n                      onClick={() =>\n                        setIsPromptExpanded((previous) => !previous)\n                      }\n                      className=\"absolute bottom-3 right-4 z-10 inline-flex items-center gap-1 rounded-md border border-border bg-card/90 px-2 py-1 text-xs text-muted-foreground transition-colors hover:text-foreground\"\n                    >\n                      <span>\n                        {isPromptExpanded ? \"Show less\" : \"Show full prompt\"}\n                      </span>\n                      <ChevronDown\n                        size={14}\n                        className={cn(\n                          \"transition-transform\",\n                          isPromptExpanded && \"rotate-180\",\n                        )}\n                      />\n                    </button>\n                  )}\n                </div>\n              ) : (\n                <button\n                  type=\"button\"\n                  onClick={handleCopyClick}\n                  className=\"group flex w-full items-center justify-between gap-4 px-4 py-6 transition-colors hover:bg-muted/50\"\n                >\n                  {highlightedCode ? (\n                    <div\n                      className=\"overflow-x-auto font-mono text-base leading-relaxed highlighted-code\"\n                      dangerouslySetInnerHTML={{ __html: highlightedCode }}\n                    />\n                  ) : (\n                    <pre className=\"overflow-x-auto font-mono text-base leading-relaxed text-foreground/80\">\n                      <code>{activeCode}</code>\n                    </pre>\n                  )}\n                  <span className=\"shrink-0 text-muted-foreground transition-colors group-hover:text-foreground\">\n                    {didCopy ? <Check size={16} /> : <Copy size={16} />}\n                  </span>\n                </button>\n              )\n            ) : (\n              <div className=\"group relative\">\n                <button\n                  type=\"button\"\n                  onClick={handleCopyClick}\n                  className=\"touch-hitbox absolute! right-4 top-4 text-muted-foreground opacity-0 transition-opacity hover:text-foreground group-hover:opacity-100 z-10\"\n                >\n                  {didCopy ? <Check size={16} /> : <Copy size={16} />}\n                </button>\n                {highlightedCode ? (\n                  <div\n                    className=\"overflow-x-auto p-4 font-mono text-[13px] leading-relaxed highlighted-code\"\n                    dangerouslySetInnerHTML={{ __html: highlightedCode }}\n                  />\n                ) : (\n                  <pre className=\"overflow-x-auto p-4 font-mono text-[13px] leading-relaxed text-foreground/80\">\n                    <code>{activeCode}</code>\n                  </pre>\n                )}\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n      {activeTab.variant === \"code\" && (\n        <span className=\"mt-4 block text-sm text-muted-foreground sm:text-base\">\n          {activeTab.description}\n        </span>\n      )}\n      {showAgentNote && activeTab.variant === \"code\" && (\n        <span className=\"mt-2 block text-sm text-muted-foreground sm:text-base\">\n          Want to connect directly to your coding agent?{\" \"}\n          <a href=\"/blog/agent\" className=\"underline hover:text-foreground/70\">\n            See our agent connection guide\n          </a>\n        </span>\n      )}\n    </div>\n  );\n};\n\nInstallTabs.displayName = \"InstallTabs\";\n"
  },
  {
    "path": "packages/website/components/mobile-demo-animation.tsx",
    "content": "\"use client\";\n\nimport {\n  useState,\n  useEffect,\n  useRef,\n  useCallback,\n  type ReactElement,\n} from \"react\";\nimport { useWebHaptics } from \"web-haptics/react\";\nimport { cn } from \"@/utils/cn\";\nimport {\n  VIBRATION_DURATION_MS,\n  TAP_FEEDBACK_DISPLAY_MS,\n  TAP_FEEDBACK_FADE_MS,\n  LABEL_OFFSET_BELOW_PX,\n  ANIMATION_RESTART_DELAY_MS,\n  SELECTION_PADDING_PX,\n  CURSOR_OFFSET_PX,\n  HINT_OVERLAY_DELAY_MS,\n  IDLE_RESTART_DELAY_MS,\n} from \"@/constants\";\n\nconst wait = (ms: number): Promise<void> =>\n  new Promise((resolve) => setTimeout(resolve, ms));\n\ninterface Position {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n}\n\ninterface BoxState extends Position {\n  visible: boolean;\n}\n\nconst HIDDEN_BOX: BoxState = {\n  visible: false,\n  x: 0,\n  y: 0,\n  width: 0,\n  height: 0,\n};\n\ninterface LabelState {\n  visible: boolean;\n  x: number;\n  y: number;\n  componentName: string;\n  tagName: string;\n}\n\nconst HIDDEN_LABEL: LabelState = {\n  visible: false,\n  x: 0,\n  y: 0,\n  componentName: \"\",\n  tagName: \"\",\n};\n\ntype LabelMode =\n  | \"idle\"\n  | \"selecting\"\n  | \"grabbing\"\n  | \"copied\"\n  | \"commenting\"\n  | \"submitted\"\n  | \"fading\";\ntype CursorType = \"default\" | \"crosshair\" | \"grabbing\";\n\ninterface HitElement {\n  position: Position;\n  name: string;\n  tag: string;\n}\n\nconst METRIC_CARD_NAMES = [\"RevenueCard\", \"UsersCard\", \"OrdersCard\"];\n\nconst ACTIVITY_DATA = [\n  { label: \"New signup\", time: \"2m ago\", component: \"SignupRow\" },\n  { label: \"Order placed\", time: \"5m ago\", component: \"OrderRow\" },\n  { label: \"Payment received\", time: \"12m ago\", component: \"PaymentRow\" },\n];\n\nconst createSelectionBox = (position: Position, padding: number): BoxState => ({\n  visible: true,\n  x: position.x - padding,\n  y: position.y - padding,\n  width: position.width + padding * 2,\n  height: position.height + padding * 2,\n});\n\nconst INITIAL_CURSOR_POSITION = { x: 150, y: 80 };\n\nconst getElementCenter = (position: Position): { x: number; y: number } => ({\n  x: position.x + position.width / 2,\n  y: position.y + position.height / 2,\n});\n\nconst CheckIcon = (): ReactElement => (\n  <svg\n    width=\"14\"\n    height=\"14\"\n    viewBox=\"0 0 21 21\"\n    fill=\"none\"\n    className=\"shrink-0 text-black/85\"\n  >\n    <path\n      d=\"M20.1767 10.0875C20.1767 15.6478 15.6576 20.175 10.0875 20.175C4.52715 20.175 0 15.6478 0 10.0875C0 4.51914 4.52715 0 10.0875 0C15.6576 0 20.1767 4.51914 20.1767 10.0875ZM13.0051 6.23867L8.96699 12.7041L7.08476 10.3143C6.83358 9.99199 6.59941 9.88828 6.28984 9.88828C5.79414 9.88828 5.39961 10.2918 5.39961 10.7893C5.39961 11.0367 5.48925 11.2621 5.66386 11.4855L8.05703 14.3967C8.33027 14.7508 8.63183 14.9103 8.99902 14.9103C9.36445 14.9103 9.68105 14.7312 9.90546 14.3896L14.4742 7.27206C14.6107 7.04765 14.7289 6.80898 14.7289 6.58359C14.7289 6.07187 14.281 5.72968 13.7934 5.72968C13.4937 5.72968 13.217 5.90527 13.0051 6.23867Z\"\n      fill=\"currentColor\"\n    />\n  </svg>\n);\n\nconst SubmitIcon = (): ReactElement => (\n  <svg\n    width=\"10\"\n    height=\"10\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    className=\"shrink-0 text-white\"\n  >\n    <path\n      d=\"M12 19V5M5 12l7-7 7 7\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2.5\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    />\n  </svg>\n);\n\nconst LoaderIcon = (): ReactElement => (\n  <svg\n    width=\"13\"\n    height=\"13\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"2\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n    className=\"shrink-0 text-[#71717a]\"\n  >\n    <path\n      className=\"animate-loader-bar\"\n      style={{ animationDelay: \"0ms\" }}\n      d=\"M12 2v4\"\n    />\n    <path\n      className=\"animate-loader-bar\"\n      style={{ animationDelay: \"-42ms\" }}\n      d=\"M15 6.8l2-3.5\"\n    />\n    <path\n      className=\"animate-loader-bar\"\n      style={{ animationDelay: \"-83ms\" }}\n      d=\"M17.2 9l3.5-2\"\n    />\n    <path\n      className=\"animate-loader-bar\"\n      style={{ animationDelay: \"-125ms\" }}\n      d=\"M18 12h4\"\n    />\n    <path\n      className=\"animate-loader-bar\"\n      style={{ animationDelay: \"-167ms\" }}\n      d=\"M17.2 15l3.5 2\"\n    />\n    <path\n      className=\"animate-loader-bar\"\n      style={{ animationDelay: \"-208ms\" }}\n      d=\"M15 17.2l2 3.5\"\n    />\n    <path\n      className=\"animate-loader-bar\"\n      style={{ animationDelay: \"-250ms\" }}\n      d=\"M12 18v4\"\n    />\n    <path\n      className=\"animate-loader-bar\"\n      style={{ animationDelay: \"-292ms\" }}\n      d=\"M9 17.2l-2 3.5\"\n    />\n    <path\n      className=\"animate-loader-bar\"\n      style={{ animationDelay: \"-333ms\" }}\n      d=\"M6.8 15l-3.5 2\"\n    />\n    <path\n      className=\"animate-loader-bar\"\n      style={{ animationDelay: \"-375ms\" }}\n      d=\"M2 12h4\"\n    />\n    <path\n      className=\"animate-loader-bar\"\n      style={{ animationDelay: \"-417ms\" }}\n      d=\"M6.8 9l-3.5-2\"\n    />\n    <path\n      className=\"animate-loader-bar\"\n      style={{ animationDelay: \"-458ms\" }}\n      d=\"M9 6.8l-2-3.5\"\n    />\n  </svg>\n);\n\nconst DefaultCursor = (): ReactElement => (\n  <svg\n    width=\"32\"\n    height=\"32\"\n    viewBox=\"0 0 32 32\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <g fill=\"none\" fillRule=\"evenodd\" transform=\"translate(10 7)\">\n      <path\n        d=\"m6.148 18.473 1.863-1.003 1.615-.839-2.568-4.816h4.332l-11.379-11.408v16.015l3.316-3.221z\"\n        fill=\"#fff\"\n      />\n      <path\n        d=\"m6.431 17 1.765-.941-2.775-5.202h3.604l-8.025-8.043v11.188l2.53-2.442z\"\n        fill=\"#000\"\n      />\n    </g>\n  </svg>\n);\n\nconst CrosshairCursor = (): ReactElement => (\n  <svg\n    width=\"32\"\n    height=\"32\"\n    viewBox=\"0 0 32 32\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <g fill=\"none\" transform=\"translate(9 9)\">\n      <path\n        d=\"m15 6h-6.01v-6h-2.98v6h-6.01v3h6.01v6h2.98v-6h6.01z\"\n        fill=\"#fff\"\n      />\n      <path\n        d=\"m13.99 7.01h-6v-6.01h-.98v6.01h-6v.98h6v6.01h.98v-6.01h6z\"\n        fill=\"#231f1f\"\n      />\n    </g>\n  </svg>\n);\n\nconst GrabbingCursor = (): ReactElement => (\n  <svg\n    width=\"32\"\n    height=\"32\"\n    viewBox=\"0 0 32 32\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    xmlnsXlink=\"http://www.w3.org/1999/xlink\"\n  >\n    <defs>\n      <linearGradient id=\"busya\" x1=\"50%\" x2=\"50%\" y1=\"0%\" y2=\"100%\">\n        <stop offset=\"0\" stopColor=\"#4ab4ef\" />\n        <stop offset=\"1\" stopColor=\"#3582e5\" />\n      </linearGradient>\n      <linearGradient id=\"busyb\" x1=\"50%\" x2=\"50%\" y1=\"0%\" y2=\"100%\">\n        <stop offset=\"0\" stopColor=\"#3481e4\" />\n        <stop offset=\"1\" stopColor=\"#2051db\" />\n      </linearGradient>\n      <linearGradient id=\"busyc\" x1=\"50%\" x2=\"50%\" y1=\"0%\" y2=\"100%\">\n        <stop offset=\"0\" stopColor=\"#6bdcfc\" />\n        <stop offset=\"1\" stopColor=\"#4dc6fa\" />\n      </linearGradient>\n      <linearGradient id=\"busyd\" x1=\"50%\" x2=\"50%\" y1=\"0%\" y2=\"100%\">\n        <stop offset=\"0\" stopColor=\"#4bc5f9\" />\n        <stop offset=\"1\" stopColor=\"#2fb0f8\" />\n      </linearGradient>\n      <mask id=\"busye\" fill=\"#fff\">\n        <path\n          d=\"m1 23c0 4.971 4.03 9 9 9 4.97 0 9-4.029 9-9 0-4.971-4.03-9-9-9-4.97 0-9 4.029-9 9z\"\n          fill=\"#fff\"\n          fillRule=\"evenodd\"\n        />\n      </mask>\n    </defs>\n    <g fill=\"none\" fillRule=\"evenodd\" transform=\"translate(7)\">\n      <g mask=\"url(#busye)\" className=\"origin-[10px_23px] animate-spin\">\n        <g transform=\"translate(1 14)\">\n          <path d=\"m0 0h9v9h-9z\" fill=\"url(#busya)\" />\n          <path d=\"m9 9h9v9h-9z\" fill=\"url(#busyb)\" />\n          <path d=\"m9 0h9v9h-9z\" fill=\"url(#busyc)\" />\n          <path d=\"m0 9h9v9h-9z\" fill=\"url(#busyd)\" />\n        </g>\n      </g>\n      <g fillRule=\"nonzero\">\n        <path\n          d=\"m0 16.422v-16.015l11.591 11.619h-7.041l-.151.124z\"\n          fill=\"#fff\"\n        />\n        <path d=\"m1 2.814v11.188l2.969-2.866.16-.139h5.036z\" fill=\"#000\" />\n      </g>\n    </g>\n  </svg>\n);\n\nconst CursorIcon = ({ type }: { type: CursorType }): ReactElement | null => {\n  if (type === \"default\") return <DefaultCursor />;\n  if (type === \"crosshair\") return <CrosshairCursor />;\n  if (type === \"grabbing\") return <GrabbingCursor />;\n  return null;\n};\n\nexport const MobileDemoAnimation = (): ReactElement => {\n  const { trigger: triggerHaptic } = useWebHaptics();\n  const [cursorPos, setCursorPos] = useState(INITIAL_CURSOR_POSITION);\n  const [isCursorVisible, setIsCursorVisible] = useState(false);\n  const [selectionBox, setSelectionBox] = useState<BoxState>(HIDDEN_BOX);\n  const [successFlash, setSuccessFlash] = useState<BoxState>(HIDDEN_BOX);\n  const [label, setLabel] = useState<LabelState>(HIDDEN_LABEL);\n  const [labelMode, setLabelMode] = useState<LabelMode>(\"idle\");\n  const [cursorType, setCursorType] = useState<CursorType>(\"default\");\n  const [commentText, setCommentText] = useState(\"\");\n  const [isHintVisible, setIsHintVisible] = useState(false);\n  const hintTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  const containerRef = useRef<HTMLDivElement>(null);\n  const metricCardRefs = useRef<(HTMLDivElement | null)[]>([]);\n  const metricValueRef = useRef<HTMLDivElement>(null);\n  const exportButtonRef = useRef<HTMLDivElement>(null);\n  const activityRowRefs = useRef<(HTMLDivElement | null)[]>([]);\n  const isCancelledRef = useRef(false);\n  const animationLoopRef = useRef<(() => void) | null>(null);\n  const tapTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const tapTimerInnerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const idleRestartTimerRef = useRef<ReturnType<typeof setTimeout> | null>(\n    null,\n  );\n\n  const metricCardPositions = useRef<(Position | null)[]>([]);\n  const metricValuePosition = useRef<Position>({\n    x: 0,\n    y: 0,\n    width: 0,\n    height: 0,\n  });\n  const exportButtonPosition = useRef<Position>({\n    x: 0,\n    y: 0,\n    width: 0,\n    height: 0,\n  });\n  const activityRowPositions = useRef<(Position | null)[]>([]);\n\n  const measureElementPositions = (): void => {\n    const container = containerRef.current;\n    if (!container) return;\n\n    const containerRect = container.getBoundingClientRect();\n\n    const measureRelativePosition = (\n      element: HTMLElement | null,\n      positionRef: React.MutableRefObject<Position>,\n    ): void => {\n      if (!element) return;\n      const rect = element.getBoundingClientRect();\n      positionRef.current = {\n        x: rect.left - containerRect.left,\n        y: rect.top - containerRect.top,\n        width: rect.width,\n        height: rect.height,\n      };\n    };\n\n    measureRelativePosition(metricValueRef.current, metricValuePosition);\n    measureRelativePosition(exportButtonRef.current, exportButtonPosition);\n\n    const measureRefArray = (\n      refs: (HTMLElement | null)[],\n    ): (Position | null)[] =>\n      refs.map((ref) => {\n        if (!ref) return null;\n        const rect = ref.getBoundingClientRect();\n        return {\n          x: rect.left - containerRect.left,\n          y: rect.top - containerRect.top,\n          width: rect.width,\n          height: rect.height,\n        };\n      });\n\n    metricCardPositions.current = measureRefArray(metricCardRefs.current);\n    activityRowPositions.current = measureRefArray(activityRowRefs.current);\n  };\n\n  useEffect(() => {\n    const visualViewport = window.visualViewport;\n    const measurementTimer = setTimeout(measureElementPositions, 100);\n    window.addEventListener(\"resize\", measureElementPositions);\n    visualViewport?.addEventListener(\"resize\", measureElementPositions);\n    visualViewport?.addEventListener(\"scroll\", measureElementPositions);\n    return () => {\n      clearTimeout(measurementTimer);\n      window.removeEventListener(\"resize\", measureElementPositions);\n      visualViewport?.removeEventListener(\"resize\", measureElementPositions);\n      visualViewport?.removeEventListener(\"scroll\", measureElementPositions);\n    };\n  }, []);\n\n  useEffect(() => {\n    hintTimerRef.current = setTimeout(() => {\n      setIsHintVisible(true);\n    }, HINT_OVERLAY_DELAY_MS);\n    return () => {\n      if (hintTimerRef.current) clearTimeout(hintTimerRef.current);\n    };\n  }, []);\n\n  const resetAnimationState = useCallback((): void => {\n    setCursorPos(INITIAL_CURSOR_POSITION);\n    setIsCursorVisible(false);\n    setCursorType(\"default\");\n    setSelectionBox(HIDDEN_BOX);\n    setSuccessFlash(HIDDEN_BOX);\n    setLabel(HIDDEN_LABEL);\n    setLabelMode(\"idle\");\n    setCommentText(\"\");\n  }, []);\n\n  useEffect(() => {\n    const displaySelectionLabel = (\n      x: number,\n      y: number,\n      componentName: string,\n      tagName: string,\n    ): void => {\n      setLabel({ visible: true, x, y, componentName, tagName });\n      setLabelMode(\"selecting\");\n    };\n\n    const fadeOutSelectionLabel = async (): Promise<void> => {\n      setLabelMode(\"fading\");\n      await wait(300);\n      setLabel(HIDDEN_LABEL);\n      setLabelMode(\"idle\");\n    };\n\n    const simulateClickAndCopy = async (position: Position): Promise<void> => {\n      setSelectionBox(HIDDEN_BOX);\n      setLabelMode(\"grabbing\");\n      setCursorType(\"grabbing\");\n      setSuccessFlash(createSelectionBox(position, SELECTION_PADDING_PX));\n      await wait(400);\n      if (isCancelledRef.current) return;\n\n      setLabelMode(\"copied\");\n      await wait(500);\n      if (isCancelledRef.current) return;\n\n      setSuccessFlash(HIDDEN_BOX);\n      await fadeOutSelectionLabel();\n      setCursorType(\"crosshair\");\n    };\n\n    const simulateComment = async (\n      position: Position,\n      comment: string,\n    ): Promise<void> => {\n      await wait(300);\n      if (isCancelledRef.current) return;\n\n      setLabelMode(\"commenting\");\n      setCommentText(\"\");\n      await wait(200);\n      if (isCancelledRef.current) return;\n\n      for (let charIndex = 0; charIndex <= comment.length; charIndex++) {\n        if (isCancelledRef.current) return;\n        setCommentText(comment.slice(0, charIndex));\n        await wait(50);\n      }\n      await wait(300);\n      if (isCancelledRef.current) return;\n\n      setLabelMode(\"submitted\");\n      setSuccessFlash(createSelectionBox(position, SELECTION_PADDING_PX));\n      await wait(500);\n      if (isCancelledRef.current) return;\n\n      setSuccessFlash(HIDDEN_BOX);\n      setSelectionBox(HIDDEN_BOX);\n      await fadeOutSelectionLabel();\n      setCommentText(\"\");\n    };\n\n    const executeAnimationSequence = async (): Promise<void> => {\n      resetAnimationState();\n      measureElementPositions();\n\n      if (isCancelledRef.current) return;\n      await wait(500);\n      if (isCancelledRef.current) return;\n\n      setIsCursorVisible(true);\n      setCursorType(\"crosshair\");\n      await wait(300);\n      if (isCancelledRef.current) return;\n\n      const buttonPos = exportButtonPosition.current;\n      const buttonCenter = getElementCenter(buttonPos);\n      setCursorPos(buttonCenter);\n      await wait(400);\n      if (isCancelledRef.current) return;\n\n      setSelectionBox(createSelectionBox(buttonPos, SELECTION_PADDING_PX));\n      displaySelectionLabel(\n        buttonCenter.x,\n        buttonPos.y + buttonPos.height + LABEL_OFFSET_BELOW_PX,\n        \"ExportBtn\",\n        \"button\",\n      );\n      await simulateClickAndCopy(buttonPos);\n      if (isCancelledRef.current) return;\n\n      const cardPos = metricCardPositions.current[0];\n      if (!cardPos) return;\n      const cardCenter = getElementCenter(cardPos);\n      setCursorPos(cardCenter);\n      await wait(400);\n      if (isCancelledRef.current) return;\n\n      setSelectionBox(createSelectionBox(cardPos, SELECTION_PADDING_PX));\n      displaySelectionLabel(\n        cardPos.x + cardPos.width / 2,\n        cardPos.y + cardPos.height + LABEL_OFFSET_BELOW_PX,\n        \"MetricCard\",\n        \"div\",\n      );\n      await simulateComment(cardPos, \"show graph\");\n      if (isCancelledRef.current) return;\n\n      const valuePos = metricValuePosition.current;\n      const valueCenter = getElementCenter(valuePos);\n      setCursorPos(valueCenter);\n      await wait(400);\n      if (isCancelledRef.current) return;\n\n      setSelectionBox(createSelectionBox(valuePos, SELECTION_PADDING_PX));\n      displaySelectionLabel(\n        valuePos.x + valuePos.width / 2,\n        valuePos.y + valuePos.height + LABEL_OFFSET_BELOW_PX,\n        \"StatValue\",\n        \"span\",\n      );\n      await simulateComment(valuePos, \"format as USD\");\n      if (isCancelledRef.current) return;\n\n      const signupRowPos = activityRowPositions.current[0];\n      if (signupRowPos) {\n        const signupCenter = getElementCenter(signupRowPos);\n        setCursorPos(signupCenter);\n        await wait(400);\n        if (isCancelledRef.current) return;\n\n        setSelectionBox(createSelectionBox(signupRowPos, SELECTION_PADDING_PX));\n        displaySelectionLabel(\n          signupCenter.x,\n          signupRowPos.y + signupRowPos.height + LABEL_OFFSET_BELOW_PX,\n          \"SignupRow\",\n          \"div\",\n        );\n        await simulateComment(signupRowPos, \"add avatar\");\n        if (isCancelledRef.current) return;\n      }\n\n      const orderRowPos = activityRowPositions.current[1];\n      if (orderRowPos) {\n        const orderCenter = getElementCenter(orderRowPos);\n        setCursorPos(orderCenter);\n        await wait(400);\n        if (isCancelledRef.current) return;\n\n        setSelectionBox(createSelectionBox(orderRowPos, SELECTION_PADDING_PX));\n        displaySelectionLabel(\n          orderCenter.x,\n          orderRowPos.y + orderRowPos.height + LABEL_OFFSET_BELOW_PX,\n          \"OrderRow\",\n          \"div\",\n        );\n        await wait(400);\n        if (isCancelledRef.current) return;\n\n        await simulateClickAndCopy(orderRowPos);\n        if (isCancelledRef.current) return;\n      }\n\n      setIsCursorVisible(false);\n      setCursorType(\"default\");\n      await wait(ANIMATION_RESTART_DELAY_MS);\n    };\n\n    const runAnimationLoop = async (): Promise<void> => {\n      while (!isCancelledRef.current) {\n        await executeAnimationSequence();\n      }\n    };\n\n    animationLoopRef.current = () => {\n      isCancelledRef.current = false;\n      runAnimationLoop();\n    };\n\n    const handleVisibilityChange = (): void => {\n      if (document.visibilityState === \"visible\") {\n        isCancelledRef.current = true;\n        resetAnimationState();\n        isCancelledRef.current = false;\n        runAnimationLoop();\n      }\n    };\n\n    document.addEventListener(\"visibilitychange\", handleVisibilityChange);\n    isCancelledRef.current = false;\n    runAnimationLoop();\n\n    return () => {\n      isCancelledRef.current = true;\n      if (tapTimerRef.current) clearTimeout(tapTimerRef.current);\n      if (tapTimerInnerRef.current) clearTimeout(tapTimerInnerRef.current);\n      if (idleRestartTimerRef.current)\n        clearTimeout(idleRestartTimerRef.current);\n      document.removeEventListener(\"visibilitychange\", handleVisibilityChange);\n    };\n  }, [resetAnimationState]);\n\n  const handleTap = useCallback(\n    (event: React.MouseEvent<HTMLDivElement>) => {\n      const container = containerRef.current;\n      if (!container) return;\n\n      if (tapTimerRef.current) clearTimeout(tapTimerRef.current);\n      if (tapTimerInnerRef.current) clearTimeout(tapTimerInnerRef.current);\n      if (idleRestartTimerRef.current)\n        clearTimeout(idleRestartTimerRef.current);\n      isCancelledRef.current = true;\n\n      triggerHaptic(VIBRATION_DURATION_MS);\n\n      setIsCursorVisible(false);\n      setIsHintVisible(false);\n      if (hintTimerRef.current) {\n        clearTimeout(hintTimerRef.current);\n        hintTimerRef.current = null;\n      }\n      setCursorType(\"default\");\n      setCommentText(\"\");\n\n      const containerRect = container.getBoundingClientRect();\n      const tapX = event.clientX - containerRect.left;\n      const tapY = event.clientY - containerRect.top;\n\n      const distanceToPosition = (position: Position): number => {\n        const clampedX = Math.max(\n          position.x,\n          Math.min(tapX, position.x + position.width),\n        );\n        const clampedY = Math.max(\n          position.y,\n          Math.min(tapY, position.y + position.height),\n        );\n        return Math.hypot(tapX - clampedX, tapY - clampedY);\n      };\n\n      const allElements: HitElement[] = [\n        {\n          position: exportButtonPosition.current,\n          name: \"ExportBtn\",\n          tag: \"button\",\n        },\n        {\n          position: metricValuePosition.current,\n          name: \"StatValue\",\n          tag: \"span\",\n        },\n        ...metricCardPositions.current\n          .map((cardPosition, index) =>\n            cardPosition\n              ? {\n                  position: cardPosition,\n                  name: METRIC_CARD_NAMES[index],\n                  tag: \"div\",\n                }\n              : null,\n          )\n          .filter((element): element is HitElement => element !== null),\n        ...activityRowPositions.current\n          .map((rowPosition, index) =>\n            rowPosition\n              ? {\n                  position: rowPosition,\n                  name: ACTIVITY_DATA[index].component,\n                  tag: \"div\",\n                }\n              : null,\n          )\n          .filter((element): element is HitElement => element !== null),\n      ];\n\n      let closestElement = allElements[0];\n      let closestDistance = distanceToPosition(closestElement.position);\n\n      const areaOf = (position: Position): number =>\n        position.width * position.height;\n\n      for (let index = 1; index < allElements.length; index++) {\n        const distance = distanceToPosition(allElements[index].position);\n        const isSameDistance = distance === closestDistance;\n        const isSmallerElement =\n          isSameDistance &&\n          areaOf(allElements[index].position) < areaOf(closestElement.position);\n        if (distance < closestDistance || isSmallerElement) {\n          closestDistance = distance;\n          closestElement = allElements[index];\n        }\n      }\n\n      const targetPosition = closestElement.position;\n      const labelX = getElementCenter(targetPosition).x;\n      const labelY =\n        targetPosition.y + targetPosition.height + LABEL_OFFSET_BELOW_PX;\n      const selectionBounds = createSelectionBox(\n        targetPosition,\n        SELECTION_PADDING_PX,\n      );\n\n      setSelectionBox(selectionBounds);\n      setSuccessFlash(selectionBounds);\n      setLabel({\n        visible: true,\n        x: labelX,\n        y: labelY,\n        componentName: closestElement.name,\n        tagName: closestElement.tag,\n      });\n      setLabelMode(\"copied\");\n\n      tapTimerRef.current = setTimeout(() => {\n        if (isCancelledRef.current) {\n          setLabelMode(\"fading\");\n          setSuccessFlash(HIDDEN_BOX);\n\n          tapTimerInnerRef.current = setTimeout(() => {\n            setSelectionBox(HIDDEN_BOX);\n            setLabel(HIDDEN_LABEL);\n            setLabelMode(\"idle\");\n\n            idleRestartTimerRef.current = setTimeout(() => {\n              if (animationLoopRef.current) {\n                animationLoopRef.current();\n              }\n            }, IDLE_RESTART_DELAY_MS);\n          }, TAP_FEEDBACK_FADE_MS);\n        }\n      }, TAP_FEEDBACK_DISPLAY_MS);\n    },\n    [triggerHaptic],\n  );\n\n  const isLabelVisible = label.visible && labelMode !== \"fading\";\n\n  return (\n    <div className=\"mt-3\">\n      <style>{`\n        @keyframes loader-bar {\n          0% { opacity: 1; }\n          50% { opacity: 0.5; }\n          100% { opacity: 0.2; }\n        }\n        .animate-loader-bar {\n          animation: loader-bar 0.5s linear infinite;\n        }\n        @keyframes shimmer {\n          0% { background-position: 200% 0; }\n          100% { background-position: -200% 0; }\n        }\n        .shimmer-text {\n          background: linear-gradient(90deg, #818181 0%, #818181 35%, #ffffff 50%, #818181 65%, #818181 100%);\n          background-size: 150% 100%;\n          -webkit-background-clip: text;\n          background-clip: text;\n          color: transparent;\n          animation: shimmer 1s ease-in-out infinite;\n        }\n      `}</style>\n\n      <div className=\"overflow-hidden rounded-xl border border-border bg-card shadow-lg shadow-black/20\">\n        <div\n          ref={containerRef}\n          onClick={handleTap}\n          className=\"relative p-4 pb-14\"\n        >\n          <div className=\"mb-4 flex items-center justify-between\">\n            <div>\n              <div className=\"text-[13px] font-semibold text-foreground\">\n                Overview\n              </div>\n              <div className=\"text-[11px] text-muted-foreground\">\n                Last 30 days\n              </div>\n            </div>\n            <div\n              ref={exportButtonRef}\n              className=\"rounded-md bg-muted px-3 py-1.5 text-[11px] font-medium text-muted-foreground\"\n            >\n              Export\n            </div>\n          </div>\n\n          <div className=\"mb-4 grid grid-cols-3 gap-2.5\">\n            <div\n              ref={(el) => {\n                metricCardRefs.current[0] = el;\n              }}\n              className=\"rounded-lg border border-border bg-muted/50 p-2.5\"\n            >\n              <div className=\"mb-1 text-[10px] font-medium text-muted-foreground\">\n                Revenue\n              </div>\n              <div\n                ref={metricValueRef}\n                className=\"text-[18px] font-semibold tabular-nums text-foreground\"\n              >\n                $12.4k\n              </div>\n              <div className=\"mt-1 text-[10px] text-muted-foreground\">\n                +12.5%\n              </div>\n            </div>\n\n            <div\n              ref={(el) => {\n                metricCardRefs.current[1] = el;\n              }}\n              className=\"rounded-lg border border-border bg-muted/50 p-2.5\"\n            >\n              <div className=\"mb-1 text-[10px] font-medium text-muted-foreground\">\n                Users\n              </div>\n              <div className=\"text-[18px] font-semibold tabular-nums text-foreground\">\n                2,847\n              </div>\n              <div className=\"mt-1 text-[10px] text-muted-foreground\">\n                +8.2%\n              </div>\n            </div>\n\n            <div\n              ref={(el) => {\n                metricCardRefs.current[2] = el;\n              }}\n              className=\"rounded-lg border border-border bg-muted/50 p-2.5\"\n            >\n              <div className=\"mb-1 text-[10px] font-medium text-muted-foreground\">\n                Orders\n              </div>\n              <div className=\"text-[18px] font-semibold tabular-nums text-foreground\">\n                384\n              </div>\n              <div className=\"mt-1 text-[10px] text-muted-foreground\">\n                -2.1%\n              </div>\n            </div>\n          </div>\n\n          <div className=\"rounded-lg border border-border\">\n            <div className=\"border-b border-border px-3 py-2\">\n              <div className=\"text-[11px] font-medium text-muted-foreground\">\n                Recent Activity\n              </div>\n            </div>\n            <div className=\"divide-y divide-border\">\n              {ACTIVITY_DATA.map((activity, activityIndex) => (\n                <div\n                  key={activity.label}\n                  ref={(el) => {\n                    activityRowRefs.current[activityIndex] = el;\n                  }}\n                  className=\"flex items-center justify-between px-3 py-2\"\n                >\n                  <div className=\"flex items-center gap-2\">\n                    <div className=\"h-5 w-5 rounded-full bg-muted/50\" />\n                    <span className=\"text-[11px] text-muted-foreground\">\n                      {activity.label}\n                    </span>\n                  </div>\n                  <span className=\"text-[10px] text-muted-foreground\">\n                    {activity.time}\n                  </span>\n                </div>\n              ))}\n            </div>\n          </div>\n\n          <div\n            className={cn(\n              \"absolute inset-x-0 bottom-0 z-[70] flex items-center justify-center bg-gradient-to-t from-card via-card/90 to-card/0 pb-3 pt-8 transition-opacity duration-700\",\n              isHintVisible ? \"opacity-100\" : \"opacity-0 pointer-events-none\",\n            )}\n          >\n            <span className=\"rounded-full bg-foreground/10 px-3 py-1.5 text-[12px] font-medium text-muted-foreground\">\n              👆 Tap any element to copy\n            </span>\n          </div>\n\n          <div\n            className={cn(\n              \"pointer-events-none absolute inset-0 z-50 transition-opacity duration-200\",\n              cursorType === \"crosshair\" ? \"opacity-100\" : \"opacity-0\",\n            )}\n          >\n            <div\n              className=\"absolute left-0 right-0 h-px bg-[#d239c0]\"\n              style={{ top: cursorPos.y }}\n            />\n            <div\n              className=\"absolute bottom-0 top-0 w-px bg-[#d239c0]\"\n              style={{ left: cursorPos.x }}\n            />\n          </div>\n\n          <div\n            className={cn(\n              \"pointer-events-none absolute z-60 transition-opacity duration-200\",\n              isCursorVisible ? \"opacity-100\" : \"opacity-0\",\n            )}\n            style={{\n              left: cursorPos.x - CURSOR_OFFSET_PX,\n              top: cursorPos.y - CURSOR_OFFSET_PX,\n            }}\n          >\n            <CursorIcon type={cursorType} />\n          </div>\n\n          <div\n            className={cn(\n              \"pointer-events-none absolute z-40 rounded-lg border-2 border-[#d239c0]/50 bg-[#d239c0]/8 transition-[opacity,transform] duration-150\",\n              selectionBox.visible\n                ? \"scale-100 opacity-100\"\n                : \"scale-[0.98] opacity-0\",\n            )}\n            style={{\n              left: selectionBox.x,\n              top: selectionBox.y,\n              width: selectionBox.width,\n              height: selectionBox.height,\n            }}\n          />\n\n          <div\n            className={cn(\n              \"pointer-events-none absolute z-42 rounded-lg border-2 border-[#d239c0] bg-[#d239c0]/15 transition-[opacity,transform] duration-200\",\n              successFlash.visible\n                ? \"scale-100 opacity-100\"\n                : \"scale-[1.02] opacity-0\",\n            )}\n            style={{\n              left: successFlash.x,\n              top: successFlash.y,\n              width: successFlash.width,\n              height: successFlash.height,\n            }}\n          />\n\n          <div\n            className={cn(\n              \"pointer-events-none absolute z-55 rounded-[10px] bg-white shadow-[0_1px_2px_#51515140] transition-[opacity,transform] duration-300 ease-out\",\n              isLabelVisible ? \"scale-100 opacity-100\" : \"scale-95 opacity-0\",\n            )}\n            style={{\n              left: label.x,\n              top: label.y,\n              transform: \"translateX(-50%)\",\n            }}\n          >\n            <div\n              className=\"absolute left-1/2 h-0 w-0 -translate-x-1/2\"\n              style={{\n                top: -5,\n                borderLeft: \"5px solid transparent\",\n                borderRight: \"5px solid transparent\",\n                borderBottom: \"5px solid white\",\n              }}\n            />\n            {labelMode === \"selecting\" && (\n              <div className=\"flex items-center py-1.5 px-2\">\n                <span className=\"text-[13px] leading-4 font-medium text-black\">\n                  {label.componentName}\n                </span>\n                <span className=\"text-[13px] leading-4 font-medium text-black/50\">\n                  .{label.tagName}\n                </span>\n              </div>\n            )}\n            {labelMode === \"grabbing\" && (\n              <div className=\"flex items-center gap-[5px] py-1.5 px-2\">\n                <LoaderIcon />\n                <span className=\"shimmer-text text-[13px] leading-4 font-medium\">\n                  Grabbing…\n                </span>\n              </div>\n            )}\n            {(labelMode === \"copied\" ||\n              labelMode === \"submitted\" ||\n              labelMode === \"fading\") && (\n              <div className=\"flex items-center gap-[5px] py-1.5 px-2\">\n                <CheckIcon />\n                <span className=\"text-[13px] leading-4 font-medium text-black\">\n                  Copied\n                </span>\n              </div>\n            )}\n            {labelMode === \"commenting\" && (\n              <div className=\"flex flex-col min-w-[140px]\">\n                <div className=\"flex items-center pt-1.5 pb-1 px-2\">\n                  <span className=\"text-[13px] leading-4 font-medium text-black\">\n                    {label.componentName}\n                  </span>\n                  <span className=\"text-[13px] leading-4 font-medium text-black/50\">\n                    .{label.tagName}\n                  </span>\n                </div>\n                <div className=\"flex items-end justify-between gap-2 px-2 pb-1.5 border-t border-black/5 pt-1\">\n                  <span\n                    className={cn(\n                      \"text-[13px] leading-4 font-medium\",\n                      commentText ? \"text-black\" : \"text-black/40\",\n                    )}\n                  >\n                    {commentText || \"Add context\"}\n                  </span>\n                  <div className=\"shrink-0 flex items-center justify-center size-4 rounded-full bg-black\">\n                    <SubmitIcon />\n                  </div>\n                </div>\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n\n      <p className=\"mt-1.5 text-sm text-muted-foreground/60\">\n        This website is best viewed on desktop\n      </p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/website/components/react-grab-logo.tsx",
    "content": "\"use client\";\n\nimport { useState, type ReactElement } from \"react\";\n\nconst BASE_ANIMATION_DURATION_MS = 400;\nconst SPEED_INCREMENT = 0.5;\n\ninterface ReactGrabLogoProps {\n  width?: number;\n  height?: number;\n  className?: string;\n}\n\nexport const ReactGrabLogo = ({\n  width = 44,\n  height = 44,\n  className = \"\",\n}: ReactGrabLogoProps): ReactElement => {\n  const [clickCount, setClickCount] = useState(0);\n\n  const speedMultiplier = 1 + clickCount * SPEED_INCREMENT;\n  const animationDuration = Math.round(\n    BASE_ANIMATION_DURATION_MS / speedMultiplier,\n  );\n\n  const handleClick = () => {\n    setClickCount((previousCount) => previousCount + 1);\n  };\n\n  const handleMouseLeave = () => {\n    setClickCount(0);\n  };\n\n  return (\n    <svg\n      width={width}\n      height={height}\n      viewBox=\"0 0 330 330\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      className={`react-grab-logo ${className}`}\n      onClick={handleClick}\n      onMouseLeave={handleMouseLeave}\n      style={{\n        display: \"inline-block\",\n        verticalAlign: \"middle\",\n        marginRight: 8,\n        cursor: \"pointer\",\n        // @ts-expect-error CSS custom property\n        \"--arrow-animation-duration\": `${animationDuration}ms`,\n      }}\n    >\n      <g clipPath=\"url(#clip0_0_3)\">\n        <mask\n          id=\"mask0_0_3\"\n          style={{ maskType: \"luminance\" }}\n          maskUnits=\"userSpaceOnUse\"\n          x=\"0\"\n          y=\"0\"\n          width=\"294\"\n          height=\"294\"\n        >\n          <path d=\"M294 0H0V294H294V0Z\" fill=\"white\" />\n        </mask>\n        <g mask=\"url(#mask0_0_3)\">\n          <path\n            d=\"M144.599 47.4924C169.712 27.3959 194.548 20.0265 212.132 30.1797C227.847 39.2555 234.881 60.3243 231.926 89.516C231.677 92.0069 231.328 94.5423 230.94 97.1058L228.526 110.14C228.517 110.136 228.505 110.132 228.495 110.127C228.486 110.165 228.479 110.203 228.468 110.24L216.255 105.741C216.256 105.736 216.248 105.728 216.248 105.723C207.915 103.125 199.421 101.075 190.82 99.5888L190.696 99.5588L173.526 97.2648L173.511 97.2631C173.492 97.236 173.467 97.2176 173.447 97.1905C163.862 96.2064 154.233 95.7166 144.599 95.7223C134.943 95.7162 125.295 96.219 115.693 97.2286C110.075 105.033 104.859 113.118 100.063 121.453C95.2426 129.798 90.8624 138.391 86.939 147.193C90.8624 155.996 95.2426 164.588 100.063 172.933C104.866 181.302 110.099 189.417 115.741 197.245C115.749 197.245 115.758 197.246 115.766 197.247L115.752 197.27L115.745 197.283L115.754 197.296L126.501 211.013L126.574 211.089C132.136 217.767 138.126 224.075 144.507 229.974L144.609 230.082L154.572 238.287C154.539 238.319 154.506 238.35 154.472 238.38C154.485 238.392 154.499 238.402 154.513 238.412L143.846 247.482L143.827 247.497C126.56 261.128 109.472 268.745 94.8019 268.745C88.5916 268.837 82.4687 267.272 77.0657 264.208C61.3496 255.132 54.3164 234.062 57.2707 204.871C57.528 202.307 57.8806 199.694 58.2904 197.054C28.3363 185.327 9.52301 167.51 9.52301 147.193C9.52301 129.042 24.2476 112.396 50.9901 100.375C53.3443 99.3163 55.7938 98.3058 58.2904 97.3526C57.8806 94.7023 57.528 92.0803 57.2707 89.516C54.3164 60.3243 61.3496 39.2555 77.0657 30.1797C94.6494 20.0265 119.486 27.3959 144.599 47.4924ZM70.6423 201.315C70.423 202.955 70.2229 204.566 70.0704 206.168C67.6686 229.567 72.5478 246.628 83.3615 252.988L83.5176 253.062C95.0399 259.717 114.015 254.426 134.782 238.38C125.298 229.45 116.594 219.725 108.764 209.314C95.8516 207.742 83.0977 205.066 70.6423 201.315ZM80.3534 163.438C77.34 171.677 74.8666 180.104 72.9484 188.664C81.1787 191.224 89.5657 193.247 98.0572 194.724L98.4618 194.813C95.2115 189.865 92.0191 184.66 88.9311 179.378C85.8433 174.097 83.003 168.768 80.3534 163.438ZM60.759 110.203C59.234 110.839 57.7378 111.475 56.27 112.11C34.7788 121.806 22.3891 134.591 22.3891 147.193C22.3891 160.493 36.4657 174.297 60.7494 184.26C63.7439 171.581 67.8124 159.182 72.9104 147.193C67.822 135.23 63.7566 122.855 60.759 110.203ZM98.4137 99.6404C89.8078 101.145 81.3075 103.206 72.9676 105.809C74.854 114.203 77.2741 122.468 80.2132 130.554L80.3059 130.939C82.9938 125.6 85.8049 120.338 88.8834 115.008C91.9618 109.679 95.1544 104.569 98.4137 99.6404ZM94.9258 38.5215C90.9331 38.4284 86.9866 39.3955 83.4891 41.3243C72.6291 47.6015 67.6975 64.5954 70.0424 87.9446L70.0416 88.2194C70.194 89.8208 70.3941 91.4325 70.6134 93.0624C83.0737 89.3364 95.8263 86.6703 108.736 85.0924C116.57 74.6779 125.28 64.9532 134.773 56.0249C119.877 44.5087 105.895 38.5215 94.9258 38.5215ZM205.737 41.3148C202.268 39.398 198.355 38.4308 194.394 38.5099L194.29 38.512C183.321 38.512 169.34 44.4991 154.444 56.0153C163.93 64.9374 172.634 74.6557 180.462 85.064C193.375 86.6345 206.128 89.3102 218.584 93.0624C218.812 91.4325 219.003 89.8118 219.165 88.2098C221.548 64.7099 216.65 47.6164 205.737 41.3148ZM144.552 64.3097C138.104 70.2614 132.054 76.6306 126.443 83.3765C132.39 82.995 138.426 82.8046 144.552 82.8046C150.727 82.8046 156.778 83.0143 162.707 83.3765C157.08 76.6293 151.015 70.2596 144.552 64.3097Z\"\n            fill=\"white\"\n          />\n          <path\n            d=\"M144.598 47.4924C169.712 27.3959 194.547 20.0265 212.131 30.1797C227.847 39.2555 234.88 60.3243 231.926 89.516C231.677 92.0069 231.327 94.5423 230.941 97.1058L228.526 110.14L228.496 110.127C228.487 110.165 228.478 110.203 228.469 110.24L216.255 105.741L216.249 105.723C207.916 103.125 199.42 101.075 190.82 99.5888L190.696 99.5588L173.525 97.2648L173.511 97.263C173.492 97.236 173.468 97.2176 173.447 97.1905C163.863 96.2064 154.234 95.7166 144.598 95.7223C134.943 95.7162 125.295 96.219 115.693 97.2286C110.075 105.033 104.859 113.118 100.063 121.453C95.2426 129.798 90.8622 138.391 86.939 147.193C90.8622 155.996 95.2426 164.588 100.063 172.933C104.866 181.302 110.099 189.417 115.741 197.245L115.766 197.247L115.752 197.27L115.745 197.283L115.754 197.296L126.501 211.013L126.574 211.089C132.136 217.767 138.126 224.075 144.506 229.974L144.61 230.082L154.572 238.287C154.539 238.319 154.506 238.35 154.473 238.38L154.512 238.412L143.847 247.482L143.827 247.497C126.56 261.13 109.472 268.745 94.8018 268.745C88.5915 268.837 82.4687 267.272 77.0657 264.208C61.3496 255.132 54.3162 234.062 57.2707 204.871C57.528 202.307 57.8806 199.694 58.2904 197.054C28.3362 185.327 9.52298 167.51 9.52298 147.193C9.52298 129.042 24.2476 112.396 50.9901 100.375C53.3443 99.3163 55.7938 98.3058 58.2904 97.3526C57.8806 94.7023 57.528 92.0803 57.2707 89.516C54.3162 60.3243 61.3496 39.2555 77.0657 30.1797C94.6493 20.0265 119.486 27.3959 144.598 47.4924ZM70.6422 201.315C70.423 202.955 70.2229 204.566 70.0704 206.168C67.6686 229.567 72.5478 246.628 83.3615 252.988L83.5175 253.062C95.0399 259.717 114.015 254.426 134.782 238.38C125.298 229.45 116.594 219.725 108.764 209.314C95.8515 207.742 83.0977 205.066 70.6422 201.315ZM80.3534 163.438C77.34 171.677 74.8666 180.104 72.9484 188.664C81.1786 191.224 89.5657 193.247 98.0572 194.724L98.4618 194.813C95.2115 189.865 92.0191 184.66 88.931 179.378C85.8433 174.097 83.003 168.768 80.3534 163.438ZM60.7589 110.203C59.234 110.839 57.7378 111.475 56.2699 112.11C34.7788 121.806 22.3891 134.591 22.3891 147.193C22.3891 160.493 36.4657 174.297 60.7494 184.26C63.7439 171.581 67.8124 159.182 72.9103 147.193C67.822 135.23 63.7566 122.855 60.7589 110.203ZM98.4137 99.6404C89.8078 101.145 81.3075 103.206 72.9676 105.809C74.8539 114.203 77.2741 122.468 80.2132 130.554L80.3059 130.939C82.9938 125.6 85.8049 120.338 88.8834 115.008C91.9618 109.679 95.1544 104.569 98.4137 99.6404ZM94.9258 38.5215C90.9331 38.4284 86.9866 39.3955 83.4891 41.3243C72.629 47.6015 67.6975 64.5954 70.0424 87.9446L70.0415 88.2194C70.194 89.8208 70.3941 91.4325 70.6134 93.0624C83.0737 89.3364 95.8262 86.6703 108.736 85.0924C116.57 74.6779 125.28 64.9532 134.772 56.0249C119.877 44.5087 105.895 38.5215 94.9258 38.5215ZM205.737 41.3148C202.268 39.398 198.355 38.4308 194.394 38.5099L194.291 38.512C183.321 38.512 169.34 44.4991 154.443 56.0153C163.929 64.9374 172.634 74.6557 180.462 85.064C193.374 86.6345 206.129 89.3102 218.584 93.0624C218.813 91.4325 219.003 89.8118 219.166 88.2098C221.548 64.7099 216.65 47.6164 205.737 41.3148ZM144.551 64.3097C138.103 70.2614 132.055 76.6306 126.443 83.3765C132.389 82.995 138.427 82.8046 144.551 82.8046C150.727 82.8046 156.779 83.0143 162.707 83.3765C157.079 76.6293 151.015 70.2596 144.551 64.3097Z\"\n            fill=\"#fc4efd\"\n          />\n        </g>\n        <mask\n          id=\"mask1_0_3\"\n          style={{ maskType: \"luminance\" }}\n          maskUnits=\"userSpaceOnUse\"\n          x=\"102\"\n          y=\"84\"\n          width=\"161\"\n          height=\"162\"\n        >\n          <path\n            d=\"M235.282 84.827L102.261 112.259L129.693 245.28L262.714 217.848L235.282 84.827Z\"\n            fill=\"white\"\n          />\n        </mask>\n        <g mask=\"url(#mask1_0_3)\">\n          <path\n            className=\"react-grab-arrow\"\n            d=\"M136.863 129.916L213.258 141.224C220.669 142.322 222.495 152.179 215.967 155.856L187.592 171.843L184.135 204.227C183.339 211.678 173.564 213.901 169.624 207.526L129.021 141.831C125.503 136.14 130.245 128.936 136.863 129.916Z\"\n            fill=\"#fc4efd\"\n            stroke=\"#fc4efd\"\n            strokeWidth=\"0.817337\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          />\n        </g>\n      </g>\n      <defs>\n        <clipPath id=\"clip0_0_3\">\n          <rect width=\"294\" height=\"294\" fill=\"white\" />\n        </clipPath>\n      </defs>\n    </svg>\n  );\n};\n\nReactGrabLogo.displayName = \"ReactGrabLogo\";\n"
  },
  {
    "path": "packages/website/components/table-of-contents.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\ninterface TocHeading {\n  id: string;\n  text: string;\n  level: number;\n}\n\ninterface TableOfContentsProps {\n  headings: TocHeading[];\n}\n\nexport const TableOfContents = ({ headings }: TableOfContentsProps) => {\n  const [activeId, setActiveId] = useState<string>(\"\");\n\n  useEffect(() => {\n    const observer = new IntersectionObserver(\n      (entries) => {\n        for (const entry of entries) {\n          if (entry.isIntersecting) {\n            setActiveId(entry.target.id);\n          }\n        }\n      },\n      {\n        rootMargin: \"-96px 0px -80% 0px\",\n        threshold: 0,\n      },\n    );\n\n    const headingElements = headings\n      .map((heading) => document.getElementById(heading.id))\n      .filter(Boolean) as HTMLElement[];\n\n    for (const element of headingElements) {\n      observer.observe(element);\n    }\n\n    return () => {\n      for (const element of headingElements) {\n        observer.unobserve(element);\n      }\n    };\n  }, [headings]);\n\n  const handleClick = (\n    event: React.MouseEvent<HTMLAnchorElement>,\n    id: string,\n  ) => {\n    event.preventDefault();\n    const element = document.getElementById(id);\n    if (element) {\n      element.scrollIntoView({ behavior: \"smooth\" });\n      setActiveId(id);\n    }\n  };\n\n  if (headings.length === 0) {\n    return null;\n  }\n\n  return (\n    <nav className=\"hidden lg:block w-48 shrink-0\">\n      <div className=\"sticky top-24 bg-background py-4 -my-4 px-2 -mx-2 rounded-lg opacity-50 hover:opacity-100 transition-opacity\">\n        <div className=\"text-sm font-medium text-muted-foreground mb-4\">\n          On this page\n        </div>\n        <ul className=\"flex flex-col gap-2\">\n          {headings.map((heading) => {\n            const isActive = activeId === heading.id;\n            const indentClass = heading.level === 4 ? \"pl-3\" : \"\";\n\n            return (\n              <li key={heading.id}>\n                <a\n                  href={`#${heading.id}`}\n                  onClick={(event) => handleClick(event, heading.id)}\n                  className={`block text-sm border-l-2 pl-3 -ml-0.5 ${indentClass} ${\n                    isActive\n                      ? \"text-neutral-200 border-[#ff4fff]\"\n                      : \"text-neutral-500 border-transparent hover:text-muted-foreground hover:border-neutral-700\"\n                  }`}\n                >\n                  {heading.text}\n                </a>\n              </li>\n            );\n          })}\n        </ul>\n      </div>\n    </nav>\n  );\n};\n\nTableOfContents.displayName = \"TableOfContents\";\n"
  },
  {
    "path": "packages/website/components/ui/button.tsx",
    "content": "\"use client\";\n\nimport { Button as ButtonPrimitive } from \"@base-ui/react/button\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/utils/cn\";\n\nconst buttonVariants = cva(\n  \"group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-sm font-normal whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/80\",\n        outline:\n          \"border-border bg-background shadow-xs hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground\",\n        ghost:\n          \"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50\",\n        destructive:\n          \"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default:\n          \"h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2\",\n        xs: \"h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3\",\n        sm: \"h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5\",\n        lg: \"h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3\",\n        icon: \"size-9\",\n        \"icon-xs\":\n          \"size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3\",\n        \"icon-sm\":\n          \"size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md\",\n        \"icon-lg\": \"size-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nfunction Button({\n  className,\n  variant = \"default\",\n  size = \"default\",\n  ...props\n}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {\n  return (\n    <ButtonPrimitive\n      data-slot=\"button\"\n      className={cn(buttonVariants({ variant, size, className }))}\n      {...props}\n    />\n  );\n}\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "packages/website/components/ui/collapsible.tsx",
    "content": "\"use client\";\n\nimport { useState, useMemo, type ReactElement, type ReactNode } from \"react\";\nimport { motion, AnimatePresence } from \"motion/react\";\nimport { ChevronDown, ChevronRight } from \"lucide-react\";\n\ninterface CollapsibleProps {\n  header: ReactNode;\n  children: ReactNode;\n  defaultExpanded?: boolean;\n  isStreaming?: boolean;\n  autoExpandOnStreaming?: boolean;\n}\n\nexport const Collapsible = ({\n  header,\n  children,\n  defaultExpanded = true,\n  isStreaming = false,\n  autoExpandOnStreaming = true,\n}: CollapsibleProps): ReactElement => {\n  const [manualExpanded, setManualExpanded] = useState<boolean | null>(null);\n\n  const isExpanded = useMemo(() => {\n    if (manualExpanded !== null) {\n      return manualExpanded;\n    }\n    if (isStreaming && autoExpandOnStreaming) {\n      return true;\n    }\n    return defaultExpanded;\n  }, [manualExpanded, isStreaming, defaultExpanded, autoExpandOnStreaming]);\n\n  const handleToggle = () => {\n    setManualExpanded(!isExpanded);\n  };\n\n  return (\n    <div>\n      <button\n        type=\"button\"\n        onClick={handleToggle}\n        className=\"w-full text-left group relative focus:outline-none\"\n      >\n        <div className=\"flex items-center text-muted-foreground\">\n          {header}\n          <span className=\"ml-2 opacity-50\">\n            {isExpanded ? (\n              <ChevronDown className=\"w-3 h-3\" />\n            ) : (\n              <ChevronRight className=\"w-3 h-3\" />\n            )}\n          </span>\n        </div>\n      </button>\n\n      <AnimatePresence initial={false}>\n        {isExpanded && (\n          <motion.div\n            initial={{ opacity: 0, height: 0 }}\n            animate={{ opacity: 1, height: \"auto\" }}\n            exit={{ opacity: 0, height: 0 }}\n            transition={{ duration: 0.2, ease: \"easeOut\" }}\n            className=\"overflow-hidden\"\n          >\n            {children}\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n};\n\nCollapsible.displayName = \"Collapsible\";\n"
  },
  {
    "path": "packages/website/components/ui/data-table-card.tsx",
    "content": "import type { ReactNode } from \"react\";\n\ninterface DataTableCardProps {\n  title: string;\n  description?: ReactNode;\n  actions?: ReactNode;\n  children: ReactNode;\n}\n\nexport const DataTableCard = ({\n  title,\n  description,\n  actions,\n  children,\n}: DataTableCardProps) => {\n  return (\n    <div className=\"bg-card border border-border rounded-lg overflow-hidden shadow-lg\">\n      <div className=\"p-4 border-b border-border flex flex-col sm:flex-row sm:items-center justify-between gap-4\">\n        <div>\n          <h3 className=\"text-sm font-medium text-foreground/80\">{title}</h3>\n          {description && (\n            <p className=\"text-xs text-muted-foreground mt-1\">{description}</p>\n          )}\n        </div>\n        {actions}\n      </div>\n      <div className=\"overflow-x-auto\">{children}</div>\n    </div>\n  );\n};\n\nDataTableCard.displayName = \"DataTableCard\";\n"
  },
  {
    "path": "packages/website/components/ui/scrollable.tsx",
    "content": "\"use client\";\n\nimport { useRef, useState, useEffect, type ReactElement } from \"react\";\n\ninterface ScrollableProps {\n  children: React.ReactNode;\n  className?: string;\n  maxHeight?: string;\n}\n\nexport const Scrollable = ({\n  children,\n  className = \"\",\n  maxHeight = \"200px\",\n}: ScrollableProps): ReactElement => {\n  const contentRef = useRef<HTMLDivElement>(null);\n  const scrollbarRef = useRef<HTMLDivElement>(null);\n  const [isHovered, setIsHovered] = useState(false);\n  const [showScrollbar, setShowScrollbar] = useState(false);\n  const [scrollbarHeight, setScrollbarHeight] = useState(0);\n  const [scrollbarTop, setScrollbarTop] = useState(0);\n\n  useEffect(() => {\n    const updateScrollbar = () => {\n      if (!contentRef.current) return;\n\n      const element = contentRef.current;\n      const hasScroll = element.scrollHeight > element.clientHeight;\n      setShowScrollbar(hasScroll);\n\n      if (hasScroll) {\n        const scrollRatio = element.clientHeight / element.scrollHeight;\n        const newScrollbarHeight = Math.max(\n          element.clientHeight * scrollRatio,\n          20,\n        );\n        setScrollbarHeight(newScrollbarHeight);\n\n        const scrollPercentage =\n          element.scrollTop / (element.scrollHeight - element.clientHeight);\n        const maxScrollbarTop = element.clientHeight - newScrollbarHeight;\n        setScrollbarTop(scrollPercentage * maxScrollbarTop);\n      }\n    };\n\n    const element = contentRef.current;\n    if (!element) return;\n\n    updateScrollbar();\n    element.addEventListener(\"scroll\", updateScrollbar);\n    window.addEventListener(\"resize\", updateScrollbar);\n\n    return () => {\n      element.removeEventListener(\"scroll\", updateScrollbar);\n      window.removeEventListener(\"resize\", updateScrollbar);\n    };\n  }, [children]);\n\n  const handleScrollbarMouseDown = (event: React.MouseEvent) => {\n    event.preventDefault();\n    const startY = event.clientY;\n    const startScrollTop = contentRef.current?.scrollTop || 0;\n\n    const handleMouseMove = (moveEvent: MouseEvent) => {\n      if (!contentRef.current) return;\n\n      const deltaY = moveEvent.clientY - startY;\n      const scrollRatio =\n        contentRef.current.scrollHeight / contentRef.current.clientHeight;\n      contentRef.current.scrollTop = startScrollTop + deltaY * scrollRatio;\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  return (\n    <div\n      className=\"relative\"\n      onMouseEnter={() => setIsHovered(true)}\n      onMouseLeave={() => setIsHovered(false)}\n    >\n      <div\n        ref={contentRef}\n        className={`overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden ${className}`}\n        style={{ maxHeight }}\n      >\n        {children}\n      </div>\n      <div className=\"absolute bottom-0 left-0 right-0 h-8 pointer-events-none bg-linear-to-t from-black/50 to-transparent\" />\n      {showScrollbar && (\n        <div\n          className={`absolute right-0 top-0 w-1 transition-opacity duration-200 ${\n            isHovered ? \"opacity-100\" : \"opacity-0\"\n          }`}\n          style={{ height: maxHeight }}\n        >\n          <div\n            ref={scrollbarRef}\n            className=\"absolute right-0 w-1 bg-[#3a3a3a] cursor-pointer hover:bg-[#4a4a4a] transition-colors\"\n            style={{\n              height: `${scrollbarHeight}px`,\n              top: `${scrollbarTop}px`,\n            }}\n            onMouseDown={handleScrollbarMouseDown}\n          />\n        </div>\n      )}\n    </div>\n  );\n};\n\nScrollable.displayName = \"Scrollable\";\n"
  },
  {
    "path": "packages/website/components/user-message.tsx",
    "content": "\"use client\";\n\nimport { type ReactElement } from \"react\";\nimport { motion } from \"motion/react\";\nimport { type StreamRenderedBlock } from \"@/hooks/use-stream\";\n\ninterface UserMessageProps {\n  block: StreamRenderedBlock;\n  skipAnimation?: boolean;\n}\n\nexport const UserMessage = ({\n  block,\n  skipAnimation = false,\n}: UserMessageProps): ReactElement => {\n  return (\n    <motion.div\n      initial={skipAnimation ? { opacity: 1, y: 0 } : { opacity: 0, y: 5 }}\n      animate={{ opacity: 1, y: 0 }}\n      transition={{ duration: 0.3, ease: \"easeOut\" }}\n      className=\"ml-auto w-full max-w-full sm:w-auto sm:max-w-[80%] text-left text-foreground bg-card border border-border rounded-lg px-3 py-2\"\n    >\n      {block.content}\n    </motion.div>\n  );\n};\n\nUserMessage.displayName = \"UserMessage\";\n"
  },
  {
    "path": "packages/website/components/view-docs-button.tsx",
    "content": "import { type ReactElement } from \"react\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport { cn } from \"@/utils/cn\";\nimport { BookOpen } from \"lucide-react\";\n\nexport const ViewDocsButton = (): ReactElement => (\n  <a\n    href=\"https://github.com/aidenybai/react-grab#readme\"\n    target=\"_blank\"\n    rel=\"noreferrer\"\n    className={cn(\n      buttonVariants({ variant: \"outline\" }),\n      \"hidden h-auto gap-2 px-3 py-1.5 text-sm active:scale-[0.98] sm:inline-flex sm:text-base\",\n    )}\n  >\n    <BookOpen className=\"h-[15px] w-[15px]\" />\n    View docs\n  </a>\n);\n\nViewDocsButton.displayName = \"ViewDocsButton\";\n"
  },
  {
    "path": "packages/website/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"base-vega\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"\",\n    \"css\": \"app/globals.css\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"iconLibrary\": \"lucide\",\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/utils/cn\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  },\n  \"registries\": {}\n}\n"
  },
  {
    "path": "packages/website/constants.ts",
    "content": "export const INITIAL_STREAM_DELAY_MS = 100;\nexport const BLOCK_TRANSITION_DELAY_MS = 50;\nexport const DEFAULT_CHUNK_SIZE = 4;\nexport const IMMEDIATE_TIMEOUT_MS = 0;\nexport const STREAM_DEMO_CHUNK_DELAY_MS = 20;\nexport const STREAM_DEMO_BLOCK_DELAY_MS = 400;\nexport const STREAM_DEMO_PRELOAD_ANIMATION_DELAY_MULTIPLIER = 0.03;\nexport const GREP_SEARCH_DELAY_MS = 400;\n\nexport const BENCHMARK_GRID_INTERVAL_SECONDS = 5;\nexport const BENCHMARK_CHART_HEIGHT_PX = 320;\nexport const BENCHMARK_BAR_SIZE_PX = 40;\nexport const BENCHMARK_BAR_GAP_PX = 12;\nexport const BENCHMARK_ANIMATION_DURATION_MS = 1000;\nexport const BENCHMARK_CONTROL_COLOR = \"#525252\";\nexport const BENCHMARK_TREATMENT_COLOR = \"#ff4fff\";\nexport const BENCHMARK_LIVE_COUNTER_INTERVAL_MS = 50;\nexport const BENCHMARK_TOOLTIP_CONTROL_SECONDS = 16.8;\nexport const BENCHMARK_TOOLTIP_TREATMENT_SECONDS = 5.8;\nexport const BENCHMARK_TOOLTIP_MAX_SECONDS = 20;\nexport const BENCHMARK_TOOLTIP_SPEEDUP_FACTOR = \"3\";\n\nexport const CLICK_FEEDBACK_DURATION_MS = 300;\nexport const COPY_FEEDBACK_DURATION_MS = 1200;\nexport const TOOLTIP_HOVER_DELAY_MS = 150;\nexport const HOTKEY_KEYUP_DELAY_MS = 150;\nexport const CODE_BLOCK_COLLAPSE_LINE_THRESHOLD = 15;\nexport const CODE_BLOCK_MAX_HEIGHT_PX = 400;\nexport const PROMPT_INSTALL_COLLAPSE_LINE_THRESHOLD = 12;\nexport const PROMPT_INSTALL_MAX_HEIGHT_PX = 260;\nexport const TIMER_UPDATE_INTERVAL_MS = 100;\nexport const AGENT_CYCLE_INTERVAL_MS = 3000;\n\nexport const VIBRATION_DURATION_MS = 100;\nexport const TAP_FEEDBACK_DISPLAY_MS = 800;\nexport const TAP_FEEDBACK_FADE_MS = 300;\nexport const LABEL_OFFSET_BELOW_PX = 10;\nexport const ANIMATION_RESTART_DELAY_MS = 200;\nexport const SELECTION_PADDING_PX = 4;\nexport const CURSOR_OFFSET_PX = 16;\nexport const HINT_OVERLAY_DELAY_MS = 3000;\nexport const IDLE_RESTART_DELAY_MS = 5000;\n"
  },
  {
    "path": "packages/website/hooks/use-stream.ts",
    "content": "\"use client\";\n\nimport { useState, useEffect, useRef, type ReactNode } from \"react\";\nimport {\n  INITIAL_STREAM_DELAY_MS,\n  BLOCK_TRANSITION_DELAY_MS,\n  DEFAULT_CHUNK_SIZE,\n  IMMEDIATE_TIMEOUT_MS,\n} from \"../constants\";\n\ntype BlockContent = string | ReactNode | Array<string | ReactNode>;\n\nconst getTextContent = (content: BlockContent): string => {\n  if (typeof content === \"string\") return content;\n  if (Array.isArray(content)) {\n    return content\n      .filter((item): item is string => typeof item === \"string\")\n      .join(\"\");\n  }\n  return \"\";\n};\n\nexport type StreamStatus = \"pending\" | \"streaming\" | \"complete\";\n\nexport interface StreamBlock {\n  id: string;\n  type:\n    | \"thought\"\n    | \"message\"\n    | \"tool_call\"\n    | \"planning\"\n    | \"user_message\"\n    | \"code_block\";\n  content: string | ReactNode | Array<string | ReactNode>;\n  duration?: number;\n  metadata?: Record<string, unknown>;\n}\n\ninterface StreamChunk {\n  id: string;\n  text: string;\n}\n\nexport interface StreamRenderedBlock {\n  id: string;\n  type:\n    | \"thought\"\n    | \"message\"\n    | \"tool_call\"\n    | \"planning\"\n    | \"user_message\"\n    | \"code_block\";\n  content: string | ReactNode | Array<string | ReactNode>;\n  chunks: StreamChunk[];\n  status: StreamStatus;\n  startTime?: number;\n  endTime?: number;\n  duration?: number;\n  metadata?: Record<string, unknown>;\n}\n\ninterface UseStreamOptions {\n  blocks: StreamBlock[];\n  chunkSize?: number;\n  chunkDelayMs?: number;\n  blockDelayMs?: number;\n  storageKey?: string;\n  pauseAtBlockId?: string;\n  skipAnimation?: boolean;\n}\n\ninterface StreamState {\n  currentBlockIndex: number;\n  currentContent: string | ReactNode | Array<string | ReactNode>;\n  status: StreamStatus;\n  blocks: StreamRenderedBlock[];\n  wasPreloaded: boolean;\n  isPaused: boolean;\n}\n\ninterface UseStreamReturn extends StreamState {\n  resume: () => void;\n}\n\nconst hasRawParam = (): boolean => {\n  if (typeof window === \"undefined\") return false;\n  const params = new URLSearchParams(window.location.search);\n  return params.has(\"raw\");\n};\n\nconst saveCompletionToStorage = (key: string): void => {\n  if (typeof window !== \"undefined\" && !hasRawParam()) {\n    localStorage.setItem(key, \"true\");\n  }\n};\n\nconst hasCompletedStream = (key: string): boolean => {\n  if (typeof window === \"undefined\") return false;\n  if (hasRawParam()) return false;\n  return localStorage.getItem(key) === \"true\";\n};\n\nexport const useStream = ({\n  blocks,\n  chunkSize,\n  chunkDelayMs,\n  blockDelayMs,\n  storageKey = \"stream-completed\",\n  pauseAtBlockId,\n  skipAnimation = false,\n}: UseStreamOptions): UseStreamReturn => {\n  const [state, setState] = useState<StreamState>(() => ({\n    currentBlockIndex: 0,\n    currentContent: \"\",\n    status: \"pending\",\n    wasPreloaded: false,\n    isPaused: false,\n    blocks: blocks.map((block) => ({\n      id: block.id,\n      type: block.type,\n      content: \"\",\n      chunks: [],\n      status: \"pending\" as StreamStatus,\n      duration: block.duration,\n      metadata: block.metadata,\n    })),\n  }));\n\n  const [hasCheckedStorage, setHasCheckedStorage] = useState(false);\n\n  useEffect(() => {\n    if (hasCheckedStorage) return;\n\n    if (typeof window !== \"undefined\") {\n      const hasCompleted = skipAnimation || hasCompletedStream(storageKey);\n\n      if (hasCompleted) {\n        if (skipAnimation) {\n          saveCompletionToStorage(storageKey);\n        }\n        // eslint-disable-next-line react-hooks/set-state-in-effect\n        setState({\n          currentBlockIndex: blocks.length,\n          currentContent: \"\",\n          status: \"complete\",\n          wasPreloaded: true,\n          isPaused: false,\n          blocks: blocks.map((block) => ({\n            id: block.id,\n            type: block.type,\n            content: block.content,\n            chunks: [],\n            status: \"complete\" as StreamStatus,\n            duration: block.duration,\n            metadata: block.metadata,\n          })),\n        });\n      }\n\n      setHasCheckedStorage(true);\n    }\n  }, [blocks, storageKey, hasCheckedStorage, skipAnimation]);\n\n  const streamingRef = useRef(false);\n  const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);\n  const currentBlockIdxRef = useRef(0);\n  const currentCharIdxRef = useRef(0);\n  const resumeCallbackRef = useRef<(() => void) | null>(null);\n  const blocksRef = useRef(blocks);\n\n  useEffect(() => {\n    blocksRef.current = blocks;\n  }, [blocks]);\n\n  useEffect(() => {\n    if (streamingRef.current || blocks.length === 0 || !hasCheckedStorage)\n      return;\n\n    if (hasCompletedStream(storageKey)) return;\n\n    streamingRef.current = true;\n    currentBlockIdxRef.current = 0;\n    currentCharIdxRef.current = 0;\n\n    const advanceToNextBlock = (delayMs: number): boolean => {\n      const previousBlockIdx = currentBlockIdxRef.current - 1;\n      const currentBlocks = blocksRef.current;\n      const justCompletedBlock = currentBlocks[previousBlockIdx];\n\n      if (pauseAtBlockId && justCompletedBlock?.id === pauseAtBlockId) {\n        setState((prev) => ({ ...prev, isPaused: true }));\n        resumeCallbackRef.current = () => {\n          setState((prev) => ({ ...prev, isPaused: false }));\n          timeoutRef.current = setTimeout(streamNextChunk, delayMs);\n        };\n        return true;\n      }\n\n      if (currentBlockIdxRef.current < currentBlocks.length) {\n        timeoutRef.current = setTimeout(streamNextChunk, delayMs);\n      } else {\n        setState((prev) => ({ ...prev, status: \"complete\" }));\n        saveCompletionToStorage(storageKey);\n      }\n      return false;\n    };\n\n    const streamNextChunk = (): void => {\n      const currentBlockIdx = currentBlockIdxRef.current;\n      const currentCharIdx = currentCharIdxRef.current;\n      const currentBlocks = blocksRef.current;\n\n      if (currentBlockIdx >= currentBlocks.length) {\n        setState((prev) => ({ ...prev, status: \"complete\" }));\n        saveCompletionToStorage(storageKey);\n        return;\n      }\n\n      const currentBlock = currentBlocks[currentBlockIdx];\n      const blockContent = currentBlock.content;\n      const isToolCall = currentBlock.type === \"tool_call\";\n      const isArray = Array.isArray(blockContent);\n      const textContent = getTextContent(blockContent);\n      const isReactNode = typeof blockContent !== \"string\" && !isArray;\n      const isInstantBlock =\n        currentBlock.type === \"user_message\" || isReactNode;\n\n      if (currentCharIdx === 0) {\n        setState((prev) => {\n          const newBlocks = [...prev.blocks];\n          newBlocks[currentBlockIdx] = {\n            ...newBlocks[currentBlockIdx],\n            status: \"streaming\",\n            startTime: Date.now(),\n          };\n          return {\n            ...prev,\n            currentBlockIndex: currentBlockIdx,\n            status: \"streaming\",\n            blocks: newBlocks,\n          };\n        });\n      }\n\n      if (isToolCall) {\n        if (currentCharIdx === 0) {\n          timeoutRef.current = setTimeout(() => {\n            setState((prev) => {\n              const newBlocks = [...prev.blocks];\n              newBlocks[currentBlockIdx] = {\n                ...newBlocks[currentBlockIdx],\n                content: blockContent,\n                status: \"complete\",\n                endTime: Date.now(),\n              };\n              return {\n                ...prev,\n                currentContent: blockContent,\n                blocks: newBlocks,\n              };\n            });\n\n            currentBlockIdxRef.current++;\n            currentCharIdxRef.current = 0;\n            advanceToNextBlock(IMMEDIATE_TIMEOUT_MS);\n          }, blockDelayMs);\n        }\n        return;\n      }\n\n      if (isInstantBlock) {\n        setState((prev) => {\n          const newBlocks = [...prev.blocks];\n          const existingBlock = newBlocks[currentBlockIdx];\n          if (!existingBlock) return prev;\n\n          newBlocks[currentBlockIdx] = {\n            ...existingBlock,\n            content: blockContent,\n            chunks: [],\n            status: \"complete\",\n            endTime: Date.now(),\n          };\n\n          return {\n            ...prev,\n            currentContent: blockContent,\n            blocks: newBlocks,\n          };\n        });\n\n        currentBlockIdxRef.current++;\n        currentCharIdxRef.current = 0;\n\n        if (advanceToNextBlock(IMMEDIATE_TIMEOUT_MS)) return;\n        return;\n      }\n\n      if (typeof blockContent !== \"string\" && !isArray) return;\n\n      const endIdx = Math.min(\n        currentCharIdx + (chunkSize || DEFAULT_CHUNK_SIZE),\n        textContent.length,\n      );\n      const chunk = textContent.slice(currentCharIdx, endIdx);\n\n      setState((prev) => {\n        const newBlocks = [...prev.blocks];\n        const existingBlock = newBlocks[currentBlockIdx];\n        if (!existingBlock) return prev;\n\n        const nextChunkId = `${existingBlock.id}-${existingBlock.chunks.length}`;\n        const existingTextContent = getTextContent(existingBlock.content);\n        const newTextContent = existingTextContent + chunk;\n\n        const newContent = isArray\n          ? blockContent.map((item) =>\n              typeof item === \"string\" ? newTextContent : item,\n            )\n          : newTextContent;\n\n        newBlocks[currentBlockIdx] = {\n          ...existingBlock,\n          content: newContent,\n          chunks: [\n            ...existingBlock.chunks,\n            {\n              id: nextChunkId,\n              text: chunk,\n            },\n          ],\n        };\n        return {\n          ...prev,\n          currentContent: newBlocks[currentBlockIdx].content,\n          blocks: newBlocks,\n        };\n      });\n\n      currentCharIdxRef.current = endIdx;\n\n      if (currentCharIdxRef.current >= textContent.length) {\n        setState((prev) => {\n          const newBlocks = [...prev.blocks];\n          newBlocks[currentBlockIdx] = {\n            ...newBlocks[currentBlockIdx],\n            status: \"complete\",\n            endTime: Date.now(),\n          };\n          return {\n            ...prev,\n            blocks: newBlocks,\n          };\n        });\n\n        currentBlockIdxRef.current++;\n        currentCharIdxRef.current = 0;\n        advanceToNextBlock(BLOCK_TRANSITION_DELAY_MS);\n      } else {\n        timeoutRef.current = setTimeout(streamNextChunk, chunkDelayMs);\n      }\n    };\n\n    timeoutRef.current = setTimeout(streamNextChunk, INITIAL_STREAM_DELAY_MS);\n\n    return () => {\n      if (timeoutRef.current) {\n        clearTimeout(timeoutRef.current);\n      }\n    };\n  }, [\n    blocks,\n    chunkSize,\n    chunkDelayMs,\n    blockDelayMs,\n    storageKey,\n    pauseAtBlockId,\n    hasCheckedStorage,\n  ]);\n\n  const resume = (): void => {\n    if (resumeCallbackRef.current) {\n      resumeCallbackRef.current();\n      resumeCallbackRef.current = null;\n    }\n  };\n\n  return { ...state, resume };\n};\n"
  },
  {
    "path": "packages/website/instrumentation-client.ts",
    "content": "import { init } from \"react-grab/core\";\n\ndeclare global {\n  interface Window {\n    __REACT_GRAB__?: ReturnType<typeof init>;\n  }\n}\n\nif (typeof window !== \"undefined\" && !window.__REACT_GRAB__) {\n  const api = init({});\n\n  api.registerPlugin({\n    name: \"website-events\",\n    hooks: {\n      onActivate: () => {\n        window.dispatchEvent(new CustomEvent(\"react-grab:activated\"));\n      },\n      onDeactivate: () => {\n        window.dispatchEvent(new CustomEvent(\"react-grab:deactivated\"));\n      },\n    },\n  });\n\n  const isMobile =\n    navigator.maxTouchPoints > 0 || matchMedia(\"(pointer: coarse)\").matches;\n  if (isMobile) {\n    api.registerPlugin({\n      name: \"mobile-no-toolbar\",\n      theme: { toolbar: { enabled: false } },\n    });\n  }\n\n  window.__REACT_GRAB__ = api;\n}\n"
  },
  {
    "path": "packages/website/lib/api-helpers.ts",
    "content": "type HttpMethod = \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"PATCH\" | \"OPTIONS\";\n\ninterface CorsHeadersOptions {\n  methods?: readonly HttpMethod[] | HttpMethod[] | \"*\";\n  headers?: readonly string[] | string[] | \"*\";\n  origin?: string;\n}\n\nexport const getCorsHeaders = (\n  options: CorsHeadersOptions = {},\n): Record<string, string> => {\n  const {\n    methods = [\"GET\", \"OPTIONS\"],\n    headers = [\"Content-Type\"],\n    origin = \"*\",\n  } = options;\n\n  return {\n    \"Access-Control-Allow-Origin\": origin,\n    \"Access-Control-Allow-Methods\": methods === \"*\" ? \"*\" : methods.join(\", \"),\n    \"Access-Control-Allow-Headers\": headers === \"*\" ? \"*\" : headers.join(\", \"),\n  };\n};\n\nexport const createOptionsResponse = (\n  corsOptions?: CorsHeadersOptions,\n): Response => {\n  return new Response(null, {\n    status: 204,\n    headers: getCorsHeaders(corsOptions),\n  });\n};\n\nexport const createJsonResponse = (\n  data: unknown,\n  options: { status?: number; corsOptions?: CorsHeadersOptions } = {},\n): Response => {\n  const { status = 200, corsOptions } = options;\n  return new Response(JSON.stringify(data), {\n    status,\n    headers: {\n      ...getCorsHeaders(corsOptions),\n      \"Content-Type\": \"application/json\",\n    },\n  });\n};\n\nexport const createErrorResponse = (\n  error: unknown,\n  options: { status?: number; corsOptions?: CorsHeadersOptions } = {},\n): Response => {\n  const { status = 500, corsOptions } = options;\n  const errorMessage = error instanceof Error ? error.message : \"Unknown error\";\n  return createJsonResponse({ error: errorMessage }, { status, corsOptions });\n};\n"
  },
  {
    "path": "packages/website/lib/shiki.ts",
    "content": "import {\n  createHighlighter,\n  type CodeToHastOptions,\n  type Highlighter,\n} from \"shiki\";\n\nconst SHIKI_COLOR_OVERRIDES: Record<string, string> = {\n  \"#99FFE4\": \"#9f9f9f\",\n  \"#FFC799\": \"#ffa0f3\",\n};\n\nconst LINE_SPAN_REGEX = /<span class=(\"|')line\\1>/g;\n\nconst applyColorOverrides = (html: string): string => {\n  return Object.entries(SHIKI_COLOR_OVERRIDES).reduce(\n    (result, [from, to]) => result.replace(new RegExp(from, \"gi\"), to),\n    html,\n  );\n};\n\nconst removeBackground = (html: string): string => {\n  return html\n    .replace(/style=\"[^\"]*background-color:[^;\"]*;?[^\"]*\"/gi, (match) => {\n      return match.replace(/background-color:[^;\"]*;?/gi, \"\");\n    })\n    .replace(/style=\"\"/g, \"\");\n};\n\nconst injectLineNumbers = (html: string): string => {\n  let lineNumber = 1;\n  return html.replace(LINE_SPAN_REGEX, () => {\n    const current = lineNumber;\n    lineNumber += 1;\n    return `<span class=\"line\"><span class=\"line-number\" data-line=\"${current}\"></span>`;\n  });\n};\n\nconst globalForShiki = globalThis as unknown as {\n  shikiHighlighter: Highlighter | undefined;\n  shikiHighlighterPromise: Promise<Highlighter> | undefined;\n};\n\nconst getHighlighter = async (): Promise<Highlighter> => {\n  if (globalForShiki.shikiHighlighter) {\n    return globalForShiki.shikiHighlighter;\n  }\n\n  if (globalForShiki.shikiHighlighterPromise) {\n    return globalForShiki.shikiHighlighterPromise;\n  }\n\n  globalForShiki.shikiHighlighterPromise = createHighlighter({\n    themes: [\"vesper\"],\n    langs: [\"typescript\", \"javascript\", \"tsx\", \"jsx\", \"html\", \"json\", \"bash\"],\n  }).then((highlighter) => {\n    globalForShiki.shikiHighlighter = highlighter;\n    return highlighter;\n  });\n\n  return globalForShiki.shikiHighlighterPromise;\n};\n\nconst highlightChangedLines = (\n  html: string,\n  changedLines?: number[],\n): string => {\n  if (!changedLines?.length) return html;\n\n  const changedLinesSet = new Set(changedLines);\n  let lineNumber = 0;\n  return html.replace(LINE_SPAN_REGEX, (match) => {\n    lineNumber += 1;\n    return changedLinesSet.has(lineNumber)\n      ? `<span class=\"line line-changed\">`\n      : match;\n  });\n};\n\ninterface HighlightCodeOptions {\n  code: string;\n  lang: string;\n  theme?: \"vesper\";\n  showLineNumbers?: boolean;\n  changedLines?: number[];\n}\n\nexport const highlightCode = async ({\n  code,\n  lang,\n  theme = \"vesper\",\n  showLineNumbers = false,\n  changedLines,\n}: HighlightCodeOptions): Promise<string> => {\n  const highlighter = await getHighlighter();\n  const options: CodeToHastOptions = { lang, theme };\n  let html = await highlighter.codeToHtml(code, options);\n  html = applyColorOverrides(html);\n  html = removeBackground(html);\n  if (showLineNumbers) {\n    html = injectLineNumbers(html);\n  }\n  html = highlightChangedLines(html, changedLines);\n  return html;\n};\n"
  },
  {
    "path": "packages/website/next.config.ts",
    "content": "import type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {\n  serverExternalPackages: [\"react-grab\"],\n  // HACK: disable react compiler to avoid issues with source mangling\n  reactCompiler: false,\n  productionBrowserSourceMaps: true,\n  turbopack: {},\n  experimental: {\n    optimizeCss: true,\n    inlineCss: true,\n  },\n  devIndicators: false,\n  webpack: (config, { dev, isServer }) => {\n    if (!isServer && !dev) {\n      config.devtool = \"source-map\";\n    }\n    return config;\n  },\n  redirects: async () => {\n    return [\n      {\n        source: \"/docs\",\n        destination: \"https://github.com/aidenybai/react-grab#readme\",\n        permanent: false,\n      },\n      {\n        source: \"/primitives\",\n        destination:\n          \"https://github.com/aidenybai/react-grab/tree/main?tab=readme-ov-file#primitives\",\n        permanent: false,\n      },\n    ];\n  },\n  rewrites: async () => {\n    return {\n      beforeFiles: [\n        {\n          source: \"/\",\n          destination: \"/llms.txt\",\n          has: [\n            {\n              type: \"header\",\n              key: \"accept\",\n              value: \"(.*)text/markdown(.*)\",\n            },\n          ],\n        },\n        {\n          source: \"/llm.txt\",\n          destination: \"/llms.txt\",\n        },\n      ],\n    };\n  },\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "packages/website/package.json",
    "content": "{\n  \"name\": \"@react-grab/website\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"pnpm --filter react-grab build && pnpm --filter react-grab exec cp dist/index.global.js ../website/public/script.js && pnpm --filter @react-grab/design-system build && next build\",\n    \"start\": \"next start\",\n    \"lint\": \"oxlint\"\n  },\n  \"dependencies\": {\n    \"@base-ui/react\": \"^1.2.0\",\n    \"@react-grab/design-system\": \"workspace:*\",\n    \"@vercel/analytics\": \"^1.5.0\",\n    \"@vercel/firewall\": \"^1.1.1\",\n    \"ai\": \"^5.0.36\",\n    \"botid\": \"^1.5.10\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"lucide-react\": \"^0.553.0\",\n    \"motion\": \"^12.23.24\",\n    \"next\": \"16.0.10\",\n    \"nuqs\": \"^2.8.1\",\n    \"pretty-ms\": \"^9.3.0\",\n    \"react\": \"19.2.1\",\n    \"react-dom\": \"19.2.1\",\n    \"react-grab\": \"workspace:*\",\n    \"recharts\": \"^2.15.4\",\n    \"shiki\": \"^1.24.3\",\n    \"tailwind-merge\": \"^3.4.0\",\n    \"torph\": \"^0.0.5\",\n    \"web-haptics\": \"^0.0.6\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/postcss\": \"^4\",\n    \"@types/node\": \"^20\",\n    \"@types/react\": \"^19\",\n    \"@types/react-dom\": \"^19\",\n    \"critters\": \"^0.0.25\",\n    \"eslint\": \"^9\",\n    \"eslint-config-next\": \"16.0.7\",\n    \"tailwindcss\": \"^4\",\n    \"tw-animate-css\": \"^1.4.0\",\n    \"typescript\": \"^5\"\n  },\n  \"browserslist\": [\n    \"chrome >= 91\",\n    \"firefox >= 90\",\n    \"safari >= 15\",\n    \"edge >= 91\"\n  ]\n}\n"
  },
  {
    "path": "packages/website/postcss.config.mjs",
    "content": "const config = {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "packages/website/public/install.md",
    "content": "# React Grab\n\nGrab any element in your app and give it to Cursor, Claude Code, or other AI coding agents.\n\n## OBJECTIVE\n\nInstall React Grab in a React project to enable element grabbing for AI coding agents.\n\n## DONE WHEN\n\n- [ ] React Grab package is installed\n- [ ] Framework integration is configured (Next.js, Vite, or Webpack)\n- [ ] Running the dev server shows the grab overlay when pressing the activation key\n\n## INSTALLATION\n\n### CLI (Recommended)\n\nRun this command at your project root. Use the `-y` flag to skip interactive prompts:\n\n```bash\nnpx -y grab@latest init -y\n```\n\nThe CLI will auto-detect your framework and configure everything automatically.\n\n### Init Options\n\n```\nOptions:\n  -f, --framework        Override detected framework [choices: \"next\", \"vite\", \"webpack\"]\n  -p, --package-manager  Override detected package manager [choices: \"npm\", \"yarn\", \"pnpm\", \"bun\"]\n  -r, --router           Next.js router type [choices: \"app\", \"pages\"]\n  -k, --key              Activation key (e.g., \"Meta+K\", \"Ctrl+Shift+G\", \"Space\")\n  -y, --yes              Skip all prompts and use auto-detected/default values\n      --skip-install     Only modify config files, skip package install\n```\n\n### Examples\n\n```bash\nnpx -y grab@latest init -y                         # Auto-detect and install without prompts\nnpx -y grab@latest init -f next -r app -y          # Configure for Next.js App Router\nnpx -y grab@latest init -k \"Meta+K\" -y             # Set activation key to Cmd+K / Win+K\n```\n\n## MANUAL INSTALLATION\n\nIf CLI fails, install manually based on your framework:\n\n### 1. Install the package\n\n```bash\nnpm install react-grab@latest\n# or yarn add react-grab@latest\n# or pnpm add react-grab@latest\n# or bun add react-grab@latest\n```\n\n### 2. Configure for your framework\n\n#### Next.js (App Router)\n\nAdd to `app/layout.tsx`:\n\n```jsx\nimport Script from \"next/script\";\n\nexport default function RootLayout({ children }) {\n  return (\n    <html>\n      <head>\n        {process.env.NODE_ENV === \"development\" && (\n          <Script\n            src=\"//unpkg.com/react-grab/dist/index.global.js\"\n            crossOrigin=\"anonymous\"\n            strategy=\"beforeInteractive\"\n          />\n        )}\n      </head>\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\n#### Next.js (Pages Router)\n\nAdd to `pages/_document.tsx`:\n\n```jsx\nimport { Html, Head, Main, NextScript } from \"next/document\";\n\nexport default function Document() {\n  return (\n    <Html lang=\"en\">\n      <Head>\n        {process.env.NODE_ENV === \"development\" && (\n          <Script\n            src=\"//unpkg.com/react-grab/dist/index.global.js\"\n            crossOrigin=\"anonymous\"\n            strategy=\"beforeInteractive\"\n          />\n        )}\n      </Head>\n      <body>\n        <Main />\n        <NextScript />\n      </body>\n    </Html>\n  );\n}\n```\n\n#### Vite\n\nAdd to your main entry file (e.g., `src/main.tsx`):\n\n```tsx\nif (import.meta.env.DEV) {\n  import(\"react-grab\");\n}\n```\n\n#### Webpack\n\nAdd to your main entry file (e.g., `src/index.tsx`):\n\n```tsx\nif (process.env.NODE_ENV === \"development\") {\n  import(\"react-grab\");\n}\n```\n\n## VERIFICATION\n\n1. Start your development server\n2. Open your app in the browser\n3. Hover over any UI element and press **Cmd+C** (Mac) or **Ctrl+C** (Windows/Linux)\n4. The element's context should be copied to your clipboard\n\nExpected clipboard output format:\n\n```\n<a class=\"ml-auto inline-block text-sm\" href=\"#\">\n  Forgot your password?\n</a>\nin LoginForm at components/login-form.tsx:46:19\n```\n\n## CONFIGURATION (OPTIONAL)\n\nCustomize React Grab behavior:\n\n```bash\nnpx -y grab@latest config -k \"Meta+K\"              # Set activation key to Cmd+K / Win+K\nnpx -y grab@latest config -m hold --hold-duration 150   # Use hold mode with 150ms delay\nnpx -y grab@latest config --context-lines 5        # Include 5 lines of context\n```\n\n## TROUBLESHOOTING\n\n- **Overlay not showing**: Ensure you're in development mode (`NODE_ENV=development`)\n- **Copy not working**: Check if another extension is capturing the keyboard shortcut\n- **Framework not detected**: Use `-f` flag to specify framework manually\n\n## RESOURCES\n\n- Documentation: https://react-grab.com/llms.txt\n- GitHub: https://github.com/aidenybai/react-grab\n"
  },
  {
    "path": "packages/website/public/llms.txt",
    "content": "# React Grab\n\nGrab any element in your app and give it to Cursor, Claude Code, or other AI coding agents.\n\n## Usage\n\nOnce installed, hover over any UI element in your browser and press:\n\n- **⌘C** (Cmd+C) on Mac\n- **Ctrl+C** on Windows/Linux\n\nThis copies the element's context (file name, React component, and HTML source code) to your clipboard ready to paste into your coding agent. For example:\n\n```\n<a class=\"ml-auto inline-block text-sm\" href=\"#\">\n  Forgot your password?\n</a>\nin LoginForm at components/login-form.tsx:46:19\n```\n\n## Installation\n\n### CLI (Recommended)\n\nRun this command at your project root. Use the `-y` flag to skip interactive prompts (required for non-interactive environments):\n\n```bash\nnpx -y grab@latest init -y\n```\n\nInit Options:\n```\nOptions:\n  -y, --yes              Skip confirmation prompts\n  -f, --force            Force overwrite existing config\n  -a, --agent <agent>    Connect to your agent (claude-code, cursor, opencode, codex, gemini, amp, ami, droid, mcp)\n  -k, --key <key>        Activation key (e.g., \"Meta+K\", \"Ctrl+Shift+G\", \"Space\")\n      --skip-install     Skip package installation\n  -c, --cwd <cwd>        Working directory (defaults to current directory)\n\nExamples:\n  npx -y grab@latest init -y                         # Auto-detect and install without prompts\n  npx -y grab@latest init -a cursor -y               # Connect to Cursor agent\n  npx -y grab@latest init -a mcp -y                  # Connect via MCP\n  npx -y grab@latest init -k \"Meta+K\" -y             # Set activation key to Cmd+K / Win+K\n```\n\nThe CLI will detect your framework and add the necessary scripts automatically.\n\n### Agent Connection\n\nConnect React Grab to your coding agent:\n\n```bash\nnpx -y grab@latest add [agent]\n```\n\nAdd Options:\n```\nOptions:\n  -y, --yes              Skip confirmation prompts\n  -c, --cwd <cwd>        Working directory (defaults to current directory)\n\nAgents: claude-code, cursor, opencode, codex, gemini, amp, ami, droid, mcp\n\nExamples:\n  npx -y grab@latest add cursor                      # Connect to Cursor\n  npx -y grab@latest add claude-code                  # Connect to Claude Code\n  npx -y grab@latest add mcp                          # Connect via MCP\n```\n\nDisconnect an agent:\n\n```bash\nnpx -y grab@latest remove [agent]\n```\n\n### Configuration\n\nConfigure React Grab options (activation key, mode, etc.):\n\n```bash\nnpx -y grab@latest config\n```\n\nConfig Options:\n```\nOptions:\n  -y, --yes                  Skip confirmation prompts\n  -k, --key <key>            Activation key (e.g., \"Meta+K\", \"Ctrl+Shift+G\", \"Space\")\n  -m, --mode <mode>          Activation mode (toggle, hold)\n      --hold-duration <ms>   Key hold duration in milliseconds (for hold mode)\n      --allow-input <bool>   Allow activation inside input fields (true/false)\n      --context-lines <n>    Max context lines to include\n      --cdn <domain>         CDN domain (e.g., unpkg.com, custom.react-grab.com)\n  -c, --cwd <cwd>            Working directory (defaults to current directory)\n\nExamples:\n  npx -y grab@latest config -k \"Meta+K\"              # Set activation key to Cmd+K / Win+K\n  npx -y grab@latest config -k \"Ctrl+Shift+G\"        # Set activation key to Ctrl+Shift+G\n  npx -y grab@latest config -m hold --hold-duration 150   # Use hold mode with 150ms delay\n  npx -y grab@latest config --context-lines 5         # Include 5 lines of context\n```\n\nTo discover all available commands and flags:\n\n```bash\nnpx -y grab@latest --help\nnpx -y grab@latest init --help\nnpx -y grab@latest add --help\nnpx -y grab@latest config --help\n```\n\n### Manual Installation\n\nIf you prefer manual installation, use the correct package manager for your project:\n\n```bash\nnpm install react-grab@latest\n# or\nyarn add react-grab@latest\n# or\npnpm add react-grab@latest\n# or\nbun add react-grab@latest\n```\n\n### Next.js (App router)\n\nAdd this inside of your `app/layout.tsx`:\n\n```jsx\nimport Script from \"next/script\";\n\nexport default function RootLayout({ children }) {\n  return (\n    <html>\n      <head>\n        {process.env.NODE_ENV === \"development\" && (\n          <Script\n            src=\"//unpkg.com/react-grab/dist/index.global.js\"\n            crossOrigin=\"anonymous\"\n            strategy=\"beforeInteractive\"\n          />\n        )}\n      </head>\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\n### Next.js (Pages router)\n\nAdd this into your `pages/_document.tsx`:\n\n```jsx\nimport { Html, Head, Main, NextScript } from \"next/document\";\n\nexport default function Document() {\n  return (\n    <Html lang=\"en\">\n      <Head>\n        {process.env.NODE_ENV === \"development\" && (\n          <Script\n            src=\"//unpkg.com/react-grab/dist/index.global.js\"\n            crossOrigin=\"anonymous\"\n            strategy=\"beforeInteractive\"\n          />\n        )}\n      </Head>\n      <body>\n        <Main />\n        <NextScript />\n      </body>\n    </Html>\n  );\n}\n```\n\n### Vite\n\nAdd this to your `index.html`:\n\n```html\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <script type=\"module\">\n      if (import.meta.env.DEV) {\n        import(\"react-grab\");\n      }\n    </script>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n```\n\n### Webpack\n\nFirst, install React Grab:\n\n```bash\nnpm install react-grab\n```\n\nThen add this at the top of your main entry file (e.g., `src/index.tsx` or `src/main.tsx`):\n\n```tsx\nif (process.env.NODE_ENV === \"development\") {\n  import(\"react-grab\");\n}\n```\n"
  },
  {
    "path": "packages/website/public/r/index.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry.json\",\n  \"name\": \"react-grab\",\n  \"homepage\": \"https://react-grab.com\",\n  \"items\": [\n    {\n      \"name\": \"react-grab\",\n      \"type\": \"registry:component\",\n      \"title\": \"React Grab\",\n      \"description\": \"Loads React Grab as early as possible — select context for coding agents directly from your website.\",\n      \"dependencies\": [\"react-grab\"],\n      \"files\": [\n        {\n          \"path\": \"registry/react-grab.tsx\",\n          \"type\": \"registry:component\",\n          \"target\": \"components/react-grab.tsx\"\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/website/public/r/react-grab.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry-item.json\",\n  \"name\": \"react-grab\",\n  \"type\": \"registry:component\",\n  \"title\": \"React Grab\",\n  \"description\": \"Loads React Grab as early as possible — select context for coding agents directly from your website.\",\n  \"dependencies\": [\n    \"react-grab\"\n  ],\n  \"files\": [\n    {\n      \"path\": \"components/react-grab.tsx\",\n      \"content\": \"\\\"use client\\\";\\n\\nimport { useEffect } from \\\"react\\\";\\n\\nconst SCRIPT_ID = \\\"react-grab-script\\\";\\nconst SCRIPT_SRC = \\\"https://unpkg.com/react-grab/dist/index.global.js\\\";\\n\\nexport const ReactGrab = () => {\\n  useEffect(() => {\\n    if (process.env.NODE_ENV !== \\\"development\\\") return;\\n    if (document.getElementById(SCRIPT_ID)) return;\\n    const script = document.createElement(\\\"script\\\");\\n    script.id = SCRIPT_ID;\\n    script.src = SCRIPT_SRC;\\n    script.crossOrigin = \\\"anonymous\\\";\\n    document.head.appendChild(script);\\n  }, []);\\n  return null;\\n};\\n\",\n      \"type\": \"registry:component\",\n      \"target\": \"components/react-grab.tsx\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/website/public/results.json",
    "content": "[\n  {\n    \"testName\": \"Forgot Password Link\",\n    \"type\": \"treatment\",\n    \"inputTokens\": 13432,\n    \"outputTokens\": 10,\n    \"costUsd\": 0.006908599999999999,\n    \"durationMs\": 4755,\n    \"toolCalls\": 0,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Grayscale Avatar\",\n    \"type\": \"treatment\",\n    \"inputTokens\": 28369,\n    \"outputTokens\": 89,\n    \"costUsd\": 0.022908249999999998,\n    \"durationMs\": 9423,\n    \"toolCalls\": 1,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Time Range Toggle\",\n    \"type\": \"control\",\n    \"inputTokens\": 26980,\n    \"outputTokens\": 139,\n    \"costUsd\": 0.01833385,\n    \"durationMs\": 10164,\n    \"toolCalls\": 1,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Forgot Password Link\",\n    \"type\": \"control\",\n    \"inputTokens\": 41795,\n    \"outputTokens\": 261,\n    \"costUsd\": 0.02949275,\n    \"durationMs\": 13411,\n    \"toolCalls\": 5,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Grayscale Avatar\",\n    \"type\": \"control\",\n    \"inputTokens\": 56388,\n    \"outputTokens\": 406,\n    \"costUsd\": 0.0380717,\n    \"durationMs\": 19256,\n    \"toolCalls\": 6,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Editable Target Input\",\n    \"type\": \"treatment\",\n    \"inputTokens\": 13443,\n    \"outputTokens\": 10,\n    \"costUsd\": 0.007274849999999999,\n    \"durationMs\": 4082,\n    \"toolCalls\": 0,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Drag Handle\",\n    \"type\": \"treatment\",\n    \"inputTokens\": 13458,\n    \"outputTokens\": 10,\n    \"costUsd\": 0.0072361,\n    \"durationMs\": 4445,\n    \"toolCalls\": 0,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Time Range Toggle\",\n    \"type\": \"treatment\",\n    \"inputTokens\": 31849,\n    \"outputTokens\": 73,\n    \"costUsd\": 0.036040600000000006,\n    \"durationMs\": 7015,\n    \"toolCalls\": 1,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Drag Handle\",\n    \"type\": \"control\",\n    \"inputTokens\": 27066,\n    \"outputTokens\": 105,\n    \"costUsd\": 0.018257749999999996,\n    \"durationMs\": 10539,\n    \"toolCalls\": 1,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Editable Target Input\",\n    \"type\": \"control\",\n    \"inputTokens\": 51296,\n    \"outputTokens\": 354,\n    \"costUsd\": 0.0659306,\n    \"durationMs\": 13507,\n    \"toolCalls\": 4,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Quick Create Button\",\n    \"type\": \"treatment\",\n    \"inputTokens\": 13461,\n    \"outputTokens\": 10,\n    \"costUsd\": 0.007172349999999999,\n    \"durationMs\": 4085,\n    \"toolCalls\": 0,\n    \"success\": true\n  },\n  {\n    \"testName\": \"OTP Input\",\n    \"type\": \"treatment\",\n    \"inputTokens\": 41971,\n    \"outputTokens\": 179,\n    \"costUsd\": 0.02775205,\n    \"durationMs\": 12276,\n    \"toolCalls\": 2,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Dropdown Actions\",\n    \"type\": \"control\",\n    \"inputTokens\": 42639,\n    \"outputTokens\": 347,\n    \"costUsd\": 0.0324071,\n    \"durationMs\": 12787,\n    \"toolCalls\": 4,\n    \"success\": true\n  },\n  {\n    \"testName\": \"OTP Input\",\n    \"type\": \"control\",\n    \"inputTokens\": 43219,\n    \"outputTokens\": 246,\n    \"costUsd\": 0.034022199999999996,\n    \"durationMs\": 13729,\n    \"toolCalls\": 4,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Quick Create Button\",\n    \"type\": \"control\",\n    \"inputTokens\": 82804,\n    \"outputTokens\": 387,\n    \"costUsd\": 0.041603799999999996,\n    \"durationMs\": 22528,\n    \"toolCalls\": 5,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Tabs with Badges\",\n    \"type\": \"treatment\",\n    \"inputTokens\": 13501,\n    \"outputTokens\": 10,\n    \"costUsd\": 0.00726735,\n    \"durationMs\": 5650,\n    \"toolCalls\": 0,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Dropdown Actions\",\n    \"type\": \"treatment\",\n    \"inputTokens\": 28173,\n    \"outputTokens\": 94,\n    \"costUsd\": 0.023455399999999998,\n    \"durationMs\": 7932,\n    \"toolCalls\": 1,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Tabs with Badges\",\n    \"type\": \"control\",\n    \"inputTokens\": 26921,\n    \"outputTokens\": 100,\n    \"costUsd\": 0.0185971,\n    \"durationMs\": 9125,\n    \"toolCalls\": 1,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Status Badge\",\n    \"type\": \"treatment\",\n    \"inputTokens\": 37067,\n    \"outputTokens\": 83,\n    \"costUsd\": 0.0550904,\n    \"durationMs\": 9202,\n    \"toolCalls\": 1,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Status Badge\",\n    \"type\": \"control\",\n    \"inputTokens\": 41996,\n    \"outputTokens\": 434,\n    \"costUsd\": 0.12066460000000001,\n    \"durationMs\": 77383,\n    \"toolCalls\": 39,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Keyboard Shortcut Badge\",\n    \"type\": \"treatment\",\n    \"inputTokens\": 13443,\n    \"outputTokens\": 11,\n    \"costUsd\": 0.007494849999999999,\n    \"durationMs\": 3540,\n    \"toolCalls\": 0,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Team Switcher Dropdown\",\n    \"type\": \"treatment\",\n    \"inputTokens\": 28284,\n    \"outputTokens\": 86,\n    \"costUsd\": 0.02316615,\n    \"durationMs\": 8796,\n    \"toolCalls\": 1,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Team Switcher Dropdown\",\n    \"type\": \"control\",\n    \"inputTokens\": 27178,\n    \"outputTokens\": 278,\n    \"costUsd\": 0.023315100000000002,\n    \"durationMs\": 11111,\n    \"toolCalls\": 3,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Keyboard Shortcut Badge\",\n    \"type\": \"control\",\n    \"inputTokens\": 40524,\n    \"outputTokens\": 184,\n    \"costUsd\": 0.023354549999999998,\n    \"durationMs\": 11419,\n    \"toolCalls\": 2,\n    \"success\": true\n  },\n  {\n    \"testName\": \"GitHub Link Button\",\n    \"type\": \"control\",\n    \"inputTokens\": 55331,\n    \"outputTokens\": 332,\n    \"costUsd\": 0.033861249999999996,\n    \"durationMs\": 15488,\n    \"toolCalls\": 6,\n    \"success\": true\n  },\n  {\n    \"testName\": \"GitHub Link Button\",\n    \"type\": \"treatment\",\n    \"inputTokens\": 13437,\n    \"outputTokens\": 10,\n    \"costUsd\": 0.007202349999999999,\n    \"durationMs\": 3826,\n    \"toolCalls\": 0,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Full Name Input Field\",\n    \"type\": \"treatment\",\n    \"inputTokens\": 13449,\n    \"outputTokens\": 10,\n    \"costUsd\": 0.00556935,\n    \"durationMs\": 3610,\n    \"toolCalls\": 0,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Sidebar Trigger Toggle\",\n    \"type\": \"treatment\",\n    \"inputTokens\": 13446,\n    \"outputTokens\": 10,\n    \"costUsd\": 0.0073211,\n    \"durationMs\": 4398,\n    \"toolCalls\": 0,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Full Name Input Field\",\n    \"type\": \"control\",\n    \"inputTokens\": 26910,\n    \"outputTokens\": 87,\n    \"costUsd\": 0.017492399999999998,\n    \"durationMs\": 7590,\n    \"toolCalls\": 1,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Sidebar Trigger Toggle\",\n    \"type\": \"control\",\n    \"inputTokens\": 43511,\n    \"outputTokens\": 315,\n    \"costUsd\": 0.035958649999999995,\n    \"durationMs\": 13575,\n    \"toolCalls\": 4,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Sign Up With Google Button\",\n    \"type\": \"treatment\",\n    \"inputTokens\": 13442,\n    \"outputTokens\": 10,\n    \"costUsd\": 0.0074110999999999995,\n    \"durationMs\": 3825,\n    \"toolCalls\": 0,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Field Description Text\",\n    \"type\": \"treatment\",\n    \"inputTokens\": 13457,\n    \"outputTokens\": 10,\n    \"costUsd\": 0.00715735,\n    \"durationMs\": 5500,\n    \"toolCalls\": 0,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Sign Up With Google Button\",\n    \"type\": \"control\",\n    \"inputTokens\": 41445,\n    \"outputTokens\": 165,\n    \"costUsd\": 0.0274517,\n    \"durationMs\": 12215,\n    \"toolCalls\": 2,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Revenue Card Badge\",\n    \"type\": \"control\",\n    \"inputTokens\": 42760,\n    \"outputTokens\": 351,\n    \"costUsd\": 0.0343904,\n    \"durationMs\": 12325,\n    \"toolCalls\": 6,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Field Description Text\",\n    \"type\": \"control\",\n    \"inputTokens\": 54198,\n    \"outputTokens\": 252,\n    \"costUsd\": 0.028527399999999998,\n    \"durationMs\": 14847,\n    \"toolCalls\": 3,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Projects More Button\",\n    \"type\": \"treatment\",\n    \"inputTokens\": 13460,\n    \"outputTokens\": 10,\n    \"costUsd\": 0.0071936,\n    \"durationMs\": 4092,\n    \"toolCalls\": 0,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Calendar Date Cell\",\n    \"type\": \"treatment\",\n    \"inputTokens\": 13451,\n    \"outputTokens\": 10,\n    \"costUsd\": 0.0074198499999999995,\n    \"durationMs\": 4816,\n    \"toolCalls\": 0,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Revenue Card Badge\",\n    \"type\": \"treatment\",\n    \"inputTokens\": 13446,\n    \"outputTokens\": 10,\n    \"costUsd\": 0.0055581,\n    \"durationMs\": 4091,\n    \"toolCalls\": 0,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Calendar Date Cell\",\n    \"type\": \"control\",\n    \"inputTokens\": 44039,\n    \"outputTokens\": 235,\n    \"costUsd\": 0.03742115,\n    \"durationMs\": 15216,\n    \"toolCalls\": 3,\n    \"success\": true\n  },\n  {\n    \"testName\": \"Projects More Button\",\n    \"type\": \"control\",\n    \"inputTokens\": 56754,\n    \"outputTokens\": 485,\n    \"costUsd\": 0.03948595,\n    \"durationMs\": 20178,\n    \"toolCalls\": 6,\n    \"success\": true\n  }\n]\n"
  },
  {
    "path": "packages/website/public/test-cases.json",
    "content": "[\n  {\n    \"name\": \"Grayscale Avatar\",\n    \"prompt\": \"Find the grayscale avatar in the user menu\"\n  },\n  {\n    \"name\": \"Forgot Password Link\",\n    \"prompt\": \"Find the forgot password link in the login form\"\n  },\n  {\n    \"name\": \"Time Range Toggle\",\n    \"prompt\": \"Find the time range toggle group showing Last 3 months, Last 30 days, Last 7 days\"\n  },\n  {\n    \"name\": \"Drag Handle\",\n    \"prompt\": \"Find the drag handle with grip vertical icon in the table rows\"\n  },\n  {\n    \"name\": \"Editable Target Input\",\n    \"prompt\": \"Find the inline editable target input field with transparent background in the data table\"\n  },\n  {\n    \"name\": \"OTP Input\",\n    \"prompt\": \"Find the OTP input with separator showing 6-digit verification code split into two groups\"\n  },\n  {\n    \"name\": \"Quick Create Button\",\n    \"prompt\": \"Find the Quick Create button with primary background color in the sidebar\"\n  },\n  {\n    \"name\": \"Dropdown Actions\",\n    \"prompt\": \"Find the show-on-hover dropdown menu button with three dots in the documents section\"\n  },\n  {\n    \"name\": \"Status Badge\",\n    \"prompt\": \"Find the status badge with green checkmark icon showing Done status\"\n  },\n  {\n    \"name\": \"Tabs with Badges\",\n    \"prompt\": \"Find the tab button showing Past Performance with a badge counter showing 3\"\n  },\n  {\n    \"name\": \"Team Switcher Dropdown\",\n    \"prompt\": \"Find the team switcher dropdown button with chevron icon in the sidebar\"\n  },\n  {\n    \"name\": \"Keyboard Shortcut Badge\",\n    \"prompt\": \"Find the keyboard shortcut indicator showing ⌘1 in the team dropdown menu\"\n  },\n  {\n    \"name\": \"GitHub Link Button\",\n    \"prompt\": \"Find the GitHub link button in the header toolbar\"\n  },\n  {\n    \"name\": \"Sidebar Trigger Toggle\",\n    \"prompt\": \"Find the sidebar toggle trigger button at the top of the header\"\n  },\n  {\n    \"name\": \"Full Name Input Field\",\n    \"prompt\": \"Find the full name input field with placeholder John Doe in the signup form\"\n  },\n  {\n    \"name\": \"Field Description Text\",\n    \"prompt\": \"Find the helper text saying We'll use this to contact you below the email input\"\n  },\n  {\n    \"name\": \"Sign Up With Google Button\",\n    \"prompt\": \"Find the Sign up with Google button with outline variant in the signup form\"\n  },\n  {\n    \"name\": \"Revenue Card Badge\",\n    \"prompt\": \"Find the trending up badge showing +12.5% in the Total Revenue card\"\n  },\n  {\n    \"name\": \"Calendar Date Cell\",\n    \"prompt\": \"Find the selected date cell in the calendar component showing June 12\"\n  },\n  {\n    \"name\": \"Projects More Button\",\n    \"prompt\": \"Find the More button with horizontal dots icon at the bottom of the projects list\"\n  }\n]\n"
  },
  {
    "path": "packages/website/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\",\n    \".next/dev/types/**/*.ts\",\n    \"**/*.mts\"\n  ],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/website/utils/cn.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport const cn = (...classes: ClassValue[]): string => twMerge(clsx(classes));\n"
  },
  {
    "path": "packages/website/utils/detect-mobile.ts",
    "content": "export const detectMobile = (): boolean => {\n  if (typeof window === \"undefined\") return false;\n\n  const hasTouchPoints = navigator.maxTouchPoints > 0;\n  const hasTouchMedia = window.matchMedia(\"(pointer: coarse)\").matches;\n\n  return hasTouchPoints || hasTouchMedia;\n};\n"
  },
  {
    "path": "packages/website/utils/get-key-from-code.ts",
    "content": "const SPECIAL_KEY_SYMBOLS: Record<string, string> = {\n  Backspace: \"⌫\",\n  Tab: \"⇥\",\n  Enter: \"↵\",\n  Space: \"Space\",\n  ArrowUp: \"↑\",\n  ArrowDown: \"↓\",\n  ArrowLeft: \"←\",\n  ArrowRight: \"→\",\n  Delete: \"Del\",\n  Comma: \",\",\n  Period: \".\",\n  Slash: \"/\",\n  Backslash: \"\\\\\",\n  BracketLeft: \"[\",\n  BracketRight: \"]\",\n  Semicolon: \";\",\n  Quote: \"'\",\n  Backquote: \"`\",\n  Minus: \"-\",\n  Equal: \"=\",\n};\n\nexport const getKeyFromCode = (code: string): string | null => {\n  if (code.startsWith(\"Key\")) return code.slice(3).toUpperCase();\n  if (code.startsWith(\"Digit\")) return code.slice(5);\n  return SPECIAL_KEY_SYMBOLS[code] ?? null;\n};\n"
  },
  {
    "path": "packages/website/utils/hotkey-to-string.ts",
    "content": "import type { RecordedHotkey } from \"@/components/grab-element-button\";\n\nexport const hotkeyToString = (hotkey: RecordedHotkey): string => {\n  const parts: string[] = [];\n  if (hotkey.metaKey) parts.push(\"Meta\");\n  if (hotkey.ctrlKey) parts.push(\"Ctrl\");\n  if (hotkey.shiftKey) parts.push(\"Shift\");\n  if (hotkey.altKey) parts.push(\"Alt\");\n  if (hotkey.key) {\n    const keyDisplay = hotkey.key === \" \" ? \"Space\" : hotkey.key.toLowerCase();\n    parts.push(keyDisplay);\n  }\n  return parts.join(\"+\");\n};\n"
  },
  {
    "path": "packages/website/utils/parse-changelog.ts",
    "content": "interface ChangelogEntry {\n  version: string;\n  changeType: string;\n  changes: string[];\n}\n\nexport const parseChangelog = (markdown: string): ChangelogEntry[] => {\n  const entries: ChangelogEntry[] = [];\n  const lines = markdown.split(\"\\n\");\n\n  let currentEntry: ChangelogEntry | null = null;\n\n  for (const line of lines) {\n    const versionMatch = line.match(/^## (\\d+\\.\\d+\\.\\d+(?:-[a-zA-Z0-9.]+)?)/);\n    if (versionMatch) {\n      if (currentEntry) {\n        entries.push(currentEntry);\n      }\n      currentEntry = {\n        version: versionMatch[1],\n        changeType: \"\",\n        changes: [],\n      };\n      continue;\n    }\n\n    const changeTypeMatch = line.match(/^### (.+)/);\n    if (changeTypeMatch && currentEntry) {\n      currentEntry.changeType = changeTypeMatch[1];\n      continue;\n    }\n\n    const changeMatch = line.match(/^- (.+)/);\n    if (changeMatch && currentEntry) {\n      currentEntry.changes.push(changeMatch[1]);\n    }\n  }\n\n  if (currentEntry) {\n    entries.push(currentEntry);\n  }\n\n  return entries;\n};\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - \"packages/*\"\n"
  },
  {
    "path": "turbo.json",
    "content": "{\n  \"$schema\": \"https://turbo.build/schema.json\",\n  \"concurrency\": \"20\",\n  \"tasks\": {\n    \"build\": {\n      \"dependsOn\": [\"^build\"],\n      \"outputs\": [\"dist/**\", \"r/**\"]\n    },\n    \"dev\": {\n      \"dependsOn\": [\"^build\"],\n      \"cache\": false,\n      \"persistent\": true\n    },\n    \"test\": {\n      \"dependsOn\": [\"build\"],\n      \"cache\": false\n    },\n    \"lint\": {\n      \"dependsOn\": [\"^build\"]\n    }\n  }\n}\n"
  },
  {
    "path": "vercel.json",
    "content": "{\n  \"buildCommand\": \"pnpm --filter react-grab build && pnpm --filter react-grab exec cp dist/index.global.js ../website/public/script.js && pnpm --filter @react-grab/website build\",\n  \"installCommand\": \"pnpm install\",\n  \"redirects\": [],\n  \"headers\": [\n    {\n      \"source\": \"/script.js\",\n      \"headers\": [\n        { \"key\": \"Access-Control-Allow-Origin\", \"value\": \"*\" },\n        {\n          \"key\": \"Cache-Control\",\n          \"value\": \"public, max-age=0, must-revalidate\"\n        },\n        {\n          \"key\": \"Content-Type\",\n          \"value\": \"application/javascript; charset=utf-8\"\n        }\n      ]\n    },\n    {\n      \"source\": \"/api/version\",\n      \"headers\": [\n        { \"key\": \"Access-Control-Allow-Credentials\", \"value\": \"true\" },\n        { \"key\": \"Access-Control-Allow-Origin\", \"value\": \"*\" },\n        {\n          \"key\": \"Access-Control-Allow-Methods\",\n          \"value\": \"GET,OPTIONS,PATCH,DELETE,POST,PUT\"\n        },\n        {\n          \"key\": \"Access-Control-Allow-Headers\",\n          \"value\": \"X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version\"\n        }\n      ]\n    }\n  ]\n}\n"
  }
]