@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
45 lines (44 loc) • 2.01 kB
JavaScript
import { wrap } from 'motion';
export function useCycle(...args) {
const getItems = args.length === 1 && typeof args[0] === 'function'
? args[0]
: () => args;
if (getItems().length === 0) {
throw new Error('useCycle requires at least one item');
}
let index = $state(0);
return {
get current() {
const items = getItems();
// Reactive-getter form: if the consumer's source emptied
// mid-cycle the public type can no longer be honored. Throw
// loudly so the bug surfaces immediately rather than leaking
// `undefined` through a `T`-typed read.
if (items.length === 0) {
throw new Error('useCycle items getter returned an empty list');
}
// Clamp on read so out-of-range indexes (from `cycle(-5)` or
// `cycle(99)`, or items shrinking under us in the getter form)
// resolve to the nearest valid edge instead of `undefined`.
const safeIndex = index < 0 ? 0 : index >= items.length ? items.length - 1 : index;
return items[safeIndex];
},
cycle: (next) => {
const items = getItems();
if (items.length === 0)
return;
// Reject non-finite / non-integer indexes up-front: `NaN` slips
// past the read-time clamp (NaN comparisons return false for
// both `< 0` and `>= length`) and would silently make `.current`
// resolve to `undefined`, breaking the `T` contract. Throw
// loudly to surface the consumer bug at write-time.
if (typeof next === 'number' && !Number.isInteger(next)) {
throw new Error('useCycle index must be a finite integer');
}
const target = typeof next === 'number' ? next : wrap(0, items.length, index + 1);
if (target === index)
return;
index = target;
}
};
}