@humanspeak/svelte-motion 0.4.7 → 0.4.9

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.
@@ -1,266 +0,0 @@
1
- import { animate, inView as motionInView } from 'motion';
2
- import { readable, writable } from 'svelte/store';
3
- import { createAttachable } from './attachable.js';
4
- import {} from './dom.js';
5
- const CSS_FUNCTION_RE = /\b(var|calc|min|max|clamp|rgb|rgba|hsl|hsla|url)\s*\(/i;
6
- /**
7
- * Read an inline CSS-function value (var/calc/url/etc.) for `propName`
8
- * directly from the element's style declaration. Returns `null` for missing
9
- * or non-function values so the caller can fall back to `getComputedStyle`.
10
- *
11
- * Uses `style.getPropertyValue` so values like `url(data:image/svg+xml;...)`
12
- * with nested semicolons are preserved intact - the browser has already
13
- * parsed the declaration, no string scraping required.
14
- *
15
- * @param el Element whose inline style is read.
16
- * @param propName Camel-case JS property name (e.g. `borderColor`).
17
- * @returns The inline CSS function value, or `null`.
18
- * @example
19
- * getInlineCssFunction(node, 'background') // => 'var(--brand)' | null
20
- */
21
- const getInlineCssFunction = (el, propName) => {
22
- const kebab = propName.replace(/([A-Z])/g, '-$1').toLowerCase();
23
- const value = el.style.getPropertyValue(kebab).trim();
24
- if (!value)
25
- return null;
26
- return CSS_FUNCTION_RE.test(value) ? value : null;
27
- };
28
- /**
29
- * Split a whileInView definition into keyframes and an optional nested transition.
30
- *
31
- * @param def While-in-view record that may include a nested `transition`.
32
- * @returns Object with `keyframes` (no `transition`) and optional `transition`.
33
- * @example
34
- * // With transition
35
- * splitInViewDefinition({ opacity: 1, y: 0, transition: { duration: 0.5 } })
36
- * // => { keyframes: { opacity: 1, y: 0 }, transition: { duration: 0.5 } }
37
- *
38
- * @example
39
- * // Without transition
40
- * splitInViewDefinition({ opacity: 1, scale: 1 })
41
- * // => { keyframes: { opacity: 1, scale: 1 }, transition: undefined }
42
- */
43
- export const splitInViewDefinition = (def) => {
44
- const { transition, ...rest } = (def ?? {});
45
- return { keyframes: rest, transition };
46
- };
47
- /**
48
- * Compute the baseline values to restore to when element leaves viewport.
49
- *
50
- * Preference order per key: `animate` → `initial` → neutral transform defaults
51
- * → computed style value if present.
52
- *
53
- * @param el Target element.
54
- * @param opts Source records for baseline computation.
55
- * @returns Minimal baseline record to restore when element leaves viewport.
56
- * @example
57
- * computeInViewBaseline(element, {
58
- * initial: { opacity: 0, y: 50 },
59
- * animate: { opacity: 1, y: 0 },
60
- * whileInView: { opacity: 1, scale: 1.1 }
61
- * })
62
- * // => { opacity: 1, scale: 1 } (scale defaults to 1, opacity from animate)
63
- */
64
- export const computeInViewBaseline = (el, opts) => {
65
- const baseline = {};
66
- const initialRecord = (opts.initial ?? {});
67
- const animateRecord = (opts.animate ?? {});
68
- const whileInViewRecordRaw = (opts.whileInView ?? {});
69
- const whileInViewRecord = { ...whileInViewRecordRaw };
70
- delete whileInViewRecord.transition;
71
- const neutralTransformDefaults = {
72
- x: 0,
73
- y: 0,
74
- translateX: 0,
75
- translateY: 0,
76
- scale: 1,
77
- scaleX: 1,
78
- scaleY: 1,
79
- rotate: 0,
80
- rotateX: 0,
81
- rotateY: 0,
82
- rotateZ: 0,
83
- skewX: 0,
84
- skewY: 0,
85
- opacity: 1
86
- };
87
- const cs = getComputedStyle(el);
88
- for (const key of Object.keys(whileInViewRecord)) {
89
- if (Object.prototype.hasOwnProperty.call(animateRecord, key)) {
90
- baseline[key] = animateRecord[key];
91
- }
92
- else if (Object.prototype.hasOwnProperty.call(initialRecord, key)) {
93
- baseline[key] = initialRecord[key];
94
- }
95
- else if (key in neutralTransformDefaults) {
96
- baseline[key] = neutralTransformDefaults[key];
97
- }
98
- else {
99
- const inlineValue = getInlineCssFunction(el, key);
100
- if (inlineValue) {
101
- baseline[key] = inlineValue;
102
- }
103
- else if (key in cs) {
104
- baseline[key] = cs[key];
105
- }
106
- }
107
- }
108
- return baseline;
109
- };
110
- /**
111
- * Attach whileInView interactions to an element via motion's `inView` primitive.
112
- *
113
- * On entry, animates to `whileInView` state (using the nested `transition` if
114
- * provided). On exit, restores the changed keys to a baseline computed from
115
- * `initial` / `animate` / neutral transform defaults / inline styles.
116
- *
117
- * Delegates to motion's `inView` so the IntersectionObserver implementation
118
- * is shared with the public `useInView` hook.
119
- *
120
- * @param el Target element.
121
- * @param whileInView While-in-view definition.
122
- * @param mergedTransition Root/component merged transition.
123
- * @param callbacks Optional lifecycle callbacks for in-view start/end and animation completion.
124
- * @param baselineSources Optional sources used to compute baseline.
125
- * @param viewport Optional IntersectionObserver options. `amount` defaults to `0` (any pixel visible).
126
- * @returns Cleanup function to stop observing.
127
- * @example
128
- * const cleanup = attachWhileInView(
129
- * element,
130
- * { opacity: 1, y: 0, transition: { duration: 0.5 } },
131
- * { duration: 0.3 },
132
- * {
133
- * onStart: () => console.log('Entered viewport'),
134
- * onEnd: () => console.log('Left viewport')
135
- * },
136
- * { initial: { opacity: 0, y: 50 } },
137
- * { once: true, amount: 0.5 }
138
- * )
139
- * // Later: cleanup() to stop observing
140
- */
141
- export const attachWhileInView = (el, whileInView, mergedTransition, callbacks, baselineSources, viewport) => {
142
- if (!whileInView)
143
- return () => { };
144
- let latched = false;
145
- const stop = motionInView(el, () => {
146
- if (latched)
147
- return;
148
- const inViewBaseline = computeInViewBaseline(el, {
149
- initial: baselineSources?.initial,
150
- animate: baselineSources?.animate,
151
- whileInView
152
- });
153
- callbacks?.onStart?.();
154
- const { keyframes, transition } = splitInViewDefinition(whileInView);
155
- const animation = animate(el, keyframes, (transition ?? mergedTransition));
156
- animation.finished
157
- .then(() => {
158
- callbacks?.onAnimationComplete?.(keyframes);
159
- })
160
- .catch(() => {
161
- /* animation cancelled — skip completion callback */
162
- });
163
- if (viewport?.once) {
164
- // Latch on first entry. Don't return an exit handler so the
165
- // element holds its in-view state and we never animate back.
166
- // Stop observing entirely after the entry handler returns.
167
- latched = true;
168
- queueMicrotask(stop);
169
- return;
170
- }
171
- return () => {
172
- if (Object.keys(inViewBaseline).length > 0) {
173
- animate(el, inViewBaseline, mergedTransition);
174
- }
175
- callbacks?.onEnd?.();
176
- };
177
- }, {
178
- root: viewport?.root,
179
- // framer-motion types `margin` as a CSS-shorthand template literal;
180
- // we expose plain `string` so consumers can pass any computed value.
181
- margin: viewport?.margin,
182
- amount: viewport?.amount ?? 0
183
- });
184
- return stop;
185
- };
186
- /**
187
- * Returns a Svelte readable store that tracks whether `target` is in the
188
- * viewport. Mirrors Framer Motion's `useInView` so the same options
189
- * (`root`, `margin`, `amount`, `once`, `initial`) work as in React.
190
- *
191
- * `target` (and `options.root`) accept either an `HTMLElement` directly or a
192
- * getter `() => HTMLElement | undefined`. With Svelte's `bind:this`, the
193
- * element isn't available until after mount, so element resolution is
194
- * deferred until the first subscriber arrives; if the element isn't ready,
195
- * the hook polls on `requestAnimationFrame` until it is.
196
- *
197
- * SSR-safe: returns a static `readable(initial)` when `window` or
198
- * `IntersectionObserver` is unavailable.
199
- *
200
- * @param target - Element (or getter) to observe.
201
- * @param options - Optional `UseInViewOptions` (`root`, `margin`, `amount`,
202
- * `once`, `initial`).
203
- * @returns A `Readable<boolean>` that flips to `true` while `target` is in view.
204
- * @see https://motion.dev/docs/react-use-in-view
205
- *
206
- * @example
207
- * ```svelte
208
- * <script>
209
- * import { useInView } from '@humanspeak/svelte-motion'
210
- *
211
- * let ref
212
- * const inView = useInView(() => ref, { once: true })
213
- *
214
- * $effect(() => {
215
- * if ($inView) trackImpression()
216
- * })
217
- * </script>
218
- *
219
- * <div bind:this={ref}>{$inView ? 'visible' : 'hidden'}</div>
220
- * ```
221
- */
222
- export const useInView = (target, options = {}) => {
223
- const initial = options.initial ?? false;
224
- if (typeof window === 'undefined' || typeof IntersectionObserver === 'undefined') {
225
- return readable(initial);
226
- }
227
- const store = writable(initial);
228
- let current = initial;
229
- const update = (next) => {
230
- if (next === current)
231
- return;
232
- current = next;
233
- store.set(next);
234
- };
235
- let latched = false;
236
- const attachable = createAttachable({
237
- refs: { target, root: options.root },
238
- isLatched: () => latched,
239
- onAttach: ({ target: el, root }, stop) => motionInView(el, () => {
240
- update(true);
241
- if (options.once) {
242
- // Detach inside the entry callback; motion's inView
243
- // handles re-entry safely via observer.unobserve.
244
- latched = true;
245
- stop();
246
- return;
247
- }
248
- return () => update(false);
249
- }, {
250
- root: root,
251
- // framer-motion types `margin` as a CSS-shorthand template
252
- // literal; we expose plain `string` so the public API is
253
- // ergonomic and forward-compat with future motion changes.
254
- margin: options.margin,
255
- amount: options.amount
256
- })
257
- });
258
- return readable(initial, (set) => {
259
- const release = attachable.subscribe();
260
- const unsub = store.subscribe(set);
261
- return () => {
262
- unsub();
263
- release();
264
- };
265
- });
266
- };
@@ -1,20 +0,0 @@
1
- import { type Readable } from 'svelte/store';
2
- /**
3
- * Returns a readable store that reflects the user's `prefers-reduced-motion` setting.
4
- *
5
- * Defaults to `false` in SSR or when `matchMedia` is unavailable/throws.
6
- *
7
- * @returns {Readable<boolean>} `true` when the user prefers reduced motion.
8
- * @see https://motion.dev/docs/react-reduced-motion
9
- *
10
- * @example
11
- * ```svelte
12
- * <script>
13
- * import { useReducedMotion } from '@humanspeak/svelte-motion'
14
- * const reduced = useReducedMotion()
15
- * </script>
16
- *
17
- * <div style:transform={$reduced ? 'none' : 'rotate(45deg)'}>...</div>
18
- * ```
19
- */
20
- export declare const useReducedMotion: () => Readable<boolean>;
@@ -1,42 +0,0 @@
1
- import { readable } from 'svelte/store';
2
- const REDUCED_MOTION_QUERY = '(prefers-reduced-motion: reduce)';
3
- const SSR_FALLBACK = readable(false, () => { });
4
- /**
5
- * Returns a readable store that reflects the user's `prefers-reduced-motion` setting.
6
- *
7
- * Defaults to `false` in SSR or when `matchMedia` is unavailable/throws.
8
- *
9
- * @returns {Readable<boolean>} `true` when the user prefers reduced motion.
10
- * @see https://motion.dev/docs/react-reduced-motion
11
- *
12
- * @example
13
- * ```svelte
14
- * <script>
15
- * import { useReducedMotion } from '@humanspeak/svelte-motion'
16
- * const reduced = useReducedMotion()
17
- * </script>
18
- *
19
- * <div style:transform={$reduced ? 'none' : 'rotate(45deg)'}>...</div>
20
- * ```
21
- */
22
- export const useReducedMotion = () => {
23
- if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
24
- return SSR_FALLBACK;
25
- }
26
- let media;
27
- try {
28
- media = window.matchMedia(REDUCED_MOTION_QUERY);
29
- }
30
- catch {
31
- return SSR_FALLBACK;
32
- }
33
- return readable(media.matches, (set) => {
34
- const handler = (event) => set(event.matches);
35
- if (typeof media.addEventListener === 'function') {
36
- media.addEventListener('change', handler);
37
- return () => media.removeEventListener('change', handler);
38
- }
39
- media.addListener(handler);
40
- return () => media.removeListener(handler);
41
- });
42
- };
@@ -1,39 +0,0 @@
1
- import { type Readable } from 'svelte/store';
2
- /**
3
- * Returns a copy of `keyframes` with transform-related keys removed when
4
- * `reduced` is `true`. Returns `keyframes` unchanged otherwise.
5
- *
6
- * The `transition` key is preserved so per-key transitions still flow through
7
- * to the animation engine.
8
- */
9
- export declare function filterReducedMotionKeyframes<T extends Record<string, unknown> | undefined>(keyframes: T, reduced: boolean): T;
10
- /**
11
- * Returns a readable store that reflects the resolved reduced-motion policy
12
- * for the current component subtree.
13
- *
14
- * Resolution combines the nearest `<MotionConfig reducedMotion>` ancestor with
15
- * the OS-level `prefers-reduced-motion` setting:
16
- *
17
- * - `'always'` → always `true`
18
- * - `'never'` (or no `<MotionConfig>` ancestor) → always `false`
19
- * - `'user'` → mirrors the OS preference, defaulting to `false` in SSR
20
- *
21
- * Use this from inside motion-aware components to decide whether to skip
22
- * transform animations.
23
- *
24
- * @returns {Readable<boolean>} `true` when descendant motion should be reduced.
25
- * @see https://motion.dev/docs/react-reduced-motion
26
- *
27
- * @example
28
- * ```svelte
29
- * <script>
30
- * import { useReducedMotionConfig } from '@humanspeak/svelte-motion'
31
- * const reduced = useReducedMotionConfig()
32
- * </script>
33
- *
34
- * {#if !$reduced}
35
- * <motion.div animate={{ x: 100 }} />
36
- * {/if}
37
- * ```
38
- */
39
- export declare const useReducedMotionConfig: () => Readable<boolean>;
@@ -1,92 +0,0 @@
1
- import { getMotionConfig } from '../components/motionConfig.context.js';
2
- import { useReducedMotion } from './reducedMotion.js';
3
- import { derived } from 'svelte/store';
4
- /**
5
- * CSS / motion property keys that move or rotate an element via `transform`.
6
- * When reduced motion is active these keys are stripped from animate keyframes
7
- * so the element stays in place while non-transform properties (opacity, color,
8
- * etc.) continue to animate.
9
- */
10
- const TRANSFORM_KEYS = new Set([
11
- 'x',
12
- 'y',
13
- 'z',
14
- 'translate',
15
- 'translateX',
16
- 'translateY',
17
- 'translateZ',
18
- 'scale',
19
- 'scaleX',
20
- 'scaleY',
21
- 'scaleZ',
22
- 'rotate',
23
- 'rotateX',
24
- 'rotateY',
25
- 'rotateZ',
26
- 'skew',
27
- 'skewX',
28
- 'skewY',
29
- 'transform',
30
- 'transformPerspective',
31
- 'perspective'
32
- ]);
33
- /**
34
- * Returns a copy of `keyframes` with transform-related keys removed when
35
- * `reduced` is `true`. Returns `keyframes` unchanged otherwise.
36
- *
37
- * The `transition` key is preserved so per-key transitions still flow through
38
- * to the animation engine.
39
- */
40
- export function filterReducedMotionKeyframes(keyframes, reduced) {
41
- if (!reduced || !keyframes)
42
- return keyframes;
43
- const out = {};
44
- for (const key of Object.keys(keyframes)) {
45
- if (!TRANSFORM_KEYS.has(key))
46
- out[key] = keyframes[key];
47
- }
48
- return out;
49
- }
50
- /**
51
- * Returns a readable store that reflects the resolved reduced-motion policy
52
- * for the current component subtree.
53
- *
54
- * Resolution combines the nearest `<MotionConfig reducedMotion>` ancestor with
55
- * the OS-level `prefers-reduced-motion` setting:
56
- *
57
- * - `'always'` → always `true`
58
- * - `'never'` (or no `<MotionConfig>` ancestor) → always `false`
59
- * - `'user'` → mirrors the OS preference, defaulting to `false` in SSR
60
- *
61
- * Use this from inside motion-aware components to decide whether to skip
62
- * transform animations.
63
- *
64
- * @returns {Readable<boolean>} `true` when descendant motion should be reduced.
65
- * @see https://motion.dev/docs/react-reduced-motion
66
- *
67
- * @example
68
- * ```svelte
69
- * <script>
70
- * import { useReducedMotionConfig } from '@humanspeak/svelte-motion'
71
- * const reduced = useReducedMotionConfig()
72
- * </script>
73
- *
74
- * {#if !$reduced}
75
- * <motion.div animate={{ x: 100 }} />
76
- * {/if}
77
- * ```
78
- */
79
- export const useReducedMotionConfig = () => {
80
- const motionConfig = getMotionConfig();
81
- // Read motionConfig?.reducedMotion *inside* the derived so dynamic
82
- // `<MotionConfig reducedMotion={...}>` updates surface to subscribers —
83
- // motionConfig uses property getters, so the value is always fresh.
84
- return derived(useReducedMotion(), ($osReduced) => {
85
- const policy = motionConfig?.reducedMotion ?? 'never';
86
- if (policy === 'always')
87
- return true;
88
- if (policy === 'never')
89
- return false;
90
- return $osReduced;
91
- });
92
- };
@@ -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
- };