UNPKG

@humanspeak/svelte-motion

Version:

Framer Motion for Svelte 5. Declarative motion.<tag> components with AnimatePresence exit animations, gestures (hover, tap, drag, focus, in-view), variants, FLIP layout animations, shared-layout transitions, spring physics, and scroll-linked motion values

136 lines (135 loc) 6.55 kB
/** * Pan gesture session. * * Direct port of framer-motion's `PanSession` * (`packages/framer-motion/src/gestures/pan/PanSession.ts`) and `PanGesture` * (`packages/framer-motion/src/gestures/pan/index.ts`). Pan is the * primitive that powers swipe-to-dismiss drawers, swipe-to-delete rows, * carousels, and any gesture that tracks pointer offset/velocity without * the constraint/momentum/snap-to-origin baggage of `drag`. * * Critical design notes (mirrors upstream): * * - `pointermove`, `pointerup`, `pointercancel` subscribe on the * `contextWindow` (defaults to `window`), NOT the source element. This * keeps the gesture alive even when the pointer leaves the element's * bounds during a fast swipe — the original element is only used for * the initial `pointerdown` and for scroll-compensation tracking. * * - `distanceThreshold` (default `3`px) gates the `onStart` callback so * a steady press without movement doesn't fire a pan. `onSessionStart` * fires immediately on pointerdown for setup work. * * - Per-frame throttling via `frame.update(updatePoint, true)` so a flood * of pointermove events doesn't run handlers more than once per render * frame. On top of that, individual handlers are routed onto motion-dom's * step lanes (see `wrapUpdate` / `wrapPostRender` above): * `onSessionStart` / `onStart` / `onMove` land on `update`, * `onEnd` / `onSessionEnd` on `postRender`. Matches upstream's * `asyncHandler` + `frame.postRender` split byte-for-byte. * * - `getPanInfo` returns `{ point, delta, offset, velocity }` — identical * shape to motion-dom's `DragInfo` / framer-motion's `PanInfo`. * * - Velocity uses a 100ms history window with the "skip the pointer-down * origin if it's too stale" tweak upstream added for hold-then-flick * gestures. */ import type { DragInfo } from '../types'; export interface PanHandlers { /** Fires on `pointerdown` regardless of whether movement follows. */ onSessionStart?: (event: PointerEvent, info: DragInfo) => void; /** Fires the first time pointer offset crosses `distanceThreshold`. */ onStart?: (event: PointerEvent, info: DragInfo) => void; /** Fires on every per-frame-throttled pointermove past threshold. */ onMove?: (event: PointerEvent, info: DragInfo) => void; /** Fires on `pointerup` / `pointercancel` if `onStart` ever fired. */ onEnd?: (event: PointerEvent, info: DragInfo) => void; /** Fires on `pointerup` / `pointercancel` always (paired with `onSessionStart`). */ onSessionEnd?: (event: PointerEvent, info: DragInfo) => void; } export interface AttachPanOptions { /** * Movement distance (in pixels) required before `onStart`/`onMove` * fire. Default `3` — same as framer-motion. A steady press with * sub-threshold drift is reported via `onSessionStart` / `onSessionEnd` * only. */ distanceThreshold?: number; /** * Window to attach the move/up/cancel listeners to. Defaults to the * source element's owner window. Override for iframe / shadow-root * scenarios. */ contextWindow?: Window | null; } /** * Cleanup function returned by `attachPan`. Carries an `update` method * that hot-swaps the live handler set without tearing down the active * `PanSession` — call this when a consumer's `onPan` reference changes * mid-gesture (the canonical Svelte 5 pattern of inline arrow handlers * passes a fresh closure every render). Without this, the host * `$effect` would have to teardown + re-attach and the user's in-flight * pan would silently die. */ export type AttachPanCleanup = (() => void) & { update: (next: PanHandlers) => void; }; /** * Attach a pan gesture session to `el`. Returns a cleanup function that * tears down the pointerdown listener and ends any in-flight session, * with a `.update(next)` method for hot-swapping handlers mid-gesture. * * Internally a fresh `PanSession` spawns on each pointerdown — the * outer attachment just keeps the pointerdown listener alive across the * element's lifetime. * * SSR-safe: returns a no-op cleanup if `window` is undefined. The Svelte * `$effect` consumer never fires on the server anyway, but defending the * boundary lets the module load cleanly in node-only test runners. * * Lifecycle guarantee: when the returned cleanup runs mid-gesture, the * session synthesizes `onEnd` + `onSessionEnd` against the raw handlers * BEFORE removing listeners (see `PanSession.dispatchTerminal`). Hosts * (e.g. `_MotionContainer`'s pan `$effect`) can put their `whilePan` * revert logic inside the user-supplied `onEnd` and rely on it firing * exactly once per gesture — whether the user released or the host * forced teardown. * * @param el Target element to bind `pointerdown` on. Move/up/cancel * events are listened for on the element's owning window so a fast * swipe past the element's bounds keeps the gesture alive. * @param handlers Pan lifecycle handlers. Any subset of * `onSessionStart` (fires on pointerdown), `onStart` (fires the first * time the cumulative offset crosses `distanceThreshold`), `onMove` * (per-frame-throttled on every pointermove past threshold), `onEnd` * (fires on pointerup/cancel if `onStart` ever fired), `onSessionEnd` * (fires on every pointerup/cancel where a pointermove occurred). * @param options Per-session config. `distanceThreshold` (default 3px) * gates the start callback; `contextWindow` overrides the owning * window (use for shadow-root / iframe scenarios). * @returns A cleanup function with an attached `.update(next)` method. * Calling the cleanup ends the session + removes the pointerdown * listener. Calling `.update(next)` swaps handlers in place on the * live session without rebuilding it — the canonical Svelte pattern * for inline arrow handlers that change identity each render. * * @example * ```ts * const cleanup = attachPan(node, { * onStart: (_event, info) => console.log('start', info.offset), * onMove: (_event, info) => x.set(info.offset.x), * onEnd: (_event, info) => { * if (Math.abs(info.velocity.x) > 600) commit() * else animate(x, 0, { type: 'spring' }) * } * }) * * // Later, swap handlers without ending the live gesture: * cleanup.update({ onMove: (_e, info) => x.set(info.offset.x * 2) }) * * // On unmount: * cleanup() * ``` */ export declare const attachPan: (el: HTMLElement, handlers: PanHandlers, options?: AttachPanOptions) => AttachPanCleanup;