# AGENTS.md — @page-speed/media-immersive This file is written for AI coding agents (Claude Code, Codex, Cursor, Copilot, Perplexity Computer) working in this repo. It describes the architecture, the invariants that must be preserved, and the non-obvious decisions embedded in the code. ## Mental model `@page-speed/media-immersive` is a **TikTok/Reels/Shorts-style vertical video feed** with a portal-mounted fullscreen viewer. It composes `@page-speed/img` (posters/thumbnails) and `@page-speed/video` (HLS + mp4 fallback playback) — this library does **not** own video playback mechanics. The three consumer patterns are: 1. **Carousel row + fullscreen viewer** (default ``). 2. **Controlled** — viewer portal only, triggered imperatively via `ref`. 3. **Fully custom** — bring your own layout with `` + primitives. State (`items`, `activeIndex`, `isOpen`, `isMuted`) lives in `ImmersiveFeedProvider`. All primitives read it via `useImmersiveFeed()`. ## Golden rules (do not violate) 1. **Do not add heavy runtime dependencies.** No `framer-motion`, no `react-spring`, no `@radix-ui/*`, no gesture library. The library must stay small (target: <15KB gzipped for the full feed). The pointer-events pager and CSS transforms in `useVerticalPagerGestures` are hand-rolled specifically to avoid these. 2. **Do not couple to a specific consumer.** The library ships **zero default actions** for the right rail and no baked-in brand chrome. The rail is empty until the consumer passes `actions={[...]}`. This is required for JV partners (restaurants, charter schools, etc.) who need very different action sets. 3. **Do not replace the transform-based pager with CSS `scroll-snap`.** This was deliberately not chosen. See § "Why not scroll-snap" below. 4. **Do not import from `@page-speed/lightbox` at runtime.** Match its hook signatures (`useKeyboardShortcuts`, gallery-state shape) for developer consistency, but keep this library dependency-free from lightbox at runtime. 5. **Every layout-critical CSS property must be applied inline**, not only via the injected stylesheet. The stylesheet is polish; failing to load it must not break positioning. 6. **All internal imports must use `.js` extensions** (not `.ts` or extensionless). Required by `moduleResolution: "NodeNext"`. Test files are the exception (vitest resolves `.ts` fine). 7. **Every source file that touches the DOM starts with `"use client"`.** Required for RSC integration in Toastability/app and consumer Next.js apps. 8. **`sideEffects: false` and per-subpath single-file exports are non-negotiable.** Do not add barrels or side-effectful modules that break tree-shaking. Every `./x` export in package.json must map to one compiled file. ## Repository layout ``` page-speed-media-immersive/ ├── src/ │ ├── index.ts # public re-exports (root) │ ├── core/ │ │ ├── index.ts # core barrel │ │ ├── ImmersiveFeed.tsx # convenience wrapper │ │ ├── ImmersiveFeedProvider.tsx # Context + state + useImmersiveFeed() │ │ ├── ImmersiveViewer.tsx # Fullscreen viewer (largest file) │ │ ├── ImmersiveViewerHeader.tsx # Top chrome (close, counter, mute) │ │ ├── ImmersiveViewerActions.tsx # Right-side action rail │ │ └── ImmersiveViewerCaption.tsx # Bottom caption card │ ├── thumbnails/ │ │ ├── index.ts │ │ ├── ThumbnailCard.tsx # Single tappable poster tile │ │ └── ThumbnailStrip.tsx # Horizontal scroll-snap row │ ├── hooks/ │ │ ├── index.ts │ │ ├── useVerticalPagerGestures.ts # ← Critical file. See § "The pager" │ │ ├── useScrollLock.ts # iOS-safe body scroll lock │ │ ├── useKeyboardShortcuts.ts # Mirrors @page-speed/lightbox signature │ │ └── useResponsiveness.ts # Breakpoint hook (same shape as lightbox) │ ├── portal/ │ │ ├── index.ts │ │ ├── ImmersivePortal.tsx # createPortal + scoped-reset root │ │ └── injectScopedStylesheet.ts # One-shot