{"owner":"pmndrs","repo":"react-spring","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["CLAUDE.md"],"files":{"CLAUDE.md":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Stack\n\n- **Package manager**: pnpm 9.15.9 with strict isolated `node_modules` (default). Pinned via `packageManager` in root `package.json` and activated through Corepack. Scripts assume `pnpm`.\n- **Node**: `.nvmrc` is `24.16.0`; `engines.node` in root `package.json` is `>=24.16.0`; CI runs the unit-test matrix on Node `22.x` and `24.x` and every other job off `.nvmrc`.\n- **Monorepo**: Turborepo + pnpm workspaces. Workspaces declared in `pnpm-workspace.yaml`: `packages/*`, `targets/*`, `demo`, `docs`.\n- **Bundler**: `tsdown` per package, sharing `tsdown.config.base.mjs`. The library is **ESM-only** — each package emits a single modern ESM bundle (`dist/<name>.modern.mjs`) plus its `.d.mts` types. No CJS, no legacy/pre-compiled variants.\n- **Tests**: Vitest in browser mode (Chromium via Playwright) for both unit and E2E projects, `vitest-browser-react` for rendering, `tsc --noEmit` (types). Root config: `vitest.config.ts`.\n- **Lint/format**: [oxlint](https://oxc.rs/docs/guide/usage/linter) (root `.oxlintrc.json`) + [oxfmt](https://oxc.rs/docs/guide/usage/formatter) (root `.oxfmtrc.json`). Husky `pre-commit` runs `oxfmt --check`; `commit-msg` runs commitlint with `@commitlint/config-conventional`. VS Code users should install the [oxc extension](https://marketplace.visualstudio.com/items?itemName=oxc.oxc-vscode) — see `.vscode/extensions.json`.\n\n## Common commands\n\n| Task                                 | Command                                                                                       |\n| ------------------------------------ | --------------------------------------------------------------------------------------------- |\n| Install                              | `pnpm install --frozen-lockfile`                                                              |\n| First-time browser install           | `pnpm exec playwright install chromium`                                                       |\n| Build all packages                   | `pnpm build` (turbo, respects `^build` deps)                                                  |\n| Build everything except docs         | `pnpm build-ci`                                                                               |\n| Watch-build all packages in parallel | `pnpm dev`                                                                                    |\n| Run docs / demo dev servers          | `pnpm docs:dev` / `pnpm demo:dev`                                                             |\n| Full test suite                      | `pnpm test` (ts + unit + e2e)                                                                 |\n| Unit tests                           | `pnpm test:unit`                                                                              |\n| Watch unit tests                     | `pnpm vitest --project unit`                                                                  |\n| Single test file                     | `pnpm vitest run packages/core/src/SpringValue.test.ts`                                       |\n| Filter by test name                  | `pnpm vitest run -t \"interpolation\"`                                                          |\n| Headed (visible browser) debugging   | `pnpm vitest --project unit --browser.headless=false <file>`                                  |\n| Coverage                             | `pnpm test:cov` (thresholds: 80% statements / 74% branches / 71% functions / 82% lines)       |\n| Type-check                           | `pnpm test:ts`                                                                                |\n| Parallax E2E                         | `pnpm test:e2e` (programmatically serves `packages/parallax/test`, runs Vitest browser specs) |\n| Lint                                 | `pnpm lint` (turbo across packages)                                                           |\n| Format                               | `pnpm format` / `pnpm format:check`                                                           |\n\nNote: `vitest.config.ts` aliases `@react-spring/*` to the package source under `packages/*/src/index.ts`, so unit tests run **without** a prior build. Anything outside Vitest (docs, publish-ci) needs `pnpm build` first.\n\nStrict isolation: `node_modules` is non-hoisted, so a workspace can only `import` packages it declares in its own `package.json`. If you see a `Cannot find module 'foo'` error after adding an import, add `foo` to that workspace's `dependencies` / `peerDependencies` / `devDependencies` — do not add a hoist rule.\n\n## Architecture\n\nThe library is a layered monorepo. Read packages bottom-up — each layer is target-agnostic until you reach `targets/*`. There is no umbrella package: consumers install a target directly (`@react-spring/web` or `@react-spring/three`).\n\n```\n          targets/web     targets/three\n              │                 │\n              └────────┬────────┘\n                       │\n              packages/core    ─── declarative API: hooks, components, SpringValue, Controller, SpringRef\n                       │\n              packages/animated ── Animated{Value,String,Array,Object}, createHost, withAnimated\n                       │\n              packages/shared  ─── FrameLoop, interpolation, colours, fluid observers, Globals, internal hooks\n                       │\n              packages/rafz    ─── single global rAF scheduler (queues + setTimeout)\n                       │\n              packages/types   ─── pure TS types, no runtime\n```\n\n### Layer responsibilities\n\n- **`@react-spring/rafz`** — one `requestAnimationFrame` loop with five queues (`onStart`, `update`, `onFrame`, `write`, `onFinish`) plus rAF-driven `setTimeout`. `frameLoop` mode is `'always'` by default; `targets/three` flips it to `'demand'` and drives ticks via `addEffect` from `@react-three/fiber`. `__raf` is the test-only state reset (`__raf.clear()`).\n- **`@react-spring/shared`** — owns `Globals` (the runtime config bag) and `FrameLoop`. Targets call `Globals.assign({ batchedUpdates, createStringInterpolator, colors, ... })` at module load to plug in platform-specific behaviour. Also exports the small internal hooks (`useConstant`, `useForceUpdate`, `useIsomorphicLayoutEffect`, etc.) that core builds on.\n- **`@react-spring/animated`** — the `Animated` class hierarchy that backs every animatable prop, plus `createHost(primitives, hostConfig)`. The `hostConfig` has three seams every target implements:\n  - `applyAnimatedValues(node, props)` — push the latest values to the platform's native node.\n  - `createAnimatedStyle(style)` — wrap the `style` prop.\n  - `getComponentProps(props)` — filter props before forwarding (e.g. web drops `scrollTop`/`scrollLeft`).\n- **`@react-spring/core`** — platform-agnostic spring engine. `SpringValue` (single animated value), `Controller` (group of springs), `SpringRef` (imperative handle), `Interpolation`. Public hooks/components live under `src/hooks` and `src/components`.\n- **Targets** — thin adapters. Each `targets/<name>/src/index.ts` follows the same template: `Globals.assign(...)`, define `primitives`, build a host via `createHost(primitives, { applyAnimatedValues, ... })`, then `export const animated = host.animated` and `export * from '@react-spring/core'`. To add an `animated.X` shorthand for a new element, add it to that target's `primitives.ts`.\n- **`@react-spring/parallax`** — extra component layered on `@react-spring/web`. Its `test/` folder is the Vite app the E2E project serves and drives (`tests/e2e/parallax.spec.ts`).\n\n### Where animation values flow\n\n1. A hook (`useSpring`) creates a `Controller` of `SpringValue`s in `@react-spring/core`.\n2. `SpringValue` registers as a `FluidValue` (observable from `shared/fluids`).\n3. `FrameLoop` from `shared` schedules ticks on `rafz`'s `update` queue.\n4. Each tick computes a new value, emits a `change` event, and the `Animated` tree subscribed via `withAnimated` reads it.\n5. `withAnimated` calls the target's `applyAnimatedValues` (or React's reconciler for prop changes) to write to the host node.\n\n## Testing model\n\n`packages/core/test/setup.ts` is referenced from `vitest.config.ts`'s `unit` project `setupFiles` and is the single most important file for writing animation tests:\n\n- Replaces `rafz`'s native rAF with a `mock-raf` instance every `beforeEach`, then clears `frameLoop` and `__raf`.\n- Adds globals you should use **instead of** `vi.runAllTimers()` / `vi.advanceTimersByTime`:\n  - `advance(n?)` — step `n` frames.\n  - `advanceByTime(ms)` — step until a `setTimeout(ms)` fires.\n  - `advanceUntil(predicate)` — step until `predicate()` is true (cap 1000 frames; throws on infinite loop).\n  - `advanceUntilIdle()` — step until both `frameLoop` and `rafz` queues are empty.\n  - `advanceUntilValue(spring, value)` — step until `spring` reaches/passes `value`.\n  - `getFrames(target)` / `countBounces(spring)` — inspect recorded frame history.\n  - `setSkipAnimation(bool)` — toggle `Globals.skipAnimation`.\n\nRendering uses `vitest-browser-react` (`import { render } from 'vitest-browser-react'`). Hook tests use the in-repo `renderHook` helper at `tests/helpers/renderHook.tsx` (imported as `@tests/helpers/renderHook`) since `vitest-browser-react` does not ship `renderHook`. `act` comes from `react` (React 19 native).\n\nVitest fake timers are enabled globally and fake all non-rAF time sources (`setTimeout`, `setInterval`, `Date`, `queueMicrotask`, `performance`). rAF is owned by `@react-spring/mock-raf` — never fake `requestAnimationFrame`.\n\nThe E2E project (`pnpm test:e2e`) lives at `tests/e2e/`. `global-setup.ts` boots the parallax fixture via Vite's programmatic API on an ephemeral port and exposes the URL through Vitest's `provide`/`inject` channel (`inject('baseUrl')`).\n\n## Releases (changesets)\n\nAll packages are version-locked. Workflow:\n\n```sh\npnpm changeset    # add a changeset describing the change\npnpm vers         # bumps versions + internal deps\npnpm release      # clean install, build, type-check, unit test, then changeset publish\n```\n\nFor prereleases enter pre-mode first: `pnpm changeset pre enter beta|alpha|next`.\n\n## Conventions\n\n- Commits and PR titles follow Conventional Commits (`feat:`, `fix:`, `chore:`, …). commitlint enforces this on `commit-msg`.\n- Prettier config: no semis, single quotes, 2-space tabs, ES5 trailing commas, `arrowParens: 'avoid'`, 80-col print width.\n- ESLint: `no-console` is `error` except `warn`/`error`; unused vars must be prefixed `_`; `@typescript-eslint/no-explicit-any` is **off** intentionally (the spring engine leans on `any` for variance).\n- The default branch is `next` (also treated as the PR base). `main` may exist but `next` is the active line.\n\n<!-- SPECKIT START -->\n\nFor additional context about technologies to be used, project structure,\nshell commands, and other important information, read the current plan:\n[specs/003-remix-to-react-router-7/plan.md](./specs/003-remix-to-react-router-7/plan.md)\n\n<!-- SPECKIT END -->\n"}}