# InView — AI Reference `@macrulez/inview-core` / `-vue` / `-nuxt` / `-react` — framework-agnostic scroll / element-visibility / viewport-position engines, with Vue 3, Nuxt, and React adapters over the same core. Version 0.2.1. This document is hand-written for AI agents and other tools that generate code against this package: every signature, default, and behavior note below is verified directly against the TypeScript source (not summarized from prose docs), and prose is kept to the minimum needed to use the API correctly. For human-readable narrative docs (why you'd reach for each piece, worked examples, migration notes), see the interactive site instead: - Full docs (EN): https://npm.vuecraft.ru/en/packages/inview/guide/overview - Full docs (RU): https://npm.vuecraft.ru/packages/inview/guide/overview - GitHub: https://github.com/macrulezru/inview - npm: https://www.npmjs.com/package/@macrulez/inview-core Links below starting with "/" are relative to https://npm.vuecraft.ru. --- ## 1. Package map — what to import from where | Package | Install | Peer deps | Provides | |---|---|---|---| | `@macrulez/inview-core` | `npm install @macrulez/inview-core` | none | Framework-agnostic engines + utilities (section 4). | | `@macrulez/inview-vue` | `npm install @macrulez/inview-vue` | `vue: ^3.3.0` | Vue composables + `v-reveal` + `` (section 5), **plus `export * from '@macrulez/inview-core'`** — the full core surface. | | `@macrulez/inview-nuxt` | `npm install @macrulez/inview-nuxt` | `nuxt: ^3.9.0 \|\| ^4.0.0` | Nuxt module: auto-imports everything `-vue` exports (composables + all of core), registers `v-reveal` globally (section 6). Depends on `-vue` internally. | | `@macrulez/inview-react` | `npm install @macrulez/inview-react` | `react: ^18.0.0 \|\| ^19.0.0` | React hooks + `` + `` (section 7), **plus `export * from '@macrulez/inview-core'`**. | **Rule: install exactly one adapter package and import everything from it — including core-level things like `createRevealController` or `easings`.** `-vue` and `-react` both re-export 100% of `-core`; installing `-core` alongside an adapter is redundant, never required. `-nuxt` doesn't need `-vue` installed separately either — it depends on it internally and auto-imports its exports. --- ## 2. Cross-framework behavior differences Read this before generating code — these are real behavioral differences, not just API-surface renaming. - **Reactivity shape.** Vue composables return `Ref`s. React hooks return plain values (a re-render is the update signal, via `useSyncExternalStore`). Core functions/engines return plain values with a `subscribe(cb)` method and no reactivity primitive of their own — you drive UI updates yourself. - **Target parameter.** Vue accepts `MaybeRefOrGetter` (ref, getter, or plain value — resolved via `toValue()`). React accepts a plain `HTMLElement | null`; to change target after mount, put it in `useState` (not a bare `useRef`, which doesn't trigger a re-render/re-subscribe). - **App-level defaults exist in both Vue and React, but as two unrelated mechanisms.** Vue's `useElementVisibility`/`v-reveal`/`` fall back to the module-level `viewportDefaults` object (overridable via `setViewportDefaults()`, which the Nuxt module calls for you from `nuxt.config.ts`). React's `useElementVisibility`/`` fall back to React context instead — ``/`useInviewDefaults()` (added in 0.2.1). **The two don't talk to each other**: calling Vue's `setViewportDefaults()` has zero effect on React, and there is no Nuxt module wiring for `` (Nuxt only wraps Vue). - **`v-reveal` is Vue-only** — a directive concept doesn't exist in React. **`` exists in both**, but with a different shape: Vue's is a renderless component with a scoped slot (5.6); React's is a component whose `children` is a render-prop function, not JSX nodes (7.4). For the page-wide "toggle a class on many elements, including ones not in the DOM yet" case in React, use `createRevealController` directly instead — there's no directive equivalent there either. - **`createRevealController` has no dedicated hook/composable in any adapter.** It's a page-wide DOM scan, not per-component state. Call it directly — Vue `onMounted`, a Nuxt client plugin, or a React `useEffect` — from whichever adapter package is already installed. - **Options are captured once per (re)subscribe, not deeply reactive, in BOTH Vue and React** — for `useScroll`, `useElementVisibility`, and their React equivalents, only the `target` (and, in Vue's `useElementVisibility`, `root`) is watched/is a dependency. Changing `threshold`/`rootMargin`/`once`/`onEnter`/`onLeave` on an existing call without changing `target` does **not** re-subscribe with the new values. To force a refresh, change `target` (e.g. toggle to `null` and back), or unmount/remount. --- ## 3. Core types (verbatim from `@macrulez/inview-core`'s `types.ts`) ```ts type ScrollDirection = 'up' | 'down' | 'left' | 'right' | null interface ScrollState { x: number y: number direction: ScrollDirection progress: number // 0..1 velocity: number // px moved since the last animation frame — NOT px/second isScrolling: boolean } interface ScrollToOptions { behavior?: 'auto' | 'smooth' // default: 'auto' if prefersReducedMotion(), else 'smooth' } interface ScrollEngineOptions { idleTimeout?: number // ms of scroll inactivity before isScrolling flips back to false. default 150 } type IntersectionEdge = 'enter-top' | 'enter-bottom' | 'leave-top' | 'leave-bottom' interface IntersectionInfo { isIntersecting: boolean intersectionRatio: number boundingClientRect: DOMRectReadOnly edge?: IntersectionEdge // only set on an actual enter/leave transition, undefined on a same-state tick } interface ObserverPoolOptions { root?: Element | Document | null rootMargin?: string threshold?: number | number[] } interface VisibilityObserveOptions extends ObserverPoolOptions { once?: boolean onEnter?: (info: IntersectionInfo) => void onLeave?: (info: IntersectionInfo) => void } interface DOMRectLike { top: number; left: number; right: number; bottom: number; width: number; height: number } interface ElementTrackerState { rect: DOMRectLike viewportProgress: number // 0..1 — see exact formula in section 4 distanceFromCenter: number // px, element center minus viewport center; can be negative } ``` --- ## 4. `@macrulez/inview-core` ### 4.1 `createScrollEngine(target?, options?)` ```ts function createScrollEngine( target: Window | HTMLElement = window, // SSR: no window → no-op engine, static zero state options?: ScrollEngineOptions // { idleTimeout?: number = 150 } ): ScrollEngine interface ScrollEngine { getState(): ScrollState subscribe(cb: (state: ScrollState) => void): () => void // returns unsubscribe scrollTo(pos: number | { x?: number; y?: number }, opts?: ScrollToOptions): void destroy(): void } ``` - One shared `requestAnimationFrame` loop (`rafLoop`) drives `velocity` recomputation across every active engine — not one rAF callback per engine. - `velocity` is `Math.hypot(dx, dy)` between the last two animation frames — **px per frame, not px per second.** Don't treat it as a standard speed unit without accounting for the viewer's actual frame rate. - `direction` only changes on a nonzero scroll delta; on a zero-delta scroll event it keeps whatever the last computed direction was (never resets to `null` mid-session). - `isScrolling` flips back to `false` (and `velocity` resets to `0`) after `idleTimeout` ms (default 150) with no further scroll events. - `scrollTo`: `behavior` defaults to `'smooth'`, or `'auto'` if `prefersReducedMotion()` is true. - `destroy()` removes the scroll listener, clears the idle timer, and removes this engine's callback from the shared rAF loop. ### 4.2 `createVisibilityEngine(pool?)` ```ts function createVisibilityEngine(pool?: ObserverPool): VisibilityEngine // default: the shared `observerPool` singleton interface VisibilityEngine { observe(el: Element, options: VisibilityObserveOptions, cb: (info: IntersectionInfo) => void): () => void // returns unobserve destroy(): void } ``` - `observe()`'s `cb` fires on every native intersection-observer tick (both entering and leaving, and re-fires for ratio changes if the underlying `threshold` is an array with multiple steps). `onEnter`/ `onLeave` fire only on an actual `edge` transition (see `IntersectionInfo` above) — not on every tick. - `once: true` auto-unobserves right after the first tick where `isIntersecting` is `true`. - Passing no `pool` arg defaults to the module-level `observerPool` singleton — **two separate `createVisibilityEngine()` calls with no explicit pool still share the same underlying native `IntersectionObserver` instances** for matching `(root, rootMargin, threshold)`, because pooling happens one level down, in `ObserverPool` itself (section 4.5), keyed independently of which `VisibilityEngine` wrapper is calling it. (`useElementVisibility` and `v-reveal` in the Vue package each instantiate their own `createVisibilityEngine()` — they still pool together.) ### 4.3 `createElementTracker(el)` ```ts function createElementTracker(el: HTMLElement): ElementTracker // SSR: no window → static zero state interface ElementTracker { getState(): ElementTrackerState subscribe(cb: (state: ElementTrackerState) => void): () => void destroy(): void } ``` - Polls `el.getBoundingClientRect()` on the shared rAF loop; only notifies subscribers when the computed state (`rect`, `viewportProgress`, `distanceFromCenter`) actually differs from the previous tick. - `viewportProgress` formula: `total = viewportHeight + rect.height`, `traveled = viewportHeight - rect.top`, `progress = clamp(traveled / total, 0, 1)`. Concretely: `0` when the element's top edge is exactly at the viewport's bottom edge (about to enter); `1` when the element's bottom edge is exactly at the viewport's top edge (about to leave). - `distanceFromCenter = elementCenterY - viewportCenterY` — negative when the element's center is above the viewport's center. ### 4.4 `createRevealController(options?)` ```ts function createRevealController(options?: RevealControllerOptions): RevealController // SSR: no document → no-op stub interface RevealControllerOptions { selector?: string // default '[data-reveal], .reveal' activeClass?: string | null // default 'in'; pass null to disable the class toggle activeAttribute?: string | null // default null (off) — set alongside activeClass for pure-CSS attribute selectors once?: boolean // default true — NOTE: differs from every other API in this package, which defaults `once` to false threshold?: number | number[] rootMargin?: string root?: Element | null stagger?: RevealStaggerOptions | false // default {} (STAGGER IS ON BY DEFAULT — pass `false` explicitly to disable) watchMutations?: boolean // default true watchRoot?: Element // default document.body onEnter?: (el: Element, info: IntersectionInfo) => void // NOTE: (el, info) — two args, unlike VisibilityObserveOptions.onEnter(info) onLeave?: (el: Element, info: IntersectionInfo) => void pool?: ObserverPool } interface RevealStaggerOptions extends StaggerDelayOptions { // { step?, max?, unit?, mode? } — see 4.6 cssVar?: string | null // default '--reveal-delay'. null skips writing the CSS var entirely applyInlineDelay?: boolean // default true — ALSO sets an inline `transition-delay` style, see note below } interface RevealController { refresh(): void // re-scans `selector` for elements not yet observed — for a synchronous DOM change made before watchMutations' MutationObserver callback (microtask-batched) has run destroy(): void } ``` - **Two defaults that differ from the rest of the package and are easy to miss:** `once` defaults to `true` (every other visibility-related API in this package defaults `once` to `false`), and `stagger` defaults to `{}` (**enabled**, with `staggerDelay`'s own defaults — `step: 60, max: Infinity` — writing `--reveal-delay` to every matched element) — pass `stagger: false` explicitly to opt out. - **Since 0.2.1, an enabled `stagger` writes the computed delay TWO places by default**: the `--reveal-delay` CSS var (as before) AND an inline `transition-delay` style directly on the element (`applyInlineDelay: true` by default). The inline style wins the cascade over any stylesheet rule targeting the same property on the same element (without needing `!important`) — if your own CSS sets `transition-delay` on the matched element itself, it will silently be overridden by this inline style unless you pass `applyInlineDelay: false`. The CSS var is still written (unless `cssVar: null`) because, unlike the inline style, it's inheritable — needed for a descendant selector like `.reveal > .icon { transition-delay: var(--reveal-delay) }`. - Scans `document` for `selector` on creation, then (if `watchMutations`, default true) watches `watchRoot` via `MutationObserver({ childList: true, subtree: true })` — elements matching `selector` that appear later (e.g. a `v-for` block rendered after an async fetch) are picked up automatically; removed elements are unobserved automatically. - Per-element `data-reveal-*` attributes override the controller's own options on that element only (controller option applies where the attribute is absent): - `data-reveal-once="false"` — anything other than the literal string `"false"` parses as `true`. - `data-reveal-threshold="0,0.5,1"` — comma-separated → parsed to a `number[]`; a single value parses to a plain `number`. - `data-reveal-root-margin="-10% 0px"` - `data-reveal-class="visible"` — overrides `activeClass` for this element. - `data-reveal-delay="240"` — explicit stagger delay in ms, bypasses the computed `staggerDelay()` index entirely for this element. - `data-reveal-group="hero"` — stagger index is counted within this named group instead of globally (a separate counter per group value; unset group = counted under `''`). - Built entirely on `createVisibilityEngine`/`ObserverPool` — not a second observer mechanism. ### 4.5 `observerPool` / `ObserverPool` ```ts class ObserverPool { observe(el: Element, options: ObserverPoolOptions, cb: (entry: IntersectionObserverEntry) => void): () => void stats(): ObserverPoolStat[] // added in 0.2.1 — debug-only introspection, see below } const observerPool: ObserverPool // shared default singleton interface ObserverPoolStat { key: string // the (root, rootMargin, threshold) pooling key elementCount: number // how many distinct elements sit on this native observer } ``` - `stats()` (0.2.1+) lists every currently-alive native `IntersectionObserver` with its pool key and element count — for confirming pooling is actually collapsing observers as expected, instead of silently fragmenting because a "shared" inline `threshold` array turned out to carry different values across call sites. - Pools native `IntersectionObserver` instances by key `` `${root ? 'r' : 'window'}|${rootMargin}|${threshold}` ``, where a `threshold` array is joined with `,` — **pooled by value, not by reference**: two different array instances with the same numbers share one observer. - One native `IntersectionObserver` is created per unique key; a pool entry is disposed (`observer.disconnect()`) once its last observed element unobserves. - `typeof IntersectionObserver === 'undefined'` (SSR, or an environment without the API) → `observe()` returns a no-op unobserve function immediately. - `rafLoop` (also exported) is the internal shared `requestAnimationFrame` loop used by `createScrollEngine`/`createElementTracker`. Not meant to be used directly — no public API contract beyond existing. ### 4.6 Utilities ```ts function staggerDelay(index: number, options?: StaggerDelayOptions): string interface StaggerDelayOptions { step?: number // ms, default 60 max?: number // default Infinity unit?: 'ms' | 's' // default 'ms' mode?: 'linear' | 'cycle' // default 'linear' — added in 0.2.1, see below } // mode: 'linear' (default): effectiveIndex = Math.min(max(0, index), max) — flattens past `max`, // every index beyond it shares the same delay. // mode: 'cycle' (0.2.1+, only differs when `max` is finite): effectiveIndex = max(0, index) % (max + 1) // — wraps back to 0 right after `max`, producing a repeating wave (0,1,2,...,max,0,1,2,...) // instead of everything past `max` sharing one delay. With max: Infinity, 'cycle' behaves // identically to 'linear'. // ms = effectiveIndex * step; returns e.g. '180ms' or '1.2s' function mapRange(value: number, from: [number, number], to: [number, number], clampResult?: boolean /* default false */): number // linear remap; clampResult=true clamps the result into `to`'s range (order-independent — handles a descending `to` range too) function bindCSSVar(el: HTMLElement, name: string, value: number | string): void // el.style.setProperty(name.startsWith('--') ? name : `--${name}`, String(value)) function prefersReducedMotion(): boolean // matchMedia('(prefers-reduced-motion: reduce)').matches; false if no window/matchMedia (SSR-safe) function clamp(value: number, min: number, max: number): number const easings: Record number> // names: linear, easeInQuad, easeOutQuad, easeInOutQuad, easeInCubic, easeOutCubic, // easeInOutCubic, easeInSine, easeOutSine, easeInOutSine ``` --- ## 5. `@macrulez/inview-vue` (own exports — also re-exports all of section 4) ### 5.1 `useScroll(target?, options?)` ```ts function useScroll( target?: MaybeRefOrGetter, // default: window (SSR: undefined) options?: ScrollEngineOptions ): { x: Ref; y: Ref; direction: Ref progress: Ref; velocity: Ref; isScrolling: Ref scrollTo: (pos: number | { x?: number; y?: number }, opts?: ScrollToOptions) => void } ``` Thin ref wrapper over `createScrollEngine`. Re-creates the engine only when `toValue(target)` changes (`watch(..., { immediate: true })`) — `options` is captured at that point, not reactive on its own (see section 2). ### 5.2 `useElementVisibility(target, options?)` ```ts function useElementVisibility( target: MaybeRefOrGetter, options?: { threshold?: number | number[] // default: viewportDefaults.threshold rootMargin?: string // default: viewportDefaults.rootMargin root?: MaybeRefOrGetter once?: boolean // default: viewportDefaults.once onEnter?: (info: IntersectionInfo) => void onLeave?: (info: IntersectionInfo) => void } ): { isVisible: Ref; ratio: Ref } ``` Uses one module-level shared `createVisibilityEngine()` instance across every call site in the app (pooled, see 4.2/4.5). Watches `[toValue(target), toValue(options.root)]` — changing `root` alone forces a re-subscribe; other options do not (section 2). ### 5.3 `useElementViewport(target)` ```ts function useElementViewport( target: MaybeRefOrGetter ): { rect: Ref; viewportProgress: Ref; distanceFromCenter: Ref } ``` Ref wrapper over `createElementTracker`. No options. ### 5.4 `useParallaxLayer(target, options)` ```ts function useParallaxLayer( target: MaybeRefOrGetter, options: { speed: number // required. 1 = moves with scroll; >1 faster; <1 slower; negative reverses direction axis?: 'x' | 'y' // default 'y' clamp?: boolean // default false — clamp offset at the 0/1 progress edges instead of extrapolating past them easing?: (t: number) => number // applied to viewportProgress before mapping to an offset range?: number // px offset amplitude at speed 1. default 100 } ): { style: ComputedRef<{ transform: string }>; progress: Ref } ``` Not a raw wrapper — own logic on top of `useElementViewport`. Exact formula: `amplitude = range * speed`; `offset = mapRange(progress, [0,1], [amplitude, -amplitude], clamp)`; `transform: translateY(${offset}px)` (or `translateX` for `axis: 'x'`). Returns `{ transform: 'none' }` unconditionally when `prefersReducedMotion()` is true (checked on every computed re-evaluation, not itself reactive to a live OS-setting change mid-session). ### 5.5 `v-reveal` (directive) ```ts const vReveal: Directive interface RevealDirectiveOptions { class?: string | null // default 'in'; pass null to disable the class toggle attribute?: string | null // default null once?: boolean // default: binding.modifiers.once || viewportDefaults.once threshold?: number | number[] // default: viewportDefaults.threshold rootMargin?: string // default: viewportDefaults.rootMargin root?: HTMLElement | null onEnter?: (info: IntersectionInfo) => void onLeave?: (info: IntersectionInfo) => void } ``` Usage: `v-reveal` (bare), `v-reveal.once` (shorthand for `{ once: true }`), or `v-reveal="{ threshold: 0.3, class: 'visible' }"`. - DOM-level (`mounted`/`updated`/`unmounted` hooks), not tied to `setup()` — works inside `v-for`, including items rendered after an async fetch, where a composable can't be called per iteration. - Own module-level `createVisibilityEngine()` instance, separate from `useElementVisibility`'s — but both still pool onto the same native observers via the shared `observerPool` (section 4.2). - `updated` hook re-binds (`unbind` + `bind`) only when the binding value actually changed — as of 0.2.1 this is a **field-level value comparison** (`class`/`attribute`/`once`/`threshold`/`rootMargin`/`root`; a `threshold` array compared by its joined contents, not reference), not raw reference equality. `onEnter`/`onLeave` identity is deliberately excluded from the comparison — changing only the closure passed as `onEnter` does NOT trigger a re-bind (the directive keeps observing with whichever `onEnter` was captured at the last actual re-bind). Passing a fresh object literal inline in the template (`v-reveal="{ threshold: 0.3 }"`) on every re-render is therefore *not* a perf problem by itself anymore — it only re-binds when a compared field's value truly changes. ### 5.6 `` (renderless component) ```ts // Props (all default to `undefined`, falling through entirely to useElementVisibility's own viewportDefaults chain — imposes no defaults of its own): { once?: boolean threshold?: number | number[] rootMargin?: string root?: HTMLElement | null as?: string // tag for the measured wrapping element. default 'div' } // Emits: 'enter' | 'leave', both with a single IntersectionInfo argument // Default slot props: { isVisible: boolean, ratio: number } ``` ```vue ``` Always renders one real wrapping element (`h(props.as, { ref: el }, slot(...))`) — an `IntersectionObserver` needs an actual element to measure; "renderless" here means no imposed styling/behavior beyond that wrapper, not zero DOM. Internally just `useElementVisibility` — same pooling, same `viewportDefaults` fallback. React has its own `` (7.4, added in 0.2.1) — same idea, different shape: a render-prop function instead of a scoped slot. ### 5.7 `viewportDefaults` / `setViewportDefaults(overrides)` ```ts interface ViewportDefaults { threshold: number | number[]; rootMargin: string; once: boolean } const viewportDefaults: ViewportDefaults // { threshold: 0, rootMargin: '0px', once: false } function setViewportDefaults(overrides: Partial): void // merges — only provided keys change ``` Read by `useElementVisibility`, `v-reveal`, and `` alike (not by `createRevealController`, which has its own independent defaults — see 4.4). `setViewportDefaults` mutates the shared object in place; call it once at app startup (the Nuxt module does this for you from `nuxt.config.ts`, see section 6). --- ## 6. `@macrulez/inview-nuxt` Not an export surface — a Nuxt module. ```ts // nuxt.config.ts export default defineNuxtConfig({ modules: ['@macrulez/inview-nuxt'], inview: { // config key: 'inview' defaultThreshold: 0, // number | number[], default 0 defaultRootMargin: '0px', // string, default '0px' defaultOnce: false, // boolean, default false — forwarded to viewportDefaults.once }, }) ``` - **Auto-imports** (via `@nuxt/kit`'s `addImports`, all sourced from `@macrulez/inview-vue`, i.e. including its full core re-export) — no explicit import needed for any of these, anywhere in the app: - Composables: `useScroll`, `useElementVisibility`, `useElementViewport`, `useParallaxLayer` - Utilities: `mapRange`, `bindCSSVar`, `prefersReducedMotion`, `easings`, `clamp`, `staggerDelay` - Core engines: `createScrollEngine`, `createVisibilityEngine`, `createElementTracker`, `createRevealController`, `observerPool`, `ObserverPool`, `rafLoop` - **`v-reveal` is registered globally** by a client-only plugin (`nuxtApp.vueApp.directive('reveal', vReveal)`) — usable in any template with no import. - **NOT auto-imported/registered — needs a manual import despite the module being installed:** `` (components aren't handled by this module's `addImports` list at all — `import { InView } from '@macrulez/inview-vue'` explicitly), and `setViewportDefaults`/ `viewportDefaults` (the module's own plugin already calls `setViewportDefaults` once for you from `nuxt.config.ts`; import it yourself only if you need to change defaults again at runtime, or read the current values). - The client plugin (`plugin.client.ts`, client-only via Nuxt's filename convention) reads `useRuntimeConfig().public.inview` and calls `setViewportDefaults({ threshold: defaultThreshold, rootMargin: defaultRootMargin, once: defaultOnce })`. The composables are already SSR-safe no-ops on their own — this plugin's client-only-ness is purely about *when* the defaults get seeded, not about guarding usage. No `` wrapper is needed anywhere because of this module. --- ## 7. `@macrulez/inview-react` (own exports — also re-exports all of section 4) The four hooks mirror the Vue composables' shape and behavior, with two structural differences applied uniformly: plain values instead of `Ref`s, and a plain `HTMLElement | null` instead of `MaybeRefOrGetter` for `target`. Each hook is built on `useSyncExternalStore` with a real `getServerSnapshot` (a static zero/empty value) for SSR. As of 0.2.1 the package also has its own `` component and an ``/ `useInviewDefaults()` context pair (7.3–7.4) — the React equivalents of Vue's `` and `viewportDefaults`/`setViewportDefaults()`. ### 7.1 `useScroll(target?, options?)` ```ts function useScroll(target?: Window | HTMLElement | null, options?: ScrollEngineOptions): { x: number; y: number; direction: ScrollDirection; progress: number velocity: number; isScrolling: boolean scrollTo: (pos: number | { x?: number; y?: number }, opts?: ScrollToOptions) => void } ``` Engine is re-created only when the `target` reference changes (an internal `useRef` holder compares by `!==`). `options` is read fresh each time the engine is (re-)created, not on every render (section 2). ### 7.2 `useElementVisibility(target, options?)` ```ts function useElementVisibility(target: HTMLElement | null, options?: { threshold?: number | number[] // default: useInviewDefaults().threshold (see 7.3) rootMargin?: string // default: useInviewDefaults().rootMargin root?: HTMLElement | null once?: boolean // default: useInviewDefaults().once onEnter?: (info: IntersectionInfo) => void onLeave?: (info: IntersectionInfo) => void }): { isVisible: boolean; ratio: number } ``` Module-level shared `createVisibilityEngine()` instance across every call site in the app (same pooling behavior as Vue's, separately from it — the React and Vue packages each have their own singleton, but both still pool through the shared native-observer layer if used in the same page). `subscribe` is memoized on `[target]` only — changing other options without changing `target` does not re-subscribe (section 2). As of 0.2.1, `threshold`/`rootMargin`/`once` fall back to `useInviewDefaults()` (7.3) — the React context equivalent of Vue's `viewportDefaults`; before 0.2.1 there was no fallback mechanism at all in React (fixed defaults of `0`/`'0px'`/falsy from `ObserverPool` alone). ### 7.3 `useInviewDefaults()` / `` ```ts interface ViewportDefaults { threshold: number | number[]; rootMargin: string; once: boolean } // default when there's no provider in the tree: { threshold: 0, rootMargin: '0px', once: false } function useInviewDefaults(): ViewportDefaults // returns the nearest 's merged defaults, or the package-wide default above interface InviewProviderProps { defaults?: Partial // merged on top of the package-wide default children: ReactNode } function InviewProvider(props: InviewProviderProps): JSX.Element ``` Added in 0.2.1. Read by `useElementVisibility` (7.2) and `` (7.4) alike — set once at the app root instead of repeating `threshold`/`rootMargin`/`once` at every call site. An option passed directly to `useElementVisibility`/`` always wins over the provider's value (same override order as Vue's `viewportDefaults`). `InviewProvider`/`useInviewDefaults` are entirely separate from Vue's `viewportDefaults`/`setViewportDefaults()` — no shared state, no Nuxt wiring. ```tsx import { InviewProvider } from '@macrulez/inview-react' function App() { return ( ) } ``` ### 7.4 `` ```ts interface InViewProps { once?: boolean // default: useInviewDefaults().once threshold?: number | number[] // default: useInviewDefaults().threshold rootMargin?: string // default: useInviewDefaults().rootMargin root?: HTMLElement | null as?: keyof JSX.IntrinsicElements // default 'div' — tag for the measured wrapper onEnter?: (info: IntersectionInfo) => void onLeave?: (info: IntersectionInfo) => void children: (state: { isVisible: boolean; ratio: number }) => ReactNode // REQUIRED, a render-prop function } function InView(props: InViewProps): JSX.Element ``` Added in 0.2.1. Render-prop wrapper over `useElementVisibility`; always renders one real wrapping element (`as`, default `'div'`) via `createElement(as, { ref: setNode }, children(...))` — an `IntersectionObserver` needs an actual element to measure. **`children` is a function, not JSX nodes** — `{someElement}` is wrong; it must be `{(state) => someElement}`. Not the same shape as Vue's `` (5.6), which uses a scoped slot. ```tsx import { InView } from '@macrulez/inview-react' {({ isVisible }) => (isVisible ? : null)} ``` ### 7.5 `useElementViewport(target)` ```ts function useElementViewport(target: HTMLElement | null): ElementTrackerState // { rect: DOMRectLike; viewportProgress: number; distanceFromCenter: number } ``` No options. ### 7.6 `useParallaxLayer(target, options)` ```ts function useParallaxLayer(target: HTMLElement | null, options: { speed: number; axis?: 'x' | 'y'; clamp?: boolean easing?: (t: number) => number; range?: number }): { style: { transform: string }; progress: number } ``` Same formula as Vue's (section 5.4). `style` is `useMemo`'d on `[progress, axis, range, speed, clamp]`. ### 7.7 No `v-reveal` equivalent There is no directive concept in React (unlike ``, which does have a React equivalent as of 0.2.1 — see 7.4). For the page-wide "toggle a class on many elements, including ones added later" case, call `createRevealController` (re-exported from `-core`, section 4.4) directly in a `useEffect`: ```tsx import { useEffect } from 'react' import { createRevealController } from '@macrulez/inview-react' useEffect(() => { const controller = createRevealController({ stagger: { step: 70, max: 4 } }) return () => controller.destroy() }, []) ``` --- ## 8. Consolidated gotcha list Cross-cutting facts most likely to produce subtly wrong generated code if missed — each is explained in full where it first applies above, listed here for a fast pre-flight check: 1. `createScrollEngine`'s `velocity` is px-per-animation-frame, not px-per-second (4.1). 2. `useScroll`/`useElementVisibility` (both frameworks) capture non-target options once per (re)subscribe — changing them reactively without changing `target`/`root` has no effect (section 2, 5.1, 5.2, 7.1, 7.2). 3. `createRevealController` defaults `once: true` and `stagger: {}` (enabled) — both are non-default-in-spirit compared to the rest of the package; pass `once: false` / `stagger: false` explicitly to opt out (4.4). 4. `createRevealController`'s `onEnter`/`onLeave` take `(el, info)` — two args — while every other visibility API's `onEnter`/`onLeave` take `(info)` alone (4.4 vs 4.2/5.2/5.5/5.6/7.2). 5. React's `useElementVisibility`/`` never read Vue's `viewportDefaults` — React has its own, separate context-based mechanism since 0.2.1 (``/`useInviewDefaults()`, 7.3), with no bridge between the two (section 2, 7.2, 7.3). 6. The Nuxt module does not auto-import/register Vue's `` or `setViewportDefaults`/`viewportDefaults` — only composables, utilities, core engines, and the `v-reveal` directive (section 6). It also has no involvement with React's `` at all (Nuxt only wraps Vue). 7. `ObserverPool` pools `threshold` arrays by value (joined into a string key), not by reference — two different array instances with identical numbers share one native observer (4.5). 8. `v-reveal`'s `updated` hook (0.2.1+) re-binds on a field-level value comparison, not reference equality — a fresh inline options object on every re-render no longer forces a re-subscribe by itself, but changing only the `onEnter`/`onLeave` closure identity is deliberately ignored and will NOT re-bind either (5.5). 9. `createRevealController`'s `stagger` (0.2.1+) sets an inline `transition-delay` style by default, in addition to the `--reveal-delay` CSS var — the inline style silently wins over any same-element CSS `transition-delay` rule you declare. Pass `applyInlineDelay: false` if you only want the CSS var (4.4). 10. React's `` (0.2.1+) takes `children` as a function (`(state) => ReactNode`), not JSX nodes — `{someNode}` is a type error, not a smaller variant of the API (7.4). 11. `staggerDelay`'s `mode: 'cycle'` (0.2.1+) only differs from the default `'linear'` mode when `max` is finite — with `max: Infinity` (the default), `'cycle'` and `'linear'` compute identically (4.6).