@humanspeak/svelte-motion 0.4.8 → 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.
Files changed (49) hide show
  1. package/dist/html/_MotionContainer.svelte +8 -8
  2. package/dist/index.d.ts +17 -11
  3. package/dist/index.js +9 -9
  4. package/dist/utils/attachable.js +14 -9
  5. package/dist/utils/augmentMotionValue.svelte.d.ts +156 -0
  6. package/dist/utils/augmentMotionValue.svelte.js +193 -0
  7. package/dist/utils/booleanSnapshot.svelte.d.ts +37 -0
  8. package/dist/utils/booleanSnapshot.svelte.js +48 -0
  9. package/dist/utils/dom.d.ts +13 -0
  10. package/dist/utils/dom.js +19 -0
  11. package/dist/utils/inView.svelte.d.ts +209 -0
  12. package/dist/utils/inView.svelte.js +323 -0
  13. package/dist/utils/motionTemplate.svelte.d.ts +53 -0
  14. package/dist/utils/motionTemplate.svelte.js +78 -0
  15. package/dist/utils/motionValue.svelte.d.ts +61 -0
  16. package/dist/utils/motionValue.svelte.js +49 -0
  17. package/dist/utils/reducedMotion.svelte.d.ts +43 -0
  18. package/dist/utils/reducedMotion.svelte.js +80 -0
  19. package/dist/utils/reducedMotionConfig.svelte.d.ts +74 -0
  20. package/dist/utils/reducedMotionConfig.svelte.js +144 -0
  21. package/dist/utils/scroll.svelte.d.ts +91 -0
  22. package/dist/utils/scroll.svelte.js +259 -0
  23. package/dist/utils/spring.svelte.d.ts +2 -6
  24. package/dist/utils/spring.svelte.js +6 -70
  25. package/dist/utils/time.svelte.d.ts +47 -0
  26. package/dist/utils/time.svelte.js +128 -0
  27. package/dist/utils/transform.svelte.d.ts +170 -0
  28. package/dist/utils/transform.svelte.js +189 -0
  29. package/dist/utils/velocity.svelte.d.ts +61 -0
  30. package/dist/utils/velocity.svelte.js +132 -0
  31. package/package.json +1 -1
  32. package/dist/utils/inView.d.ts +0 -136
  33. package/dist/utils/inView.js +0 -266
  34. package/dist/utils/motionTemplate.d.ts +0 -21
  35. package/dist/utils/motionTemplate.js +0 -33
  36. package/dist/utils/motionValue.d.ts +0 -6
  37. package/dist/utils/motionValue.js +0 -13
  38. package/dist/utils/reducedMotion.d.ts +0 -20
  39. package/dist/utils/reducedMotion.js +0 -42
  40. package/dist/utils/reducedMotionConfig.d.ts +0 -39
  41. package/dist/utils/reducedMotionConfig.js +0 -92
  42. package/dist/utils/scroll.d.ts +0 -63
  43. package/dist/utils/scroll.js +0 -79
  44. package/dist/utils/time.d.ts +0 -14
  45. package/dist/utils/time.js +0 -68
  46. package/dist/utils/transform.d.ts +0 -74
  47. package/dist/utils/transform.js +0 -211
  48. package/dist/utils/velocity.d.ts +0 -15
  49. package/dist/utils/velocity.js +0 -62
