@hanzogui/animations-reanimated 7.3.1 → 8.0.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("@hanzogui/animation-helpers");
40
40
  var import_core = require("@hanzogui/core");
41
41
  var import_use_presence = require("@hanzogui/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");
@@ -50,6 +51,16 @@ const getDefaultExport = module2 => {
50
51
  return mod;
51
52
  };
52
53
  const Animated = getDefaultExport(import_react_native_reanimated.default);
54
+ const silenceAnimatedComponentDevCheck = style => {
55
+ if (process.env.NODE_ENV !== "development" || import_core.isWeb || !style || typeof style !== "object") {
56
+ return;
57
+ }
58
+ Object.defineProperty(style, "_requiresAnimatedComponent", {
59
+ configurable: true,
60
+ enumerable: false,
61
+ value: true
62
+ });
63
+ };
53
64
  const resolveDynamicValue = (value, isDark) => {
54
65
  if (value !== null && typeof value === "object" && "dynamic" in value && typeof value.dynamic === "object") {
55
66
  const dynamic = value.dynamic;
@@ -57,21 +68,248 @@ const resolveDynamicValue = (value, isDark) => {
57
68
  }
58
69
  return value;
59
70
  };
60
- const applyAnimation = (targetValue, config, callback) => {
71
+ const updateMapperState = import_core.isWeb ? (state, emitted) => {
72
+ state.value.emitted = emitted;
73
+ } : (state, emitted) => {
74
+ "worklet";
75
+
76
+ if (!globalThis._WORKLET) return;
77
+ state.modify(current => {
78
+ current.emitted = emitted;
79
+ return current;
80
+ }, false);
81
+ };
82
+ const cloneAnimationValue = value => {
83
+ if (Array.isArray(value)) {
84
+ return value.map(cloneAnimationValue);
85
+ }
86
+ if (value && typeof value === "object") {
87
+ const next = {};
88
+ for (const key in value) {
89
+ next[key] = cloneAnimationValue(value[key]);
90
+ }
91
+ return next;
92
+ }
93
+ return value;
94
+ };
95
+ const cloneStyleRecord = style => {
96
+ const next = {};
97
+ for (const key in style) {
98
+ next[key] = cloneAnimationValue(style[key]);
99
+ }
100
+ return next;
101
+ };
102
+ const cloneTransitionConfig = config => {
103
+ return cloneAnimationValue(config);
104
+ };
105
+ const getAnimatedTransforms = value => {
106
+ if (!Array.isArray(value)) return [];
107
+ return value.filter(item => item !== null && typeof item === "object" && !Array.isArray(item));
108
+ };
109
+ const getRemovedAnimatedKeys = (current, previous) => {
110
+ const removedKeys = {};
111
+ for (const key of previous) {
112
+ if (!current.has(key)) removedKeys[key] = true;
113
+ }
114
+ return removedKeys;
115
+ };
116
+ const getImplicitDefault = (key, targetValue) => {
117
+ "worklet";
118
+
119
+ const value = key === "opacity" || key === "scale" || key === "scaleX" || key === "scaleY" ? 1 : 0;
120
+ if (typeof targetValue !== "string") return value;
121
+ let suffixIndex = 0;
122
+ while (suffixIndex < targetValue.length) {
123
+ const character = targetValue[suffixIndex];
124
+ const code = character.charCodeAt(0);
125
+ if (code >= 48 && code <= 57 || character === "." || character === "-" || character === "+") {
126
+ suffixIndex++;
127
+ } else {
128
+ break;
129
+ }
130
+ }
131
+ return suffixIndex > 0 ? `${value}${targetValue.slice(suffixIndex)}` : value;
132
+ };
133
+ const COLOR_STYLE_KEYS = {
134
+ backgroundColor: true,
135
+ borderColor: true,
136
+ borderLeftColor: true,
137
+ borderRightColor: true,
138
+ borderTopColor: true,
139
+ borderBottomColor: true,
140
+ color: true
141
+ };
142
+ const toReanimatedColor = value => {
143
+ "worklet";
144
+
145
+ if (typeof value === "number") {
146
+ const color = value >>> 0;
147
+ const alpha = (color >>> 24 & 255) / 255;
148
+ const red = color >>> 16 & 255;
149
+ const green = color >>> 8 & 255;
150
+ const blue = color & 255;
151
+ return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
152
+ }
153
+ if (typeof value !== "string") return value;
154
+ const trimmed = value.trim();
155
+ if (trimmed === "transparent") return "rgba(0, 0, 0, 0)";
156
+ return trimmed;
157
+ };
158
+ const isReanimatedColorHistory = value => {
159
+ "worklet";
160
+
161
+ const normalized = toReanimatedColor(value);
162
+ return typeof normalized === "string" && normalized.startsWith("rgba(");
163
+ };
164
+ const normalizeAnimationColor = value => {
165
+ if (typeof value === "number") {
166
+ return toReanimatedColor(value);
167
+ }
168
+ if (typeof value !== "string") return;
169
+ const color = (0, import_normalize_colors.default)(value);
170
+ if (color == null) return;
171
+ const red = Math.round((color & 4278190080) >>> 24);
172
+ const green = Math.round((color & 16711680) >>> 16);
173
+ const blue = Math.round((color & 65280) >>> 8);
174
+ const alpha = (color & 255) / 255;
175
+ return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
176
+ };
177
+ const buildSnapshot = (animated, statics, transforms, previousKeys, lastPainted) => {
178
+ const snapshotAnimated = {};
179
+ const snapshotStatics = cloneStyleRecord(statics);
180
+ for (const key in snapshotStatics) {
181
+ if (!COLOR_STYLE_KEYS[key]) continue;
182
+ const normalized = normalizeAnimationColor(snapshotStatics[key]);
183
+ if (normalized !== void 0) snapshotStatics[key] = normalized;
184
+ }
185
+ for (const key in animated) {
186
+ if (key === "transform") continue;
187
+ const value = animated[key];
188
+ if (COLOR_STYLE_KEYS[key]) {
189
+ const normalized = normalizeAnimationColor(value);
190
+ if (normalized === void 0) {
191
+ snapshotStatics[key] = cloneAnimationValue(value);
192
+ continue;
193
+ }
194
+ snapshotAnimated[key] = normalized;
195
+ } else {
196
+ snapshotAnimated[key] = cloneAnimationValue(value);
197
+ }
198
+ }
199
+ const snapshotTransforms = transforms.map(cloneStyleRecord);
200
+ const keys = new Set(Object.keys(snapshotAnimated));
201
+ for (const transform of snapshotTransforms) {
202
+ const key = Object.keys(transform)[0];
203
+ const value = key ? transform[key] : void 0;
204
+ if (key && (typeof value === "number" || typeof value === "string")) {
205
+ keys.add(`transform:${key}`);
206
+ }
207
+ }
208
+ const seeds = {};
209
+ for (const key of keys) {
210
+ let value = lastPainted[key];
211
+ if (typeof value !== "number" && typeof value !== "string") continue;
212
+ if (value === "auto" || typeof value === "string" && value.startsWith("calc")) {
213
+ continue;
214
+ }
215
+ if (COLOR_STYLE_KEYS[key]) value = normalizeAnimationColor(value);
216
+ if (value !== void 0) seeds[key] = value;
217
+ }
218
+ const gatedKeys = {};
219
+ for (const key of keys) gatedKeys[key] = true;
220
+ const removedKeys = getRemovedAnimatedKeys(keys, previousKeys);
221
+ const painted = {
222
+ ...snapshotStatics,
223
+ ...snapshotAnimated
224
+ };
225
+ const staticTransforms = getAnimatedTransforms(snapshotStatics.transform);
226
+ delete painted.transform;
227
+ for (const transform of staticTransforms) {
228
+ const key = Object.keys(transform)[0];
229
+ if (key) painted[`transform:${key}`] = transform[key];
230
+ }
231
+ for (const transform of snapshotTransforms) {
232
+ const key = Object.keys(transform)[0];
233
+ if (key) painted[`transform:${key}`] = transform[key];
234
+ }
235
+ return {
236
+ value: {
237
+ animated: snapshotAnimated,
238
+ statics: snapshotStatics,
239
+ transforms: snapshotTransforms,
240
+ gatedKeys,
241
+ seeds,
242
+ removedKeys,
243
+ removeTransform: Object.keys(removedKeys).some(key => key.startsWith("transform:")),
244
+ clearValue: import_core.isWeb ? "" : null
245
+ },
246
+ painted,
247
+ keys
248
+ };
249
+ };
250
+ const getGatedKeys = snapshot => Object.keys(snapshot.gatedKeys);
251
+ const createReanimatedConfig = config => {
252
+ "worklet";
253
+
254
+ const next = {};
255
+ const source = config;
256
+ for (const key in source) {
257
+ if (key === "type" || key === "delay") continue;
258
+ const value = source[key];
259
+ if (value !== void 0) {
260
+ next[key] = value;
261
+ }
262
+ }
263
+ return next;
264
+ };
265
+ const applyAnimation = (targetValue, config, callback, seedValue, validateStartAsColor = false) => {
61
266
  "worklet";
62
267
 
63
268
  const delay = config.delay;
269
+ const reanimatedConfig = createReanimatedConfig(config);
64
270
  let animatedValue;
65
271
  if (config.type === "timing") {
66
- animatedValue = (0, import_react_native_reanimated.withTiming)(targetValue, config, callback);
272
+ animatedValue = (0, import_react_native_reanimated.withTiming)(targetValue, reanimatedConfig, callback);
67
273
  } else {
68
- animatedValue = (0, import_react_native_reanimated.withSpring)(targetValue, config, callback);
274
+ animatedValue = (0, import_react_native_reanimated.withSpring)(targetValue, reanimatedConfig, callback);
275
+ }
276
+ if (seedValue !== void 0 || validateStartAsColor) {
277
+ const innerOnStart = animatedValue.onStart;
278
+ animatedValue.onStart = (animation, value, timestamp, previousAnimation) => {
279
+ "worklet";
280
+
281
+ const startValue = validateStartAsColor ? isReanimatedColorHistory(value) ? toReanimatedColor(value) : seedValue ?? targetValue : seedValue;
282
+ innerOnStart(animation, startValue, timestamp, previousAnimation);
283
+ };
69
284
  }
70
285
  if (delay && delay > 0) {
71
286
  animatedValue = (0, import_react_native_reanimated.withDelay)(delay, animatedValue);
72
287
  }
73
288
  return animatedValue;
74
289
  };
290
+ const animateSnapshotValue = (key, implicitKey, targetValue, config, previouslyEmitted, seedValue, gated, currentlyExiting, exitCycleId, currentlyCompletingAnimation, didAnimateCycleId, markExitKeyDone, markDidAnimateKeyDone, validateStartAsColor = false) => {
291
+ "worklet";
292
+
293
+ const cycleGated = gated && (currentlyExiting || currentlyCompletingAnimation);
294
+ if (!previouslyEmitted && seedValue === void 0 && !cycleGated) {
295
+ return targetValue;
296
+ }
297
+ let callback;
298
+ if (gated && currentlyExiting) {
299
+ callback = finished => {
300
+ "worklet";
301
+
302
+ (0, import_react_native_reanimated.runOnJS)(markExitKeyDone)(key, exitCycleId, finished ?? false);
303
+ };
304
+ } else if (gated && currentlyCompletingAnimation) {
305
+ callback = finished => {
306
+ "worklet";
307
+
308
+ (0, import_react_native_reanimated.runOnJS)(markDidAnimateKeyDone)(key, didAnimateCycleId, finished ?? false);
309
+ };
310
+ }
311
+ return applyAnimation(targetValue, config, callback, previouslyEmitted ? void 0 : seedValue ?? getImplicitDefault(implicitKey, targetValue), validateStartAsColor);
312
+ };
75
313
  const ANIMATABLE_PROPERTIES = {
76
314
  // Transform
77
315
  transform: true,
@@ -146,6 +384,23 @@ const canAnimateProperty = (key, value, animateOnly) => {
146
384
  if (animateOnly && !animateOnly.includes(key)) return false;
147
385
  return true;
148
386
  };
387
+ const splitAnimationStyles = (style, isDark, disableAnimation, animateOnly) => {
388
+ const animated = {};
389
+ const statics = {};
390
+ for (const key in style) {
391
+ const value = resolveDynamicValue(style[key], isDark);
392
+ if (value === void 0) continue;
393
+ if (!disableAnimation && canAnimateProperty(key, value, animateOnly)) {
394
+ animated[key] = cloneAnimationValue(value);
395
+ } else {
396
+ statics[key] = cloneAnimationValue(value);
397
+ }
398
+ }
399
+ return {
400
+ animated,
401
+ statics
402
+ };
403
+ };
149
404
  function createWebAnimatedComponent(defaultTag) {
150
405
  const isText = defaultTag === "span";
151
406
  const Component = Animated.createAnimatedComponent((0, import_react.forwardRef)((propsIn, ref) => {
@@ -184,43 +439,43 @@ const AnimatedText = createWebAnimatedComponent("span");
184
439
  function buildTransitionConfig(transition, animations, animationState, styleKeys) {
185
440
  const normalized = (0, import_animation_helpers.normalizeTransition)(transition);
186
441
  const effectiveKey = (0, import_animation_helpers.getEffectiveAnimation)(normalized, animationState);
187
- let base = effectiveKey ? animations[effectiveKey] ?? {
442
+ let base = cloneTransitionConfig(effectiveKey ? animations[effectiveKey] ?? {
188
443
  type: "spring"
189
444
  } : {
190
445
  type: "spring"
191
- };
446
+ });
192
447
  if (normalized.delay) {
193
- base = {
448
+ base = cloneTransitionConfig({
194
449
  ...base,
195
450
  delay: normalized.delay
196
- };
451
+ });
197
452
  }
198
453
  if (normalized.config) {
199
- base = {
454
+ base = cloneTransitionConfig({
200
455
  ...base,
201
456
  ...normalized.config
202
- };
457
+ });
203
458
  if (base.type !== "timing" && normalized.config.duration !== void 0 && normalized.config.damping === void 0 && normalized.config.stiffness === void 0 && normalized.config.mass === void 0) {
204
- base = {
459
+ base = cloneTransitionConfig({
205
460
  ...base,
206
461
  type: "timing"
207
- };
462
+ });
208
463
  }
209
464
  }
210
465
  const propertyConfigs = {};
211
466
  for (const key of styleKeys) {
212
467
  const propAnimation = normalized.properties[key];
213
468
  if (typeof propAnimation === "string") {
214
- propertyConfigs[key] = animations[propAnimation] ?? base;
469
+ propertyConfigs[key] = cloneTransitionConfig(animations[propAnimation] ?? base);
215
470
  } else if (propAnimation && typeof propAnimation === "object") {
216
471
  const configType = propAnimation.type;
217
472
  const baseForProp = configType ? animations[configType] ?? base : base;
218
- propertyConfigs[key] = {
473
+ propertyConfigs[key] = cloneTransitionConfig({
219
474
  ...baseForProp,
220
475
  ...propAnimation
221
- };
476
+ });
222
477
  } else {
223
- propertyConfigs[key] = base;
478
+ propertyConfigs[key] = cloneTransitionConfig(base);
224
479
  }
225
480
  }
226
481
  return {
@@ -242,10 +497,10 @@ function getStyleKeys(style) {
242
497
  function createAnimations(animationsConfig) {
243
498
  const animations = {};
244
499
  for (const key in animationsConfig) {
245
- animations[key] = {
500
+ animations[key] = cloneTransitionConfig({
246
501
  type: "spring",
247
502
  ...animationsConfig[key]
248
- };
503
+ });
249
504
  }
250
505
  return {
251
506
  needsCustomComponent: true,
@@ -281,19 +536,22 @@ function createAnimations(animationsConfig) {
281
536
  sharedValue.value = next;
282
537
  onFinish?.();
283
538
  } else {
284
- const cb = onFinish ? () => {
539
+ const cb = onFinish ? finished => {
285
540
  "worklet";
286
541
 
287
- (0, import_react_native_reanimated.runOnJS)(onFinish)();
542
+ if (finished !== false) {
543
+ (0, import_react_native_reanimated.runOnJS)(onFinish)();
544
+ }
288
545
  } : void 0;
546
+ const animationConfig = cloneTransitionConfig(config);
289
547
  if (import_core.isWeb) {
290
- sharedValue.value = config.type === "spring" ? (0, import_react_native_reanimated.withSpring)(next, config, cb) : (0, import_react_native_reanimated.withTiming)(next, config, cb);
548
+ sharedValue.value = applyAnimation(next, animationConfig, cb);
291
549
  } else {
292
- (0, import_react_native_reanimated.runOnUI)(() => {
550
+ (0, import_react_native_reanimated.runOnUI)((targetValue, animationConfig2) => {
293
551
  "worklet";
294
552
 
295
- sharedValue.value = config.type === "spring" ? (0, import_react_native_reanimated.withSpring)(next, config, cb) : (0, import_react_native_reanimated.withTiming)(next, config, cb);
296
- })();
553
+ sharedValue.value = applyAnimation(targetValue, animationConfig2, cb);
554
+ })(next, animationConfig);
297
555
  }
298
556
  }
299
557
  },
@@ -332,20 +590,24 @@ function createAnimations(animationsConfig) {
332
590
 
333
591
  return getStyle(instance.value);
334
592
  });
335
- return (0, import_react_native_reanimated.useAnimatedStyle)(() => {
593
+ const animatedStyle = (0, import_react_native_reanimated.useAnimatedStyle)(() => {
336
594
  "worklet";
337
595
 
338
596
  return styleVal.value;
339
597
  });
598
+ silenceAnimatedComponentDevCheck(animatedStyle);
599
+ return animatedStyle;
340
600
  },
341
601
  useAnimatedNumbersStyle(vals, getStyle) {
342
602
  const instances = vals.map(v => v.getInstance());
343
- return (0, import_react_native_reanimated.useAnimatedStyle)(() => {
603
+ const animatedStyle = (0, import_react_native_reanimated.useAnimatedStyle)(() => {
344
604
  "worklet";
345
605
 
346
606
  const currentValues = instances.map(inst => inst.value);
347
607
  return getStyle(...currentValues);
348
608
  }, import_core.isWeb ? [getStyle, ...instances] : void 0);
609
+ silenceAnimatedComponentDevCheck(animatedStyle);
610
+ return animatedStyle;
349
611
  },
350
612
  // =========================================================================
351
613
  // useAnimations - Main animation hook for components
@@ -359,7 +621,8 @@ function createAnimations(animationsConfig) {
359
621
  useStyleEmitter,
360
622
  themeName,
361
623
  stateRef,
362
- styleState
624
+ styleState,
625
+ onDidAnimate
363
626
  } = animationProps;
364
627
  const isHydrating = componentState.unmounted === true;
365
628
  const isMounting = componentState.unmounted === "should-enter";
@@ -377,6 +640,25 @@ function createAnimations(animationsConfig) {
377
640
  const disableAnimation = isHydrating || !animationKey;
378
641
  const isDark = themeName?.startsWith("dark") || false;
379
642
  const sendExitComplete = presence?.[1];
643
+ const onDidAnimateRef = (0, import_react.useRef)(onDidAnimate);
644
+ (0, import_core.useIsomorphicLayoutEffect)(() => {
645
+ onDidAnimateRef.current = onDidAnimate;
646
+ }, [onDidAnimate]);
647
+ const didAnimateCycleIdRef = (0, import_react.useRef)(0);
648
+ const pendingDidAnimateKeysRef = (0, import_react.useRef)(/* @__PURE__ */new Set());
649
+ const didAnimateCompletedRef = (0, import_react.useRef)(false);
650
+ const isCompletingAnimationRef = (0, import_react_native_reanimated.useSharedValue)(false);
651
+ const didAnimateCycleIdShared = (0, import_react_native_reanimated.useSharedValue)(0);
652
+ const markDidAnimateKeyDone = (0, import_core.useEvent)((key, cycleId, finished) => {
653
+ if (!finished || cycleId !== didAnimateCycleIdRef.current) return;
654
+ if (didAnimateCompletedRef.current) return;
655
+ pendingDidAnimateKeysRef.current.delete(key);
656
+ if (pendingDidAnimateKeysRef.current.size === 0) {
657
+ didAnimateCompletedRef.current = true;
658
+ isCompletingAnimationRef.value = false;
659
+ onDidAnimateRef.current?.();
660
+ }
661
+ });
380
662
  const exitCycleIdRef = (0, import_react.useRef)(0);
381
663
  const pendingExitKeysRef = (0, import_react.useRef)(/* @__PURE__ */new Set());
382
664
  const exitCompletedRef = (0, import_react.useRef)(false);
@@ -398,6 +680,9 @@ function createAnimations(animationsConfig) {
398
680
  exitCycleIdRef.current++;
399
681
  exitCompletedRef.current = false;
400
682
  pendingExitKeysRef.current.clear();
683
+ didAnimateCycleIdRef.current++;
684
+ pendingDidAnimateKeysRef.current.clear();
685
+ didAnimateCompletedRef.current = true;
401
686
  }
402
687
  if (justStoppedExiting) {
403
688
  exitCycleIdRef.current++;
@@ -406,44 +691,85 @@ function createAnimations(animationsConfig) {
406
691
  (0, import_core.useIsomorphicLayoutEffect)(() => {
407
692
  isExitingRef.value = isExiting;
408
693
  exitCycleIdShared.value = exitCycleIdRef.current;
409
- }, [isExiting, exitCycleIdRef.current]);
694
+ if (justStartedExiting) {
695
+ isCompletingAnimationRef.value = false;
696
+ didAnimateCycleIdShared.value = didAnimateCycleIdRef.current;
697
+ }
698
+ }, [isExiting, exitCycleIdRef.current, justStartedExiting]);
410
699
  import_react.default.useEffect(() => {
411
700
  wasExitingRef.current = isExiting;
412
701
  });
413
- const animatedTargetsRef = (0, import_react_native_reanimated.useSharedValue)(null);
414
- const staticTargetsRef = (0, import_react_native_reanimated.useSharedValue)(null);
415
- const transformTargetsRef = (0, import_react_native_reanimated.useSharedValue)(null);
702
+ const committedRenderKeysRef = (0, import_react.useRef)(/* @__PURE__ */new Set());
703
+ const lastPaintedRef = (0, import_react.useRef)({});
704
+ const carryRef = (0, import_react.useRef)({});
416
705
  const {
417
706
  animatedStyles,
418
- staticStyles
707
+ staticStyles,
708
+ nextCarry
419
709
  } = (0, import_react.useMemo)(() => {
420
- const animated = {};
421
- const staticStyles2 = {};
422
710
  const animateOnly = props.animateOnly;
423
- for (const key in style) {
424
- const rawValue = style[key];
425
- const value = resolveDynamicValue(rawValue, isDark);
426
- if (value === void 0) continue;
427
- if (disableAnimation) {
428
- staticStyles2[key] = value;
429
- continue;
430
- }
431
- if (canAnimateProperty(key, value, animateOnly)) {
432
- animated[key] = value;
433
- } else {
434
- staticStyles2[key] = value;
435
- }
711
+ const {
712
+ animated,
713
+ statics
714
+ } = splitAnimationStyles(style, isDark, disableAnimation, animateOnly);
715
+ const nextCarry2 = cloneStyleRecord(carryRef.current);
716
+ for (const key in nextCarry2) {
717
+ if (!(key in animated)) delete nextCarry2[key];
436
718
  }
437
- if (isMounting) {
438
- for (const key in animated) {
439
- staticStyles2[key] = animated[key];
719
+ for (const key in animated) {
720
+ if (!(key in nextCarry2)) {
721
+ nextCarry2[key] = cloneAnimationValue(animated[key]);
440
722
  }
723
+ statics[key] = nextCarry2[key];
441
724
  }
442
725
  return {
443
726
  animatedStyles: animated,
444
- staticStyles: staticStyles2
727
+ staticStyles: statics,
728
+ nextCarry: nextCarry2
445
729
  };
446
- }, [disableAnimation, style, isDark, isMounting, props.animateOnly]);
730
+ }, [disableAnimation, style, isDark, props.animateOnly]);
731
+ const renderSnapshot = (0, import_react.useMemo)(() => buildSnapshot(animatedStyles, staticStyles, getAnimatedTransforms(animatedStyles.transform), committedRenderKeysRef.current, lastPaintedRef.current), [animatedStyles, staticStyles]);
732
+ const renderSnapshotRef = (0, import_react_native_reanimated.useSharedValue)({
733
+ animated: {},
734
+ statics: {},
735
+ transforms: [],
736
+ gatedKeys: {},
737
+ seeds: {},
738
+ removedKeys: {},
739
+ removeTransform: false,
740
+ clearValue: import_core.isWeb ? "" : null
741
+ });
742
+ const emitterSnapshotRef = (0, import_react_native_reanimated.useSharedValue)(null);
743
+ const emitterKeysRef = (0, import_react.useRef)(null);
744
+ (0, import_core.useIsomorphicLayoutEffect)(() => {
745
+ carryRef.current = nextCarry;
746
+ committedRenderKeysRef.current = renderSnapshot.keys;
747
+ lastPaintedRef.current = renderSnapshot.painted;
748
+ }, [nextCarry, renderSnapshot]);
749
+ const pseudoActiveRef = (0, import_react.useRef)(false);
750
+ const isExitingJSRef = (0, import_react.useRef)(false);
751
+ isExitingJSRef.current = isExiting;
752
+ const publishedSnapshotRef = (0, import_react.useRef)(null);
753
+ (0, import_core.useIsomorphicLayoutEffect)(() => {
754
+ const droppingLatch = (isExiting || !pseudoActiveRef.current) && emitterSnapshotRef.value !== null;
755
+ const emitterKeys = droppingLatch ? emitterKeysRef.current : null;
756
+ if (droppingLatch || publishedSnapshotRef.current !== renderSnapshot.value) {
757
+ const removedKeys = emitterKeys ? {
758
+ ...renderSnapshot.value.removedKeys,
759
+ ...getRemovedAnimatedKeys(renderSnapshot.keys, emitterKeys)
760
+ } : renderSnapshot.value.removedKeys;
761
+ renderSnapshotRef.value = {
762
+ ...renderSnapshot.value,
763
+ removedKeys,
764
+ removeTransform: Object.keys(removedKeys).some(key => key.startsWith("transform:"))
765
+ };
766
+ publishedSnapshotRef.current = renderSnapshot.value;
767
+ }
768
+ if (droppingLatch) {
769
+ emitterSnapshotRef.value = null;
770
+ emitterKeysRef.current = null;
771
+ }
772
+ });
447
773
  const {
448
774
  baseConfig,
449
775
  propertyConfigs
@@ -457,8 +783,8 @@ function createAnimations(animationsConfig) {
457
783
  propertyConfigs: {}
458
784
  };
459
785
  }
460
- return buildTransitionConfig(props.transition, animations, animationState, getStyleKeys(animatedStyles));
461
- }, [isHydrating, props.transition, animatedStyles, animationState]);
786
+ return buildTransitionConfig(effectiveTransition, animations, animationState, getStyleKeys(animatedStyles));
787
+ }, [isHydrating, effectiveTransition, animatedStyles, animationState]);
462
788
  const configRef = (0, import_react_native_reanimated.useSharedValue)({
463
789
  baseConfig,
464
790
  propertyConfigs,
@@ -466,92 +792,74 @@ function createAnimations(animationsConfig) {
466
792
  isHydrating
467
793
  });
468
794
  (0, import_core.useIsomorphicLayoutEffect)(() => {
469
- configRef.set({
795
+ configRef.value = {
470
796
  baseConfig,
471
797
  propertyConfigs,
472
798
  disableAnimation,
473
799
  isHydrating
474
- });
800
+ };
475
801
  }, [baseConfig, propertyConfigs, disableAnimation, isHydrating]);
476
- useStyleEmitter?.((nextStyle, effectiveTransition2) => {
802
+ useStyleEmitter?.((nextStyle, effectiveTransition2, pseudoActive) => {
803
+ if (isExitingJSRef.current) return;
804
+ pseudoActiveRef.current = pseudoActive === true;
477
805
  const animateOnly = props.animateOnly;
478
- const animated = {};
479
- const statics = {};
480
- const transforms = [];
481
806
  const transitionToUse = effectiveTransition2 ?? props.transition;
482
807
  const {
483
808
  baseConfig: newBase,
484
809
  propertyConfigs: newPropertyConfigs
485
810
  } = buildTransitionConfig(transitionToUse, animations, animationState, getStyleKeys(nextStyle));
486
- configRef.set({
811
+ configRef.value = {
487
812
  baseConfig: newBase,
488
813
  propertyConfigs: newPropertyConfigs,
489
- disableAnimation: configRef.get().disableAnimation,
490
- isHydrating: configRef.get().isHydrating
491
- });
492
- for (const key in nextStyle) {
493
- const rawValue = nextStyle[key];
494
- const value = resolveDynamicValue(rawValue, isDark);
495
- if (value == void 0) continue;
496
- if (configRef.get().disableAnimation) {
497
- statics[key] = value;
498
- continue;
499
- }
500
- if (key === "transform" && Array.isArray(value)) {
501
- for (const t of value) {
502
- if (t && typeof t === "object") {
503
- const tKey = Object.keys(t)[0];
504
- const tVal = t[tKey];
505
- if (typeof tVal === "number" || typeof tVal === "string") {
506
- transforms.push(t);
507
- }
508
- }
509
- }
510
- continue;
511
- }
512
- if (canAnimateProperty(key, value, animateOnly)) {
513
- animated[key] = value;
514
- } else {
515
- statics[key] = value;
516
- }
517
- }
518
- animatedTargetsRef.set(animated);
519
- staticTargetsRef.set(statics);
520
- transformTargetsRef.set(transforms);
814
+ disableAnimation: configRef.value.disableAnimation,
815
+ isHydrating: configRef.value.isHydrating
816
+ };
817
+ const previousKeys = emitterKeysRef.current ?? committedRenderKeysRef.current;
818
+ const {
819
+ animated,
820
+ statics
821
+ } = splitAnimationStyles(nextStyle, isDark, configRef.value.disableAnimation, animateOnly);
822
+ const snapshot = buildSnapshot(animated, statics, getAnimatedTransforms(animated.transform), previousKeys, lastPaintedRef.current);
823
+ emitterSnapshotRef.value = snapshot.value;
824
+ emitterKeysRef.current = snapshot.keys;
825
+ lastPaintedRef.current = snapshot.painted;
521
826
  if (process.env.NODE_ENV === "development" && props.debug && props.debug !== "profile") {
522
827
  console.info("[animations-reanimated] useStyleEmitter update", {
523
828
  animated,
524
829
  statics,
525
- transforms
830
+ transforms: snapshot.value.transforms
526
831
  });
527
832
  }
528
833
  });
529
834
  const exitKeysRegistered = (0, import_react.useRef)(false);
530
835
  if (justStartedExiting && sendExitComplete) {
531
- const exitKeys = [];
532
- const animateOnly = props.animateOnly;
533
- for (const key in animatedStyles) {
534
- if (key === "transform") continue;
535
- if (canAnimateProperty(key, animatedStyles[key], animateOnly)) {
536
- exitKeys.push(key);
537
- }
538
- }
539
- const transforms = animatedStyles.transform;
540
- if (transforms && Array.isArray(transforms)) {
541
- for (const t of transforms) {
542
- if (!t) continue;
543
- const tKey = Object.keys(t)[0];
544
- if (tKey) {
545
- if (animateOnly && !animateOnly.includes(tKey)) {
546
- continue;
547
- }
548
- exitKeys.push(`transform:${tKey}`);
549
- }
550
- }
551
- }
836
+ const exitKeys = getGatedKeys(renderSnapshot.value);
552
837
  pendingExitKeysRef.current = new Set(exitKeys);
553
838
  exitKeysRegistered.current = exitKeys.length > 0;
554
839
  }
840
+ const previousDidAnimateStyleRef = (0, import_react.useRef)(null);
841
+ const didAnimateStyle = onDidAnimate ? JSON.stringify(style) : null;
842
+ const previousDidAnimateStyle = previousDidAnimateStyleRef.current;
843
+ const shouldRegisterDidAnimate = didAnimateStyle !== null && previousDidAnimateStyle !== null && previousDidAnimateStyle !== didAnimateStyle && !isExiting && !isEntering && !justFinishedEntering;
844
+ const didAnimateKeys = shouldRegisterDidAnimate ? getGatedKeys(renderSnapshot.value) : [];
845
+ (0, import_core.useIsomorphicLayoutEffect)(() => {
846
+ if (didAnimateStyle === null) {
847
+ previousDidAnimateStyleRef.current = null;
848
+ isCompletingAnimationRef.value = false;
849
+ return;
850
+ }
851
+ const previousStyleStillCurrent = previousDidAnimateStyleRef.current === previousDidAnimateStyle;
852
+ previousDidAnimateStyleRef.current = didAnimateStyle;
853
+ if (!shouldRegisterDidAnimate || !previousStyleStillCurrent) return;
854
+ const cycleId = ++didAnimateCycleIdRef.current;
855
+ pendingDidAnimateKeysRef.current = new Set(didAnimateKeys);
856
+ didAnimateCompletedRef.current = didAnimateKeys.length === 0;
857
+ isCompletingAnimationRef.value = didAnimateKeys.length > 0;
858
+ didAnimateCycleIdShared.value = cycleId;
859
+ if (didAnimateKeys.length === 0) {
860
+ onDidAnimateRef.current?.();
861
+ }
862
+ }, [didAnimateStyle, shouldRegisterDidAnimate]);
555
863
  import_react.default.useEffect(() => {
556
864
  if (!justStartedExiting || !sendExitComplete) return;
557
865
  if (!exitKeysRegistered.current && pendingExitKeysRef.current.size === 0) {
@@ -561,76 +869,77 @@ function createAnimations(animationsConfig) {
561
869
  }
562
870
  }
563
871
  }, [justStartedExiting, sendExitComplete]);
872
+ const mapperStateRef = (0, import_react_native_reanimated.useSharedValue)({
873
+ emitted: {}
874
+ });
564
875
  const animatedStyle = (0, import_react_native_reanimated.useAnimatedStyle)(() => {
565
876
  "worklet";
566
877
 
567
- if (disableAnimation || isHydrating) {
878
+ const mapperState = mapperStateRef.value;
879
+ const config = configRef.value;
880
+ if (config.disableAnimation || config.isHydrating) {
881
+ updateMapperState(mapperStateRef, {});
568
882
  return {};
569
883
  }
884
+ const previouslyEmitted = mapperState.emitted;
885
+ const emitted = {};
570
886
  const result = {};
571
- const config = configRef.get();
572
- const emitterAnimated = animatedTargetsRef.value;
573
- const emitterStatic = staticTargetsRef.value;
574
- const emitterTransforms = transformTargetsRef.value;
575
- const hasEmitterUpdates = emitterAnimated !== null;
576
- const animatedValues = hasEmitterUpdates ? emitterAnimated : animatedStyles;
577
- const staticValues = hasEmitterUpdates ? emitterStatic : {};
887
+ const emitterSnapshot = emitterSnapshotRef.value;
888
+ const snapshot = emitterSnapshot ?? renderSnapshotRef.value;
889
+ const animatedValues = snapshot.animated;
890
+ const staticValues = snapshot.statics;
578
891
  const currentlyExiting = isExitingRef.value;
579
892
  const currentCycleId = exitCycleIdShared.value;
893
+ const currentlyCompletingAnimation = isCompletingAnimationRef.value;
894
+ const currentDidAnimateCycleId = didAnimateCycleIdShared.value;
580
895
  for (const key in staticValues) {
581
896
  result[key] = staticValues[key];
582
897
  }
898
+ for (const key in snapshot.removedKeys) {
899
+ if (!key.startsWith("transform:") && !(key in staticValues)) {
900
+ result[key] = snapshot.clearValue;
901
+ }
902
+ }
583
903
  for (const key in animatedValues) {
584
904
  if (key === "transform") continue;
585
905
  const targetValue = animatedValues[key];
586
- const propConfig = config.propertyConfigs[key] ?? config.baseConfig;
587
- let callback;
588
- if (currentlyExiting) {
589
- const capturedKey = key;
590
- const capturedCycleId = currentCycleId;
591
- callback = finished => {
592
- "worklet";
593
-
594
- (0, import_react_native_reanimated.runOnJS)(markExitKeyDone)(capturedKey, capturedCycleId, finished ?? false);
595
- };
906
+ emitted[key] = true;
907
+ if (typeof targetValue !== "number" && typeof targetValue !== "string") {
908
+ result[key] = targetValue;
909
+ continue;
596
910
  }
597
- result[key] = applyAnimation(targetValue, propConfig, callback);
911
+ 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]);
598
912
  }
599
- const transforms = hasEmitterUpdates ? emitterTransforms : animatedStyles.transform;
600
- if (transforms && Array.isArray(transforms)) {
913
+ const transforms = snapshot.transforms;
914
+ if (transforms.length > 0 || snapshot.removeTransform) {
601
915
  const validTransforms = [];
602
916
  for (const t of transforms) {
603
917
  if (!t) continue;
604
918
  const keys = Object.keys(t);
605
919
  if (keys.length === 0) continue;
606
- const value = t[keys[0]];
920
+ const transformKey = keys[0];
921
+ const value = t[transformKey];
607
922
  if (typeof value === "number" || typeof value === "string") {
608
- const transformKey = Object.keys(t)[0];
609
- const targetValue = t[transformKey];
610
923
  const propConfig = config.propertyConfigs[transformKey] ?? config.baseConfig;
611
- let callback;
612
- if (currentlyExiting) {
613
- const capturedKey = `transform:${transformKey}`;
614
- const capturedCycleId = currentCycleId;
615
- callback = finished => {
616
- "worklet";
617
-
618
- (0, import_react_native_reanimated.runOnJS)(markExitKeyDone)(capturedKey, capturedCycleId, finished ?? false);
619
- };
620
- }
924
+ const subKey = `transform:${transformKey}`;
925
+ emitted[subKey] = true;
621
926
  validTransforms.push({
622
- [transformKey]: applyAnimation(targetValue, propConfig, callback)
927
+ [transformKey]: animateSnapshotValue(subKey, transformKey, value, propConfig, !!previouslyEmitted[subKey], snapshot.seeds[subKey], !!snapshot.gatedKeys[subKey], currentlyExiting, currentCycleId, currentlyCompletingAnimation, currentDidAnimateCycleId, markExitKeyDone, markDidAnimateKeyDone)
623
928
  });
929
+ } else {
930
+ validTransforms.push(t);
624
931
  }
625
932
  }
626
- if (validTransforms.length > 0) {
933
+ if (validTransforms.length > 0 || !("transform" in staticValues)) {
627
934
  result.transform = validTransforms;
628
935
  }
629
936
  }
937
+ updateMapperState(mapperStateRef, emitted);
630
938
  return result;
631
- }, import_core.isWeb ? [animatedStyles, baseConfig, propertyConfigs, disableAnimation, isHydrating,
632
- // pass SharedValues so the mapper watches them on web (no babel plugin)
633
- animatedTargetsRef, staticTargetsRef, transformTargetsRef, isExitingRef, exitCycleIdShared, markExitKeyDone] : void 0);
939
+ }, import_core.isWeb ? [
940
+ // pass SharedValues so the stable mapper watches them on web (no babel plugin)
941
+ renderSnapshotRef, emitterSnapshotRef, configRef, isExitingRef, exitCycleIdShared, markExitKeyDone, isCompletingAnimationRef, didAnimateCycleIdShared, markDidAnimateKeyDone] : void 0);
942
+ silenceAnimatedComponentDevCheck(animatedStyle);
634
943
  if (process.env.NODE_ENV === "development" && props.debug && props.debug !== "profile") {
635
944
  console.info("[animations-reanimated] useAnimations", {
636
945
  animationKey,