@hanzogui/animations-reanimated 8.3.1 → 8.3.3

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.
@@ -0,0 +1,1156 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { getEffectiveAnimation, normalizeTransition } from '@hanzogui/animation-helpers';
3
+ import { getSplitStyles, hooks, isWeb, Text, useComposedRefs, useEvent, useIsomorphicLayoutEffect, useThemeWithState, View, } from '@hanzogui/core';
4
+ import { ResetPresence, usePresence } from '@hanzogui/use-presence';
5
+ import normalizeColor from '@react-native/normalize-colors';
6
+ import React, { forwardRef, useMemo, useRef } from 'react';
7
+ import Animated_, { cancelAnimation, runOnJS, runOnUI, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, withDelay, withSpring, withTiming, } from 'react-native-reanimated';
8
+ // =============================================================================
9
+ // ESM/CJS compatibility
10
+ // =============================================================================
11
+ /**
12
+ * Handle ESM/CJS module interop for react-native-reanimated
13
+ * @see https://github.com/evanw/esbuild/issues/2480#issuecomment-1833104754
14
+ */
15
+ const getDefaultExport = (module) => {
16
+ const mod = module;
17
+ if (mod.__esModule || mod[Symbol.toStringTag] === 'Module') {
18
+ return mod.default || mod;
19
+ }
20
+ return mod;
21
+ };
22
+ const Animated = getDefaultExport(Animated_);
23
+ const silenceAnimatedComponentDevCheck = (style) => {
24
+ if (process.env.NODE_ENV !== 'development' ||
25
+ isWeb ||
26
+ !style ||
27
+ typeof style !== 'object') {
28
+ return;
29
+ }
30
+ // react fabric's dev performance logger diffs component props and reads the
31
+ // reanimated guard getter even when the style is on an animated component.
32
+ Object.defineProperty(style, '_requiresAnimatedComponent', {
33
+ configurable: true,
34
+ enumerable: false,
35
+ value: true,
36
+ });
37
+ };
38
+ // =============================================================================
39
+ // Utility Functions
40
+ // =============================================================================
41
+ /**
42
+ * Resolve dynamic theme values (e.g., `{dynamic: {dark: "value", light: "value"}}`)
43
+ */
44
+ const resolveDynamicValue = (value, isDark) => {
45
+ if (value !== null &&
46
+ typeof value === 'object' &&
47
+ 'dynamic' in value &&
48
+ typeof value.dynamic === 'object') {
49
+ const dynamic = value.dynamic;
50
+ return isDark ? dynamic.dark : dynamic.light;
51
+ }
52
+ return value;
53
+ };
54
+ const updateMapperState = isWeb
55
+ ? (state, emitted) => {
56
+ state.value.emitted = emitted;
57
+ }
58
+ : (state, emitted) => {
59
+ 'worklet';
60
+ if (!globalThis._WORKLET)
61
+ return;
62
+ state.modify((current) => {
63
+ current.emitted = emitted;
64
+ return current;
65
+ }, false);
66
+ };
67
+ const cloneAnimationValue = (value) => {
68
+ if (Array.isArray(value)) {
69
+ return value.map(cloneAnimationValue);
70
+ }
71
+ if (value && typeof value === 'object') {
72
+ const next = {};
73
+ for (const key in value) {
74
+ next[key] = cloneAnimationValue(value[key]);
75
+ }
76
+ return next;
77
+ }
78
+ return value;
79
+ };
80
+ const cloneStyleRecord = (style) => {
81
+ const next = {};
82
+ for (const key in style) {
83
+ next[key] = cloneAnimationValue(style[key]);
84
+ }
85
+ return next;
86
+ };
87
+ const cloneTransitionConfig = (config) => {
88
+ return cloneAnimationValue(config);
89
+ };
90
+ const getAnimatedTransforms = (value) => {
91
+ if (!Array.isArray(value))
92
+ return [];
93
+ return value.filter((item) => item !== null && typeof item === 'object' && !Array.isArray(item));
94
+ };
95
+ const getRemovedAnimatedKeys = (current, previous) => {
96
+ const removedKeys = {};
97
+ for (const key of previous) {
98
+ if (!current.has(key))
99
+ removedKeys[key] = true;
100
+ }
101
+ return removedKeys;
102
+ };
103
+ // implicit start values for style keys that were never painted (e.g. a key
104
+ // introduced by exitStyle with no base value). most numerics default to 0.
105
+ const getImplicitDefault = (key, targetValue) => {
106
+ 'worklet';
107
+ const value = key === 'opacity' || key === 'scale' || key === 'scaleX' || key === 'scaleY' ? 1 : 0;
108
+ if (typeof targetValue !== 'string')
109
+ return value;
110
+ // reanimated derives a string animation's suffix from its start value. keep
111
+ // units on implicit starts so fresh `50%` and `90deg` keys interpolate.
112
+ let suffixIndex = 0;
113
+ while (suffixIndex < targetValue.length) {
114
+ const character = targetValue[suffixIndex];
115
+ const code = character.charCodeAt(0);
116
+ if ((code >= 48 && code <= 57) ||
117
+ character === '.' ||
118
+ character === '-' ||
119
+ character === '+') {
120
+ suffixIndex++;
121
+ }
122
+ else {
123
+ break;
124
+ }
125
+ }
126
+ return suffixIndex > 0 ? `${value}${targetValue.slice(suffixIndex)}` : value;
127
+ };
128
+ const COLOR_STYLE_KEYS = {
129
+ backgroundColor: true,
130
+ borderColor: true,
131
+ borderLeftColor: true,
132
+ borderRightColor: true,
133
+ borderTopColor: true,
134
+ borderBottomColor: true,
135
+ color: true,
136
+ };
137
+ // reanimated can pass either its rgba output or a processed numeric color back
138
+ // as animation history. snapshot targets are normalized on the JS side below;
139
+ // this worklet conversion exists only to validate that in-flight history.
140
+ const toReanimatedColor = (value) => {
141
+ 'worklet';
142
+ if (typeof value === 'number') {
143
+ const color = value >>> 0;
144
+ const alpha = ((color >>> 24) & 255) / 255;
145
+ const red = (color >>> 16) & 255;
146
+ const green = (color >>> 8) & 255;
147
+ const blue = color & 255;
148
+ return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
149
+ }
150
+ if (typeof value !== 'string')
151
+ return value;
152
+ const trimmed = value.trim();
153
+ if (trimmed === 'transparent')
154
+ return 'rgba(0, 0, 0, 0)';
155
+ return trimmed;
156
+ };
157
+ const isReanimatedColorHistory = (value) => {
158
+ 'worklet';
159
+ const normalized = toReanimatedColor(value);
160
+ return typeof normalized === 'string' && normalized.startsWith('rgba(');
161
+ };
162
+ const normalizeAnimationColor = (value) => {
163
+ if (typeof value === 'number') {
164
+ return toReanimatedColor(value);
165
+ }
166
+ if (typeof value !== 'string')
167
+ return;
168
+ const color = normalizeColor(value);
169
+ if (color == null)
170
+ return;
171
+ const red = Math.round((color & 0xff000000) >>> 24);
172
+ const green = Math.round((color & 0x00ff0000) >>> 16);
173
+ const blue = Math.round((color & 0x0000ff00) >>> 8);
174
+ const alpha = (color & 0x000000ff) / 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])
182
+ continue;
183
+ const normalized = normalizeAnimationColor(snapshotStatics[key]);
184
+ if (normalized !== undefined)
185
+ snapshotStatics[key] = normalized;
186
+ }
187
+ for (const key in animated) {
188
+ if (key === 'transform')
189
+ continue;
190
+ const value = animated[key];
191
+ if (COLOR_STYLE_KEYS[key]) {
192
+ const normalized = normalizeAnimationColor(value);
193
+ if (normalized === undefined) {
194
+ // invalid colors must paint plainly. creating a descriptor makes
195
+ // reanimated's string interpolator emit '<letters>NaN'.
196
+ snapshotStatics[key] = cloneAnimationValue(value);
197
+ continue;
198
+ }
199
+ snapshotAnimated[key] = normalized;
200
+ }
201
+ else {
202
+ snapshotAnimated[key] = cloneAnimationValue(value);
203
+ }
204
+ }
205
+ const snapshotTransforms = transforms.map(cloneStyleRecord);
206
+ const keys = new Set(Object.keys(snapshotAnimated));
207
+ for (const transform of snapshotTransforms) {
208
+ const key = Object.keys(transform)[0];
209
+ const value = key ? transform[key] : undefined;
210
+ if (key && (typeof value === 'number' || typeof value === 'string')) {
211
+ keys.add(`transform:${key}`);
212
+ }
213
+ }
214
+ const seeds = {};
215
+ for (const key of keys) {
216
+ let value = lastPainted[key];
217
+ if (typeof value !== 'number' && typeof value !== 'string')
218
+ continue;
219
+ if (value === 'auto' || (typeof value === 'string' && value.startsWith('calc'))) {
220
+ continue;
221
+ }
222
+ if (COLOR_STYLE_KEYS[key])
223
+ value = normalizeAnimationColor(value);
224
+ if (value !== undefined)
225
+ seeds[key] = value;
226
+ }
227
+ const gatedKeys = {};
228
+ for (const key of keys)
229
+ gatedKeys[key] = true;
230
+ const removedKeys = getRemovedAnimatedKeys(keys, previousKeys);
231
+ const painted = {
232
+ ...snapshotStatics,
233
+ ...snapshotAnimated,
234
+ };
235
+ const staticTransforms = getAnimatedTransforms(snapshotStatics.transform);
236
+ delete painted.transform;
237
+ for (const transform of staticTransforms) {
238
+ const key = Object.keys(transform)[0];
239
+ if (key)
240
+ painted[`transform:${key}`] = transform[key];
241
+ }
242
+ for (const transform of snapshotTransforms) {
243
+ const key = Object.keys(transform)[0];
244
+ if (key)
245
+ painted[`transform:${key}`] = transform[key];
246
+ }
247
+ return {
248
+ value: {
249
+ animated: snapshotAnimated,
250
+ statics: snapshotStatics,
251
+ transforms: snapshotTransforms,
252
+ gatedKeys,
253
+ seeds,
254
+ removedKeys,
255
+ removeTransform: Object.keys(removedKeys).some((key) => key.startsWith('transform:')),
256
+ clearValue: isWeb ? '' : null,
257
+ },
258
+ painted,
259
+ keys,
260
+ };
261
+ };
262
+ const getGatedKeys = (snapshot) => Object.keys(snapshot.gatedKeys);
263
+ const createReanimatedConfig = (config) => {
264
+ 'worklet';
265
+ const next = {};
266
+ const source = config;
267
+ for (const key in source) {
268
+ if (key === 'type' || key === 'delay')
269
+ continue;
270
+ const value = source[key];
271
+ if (value !== undefined) {
272
+ next[key] = value;
273
+ }
274
+ }
275
+ return next;
276
+ };
277
+ /**
278
+ * Apply animation to a value based on config, with optional completion callback
279
+ */
280
+ const applyAnimation = (targetValue, config, callback, seedValue, validateStartAsColor = false) => {
281
+ 'worklet';
282
+ const delay = config.delay;
283
+ const reanimatedConfig = createReanimatedConfig(config);
284
+ let animatedValue;
285
+ if (config.type === 'timing') {
286
+ animatedValue = withTiming(targetValue, reanimatedConfig, callback);
287
+ }
288
+ else {
289
+ animatedValue = withSpring(targetValue, reanimatedConfig, callback);
290
+ }
291
+ // reanimated starts a descriptor from its per-view history for the key — the
292
+ // key's last output value, or the descriptor's own toValue when there is
293
+ // none (an instant snap). both are wrong for a key that turns animated with
294
+ // a painted predecessor: history may hold a stale clear-value ('') from when
295
+ // the key was last removed, which poisons the start value (NaN frames). the
296
+ // wrapper substitutes the seed for whatever start value reanimated passes.
297
+ // color history gets the same treatment only when reanimated's own parser
298
+ // rejects it, preserving valid in-flight values during interruption.
299
+ if (seedValue !== undefined || validateStartAsColor) {
300
+ const innerOnStart = animatedValue.onStart;
301
+ animatedValue.onStart = (animation, value, timestamp, previousAnimation) => {
302
+ 'worklet';
303
+ const startValue = validateStartAsColor
304
+ ? isReanimatedColorHistory(value)
305
+ ? toReanimatedColor(value)
306
+ : (seedValue ?? targetValue)
307
+ : seedValue;
308
+ innerOnStart(animation, startValue, timestamp, previousAnimation);
309
+ };
310
+ }
311
+ if (delay && delay > 0) {
312
+ animatedValue = withDelay(delay, animatedValue);
313
+ }
314
+ return animatedValue;
315
+ };
316
+ const animateSnapshotValue = (key, implicitKey, targetValue, config, previouslyEmitted, seedValue, gated, currentlyExiting, exitCycleId, currentlyCompletingAnimation, didAnimateCycleId, markExitKeyDone, markDidAnimateKeyDone, validateStartAsColor = false) => {
317
+ 'worklet';
318
+ const cycleGated = gated && (currentlyExiting || currentlyCompletingAnimation);
319
+ if (!previouslyEmitted && seedValue === undefined && !cycleGated) {
320
+ return targetValue;
321
+ }
322
+ let callback;
323
+ if (gated && currentlyExiting) {
324
+ callback = (finished) => {
325
+ 'worklet';
326
+ runOnJS(markExitKeyDone)(key, exitCycleId, finished ?? false);
327
+ };
328
+ }
329
+ else if (gated && currentlyCompletingAnimation) {
330
+ callback = (finished) => {
331
+ 'worklet';
332
+ runOnJS(markDidAnimateKeyDone)(key, didAnimateCycleId, finished ?? false);
333
+ };
334
+ }
335
+ return applyAnimation(targetValue, config, callback, previouslyEmitted
336
+ ? undefined
337
+ : (seedValue ?? getImplicitDefault(implicitKey, targetValue)), validateStartAsColor);
338
+ };
339
+ // =============================================================================
340
+ // Animatable Properties
341
+ // =============================================================================
342
+ const ANIMATABLE_PROPERTIES = {
343
+ // Transform
344
+ transform: true,
345
+ // Opacity
346
+ opacity: true,
347
+ // Dimensions
348
+ height: true,
349
+ width: true,
350
+ minWidth: true,
351
+ minHeight: true,
352
+ maxWidth: true,
353
+ maxHeight: true,
354
+ // Background
355
+ backgroundColor: true,
356
+ // Border colors
357
+ borderColor: true,
358
+ borderLeftColor: true,
359
+ borderRightColor: true,
360
+ borderTopColor: true,
361
+ borderBottomColor: true,
362
+ // Border radius
363
+ borderRadius: true,
364
+ borderTopLeftRadius: true,
365
+ borderTopRightRadius: true,
366
+ borderBottomLeftRadius: true,
367
+ borderBottomRightRadius: true,
368
+ // Border width
369
+ borderWidth: true,
370
+ borderLeftWidth: true,
371
+ borderRightWidth: true,
372
+ borderTopWidth: true,
373
+ borderBottomWidth: true,
374
+ // Text
375
+ color: true,
376
+ fontSize: true,
377
+ fontWeight: true,
378
+ lineHeight: true,
379
+ letterSpacing: true,
380
+ // Position
381
+ left: true,
382
+ right: true,
383
+ top: true,
384
+ bottom: true,
385
+ // Margin
386
+ margin: true,
387
+ marginTop: true,
388
+ marginBottom: true,
389
+ marginLeft: true,
390
+ marginRight: true,
391
+ marginHorizontal: true,
392
+ marginVertical: true,
393
+ // Padding
394
+ padding: true,
395
+ paddingTop: true,
396
+ paddingBottom: true,
397
+ paddingLeft: true,
398
+ paddingRight: true,
399
+ paddingHorizontal: true,
400
+ paddingVertical: true,
401
+ // Flex/Gap
402
+ gap: true,
403
+ rowGap: true,
404
+ columnGap: true,
405
+ flex: true,
406
+ flexGrow: true,
407
+ flexShrink: true,
408
+ };
409
+ /**
410
+ * Check if a style property can be animated
411
+ */
412
+ const canAnimateProperty = (key, value, animateOnly) => {
413
+ if (!ANIMATABLE_PROPERTIES[key])
414
+ return false;
415
+ if (value === 'auto')
416
+ return false;
417
+ if (typeof value === 'string' && value.startsWith('calc'))
418
+ return false;
419
+ if (animateOnly && !animateOnly.includes(key))
420
+ return false;
421
+ return true;
422
+ };
423
+ const splitAnimationStyles = (style, isDark, disableAnimation, animateOnly) => {
424
+ const animated = {};
425
+ const statics = {};
426
+ for (const key in style) {
427
+ const value = resolveDynamicValue(style[key], isDark);
428
+ if (value === undefined)
429
+ continue;
430
+ if (!disableAnimation && canAnimateProperty(key, value, animateOnly)) {
431
+ animated[key] = cloneAnimationValue(value);
432
+ }
433
+ else {
434
+ statics[key] = cloneAnimationValue(value);
435
+ }
436
+ }
437
+ return { animated, statics };
438
+ };
439
+ // =============================================================================
440
+ // Animated Components (Web)
441
+ // =============================================================================
442
+ /**
443
+ * Create a Gui-compatible animated component for web
444
+ * Supports data- attributes, className, and proper style handling
445
+ */
446
+ function createWebAnimatedComponent(defaultTag) {
447
+ const isText = defaultTag === 'span';
448
+ const Component = Animated.createAnimatedComponent(forwardRef((propsIn, ref) => {
449
+ const { forwardedRef, render = defaultTag, ...rest } = propsIn;
450
+ const hostRef = useRef(null);
451
+ const composedRefs = useComposedRefs(forwardedRef, ref, hostRef);
452
+ const stateRef = useRef({
453
+ get host() {
454
+ return hostRef.current;
455
+ },
456
+ });
457
+ const [, themeState] = useThemeWithState({});
458
+ const result = getSplitStyles(rest, isText ? Text.staticConfig : View.staticConfig, themeState?.theme ?? {}, themeState?.name ?? '', { unmounted: false }, { isAnimated: false, noClass: true });
459
+ const viewProps = result?.viewProps ?? {};
460
+ const Element = render;
461
+ const transformedProps = hooks.usePropsTransform?.(render, viewProps, stateRef, false);
462
+ return _jsx(Element, { ...transformedProps, ref: composedRefs });
463
+ }));
464
+ Component.acceptRenderProp = true;
465
+ return Component;
466
+ }
467
+ const AnimatedView = createWebAnimatedComponent('div');
468
+ const AnimatedText = createWebAnimatedComponent('span');
469
+ /**
470
+ * Builds animation config from a transition prop.
471
+ * Shared logic used in both initial render and style emitter updates.
472
+ */
473
+ function buildTransitionConfig(transition, animations, animationState, styleKeys) {
474
+ const normalized = normalizeTransition(transition);
475
+ const effectiveKey = getEffectiveAnimation(normalized, animationState);
476
+ let base = cloneTransitionConfig(effectiveKey
477
+ ? (animations[effectiveKey] ??
478
+ { type: 'spring' })
479
+ : { type: 'spring' });
480
+ if (normalized.delay) {
481
+ base = cloneTransitionConfig({ ...base, delay: normalized.delay });
482
+ }
483
+ if (normalized.config) {
484
+ base = cloneTransitionConfig({ ...base, ...normalized.config });
485
+ // infer type: 'timing' if duration is provided without spring params
486
+ if (base.type !== 'timing' &&
487
+ normalized.config.duration !== undefined &&
488
+ normalized.config.damping === undefined &&
489
+ normalized.config.stiffness === undefined &&
490
+ normalized.config.mass === undefined) {
491
+ base = cloneTransitionConfig({ ...base, type: 'timing' });
492
+ }
493
+ }
494
+ // build per-property configs
495
+ const propertyConfigs = {};
496
+ for (const key of styleKeys) {
497
+ const propAnimation = normalized.properties[key];
498
+ if (typeof propAnimation === 'string') {
499
+ propertyConfigs[key] = cloneTransitionConfig(animations[propAnimation] ?? base);
500
+ }
501
+ else if (propAnimation && typeof propAnimation === 'object') {
502
+ const configType = propAnimation.type;
503
+ const baseForProp = configType
504
+ ? (animations[configType] ?? base)
505
+ : base;
506
+ propertyConfigs[key] = cloneTransitionConfig({
507
+ ...baseForProp,
508
+ ...propAnimation,
509
+ });
510
+ }
511
+ else {
512
+ propertyConfigs[key] = cloneTransitionConfig(base);
513
+ }
514
+ }
515
+ return { baseConfig: base, propertyConfigs };
516
+ }
517
+ /**
518
+ * Extracts all style keys including transform sub-properties.
519
+ */
520
+ function getStyleKeys(style) {
521
+ const keys = new Set(Object.keys(style));
522
+ if (style.transform && Array.isArray(style.transform)) {
523
+ for (const t of style.transform) {
524
+ if (t && typeof t === 'object') {
525
+ keys.add(Object.keys(t)[0]);
526
+ }
527
+ }
528
+ }
529
+ return keys;
530
+ }
531
+ // =============================================================================
532
+ // Animation Driver Factory
533
+ // =============================================================================
534
+ /**
535
+ * Create a Reanimated-based animation driver for Gui.
536
+ *
537
+ * This is a native Reanimated implementation without Moti dependency.
538
+ * It provides smooth spring and timing animations with full support for:
539
+ * - Per-property animation configurations
540
+ * - Exit animations with proper completion callbacks
541
+ * - Dynamic theme value resolution
542
+ * - Transform property animations
543
+ * - avoidReRenders optimization for hover/press/focus states
544
+ *
545
+ * @example
546
+ * ```tsx
547
+ * const animations = createAnimations({
548
+ * fast: { type: 'spring', damping: 20, stiffness: 250 },
549
+ * slow: { type: 'timing', duration: 500 },
550
+ * })
551
+ *
552
+ * ```
553
+ */
554
+ export function createAnimations(animationsConfig) {
555
+ // Normalize animation configs - default to spring if not specified
556
+ // This matches behavior of moti and motion drivers
557
+ const animations = {};
558
+ for (const key in animationsConfig) {
559
+ animations[key] = cloneTransitionConfig({
560
+ type: 'spring',
561
+ ...animationsConfig[key],
562
+ });
563
+ }
564
+ return {
565
+ needsCustomComponent: true,
566
+ View: isWeb ? AnimatedView : Animated.View,
567
+ Text: isWeb ? AnimatedText : Animated.Text,
568
+ isReactNative: true,
569
+ inputStyle: 'value',
570
+ outputStyle: 'inline',
571
+ avoidReRenders: true,
572
+ animations,
573
+ usePresence,
574
+ ResetPresence,
575
+ // =========================================================================
576
+ // useAnimatedNumber - For imperative animated values
577
+ // =========================================================================
578
+ useAnimatedNumber(initial) {
579
+ const sharedValue = useSharedValue(initial);
580
+ return useMemo(() => ({
581
+ getInstance() {
582
+ 'worklet';
583
+ return sharedValue;
584
+ },
585
+ getValue() {
586
+ 'worklet';
587
+ return sharedValue.value;
588
+ },
589
+ setValue(next, config = { type: 'spring' }, onFinish) {
590
+ if (config.type === 'direct') {
591
+ sharedValue.value = next;
592
+ onFinish?.();
593
+ }
594
+ else {
595
+ // animations must start on the ui thread in reanimated 4.x -
596
+ // withSpring/withTiming return animation descriptors that only
597
+ // work when assigned to a SharedValue inside the UI runtime
598
+ const cb = onFinish
599
+ ? (finished) => {
600
+ 'worklet';
601
+ if (finished !== false) {
602
+ runOnJS(onFinish)();
603
+ }
604
+ }
605
+ : undefined;
606
+ const animationConfig = cloneTransitionConfig(config);
607
+ if (isWeb) {
608
+ // on web there's no UI thread - set directly
609
+ sharedValue.value = applyAnimation(next, animationConfig, cb);
610
+ }
611
+ else {
612
+ runOnUI((targetValue, animationConfig) => {
613
+ 'worklet';
614
+ sharedValue.value = applyAnimation(targetValue, animationConfig, cb);
615
+ })(next, animationConfig);
616
+ }
617
+ }
618
+ },
619
+ stop() {
620
+ cancelAnimation(sharedValue);
621
+ },
622
+ }), [sharedValue]);
623
+ },
624
+ // =========================================================================
625
+ // useAnimatedNumberReaction - React to animated value changes
626
+ // =========================================================================
627
+ useAnimatedNumberReaction({ value }, onValue) {
628
+ const instance = value.getInstance();
629
+ return useAnimatedReaction(() => instance.value, (next, prev) => {
630
+ if (prev !== next) {
631
+ runOnJS(onValue)(next);
632
+ }
633
+ }, [onValue, instance]);
634
+ },
635
+ // =========================================================================
636
+ // useAnimatedNumberStyle - Create animated styles from values
637
+ // =========================================================================
638
+ useAnimatedNumberStyle(val, getStyle) {
639
+ const instance = val.getInstance();
640
+ if (isWeb) {
641
+ // web: must pass explicit deps (no babel plugin)
642
+ return useAnimatedStyle(() => {
643
+ 'worklet';
644
+ return getStyle(instance.value);
645
+ }, [instance, getStyle]);
646
+ }
647
+ const styleVal = useDerivedValue(() => {
648
+ 'worklet';
649
+ return getStyle(instance.value);
650
+ });
651
+ const animatedStyle = useAnimatedStyle(() => {
652
+ 'worklet';
653
+ return styleVal.value;
654
+ });
655
+ silenceAnimatedComponentDevCheck(animatedStyle);
656
+ return animatedStyle;
657
+ },
658
+ useAnimatedNumbersStyle(vals, getStyle) {
659
+ const instances = vals.map((v) => v.getInstance());
660
+ const animatedStyle = useAnimatedStyle(() => {
661
+ 'worklet';
662
+ const currentValues = instances.map((inst) => inst.value);
663
+ return getStyle(...currentValues);
664
+ }, isWeb ? [getStyle, ...instances] : undefined);
665
+ silenceAnimatedComponentDevCheck(animatedStyle);
666
+ return animatedStyle;
667
+ },
668
+ // =========================================================================
669
+ // useAnimations - Main animation hook for components
670
+ // =========================================================================
671
+ useAnimations(animationProps) {
672
+ const { props, presence, style, componentState, useStyleEmitter, themeName, stateRef, styleState, onDidAnimate, } = animationProps;
673
+ // State flags
674
+ const isHydrating = componentState.unmounted === true;
675
+ const isMounting = componentState.unmounted === 'should-enter';
676
+ const isEntering = !!componentState.unmounted;
677
+ // Presence state for exit animations
678
+ const isExiting = presence?.[0] === false;
679
+ // Track if we just finished entering (transition from entering to not entering)
680
+ const wasEnteringRef = useRef(isEntering);
681
+ const justFinishedEntering = wasEnteringRef.current && !isEntering;
682
+ React.useEffect(() => {
683
+ wasEnteringRef.current = isEntering;
684
+ });
685
+ // Use effectiveTransition computed by createComponent (single source of truth)
686
+ const effectiveTransition = styleState?.effectiveTransition ?? props.transition;
687
+ const normalized = normalizeTransition(effectiveTransition);
688
+ // Use 'enter' if we're mounting OR if we just finished entering
689
+ const animationState = isExiting
690
+ ? 'exit'
691
+ : isMounting || justFinishedEntering
692
+ ? 'enter'
693
+ : 'default';
694
+ const animationKey = getEffectiveAnimation(normalized, animationState);
695
+ const disableAnimation = isHydrating || !animationKey;
696
+ // Theme state for dynamic values - use themeName from props instead of hook
697
+ const isDark = themeName?.startsWith('dark') || false;
698
+ // Get sendExitComplete callback from presence
699
+ const sendExitComplete = presence?.[1];
700
+ const onDidAnimateRef = useRef(onDidAnimate);
701
+ useIsomorphicLayoutEffect(() => {
702
+ onDidAnimateRef.current = onDidAnimate;
703
+ }, [onDidAnimate]);
704
+ const didAnimateCycleIdRef = useRef(0);
705
+ const pendingDidAnimateKeysRef = useRef(new Set());
706
+ const didAnimateCompletedRef = useRef(false);
707
+ const isCompletingAnimationRef = useSharedValue(false);
708
+ const didAnimateCycleIdShared = useSharedValue(0);
709
+ const markDidAnimateKeyDone = useEvent((key, cycleId, finished) => {
710
+ if (!finished || cycleId !== didAnimateCycleIdRef.current)
711
+ return;
712
+ if (didAnimateCompletedRef.current)
713
+ return;
714
+ pendingDidAnimateKeysRef.current.delete(key);
715
+ if (pendingDidAnimateKeysRef.current.size === 0) {
716
+ didAnimateCompletedRef.current = true;
717
+ isCompletingAnimationRef.value = false;
718
+ onDidAnimateRef.current?.();
719
+ }
720
+ });
721
+ // =========================================================================
722
+ // Exit cycle state for deterministic per-property completion tracking
723
+ // =========================================================================
724
+ const exitCycleIdRef = useRef(0);
725
+ const pendingExitKeysRef = useRef(new Set());
726
+ const exitCompletedRef = useRef(false);
727
+ const wasExitingRef = useRef(false);
728
+ // detect transition into/out of exiting state
729
+ const justStartedExiting = isExiting && !wasExitingRef.current;
730
+ const justStoppedExiting = !isExiting && wasExitingRef.current;
731
+ // stable callback to mark a property as done (called from worklet via runOnJS)
732
+ const markExitKeyDone = useEvent((key, cycleId, finished) => {
733
+ // ignore callbacks from stale cycles
734
+ if (cycleId !== exitCycleIdRef.current)
735
+ return;
736
+ // ignore if already completed
737
+ if (exitCompletedRef.current)
738
+ return;
739
+ // count both finished and canceled animations as "done" during exit
740
+ // (element is leaving anyway, canceled animations shouldn't block completion)
741
+ pendingExitKeysRef.current.delete(key);
742
+ // check if all exit animations are done
743
+ if (pendingExitKeysRef.current.size === 0) {
744
+ exitCompletedRef.current = true;
745
+ sendExitComplete?.();
746
+ }
747
+ });
748
+ // SharedValue to pass exit state into worklet
749
+ const isExitingRef = useSharedValue(isExiting);
750
+ const exitCycleIdShared = useSharedValue(exitCycleIdRef.current);
751
+ // start new exit cycle only on transition INTO exiting (not every render while exiting)
752
+ if (justStartedExiting) {
753
+ exitCycleIdRef.current++;
754
+ exitCompletedRef.current = false;
755
+ pendingExitKeysRef.current.clear();
756
+ didAnimateCycleIdRef.current++;
757
+ pendingDidAnimateKeysRef.current.clear();
758
+ didAnimateCompletedRef.current = true;
759
+ }
760
+ // invalidate pending callbacks when exit is canceled/interrupted
761
+ if (justStoppedExiting) {
762
+ exitCycleIdRef.current++;
763
+ pendingExitKeysRef.current.clear();
764
+ }
765
+ // update shared values (using effect to avoid writing during render)
766
+ useIsomorphicLayoutEffect(() => {
767
+ isExitingRef.value = isExiting;
768
+ exitCycleIdShared.value = exitCycleIdRef.current;
769
+ if (justStartedExiting) {
770
+ isCompletingAnimationRef.value = false;
771
+ didAnimateCycleIdShared.value = didAnimateCycleIdRef.current;
772
+ }
773
+ }, [isExiting, exitCycleIdRef.current, justStartedExiting]);
774
+ // track previous exiting state
775
+ React.useEffect(() => {
776
+ wasExitingRef.current = isExiting;
777
+ });
778
+ // =========================================================================
779
+ // avoidRerenders: SharedValues for style updates without re-renders
780
+ // =========================================================================
781
+ const committedRenderKeysRef = useRef(new Set());
782
+ // full painted style of the last committed source (render or emitter):
783
+ // statics + animated targets, transform sub-keys flattened as `transform:key`.
784
+ // seeds read from it.
785
+ const lastPaintedRef = useRef({});
786
+ // per-key first-animated values React keeps painting under the mapper —
787
+ // see the styles memo below
788
+ const carryRef = useRef({});
789
+ // Separate styles into animated and static
790
+ const { animatedStyles, staticStyles, nextCarry } = useMemo(() => {
791
+ const animateOnly = props.animateOnly;
792
+ const { animated, statics } = splitAnimationStyles(style, isDark, disableAnimation, animateOnly);
793
+ // every animated key keeps its FIRST animated value in React's style for
794
+ // as long as it stays animated. the value never changes across renders,
795
+ // so React's per-key style diff never touches the key again and the
796
+ // mapper's inline writes survive every commit — reanimated's Fabric
797
+ // commit hook provides this guarantee on native, web has no equivalent.
798
+ // during enter the carry is the enterStyle (the mapper animates over it);
799
+ // a key appearing post-mount (a just-measured height) paints its value in
800
+ // the same commit the mapper first sees it, a frame before the mapper's
801
+ // first write.
802
+ // derive from committed state. mutating carryRef during render leaks
803
+ // abandoned concurrent renders into later commits.
804
+ const nextCarry = cloneStyleRecord(carryRef.current);
805
+ for (const key in nextCarry) {
806
+ if (!(key in animated))
807
+ delete nextCarry[key];
808
+ }
809
+ for (const key in animated) {
810
+ if (!(key in nextCarry)) {
811
+ nextCarry[key] = cloneAnimationValue(animated[key]);
812
+ }
813
+ statics[key] = nextCarry[key];
814
+ }
815
+ return { animatedStyles: animated, staticStyles: statics, nextCarry };
816
+ }, [disableAnimation, style, isDark, props.animateOnly]);
817
+ const renderSnapshot = useMemo(() => buildSnapshot(animatedStyles, staticStyles, getAnimatedTransforms(animatedStyles.transform), committedRenderKeysRef.current, lastPaintedRef.current), [animatedStyles, staticStyles]);
818
+ const renderSnapshotRef = useSharedValue({
819
+ animated: {},
820
+ statics: {},
821
+ transforms: [],
822
+ gatedKeys: {},
823
+ seeds: {},
824
+ removedKeys: {},
825
+ removeTransform: false,
826
+ clearValue: isWeb ? '' : null,
827
+ });
828
+ const emitterSnapshotRef = useSharedValue(null);
829
+ const emitterKeysRef = useRef(null);
830
+ useIsomorphicLayoutEffect(() => {
831
+ carryRef.current = nextCarry;
832
+ committedRenderKeysRef.current = renderSnapshot.keys;
833
+ lastPaintedRef.current = renderSnapshot.painted;
834
+ }, [nextCarry, renderSnapshot]);
835
+ // reconcile the emitter latch with real re-renders.
836
+ //
837
+ // the avoidReRenders fast path lets a pure pseudo change (hover/press/focus) push
838
+ // styles straight to the worklet via useStyleEmitter with no React commit — it sets
839
+ // emitterSnapshotRef. the worklet then treats `emitterSnapshotRef !== null` as a
840
+ // permanent latch: once the emitter has fired even once, it reads its last-emitted
841
+ // snapshot and IGNORES `animatedStyles` from every subsequent re-render. so a base
842
+ // style that changes through React (e.g. a row's `backgroundColor` flipping with an
843
+ // external selection store, not a hover) never reaches the screen — it stays stuck on
844
+ // the stale emitted value until the next hover re-fires the emitter. (the "active row
845
+ // highlight gets stuck until you hover it again" report.)
846
+ //
847
+ // a real re-render is the freshest source of truth for the base style, so on render we
848
+ // drop the emitter latch and let the worklet fall back to `animatedStyles`. the one
849
+ // exception: while a self pseudo (hover/press/focus) is active the emitted style is a
850
+ // transient override that isn't in this render, so keep it latched — otherwise an
851
+ // unrelated re-render while hovering would snap back to the base. `pseudoActiveRef`
852
+ // tracks that flag straight off the emitter (which fires on every pseudo transition), so
853
+ // the un-hover/un-press emission flips it false and the next render reclaims the style.
854
+ // no value comparison needed; runtime media stays correct because a render re-reads live
855
+ // media into `animatedStyles`.
856
+ // exit overrides the pseudo exception: the exit render's style IS the
857
+ // authoritative target, and an element that exits while hovered/focused (every
858
+ // dialog closed by clicking a button inside it) would otherwise keep the stale
859
+ // emitted base latched — the worklet then "animates" to values already on
860
+ // screen, every exit-key callback completes in a frame, and AnimatePresence
861
+ // unmounts immediately (the dialog-exit-snap). mirrored into a plain ref so the
862
+ // emitter callback (which runs outside render) can read it.
863
+ const pseudoActiveRef = useRef(false);
864
+ const isExitingJSRef = useRef(false);
865
+ isExitingJSRef.current = isExiting;
866
+ // the committed render is the worklet's source of truth whenever the emitter is not
867
+ // latched, so every render must publish its snapshot — not just the renders that drop
868
+ // a latch. animated keys live only in this snapshot after mount (staticStyles carries
869
+ // them during mount only), so a render that never publishes leaves the worklet reading
870
+ // an empty snapshot and the animated properties never reach the screen at all.
871
+ const publishedSnapshotRef = useRef(null);
872
+ useIsomorphicLayoutEffect(() => {
873
+ const droppingLatch = (isExiting || !pseudoActiveRef.current) && emitterSnapshotRef.value !== null;
874
+ // when the latch drops, keys the emitter owned that this render no longer has must
875
+ // be cleared too, otherwise reanimated keeps painting the stale emitted value
876
+ const emitterKeys = droppingLatch ? emitterKeysRef.current : null;
877
+ if (droppingLatch || publishedSnapshotRef.current !== renderSnapshot.value) {
878
+ const removedKeys = emitterKeys
879
+ ? {
880
+ ...renderSnapshot.value.removedKeys,
881
+ ...getRemovedAnimatedKeys(renderSnapshot.keys, emitterKeys),
882
+ }
883
+ : renderSnapshot.value.removedKeys;
884
+ renderSnapshotRef.value = {
885
+ ...renderSnapshot.value,
886
+ removedKeys,
887
+ removeTransform: Object.keys(removedKeys).some((key) => key.startsWith('transform:')),
888
+ };
889
+ publishedSnapshotRef.current = renderSnapshot.value;
890
+ }
891
+ if (droppingLatch) {
892
+ emitterSnapshotRef.value = null;
893
+ emitterKeysRef.current = null;
894
+ }
895
+ });
896
+ // Build animation config with per-property overrides using normalized transition
897
+ const { baseConfig, propertyConfigs } = useMemo(() => {
898
+ if (isHydrating) {
899
+ return {
900
+ baseConfig: { type: 'timing', duration: 0 },
901
+ propertyConfigs: {},
902
+ };
903
+ }
904
+ // use effectiveTransition (createComponent's single source of truth) so a
905
+ // platform-pseudo component with no declared transition resolves its BASE
906
+ // config to instant ('0ms') too — not just its pseudo flips. otherwise base
907
+ // style changes on the driver path animate with the default spring and a
908
+ // hover-out / base color change visibly lingers (looks "stuck").
909
+ return buildTransitionConfig(effectiveTransition, animations, animationState, getStyleKeys(animatedStyles));
910
+ }, [isHydrating, effectiveTransition, animatedStyles, animationState]);
911
+ // Store config in SharedValue for worklet access (concurrent-safe)
912
+ // Using useEffect to avoid writing to shared value during render
913
+ const configRef = useSharedValue({
914
+ baseConfig,
915
+ propertyConfigs,
916
+ disableAnimation,
917
+ isHydrating,
918
+ });
919
+ useIsomorphicLayoutEffect(() => {
920
+ configRef.value = {
921
+ baseConfig,
922
+ propertyConfigs,
923
+ disableAnimation,
924
+ isHydrating,
925
+ };
926
+ }, [baseConfig, propertyConfigs, disableAnimation, isHydrating]);
927
+ // =========================================================================
928
+ // avoidRerenders: register style emitter callback
929
+ // when hover/press/etc state changes, this is called instead of re-rendering
930
+ // =========================================================================
931
+ useStyleEmitter?.((nextStyle, effectiveTransition, pseudoActive) => {
932
+ // while exiting, the exit render owns the style — a pseudo flip mid-exit
933
+ // (hover-out as the element fades under the cursor) must not re-latch the
934
+ // base style over the in-flight exit targets.
935
+ if (isExitingJSRef.current)
936
+ return;
937
+ // track whether a self pseudo is active so the render-time layout effect knows whether
938
+ // this emitter snapshot is a transient override to keep latched or a base it can drop.
939
+ pseudoActiveRef.current = pseudoActive === true;
940
+ const animateOnly = props.animateOnly;
941
+ // effectiveTransition is computed in createComponent based on entering/exiting pseudo states
942
+ // rebuild config whenever transition changes (entering OR exiting pseudo states)
943
+ const transitionToUse = effectiveTransition ?? props.transition;
944
+ const { baseConfig: newBase, propertyConfigs: newPropertyConfigs } = buildTransitionConfig(transitionToUse, animations, animationState, getStyleKeys(nextStyle));
945
+ // update configRef with the new config
946
+ configRef.value = {
947
+ baseConfig: newBase,
948
+ propertyConfigs: newPropertyConfigs,
949
+ disableAnimation: configRef.value.disableAnimation,
950
+ isHydrating: configRef.value.isHydrating,
951
+ };
952
+ const previousKeys = emitterKeysRef.current ?? committedRenderKeysRef.current;
953
+ const { animated, statics } = splitAnimationStyles(nextStyle, isDark, configRef.value.disableAnimation, animateOnly);
954
+ const snapshot = buildSnapshot(animated, statics, getAnimatedTransforms(animated.transform), previousKeys, lastPaintedRef.current);
955
+ emitterSnapshotRef.value = snapshot.value;
956
+ emitterKeysRef.current = snapshot.keys;
957
+ lastPaintedRef.current = snapshot.painted;
958
+ if (process.env.NODE_ENV === 'development' &&
959
+ props.debug &&
960
+ props.debug !== 'profile') {
961
+ console.info('[animations-reanimated] useStyleEmitter update', {
962
+ animated,
963
+ statics,
964
+ transforms: snapshot.value.transforms,
965
+ });
966
+ }
967
+ });
968
+ // Compute and register exit keys synchronously during render to avoid race conditions
969
+ // This must happen BEFORE useAnimatedStyle runs so callbacks have a populated set
970
+ const exitKeysRegistered = useRef(false);
971
+ if (justStartedExiting && sendExitComplete) {
972
+ const exitKeys = getGatedKeys(renderSnapshot.value);
973
+ pendingExitKeysRef.current = new Set(exitKeys);
974
+ exitKeysRegistered.current = exitKeys.length > 0;
975
+ }
976
+ // onDidAnimate is an internal completion hook used by components that
977
+ // need to retain content through an in-place style transition. register
978
+ // the exact keys that can animate before publishing the cycle to the
979
+ // worklet; shared values are only written from the layout effect, never
980
+ // from the mapper that reads them.
981
+ const previousDidAnimateStyleRef = useRef(null);
982
+ const didAnimateStyle = onDidAnimate ? JSON.stringify(style) : null;
983
+ const previousDidAnimateStyle = previousDidAnimateStyleRef.current;
984
+ const shouldRegisterDidAnimate = didAnimateStyle !== null &&
985
+ previousDidAnimateStyle !== null &&
986
+ previousDidAnimateStyle !== didAnimateStyle &&
987
+ !isExiting &&
988
+ !isEntering &&
989
+ !justFinishedEntering;
990
+ const didAnimateKeys = shouldRegisterDidAnimate
991
+ ? getGatedKeys(renderSnapshot.value)
992
+ : [];
993
+ useIsomorphicLayoutEffect(() => {
994
+ if (didAnimateStyle === null) {
995
+ previousDidAnimateStyleRef.current = null;
996
+ isCompletingAnimationRef.value = false;
997
+ return;
998
+ }
999
+ // only a committed render may advance the authoritative cycle. the
1000
+ // captured previous value prevents StrictMode's repeated effect from
1001
+ // registering the same transition twice.
1002
+ const previousStyleStillCurrent = previousDidAnimateStyleRef.current === previousDidAnimateStyle;
1003
+ previousDidAnimateStyleRef.current = didAnimateStyle;
1004
+ if (!shouldRegisterDidAnimate || !previousStyleStillCurrent)
1005
+ return;
1006
+ const cycleId = ++didAnimateCycleIdRef.current;
1007
+ pendingDidAnimateKeysRef.current = new Set(didAnimateKeys);
1008
+ didAnimateCompletedRef.current = didAnimateKeys.length === 0;
1009
+ isCompletingAnimationRef.value = didAnimateKeys.length > 0;
1010
+ didAnimateCycleIdShared.value = cycleId;
1011
+ if (didAnimateKeys.length === 0) {
1012
+ onDidAnimateRef.current?.();
1013
+ }
1014
+ }, [didAnimateStyle, shouldRegisterDidAnimate]);
1015
+ // handle zero-animation case in effect (after render commit)
1016
+ React.useEffect(() => {
1017
+ if (!justStartedExiting || !sendExitComplete)
1018
+ return;
1019
+ // if no keys were registered, complete immediately
1020
+ if (!exitKeysRegistered.current && pendingExitKeysRef.current.size === 0) {
1021
+ if (!exitCompletedRef.current) {
1022
+ exitCompletedRef.current = true;
1023
+ sendExitComplete();
1024
+ }
1025
+ }
1026
+ }, [justStartedExiting, sendExitComplete]);
1027
+ // the worklet's own record of which animated keys it has emitted (mirrors
1028
+ // reanimated's per-view style history, which resets whenever a key leaves
1029
+ // the output). a key not in here is fresh to the mapper no matter what
1030
+ // React has committed — several renders can coalesce into one mapper run.
1031
+ // native updates use modify with the same object and no forced update, so
1032
+ // the mapper doesn't react to its own history write.
1033
+ const mapperStateRef = useSharedValue({
1034
+ emitted: {},
1035
+ });
1036
+ // Create animated style
1037
+ const animatedStyle = useAnimatedStyle(() => {
1038
+ 'worklet';
1039
+ const mapperState = mapperStateRef.value;
1040
+ const config = configRef.value;
1041
+ if (config.disableAnimation || config.isHydrating) {
1042
+ // the empty return wipes reanimated's per-key history, so ours
1043
+ // resets with it
1044
+ updateMapperState(mapperStateRef, {});
1045
+ return {};
1046
+ }
1047
+ const previouslyEmitted = mapperState.emitted;
1048
+ const emitted = {};
1049
+ const result = {};
1050
+ // Check if we have avoidRerenders updates from useStyleEmitter
1051
+ const emitterSnapshot = emitterSnapshotRef.value;
1052
+ const snapshot = emitterSnapshot ?? renderSnapshotRef.value;
1053
+ // Use emitter values if available, otherwise use the committed render snapshot.
1054
+ // statics must fall back to the render's staticStyles (not {}): once an
1055
+ // emitter snapshot has applied static keys, reanimated never unsets
1056
+ // them, so after the latch drops a stale emitted value (e.g. a border
1057
+ // color that missed animateOnly) would keep painting over the fresh
1058
+ // style prop forever
1059
+ const animatedValues = snapshot.animated;
1060
+ const staticValues = snapshot.statics;
1061
+ // read exit state from shared values
1062
+ const currentlyExiting = isExitingRef.value;
1063
+ const currentCycleId = exitCycleIdShared.value;
1064
+ const currentlyCompletingAnimation = isCompletingAnimationRef.value;
1065
+ const currentDidAnimateCycleId = didAnimateCycleIdShared.value;
1066
+ // Include static values from emitter (for hover/press style changes)
1067
+ for (const key in staticValues) {
1068
+ result[key] = staticValues[key];
1069
+ }
1070
+ for (const key in snapshot.removedKeys) {
1071
+ if (!key.startsWith('transform:') && !(key in staticValues)) {
1072
+ result[key] = snapshot.clearValue;
1073
+ }
1074
+ }
1075
+ // Animate regular properties
1076
+ for (const key in animatedValues) {
1077
+ if (key === 'transform')
1078
+ continue;
1079
+ const targetValue = animatedValues[key];
1080
+ emitted[key] = true;
1081
+ if (typeof targetValue !== 'number' && typeof targetValue !== 'string') {
1082
+ result[key] = targetValue;
1083
+ continue;
1084
+ }
1085
+ 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]);
1086
+ }
1087
+ // Handle transforms
1088
+ const transforms = snapshot.transforms;
1089
+ // Animate transform properties with validation
1090
+ if (transforms.length > 0 || snapshot.removeTransform) {
1091
+ const validTransforms = [];
1092
+ for (const t of transforms) {
1093
+ if (!t)
1094
+ continue;
1095
+ const keys = Object.keys(t);
1096
+ if (keys.length === 0)
1097
+ continue;
1098
+ const transformKey = keys[0];
1099
+ const value = t[transformKey];
1100
+ if (typeof value === 'number' || typeof value === 'string') {
1101
+ const propConfig = config.propertyConfigs[transformKey] ?? config.baseConfig;
1102
+ const subKey = `transform:${transformKey}`;
1103
+ emitted[subKey] = true;
1104
+ validTransforms.push({
1105
+ [transformKey]: animateSnapshotValue(subKey, transformKey, value, propConfig, !!previouslyEmitted[subKey], snapshot.seeds[subKey], !!snapshot.gatedKeys[subKey], currentlyExiting, currentCycleId, currentlyCompletingAnimation, currentDidAnimateCycleId, markExitKeyDone, markDidAnimateKeyDone),
1106
+ });
1107
+ }
1108
+ else {
1109
+ validTransforms.push(t);
1110
+ }
1111
+ }
1112
+ if (validTransforms.length > 0 || !('transform' in staticValues)) {
1113
+ result.transform = validTransforms;
1114
+ }
1115
+ }
1116
+ updateMapperState(mapperStateRef, emitted);
1117
+ return result;
1118
+ }, isWeb
1119
+ ? [
1120
+ // pass SharedValues so the stable mapper watches them on web (no babel plugin)
1121
+ renderSnapshotRef,
1122
+ emitterSnapshotRef,
1123
+ configRef,
1124
+ isExitingRef,
1125
+ exitCycleIdShared,
1126
+ markExitKeyDone,
1127
+ isCompletingAnimationRef,
1128
+ didAnimateCycleIdShared,
1129
+ markDidAnimateKeyDone,
1130
+ ]
1131
+ : undefined);
1132
+ silenceAnimatedComponentDevCheck(animatedStyle);
1133
+ // Debug logging
1134
+ if (process.env.NODE_ENV === 'development' &&
1135
+ props.debug &&
1136
+ props.debug !== 'profile') {
1137
+ console.info('[animations-reanimated] useAnimations', {
1138
+ animationKey,
1139
+ componentState,
1140
+ isExiting,
1141
+ animatedStyles,
1142
+ staticStyles,
1143
+ baseConfig,
1144
+ propertyConfigs,
1145
+ });
1146
+ }
1147
+ // staticStyles carries animated keys only while mounting or on a key's
1148
+ // first post-mount appearance; after that the mapper owns them and the
1149
+ // commit bridge above keeps React commits from wiping its inline writes.
1150
+ return {
1151
+ style: [staticStyles, animatedStyle],
1152
+ };
1153
+ },
1154
+ };
1155
+ }
1156
+ //# sourceMappingURL=createAnimations.js.map