@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
99 lines (98 loc) • 4.42 kB
TypeScript
/** Function returned by {@link useCycle} for advancing or jumping the index. */
export type Cycle = (next?: number) => void;
/**
* State returned by {@link useCycle}: an object with a reactive `.current`
* getter and a `cycle` function. Both reads and writes flow through the
* same object, so consumers don't need to destructure (which would
* snapshot `.current` and lose reactivity under runes).
*/
export type CycleState<T> = {
readonly current: T;
cycle: Cycle;
};
/**
* Function that returns the current items list, used by the reactive
* overload of {@link useCycle}. The function is re-invoked on every read
* so changes to the underlying reactive source propagate automatically.
*/
export type CycleItemsGetter<T> = () => readonly T[];
/**
* Cycles through a series of values. Mirrors framer-motion's `useCycle`.
*
* Two call forms:
*
* - **Varargs** — `useCycle(...items)` — items are captured once and stay
* fixed for the cycle's lifetime. Matches React framer-motion's signature.
* - **Reactive getter** — `useCycle(() => items)` — items are read on every
* access, so passing a `$state`/`$derived` source lets the cycle pick up
* list changes without recreating it.
*
* In both forms:
*
* - `state.current` is reactive — read it in templates / `$derived` / `$effect`
* and it tracks both index changes and (in the getter form) item changes.
* - `state.cycle()` advances to the next item (wrapping at the end).
* - `state.cycle(i)` jumps to index `i`. The index is stored as-given;
* `.current` then clamps on read so any out-of-range index — negative,
* overflow, or items shrinking underneath the reactive-getter form —
* resolves to the nearest valid edge (`items[0]` or `items[length - 1]`)
* instead of `undefined`. This is a defensive divergence from React
* framer-motion (which returns `items[i]`, possibly undefined) so the
* reactive form stays safe and `.current` always honors its `T` type.
* If the reactive getter ever returns an empty list, `.current` throws.
* - Calls that resolve to the current index are no-ops, matching React
* `useState`'s `Object.is` bail-out.
*
* Two deliberate divergences from React's `useCycle`:
*
* 1. Return shape — React's `[value, cycle]` tuple can't survive
* destructuring under Svelte 5 runes (snapshots the value, loses
* reactivity), so we return `{ current, cycle }`.
* 2. Out-of-range reads always clamp (see above) instead of returning
* `items[i]` undefined.
*
* Otherwise 1:1 with React, including same-index no-op bail-out and
* the `wrap(0, length, index + 1)` advance semantics.
*
* Ambiguity: `useCycle(fn)` with a single function value is treated as the
* reactive overload, not as a single-item cycle. To cycle through one
* function value, use `useCycle(() => [fn])` or just call it directly —
* a single-item cycle is a no-op anyway.
*
* @see https://motion.dev/docs/react-use-cycle
*
* @example Static varargs
* ```svelte
* <script lang="ts">
* import { motion, useCycle } from '@humanspeak/svelte-motion'
*
* const x = useCycle(0, 50, 100)
* </script>
*
* <motion.div animate={{ x: x.current }} onclick={() => x.cycle()} />
* ```
*
* @example Reactive items
* ```svelte
* <script lang="ts">
* let { labels }: { labels: string[] } = $props()
* const variant = useCycle(() => labels)
* </script>
*
* <motion.div animate={variant.current} onclick={() => variant.cycle()} />
* ```
*
* @param itemsGetter Function returning the current items list; re-invoked
* on every `.current` read so reactive sources propagate. Use this form
* when items can change between mount and unmount. (Reactive overload.)
* @param items One or more values to cycle through. Captured once at call
* time and fixed for the cycle's lifetime. (Varargs overload.)
* @returns A `CycleState<T>` with a reactive `.current` getter and a
* `cycle(next?: number)` advance/jump function. `.current` always
* honors its `T` type by clamping out-of-range indexes; it throws
* if a reactive getter empties the items list mid-cycle.
* `.cycle()` throws on non-integer (`NaN`, `1.5`, `Infinity`)
* indexes and returns early as a no-op on empty items.
*/
export declare function useCycle<T>(itemsGetter: CycleItemsGetter<T>): CycleState<T>;
export declare function useCycle<T>(...items: T[]): CycleState<T>;