@@ -8,7 +8,7 @@
8
8
  import {
9
9
  filterReducedMotionKeyframes,
10
10
  useReducedMotionConfig
11
- } from '../utils/reducedMotionConfig'
11
+ } from '../utils/reducedMotionConfig.svelte'
12
12
  import type {
13
13
  MotionProps,
14
14
  MotionTransition,
@@ -29,7 +29,7 @@
29
29
  import { attachWhileTap } from '../utils/interaction'
30
30
  import { attachWhileHover } from '../utils/hover'
31
31
  import { attachWhileFocus } from '../utils/focus'
32
- import { attachWhileInView } from '../utils/inView'
32
+ import { attachWhileInView } from '../utils/inView.svelte'
33
33
  import {
34
34
  measureRect,
35
35
  computeFlipTransforms,
@@ -57,7 +57,7 @@
57
57
  setCustomContext,
58
58
  getCustomContext
59
59
  } from '../components/variantContext.context'
60
- import { get, writable } from 'svelte/store'
60
+ import { writable } from 'svelte/store'
61
61
  import {
62
62
  transformSVGPathProperties,
63
63
  computeNormalizedSVGInitialAttrs,
@@ -130,11 +130,11 @@
130
130
  let isLoaded = $state<'mounting' | 'initial' | 'ready' | 'animated'>('mounting')
131
131
  let dataPath = $state<number>(-1)
132
132
  const motionConfig = $derived(getMotionConfig())
133
- const reducedMotionStore = useReducedMotionConfig()
134
- // Seed synchronously so the first render filters keyframes correctly —
135
- // otherwise transforms could flash before the subscribe effect runs.
136
- let reducedMotion = $state(get(reducedMotionStore))
137
- $effect(() => reducedMotionStore.subscribe((value) => (reducedMotion = value)))
133
+ const reducedMotionState = useReducedMotionConfig()
134
+ // `.current` is $state-backed inside reducedMotionState; tracking it via
135
+ // $derived makes `reducedMotion` re-evaluate whenever the OS preference
136
+ // or `<MotionConfig reducedMotion>` policy changes.
137
+ const reducedMotion = $derived(reducedMotionState.current)
138
138
 
139
139
  // Get presence context to check if we're inside AnimatePresence
140
140
  const context = getAnimatePresenceContext()
package/dist/index.d.ts CHANGED
@@ -10,30 +10,36 @@ 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 type { AugmentedMotionValue } from './utils/augmentMotionValue.svelte';
13
14
  export { useCycle } from './utils/cycle.svelte';
14
15
  export type { Cycle, CycleState } from './utils/cycle.svelte';
15
16
  export { createDragControls } from './utils/dragControls';
16
- export { useInView } from './utils/inView';
17
- export type { UseInViewOptions } from './utils/inView';
18
- export { useMotionTemplate } from './utils/motionTemplate';
19
- export { useMotionValue } from './utils/motionValue';
20
- export type { MotionValue } from './utils/motionValue';
17
+ export { useInView } from './utils/inView.svelte';
18
+ export type { InViewState, UseInViewOptions } from './utils/inView.svelte';
19
+ export { useMotionTemplate } from './utils/motionTemplate.svelte';
20
+ export type { MotionTemplateInput } from './utils/motionTemplate.svelte';
21
+ export { useMotionValue } from './utils/motionValue.svelte';
22
+ export type { MotionValue, RawMotionValue } from './utils/motionValue.svelte';
21
23
  export { useMotionValueEvent } from './utils/motionValueEvent';
22
- export { useReducedMotion } from './utils/reducedMotion';
23
- export { useReducedMotionConfig } from './utils/reducedMotionConfig';
24
- export { useScroll } from './utils/scroll';
24
+ export { useReducedMotion } from './utils/reducedMotion.svelte';
25
+ export type { ReducedMotionState } from './utils/reducedMotion.svelte';
26
+ export { useReducedMotionConfig } from './utils/reducedMotionConfig.svelte';
27
+ export { useScroll } from './utils/scroll.svelte';
28
+ export type { UseScrollOptions, UseScrollReturn } from './utils/scroll.svelte';
25
29
  export { useSpring } from './utils/spring.svelte';
26
30
  export type { SpringMotionValue, UseSpringOptions } from './utils/spring.svelte';
27
31
  export { useIsPresent, usePresence } from './utils/usePresence';
28
32
  export type { UsePresenceState } from './utils/usePresence';
29
- export { useVelocity } from './utils/velocity';
33
+ export { useVelocity } from './utils/velocity.svelte';
34
+ export type { VelocitySource } from './utils/velocity.svelte';
30
35
  /**
31
36
  * @deprecated Use `styleString` instead for reactive styles with automatic unit handling.
32
37
  */
33
38
  export { stringifyStyleObject } from './utils/styleObject';
34
39
  export { styleString } from './utils/styleObject.svelte';
35
- export { useTime } from './utils/time';
36
- export { useTransform } from './utils/transform';
40
+ export { useTime } from './utils/time.svelte';
41
+ export { useTransform } from './utils/transform.svelte';
42
+ export type { MultiTransformer, SingleTransformer, TransformOptions, TransformOutputMap, TransformSource } from './utils/transform.svelte';
37
43
  export { AnimatePresence, LayoutGroup, MotionConfig, PresenceChild };
38
44
  export { default as MotionA } from './html/A.svelte';
39
45
  export { default as MotionAbbr } from './html/Abbr.svelte';
package/dist/index.js CHANGED
@@ -13,23 +13,23 @@ export { useAnimate } from './utils/animate.svelte';
13
13
  export { useAnimationFrame } from './utils/animationFrame';
14
14
  export { useCycle } from './utils/cycle.svelte';
15
15
  export { createDragControls } from './utils/dragControls';
16
- export { useInView } from './utils/inView';
17
- export { useMotionTemplate } from './utils/motionTemplate';
18
- export { useMotionValue } from './utils/motionValue';
16
+ export { useInView } from './utils/inView.svelte';
17
+ export { useMotionTemplate } from './utils/motionTemplate.svelte';
18
+ export { useMotionValue } from './utils/motionValue.svelte';
19
19
  export { useMotionValueEvent } from './utils/motionValueEvent';
20
- export { useReducedMotion } from './utils/reducedMotion';
21
- export { useReducedMotionConfig } from './utils/reducedMotionConfig';
22
- export { useScroll } from './utils/scroll';
20
+ export { useReducedMotion } from './utils/reducedMotion.svelte';
21
+ export { useReducedMotionConfig } from './utils/reducedMotionConfig.svelte';
22
+ export { useScroll } from './utils/scroll.svelte';
23
23
  export { useSpring } from './utils/spring.svelte';
24
24
  export { useIsPresent, usePresence } from './utils/usePresence';
25
- export { useVelocity } from './utils/velocity';
25
+ export { useVelocity } from './utils/velocity.svelte';
26
26
  /**
27
27
  * @deprecated Use `styleString` instead for reactive styles with automatic unit handling.
28
28
  */
29
29
  export { stringifyStyleObject } from './utils/styleObject';
30
30
  export { styleString } from './utils/styleObject.svelte';
31
- export { useTime } from './utils/time';
32
- export { useTransform } from './utils/transform';
31
+ export { useTime } from './utils/time.svelte';
32
+ export { useTransform } from './utils/transform.svelte';
33
33
  export { AnimatePresence, LayoutGroup, MotionConfig, PresenceChild };
34
34
  // Named component exports — tree-shakeable alternative to the `motion` object
35
35
  export { default as MotionA } from './html/A.svelte';
@@ -1,3 +1,4 @@
1
+ import { cancelMicrotask, microtask } from 'motion-dom';
1
2
  import { resolveElement } from './dom.js';
2
3
  /**
3
4
  * Builds a subscriber-refcounted DOM-attachment primitive. Both `useScroll`
@@ -27,17 +28,21 @@ import { resolveElement } from './dom.js';
27
28
  */
28
29
  export const createAttachable = (config) => {
29
30
  let cleanup;
30
- let pollRaf = 0;
31
+ let pollScheduled = false;
31
32
  let subscriberCount = 0;
32
33
  // When stop() runs synchronously inside onAttach, cleanup hasn't been
33
34
  // assigned yet. Defer the teardown so the just-returned disposer still
34
35
  // gets invoked.
35
36
  let attaching = false;
36
37
  let stopRequestedDuringAttach = false;
38
+ const pollTick = () => {
39
+ pollScheduled = false;
40
+ tryAttach();
41
+ };
37
42
  const cancelPoll = () => {
38
- if (pollRaf) {
39
- cancelAnimationFrame(pollRaf);
40
- pollRaf = 0;
43
+ if (pollScheduled) {
44
+ cancelMicrotask(pollTick);
45
+ pollScheduled = false;
41
46
  }
42
47
  };
43
48
  const stop = () => {
@@ -65,11 +70,11 @@ export const createAttachable = (config) => {
65
70
  els[key] = el;
66
71
  }
67
72
  if (needsPoll) {
68
- if (!pollRaf) {
69
- pollRaf = requestAnimationFrame(() => {
70
- pollRaf = 0;
71
- tryAttach();
72
- });
73
+ // Microtask schedule (matches useScroll's pattern) — one frame
74
+ // faster than rAF for refs that hydrate the same tick.
75
+ if (!pollScheduled) {
76
+ pollScheduled = true;
77
+ microtask.read(pollTick);
73
78
  }
74
79
  return;
75
80
  }
@@ -0,0 +1,156 @@
1
+ import { type MotionValue } from 'motion-dom';
2
+ import { type Readable } from 'svelte/store';
3
+ /**
4
+ * A motion-dom `MotionValue<T>` augmented with the two affordances every
5
+ * Tier-2 hook in this library exposes for Svelte 5 consumers:
6
+ *
7
+ * - `current` — a `$state`-backed reactive getter. Reads inside templates,
8
+ * `$derived`, and `$effect` track changes automatically without going
9
+ * through `.subscribe()`.
10
+ * - `subscribe(run)` — Svelte readable store contract. Calls `run(value)`
11
+ * once synchronously on subscribe, then on every change. Lets the same
12
+ * value drive `$value` template syntax, `svelte/store`'s `get()`, and
13
+ * anything else that expects a readable.
14
+ *
15
+ * Every Wave 3 hook (`useMotionValue`, `useTransform`, `useScroll`,
16
+ * `useTime`, `useVelocity`, `useMotionTemplate`) and `useSpring` returns a
17
+ * value of this shape. The point is to keep one shared surface across all
18
+ * motion-value-producing hooks instead of repeating the wiring in each.
19
+ */
20
+ export type AugmentedMotionValue<T> = Omit<MotionValue<T>, 'current'> & {
21
+ /** Reactive read in Svelte 5 templates / `$derived` / `$effect`. */
22
+ readonly current: T;
23
+ /** Svelte readable store compatibility. */
24
+ subscribe: (run: (value: T) => void) => () => void;
25
+ };
26
+ /**
27
+ * Detects a Svelte readable store, excluding motion-dom `MotionValue`
28
+ * instances (which also expose `subscribe`-shaped APIs in some versions).
29
+ * Used by hook factories that accept either a `MotionValue` or a readable
30
+ * store as a source.
31
+ *
32
+ * @template T The value type the readable emits.
33
+ * @param value Any value to test.
34
+ * @returns Whether the value is a Svelte readable (and not a `MotionValue`).
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * import { writable } from 'svelte/store'
39
+ * import { motionValue } from 'motion-dom'
40
+ *
41
+ * isSvelteReadable(writable(0)) // true
42
+ * isSvelteReadable(motionValue(0)) // false (a MotionValue, not a readable)
43
+ * isSvelteReadable({ subscribe: 'nope' }) // false (subscribe isn't callable)
44
+ * ```
45
+ */
46
+ export declare const isSvelteReadable: <T = unknown>(value: unknown) => value is Readable<T>;
47
+ /**
48
+ * Synchronously samples a source: returns `T` directly, calls `.get()` on
49
+ * a `MotionValue`, or `svelte/store`'s `get()` on a readable. Used by hook
50
+ * factories to seed an initial value before any subscription is established.
51
+ *
52
+ * @template T The value type.
53
+ * @param source A plain value, a `MotionValue`, or a Svelte readable.
54
+ * @returns The current value of the source.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * sampleSource(42) // 42
59
+ * sampleSource(motionValue(7)) // 7
60
+ * sampleSource(writable(11)) // 11
61
+ * ```
62
+ */
63
+ export declare const sampleSource: <T>(source: T | MotionValue<T> | Readable<T>) => T;
64
+ /**
65
+ * Layer Svelte 5 affordances onto a motion-dom `MotionValue`:
66
+ *
67
+ * 1. A `$state`-tracked `.current` accessor. motion-dom writes to its own
68
+ * `current` field on every frame via an internal setter; we redirect that
69
+ * setter through `$state` so templates and `$derived` / `$effect` re-run
70
+ * automatically. Same-value writes are skipped (motion-dom can call the
71
+ * setter at rest, and `$state` would itself dedupe, but the explicit
72
+ * check avoids the extra accessor work).
73
+ *
74
+ * 2. A `.subscribe(run)` shim implementing the Svelte readable store
75
+ * contract: synchronous initial emit, then re-emit on every change.
76
+ * Forwarded to motion-dom's `.on('change', …)` event bus.
77
+ *
78
+ * 3. `.destroy()` is wrapped so a caller-supplied `dispose` runs once before
79
+ * motion-dom's own teardown (and only once, guarded against re-entrant
80
+ * or duplicate destroy calls).
81
+ *
82
+ * The returned reference is the same `MotionValue` passed in, only retyped —
83
+ * so identity checks (`isMotionValue`, `===`) still work and motion-dom's
84
+ * own machinery (animation, follow, composition) is untouched.
85
+ *
86
+ * **Call once per MotionValue.** This function mutates the value (rewrites
87
+ * `current`, `subscribe`, `destroy`). Calling it twice would re-define the
88
+ * accessors and the first call's `dispose` would be discarded.
89
+ *
90
+ * @template T The value type — typically `number` or `string`.
91
+ * @param value The motion-dom `MotionValue` to augment.
92
+ * @param dispose Optional cleanup that runs once when `.destroy()` is first called (before motion-dom's internal teardown). Defaults to a no-op.
93
+ * @returns The same `MotionValue` typed as {@link AugmentedMotionValue}.
94
+ *
95
+ * @example
96
+ * ```svelte
97
+ * <script lang="ts">
98
+ * import { motionValue } from 'motion-dom'
99
+ * import { augmentMotionValue } from './augmentMotionValue.svelte.js'
100
+ *
101
+ * const mv = motionValue(0)
102
+ * const aug = augmentMotionValue(mv, () => console.log('disposed'))
103
+ *
104
+ * $effect(() => () => aug.destroy())
105
+ * </script>
106
+ *
107
+ * <div style="transform: translateX({aug.current}px)">{aug.current}</div>
108
+ * ```
109
+ */
110
+ export declare const augmentMotionValue: <T>(value: MotionValue<T>, dispose?: VoidFunction) => AugmentedMotionValue<T>;
111
+ /**
112
+ * Bridges a Svelte `Readable<T>` into a motion-dom `MotionValue<T>` that
113
+ * mirrors the readable's emissions, so motion-dom primitives (`mapValue`,
114
+ * `transformValue`, `attachFollow`, `getVelocity`, etc.) that only accept
115
+ * `MotionValue` can track readable-shaped sources.
116
+ *
117
+ * The bridge:
118
+ * 1. Seeds via `get(source)` so the initial value is correct synchronously.
119
+ * 2. Subscribes to the readable, skipping the *synchronous initial emit*
120
+ * (Svelte readables always fire one on subscribe, but the seed already
121
+ * has it — without the skip the bridge would double-write on attach).
122
+ * 3. Optionally coerces each emit through `coerce` — useful for unit-string
123
+ * sources (e.g. `"100px"` → `100`).
124
+ *
125
+ * Returns the bridge value and a `dispose` that tears down the subscription
126
+ * and destroys the bridge MV. Callers register `dispose` with their lifecycle
127
+ * ($effect cleanup or the augmented `destroy`'s `dispose` slot).
128
+ *
129
+ * @template TIn The readable's emit type (often `number | string`).
130
+ * @template TOut The bridge MotionValue's value type (often `number`).
131
+ * @param source A Svelte readable store.
132
+ * @param coerce Optional transform applied to each emit (and the initial seed). Identity by default.
133
+ * @returns A `MotionValue<TOut>` mirroring the readable + a dispose function.
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * import { writable } from 'svelte/store'
138
+ *
139
+ * // Identity bridge — readable<number> → motionValue<number>
140
+ * const w = writable(0)
141
+ * const { value: mv, dispose } = bridgeReadableToMotionValue(w)
142
+ * w.set(50); mv.get() === 50
143
+ *
144
+ * // Coerce bridge — readable<string> → motionValue<number>
145
+ * const w2 = writable('100px')
146
+ * const bridge = bridgeReadableToMotionValue<string, number>(w2, parseFloat)
147
+ * bridge.value.get() === 100
148
+ *
149
+ * // Always pair with dispose() in your $effect cleanup.
150
+ * $effect(() => () => dispose())
151
+ * ```
152
+ */
153
+ export declare const bridgeReadableToMotionValue: <TIn, TOut = TIn>(source: Readable<TIn>, coerce?: (v: TIn) => TOut) => {
154
+ value: MotionValue<TOut>;
155
+ dispose: VoidFunction;
156
+ };
@@ -0,0 +1,193 @@
1
+ import { isMotionValue, motionValue } from 'motion-dom';
2
+ import { get } from 'svelte/store';
3
+ /**
4
+ * Detects a Svelte readable store, excluding motion-dom `MotionValue`
5
+ * instances (which also expose `subscribe`-shaped APIs in some versions).
6
+ * Used by hook factories that accept either a `MotionValue` or a readable
7
+ * store as a source.
8
+ *
9
+ * @template T The value type the readable emits.
10
+ * @param value Any value to test.
11
+ * @returns Whether the value is a Svelte readable (and not a `MotionValue`).
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { writable } from 'svelte/store'
16
+ * import { motionValue } from 'motion-dom'
17
+ *
18
+ * isSvelteReadable(writable(0)) // true
19
+ * isSvelteReadable(motionValue(0)) // false (a MotionValue, not a readable)
20
+ * isSvelteReadable({ subscribe: 'nope' }) // false (subscribe isn't callable)
21
+ * ```
22
+ */
23
+ export const isSvelteReadable = (value) => {
24
+ return (!!value &&
25
+ typeof value === 'object' &&
26
+ typeof value.subscribe === 'function' &&
27
+ !isMotionValue(value));
28
+ };
29
+ /**
30
+ * Synchronously samples a source: returns `T` directly, calls `.get()` on
31
+ * a `MotionValue`, or `svelte/store`'s `get()` on a readable. Used by hook
32
+ * factories to seed an initial value before any subscription is established.
33
+ *
34
+ * @template T The value type.
35
+ * @param source A plain value, a `MotionValue`, or a Svelte readable.
36
+ * @returns The current value of the source.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * sampleSource(42) // 42
41
+ * sampleSource(motionValue(7)) // 7
42
+ * sampleSource(writable(11)) // 11
43
+ * ```
44
+ */
45
+ export const sampleSource = (source) => {
46
+ if (isMotionValue(source))
47
+ return source.get();
48
+ if (isSvelteReadable(source))
49
+ return get(source);
50
+ return source;
51
+ };
52
+ /**
53
+ * Layer Svelte 5 affordances onto a motion-dom `MotionValue`:
54
+ *
55
+ * 1. A `$state`-tracked `.current` accessor. motion-dom writes to its own
56
+ * `current` field on every frame via an internal setter; we redirect that
57
+ * setter through `$state` so templates and `$derived` / `$effect` re-run
58
+ * automatically. Same-value writes are skipped (motion-dom can call the
59
+ * setter at rest, and `$state` would itself dedupe, but the explicit
60
+ * check avoids the extra accessor work).
61
+ *
62
+ * 2. A `.subscribe(run)` shim implementing the Svelte readable store
63
+ * contract: synchronous initial emit, then re-emit on every change.
64
+ * Forwarded to motion-dom's `.on('change', …)` event bus.
65
+ *
66
+ * 3. `.destroy()` is wrapped so a caller-supplied `dispose` runs once before
67
+ * motion-dom's own teardown (and only once, guarded against re-entrant
68
+ * or duplicate destroy calls).
69
+ *
70
+ * The returned reference is the same `MotionValue` passed in, only retyped —
71
+ * so identity checks (`isMotionValue`, `===`) still work and motion-dom's
72
+ * own machinery (animation, follow, composition) is untouched.
73
+ *
74
+ * **Call once per MotionValue.** This function mutates the value (rewrites
75
+ * `current`, `subscribe`, `destroy`). Calling it twice would re-define the
76
+ * accessors and the first call's `dispose` would be discarded.
77
+ *
78
+ * @template T The value type — typically `number` or `string`.
79
+ * @param value The motion-dom `MotionValue` to augment.
80
+ * @param dispose Optional cleanup that runs once when `.destroy()` is first called (before motion-dom's internal teardown). Defaults to a no-op.
81
+ * @returns The same `MotionValue` typed as {@link AugmentedMotionValue}.
82
+ *
83
+ * @example
84
+ * ```svelte
85
+ * <script lang="ts">
86
+ * import { motionValue } from 'motion-dom'
87
+ * import { augmentMotionValue } from './augmentMotionValue.svelte.js'
88
+ *
89
+ * const mv = motionValue(0)
90
+ * const aug = augmentMotionValue(mv, () => console.log('disposed'))
91
+ *
92
+ * $effect(() => () => aug.destroy())
93
+ * </script>
94
+ *
95
+ * <div style="transform: translateX({aug.current}px)">{aug.current}</div>
96
+ * ```
97
+ */
98
+ export const augmentMotionValue = (value, dispose = () => undefined) => {
99
+ // motion-dom's `.get()` returns `NonNullable<V>`, which would otherwise
100
+ // narrow `$state` to that and reject nullable-T setter writes. Cast to T
101
+ // so the state slot matches the public augmented signature; motion-dom's
102
+ // own contract guarantees it never sets a null/undefined frame value.
103
+ let current = $state(value.get());
104
+ Object.defineProperty(value, 'current', {
105
+ get: () => current,
106
+ set: (v) => {
107
+ if (v !== current)
108
+ current = v;
109
+ },
110
+ enumerable: true,
111
+ configurable: true
112
+ });
113
+ const originalDestroy = value.destroy.bind(value);
114
+ let destroyed = false;
115
+ value.destroy = () => {
116
+ if (destroyed)
117
+ return;
118
+ destroyed = true;
119
+ dispose();
120
+ originalDestroy();
121
+ };
122
+ const subscribe = (run) => {
123
+ run(value.get());
124
+ return value.on('change', run);
125
+ };
126
+ Object.defineProperty(value, 'subscribe', {
127
+ value: subscribe,
128
+ writable: false,
129
+ enumerable: false,
130
+ configurable: true
131
+ });
132
+ return value;
133
+ };
134
+ /**
135
+ * Bridges a Svelte `Readable<T>` into a motion-dom `MotionValue<T>` that
136
+ * mirrors the readable's emissions, so motion-dom primitives (`mapValue`,
137
+ * `transformValue`, `attachFollow`, `getVelocity`, etc.) that only accept
138
+ * `MotionValue` can track readable-shaped sources.
139
+ *
140
+ * The bridge:
141
+ * 1. Seeds via `get(source)` so the initial value is correct synchronously.
142
+ * 2. Subscribes to the readable, skipping the *synchronous initial emit*
143
+ * (Svelte readables always fire one on subscribe, but the seed already
144
+ * has it — without the skip the bridge would double-write on attach).
145
+ * 3. Optionally coerces each emit through `coerce` — useful for unit-string
146
+ * sources (e.g. `"100px"` → `100`).
147
+ *
148
+ * Returns the bridge value and a `dispose` that tears down the subscription
149
+ * and destroys the bridge MV. Callers register `dispose` with their lifecycle
150
+ * ($effect cleanup or the augmented `destroy`'s `dispose` slot).
151
+ *
152
+ * @template TIn The readable's emit type (often `number | string`).
153
+ * @template TOut The bridge MotionValue's value type (often `number`).
154
+ * @param source A Svelte readable store.
155
+ * @param coerce Optional transform applied to each emit (and the initial seed). Identity by default.
156
+ * @returns A `MotionValue<TOut>` mirroring the readable + a dispose function.
157
+ *
158
+ * @example
159
+ * ```ts
160
+ * import { writable } from 'svelte/store'
161
+ *
162
+ * // Identity bridge — readable<number> → motionValue<number>
163
+ * const w = writable(0)
164
+ * const { value: mv, dispose } = bridgeReadableToMotionValue(w)
165
+ * w.set(50); mv.get() === 50
166
+ *
167
+ * // Coerce bridge — readable<string> → motionValue<number>
168
+ * const w2 = writable('100px')
169
+ * const bridge = bridgeReadableToMotionValue<string, number>(w2, parseFloat)
170
+ * bridge.value.get() === 100
171
+ *
172
+ * // Always pair with dispose() in your $effect cleanup.
173
+ * $effect(() => () => dispose())
174
+ * ```
175
+ */
176
+ export const bridgeReadableToMotionValue = (source, coerce = (v) => v) => {
177
+ const bridge = motionValue(coerce(get(source)));
178
+ let seenInitial = false;
179
+ const unsub = source.subscribe((v) => {
180
+ if (!seenInitial) {
181
+ seenInitial = true;
182
+ return;
183
+ }
184
+ bridge.set(coerce(v));
185
+ });
186
+ return {
187
+ value: bridge,
188
+ dispose: () => {
189
+ unsub();
190
+ bridge.destroy();
191
+ }
192
+ };
193
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Shared `{ current, subscribe }` shape returned by the Wave 2 boolean
3
+ * snapshot hooks — `useReducedMotion`, `useReducedMotionConfig`,
4
+ * `useInView`. `.current` is `$state`-backed; `.subscribe(run)` is the
5
+ * Svelte readable store contract preserved for legacy consumers during
6
+ * the Tier 2 migration.
7
+ */
8
+ export type BooleanSnapshot = {
9
+ /** Reactive read in Svelte 5 templates / `$derived` / `$effect`. */
10
+ readonly current: boolean;
11
+ /** Svelte readable store contract — emits synchronously on subscribe. */
12
+ subscribe: (run: (value: boolean) => void) => () => void;
13
+ };
14
+ /**
15
+ * Build a `{ current, subscribe }` snapshot + an internal `set`
16
+ * function. Centralises the dedupe + subscriber-fanout that all three
17
+ * boolean-snapshot hooks need.
18
+ *
19
+ * Returns a tuple so consumers can hand the snapshot to callers while
20
+ * keeping `set` internal (it's not on the returned state object).
21
+ *
22
+ * Same-value writes via `set` are no-ops — saves a fanout call and
23
+ * means callers don't need their own change-detection guard.
24
+ *
25
+ * @param initial Starting value for the `current` cell.
26
+ * @returns A `[state, set]` tuple where `state` is the publicly-shared
27
+ * `{ current, subscribe }` and `set` is the internal updater the hook
28
+ * uses to push values from its event source.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const [state, set] = createBooleanSnapshot(media.matches)
33
+ * media.addEventListener('change', (e) => set(e.matches))
34
+ * return state // { current, subscribe }
35
+ * ```
36
+ */
37
+ export declare const createBooleanSnapshot: (initial: boolean) => [BooleanSnapshot, (value: boolean) => void];
@@ -0,0 +1,48 @@
1
+ import { SvelteSet } from 'svelte/reactivity';
2
+ /**
3
+ * Build a `{ current, subscribe }` snapshot + an internal `set`
4
+ * function. Centralises the dedupe + subscriber-fanout that all three
5
+ * boolean-snapshot hooks need.
6
+ *
7
+ * Returns a tuple so consumers can hand the snapshot to callers while
8
+ * keeping `set` internal (it's not on the returned state object).
9
+ *
10
+ * Same-value writes via `set` are no-ops — saves a fanout call and
11
+ * means callers don't need their own change-detection guard.
12
+ *
13
+ * @param initial Starting value for the `current` cell.
14
+ * @returns A `[state, set]` tuple where `state` is the publicly-shared
15
+ * `{ current, subscribe }` and `set` is the internal updater the hook
16
+ * uses to push values from its event source.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * const [state, set] = createBooleanSnapshot(media.matches)
21
+ * media.addEventListener('change', (e) => set(e.matches))
22
+ * return state // { current, subscribe }
23
+ * ```
24
+ */
25
+ export const createBooleanSnapshot = (initial) => {
26
+ let current = $state(initial);
27
+ const subscribers = new SvelteSet();
28
+ const state = {
29
+ get current() {
30
+ return current;
31
+ },
32
+ subscribe(run) {
33
+ subscribers.add(run);
34
+ run(current);
35
+ return () => {
36
+ subscribers.delete(run);
37
+ };
38
+ }
39
+ };
40
+ const set = (value) => {
41
+ if (value === current)
42
+ return;
43
+ current = value;
44
+ for (const sub of subscribers)
45
+ sub(value);
46
+ };
47
+ return [state, set];
48
+ };
@@ -41,3 +41,16 @@ export type ElementOrGetter = HTMLElement | (() => HTMLElement | null | undefine
41
41
  * ```
42
42
  */
43
43
  export declare const resolveElement: (ref?: ElementOrGetter) => HTMLElement | undefined;
44
+ /**
45
+ * Tests whether an `ElementOrGetter` is currently unresolved — defined as a
46
+ * getter that returns falsy. Direct element refs are never "pending" (they
47
+ * were already resolved at call time) and absent refs are not "pending"
48
+ * either (they're just absent).
49
+ *
50
+ * Lets microtask-defer / rAF-defer loops wait for refs to hydrate after
51
+ * `bind:this` settles on mount.
52
+ *
53
+ * @param ref Element or getter to check.
54
+ * @returns `true` only when `ref` is a getter and the getter currently returns falsy.
55
+ */
56
+ export declare const isRefPending: (ref?: ElementOrGetter) => boolean;
package/dist/utils/dom.js CHANGED
@@ -39,3 +39,22 @@ export const resolveElement = (ref) => {
39
39
  const value = typeof ref === 'function' ? ref() : ref;
40
40
  return value ?? undefined;
41
41
  };
42
+ /**
43
+ * Tests whether an `ElementOrGetter` is currently unresolved — defined as a
44
+ * getter that returns falsy. Direct element refs are never "pending" (they
45
+ * were already resolved at call time) and absent refs are not "pending"
46
+ * either (they're just absent).
47
+ *
48
+ * Lets microtask-defer / rAF-defer loops wait for refs to hydrate after
49
+ * `bind:this` settles on mount.
50
+ *
51
+ * @param ref Element or getter to check.
52
+ * @returns `true` only when `ref` is a getter and the getter currently returns falsy.
53
+ */
54
+ export const isRefPending = (ref) => {
55
+ if (!ref)
56
+ return false;
57
+ if (typeof ref !== 'function')
58
+ return false;
59
+ return !ref();
60
+ };