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

112 lines (111 loc) 3.69 kB
import { SvelteSet } from 'svelte/reactivity'; const mountedError = 'controls.start() should only be called after a component has mounted. Consider calling within a $effect.'; const setMountedError = 'controls.set() should only be called after a component has mounted. Consider calling within a $effect.'; /** * Returns true when a value looks like Motion's legacy animation controls. * * Upstream `motion-dom` treats any non-null object with a `start` * function as animation controls. Matching that narrow check keeps * `animate={controls}` detection compatible with Motion's public shape. * * @param value Value passed to `animate`. * @returns Whether `value` is an animation controls object. * * @example * ```ts * const controls = useAnimationControls() * isAnimationControls(controls) // true * isAnimationControls({ opacity: 1 }) // false * ``` */ export const isAnimationControls = (value) => { return (value !== null && typeof value === 'object' && typeof value.start === 'function'); }; /** * Create legacy animation controls. * * This mirrors upstream Motion's `animationControls()`: controls collect * subscribed motion components, guard `start`/`set` until mounted, fan out * starts to every subscriber, and stop all subscribers on unmount. * * @returns Animation controls with `subscribe`, `start`, `set`, `stop`, * and `mount`. * * @example * ```ts * const controls = animationControls() * const cleanup = controls.mount() * await controls.start({ opacity: 1 }) * cleanup() * ``` */ export const animationControls = () => { let hasMounted = false; const subscribers = new SvelteSet(); const controls = { subscribe(subscriber) { subscribers.add(subscriber); return () => { subscribers.delete(subscriber); }; }, start(definition, transitionOverride) { if (!hasMounted) { throw new Error(mountedError); } const animations = []; subscribers.forEach((subscriber) => { animations.push(subscriber.start(definition, transitionOverride)); }); return Promise.all(animations); }, set(definition) { if (!hasMounted) { throw new Error(setMountedError); } subscribers.forEach((subscriber) => subscriber.set(definition)); }, stop() { subscribers.forEach((subscriber) => subscriber.stop()); }, mount() { hasMounted = true; return () => { hasMounted = false; controls.stop(); }; } }; return controls; }; /** * Create imperative controls for one or more `motion.*` components. * * Pass the returned object to `animate={controls}`. Once mounted, call * `controls.start(definition)`, `controls.set(definition)`, or * `controls.stop()` to coordinate every subscribed component. * * @returns Mounted animation controls. * @see https://motion.dev/docs/react-use-animation-controls * * @example * ```svelte * <script lang="ts"> * import { motion, useAnimationControls } from '@humanspeak/svelte-motion' * * const controls = useAnimationControls() * </script> * * <button onclick={() => controls.start({ scale: 1.2 })}>Pop</button> * <motion.div animate={controls} /> * ``` */ export const useAnimationControls = () => { const controls = animationControls(); $effect(() => controls.mount()); return controls; }; /** Alias matching Motion's legacy `useAnimation` export. */ export const useAnimation = useAnimationControls;