@humanspeak/svelte-motion 0.4.9 → 0.5.1

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.
Files changed (36) hide show
  1. package/dist/index.d.ts +14 -7
  2. package/dist/index.js +7 -6
  3. package/dist/utils/attachable.js +14 -9
  4. package/dist/utils/augmentMotionValue.svelte.d.ts +156 -0
  5. package/dist/utils/augmentMotionValue.svelte.js +193 -0
  6. package/dist/utils/dom.d.ts +13 -0
  7. package/dist/utils/dom.js +19 -0
  8. package/dist/utils/followValue.svelte.d.ts +84 -0
  9. package/dist/utils/followValue.svelte.js +51 -0
  10. package/dist/utils/motionTemplate.svelte.d.ts +53 -0
  11. package/dist/utils/motionTemplate.svelte.js +78 -0
  12. package/dist/utils/motionValue.svelte.d.ts +61 -0
  13. package/dist/utils/motionValue.svelte.js +49 -0
  14. package/dist/utils/scroll.svelte.d.ts +91 -0
  15. package/dist/utils/scroll.svelte.js +259 -0
  16. package/dist/utils/spring.svelte.d.ts +26 -31
  17. package/dist/utils/spring.svelte.js +8 -116
  18. package/dist/utils/time.svelte.d.ts +47 -0
  19. package/dist/utils/time.svelte.js +128 -0
  20. package/dist/utils/transform.svelte.d.ts +170 -0
  21. package/dist/utils/transform.svelte.js +189 -0
  22. package/dist/utils/velocity.svelte.d.ts +61 -0
  23. package/dist/utils/velocity.svelte.js +132 -0
  24. package/package.json +1 -1
  25. package/dist/utils/motionTemplate.d.ts +0 -21
  26. package/dist/utils/motionTemplate.js +0 -33
  27. package/dist/utils/motionValue.d.ts +0 -6
  28. package/dist/utils/motionValue.js +0 -13
  29. package/dist/utils/scroll.d.ts +0 -63
  30. package/dist/utils/scroll.js +0 -79
  31. package/dist/utils/time.d.ts +0 -14
  32. package/dist/utils/time.js +0 -68
  33. package/dist/utils/transform.d.ts +0 -74
  34. package/dist/utils/transform.js +0 -211
  35. package/dist/utils/velocity.d.ts +0 -15
  36. package/dist/utils/velocity.js +0 -62
