@tamagui/animations-reanimated 2.4.6 → 2.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.
@@ -39,6 +39,7 @@ module.exports = __toCommonJS(createAnimations_exports);
39
39
  var import_animation_helpers = require("@tamagui/animation-helpers");
40
40
  var import_core = require("@tamagui/core");
41
41
  var import_use_presence = require("@tamagui/use-presence");
42
+ var import_normalize_colors = __toESM(require("@react-native/normalize-colors"), 1);
42
43
  var import_react = __toESM(require("react"), 1);
43
44
  var import_react_native_reanimated = __toESM(require("react-native-reanimated"), 1);
44
45
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -80,9 +81,162 @@ const cloneAnimationValue = value => {
80
81
  }
81
82
  return value;
82
83
  };
84
+ const cloneStyleRecord = style => {
85
+ const next = {};
86
+ for (const key in style) {
87
+ next[key] = cloneAnimationValue(style[key]);
88
+ }
89
+ return next;
90
+ };
83
91
  const cloneTransitionConfig = config => {
84
92
  return cloneAnimationValue(config);
85
93
  };
94
+ const getAnimatedTransforms = value => {
95
+ if (!Array.isArray(value)) return [];
96
+ return value.filter(item => item !== null && typeof item === "object" && !Array.isArray(item));
97
+ };
98
+ const getRemovedAnimatedKeys = (current, previous) => {
99
+ const removedKeys = {};
100
+ for (const key of previous) {
101
+ if (!current.has(key)) removedKeys[key] = true;
102
+ }
103
+ return removedKeys;
104
+ };
105
+ const getImplicitDefault = (key, targetValue) => {
106
+ "worklet";
107
+
108
+ const value = key === "opacity" || key === "scale" || key === "scaleX" || key === "scaleY" ? 1 : 0;
109
+ if (typeof targetValue !== "string") return value;
110
+ let suffixIndex = 0;
111
+ while (suffixIndex < targetValue.length) {
112
+ const character = targetValue[suffixIndex];
113
+ const code = character.charCodeAt(0);
114
+ if (code >= 48 && code <= 57 || character === "." || character === "-" || character === "+") {
115
+ suffixIndex++;
116
+ } else {
117
+ break;
118
+ }
119
+ }
120
+ return suffixIndex > 0 ? `${value}${targetValue.slice(suffixIndex)}` : value;
121
+ };
122
+ const COLOR_STYLE_KEYS = {
123
+ backgroundColor: true,
124
+ borderColor: true,
125
+ borderLeftColor: true,
126
+ borderRightColor: true,
127
+ borderTopColor: true,
128
+ borderBottomColor: true,
129
+ color: true
130
+ };
131
+ const toReanimatedColor = value => {
132
+ "worklet";
133
+
134
+ if (typeof value === "number") {
135
+ const color = value >>> 0;
136
+ const alpha = (color >>> 24 & 255) / 255;
137
+ const red = color >>> 16 & 255;
138
+ const green = color >>> 8 & 255;
139
+ const blue = color & 255;
140
+ return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
141
+ }
142
+ if (typeof value !== "string") return value;
143
+ const trimmed = value.trim();
144
+ if (trimmed === "transparent") return "rgba(0, 0, 0, 0)";
145
+ return trimmed;
146
+ };
147
+ const isReanimatedColorHistory = value => {
148
+ "worklet";
149
+
150
+ const normalized = toReanimatedColor(value);
151
+ return typeof normalized === "string" && normalized.startsWith("rgba(");
152
+ };
153
+ const normalizeAnimationColor = value => {
154
+ if (typeof value === "number") {
155
+ return toReanimatedColor(value);
156
+ }
157
+ if (typeof value !== "string") return;
158
+ const color = (0, import_normalize_colors.default)(value);
159
+ if (color == null) return;
160
+ const red = Math.round((color & 4278190080) >>> 24);
161
+ const green = Math.round((color & 16711680) >>> 16);
162
+ const blue = Math.round((color & 65280) >>> 8);
163
+ const alpha = (color & 255) / 255;
164
+ return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
165
+ };
166
+ const buildSnapshot = (animated, statics, transforms, previousKeys, lastPainted) => {
167
+ const snapshotAnimated = {};
168
+ const snapshotStatics = cloneStyleRecord(statics);
169
+ for (const key in snapshotStatics) {
170
+ if (!COLOR_STYLE_KEYS[key]) continue;
171
+ const normalized = normalizeAnimationColor(snapshotStatics[key]);
172
+ if (normalized !== void 0) snapshotStatics[key] = normalized;
173
+ }
174
+ for (const key in animated) {
175
+ if (key === "transform") continue;
176
+ const value = animated[key];
177
+ if (COLOR_STYLE_KEYS[key]) {
178
+ const normalized = normalizeAnimationColor(value);
179
+ if (normalized === void 0) {
180
+ snapshotStatics[key] = cloneAnimationValue(value);
181
+ continue;
182
+ }
183
+ snapshotAnimated[key] = normalized;
184
+ } else {
185
+ snapshotAnimated[key] = cloneAnimationValue(value);
186
+ }
187
+ }
188
+ const snapshotTransforms = transforms.map(cloneStyleRecord);
189
+ const keys = new Set(Object.keys(snapshotAnimated));
190
+ for (const transform of snapshotTransforms) {
191
+ const key = Object.keys(transform)[0];
192
+ const value = key ? transform[key] : void 0;
193
+ if (key && (typeof value === "number" || typeof value === "string")) {
194
+ keys.add(`transform:${key}`);
195
+ }
196
+ }
197
+ const seeds = {};
198
+ for (const key of keys) {
199
+ let value = lastPainted[key];
200
+ if (typeof value !== "number" && typeof value !== "string") continue;
201
+ if (value === "auto" || typeof value === "string" && value.startsWith("calc")) {
202
+ continue;
203
+ }
204
+ if (COLOR_STYLE_KEYS[key]) value = normalizeAnimationColor(value);
205
+ if (value !== void 0) seeds[key] = value;
206
+ }
207
+ const gatedKeys = {};
208
+ for (const key of keys) gatedKeys[key] = true;
209
+ const removedKeys = getRemovedAnimatedKeys(keys, previousKeys);
210
+ const painted = {
211
+ ...snapshotStatics,
212
+ ...snapshotAnimated
213
+ };
214
+ const staticTransforms = getAnimatedTransforms(snapshotStatics.transform);
215
+ delete painted.transform;
216
+ for (const transform of staticTransforms) {
217
+ const key = Object.keys(transform)[0];
218
+ if (key) painted[`transform:${key}`] = transform[key];
219
+ }
220
+ for (const transform of snapshotTransforms) {
221
+ const key = Object.keys(transform)[0];
222
+ if (key) painted[`transform:${key}`] = transform[key];
223
+ }
224
+ return {
225
+ value: {
226
+ animated: snapshotAnimated,
227
+ statics: snapshotStatics,
228
+ transforms: snapshotTransforms,
229
+ gatedKeys,
230
+ seeds,
231
+ removedKeys,
232
+ removeTransform: Object.keys(removedKeys).some(key => key.startsWith("transform:")),
233
+ clearValue: import_core.isWeb ? "" : null
234
+ },
235
+ painted,
236
+ keys
237
+ };
238
+ };
239
+ const getGatedKeys = snapshot => Object.keys(snapshot.gatedKeys);
86
240
  const createReanimatedConfig = config => {
87
241
  "worklet";
88
242
 
@@ -97,7 +251,7 @@ const createReanimatedConfig = config => {
97
251
  }
98
252
  return next;
99
253
  };
