@humanspeak/svelte-motion 0.4.9 → 0.5.0

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.
@@ -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
+ };
@@ -1,5 +1,6 @@
1
1
  import { type FollowValueOptions, type MotionValue, type SpringOptions } from 'motion-dom';
2
2
  import { type Readable } from 'svelte/store';
3
+ import { type AugmentedMotionValue } from './augmentMotionValue.svelte.js';
3
4
  /**
4
5
  * Spring + follow options for {@link useSpring}.
5
6
  *
@@ -25,12 +26,7 @@ export type UseSpringOptions = SpringOptions & Pick<FollowValueOptions, 'skipIni
25
26
  * spring be used with `$spring` template syntax, `get(spring)`, and as a
26
27
  * dependency in `useTransform`'s function form.
27
28
  */
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
- };
29
+ export type SpringMotionValue<T extends number | string> = AugmentedMotionValue<T>;
34
30
  /**
35
31
  * Creates a spring-animated `MotionValue`.
36
32
  *
@@ -1,26 +1,16 @@
1
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
- };
2
+ import {} from 'svelte/store';
3
+ import { augmentMotionValue, isSvelteReadable, sampleSource } from './augmentMotionValue.svelte.js';
14
4
  export function useSpring(source, options = {}) {
15
5
  // SSR: return a static MotionValue with no animation. Reads return the
16
6
  // best-effort initial value; .set / .jump become no-ops to avoid drifting
17
7
  // away from the server-rendered snapshot.
18
8
  if (typeof window === 'undefined') {
19
- const initial = readInitial(source);
9
+ const initial = sampleSource(source);
20
10
  const ssrValue = motionValue(initial);
21
11
  ssrValue.set = () => undefined;
22
12
  ssrValue.jump = () => undefined;
23
- return augmentForSvelte(ssrValue, () => undefined);
13
+ return augmentMotionValue(ssrValue);
24
14
  }
25
15
  // Resolve initial + follow source.
26
16
  let followSource;
@@ -32,7 +22,7 @@ export function useSpring(source, options = {}) {
32
22
  else if (isSvelteReadable(source)) {
33
23
  // Bridge a Svelte readable into a MotionValue so attachFollow can
34
24
  // track it. Synchronous initial sample comes from svelte/store's get().
35
- const initialFromReadable = get(source);
25
+ const initialFromReadable = sampleSource(source);
36
26
  svelteBridge = motionValue(initialFromReadable);
37
27
  cleanupReadableBridge = source.subscribe((v) => {
38
28
  // The Svelte readable contract calls the subscriber synchronously
@@ -60,59 +50,5 @@ export function useSpring(source, options = {}) {
60
50
  svelteBridge?.destroy();
61
51
  };
62
52
  $effect(() => () => value.destroy());
63
- return augmentForSvelte(value, dispose);
53
+ return augmentMotionValue(value, dispose);
64
54
  }
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
- };
@@ -0,0 +1,47 @@
1
+ import { type AugmentedMotionValue } from './augmentMotionValue.svelte.js';
2
+ /**
3
+ * Returns an augmented `MotionValue<number>` that ticks once per render
4
+ * frame with the milliseconds elapsed since the value was created.
5
+ *
6
+ * Mirrors React framer-motion's `useTime` 1:1: a `MotionValue<number>`
7
+ * driven by motion-dom's `frame.update(tick, true)` keep-alive callback.
8
+ * The frame loop dedupes per-frame work across all motion-dom consumers,
9
+ * so multiple `useTime()` calls share the same render schedule.
10
+ *
11
+ * Two modes:
12
+ *
13
+ * - **Unique timeline** — `useTime()` starts its own keep-alive callback.
14
+ * The motion value and the callback are torn down when the surrounding
15
+ * `$effect` scope unmounts.
16
+ * - **Shared timeline** — `useTime(id)` callers passing the same `id` all
17
+ * observe a single shared frame-loop callback. Each call still returns
18
+ * an independent motion value (so destroying one consumer's value
19
+ * doesn't ripple to others) but the values stay in lockstep. The
20
+ * shared callback cancels the moment the last consumer unmounts; the
21
+ * next `useTime(id)` call restarts it.
22
+ *
23
+ * The result is augmented with a `$state`-backed `.current` getter and a
24
+ * `.subscribe` shim — it composes with `useTransform`, `useSpring`, and
25
+ * the rest of the Tier 2 surface.
26
+ *
27
+ * Lifecycle: must be called during component initialization. SSR-safe:
28
+ * returns a static `motionValue(0)` with no frame loop on the server.
29
+ *
30
+ * @param id Optional timeline identifier for sharing across components.
31
+ * @returns A `MotionValue<number>` with `.current` and `.subscribe`.
32
+ *
33
+ * @example
34
+ * ```svelte
35
+ * <script>
36
+ * import { useTime, useTransform } from '@humanspeak/svelte-motion'
37
+ *
38
+ * const time = useTime()
39
+ * const rotate = useTransform(time, [0, 4000], [0, 360], { clamp: false })
40
+ * </script>
41
+ *
42
+ * <div style="transform: rotate({rotate.current}deg)">↻</div>
43
+ * ```
44
+ *
45
+ * @see https://motion.dev/docs/react-use-time
46
+ */
47
+ export declare const useTime: (id?: string) => AugmentedMotionValue<number>;