@@ -0,0 +1,53 @@
1
+ import { type Readable } from 'svelte/store';
2
+ import { type AugmentedMotionValue } from './augmentMotionValue.svelte.js';
3
+ /**
4
+ * Input to {@link useMotionTemplate}'s interpolation slots: a motion-dom
5
+ * `MotionValue`, a Svelte readable, or a plain `number` / `string` literal.
6
+ * Mirrors framer-motion's `useMotionTemplate` signature.
7
+ */
8
+ export type MotionTemplateInput = AugmentedMotionValue<number | string> | Readable<number | string> | number | string;
9
+ /**
10
+ * Tagged template literal that builds an augmented `MotionValue<string>`
11
+ * from interpolated motion values, Svelte readables, and plain literals.
12
+ *
13
+ * Mirrors framer-motion 1:1: returns a real motion-dom `MotionValue<string>`
14
+ * (via `transformValue`) that auto-tracks every `MotionValue.get()` called
15
+ * during template composition. Whenever any tracked input emits, the
16
+ * composer reruns and writes the new string into the result value.
17
+ *
18
+ * Svelte-readable slots are sampled via `svelte/store`'s `get()` inside the
19
+ * composer so they re-emit when adjacent motion values trigger a recompute.
20
+ * Plain `number` / `string` slots are stringified inline.
21
+ *
22
+ * The result is augmented with a `$state`-backed `.current` getter and a
23
+ * Svelte readable `.subscribe` shim so it composes with the rest of the
24
+ * Tier 2 surface and reads reactively in Svelte 5 scopes.
25
+ *
26
+ * Lifecycle: must be called during component initialization. motion-dom
27
+ * cleans up the change-subscriptions when the result `MotionValue` is
28
+ * destroyed; we wire that destroy to the surrounding `$effect`.
29
+ *
30
+ * SSR-safe: motion-dom's `transformValue` works without DOM access (no
31
+ * timers, no listeners). On the server the result is a static augmented
32
+ * motion value with no `$effect` registered.
33
+ *
34
+ * @param strings Static template string parts (supplied by the tagged-template syntax).
35
+ * @param values Interpolated motion values, Svelte readables, or literals.
36
+ * @returns An `AugmentedMotionValue<string>` with the composed template.
37
+ *
38
+ * @example
39
+ * ```svelte
40
+ * <script>
41
+ * import { useSpring, useTransform, useMotionTemplate } from '@humanspeak/svelte-motion'
42
+ *
43
+ * const x = useSpring(0)
44
+ * const blur = useTransform(x, [-100, 0, 100], [10, 0, 10])
45
+ * const filter = useMotionTemplate`blur(${blur}px)`
46
+ * </script>
47
+ *
48
+ * <div style="filter: {filter.current}">Animated blur</div>
49
+ * ```
50
+ *
51
+ * @see https://motion.dev/docs/react-use-motion-template
52
+ */
53
+ export declare const useMotionTemplate: (strings: TemplateStringsArray, ...values: MotionTemplateInput[]) => AugmentedMotionValue<string>;
@@ -0,0 +1,78 @@
1
+ import { isMotionValue, transformValue } from 'motion-dom';
2
+ import {} from 'svelte/store';
3
+ import { augmentMotionValue, sampleSource } from './augmentMotionValue.svelte.js';
4
+ /**
5
+ * Tagged template literal that builds an augmented `MotionValue<string>`
6
+ * from interpolated motion values, Svelte readables, and plain literals.
7
+ *
8
+ * Mirrors framer-motion 1:1: returns a real motion-dom `MotionValue<string>`
9
+ * (via `transformValue`) that auto-tracks every `MotionValue.get()` called
10
+ * during template composition. Whenever any tracked input emits, the
11
+ * composer reruns and writes the new string into the result value.
12
+ *
13
+ * Svelte-readable slots are sampled via `svelte/store`'s `get()` inside the
14
+ * composer so they re-emit when adjacent motion values trigger a recompute.
15
+ * Plain `number` / `string` slots are stringified inline.
16
+ *
17
+ * The result is augmented with a `$state`-backed `.current` getter and a
18
+ * Svelte readable `.subscribe` shim so it composes with the rest of the
19
+ * Tier 2 surface and reads reactively in Svelte 5 scopes.
20
+ *
21
+ * Lifecycle: must be called during component initialization. motion-dom
22
+ * cleans up the change-subscriptions when the result `MotionValue` is
23
+ * destroyed; we wire that destroy to the surrounding `$effect`.
24
+ *
25
+ * SSR-safe: motion-dom's `transformValue` works without DOM access (no
26
+ * timers, no listeners). On the server the result is a static augmented
27
+ * motion value with no `$effect` registered.
28
+ *
29
+ * @param strings Static template string parts (supplied by the tagged-template syntax).
30
+ * @param values Interpolated motion values, Svelte readables, or literals.
31
+ * @returns An `AugmentedMotionValue<string>` with the composed template.
32
+ *
33
+ * @example
34
+ * ```svelte
35
+ * <script>
36
+ * import { useSpring, useTransform, useMotionTemplate } from '@humanspeak/svelte-motion'
37
+ *
38
+ * const x = useSpring(0)
39
+ * const blur = useTransform(x, [-100, 0, 100], [10, 0, 10])
40
+ * const filter = useMotionTemplate`blur(${blur}px)`
41
+ * </script>
42
+ *
43
+ * <div style="filter: {filter.current}">Animated blur</div>
44
+ * ```
45
+ *
46
+ * @see https://motion.dev/docs/react-use-motion-template
47
+ */
48
+ export const useMotionTemplate = (strings, ...values) => {
49
+ const numFragments = strings.length;
50
+ const buildValue = () => {
51
+ let output = '';
52
+ for (let i = 0; i < numFragments; i++) {
53
+ output += strings[i] ?? '';
54
+ if (i >= values.length)
55
+ continue;
56
+ const value = values[i];
57
+ // motion-dom's collectMotionValues session inside transformValue
58
+ // auto-discovers MotionValue deps via `.get()`. Readables don't
59
+ // participate — they're sampled inline via get(); they only
60
+ // re-emit if some adjacent motion value triggers a recompute.
61
+ if (isMotionValue(value)) {
62
+ output += String(value.get());
63
+ }
64
+ else if (value && typeof value === 'object' && 'subscribe' in value) {
65
+ output += String(sampleSource(value));
66
+ }
67
+ else {
68
+ output += String(value);
69
+ }
70
+ }
71
+ return output;
72
+ };
73
+ const result = transformValue(buildValue);
74
+ if (typeof window !== 'undefined') {
75
+ $effect(() => () => result.destroy());
76
+ }
77
+ return augmentMotionValue(result);
78
+ };
@@ -0,0 +1,61 @@
1
+ import { type MotionValue as MotionDomMotionValue } from 'motion-dom';
2
+ import { type AugmentedMotionValue } from './augmentMotionValue.svelte.js';
3
+ /**
4
+ * The shape returned by {@link useMotionValue}: a real motion-dom
5
+ * `MotionValue<T>` (so it composes with `animate()`, `useTransform`,
6
+ * `useSpring`, etc. and passes `isMotionValue`) plus a Svelte 5 reactive
7
+ * `.current` getter and a Svelte readable store `.subscribe` shim.
8
+ *
9
+ * @template T The value type — typically `number` or `string`.
10
+ * @see {@link AugmentedMotionValue}
11
+ */
12
+ export type MotionValue<T = number> = AugmentedMotionValue<T>;
13
+ /**
14
+ * Re-export of motion-dom's raw `MotionValue` type for cases where the
15
+ * un-augmented shape is needed (e.g. typing follow sources, function
16
+ * dependencies). Most call sites should prefer {@link MotionValue}.
17
+ */
18
+ export type RawMotionValue<T = number> = MotionDomMotionValue<T>;
19
+ /**
20
+ * Creates a tracked, mutable value backed by motion-dom's `MotionValue`.
21
+ *
22
+ * Use `.set(v)` to write the value imperatively, `.get()` to sample it
23
+ * imperatively, and `.current` to read it inside Svelte 5 reactive scopes
24
+ * (templates, `$derived`, `$effect`). The same value also implements the
25
+ * Svelte readable store contract via `.subscribe(run)`, so legacy `$mv`
26
+ * template syntax and `svelte/store`'s `get()` keep working.
27
+ *
28
+ * Returned object is a real motion-dom `MotionValue` — it composes with
29
+ * `useTransform`, `useSpring`, `useVelocity`, the `animate()` driver, and
30
+ * passes `isMotionValue`. Unlike `useSpring`, writes are immediate — there
31
+ * is no follow source and no animation.
32
+ *
33
+ * Lifecycle: must be called during component initialization. Cleanup is
34
+ * registered via `$effect`; motion-dom's internal listeners and animation
35
+ * subscriptions are released when the surrounding component / effect tears
36
+ * down. Call `.destroy()` to clean up early.
37
+ *
38
+ * SSR-safe: motion-dom's `motionValue` runs without DOM access; on the
39
+ * server reads return the initial value and writes still work, with no
40
+ * timers or listeners attached.
41
+ *
42
+ * @template T The value type — typically `number` or `string`.
43
+ * @param initial The starting value.
44
+ * @returns A `MotionValue<T>` augmented with `.current` and `.subscribe`.
45
+ *
46
+ * @example
47
+ * ```svelte
48
+ * <script lang="ts">
49
+ * import { useMotionValue, useTransform } from '@humanspeak/svelte-motion'
50
+ *
51
+ * const x = useMotionValue(0)
52
+ * const opacity = useTransform(x, [0, 200], [0, 1])
53
+ * </script>
54
+ *
55
+ * <input type="range" min="0" max="200" oninput={(e) => x.set(+e.currentTarget.value)} />
56
+ * <div style="opacity: {opacity.current}">x = {x.current}</div>
57
+ * ```
58
+ *
59
+ * @see https://motion.dev/docs/react-motion-value
60
+ */
61
+ export declare const useMotionValue: <T = number>(initial: T) => MotionValue<T>;
@@ -0,0 +1,49 @@
1
+ import { motionValue } from 'motion-dom';
2
+ import { augmentMotionValue } from './augmentMotionValue.svelte.js';
3
+ /**
4
+ * Creates a tracked, mutable value backed by motion-dom's `MotionValue`.
5
+ *
6
+ * Use `.set(v)` to write the value imperatively, `.get()` to sample it
7
+ * imperatively, and `.current` to read it inside Svelte 5 reactive scopes
8
+ * (templates, `$derived`, `$effect`). The same value also implements the
9
+ * Svelte readable store contract via `.subscribe(run)`, so legacy `$mv`
10
+ * template syntax and `svelte/store`'s `get()` keep working.
11
+ *
12
+ * Returned object is a real motion-dom `MotionValue` — it composes with
13
+ * `useTransform`, `useSpring`, `useVelocity`, the `animate()` driver, and
14
+ * passes `isMotionValue`. Unlike `useSpring`, writes are immediate — there
15
+ * is no follow source and no animation.
16
+ *
17
+ * Lifecycle: must be called during component initialization. Cleanup is
18
+ * registered via `$effect`; motion-dom's internal listeners and animation
19
+ * subscriptions are released when the surrounding component / effect tears
20
+ * down. Call `.destroy()` to clean up early.
21
+ *
22
+ * SSR-safe: motion-dom's `motionValue` runs without DOM access; on the
23
+ * server reads return the initial value and writes still work, with no
24
+ * timers or listeners attached.
25
+ *
26
+ * @template T The value type — typically `number` or `string`.
27
+ * @param initial The starting value.
28
+ * @returns A `MotionValue<T>` augmented with `.current` and `.subscribe`.
29
+ *
30
+ * @example
31
+ * ```svelte
32
+ * <script lang="ts">
33
+ * import { useMotionValue, useTransform } from '@humanspeak/svelte-motion'
34
+ *
35
+ * const x = useMotionValue(0)
36
+ * const opacity = useTransform(x, [0, 200], [0, 1])
37
+ * </script>
38
+ *
39
+ * <input type="range" min="0" max="200" oninput={(e) => x.set(+e.currentTarget.value)} />
40
+ * <div style="opacity: {opacity.current}">x = {x.current}</div>
41
+ * ```
42
+ *
43
+ * @see https://motion.dev/docs/react-motion-value
44
+ */
45
+ export const useMotionValue = (initial) => {
46
+ const value = motionValue(initial);
47
+ $effect(() => () => value.destroy());
48
+ return augmentMotionValue(value);
49
+ };
@@ -0,0 +1,91 @@
1
+ import { type AugmentedMotionValue } from './augmentMotionValue.svelte.js';
2
+ import { type ElementOrGetter } from './dom.js';
3
+ /**
4
+ * A scroll offset edge defined as a string (e.g. `"start"`, `"end"`,
5
+ * `"center"`) or a number (0–1 progress). Each offset entry is a pair of
6
+ * `[target, container]`.
7
+ *
8
+ * Intentionally looser than motion's internal `ScrollOffset` type — that
9
+ * uses template-literal types (`` `${Edge} ${Edge}` ``) we don't want to
10
+ * surface to consumers. The runtime call paths through `scroll(...)` cast
11
+ * `options.offset as never` to bridge the two; safe because every shape our
12
+ * type allows is structurally a member of motion's union.
13
+ */
14
+ type ScrollOffset = Array<[number | string, number | string]> | string[];
15
+ /**
16
+ * Options accepted by {@link useScroll}.
17
+ */
18
+ export type UseScrollOptions = {
19
+ /** Scrollable container to track. Defaults to the page. Accepts an element or a getter function. */
20
+ container?: ElementOrGetter;
21
+ /** Target element to track position of within the container. Accepts an element or a getter function. */
22
+ target?: ElementOrGetter;
23
+ /** Scroll offset configuration for element position tracking. */
24
+ offset?: ScrollOffset;
25
+ /** Which axis to use for the single-axis `progress` value supplied to `scroll()`. */
26
+ axis?: 'x' | 'y';
27
+ };
28
+ /**
29
+ * Return type of {@link useScroll} — four motion-dom `MotionValue<number>`s
30
+ * representing scroll position and normalised progress for both axes, each
31
+ * augmented with `.current` + `.subscribe`.
32
+ */
33
+ export type UseScrollReturn = {
34
+ scrollX: AugmentedMotionValue<number>;
35
+ scrollY: AugmentedMotionValue<number>;
36
+ scrollXProgress: AugmentedMotionValue<number>;
37
+ scrollYProgress: AugmentedMotionValue<number>;
38
+ };
39
+ /**
40
+ * Creates scroll-linked motion values for building scroll-driven animations
41
+ * such as progress indicators and parallax effects.
42
+ *
43
+ * Returns four `MotionValue<number>`s: `scrollX` / `scrollY` (current
44
+ * position in px) and `scrollXProgress` / `scrollYProgress` (0–1
45
+ * normalised). Each is a real motion-dom `MotionValue` augmented with a
46
+ * `$state`-backed `.current` getter and a `.subscribe` shim, so they
47
+ * compose with `useTransform`, `useSpring`, and the rest of the Tier 2
48
+ * surface, and they read reactively in Svelte 5 templates.
49
+ *
50
+ * **GPU-accelerated when supported.** On browsers that implement CSS
51
+ * scroll-timeline (no `target`) or view-timeline (with a `target` and a
52
+ * preset `offset`), the *Progress motion values run on the compositor
53
+ * thread via WAAPI — no per-frame JS callback. The non-progress
54
+ * `scrollX` / `scrollY` motion values always use the JS-driven `scroll()`
55
+ * primitive since the absolute pixel offset isn't directly available from
56
+ * native timelines.
57
+ *
58
+ * `container` and `target` accept either an `HTMLElement` directly or a
59
+ * getter `() => HTMLElement | undefined`. The getter form is the right
60
+ * choice with `bind:this`. Element resolution is deferred to a microtask
61
+ * (matches React framer-motion 1:1, faster than rAF polling), so a getter
62
+ * that hasn't hydrated yet is retried as soon as Svelte's mount tick
63
+ * settles it.
64
+ *
65
+ * Lifecycle: the underlying `scroll()` observer and any accelerate factory
66
+ * attach at mount via `$effect` and detach at unmount, regardless of how
67
+ * many consumers are reading the values. The four motion values are torn
68
+ * down at the same time.
69
+ *
70
+ * SSR-safe: returns four static `motionValue(0)`s with no scroll observer
71
+ * on the server.
72
+ *
73
+ * @param options Optional scroll tracking configuration.
74
+ * @returns Four `MotionValue<number>`s — `scrollX`, `scrollY`, `scrollXProgress`, `scrollYProgress`.
75
+ *
76
+ * @example
77
+ * ```svelte
78
+ * <script>
79
+ * import { useScroll, useSpring } from '@humanspeak/svelte-motion'
80
+ *
81
+ * const { scrollYProgress } = useScroll()
82
+ * const scaleX = useSpring(scrollYProgress)
83
+ * </script>
84
+ *
85
+ * <div style="transform: scaleX({scaleX.current}); transform-origin: left;" />
86
+ * ```
87
+ *
88
+ * @see https://motion.dev/docs/react-use-scroll
89
+ */
90
+ export declare const useScroll: (options?: UseScrollOptions) => UseScrollReturn;
91
+ export {};
@@ -0,0 +1,259 @@
1
+ import { scroll } from 'motion';
2
+ import { cancelMicrotask, microtask, motionValue, supportsScrollTimeline, supportsViewTimeline } from 'motion-dom';
3
+ import { augmentMotionValue } from './augmentMotionValue.svelte.js';
4
+ import { isRefPending, resolveElement } from './dom.js';
5
+ /**
6
+ * Named view-timeline presets — mirror framer-motion's ScrollOffset table.
7
+ * Each entry is a pair of `[target, container]` intersection points that
8
+ * defines a phase of the scroll relationship.
9
+ */
10
+ const VIEW_TIMELINE_PRESETS = [
11
+ [
12
+ [
13
+ [0, 1],
14
+ [1, 1]
15
+ ],
16
+ 'entry'
17
+ ],
18
+ [
19
+ [
20
+ [0, 0],
21
+ [1, 0]
22
+ ],
23
+ 'exit'
24
+ ],
25
+ [
26
+ [
27
+ [1, 0],
28
+ [0, 1]
29
+ ],
30
+ 'cover'
31
+ ],
32
+ [
33
+ [
34
+ [0, 0],
35
+ [1, 1]
36
+ ],
37
+ 'contain'
38
+ ]
39
+ ];
40
+ const PROGRESS_BY_STRING = { start: 0, end: 1 };
41
+ /**
42
+ * Parse `"start end"` / `"end start"` / etc. into an explicit
43
+ * `[target, container]` intersection pair.
44
+ */
45
+ const parseStringOffset = (s) => {
46
+ const parts = s.trim().split(/\s+/);
47
+ if (parts.length !== 2)
48
+ return undefined;
49
+ const a = PROGRESS_BY_STRING[parts[0]];
50
+ const b = PROGRESS_BY_STRING[parts[1]];
51
+ if (a === undefined || b === undefined)
52
+ return undefined;
53
+ return [a, b];
54
+ };
55
+ /**
56
+ * Normalize a {@link ScrollOffset} into an array of explicit intersection
57
+ * pairs, or `undefined` if the shape doesn't match the simple two-element
58
+ * preset form.
59
+ */
60
+ const normaliseOffset = (offset) => {
61
+ if (offset.length !== 2)
62
+ return undefined;
63
+ const result = [];
64
+ for (const item of offset) {
65
+ if (Array.isArray(item)) {
66
+ result.push(item);
67
+ }
68
+ else if (typeof item === 'string') {
69
+ const parsed = parseStringOffset(item);
70
+ if (!parsed)
71
+ return undefined;
72
+ result.push(parsed);
73
+ }
74
+ else {
75
+ return undefined;
76
+ }
77
+ }
78
+ return result;
79
+ };
80
+ const matchesPreset = (offset, preset) => {
81
+ const normalised = normaliseOffset(offset);
82
+ if (!normalised)
83
+ return false;
84
+ for (let i = 0; i < 2; i++) {
85
+ const o = normalised[i];
86
+ const p = preset[i];
87
+ if (o[0] !== p[0] || o[1] !== p[1])
88
+ return false;
89
+ }
90
+ return true;
91
+ };
92
+ /**
93
+ * Map a {@link ScrollOffset} to its corresponding CSS view-timeline range, if
94
+ * any. Returning `undefined` signals the caller to fall back to JS-driven
95
+ * scroll tracking. Mirrors framer-motion's `offsetToViewTimelineRange`.
96
+ */
97
+ const offsetToViewTimelineRange = (offset) => {
98
+ if (!offset)
99
+ return { rangeStart: 'contain 0%', rangeEnd: 'contain 100%' };
100
+ for (const [preset, name] of VIEW_TIMELINE_PRESETS) {
101
+ if (matchesPreset(offset, preset)) {
102
+ return { rangeStart: `${name} 0%`, rangeEnd: `${name} 100%` };
103
+ }
104
+ }
105
+ return undefined;
106
+ };
107
+ /**
108
+ * Whether this scroll configuration can be driven by a native CSS
109
+ * scroll-timeline / view-timeline. When `true`, the resulting motion value
110
+ * runs on the compositor thread without per-frame JS callbacks.
111
+ */
112
+ const canAccelerateScroll = (target, offset) => {
113
+ if (typeof window === 'undefined')
114
+ return false;
115
+ return target
116
+ ? supportsViewTimeline() && !!offsetToViewTimelineRange(offset)
117
+ : supportsScrollTimeline();
118
+ };
119
+ /**
120
+ * Build the AccelerateConfig for a progress motion value driven by a native
121
+ * scroll-timeline / view-timeline animation. Mirrors framer-motion's
122
+ * `makeAccelerateConfig` 1:1: the `factory` defers `scroll()` until refs
123
+ * hydrate via `microtask.read`, then attaches the animation; the `times` /
124
+ * `keyframes` / `ease` / `duration` describe the 0→1 linear mapping.
125
+ */
126
+ const makeAccelerateConfig = (axis, options) => ({
127
+ factory: (animation) => {
128
+ let cleanup;
129
+ const start = () => {
130
+ if (isRefPending(options.container) || isRefPending(options.target)) {
131
+ microtask.read(start);
132
+ return;
133
+ }
134
+ cleanup = scroll(animation, {
135
+ offset: options.offset,
136
+ axis,
137
+ container: resolveElement(options.container),
138
+ target: resolveElement(options.target)
139
+ });
140
+ };
141
+ microtask.read(start);
142
+ return () => {
143
+ cancelMicrotask(start);
144
+ cleanup?.();
145
+ };
146
+ },
147
+ times: [0, 1],
148
+ keyframes: [0, 1],
149
+ ease: (v) => v,
150
+ duration: 1
151
+ });
152
+ /**
153
+ * Creates scroll-linked motion values for building scroll-driven animations
154
+ * such as progress indicators and parallax effects.
155
+ *
156
+ * Returns four `MotionValue<number>`s: `scrollX` / `scrollY` (current
157
+ * position in px) and `scrollXProgress` / `scrollYProgress` (0–1
158
+ * normalised). Each is a real motion-dom `MotionValue` augmented with a
159
+ * `$state`-backed `.current` getter and a `.subscribe` shim, so they
160
+ * compose with `useTransform`, `useSpring`, and the rest of the Tier 2
161
+ * surface, and they read reactively in Svelte 5 templates.
162
+ *
163
+ * **GPU-accelerated when supported.** On browsers that implement CSS
164
+ * scroll-timeline (no `target`) or view-timeline (with a `target` and a
165
+ * preset `offset`), the *Progress motion values run on the compositor
166
+ * thread via WAAPI — no per-frame JS callback. The non-progress
167
+ * `scrollX` / `scrollY` motion values always use the JS-driven `scroll()`
168
+ * primitive since the absolute pixel offset isn't directly available from
169
+ * native timelines.
170
+ *
171
+ * `container` and `target` accept either an `HTMLElement` directly or a
172
+ * getter `() => HTMLElement | undefined`. The getter form is the right
173
+ * choice with `bind:this`. Element resolution is deferred to a microtask
174
+ * (matches React framer-motion 1:1, faster than rAF polling), so a getter
175
+ * that hasn't hydrated yet is retried as soon as Svelte's mount tick
176
+ * settles it.
177
+ *
178
+ * Lifecycle: the underlying `scroll()` observer and any accelerate factory
179
+ * attach at mount via `$effect` and detach at unmount, regardless of how
180
+ * many consumers are reading the values. The four motion values are torn
181
+ * down at the same time.
182
+ *
183
+ * SSR-safe: returns four static `motionValue(0)`s with no scroll observer
184
+ * on the server.
185
+ *
186
+ * @param options Optional scroll tracking configuration.
187
+ * @returns Four `MotionValue<number>`s — `scrollX`, `scrollY`, `scrollXProgress`, `scrollYProgress`.
188
+ *
189
+ * @example
190
+ * ```svelte
191
+ * <script>
192
+ * import { useScroll, useSpring } from '@humanspeak/svelte-motion'
193
+ *
194
+ * const { scrollYProgress } = useScroll()
195
+ * const scaleX = useSpring(scrollYProgress)
196
+ * </script>
197
+ *
198
+ * <div style="transform: scaleX({scaleX.current}); transform-origin: left;" />
199
+ * ```
200
+ *
201
+ * @see https://motion.dev/docs/react-use-scroll
202
+ */
203
+ export const useScroll = (options = {}) => {
204
+ const scrollX = motionValue(0);
205
+ const scrollY = motionValue(0);
206
+ const scrollXProgress = motionValue(0);
207
+ const scrollYProgress = motionValue(0);
208
+ // SSR: return static motion values with no observer and no $effect.
209
+ if (typeof window === 'undefined') {
210
+ return {
211
+ scrollX: augmentMotionValue(scrollX),
212
+ scrollY: augmentMotionValue(scrollY),
213
+ scrollXProgress: augmentMotionValue(scrollXProgress),
214
+ scrollYProgress: augmentMotionValue(scrollYProgress)
215
+ };
216
+ }
217
+ // The *Progress MVs accelerate to the compositor thread when the browser
218
+ // supports CSS scroll/view-timelines; the non-progress scrollX/scrollY
219
+ // MVs always need the JS callback for absolute pixel offsets.
220
+ if (canAccelerateScroll(options.target, options.offset)) {
221
+ scrollXProgress.accelerate = makeAccelerateConfig('x', options);
222
+ scrollYProgress.accelerate = makeAccelerateConfig('y', options);
223
+ }
224
+ let cleanup;
225
+ const start = () => {
226
+ if (isRefPending(options.container) || isRefPending(options.target)) {
227
+ microtask.read(start);
228
+ return;
229
+ }
230
+ cleanup = scroll((_progress, info) => {
231
+ scrollX.set(info.x.current);
232
+ scrollY.set(info.y.current);
233
+ scrollXProgress.set(info.x.progress);
234
+ scrollYProgress.set(info.y.progress);
235
+ }, {
236
+ container: resolveElement(options.container),
237
+ target: resolveElement(options.target),
238
+ offset: options.offset,
239
+ axis: options.axis
240
+ });
241
+ };
242
+ $effect(() => {
243
+ microtask.read(start);
244
+ return () => {
245
+ cancelMicrotask(start);
246
+ cleanup?.();
247
+ scrollX.destroy();
248
+ scrollY.destroy();
249
+ scrollXProgress.destroy();
250
+ scrollYProgress.destroy();
251
+ };
252
+ });
253
+ return {
254
+ scrollX: augmentMotionValue(scrollX),
255
+ scrollY: augmentMotionValue(scrollY),
256
+ scrollXProgress: augmentMotionValue(scrollXProgress),
257
+ scrollYProgress: augmentMotionValue(scrollYProgress)
258
+ };
259
+ };