> ## Documentation Index
> Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Reduced Motion Guideline

> Any new JS-driven animation MUST respect the user's prefers-reduced-motion: reduce setting. CSS animations and transitions are already neutralized globally by…

Any new JS-driven animation MUST respect the user's `prefers-reduced-motion: reduce` setting. CSS animations and transitions are already neutralized globally by the `@media (prefers-reduced-motion: reduce)` rule in `src/index.css`, but JavaScript-driven motion bypasses that rule and must opt in explicitly.

## When this applies

* Chart/graph libraries with internal `requestAnimationFrame` interpolation (Recharts, D3, Chart.js, etc.)
* `element.scrollIntoView` / `scrollTo` / `scrollBy` calls with `behavior: 'smooth'`
* Carousels and sliders (Embla, Swiper, keen-slider)
* Web Animations API (`element.animate(...)`) and manual `requestAnimationFrame` loops
* Spring/physics libraries (Framer Motion, react-spring) — gate animation props on the hook
* Auto-scrolling tickers, marquees, parallax, and confetti effects

## How to comply

Use the existing helpers — do not re-implement the `matchMedia` check inline.

| Need                                       | Use                                                                                                               |
| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
| Read the preference in a component         | `usePrefersReducedMotion()` from `@/platform/a11y`                                                                |
| Read the preference outside React          | `prefersReducedMotion()` from `@/shared/lib/a11y/scroll`                                                          |
| `scrollIntoView` / `scrollTo` / `scrollBy` | `safeScrollIntoView` / `safeScrollTo` / `safeScrollBy` from `@/shared/lib/a11y/scroll`                            |
| Recharts series                            | Already patched globally by `installRechartsReducedMotion()` in `src/main.tsx` — no per-chart change needed       |
| Embla carousel                             | Pass `duration: reducedMotion ? 0 : <normal>` to `useEmblaCarousel`                                               |
| Framer Motion / react-spring               | Conditionally set `transition={{ duration: 0 }}` (Framer) or `immediate` (react-spring) when reduced motion is on |
| Web Animations API                         | Set `duration: 0` (or skip `.animate()` entirely) when reduced motion is on                                       |

## Rules

1. Never hardcode a non-zero animation duration without checking the preference first when the motion is JS-driven.
2. Never write `behavior: 'smooth'` directly — use the `safeScroll*` helpers. The only allowed exception is when the call site has already early-returned for reduced-motion users (see `ScrollableTabsList.tsx` for the pattern).
3. The reduced-motion path must be **functionally equivalent** — instant, but the same end state. Do not skip the action, only its animation.
4. Both branches must be reactive: if you read `usePrefersReducedMotion()`, the component will re-render when the OS toggle changes — make sure your animation re-initializes (e.g., Embla `reInit`, Framer key change) so the new duration takes effect without a reload.
5. Tests for new motion code should cover both branches (`matchMedia` mock returns `matches: true` and `matches: false`).

## Reference

* Helpers: `src/shared/lib/a11y/scroll.ts`, `src/platform/a11y/usePrefersReducedMotion.ts`
* Recharts patch: `src/shared/lib/a11y/recharts-motion.ts` (installed in `src/main.tsx`)
* Global CSS rule: `src/index.css` (`@media (prefers-reduced-motion: reduce)`)
* WCAG: [2.3.3 Animation from Interactions (AAA)](https://www.w3.org/WAI/WCAG21/Understanding/animation-from-interactions.html)
