@humanspeak/svelte-motion 0.4.7 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -10,8 +10,8 @@ export type { DragAxis, DragConstraints, DragControls, DragInfo, DragTransition,
10
10
  export { useAnimate } from './utils/animate.svelte';
11
11
  export type { AnimationScope } from './utils/animate.svelte';
12
12
  export { useAnimationFrame } from './utils/animationFrame';
13
- export { useCycle } from './utils/cycle';
14
- export type { Cycle, CycleState } from './utils/cycle';
13
+ export { useCycle } from './utils/cycle.svelte';
14
+ export type { Cycle, CycleState } from './utils/cycle.svelte';
15
15
  export { createDragControls } from './utils/dragControls';
16
16
  export { useInView } from './utils/inView';
17
17
  export type { UseInViewOptions } from './utils/inView';
@@ -22,7 +22,8 @@ export { useMotionValueEvent } from './utils/motionValueEvent';
22
22
  export { useReducedMotion } from './utils/reducedMotion';
23
23
  export { useReducedMotionConfig } from './utils/reducedMotionConfig';
24
24
  export { useScroll } from './utils/scroll';
25
- export { useSpring } from './utils/spring';
25
+ export { useSpring } from './utils/spring.svelte';
26
+ export type { SpringMotionValue, UseSpringOptions } from './utils/spring.svelte';
26
27
  export { useIsPresent, usePresence } from './utils/usePresence';
27
28
  export type { UsePresenceState } from './utils/usePresence';
28
29
  export { useVelocity } from './utils/velocity';
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ export { anticipate, backIn, backInOut, backOut, circIn, circInOut, circOut, cub
11
11
  export { clamp, distance, distance2D, interpolate, mix, pipe, progress, wrap } from 'motion';
12
12
  export { useAnimate } from './utils/animate.svelte';
13
13
  export { useAnimationFrame } from './utils/animationFrame';
14
- export { useCycle } from './utils/cycle';
14
+ export { useCycle } from './utils/cycle.svelte';
15
15
  export { createDragControls } from './utils/dragControls';
16
16
  export { useInView } from './utils/inView';
17
17
  export { useMotionTemplate } from './utils/motionTemplate';
@@ -20,7 +20,7 @@ export { useMotionValueEvent } from './utils/motionValueEvent';
20
20
  export { useReducedMotion } from './utils/reducedMotion';
21
21
  export { useReducedMotionConfig } from './utils/reducedMotionConfig';
22
22
  export { useScroll } from './utils/scroll';
23
- export { useSpring } from './utils/spring';
23
+ export { useSpring } from './utils/spring.svelte';
24
24
  export { useIsPresent, usePresence } from './utils/usePresence';
25
25
  export { useVelocity } from './utils/velocity';
26
26
  /**
@@ -0,0 +1,98 @@
1
+ /** Function returned by {@link useCycle} for advancing or jumping the index. */
2
+ export type Cycle = (next?: number) => void;
3
+ /**
4
+ * State returned by {@link useCycle}: an object with a reactive `.current`
5
+ * getter and a `cycle` function. Both reads and writes flow through the
6
+ * same object, so consumers don't need to destructure (which would
7
+ * snapshot `.current` and lose reactivity under runes).
8
+ */
9
+ export type CycleState<T> = {
10
+ readonly current: T;
11
+ cycle: Cycle;
12
+ };
13
+ /**
14
+ * Function that returns the current items list, used by the reactive
15
+ * overload of {@link useCycle}. The function is re-invoked on every read
16
+ * so changes to the underlying reactive source propagate automatically.
17
+ */
18
+ export type CycleItemsGetter<T> = () => readonly T[];
19
+ /**
20
+ * Cycles through a series of values. Mirrors framer-motion's `useCycle`.
21
+ *
22
+ * Two call forms:
23
+ *
24
+ * - **Varargs** — `useCycle(...items)` — items are captured once and stay
25
+ * fixed for the cycle's lifetime. Matches React framer-motion's signature.
26
+ * - **Reactive getter** — `useCycle(() => items)` — items are read on every
27
+ * access, so passing a `$state`/`$derived` source lets the cycle pick up
28
+ * list changes without recreating it.
29
+ *
30
+ * In both forms:
31
+ *
32
+ * - `state.current` is reactive — read it in templates / `$derived` / `$effect`
33
+ * and it tracks both index changes and (in the getter form) item changes.
34
+ * - `state.cycle()` advances to the next item (wrapping at the end).
35
+ * - `state.cycle(i)` jumps to index `i`. The index is stored as-given;
36
+ * `.current` then clamps on read so any out-of-range index — negative,
37
+ * overflow, or items shrinking underneath the reactive-getter form —
38
+ * resolves to the nearest valid edge (`items[0]` or `items[length - 1]`)
39
+ * instead of `undefined`. This is a defensive divergence from React
40
+ * framer-motion (which returns `items[i]`, possibly undefined) so the
41
+ * reactive form stays safe and `.current` always honors its `T` type.
42
+ * If the reactive getter ever returns an empty list, `.current` throws.
43
+ * - Calls that resolve to the current index are no-ops, matching React
44
+ * `useState`'s `Object.is` bail-out.
45
+ *
46
+ * Two deliberate divergences from React's `useCycle`:
47
+ *
48
+ * 1. Return shape — React's `[value, cycle]` tuple can't survive
49
+ * destructuring under Svelte 5 runes (snapshots the value, loses
50
+ * reactivity), so we return `{ current, cycle }`.
51
+ * 2. Out-of-range reads always clamp (see above) instead of returning
52
+ * `items[i]` undefined.
53
+ *
54
+ * Otherwise 1:1 with React, including same-index no-op bail-out and
55
+ * the `wrap(0, length, index + 1)` advance semantics.
56
+ *
57
+ * Ambiguity: `useCycle(fn)` with a single function value is treated as the
58
+ * reactive overload, not as a single-item cycle. To cycle through one
59
+ * function value, use `useCycle(() => [fn])` or just call it directly —
60
+ * a single-item cycle is a no-op anyway.
61
+ *
62
+ * @see https://motion.dev/docs/react-use-cycle
63
+ *
64
+ * @example Static varargs
65
+ * ```svelte
66
+ * <script lang="ts">
67
+ * import { motion, useCycle } from '@humanspeak/svelte-motion'
68
+ *
69
+ * const x = useCycle(0, 50, 100)
70
+ * </script>
71
+ *
72
+ * <motion.div animate={{ x: x.current }} onclick={() => x.cycle()} />
73
+ * ```
74
+ *
75
+ * @example Reactive items
76
+ * ```svelte
77
+ * <script lang="ts">
78
+ * let { labels }: { labels: string[] } = $props()
79
+ * const variant = useCycle(() => labels)
80
+ * </script>
81
+ *
82
+ * <motion.div animate={variant.current} onclick={() => variant.cycle()} />
83
+ * ```
84
+ *
85
+ * @param itemsGetter Function returning the current items list; re-invoked
86
+ * on every `.current` read so reactive sources propagate. Use this form
87
+ * when items can change between mount and unmount. (Reactive overload.)
88
+ * @param items One or more values to cycle through. Captured once at call
89
+ * time and fixed for the cycle's lifetime. (Varargs overload.)
90
+ * @returns A `CycleState<T>` with a reactive `.current` getter and a
91
+ * `cycle(next?: number)` advance/jump function. `.current` always
92
+ * honors its `T` type by clamping out-of-range indexes; it throws
93
+ * if a reactive getter empties the items list mid-cycle.
94
+ * `.cycle()` throws on non-integer (`NaN`, `1.5`, `Infinity`)
95
+ * indexes and returns early as a no-op on empty items.
96
+ */
97
+ export declare function useCycle<T>(itemsGetter: CycleItemsGetter<T>): CycleState<T>;
98
+ export declare function useCycle<T>(...items: T[]): CycleState<T>;
@@ -0,0 +1,44 @@
1
+ import { wrap } from 'motion';
2
+ export function useCycle(...args) {
3
+ const getItems = args.length === 1 && typeof args[0] === 'function'
4
+ ? args[0]
5
+ : () => args;
6
+ if (getItems().length === 0) {
7
+ throw new Error('useCycle requires at least one item');
8
+ }
9
+ let index = $state(0);
10
+ return {
11
+ get current() {
12
+ const items = getItems();
13
+ // Reactive-getter form: if the consumer's source emptied
14
+ // mid-cycle the public type can no longer be honored. Throw
15
+ // loudly so the bug surfaces immediately rather than leaking
16
+ // `undefined` through a `T`-typed read.
17
+ if (items.length === 0) {
18
+ throw new Error('useCycle items getter returned an empty list');
19
+ }
20
+ // Clamp on read so out-of-range indexes (from `cycle(-5)` or
21
+ // `cycle(99)`, or items shrinking under us in the getter form)
22
+ // resolve to the nearest valid edge instead of `undefined`.
23
+ const safeIndex = index < 0 ? 0 : index >= items.length ? items.length - 1 : index;
24
+ return items[safeIndex];
25
+ },
26
+ cycle: (next) => {
27
+ const items = getItems();
28
+ if (items.length === 0)
29
+ return;
30
+ // Reject non-finite / non-integer indexes up-front: `NaN` slips
31
+ // past the read-time clamp (NaN comparisons return false for
32
+ // both `< 0` and `>= length`) and would silently make `.current`
33
+ // resolve to `undefined`, breaking the `T` contract. Throw
34
+ // loudly to surface the consumer bug at write-time.
35
+ if (typeof next === 'number' && !Number.isInteger(next)) {
36
+ throw new Error('useCycle index must be a finite integer');
37
+ }
38
+ const target = typeof next === 'number' ? next : wrap(0, items.length, index + 1);
39
+ if (target === index)
40
+ return;
41
+ index = target;
42
+ }
43
+ };
44
+ }
@@ -0,0 +1,83 @@
1
+ import { type FollowValueOptions, type MotionValue, type SpringOptions } from 'motion-dom';
2
+ import { type Readable } from 'svelte/store';
3
+ /**
4
+ * Spring + follow options for {@link useSpring}.
5
+ *
6
+ * Mirrors framer-motion's `useSpring` options 1:1: every `SpringOptions` key
7
+ * (`stiffness`, `damping`, `mass`, `duration`, `visualDuration`, `bounce`,
8
+ * `velocity`, `restDelta`, `restSpeed`) plus `skipInitialAnimation` for
9
+ * scroll-restoration scenarios.
10
+ *
11
+ * @see https://motion.dev/docs/react-use-spring
12
+ */
13
+ export type UseSpringOptions = SpringOptions & Pick<FollowValueOptions, 'skipInitialAnimation'>;
14
+ /**
15
+ * The augmented `MotionValue` returned by {@link useSpring}.
16
+ *
17
+ * It IS a real `MotionValue<T>` (so it passes `isMotionValue`, composes with
18
+ * `animate()`, `useTransform`, and the rest of motion-dom). On top of that it
19
+ * adds two affordances:
20
+ *
21
+ * - `current` — a Svelte-5 reactive read backed by `$state`. Use in templates
22
+ * and `$derived` / `$effect` to track the spring value without subscribing.
23
+ * - `subscribe` — Svelte readable store contract. Calls the run function once
24
+ * synchronously with the current value, then on every change. Lets the
25
+ * spring be used with `$spring` template syntax, `get(spring)`, and as a
26
+ * dependency in `useTransform`'s function form.
27
+ */
28
+ export type SpringMotionValue<T extends number | string> = Omit<MotionValue<T>, 'current'> & {
29
+ /** Reactive read in Svelte 5 templates / `$derived` / `$effect`. */
30
+ readonly current: T;
31
+ /** Svelte readable store compatibility. */
32
+ subscribe: (run: (value: T) => void) => () => void;
33
+ };
34
+ /**
35
+ * Creates a spring-animated `MotionValue`.
36
+ *
37
+ * Set a target with `.set(v)` to animate to it using spring physics, or
38
+ * `.jump(v)` to skip the animation. Pass another `MotionValue` (or, for
39
+ * backwards compatibility, a Svelte readable store like the ones from
40
+ * `useScroll` / `useTime`) as `source` and the spring will animate towards
41
+ * whatever that source emits.
42
+ *
43
+ * Returned object is a real motion-dom `MotionValue` — composes with
44
+ * `animate()`, `useTransform`, `useVelocity`, and motion-dom's animation
45
+ * engine. On top, it exposes:
46
+ *
47
+ * - `.current` — Svelte-5 reactive read for templates and `$derived` /
48
+ * `$effect`.
49
+ * - `.subscribe(run)` — Svelte readable store contract so `$spring` template
50
+ * syntax and `useTransform(() => …, [spring])` keep working during the
51
+ * Tier 2 migration window.
52
+ *
53
+ * Lifecycle: must be called during component initialization. Cleanup is
54
+ * registered via `$effect`; the spring stops animating and unsubscribes from
55
+ * its source when the surrounding component / effect tears down. Call
56
+ * `.destroy()` to clean up early.
57
+ *
58
+ * SSR-safe: returns a static `MotionValue` with no animation on the server.
59
+ *
60
+ * @template T
61
+ * @param {number|string|MotionValue<number>|MotionValue<string>|Readable<number|string>} source Initial value or a source to follow.
62
+ * @param {UseSpringOptions} [options] Spring + follow configuration.
63
+ * @returns {SpringMotionValue<T>} A `MotionValue` with `.current` and `.subscribe`.
64
+ *
65
+ * @example
66
+ * ```svelte
67
+ * <script lang="ts">
68
+ * import { useSpring } from '@humanspeak/svelte-motion'
69
+ *
70
+ * const x = useSpring(0, { stiffness: 300, damping: 30 })
71
+ * </script>
72
+ *
73
+ * <button onclick={() => x.set(100)}>Animate</button>
74
+ * <div>{x.current}</div>
75
+ * ```
76
+ *
77
+ * @see https://motion.dev/docs/react-use-spring
78
+ */
79
+ export declare function useSpring(source: number, options?: UseSpringOptions): SpringMotionValue<number>;
80
+ export declare function useSpring(source: string, options?: UseSpringOptions): SpringMotionValue<string>;
81
+ export declare function useSpring(source: MotionValue<number>, options?: UseSpringOptions): SpringMotionValue<number>;
82
+ export declare function useSpring(source: MotionValue<string>, options?: UseSpringOptions): SpringMotionValue<string>;
83
+ export declare function useSpring<T extends number | string>(source: Readable<T>, options?: UseSpringOptions): SpringMotionValue<T>;
@@ -0,0 +1,118 @@
1
+ import { attachFollow, isMotionValue, motionValue } from 'motion-dom';
2
+ import { get } from 'svelte/store';
3
+ /**
4
+ * Detects a Svelte readable store. Excludes motion-dom `MotionValue` instances
5
+ * (which also expose `subscribe`-shaped APIs in some versions) so the
6
+ * MotionValue path is preferred.
7
+ */
8
+ const isSvelteReadable = (value) => {
9
+ return (!!value &&
10
+ typeof value === 'object' &&
11
+ typeof value.subscribe === 'function' &&
12
+ !isMotionValue(value));
13
+ };
14
+ export function useSpring(source, options = {}) {
15
+ // SSR: return a static MotionValue with no animation. Reads return the
16
+ // best-effort initial value; .set / .jump become no-ops to avoid drifting
17
+ // away from the server-rendered snapshot.
18
+ if (typeof window === 'undefined') {
19
+ const initial = readInitial(source);
20
+ const ssrValue = motionValue(initial);
21
+ ssrValue.set = () => undefined;
22
+ ssrValue.jump = () => undefined;
23
+ return augmentForSvelte(ssrValue, () => undefined);
24
+ }
25
+ // Resolve initial + follow source.
26
+ let followSource;
27
+ let cleanupReadableBridge;
28
+ let svelteBridge;
29
+ if (isMotionValue(source)) {
30
+ followSource = source;
31
+ }
32
+ else if (isSvelteReadable(source)) {
33
+ // Bridge a Svelte readable into a MotionValue so attachFollow can
34
+ // track it. Synchronous initial sample comes from svelte/store's get().
35
+ const initialFromReadable = get(source);
36
+ svelteBridge = motionValue(initialFromReadable);
37
+ cleanupReadableBridge = source.subscribe((v) => {
38
+ // The Svelte readable contract calls the subscriber synchronously
39
+ // with the current value on subscribe. Skip if it equals the
40
+ // already-seeded bridge value so attachFollow doesn't fire a
41
+ // spring on the initial emit. Subsequent emits go through set()
42
+ // and trigger animation.
43
+ if (svelteBridge.get() === v)
44
+ return;
45
+ svelteBridge.set(v);
46
+ });
47
+ followSource = svelteBridge;
48
+ }
49
+ else {
50
+ followSource = source;
51
+ }
52
+ const initial = isMotionValue(followSource) ? followSource.get() : followSource;
53
+ const value = motionValue(initial);
54
+ const stopFollow = attachFollow(value, followSource, { type: 'spring', ...options });
55
+ // Side-cleanup for our augmentations. Single-shot guard lives in the
56
+ // augmented `value.destroy` (the only caller), so no flag here.
57
+ const dispose = () => {
58
+ stopFollow?.();
59
+ cleanupReadableBridge?.();
60
+ svelteBridge?.destroy();
61
+ };
62
+ $effect(() => () => value.destroy());
63
+ return augmentForSvelte(value, dispose);
64
+ }
65
+ /**
66
+ * Pull the synchronous initial value out of any accepted source form.
67
+ */
68
+ const readInitial = (source) => {
69
+ if (typeof source === 'number' || typeof source === 'string')
70
+ return source;
71
+ if (isMotionValue(source))
72
+ return source.get();
73
+ if (isSvelteReadable(source))
74
+ return get(source);
75
+ return 0;
76
+ };
77
+ /**
78
+ * Layer Svelte-friendly affordances onto a motion-dom MotionValue: a
79
+ * `$state`-tracked `.current` accessor (routing motion-dom's internal
80
+ * `this.current = v` writes through `$state` so templates and `$derived` /
81
+ * `$effect` re-run) and a Svelte readable store `.subscribe(run)` shim.
82
+ */
83
+ const augmentForSvelte = (value, dispose) => {
84
+ let current = $state(value.get());
85
+ Object.defineProperty(value, 'current', {
86
+ get: () => current,
87
+ // Same-value writes are no-ops: motion-dom's `updateAndNotify` calls
88
+ // `setCurrent(v)` before its own change check, so spring frames at
89
+ // rest still hit this setter; skipping equal writes avoids gratuitous
90
+ // accessor work even though $state would itself dedupe.
91
+ set: (v) => {
92
+ if (v !== current)
93
+ current = v;
94
+ },
95
+ enumerable: true,
96
+ configurable: true
97
+ });
98
+ const originalDestroy = value.destroy.bind(value);
99
+ let destroyed = false;
100
+ value.destroy = () => {
101
+ if (destroyed)
102
+ return;
103
+ destroyed = true;
104
+ dispose();
105
+ originalDestroy();
106
+ };
107
+ const subscribe = (run) => {
108
+ run(value.get());
109
+ return value.on('change', run);
110
+ };
111
+ Object.defineProperty(value, 'subscribe', {
112
+ value: subscribe,
113
+ writable: false,
114
+ enumerable: false,
115
+ configurable: true
116
+ });
117
+ return value;
118
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanspeak/svelte-motion",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "description": "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. The drop-in Framer Motion alternative for Svelte and SvelteKit.",
5
5
  "keywords": [
6
6
  "svelte",
@@ -95,8 +95,8 @@
95
95
  },
96
96
  "dependencies": {
97
97
  "acorn": "^8.16.0",
98
- "motion": "^12.39.0",
99
- "motion-dom": "^12.39.0"
98
+ "motion": "^12.40.0",
99
+ "motion-dom": "^12.40.0"
100
100
  },
101
101
  "devDependencies": {
102
102
  "@changesets/cli": "^2.31.0",
@@ -115,7 +115,7 @@
115
115
  "@testing-library/jest-dom": "^6.9.1",
116
116
  "@testing-library/svelte": "^5.3.1",
117
117
  "@types/node": "^25.9.1",
118
- "@vitest/coverage-v8": "^4.1.6",
118
+ "@vitest/coverage-v8": "^4.1.7",
119
119
  "eslint": "^10.4.0",
120
120
  "eslint-config-prettier": "10.1.8",
121
121
  "eslint-plugin-import": "2.32.0",
@@ -127,7 +127,7 @@
127
127
  "html-void-elements": "^3.0.0",
128
128
  "husky": "^9.1.7",
129
129
  "jsdom": "^29.1.1",
130
- "mprocs": "^0.9.2",
130
+ "mprocs": "^0.9.3",
131
131
  "prettier": "^3.8.3",
132
132
  "prettier-plugin-organize-imports": "^4.3.0",
133
133
  "prettier-plugin-sort-json": "^4.2.0",
@@ -135,7 +135,7 @@
135
135
  "prettier-plugin-tailwindcss": "^0.8.0",
136
136
  "publint": "^0.3.21",
137
137
  "runed": "0.37.1",
138
- "svelte": "^5.55.8",
138
+ "svelte": "^5.55.9",
139
139
  "svelte-check": "^4.4.8",
140
140
  "svg-tags": "^1.0.0",
141
141
  "tailwind-merge": "^3.6.0",
@@ -145,9 +145,9 @@
145
145
  "tsx": "^4.22.3",
146
146
  "typescript": "^6.0.3",
147
147
  "typescript-eslint": "^8.59.4",
148
- "vite": "^8.0.13",
148
+ "vite": "^8.0.14",
149
149
  "vite-tsconfig-paths": "^6.1.1",
150
- "vitest": "^4.1.6"
150
+ "vitest": "^4.1.7"
151
151
  },
152
152
  "peerDependencies": {
153
153
  "svelte": "^5.0.0"
@@ -1,35 +0,0 @@
1
- import { type Readable } from 'svelte/store';
2
- export type Cycle = (next?: number) => void;
3
- export type CycleState<T> = [Readable<T>, Cycle];
4
- /**
5
- * Cycles through a series of values. Mirrors Framer Motion's `useCycle`.
6
- *
7
- * Returns a tuple `[value, cycle]`:
8
- *
9
- * - `value` is a Svelte readable store of the current item; subscribe with
10
- * `$value` in templates.
11
- * - `cycle()` advances to the next item, wrapping back to index `0` when it
12
- * passes the end.
13
- * - `cycle(i)` jumps to the item at index `i`. The index is taken as-is to
14
- * match `framer-motion` &mdash; out-of-range values yield `items[i]`, which
15
- * may be `undefined`.
16
- *
17
- * Calls that resolve to the current index are no-ops and do not notify
18
- * subscribers, matching React `useState`'s `Object.is` bail-out semantics.
19
- *
20
- * @param items - Items to cycle through. Must include at least one item.
21
- * @returns A `[Readable<T>, Cycle]` tuple.
22
- * @see https://motion.dev/docs/react-use-cycle
23
- *
24
- * @example
25
- * ```svelte
26
- * <script>
27
- * import { motion, useCycle } from '@humanspeak/svelte-motion'
28
- *
29
- * const [x, cycleX] = useCycle(0, 50, 100)
30
- * </script>
31
- *
32
- * <motion.div animate={{ x: $x }} onclick={() => cycleX()} />
33
- * ```
34
- */
35
- export declare const useCycle: <T>(...items: T[]) => CycleState<T>;
@@ -1,48 +0,0 @@
1
- import { wrap } from 'motion';
2
- import { writable } from 'svelte/store';
3
- /**
4
- * Cycles through a series of values. Mirrors Framer Motion's `useCycle`.
5
- *
6
- * Returns a tuple `[value, cycle]`:
7
- *
8
- * - `value` is a Svelte readable store of the current item; subscribe with
9
- * `$value` in templates.
10
- * - `cycle()` advances to the next item, wrapping back to index `0` when it
11
- * passes the end.
12
- * - `cycle(i)` jumps to the item at index `i`. The index is taken as-is to
13
- * match `framer-motion` &mdash; out-of-range values yield `items[i]`, which
14
- * may be `undefined`.
15
- *
16
- * Calls that resolve to the current index are no-ops and do not notify
17
- * subscribers, matching React `useState`'s `Object.is` bail-out semantics.
18
- *
19
- * @param items - Items to cycle through. Must include at least one item.
20
- * @returns A `[Readable<T>, Cycle]` tuple.
21
- * @see https://motion.dev/docs/react-use-cycle
22
- *
23
- * @example
24
- * ```svelte
25
- * <script>
26
- * import { motion, useCycle } from '@humanspeak/svelte-motion'
27
- *
28
- * const [x, cycleX] = useCycle(0, 50, 100)
29
- * </script>
30
- *
31
- * <motion.div animate={{ x: $x }} onclick={() => cycleX()} />
32
- * ```
33
- */
34
- export const useCycle = (...items) => {
35
- if (items.length === 0) {
36
- throw new Error('useCycle requires at least one item');
37
- }
38
- let index = 0;
39
- const store = writable(items[0]);
40
- const cycle = (next) => {
41
- const target = typeof next === 'number' ? next : wrap(0, items.length, index + 1);
42
- if (target === index)
43
- return;
44
- index = target;
45
- store.set(items[target]);
46
- };
47
- return [{ subscribe: store.subscribe }, cycle];
48
- };
@@ -1,51 +0,0 @@
1
- import { type Readable } from 'svelte/store';
2
- /**
3
- * Spring configuration options.
4
- *
5
- * This is a minimal subset modeled after Motion's spring transition options.
6
- * Values are tuned for sensible defaults, not parity.
7
- *
8
- * @typedef {Object} SpringOptions
9
- * @property {number=} stiffness Spring stiffness (higher = snappier). Default 170.
10
- * @property {number=} damping Spring damping (higher = less oscillation). Default 26.
11
- * @property {number=} mass Mass of the object. Default 1.
12
- * @property {number=} restDelta Threshold for absolute position delta to stop. Default 0.01.
13
- * @property {number=} restSpeed Threshold for velocity magnitude to stop. Default 0.01.
14
- */
15
- export type SpringOptions = {
16
- stiffness?: number;
17
- damping?: number;
18
- mass?: number;
19
- restDelta?: number;
20
- restSpeed?: number;
21
- };
22
- /**
23
- * Function type for updating the spring's target with animation.
24
- *
25
- * @param v New target value to animate towards (number or unit string).
26
- */
27
- type SetType = (v: number | string) => void;
28
- /**
29
- * Function type for immediately setting the spring's value without animation.
30
- *
31
- * @param v New value to set instantly (number or unit string).
32
- */
33
- type JumpType = (v: number | string) => void;
34
- /**
35
- * Creates a spring-animated readable store. The store exposes `set` to
36
- * animate towards a target, or `jump` to immediately set the value without
37
- * animation. When constructed with another readable store, the spring
38
- * automatically follows it.
39
- *
40
- * This is SSR-safe: On the server it returns a static store and no timers run.
41
- *
42
- * @template T
43
- * @param {number|string|Readable<number|string>} source Initial value or a source store to follow.
44
- * @param {SpringOptions=} options Spring configuration.
45
- * @returns {Readable<number|string> & { set: (v: number|string) => void; jump: (v: number|string) => void; }}
46
- */
47
- export declare const useSpring: (source: number | string | Readable<number | string>, options?: SpringOptions) => Readable<number | string> & {
48
- set: SetType;
49
- jump: JumpType;
50
- };
51
- export {};
@@ -1,157 +0,0 @@
1
- import { readable, writable } from 'svelte/store';
2
- /**
3
- * Parses a number or unit string into numeric value and unit.
4
- * @param {number|string} v The input value.
5
- * @returns {UnitValue} Parsed value and unit.
6
- * @private
7
- */
8
- const parseUnit = (v) => {
9
- if (typeof v === 'number')
10
- return { value: v, unit: '' };
11
- const match = String(v).match(/^(-?\d*\.?\d+)(.*)$/);
12
- if (!match || !match[1])
13
- return { value: 0, unit: '' };
14
- const parsed = Number.parseFloat(match[1]);
15
- if (!Number.isFinite(parsed))
16
- return { value: 0, unit: '' };
17
- const unit = match[2] ?? '';
18
- return { value: parsed, unit };
19
- };
20
- /**
21
- * Formats a numeric value with a unit.
22
- * @param {number} n Numeric value.
23
- * @param {string} unit Unit suffix.
24
- * @returns {number|string} Number or string with unit.
25
- * @private
26
- */
27
- const formatUnit = (n, unit) => (unit ? `${n}${unit}` : n);
28
- /**
29
- * Creates a spring-animated readable store. The store exposes `set` to
30
- * animate towards a target, or `jump` to immediately set the value without
31
- * animation. When constructed with another readable store, the spring
32
- * automatically follows it.
33
- *
34
- * This is SSR-safe: On the server it returns a static store and no timers run.
35
- *
36
- * @template T
37
- * @param {number|string|Readable<number|string>} source Initial value or a source store to follow.
38
- * @param {SpringOptions=} options Spring configuration.
39
- * @returns {Readable<number|string> & { set: (v: number|string) => void; jump: (v: number|string) => void; }}
40
- */
41
- export const useSpring = (source, options = {}) => {
42
- if (typeof window === 'undefined') {
43
- // Derive best-effort initial value for SSR to avoid hydration mismatch
44
- let initial = 0;
45
- if (typeof source === 'number' || typeof source === 'string') {
46
- initial = source;
47
- }
48
- else if (source && typeof source === 'object') {
49
- const anySource = source;
50
- if (typeof anySource.get === 'function') {
51
- const v = anySource.get();
52
- if (typeof v === 'number' || typeof v === 'string')
53
- initial = v;
54
- }
55
- else if (typeof anySource.value === 'number' || typeof anySource.value === 'string') {
56
- initial = anySource.value;
57
- }
58
- }
59
- const store = readable(initial, () => { });
60
- store.set = () => { };
61
- store.jump = () => { };
62
- return store;
63
- }
64
- const { stiffness = 170, damping = 26, mass = 1, restDelta = 0.01, restSpeed = 0.01 } = options;
65
- const state = {
66
- current: parseUnit(typeof source === 'object' ? 0 : source),
67
- target: parseUnit(typeof source === 'object' ? 0 : source)
68
- };
69
- const unit = state.current.unit || state.target.unit;
70
- const store = writable(formatUnit(state.current.value, unit));
71
- let raf = 0;
72
- let lastTime = 0;
73
- let velocity = 0;
74
- const step = (t) => {
75
- if (!lastTime)
76
- lastTime = t;
77
- // Clamp dt to a safe range to avoid instability across large time gaps
78
- const dt = Math.min(0.1, Math.max(0.001, (t - lastTime) / 1000));
79
- lastTime = t;
80
- const displacement = state.current.value - state.target.value;
81
- // Spring force based on Hooke's Law: F = -k x; damping force: -c v
82
- const spring = -stiffness * displacement;
83
- const damper = -damping * velocity;
84
- const accel = (spring + damper) / mass;
85
- velocity += accel * dt;
86
- state.current.value += velocity * dt;
87
- const isNoVelocity = Math.abs(velocity) <= restSpeed;
88
- const isNoDisplacement = Math.abs(state.current.value - state.target.value) <= restDelta;
89
- const done = isNoVelocity && isNoDisplacement;
90
- if (done) {
91
- state.current.value = state.target.value;
92
- store.set(formatUnit(state.current.value, unit));
93
- raf = 0;
94
- lastTime = 0;
95
- return;
96
- }
97
- store.set(formatUnit(state.current.value, unit));
98
- raf = requestAnimationFrame(step);
99
- };
100
- const start = () => {
101
- if (raf)
102
- return;
103
- raf = requestAnimationFrame(step);
104
- };
105
- const api = {
106
- set: (v) => {
107
- state.target = parseUnit(v);
108
- start();
109
- },
110
- jump: (v) => {
111
- state.current = parseUnit(v);
112
- state.target = parseUnit(v);
113
- velocity = 0;
114
- store.set(formatUnit(state.current.value, state.current.unit || state.target.unit));
115
- }
116
- };
117
- // If following another store, subscribe and forward values to set()
118
- if (typeof source === 'object' && 'subscribe' in source) {
119
- let followSource = true;
120
- const unsub = source.subscribe((v) => api.set(v));
121
- const wrapped = readable(formatUnit(state.current.value, unit), (set) => {
122
- const sub = store.subscribe(set);
123
- return () => {
124
- sub();
125
- unsub();
126
- followSource = false;
127
- if (raf)
128
- cancelAnimationFrame(raf);
129
- };
130
- });
131
- wrapped.set = (v) => {
132
- if (followSource)
133
- unsub();
134
- followSource = false;
135
- api.set(v);
136
- };
137
- wrapped.jump = (v) => {
138
- if (followSource)
139
- unsub();
140
- followSource = false;
141
- api.jump(v);
142
- };
143
- return wrapped;
144
- }
145
- // Standard readable wrapping internal writable
146
- const wrapped = readable(formatUnit(state.current.value, unit), (set) => {
147
- const sub = store.subscribe(set);
148
- return () => {
149
- sub();
150
- if (raf)
151
- cancelAnimationFrame(raf);
152
- };
153
- });
154
- wrapped.set = api.set;
155
- wrapped.jump = api.jump;
156
- return wrapped;
157
- };