import { RefObject } from 'react'; /** * MagneticElement — attraction with anticipation. * * The thousand free magnetic-button tutorials react to hover. This one * rides the engine: engagement = max(PointerIntent confidence, proximity * falloff), so the element starts reaching WHILE THE CURSOR IS STILL ON * ITS WAY — the pre-touch that makes award-site buttons feel alive. * * Anchor correctness: the element's rect moves as it translates, so the * true anchor is rect-center MINUS the current offset. Without this the * target drifts and the element chases its own tail. * * Writes only `transform` (compositor-safe); records and restores the * previous inline transform on destroy. * * ## Read/write split (v0.3) * * v0.1 called `getBoundingClientRect()` inside the render lane, immediately * before writing `transform`. With two magnetic elements on a page that * becomes read → write → read, and the second read is a forced synchronous * layout because the first write dirtied the tree — layout thrash scaling * with the number of magnetic elements, in the engine whose whole premise is * reads-before-writes. * * The measurement now lives in the input lane, so every magnetic element on * the page has finished reading before any of them writes. The anchor is also * cached and only re-measured when something could plausibly have moved it * (scroll, resize) or the heartbeat elapses, so the steady state costs no * layout reads at all. */ interface MagneticOptions { /** Max translation in px at full engagement. Default 12. */ strength?: number; /** Distance (px from center) where proximity pull begins. Default 90. */ reach?: number; /** How quickly the element follows the pointer. Higher is snappier. Default 12. */ speed?: number; /** Scale at full engagement (1 = off). Default 1.04. */ scale?: number; /** Start reaching while the pointer is still approaching. Default true. */ anticipate?: boolean; } /** * Magnetic attraction with anticipation — the element starts reaching * while the cursor is still approaching (PointerIntent confidence drives * pre-touch pull; proximity takes over up close). * * const { ref } = useMagneticIntent(); * * * Owns the element's inline `transform` while mounted (restored exactly on * unmount). Inactive on touch devices and under reduced motion — this one * IS motion, so it fails open to a perfectly normal element. */ declare function useMagneticIntent(options?: MagneticOptions): { ref: RefObject; active: boolean; }; /** * PointerIntent — predicts that the pointer is COMING to an element before * it arrives, from the SensorBus's smoothed velocity: cast the pointer's * trajectory forward and measure time-to-impact against the (inflated) * element rect. Confidence rises as impact nears; being inside is * confidence 1. Hysteresis (enter/exit thresholds) keeps the boolean calm. * * Use it to pre-warm expensive hovers: start the video, compile the shader, * begin the magnetic pull — 100–300ms before the cursor lands. */ /** * How eager the prediction is. * * This replaced five separate numbers — look-ahead horizon, rect inflation, a * minimum pointer speed, and a pair of confidence thresholds for gaining and * losing intent. The last two are hysteresis: correct to have, impossible to * pick by hand, and meaningless in isolation. */ type PointerIntentSensitivity = "low" | "normal" | "high"; interface PointerIntentOptions { /** * How readily the pointer is judged to be heading here. Default `"normal"`. * * - `"low"` — only a clear, committed approach counts * - `"normal"` — a good default for a link or a card * - `"high"` — fires early and more often, for something cheap to prepare */ sensitivity?: PointerIntentSensitivity; /** * Re-measure the element every frame instead of caching its position. Needed * only when the element itself moves — a carousel, something being animated. * Default false. */ dynamic?: boolean; } /** * What one sensitivity setting actually resolves to. * * These five numbers were the public options before 2.0. They are tuning, not * a control surface — "how far ahead do I look, in seconds" is not a question * a developer can answer without reading the implementation, which is why they * collapsed into three named presets. * * They are still readable, because something has to be able to show them: a * devtools panel explaining why a preset fired, or a demo drawing the * prediction geometry. The alternative is every such tool keeping its own copy * of the table and drifting from the one the runtime uses. */ interface PointerIntentTuning { /** How far ahead the pointer's path is projected, in seconds. */ readonly horizon: number; /** How far outside the element still counts as a hit, in px. */ readonly extend: number; /** Below this speed the pointer is drifting, not heading somewhere, in px/s. */ readonly minSpeed: number; /** Confidence needed to gain intent. */ readonly enter: number; /** Confidence it must fall below to lose it. Lower than `enter` — hysteresis. */ readonly exit: number; } /** * The tuning each sensitivity resolves to. Frozen: this is the runtime's own * table, not a template to copy and edit. */ declare const POINTER_INTENT_SENSITIVITY: Readonly>; interface UsePointerIntentOptions extends PointerIntentOptions { /** * Callback fired when the pointer's intent state changes. */ onIntentChange?: (intent: boolean) => void; } interface UsePointerIntentReturn { /** * Ref to attach to the target DOM element. */ ref: RefObject; /** * Boolean state that flips to true when the cursor trajectory is headed towards the element * at a sufficient speed, and false when it drifts away or slows down. Useful for triggering * React conditional pre-rendering or warming. */ intent: boolean; /** * Ref containing the smoothed time-to-impact confidence value (0 to 1). * Updated frame-accurately in the background WITHOUT triggering component re-renders. * Perfect for driving GPU-accelerated styling (e.g. magnetic pull, CSS variables, canvas distortion). */ confidenceRef: RefObject; } /** * A React hook that predicts whether the pointer is going to hover over an element *before* it arrives, * by casting the pointer's velocity vector forward and calculating time-to-impact against the element's bounding rect. * * Use this hook to hide the latency of expensive interactions (e.g., preloading 3D models, pre-compiling shaders, * fetching API data, warming up magnetic fields) 100ms - 300ms before the user actually hovers. * * ### 📚 Usage Example: * ```tsx * import { usePointerIntent } from "@vectorvesper/motion/react"; * * export function InteractiveCard() { * const { ref, intent, confidenceRef } = usePointerIntent({ * horizon: 0.4, // Cast velocity vector 400ms forward * extend: 15, // Inflate the target rect by 15px * minSpeed: 100, // Only predict if speed is > 100px/s * }); * * return ( *
* {intent && } * Confidence: {confidenceRef.current} *
* ); * } * ``` * * @param {UsePointerIntentOptions} [options={}] Configuration options specifying predictive horizons, thresholds, and callbacks. * @returns {UsePointerIntentReturn} Object containing the hybrid ref, the reactive intent state, and the non-reactive confidence ref. */ declare function usePointerIntent(options?: UsePointerIntentOptions): UsePointerIntentReturn; /** * useImageTrail — images spawn along the pointer's path and fade away: * the classic award-site gallery flourish. What the free tutorials leak, * this pools: a fixed set of nodes created once and recycled, each * flight animated with the Web Animations API — no rAF loop, no GC churn, * nothing allocated per move. * * const { ref } = useImageTrail({ images: ["/a.jpg", "/b.jpg", …] }); *
* * Fails open (does nothing) on touch devices and under reduced motion. * * A *creative* hook, not a foundation one: it is deliberately self-contained * and does NOT join the shared conductor or SensorBus. Each flight is handed to * the Web Animations API, which runs on the compositor — cheaper than a * per-frame conductor subscription would be for this effect. It needs nothing * else in the package to work. */ interface UseImageTrailOptions { /** Image sources, cycled in order. */ images: string[]; /** Rendered width of each trail image, px. Default 160. */ size?: number; /** Pointer distance between spawns, px. Default 90. */ spacing?: number; /** How long each image stays on screen, in ms. Default 900. */ duration?: number; /** Pool size = max simultaneously visible images. Default 10. */ maxActive?: number; } declare function useImageTrail(options: UseImageTrailOptions): { ref: RefObject; }; interface UseNumberTickerOptions { /** * How quickly the number reaches its target. Higher arrives sooner. * Default 6, which reads as a smooth deceleration. * * Not a duration: the value chases its target rather than running a fixed * timeline, which is what lets it retarget mid-flight without restarting. */ speed?: number; /** * Intl.NumberFormat options to style currencies, percentages, decimals, etc. */ format?: Intl.NumberFormatOptions; /** * Locale string for number formatting (e.g., "en-US", "de-DE"). * Defaults to the user's browser locale. */ locale?: string; /** * Prefix string to prepend directly to the formatted number output (e.g., "+", "> "). */ prefix?: string; /** * Suffix string to append directly to the formatted number output (e.g., "%", " ms", " units"). */ suffix?: string; } /** * A high-performance numbers ticker hook that animates count values smoothly toward a target target value. * Bypasses React state updates entirely, writing frame updates directly to the DOM node's `textContent` * to achieve 120 FPS performance with zero component re-renders. * * Automatically respects the `prefers-reduced-motion` media query, snaps instantly to the target, * and enforces tabular layout to prevent horizontal digit wobbling. * * ### 📚 Usage Example: * ```tsx * import { useNumberTicker } from "@vectorvesper/motion/react"; * * export function ScoreDisplay() { * const { ref } = useNumberTicker(8500, { * prefix: "+", * suffix: " pts", * format: { style: "decimal" } * }); * * return ; * } * ``` * * @param {number} value The target number value to count towards. * @param {UseNumberTickerOptions} [options={}] Formatting configuration, damping speeds, prefixes, and suffixes. * @returns {{ ref: RefObject }} A React ref to bind to the displaying DOM element. */ declare function useNumberTicker(value: number, options?: UseNumberTickerOptions): { ref: RefObject; }; /** * VideoScrubber — maps scroll, pointer, or manual progress onto a video's * timeline. Framework-agnostic core; React (and future) adapters are thin * lifecycle bridges. * * What it handles that naive `currentTime = x` does not: * - Seek discipline: never issues a new seek while the previous one is * in-flight (Safari queues them and stutters); sub-frame deltas are * skipped; large jumps use `fastSeek` where available (keyframe-fast). * - iOS buffering: videos are primed with a muted play()/pause() round trip * so Safari actually loads data before the first scrub. * - Attribute etiquette: `muted`/`playsInline`/`preload` are set for * scrubbing but recorded first and restored exactly on destroy(). * - Smoothing: frame-rate-independent damping on the shared conductor — * no private rAF. Under prefers-reduced-motion the scrub still works * (it is direct manipulation, not autonomous motion) but tracks * instantly, with no trailing lag. */ type ScrubDriver = "scroll" | "pointer" | "manual"; /** * How scroll position maps to progress when `driver: "scroll"`: * - "pin": for tall tracks with sticky content — 0 when the track top * docks at the viewport top, 1 when its bottom reaches the * viewport bottom (the Apple scrollytelling pattern). * - "cross": 0 as the track top enters at the viewport bottom, 1 as its * bottom exits at the top. * - "auto": "pin" when the track is taller than ~1.2 viewports, else "cross". */ type ScrubMapping = "auto" | "pin" | "cross"; interface VideoScrubberOptions { /** What drives progress. Fixed for the instance lifetime. */ driver?: ScrubDriver; /** Element whose geometry defines progress. Default: the video's parent. */ track?: HTMLElement; /** Scroll→progress mapping (scroll driver only). */ mapping?: ScrubMapping; /** * How quickly the video catches up to the scrub position. Higher arrives * sooner; `0` is instant with no trailing at all. Default 8. * * The old name for this was `smooth`, which read backwards — a lower number * produced *more* smoothing. */ speed?: number; /** Pointer driver: which axis of the track maps to progress. */ pointerAxis?: "x" | "y"; /** Fired when smoothed progress changes (per frame, deduplicated). */ onProgress?: (progress: number, time: number) => void; } declare class VideoScrubber { readonly video: HTMLVideoElement; readonly track: HTMLElement; #private; /** * Sampled once, at construction, and honoured by every later write to * `speed`. Without it the reduced-motion clamp applied only in the * constructor and any subsequent `update({ speed })` silently restored the * trailing lag — which is exactly what a React wrapper does when a `speed` * prop changes. */ constructor(video: HTMLVideoElement, options?: VideoScrubberOptions); /** Muted play/pause round trip so iOS Safari buffers before the first scrub. */ /** Smoothed progress, 0..1. */ get progress(): number; /** Where progress is heading before smoothing settles. */ get targetProgress(): number; /** * Set progress directly. The natural API for `driver: "manual"`; under * other drivers the next frame's read overwrites it. */ set(progress: number): void; /** * Reduced motion wins over any requested smoothing. Scrubbing itself is * direct manipulation, so it stays enabled — but the trailing lag is * autonomous motion, and that is the part to drop. */ /** Live-tunable options. `driver` and `track` are fixed by design. */ update(options: Pick): void; /** Stops everything and restores the video element exactly as found. */ destroy(): void; } type UseVideoScrubberOptions = Omit; interface UseVideoScrubberReturn { /** * Ref to attach to the `