@hanzogui/animations-reanimated 8.3.0 → 8.3.2

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