@hanzogui/animations-motion 8.3.0 → 8.3.2

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 (51) hide show
  1. package/dist/cjs/createAnimations.js +804 -0
  2. package/dist/cjs/createAnimations.js.map +1 -0
  3. package/dist/cjs/index.js +6 -0
  4. package/dist/cjs/index.js.map +1 -0
  5. package/dist/cjs/package.json +1 -0
  6. package/dist/cjs/polyfill.js +6 -0
  7. package/dist/cjs/polyfill.js.map +1 -0
  8. package/dist/esm/createAnimations.js +799 -0
  9. package/dist/esm/createAnimations.js.map +1 -0
  10. package/dist/esm/index.js +3 -3
  11. package/dist/esm/index.js.map +1 -1
  12. package/dist/esm/polyfill.js +6 -0
  13. package/dist/esm/polyfill.js.map +1 -0
  14. package/dist/native/createAnimations.js +799 -0
  15. package/dist/native/createAnimations.js.map +1 -0
  16. package/dist/native/index.js +20 -0
  17. package/dist/native/index.js.map +1 -0
  18. package/dist/native/polyfill.js +6 -0
  19. package/dist/native/polyfill.js.map +1 -0
  20. package/package.json +17 -17
  21. package/src/createAnimations.tsx +1 -1
  22. package/src/index.ts +2 -2
  23. package/types/createAnimations.d.ts +2 -3
  24. package/types/createAnimations.d.ts.map +1 -11
  25. package/types/index.d.ts +2 -3
  26. package/types/index.d.ts.map +1 -11
  27. package/types/index.native.d.ts +1 -2
  28. package/types/index.native.d.ts.map +1 -11
  29. package/types/polyfill.d.ts +0 -1
  30. package/types/polyfill.d.ts.map +1 -11
  31. package/dist/cjs/createAnimations.cjs +0 -720
  32. package/dist/cjs/createAnimations.native.js +0 -844
  33. package/dist/cjs/createAnimations.native.js.map +0 -1
  34. package/dist/cjs/index.cjs +0 -21
  35. package/dist/cjs/index.native.js +0 -45
  36. package/dist/cjs/index.native.js.map +0 -1
  37. package/dist/cjs/polyfill.cjs +0 -3
  38. package/dist/cjs/polyfill.native.js +0 -6
  39. package/dist/cjs/polyfill.native.js.map +0 -1
  40. package/dist/esm/createAnimations.mjs +0 -683
  41. package/dist/esm/createAnimations.mjs.map +0 -1
  42. package/dist/esm/createAnimations.native.js +0 -804
  43. package/dist/esm/createAnimations.native.js.map +0 -1
  44. package/dist/esm/index.mjs +0 -3
  45. package/dist/esm/index.mjs.map +0 -1
  46. package/dist/esm/index.native.js +0 -17
  47. package/dist/esm/index.native.js.map +0 -1
  48. package/dist/esm/polyfill.mjs +0 -4
  49. package/dist/esm/polyfill.mjs.map +0 -1
  50. package/dist/esm/polyfill.native.js +0 -4
  51. package/dist/esm/polyfill.native.js.map +0 -1