100
- const applyAnimation = (targetValue, config, callback) => {
254
+ const applyAnimation = (targetValue, config, callback, seedValue, validateStartAsColor = false) => {
101
255
  "worklet";
102
256
 
103
257
  const delay = config.delay;
@@ -108,11 +262,43 @@ const applyAnimation = (targetValue, config, callback) => {
108
262
  } else {
109
263
  animatedValue = (0, import_react_native_reanimated.withSpring)(targetValue, reanimatedConfig, callback);
110
264
  }
265
+ if (seedValue !== void 0 || validateStartAsColor) {
266
+ const innerOnStart = animatedValue.onStart;
267
+ animatedValue.onStart = (animation, value, timestamp, previousAnimation) => {
268
+ "worklet";
269
+
270
+ const startValue = validateStartAsColor ? isReanimatedColorHistory(value) ? toReanimatedColor(value) : seedValue ?? targetValue : seedValue;
271
+ innerOnStart(animation, startValue, timestamp, previousAnimation);
272
+ };
273
+ }
111
274
  if (delay && delay > 0) {
112
275
  animatedValue = (0, import_react_native_reanimated.withDelay)(delay, animatedValue);
113
276
  }
114
277
  return animatedValue;
115
278
  };
279
+ const animateSnapshotValue = (key, implicitKey, targetValue, config, previouslyEmitted, seedValue, gated, currentlyExiting, exitCycleId, currentlyCompletingAnimation, didAnimateCycleId, markExitKeyDone, markDidAnimateKeyDone, validateStartAsColor = false) => {
280
+ "worklet";
281
+
282
+ const cycleGated = gated && (currentlyExiting || currentlyCompletingAnimation);
283
+ if (!previouslyEmitted && seedValue === void 0 && !cycleGated) {
284
+ return targetValue;
285
+ }
286
+ let callback;
287
+ if (gated && currentlyExiting) {
288
+ callback = finished => {
289
+ "worklet";
290
+
291
+ (0, import_react_native_reanimated.runOnJS)(markExitKeyDone)(key, exitCycleId, finished ?? false);
292
+ };
293
+ } else if (gated && currentlyCompletingAnimation) {
294
+ callback = finished => {
295
+ "worklet";
296
+
297
+ (0, import_react_native_reanimated.runOnJS)(markDidAnimateKeyDone)(key, didAnimateCycleId, finished ?? false);
298
+ };
299
+ }
300
+ return applyAnimation(targetValue, config, callback, previouslyEmitted ? void 0 : seedValue ?? getImplicitDefault(implicitKey, targetValue), validateStartAsColor);
301
+ };
116
302
  const ANIMATABLE_PROPERTIES = {
117
303
  // Transform
118
304
  transform: true,
@@ -187,6 +373,23 @@ const canAnimateProperty = (key, value, animateOnly) => {
187
373
  if (animateOnly && !animateOnly.includes(key)) return false;
188
374
  return true;
189
375
  };
376
+ const splitAnimationStyles = (style, isDark, disableAnimation, animateOnly) => {
377
+ const animated = {};
378
+ const statics = {};
379
+ for (const key in style) {
380
+ const value = resolveDynamicValue(style[key], isDark);
381
+ if (value === void 0) continue;
382
+ if (!disableAnimation && canAnimateProperty(key, value, animateOnly)) {
383
+ animated[key] = cloneAnimationValue(value);
384
+ } else {
385
+ statics[key] = cloneAnimationValue(value);
386
+ }
387
+ }
388
+ return {
389
+ animated,
390
+ statics
391
+ };
392
+ };
190
393
  function createWebAnimatedComponent(defaultTag) {
191
394
  const isText = defaultTag === "span";
192
395
  const Component = Animated.createAnimatedComponent((0, import_react.forwardRef)((propsIn, ref) => {
@@ -407,7 +610,8 @@ function createAnimations(animationsConfig) {
407
610
  useStyleEmitter,
408
611
  themeName,
409
612
  stateRef,
410
- styleState
613
+ styleState,
614
+ onDidAnimate
411
615
  } = animationProps;
412
616
  const isHydrating = componentState.unmounted === true;
413
617
  const isMounting = componentState.unmounted === "should-enter";
@@ -425,6 +629,25 @@ function createAnimations(animationsConfig) {
425
629
  const disableAnimation = isHydrating || !animationKey;
426
630
  const isDark = themeName?.startsWith("dark") || false;
427
631
  const sendExitComplete = presence?.[1];
632
+ const onDidAnimateRef = (0, import_react.useRef)(onDidAnimate);
633
+ (0, import_core.useIsomorphicLayoutEffect)(() => {
634
+ onDidAnimateRef.current = onDidAnimate;
635
+ }, [onDidAnimate]);
636
+ const didAnimateCycleIdRef = (0, import_react.useRef)(0);
637
+ const pendingDidAnimateKeysRef = (0, import_react.useRef)(/* @__PURE__ */new Set());
638
+ const didAnimateCompletedRef = (0, import_react.useRef)(false);
639
+ const isCompletingAnimationRef = (0, import_react_native_reanimated.useSharedValue)(false);
640
+ const didAnimateCycleIdShared = (0, import_react_native_reanimated.useSharedValue)(0);
641
+ const markDidAnimateKeyDone = (0, import_core.useEvent)((key, cycleId, finished) => {
642
+ if (!finished || cycleId !== didAnimateCycleIdRef.current) return;
643
+ if (didAnimateCompletedRef.current) return;
644
+ pendingDidAnimateKeysRef.current.delete(key);
645
+ if (pendingDidAnimateKeysRef.current.size === 0) {
646
+ didAnimateCompletedRef.current = true;
647
+ isCompletingAnimationRef.value = false;
648
+ onDidAnimateRef.current?.();
649
+ }
650
+ });
428
651
  const exitCycleIdRef = (0, import_react.useRef)(0);
429
652
  const pendingExitKeysRef = (0, import_react.useRef)(/* @__PURE__ */new Set());
430
653
  const exitCompletedRef = (0, import_react.useRef)(false);
@@ -446,6 +669,9 @@ function createAnimations(animationsConfig) {
446
669
  exitCycleIdRef.current++;
447
670
  exitCompletedRef.current = false;
448
671
  pendingExitKeysRef.current.clear();
672
+ didAnimateCycleIdRef.current++;
673
+ pendingDidAnimateKeysRef.current.clear();
674
+ didAnimateCompletedRef.current = true;
449
675
  }
450
676
  if (justStoppedExiting) {
451
677
  exitCycleIdRef.current++;
@@ -454,52 +680,83 @@ function createAnimations(animationsConfig) {
454
680
  (0, import_core.useIsomorphicLayoutEffect)(() => {
455
681
  isExitingRef.value = isExiting;
456
682
  exitCycleIdShared.value = exitCycleIdRef.current;
457
- }, [isExiting, exitCycleIdRef.current]);
683
+ if (justStartedExiting) {
684
+ isCompletingAnimationRef.value = false;
685
+ didAnimateCycleIdShared.value = didAnimateCycleIdRef.current;
686
+ }
687
+ }, [isExiting, exitCycleIdRef.current, justStartedExiting]);
458
688
  import_react.default.useEffect(() => {
459
689
  wasExitingRef.current = isExiting;
460
690
  });
461
- const animatedTargetsRef = (0, import_react_native_reanimated.useSharedValue)(null);
462
- const staticTargetsRef = (0, import_react_native_reanimated.useSharedValue)(null);
463
- const transformTargetsRef = (0, import_react_native_reanimated.useSharedValue)(null);
691
+ const committedRenderKeysRef = (0, import_react.useRef)(/* @__PURE__ */new Set());
692
+ const lastPaintedRef = (0, import_react.useRef)({});
693
+ const carryRef = (0, import_react.useRef)({});
464
694
  const {
465
695
  animatedStyles,
466
- staticStyles
696
+ staticStyles,
697
+ nextCarry
467
698
  } = (0, import_react.useMemo)(() => {
468
- const animated = {};
469
- const staticStyles2 = {};
470
699
  const animateOnly = props.animateOnly;
471
- for (const key in style) {
472
- const rawValue = style[key];
473
- const value = resolveDynamicValue(rawValue, isDark);
474
- if (value === void 0) continue;
475
- if (disableAnimation) {
476
- staticStyles2[key] = cloneAnimationValue(value);
477
- continue;
478
- }
479
- if (canAnimateProperty(key, value, animateOnly)) {
480
- animated[key] = cloneAnimationValue(value);
481
- } else {
482
- staticStyles2[key] = cloneAnimationValue(value);
483
- }
700
+ const {
701
+ animated,
702
+ statics
703
+ } = splitAnimationStyles(style, isDark, disableAnimation, animateOnly);
704
+ const nextCarry2 = cloneStyleRecord(carryRef.current);
705
+ for (const key in nextCarry2) {
706
+ if (!(key in animated)) delete nextCarry2[key];
484
707
  }
485
- if (isMounting) {
486
- for (const key in animated) {
487
- staticStyles2[key] = animated[key];
708
+ for (const key in animated) {
709
+ if (!(key in nextCarry2)) {
710
+ nextCarry2[key] = cloneAnimationValue(animated[key]);
488
711
  }
712
+ statics[key] = nextCarry2[key];
489
713
  }
490
714
  return {
491
715
  animatedStyles: animated,
492
- staticStyles: staticStyles2
716
+ staticStyles: statics,
717
+ nextCarry: nextCarry2
493
718
  };
494
- }, [disableAnimation, style, isDark, isMounting, props.animateOnly]);
719
+ }, [disableAnimation, style, isDark, props.animateOnly]);
720
+ const renderSnapshot = (0, import_react.useMemo)(() => buildSnapshot(animatedStyles, staticStyles, getAnimatedTransforms(animatedStyles.transform), committedRenderKeysRef.current, lastPaintedRef.current), [animatedStyles, staticStyles]);
721
+ const renderSnapshotRef = (0, import_react_native_reanimated.useSharedValue)({
722
+ animated: {},
723
+ statics: {},
724
+ transforms: [],
725
+ gatedKeys: {},
726
+ seeds: {},
727
+ removedKeys: {},
728
+ removeTransform: false,
729
+ clearValue: import_core.isWeb ? "" : null
730
+ });
731
+ const emitterSnapshotRef = (0, import_react_native_reanimated.useSharedValue)(null);
732
+ const emitterKeysRef = (0, import_react.useRef)(null);
733
+ (0, import_core.useIsomorphicLayoutEffect)(() => {
734
+ carryRef.current = nextCarry;
735
+ committedRenderKeysRef.current = renderSnapshot.keys;
736
+ lastPaintedRef.current = renderSnapshot.painted;
737
+ }, [nextCarry, renderSnapshot]);
495
738
  const pseudoActiveRef = (0, import_react.useRef)(false);
496
739
  const isExitingJSRef = (0, import_react.useRef)(false);
497
740
  isExitingJSRef.current = isExiting;
741
+ const publishedSnapshotRef = (0, import_react.useRef)(null);
498
742
  (0, import_core.useIsomorphicLayoutEffect)(() => {
499
- if ((isExiting || !pseudoActiveRef.current) && animatedTargetsRef.value !== null) {
500
- animatedTargetsRef.value = null;
501
- staticTargetsRef.value = null;
502
- transformTargetsRef.value = null;
743
+ const droppingLatch = (isExiting || !pseudoActiveRef.current) && emitterSnapshotRef.value !== null;
744
+ const emitterKeys = droppingLatch ? emitterKeysRef.current : null;
745
+ if (droppingLatch || publishedSnapshotRef.current !== renderSnapshot.value) {
746
+ const removedKeys = emitterKeys ? {
747
+ ...renderSnapshot.value.removedKeys,
748
+ ...getRemovedAnimatedKeys(renderSnapshot.keys, emitterKeys)
749
+ } : renderSnapshot.value.removedKeys;
750
+ renderSnapshotRef.value = {
751
+ ...renderSnapshot.value,
752
+ removedKeys,
753
+ removeTransform: Object.keys(removedKeys).some(key => key.startsWith("transform:"))
754
+ };
755
+ publishedSnapshotRef.current = renderSnapshot.value;
756
+ }
757
+ if (droppingLatch) {
758
+ emitterSnapshotRef.value = null;
759
+ emitterKeysRef.current = null;
503
760
  }
504
761
  });
505
762
  const {
@@ -535,9 +792,6 @@ function createAnimations(animationsConfig) {
535
792
  if (isExitingJSRef.current) return;
536
793
  pseudoActiveRef.current = pseudoActive === true;
537
794
  const animateOnly = props.animateOnly;
538
- const animated = {};
539
- const statics = {};
540
- const transforms = [];
541
795
  const transitionToUse = effectiveTransition2 ?? props.transition;
542
796
  const {
543
797
  baseConfig: newBase,
@@ -549,69 +803,52 @@ function createAnimations(animationsConfig) {
549
803
  disableAnimation: configRef.value.disableAnimation,
550
804
  isHydrating: configRef.value.isHydrating
551
805
  };
552
- for (const key in nextStyle) {
553
- const rawValue = nextStyle[key];
554
- const value = resolveDynamicValue(rawValue, isDark);
555
- if (value == void 0) continue;
556
- if (configRef.value.disableAnimation) {
557
- statics[key] = cloneAnimationValue(value);
558
- continue;
559
- }
560
- if (key === "transform" && Array.isArray(value)) {
561
- for (const t of value) {
562
- if (t && typeof t === "object") {
563
- const tKey = Object.keys(t)[0];
564
- const tVal = t[tKey];
565
- if (typeof tVal === "number" || typeof tVal === "string") {
566
- transforms.push(cloneAnimationValue(t));
567
- }
568
- }
569
- }
570
- continue;
571
- }
572
- if (canAnimateProperty(key, value, animateOnly)) {
573
- animated[key] = cloneAnimationValue(value);
574
- } else {
575
- statics[key] = cloneAnimationValue(value);
576
- }
577
- }
578
- animatedTargetsRef.value = animated;
579
- staticTargetsRef.value = statics;
580
- transformTargetsRef.value = transforms;
806
+ const previousKeys = emitterKeysRef.current ?? committedRenderKeysRef.current;
807
+ const {
808
+ animated,
809
+ statics
810
+ } = splitAnimationStyles(nextStyle, isDark, configRef.value.disableAnimation, animateOnly);
811
+ const snapshot = buildSnapshot(animated, statics, getAnimatedTransforms(animated.transform), previousKeys, lastPaintedRef.current);
812
+ emitterSnapshotRef.value = snapshot.value;
813
+ emitterKeysRef.current = snapshot.keys;
814
+ lastPaintedRef.current = snapshot.painted;
581
815
  if (process.env.NODE_ENV === "development" && props.debug && props.debug !== "profile") {
582
816
  console.info("[animations-reanimated] useStyleEmitter update", {
583
817
  animated,
584
818
  statics,
585
- transforms
819
+ transforms: snapshot.value.transforms
586
820
  });
587
821
  }
588
822
  });
589
823
  const exitKeysRegistered = (0, import_react.useRef)(false);
590
824
  if (justStartedExiting && sendExitComplete) {
591
- const exitKeys = [];
592
- const animateOnly = props.animateOnly;
593
- for (const key in animatedStyles) {
594
- if (key === "transform") continue;
595
- if (canAnimateProperty(key, animatedStyles[key], animateOnly)) {
596
- exitKeys.push(key);
597
- }
598
- }
599
- const transforms = animatedStyles.transform;
600
- if (transforms && Array.isArray(transforms)) {
601
- for (const t of transforms) {
602
- if (!t) continue;
603
- const tKey = Object.keys(t)[0];
604
- if (tKey) {
605
- if (animateOnly && !animateOnly.includes(tKey)) {
606
- continue;
607
- }
608
- exitKeys.push(`transform:${tKey}`);
609
- }
610
- }
611
- }
825
+ const exitKeys = getGatedKeys(renderSnapshot.value);
612
826
  pendingExitKeysRef.current = new Set(exitKeys);
613
827
  exitKeysRegistered.current = exitKeys.length > 0;
614
828
  }
829
+ const previousDidAnimateStyleRef = (0, import_react.useRef)(null);
830
+ const didAnimateStyle = onDidAnimate ? JSON.stringify(style) : null;
831
+ const previousDidAnimateStyle = previousDidAnimateStyleRef.current;
832
+ const shouldRegisterDidAnimate = didAnimateStyle !== null && previousDidAnimateStyle !== null && previousDidAnimateStyle !== didAnimateStyle && !isExiting && !isEntering && !justFinishedEntering;
833
+ const didAnimateKeys = shouldRegisterDidAnimate ? getGatedKeys(renderSnapshot.value) : [];
834
+ (0, import_core.useIsomorphicLayoutEffect)(() => {
835
+ if (didAnimateStyle === null) {
836
+ previousDidAnimateStyleRef.current = null;
837
+ isCompletingAnimationRef.value = false;
838
+ return;
839
+ }
840
+ const previousStyleStillCurrent = previousDidAnimateStyleRef.current === previousDidAnimateStyle;
841
+ previousDidAnimateStyleRef.current = didAnimateStyle;
842
+ if (!shouldRegisterDidAnimate || !previousStyleStillCurrent) return;
843
+ const cycleId = ++didAnimateCycleIdRef.current;
844
+ pendingDidAnimateKeysRef.current = new Set(didAnimateKeys);
845
+ didAnimateCompletedRef.current = didAnimateKeys.length === 0;
846
+ isCompletingAnimationRef.value = didAnimateKeys.length > 0;
847
+ didAnimateCycleIdShared.value = cycleId;
848
+ if (didAnimateKeys.length === 0) {
849
+ onDidAnimateRef.current?.();
850
+ }
851
+ }, [didAnimateStyle, shouldRegisterDidAnimate]);
615
852
  import_react.default.useEffect(() => {
616
853
  if (!justStartedExiting || !sendExitComplete) return;
617
854
  if (!exitKeysRegistered.current && pendingExitKeysRef.current.size === 0) {
@@ -621,76 +858,76 @@ function createAnimations(animationsConfig) {
621
858
  }
622
859
  }
623
860
  }, [justStartedExiting, sendExitComplete]);
861
+ const mapperStateRef = (0, import_react_native_reanimated.useSharedValue)({
862
+ emitted: {}
863
+ });
624
864
  const animatedStyle = (0, import_react_native_reanimated.useAnimatedStyle)(() => {
625
865
  "worklet";
626
866
 
627
- if (disableAnimation || isHydrating) {
867
+ const mapperState = mapperStateRef.value;
868
+ const config = configRef.value;
869
+ if (config.disableAnimation || config.isHydrating) {
870
+ mapperState.emitted = {};
628
871
  return {};
629
872
  }
873
+ const previouslyEmitted = mapperState.emitted;
874
+ const emitted = {};
630
875
  const result = {};
631
- const config = configRef.value;
632
- const emitterAnimated = animatedTargetsRef.value;
633
- const emitterStatic = staticTargetsRef.value;
634
- const emitterTransforms = transformTargetsRef.value;
635
- const hasEmitterUpdates = emitterAnimated !== null;
636
- const animatedValues = hasEmitterUpdates ? emitterAnimated : animatedStyles;
637
- const staticValues = hasEmitterUpdates ? emitterStatic : staticStyles;
876
+ const emitterSnapshot = emitterSnapshotRef.value;
877
+ const snapshot = emitterSnapshot ?? renderSnapshotRef.value;
878
+ const animatedValues = snapshot.animated;
879
+ const staticValues = snapshot.statics;
638
880
  const currentlyExiting = isExitingRef.value;
639
881
  const currentCycleId = exitCycleIdShared.value;
882
+ const currentlyCompletingAnimation = isCompletingAnimationRef.value;
883
+ const currentDidAnimateCycleId = didAnimateCycleIdShared.value;
640
884
  for (const key in staticValues) {
641
885
  result[key] = staticValues[key];
642
886
  }
887
+ for (const key in snapshot.removedKeys) {
888
+ if (!key.startsWith("transform:") && !(key in staticValues)) {
889
+ result[key] = snapshot.clearValue;
890
+ }
891
+ }
643
892
  for (const key in animatedValues) {
644
893
  if (key === "transform") continue;
645
894
  const targetValue = animatedValues[key];
646
- const propConfig = config.propertyConfigs[key] ?? config.baseConfig;
647
- let callback;
648
- if (currentlyExiting) {
649
- const capturedKey = key;
650
- const capturedCycleId = currentCycleId;
651
- callback = finished => {
652
- "worklet";
653
-
654
- (0, import_react_native_reanimated.runOnJS)(markExitKeyDone)(capturedKey, capturedCycleId, finished ?? false);
655
- };
895
+ emitted[key] = true;
896
+ if (typeof targetValue !== "number" && typeof targetValue !== "string") {
897
+ result[key] = targetValue;
898
+ continue;
656
899
  }
657
- result[key] = applyAnimation(targetValue, propConfig, callback);
900
+ result[key] = animateSnapshotValue(key, key, targetValue, config.propertyConfigs[key] ?? config.baseConfig, !!previouslyEmitted[key], snapshot.seeds[key], !!snapshot.gatedKeys[key], currentlyExiting, currentCycleId, currentlyCompletingAnimation, currentDidAnimateCycleId, markExitKeyDone, markDidAnimateKeyDone, !!COLOR_STYLE_KEYS[key]);
658
901
  }
659
- const transforms = hasEmitterUpdates ? emitterTransforms : animatedStyles.transform;
660
- if (transforms && Array.isArray(transforms)) {
902
+ const transforms = snapshot.transforms;
903
+ if (transforms.length > 0 || snapshot.removeTransform) {
661
904
  const validTransforms = [];
662
905
  for (const t of transforms) {
663
906
  if (!t) continue;
664
907
  const keys = Object.keys(t);
665
908
  if (keys.length === 0) continue;
666
- const value = t[keys[0]];
909
+ const transformKey = keys[0];
910
+ const value = t[transformKey];
667
911
  if (typeof value === "number" || typeof value === "string") {
668
- const transformKey = Object.keys(t)[0];
669
- const targetValue = t[transformKey];
670
912
  const propConfig = config.propertyConfigs[transformKey] ?? config.baseConfig;
671
- let callback;
672
- if (currentlyExiting) {
673
- const capturedKey = `transform:${transformKey}`;
674
- const capturedCycleId = currentCycleId;
675
- callback = finished => {
676
- "worklet";
677
-
678
- (0, import_react_native_reanimated.runOnJS)(markExitKeyDone)(capturedKey, capturedCycleId, finished ?? false);
679
- };
680
- }
913
+ const subKey = `transform:${transformKey}`;
914
+ emitted[subKey] = true;
681
915
  validTransforms.push({
682
- [transformKey]: applyAnimation(targetValue, propConfig, callback)
916
+ [transformKey]: animateSnapshotValue(subKey, transformKey, value, propConfig, !!previouslyEmitted[subKey], snapshot.seeds[subKey], !!snapshot.gatedKeys[subKey], currentlyExiting, currentCycleId, currentlyCompletingAnimation, currentDidAnimateCycleId, markExitKeyDone, markDidAnimateKeyDone)
683
917
  });
918
+ } else {
919
+ validTransforms.push(t);
684
920
  }
685
921
  }
686
- if (validTransforms.length > 0) {
922
+ if (validTransforms.length > 0 || !("transform" in staticValues)) {
687
923
  result.transform = validTransforms;
688
924
  }
689
925
  }
926
+ mapperState.emitted = emitted;
690
927
  return result;
691
- }, import_core.isWeb ? [animatedStyles, staticStyles, baseConfig, propertyConfigs, disableAnimation, isHydrating,
692
- // pass SharedValues so the mapper watches them on web (no babel plugin)
693
- animatedTargetsRef, staticTargetsRef, transformTargetsRef, isExitingRef, exitCycleIdShared, markExitKeyDone] : void 0);
928
+ }, import_core.isWeb ? [
929
+ // pass SharedValues so the stable mapper watches them on web (no babel plugin)
930
+ renderSnapshotRef, emitterSnapshotRef, configRef, isExitingRef, exitCycleIdShared, markExitKeyDone, isCompletingAnimationRef, didAnimateCycleIdShared, markDidAnimateKeyDone] : void 0);
694
931
  silenceAnimatedComponentDevCheck(animatedStyle);
695
932
  if (process.env.NODE_ENV === "development" && props.debug && props.debug !== "profile") {
696
933
  console.info("[animations-reanimated] useAnimations", {