@@ -0,0 +1,799 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { getEffectiveAnimation, normalizeTransition } from '@hanzogui/animation-helpers';
3
+ import { ResetPresence, usePresence } from '@hanzogui/use-presence';
4
+ import { fixStyles, getConfig, getSplitStyles, hooks, styleToCSS, Text, useComposedRefs, useIsomorphicLayoutEffect, useThemeWithState, View, } from '@hanzogui/web';
5
+ import { useAnimate, useMotionValue, useMotionValueEvent, } from 'motion/react';
6
+ import React, { forwardRef, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
7
+ const isServer = typeof window === 'undefined';
8
+ // SSR-safe wrapper: framer-motion's useAnimate imports its own React copy in
9
+ // Vite SSR bundles which causes "Invalid hook call" errors. during SSR we
10
+ // don't need animations so we return a no-op scope/animate pair.
11
+ function useAnimateSSRSafe() {
12
+ if (isServer) {
13
+ return [useRef(null), () => { }];
14
+ }
15
+ return useAnimate();
16
+ }
17
+ const MotionValueStrategy = new WeakMap();
18
+ // pending setValue onFinish callbacks, keyed by motion value. setValue stores
19
+ // the callback here; the change handler in the animated component's useEffect
20
+ // consumes it by chaining to the DOM-level animate() controls so onFinish
21
+ // fires when the *visible* animation actually completes.
22
+ const PendingMotionOnFinish = new WeakMap();
23
+ function settlePendingMotionOnFinish(mv, controls) {
24
+ const onFinish = PendingMotionOnFinish.get(mv);
25
+ if (!onFinish)
26
+ return;
27
+ PendingMotionOnFinish.delete(mv);
28
+ // chain to the DOM animation's completion. settle on both resolve and
29
+ // reject — a rejection means the animation was cancelled by a later
30
+ // setValue, and the caller still needs a completion signal. use the
31
+ // real Promise interface (.then().catch()) because framer-motion types
32
+ // the .then() callbacks as VoidFunction with no error arg.
33
+ controls.then(() => onFinish()).catch(() => onFinish());
34
+ }
35
+ export function createAnimations(animations) {
36
+ let isHydratingGlobal;
37
+ const hydratingComponents = new Set();
38
+ return {
39
+ View: MotionView,
40
+ Text: MotionText,
41
+ isReactNative: false,
42
+ inputStyle: 'css',
43
+ outputStyle: 'inline',
44
+ avoidReRenders: true,
45
+ animations,
46
+ usePresence,
47
+ ResetPresence,
48
+ onMount() {
49
+ isHydratingGlobal = false;
50
+ hydratingComponents.forEach((cb) => cb());
51
+ },
52
+ useAnimations: (animationProps) => {
53
+ if (isHydratingGlobal === undefined && !getConfig().settings.disableSSR) {
54
+ isHydratingGlobal = true;
55
+ }
56
+ const { props, style, componentState, stateRef, useStyleEmitter, presence, styleProps, } = animationProps;
57
+ const animationKey = Array.isArray(props.transition)
58
+ ? props.transition[0]
59
+ : props.transition;
60
+ const isComponentHydrating = componentState.unmounted === true;
61
+ const isMounting = componentState.unmounted === 'should-enter';
62
+ const isEntering = !!componentState.unmounted;
63
+ const isExiting = presence?.[0] === false;
64
+ const sendExitComplete = presence?.[1];
65
+ // single consolidated ref with lazy init
66
+ const refs = useRef(null);
67
+ if (!refs.current) {
68
+ refs.current = {
69
+ isFirstRender: true,
70
+ lastDoAnimate: null,
71
+ lastDontAnimate: null,
72
+ controls: null,
73
+ lastAnimateAt: 0,
74
+ disposed: false,
75
+ wasExiting: false,
76
+ isExiting: false,
77
+ sendExitComplete: undefined,
78
+ animationState: 'default',
79
+ frozenExitTarget: null,
80
+ exitCompleteScheduled: false,
81
+ wasEntering: false,
82
+ wasDisabled: false,
83
+ wasNoClass: !!styleProps.noClass,
84
+ };
85
+ }
86
+ // track entering state transitions
87
+ const justFinishedEntering = refs.current.wasEntering && !isEntering;
88
+ useEffect(() => {
89
+ refs.current.wasEntering = isEntering;
90
+ });
91
+ // determine animation state for enter/exit transitions
92
+ const animationState = isExiting
93
+ ? 'exit'
94
+ : isMounting || justFinishedEntering
95
+ ? 'enter'
96
+ : 'default';
97
+ // disable animation during hydration and mounting (prevents "flying across the page")
98
+ const disableAnimation = isComponentHydrating || isMounting || !animationKey;
99
+ const [scope, animate] = useAnimateSSRSafe();
100
+ // sync ref values for reliable access from callbacks
101
+ refs.current.isExiting = isExiting;
102
+ refs.current.sendExitComplete = sendExitComplete;
103
+ refs.current.animationState = animationState;
104
+ // detect transition into exiting state
105
+ const justStartedExiting = isExiting && !refs.current.wasExiting;
106
+ const justStoppedExiting = !isExiting && refs.current.wasExiting;
107
+ // freeze exit animation target so direction changes don't reverse mid-exit
108
+ if (justStartedExiting || justStoppedExiting) {
109
+ refs.current.frozenExitTarget = null;
110
+ refs.current.exitCompleteScheduled = false;
111
+ }
112
+ // track previous exiting state
113
+ useEffect(() => {
114
+ refs.current.wasExiting = isExiting;
115
+ });
116
+ const { dontAnimate = {}, doAnimate, animationOptions, } = getMotionAnimatedProps(props, style, disableAnimation, animationState);
117
+ const [firstRenderStyle] = useState(style);
118
+ // avoid first render returning wrong styles - always render all, after that we can just mutate
119
+ if (refs.current.isFirstRender) {
120
+ refs.current.lastDontAnimate = firstRenderStyle;
121
+ }
122
+ const [isHydrating, setIsHydrating] = useState(isHydratingGlobal);
123
+ useLayoutEffect(() => {
124
+ if (isHydratingGlobal) {
125
+ hydratingComponents.add(() => {
126
+ setIsHydrating(false);
127
+ });
128
+ }
129
+ return () => {
130
+ refs.current.disposed = true;
131
+ };
132
+ }, []);
133
+ const flushAnimation = ({ doAnimate: doAnimateRaw = {}, animationOptions: passedOptions = {}, dontAnimate, }) => {
134
+ // track whether THIS flush starts a new animation (vs using stale controls)
135
+ let startedControls = null;
136
+ // read current state from refs (closure variables can be stale)
137
+ const isCurrentlyExiting = refs.current.isExiting;
138
+ const currentSendExitComplete = refs.current.sendExitComplete;
139
+ // freeze exit target: once the first exit animation starts, subsequent
140
+ // renders (e.g. direction change) should not reverse the exit animation.
141
+ let doAnimate = doAnimateRaw;
142
+ if (isCurrentlyExiting && refs.current.frozenExitTarget) {
143
+ doAnimate = refs.current.frozenExitTarget;
144
+ }
145
+ // only recompute animation options for exit animations to avoid stale state.
146
+ const animationOptions = isCurrentlyExiting && currentSendExitComplete
147
+ ? getAnimationOptions(props.transition ?? null, 'exit')
148
+ : passedOptions;
149
+ try {
150
+ const node = stateRef.current.host;
151
+ // on first render, reset stale animation refs - they can persist if component
152
+ // instance is reused (e.g. AnimatePresence keepChildrenMounted)
153
+ if (refs.current.isFirstRender) {
154
+ refs.current.lastDontAnimate = null;
155
+ refs.current.lastDoAnimate = null;
156
+ }
157
+ if (process.env.NODE_ENV === 'development') {
158
+ if (props['debug'] && props['debug'] !== 'profile') {
159
+ console.groupCollapsed(`[motion] animate (${JSON.stringify(getDiff(refs.current.lastDoAnimate, doAnimate), null, 2)})`);
160
+ console.info({
161
+ props,
162
+ componentState,
163
+ doAnimate,
164
+ dontAnimate,
165
+ animationOptions,
166
+ animationProps,
167
+ lastDoAnimate: { ...refs.current.lastDoAnimate },
168
+ lastDontAnimate: { ...refs.current.lastDontAnimate },
169
+ isExiting,
170
+ style,
171
+ node,
172
+ });
173
+ console.groupCollapsed(`trace >`);
174
+ console.trace();
175
+ console.groupEnd();
176
+ console.groupEnd();
177
+ }
178
+ }
179
+ if (!(node instanceof HTMLElement)) {
180
+ return;
181
+ }
182
+ // handle case where dontAnimate changes
183
+ const prevDont = refs.current.lastDontAnimate;
184
+ if (dontAnimate) {
185
+ if (prevDont) {
186
+ removeRemovedStyles(prevDont, dontAnimate, node, doAnimate);
187
+ const changed = getDiff(prevDont, dontAnimate);
188
+ if (changed) {
189
+ Object.assign(node.style, changed);
190
+ }
191
+ }
192
+ else {
193
+ Object.assign(node.style, dontAnimate);
194
+ }
195
+ }
196
+ if (doAnimate) {
197
+ // when a property moves from dontAnimate to doAnimate, preserve
198
+ // the current inline style value so WAAPI starts from the right place
199
+ if (prevDont) {
200
+ for (const key in prevDont) {
201
+ if (key in doAnimate) {
202
+ node.style[key] = prevDont[key];
203
+ if (refs.current.lastDoAnimate) {
204
+ refs.current.lastDoAnimate[key] = prevDont[key];
205
+ }
206
+ }
207
+ }
208
+ }
209
+ const lastAnimated = refs.current.lastDoAnimate;
210
+ if (lastAnimated) {
211
+ removeRemovedStyles(lastAnimated, doAnimate, node, dontAnimate);
212
+ }
213
+ const diff = getDiff(refs.current.lastDoAnimate, doAnimate);
214
+ if (diff) {
215
+ // capture frozen exit target on first exit diff
216
+ if (isCurrentlyExiting && !refs.current.frozenExitTarget) {
217
+ refs.current.frozenExitTarget = { ...doAnimate };
218
+ }
219
+ // capture mid-flight values so we can provide explicit [from, to]
220
+ // keyframes to WAAPI, ensuring smooth interpolation from the
221
+ // current visual state.
222
+ //
223
+ // only stop() during exit — for non-exit cases, WAAPI
224
+ // naturally replaces only conflicting property animations,
225
+ // letting non-conflicting ones (like an in-flight enter
226
+ // opacity animation) continue to completion.
227
+ const isPopperPosition = node.hasAttribute('data-popper-animate-position');
228
+ let midFlightValues = null;
229
+ if (refs.current.controls) {
230
+ try {
231
+ const computed = getComputedStyle(node);
232
+ midFlightValues = {};
233
+ for (const key in diff) {
234
+ const val = computed[key];
235
+ if (val !== undefined && val !== '') {
236
+ midFlightValues[key] = val;
237
+ }
238
+ }
239
+ }
240
+ catch {
241
+ // getComputedStyle can fail on detached nodes
242
+ }
243
+ if (isCurrentlyExiting) {
244
+ refs.current.controls.stop();
245
+ }
246
+ // write mid-flight values to inline so the 1-frame gap
247
+ // (while motion resolves keyframes) shows the correct
248
+ // position instead of stale inline styles
249
+ if (midFlightValues) {
250
+ for (const key in midFlightValues) {
251
+ ;
252
+ node.style[key] = midFlightValues[key];
253
+ }
254
+ }
255
+ // for popper position elements, cancel WAAPI animations
256
+ // directly so motion.dev's internal stop() sees "idle" state
257
+ // and skips commitStyles. without this, commitStyles writes
258
+ // a mid-flight transform that's visible for 1 frame before
259
+ // the new animation starts, causing a flash toward (0,0).
260
+ if (isPopperPosition) {
261
+ const anims = node.getAnimations();
262
+ for (const anim of anims) {
263
+ anim.cancel();
264
+ }
265
+ }
266
+ }
267
+ const fixedDiff = fixTransparentColors(diff, refs.current.lastDoAnimate, doAnimate);
268
+ // provide explicit [from, to] keyframe for transforms during
269
+ // mid-flight interruption so motion starts from the right place —
270
+ // but ONLY when we've torn down the previous animation (popper
271
+ // cancel above, or exit stop()). otherwise the prior WAAPI
272
+ // transform animation is still running, and pinning a one-frame-
273
+ // stale `from` matrix makes each new flush re-start the animation
274
+ // from that stale base instead of continuing from the live value.
275
+ // for a plain transition element being interrupted repeatedly
276
+ // (the hanzogui.dev logo dot swept back and forth — worse the more
277
+ // concurrent React work the page is doing, since each render flushes
278
+ // again) that reads as a constant stutter/reset instead of a smooth
279
+ // glide. in the un-torn-down case motion's resolver already
280
+ // interpolates from the live value, so leave it alone. (regressed in
281
+ // 9485bcef0e when this keyframe was ungated; covered by
282
+ // LogoDotInterrupt.animated.test.tsx)
283
+ //
284
+ // note: an earlier version also keyframed entering-presence-child
285
+ // interrupts, but enter no longer tears down (see the stop() comment
286
+ // above — WAAPI replaces conflicting props per-property), so those
287
+ // now rely on the same live-value interpolation. covered by
288
+ // PopoverClickDuringEnter / AnimatePresenceEnterExit.
289
+ if ((isPopperPosition || isCurrentlyExiting) &&
290
+ midFlightValues?.transform &&
291
+ fixedDiff.transform) {
292
+ fixedDiff.transform = [midFlightValues.transform, fixedDiff.transform];
293
+ }
294
+ startedControls = animate(scope.current, fixedDiff, animationOptions);
295
+ refs.current.controls = startedControls;
296
+ refs.current.lastAnimateAt = Date.now();
297
+ }
298
+ }
299
+ refs.current.lastDontAnimate = dontAnimate ? { ...dontAnimate } : {};
300
+ refs.current.lastDoAnimate = doAnimate ? { ...doAnimate } : {};
301
+ }
302
+ finally {
303
+ // exit completion: notify AnimatePresence when exit animation finishes
304
+ if (isCurrentlyExiting && currentSendExitComplete) {
305
+ if (startedControls) {
306
+ // new animation started — attach completion handler
307
+ refs.current.exitCompleteScheduled = true;
308
+ startedControls.finished
309
+ .then(() => {
310
+ // guard: only complete if still exiting (prevents stale promise
311
+ // from calling sendExitComplete after a re-entry cancels the exit)
312
+ if (refs.current.isExiting) {
313
+ currentSendExitComplete();
314
+ }
315
+ })
316
+ .catch(() => {
317
+ if (refs.current.isExiting) {
318
+ currentSendExitComplete();
319
+ }
320
+ });
321
+ }
322
+ else if (!refs.current.exitCompleteScheduled) {
323
+ // no animation started AND none previously scheduled (e.g. diff=null
324
+ // on re-render mid-exit because frozenExitTarget matches lastDoAnimate)
325
+ // — complete immediately only if we've never started an exit animation
326
+ currentSendExitComplete();
327
+ }
328
+ // else: exit animation already scheduled via a previous flush,
329
+ // its .finished promise will call sendExitComplete when done
330
+ }
331
+ }
332
+ };
333
+ useStyleEmitter?.((nextStyle, effectiveTransition) => {
334
+ const animationProps = getMotionAnimatedProps(props, nextStyle, disableAnimation, refs.current.animationState, effectiveTransition);
335
+ flushAnimation(animationProps);
336
+ });
337
+ useIsomorphicLayoutEffect(() => {
338
+ if (refs.current.isFirstRender) {
339
+ refs.current.isFirstRender = false;
340
+ refs.current.wasDisabled = disableAnimation;
341
+ // during hydration, skip inline style writes entirely — SSR CSS
342
+ // already has the correct values. writing them again as inline
343
+ // styles triggers browser style recalc that causes visible font
344
+ // flashes (fontWeight, fontSize, letterSpacing, lineHeight).
345
+ // we only need to track refs for future animation diffing.
346
+ if (isHydrating) {
347
+ if (doAnimate && Object.keys(doAnimate).length > 0) {
348
+ refs.current.lastDoAnimate = { ...doAnimate };
349
+ }
350
+ else {
351
+ refs.current.lastDoAnimate = dontAnimate ? { ...dontAnimate } : {};
352
+ }
353
+ refs.current.lastDontAnimate = dontAnimate ? { ...dontAnimate } : {};
354
+ refs.current.lastAnimateAt = Date.now();
355
+ return;
356
+ }
357
+ // after hydration, use simpler logic
358
+ refs.current.lastDontAnimate = dontAnimate ? { ...dontAnimate } : {};
359
+ refs.current.lastDoAnimate = doAnimate ? { ...doAnimate } : {};
360
+ return;
361
+ }
362
+ // when animations first turn on after the mount/hydration handoff, the
363
+ // element is already at its resting position (SSR atomic class, or the
364
+ // dontAnimate inline styles). animating now would spring from the lost
365
+ // "from" value — which for a transform reads as 0 and flashes the
366
+ // element across the screen (e.g. progress bar flashing full, #4011).
367
+ // jump straight to the resolved styles instead, so it renders at the
368
+ // right place with no enter animation. only real changes after this
369
+ // animate. components with an explicit enter animation still animate.
370
+ const justEnabled = refs.current.wasDisabled && !disableAnimation;
371
+ refs.current.wasDisabled = disableAnimation;
372
+ // SSR hydration handoff: styleProps.noClass flips false -> true on the
373
+ // render where the core strips the SSR atomic classes and moves every
374
+ // style inline (outputStyle: 'inline'). the style delta is huge but the
375
+ // visual state is unchanged — animating it would spring every property
376
+ // from the stripped (zeroed) computed values, visibly collapsing and
377
+ // re-growing SSR-painted elements right after load. jump instead: the
378
+ // inline writes below run pre-paint, so the strip is never visible.
379
+ const justStrippedClasses = !refs.current.wasNoClass && !!styleProps.noClass;
380
+ refs.current.wasNoClass = !!styleProps.noClass;
381
+ if ((justEnabled || justStrippedClasses) && animationState !== 'enter') {
382
+ const node = stateRef.current.host;
383
+ if (node instanceof HTMLElement) {
384
+ if (dontAnimate)
385
+ Object.assign(node.style, dontAnimate);
386
+ if (doAnimate)
387
+ Object.assign(node.style, doAnimate);
388
+ }
389
+ refs.current.lastDontAnimate = dontAnimate ? { ...dontAnimate } : {};
390
+ refs.current.lastDoAnimate = doAnimate ? { ...doAnimate } : {};
391
+ return;
392
+ }
393
+ flushAnimation({
394
+ doAnimate,
395
+ dontAnimate,
396
+ animationOptions,
397
+ });
398
+ }, [style, isExiting, disableAnimation, styleProps.noClass]);
399
+ if (process.env.NODE_ENV === 'development') {
400
+ if (props['debug'] && props['debug'] !== 'profile') {
401
+ console.groupCollapsed(`[motion] render`);
402
+ console.info({
403
+ style,
404
+ doAnimate,
405
+ dontAnimate,
406
+ scope,
407
+ animationOptions,
408
+ isExiting,
409
+ isFirstRender: refs.current.isFirstRender,
410
+ animationProps,
411
+ });
412
+ console.groupEnd();
413
+ }
414
+ }
415
+ return {
416
+ style: firstRenderStyle,
417
+ ref: scope,
418
+ render: 'div',
419
+ };
420
+ },
421
+ useAnimatedNumber(initial) {
422
+ const motionValue = useMotionValue(initial);
423
+ return React.useMemo(() => ({
424
+ getInstance() {
425
+ return motionValue;
426
+ },
427
+ getValue() {
428
+ return motionValue.get();
429
+ },
430
+ setValue(next, config = { type: 'spring' }, onFinish) {
431
+ if (config.type === 'direct') {
432
+ MotionValueStrategy.set(motionValue, { type: 'direct' });
433
+ motionValue.set(next);
434
+ onFinish?.();
435
+ return;
436
+ }
437
+ MotionValueStrategy.set(motionValue, config);
438
+ // we intentionally DO NOT animate the motion value itself here
439
+ // (via framer-motion's imperative animate(motionValue, next)).
440
+ // doing so drives the JS value over time, which fires a 'change'
441
+ // event per frame, and each change event kicks off a new DOM
442
+ // animate(node, ...) that cancels the previous one — the DOM
443
+ // never reaches the target (double-animation stall).
444
+ //
445
+ // instead we jump the motion value to `next` synchronously. the
446
+ // animated component's change handler receives a single change
447
+ // event, computes the final webStyle, and drives the visible
448
+ // animation via DOM animate(node, webStyle, springConfig). that
449
+ // DOM animation is the real timing source.
450
+ //
451
+ // to make `onFinish` resolve when the VISIBLE animation finishes
452
+ // (not synchronously on the change event), we stash it in
453
+ // PendingMotionOnFinish here and the change handler chains it to
454
+ // the DOM animate() controls.
455
+ if (onFinish) {
456
+ // if a previous setValue is still pending on this motion value,
457
+ // fire it now — the new setValue will cancel the prior DOM
458
+ // animation, and the caller is still owed a completion signal.
459
+ const prior = PendingMotionOnFinish.get(motionValue);
460
+ if (prior) {
461
+ PendingMotionOnFinish.delete(motionValue);
462
+ prior();
463
+ }
464
+ PendingMotionOnFinish.set(motionValue, onFinish);
465
+ }
466
+ motionValue.set(next);
467
+ },
468
+ stop() {
469
+ motionValue.stop();
470
+ },
471
+ }), [motionValue]);
472
+ },
473
+ useAnimatedNumberReaction({ value }, onValue) {
474
+ const instance = value.getInstance();
475
+ useMotionValueEvent(instance, 'change', onValue);
476
+ },
477
+ useAnimatedNumberStyle(val, getStyleProp) {
478
+ const motionValue = val.getInstance();
479
+ const getStyleRef = useRef(getStyleProp);
480
+ // we need to change useAnimatedNumberStyle to have dep args to be concurrent safe
481
+ getStyleRef.current = getStyleProp;
482
+ return useMemo(() => {
483
+ return {
484
+ getStyle: (cur) => {
485
+ return getStyleRef.current(cur);
486
+ },
487
+ motionValue,
488
+ };
489
+ }, []);
490
+ },
491
+ useAnimatedNumbersStyle(vals, getStyleProp) {
492
+ const motionValues = vals.map((v) => v.getInstance());
493
+ const getStyleRef = useRef(getStyleProp);
494
+ getStyleRef.current = getStyleProp;
495
+ return useMemo(() => {
496
+ return {
497
+ getStyle: (...currentValues) => getStyleRef.current(...currentValues),
498
+ motionValues,
499
+ };
500
+ }, []);
501
+ },
502
+ };
503
+ function getMotionAnimatedProps(props, style, disable, animationState = 'default', transitionOverride) {
504
+ if (disable) {
505
+ return {
506
+ dontAnimate: style,
507
+ };
508
+ }
509
+ const animationOptions = getAnimationOptions(transitionOverride ?? props.transition ?? null, animationState);
510
+ let dontAnimate;
511
+ let doAnimate;
512
+ const animateOnly = props.animateOnly;
513
+ for (const key in style) {
514
+ const value = style[key];
515
+ if (disableAnimationProps.has(key) || (animateOnly && !animateOnly.includes(key))) {
516
+ dontAnimate || (dontAnimate = {});
517
+ dontAnimate[key] = value;
518
+ }
519
+ else {
520
+ doAnimate || (doAnimate = {});
521
+ doAnimate[key] = value;
522
+ }
523
+ }
524
+ return {
525
+ dontAnimate,
526
+ doAnimate,
527
+ animationOptions,
528
+ };
529
+ }
530
+ function getAnimationOptions(transitionProp, animationState = 'default') {
531
+ const normalized = normalizeTransition(transitionProp);
532
+ let effectiveKey = getEffectiveAnimation(normalized, animationState);
533
+ // fallback: if we have enter/exit defined but state is 'default' and no default key,
534
+ // use enter timing as fallback to avoid empty animation options
535
+ if (!effectiveKey && animationState === 'default') {
536
+ effectiveKey = normalized.enter || normalized.exit || null;
537
+ }
538
+ const globalConfigOverride = normalized.config
539
+ ? { ...normalized.config }
540
+ : undefined;
541
+ if (!effectiveKey &&
542
+ Object.keys(normalized.properties).length === 0 &&
543
+ !globalConfigOverride) {
544
+ return {};
545
+ }
546
+ const defaultConfig = effectiveKey ? withInferredType(animations[effectiveKey]) : null;
547
+ const delay = normalized.delay;
548
+ // framer motion's animate() expects default config at the TOP LEVEL
549
+ const result = {};
550
+ if (defaultConfig) {
551
+ Object.assign(result, defaultConfig);
552
+ }
553
+ if (globalConfigOverride) {
554
+ Object.assign(result, globalConfigOverride);
555
+ if (result.type === undefined &&
556
+ result.duration !== undefined &&
557
+ result.damping === undefined &&
558
+ result.stiffness === undefined &&
559
+ result.mass === undefined) {
560
+ result.type = 'tween';
561
+ }
562
+ }
563
+ if (delay) {
564
+ result.delay = delay;
565
+ }
566
+ if (defaultConfig || globalConfigOverride || delay) {
567
+ result.default = {
568
+ ...defaultConfig,
569
+ ...globalConfigOverride,
570
+ ...(delay ? { delay } : null),
571
+ };
572
+ }
573
+ for (const [propName, animationNameOrConfig] of Object.entries(normalized.properties)) {
574
+ if (typeof animationNameOrConfig === 'string') {
575
+ result[propName] = withInferredType(animations[animationNameOrConfig]);
576
+ }
577
+ else if (animationNameOrConfig && typeof animationNameOrConfig === 'object') {
578
+ const baseConfig = animationNameOrConfig.type
579
+ ? withInferredType(animations[animationNameOrConfig.type])
580
+ : defaultConfig;
581
+ result[propName] = {
582
+ ...baseConfig,
583
+ ...animationNameOrConfig,
584
+ };
585
+ }
586
+ }
587
+ // we standardize to ms across drivers, motion expects s
588
+ convertMsToS(result);
589
+ convertMsToS(result.default);
590
+ for (const key in result) {
591
+ if (key !== 'default' && typeof result[key] === 'object') {
592
+ convertMsToS(result[key]);
593
+ }
594
+ }
595
+ return result;
596
+ }
597
+ }
598
+ function withInferredType(config) {
599
+ if (!config) {
600
+ return { type: 'spring' };
601
+ }
602
+ const isTimingBased = config.duration !== undefined &&
603
+ config.damping === undefined &&
604
+ config.stiffness === undefined &&
605
+ config.mass === undefined;
606
+ return { type: isTimingBased ? 'tween' : 'spring', ...config };
607
+ }
608
+ function convertMsToS(config) {
609
+ if (!config)
610
+ return;
611
+ if (typeof config.delay === 'number')
612
+ config.delay = config.delay / 1000;
613
+ if (typeof config.duration === 'number') {
614
+ const isTimingBased = config.type === 'tween' ||
615
+ (config.type === undefined &&
616
+ config.damping === undefined &&
617
+ config.stiffness === undefined &&
618
+ config.mass === undefined);
619
+ if (isTimingBased) {
620
+ config.duration = config.duration / 1000;
621
+ }
622
+ }
623
+ }
624
+ function removeRemovedStyles(prev, next, node, dontClearIfIn) {
625
+ for (const key in prev) {
626
+ if (!(key in next)) {
627
+ if (dontClearIfIn && key in dontClearIfIn) {
628
+ continue;
629
+ }
630
+ node.style[key] = '';
631
+ }
632
+ }
633
+ }
634
+ // truly non-animatable CSS properties (discrete, keyword-based, no interpolation)
635
+ // properties like margin, maxHeight, zIndex, etc are animatable and intentionally excluded
636
+ export const disableAnimationProps = new Set([
637
+ 'alignContent',
638
+ 'alignItems',
639
+ 'boxSizing',
640
+ 'contain',
641
+ 'containerType',
642
+ 'display',
643
+ 'flexBasis',
644
+ 'flexDirection',
645
+ 'fontFamily',
646
+ 'justifyContent',
647
+ 'overflow',
648
+ 'overflowX',
649
+ 'overflowY',
650
+ 'pointerEvents',
651
+ 'position',
652
+ 'textWrap',
653
+ 'userSelect',
654
+ ]);
655
+ const MotionView = createMotionView('div');
656
+ const MotionText = createMotionView('span');
657
+ function createMotionView(defaultTag) {
658
+ const isText = defaultTag === 'span';
659
+ const Component = forwardRef((propsIn, ref) => {
660
+ const { forwardedRef, animation, render = defaultTag, style, ...propsRest } = propsIn;
661
+ const [scope, animate] = useAnimateSSRSafe();
662
+ const hostRef = useRef(null);
663
+ const composedRefs = useComposedRefs(forwardedRef, ref, hostRef, scope);
664
+ const stateRef = useRef(null);
665
+ if (!stateRef.current) {
666
+ stateRef.current = {
667
+ get host() {
668
+ return hostRef.current;
669
+ },
670
+ };
671
+ }
672
+ const [_, state] = useThemeWithState({});
673
+ const styles = Array.isArray(style) ? style : [style];
674
+ const [animatedStyle, nonAnimatedStyles] = (() => {
675
+ let animatedStyle;
676
+ const nonAnimatedStyles = [];
677
+ for (const style of styles) {
678
+ if (style.getStyle) {
679
+ animatedStyle = style;
680
+ }
681
+ else {
682
+ nonAnimatedStyles.push(style);
683
+ }
684
+ }
685
+ return [animatedStyle, nonAnimatedStyles];
686
+ })();
687
+ function getProps(props) {
688
+ const out = getSplitStyles(props, isText ? Text.staticConfig : View.staticConfig, state?.theme, state?.name, {
689
+ unmounted: false,
690
+ }, {
691
+ isAnimated: false,
692
+ noClass: true,
693
+ resolveValues: 'auto',
694
+ });
695
+ if (!out) {
696
+ return {};
697
+ }
698
+ if (out.viewProps.style) {
699
+ fixStyles(out.viewProps.style);
700
+ styleToCSS(out.viewProps.style);
701
+ }
702
+ return out.viewProps;
703
+ }
704
+ const props = getProps({ ...propsRest, style: nonAnimatedStyles });
705
+ const Element = render || 'div';
706
+ const transformedProps = hooks.usePropsTransform?.(render, props, stateRef, false);
707
+ useEffect(() => {
708
+ if (!animatedStyle)
709
+ return;
710
+ // multi-value path: subscribe to all motion values
711
+ if (animatedStyle.motionValues) {
712
+ const mvs = animatedStyle.motionValues;
713
+ const unsubs = mvs.map((mv) => mv.on('change', () => {
714
+ const currentValues = mvs.map((v) => v.get());
715
+ const nextStyle = animatedStyle.getStyle(...currentValues);
716
+ const animationConfig = MotionValueStrategy.get(mv);
717
+ const node = hostRef.current;
718
+ const webStyle = getProps({ style: nextStyle }).style;
719
+ if (webStyle && node instanceof HTMLElement) {
720
+ const motionAnimationConfig = animationConfig?.type === 'timing'
721
+ ? { type: 'tween', duration: (animationConfig?.duration || 0) / 1000 }
722
+ : animationConfig?.type === 'direct'
723
+ ? { type: 'tween', duration: 0 }
724
+ : { type: 'spring', ...animationConfig };
725
+ const controls = animate(node, webStyle, motionAnimationConfig);
726
+ settlePendingMotionOnFinish(mv, controls);
727
+ }
728
+ }));
729
+ return () => unsubs.forEach((fn) => fn());
730
+ }
731
+ // single-value path
732
+ if (!animatedStyle.motionValue)
733
+ return;
734
+ return animatedStyle.motionValue.on('change', (value) => {
735
+ const nextStyle = animatedStyle.getStyle(value);
736
+ const animationConfig = MotionValueStrategy.get(animatedStyle.motionValue);
737
+ const node = hostRef.current;
738
+ const webStyle = getProps({ style: nextStyle }).style;
739
+ if (webStyle && node instanceof HTMLElement) {
740
+ const motionAnimationConfig = animationConfig?.type === 'timing'
741
+ ? {
742
+ type: 'tween',
743
+ duration: (animationConfig?.duration || 0) / 1000,
744
+ }
745
+ : animationConfig?.type === 'direct'
746
+ ? { type: 'tween', duration: 0 }
747
+ : {
748
+ type: 'spring',
749
+ ...animationConfig,
750
+ };
751
+ const controls = animate(node, webStyle, motionAnimationConfig);
752
+ settlePendingMotionOnFinish(animatedStyle.motionValue, controls);
753
+ }
754
+ });
755
+ }, [animatedStyle]);
756
+ return _jsx(Element, { ...transformedProps, ref: composedRefs });
757
+ });
758
+ Component['acceptRenderProp'] = true;
759
+ return Component;
760
+ }
761
+ function getDiff(previous, next) {
762
+ if (!previous) {
763
+ return next;
764
+ }
765
+ let diff = null;
766
+ for (const key in next) {
767
+ if (next[key] !== previous[key]) {
768
+ diff || (diff = {});
769
+ diff[key] = next[key];
770
+ }
771
+ }
772
+ return diff;
773
+ }
774
+ // motion.dev can't animate to "transparent" - convert it to rgba
775
+ // try to extract RGB from previous or next value for smooth color transitions
776
+ function fixTransparentColors(diff, previous, next) {
777
+ let result = diff;
778
+ for (const key in diff) {
779
+ if (diff[key] === 'transparent') {
780
+ let fixed = 'rgba(0, 0, 0, 0)';
781
+ const candidates = [previous?.[key], next?.[key]];
782
+ for (const candidate of candidates) {
783
+ if (typeof candidate === 'string' && candidate !== 'transparent') {
784
+ const rgbaMatch = candidate.match(/^rgba?\(([^,]+),\s*([^,]+),\s*([^,)]+)/);
785
+ if (rgbaMatch) {
786
+ fixed = `rgba(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]}, 0)`;
787
+ break;
788
+ }
789
+ }
790
+ }
791
+ if (result === diff) {
792
+ result = { ...diff };
793
+ }
794
+ result[key] = fixed;
795
+ }
796
+ }
797
+ return result;
798
+ }
799
+ //# sourceMappingURL=createAnimations.js.map