@tamagui/animations-reanimated 3.0.0-beta.924.1 → 3.0.1

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.
@@ -30,11 +30,13 @@ module.exports = __toCommonJS(createAnimations_exports);
30
30
  var import_animation_helpers = require("@tamagui/animation-helpers");
31
31
  var import_core = require("@tamagui/core");
32
32
  var import_internal_runtime = require("@tamagui/web/internal-runtime");
33
+ var import_textInputHost = require("./textInputHost.cjs");
33
34
  var import_use_presence = require("@tamagui/use-presence");
34
35
  var import_normalize_colors = __toESM(require("@react-native/normalize-colors"), 1);
35
36
  var import_react = __toESM(require("react"), 1);
36
37
  var import_react_native_reanimated = __toESM(require("react-native-reanimated"), 1);
37
38
  var import_jsx_runtime = require("react/jsx-runtime");
39
+ const NON_ANIMATABLE_VALUE = /^(auto|min-content|max-content|fit-content|stretch|inherit|initial|unset)\b|^calc\(/;
38
40
  const getDefaultExport = (module2) => {
39
41
  const mod = module2;
40
42
  if (mod.__esModule || mod[Symbol.toStringTag] === "Module") {
@@ -60,13 +62,15 @@ const resolveDynamicValue = (value, isDark) => {
60
62
  }
61
63
  return value;
62
64
  };
63
- const updateMapperState = import_core.isWeb ? (state, emitted) => {
65
+ const updateMapperState = import_core.isWeb ? (state, emitted, ownedStatics) => {
64
66
  state.value.emitted = emitted;
65
- } : (state, emitted) => {
67
+ state.value.ownedStatics = ownedStatics;
68
+ } : (state, emitted, ownedStatics) => {
66
69
  "worklet";
67
70
  if (!globalThis._WORKLET) return;
68
71
  state.modify((current) => {
69
72
  current.emitted = emitted;
73
+ current.ownedStatics = ownedStatics;
70
74
  return current;
71
75
  }, false);
72
76
  };
@@ -120,6 +124,11 @@ const getImplicitDefault = (key, targetValue) => {
120
124
  }
121
125
  return suffixIndex > 0 ? `${value}${targetValue.slice(suffixIndex)}` : value;
122
126
  };
127
+ const getLeadingRatio = (metrics) => {
128
+ const ratio = metrics?.lineHeight;
129
+ return typeof ratio === "number" && Number.isFinite(ratio) && ratio >= 0 ? ratio : void 0;
130
+ };
131
+ const derivesLeadingProduct = (style, ratio, inheritsFontSize, resolved) => ratio !== void 0 && (inheritsFontSize || typeof style.lineHeight === "number" && typeof style.fontSize === "number" && (!!(0, import_animation_helpers.getTransitionForKey)(resolved, "fontSize") || !!(0, import_animation_helpers.getTransitionForKey)(resolved, "lineHeight")));
123
132
  const COLOR_STYLE_KEYS = {
124
133
  backgroundColor: true,
125
134
  borderColor: true,
@@ -162,7 +171,7 @@ const normalizeAnimationColor = (value) => {
162
171
  const alpha = (color & 255) / 255;
163
172
  return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
164
173
  };
165
- const buildSnapshot = (animated, statics, transforms, previousKeys, lastPainted) => {
174
+ const buildSnapshot = (animated, statics, transforms, previousKeys, lastPainted, derivesLeading = false) => {
166
175
  const snapshotAnimated = {};
167
176
  const snapshotStatics = cloneStyleRecord(statics);
168
177
  for (const key in snapshotStatics) {
@@ -186,6 +195,7 @@ const buildSnapshot = (animated, statics, transforms, previousKeys, lastPainted)
186
195
  }
187
196
  const snapshotTransforms = transforms.map(cloneStyleRecord);
188
197
  const keys = new Set(Object.keys(snapshotAnimated));
198
+ if (derivesLeading) keys.add("lineHeight");
189
199
  for (const transform of snapshotTransforms) {
190
200
  const key = Object.keys(transform)[0];
191
201
  const value = key ? transform[key] : void 0;
@@ -194,10 +204,19 @@ const buildSnapshot = (animated, statics, transforms, previousKeys, lastPainted)
194
204
  }
195
205
  }
196
206
  const seeds = {};
207
+ const staticTransforms = getAnimatedTransforms(snapshotStatics.transform);
208
+ for (const transform of staticTransforms) {
209
+ const key = Object.keys(transform)[0];
210
+ if (keys.has(`transform:${key}`) && previousKeys.has(`transform:${key}`)) seeds[`transform:${key}`] = transform[key];
211
+ }
197
212
  for (const key of keys) {
198
- let value = lastPainted[key];
199
- if (typeof value !== "number" && typeof value !== "string") continue;
200
- if (value === "auto" || typeof value === "string" && value.startsWith("calc")) {
213
+ let value = previousKeys.has(key) ? seeds[key] ?? snapshotStatics[key] ?? lastPainted[key] : lastPainted[key];
214
+ if (typeof value !== "number" && typeof value !== "string") {
215
+ delete seeds[key];
216
+ continue;
217
+ }
218
+ if (typeof value === "string" && NON_ANIMATABLE_VALUE.test(value)) {
219
+ delete seeds[key];
201
220
  continue;
202
221
  }
203
222
  if (COLOR_STYLE_KEYS[key]) value = normalizeAnimationColor(value);
@@ -205,13 +224,13 @@ const buildSnapshot = (animated, statics, transforms, previousKeys, lastPainted)
205
224
  }
206
225
  const gatedKeys = {};
207
226
  for (const key of keys) gatedKeys[key] = true;
227
+ if (derivesLeading) delete gatedKeys.lineHeight;
208
228
  const removedKeys = getRemovedAnimatedKeys(keys, previousKeys);
209
229
  const painted = {
210
230
  ...snapshotStatics,
211
231
  ...snapshotAnimated
212
232
  };
213
- const staticTransforms = getAnimatedTransforms(snapshotStatics.transform);
214
- delete painted.transform;
233
+ painted.transform = snapshotTransforms.length ? snapshotTransforms : staticTransforms;
215
234
  for (const transform of staticTransforms) {
216
235
  const key = Object.keys(transform)[0];
217
236
  if (key) painted[`transform:${key}`] = transform[key];
@@ -226,6 +245,7 @@ const buildSnapshot = (animated, statics, transforms, previousKeys, lastPainted)
226
245
  statics: snapshotStatics,
227
246
  transforms: snapshotTransforms,
228
247
  gatedKeys,
248
+ derivesLeading,
229
249
  seeds,
230
250
  removedKeys,
231
251
  removeTransform: Object.keys(removedKeys).some((key) => key.startsWith("transform:")),
@@ -249,7 +269,7 @@ const createReanimatedConfig = (config) => {
249
269
  }
250
270
  return next;
251
271
  };
252
- const applyAnimation = (targetValue, config, callback, seedValue, validateStartAsColor = false) => {
272
+ const applyAnimation = (targetValue, config, callback, seedValue, validateStartAsColor = false, publishTo) => {
253
273
  "worklet";
254
274
  const delay = config.delay;
255
275
  const reanimatedConfig = createReanimatedConfig(config);
@@ -264,7 +284,11 @@ const applyAnimation = (targetValue, config, callback, seedValue, validateStartA
264
284
  return innerOnFrame(animation, now < animation.lastTimestamp ? animation.lastTimestamp : now);
265
285
  };
266
286
  }
267
- if (seedValue !== void 0 || validateStartAsColor) {
287
+ const isAnimationDescriptor = animatedValue !== null && typeof animatedValue !== "number" && typeof animatedValue !== "string" && typeof animatedValue !== "boolean" && typeof animatedValue !== "undefined";
288
+ if (isAnimationDescriptor && delay && delay > 0) {
289
+ animatedValue = (0, import_react_native_reanimated.withDelay)(delay, animatedValue);
290
+ }
291
+ if (isAnimationDescriptor && (seedValue !== void 0 || validateStartAsColor)) {
268
292
  const innerOnStart = animatedValue.onStart;
269
293
  animatedValue.onStart = (animation, value, timestamp, previousAnimation) => {
270
294
  "worklet";
@@ -272,15 +296,28 @@ const applyAnimation = (targetValue, config, callback, seedValue, validateStartA
272
296
  innerOnStart(animation, startValue, timestamp, previousAnimation);
273
297
  };
274
298
  }
275
- if (delay && delay > 0) {
276
- animatedValue = (0, import_react_native_reanimated.withDelay)(delay, animatedValue);
299
+ if (isAnimationDescriptor && publishTo) {
300
+ const startedOn = animatedValue.onStart;
301
+ animatedValue.onStart = (animation, value, timestamp, previousAnimation) => {
302
+ "worklet";
303
+ startedOn(animation, value, timestamp, previousAnimation);
304
+ publishTo.value = animation.current;
305
+ };
306
+ const framedOn = animatedValue.onFrame;
307
+ animatedValue.onFrame = (animation, now) => {
308
+ "worklet";
309
+ const finished = framedOn(animation, now);
310
+ publishTo.value = animation.current;
311
+ return finished;
312
+ };
277
313
  }
278
314
  return animatedValue;
279
315
  };
280
- const animateSnapshotValue = (key, implicitKey, targetValue, config, previouslyEmitted, seedValue, gated, currentlyExiting, exitCycleId, currentlyCompletingEnter, enterCycleId, currentlyCompletingUpdate, updateCycleId, markExitKeyDone, markEnterKeyDone, markUpdateKeyDone, validateStartAsColor = false) => {
316
+ const animateSnapshotValue = (key, implicitKey, targetValue, config, previouslyEmitted, seedValue, gated, currentlyExiting, exitCycleId, currentlyCompletingEnter, enterCycleId, currentlyCompletingUpdate, updateCycleId, markExitKeyDone, markEnterKeyDone, markUpdateKeyDone, validateStartAsColor = false, publishTo) => {
281
317
  "worklet";
282
318
  const cycleGated = gated && (currentlyExiting || currentlyCompletingEnter || currentlyCompletingUpdate);
283
319
  if (!previouslyEmitted && seedValue === void 0 && !cycleGated) {
320
+ if (publishTo && typeof targetValue === "number") publishTo.value = targetValue;
284
321
  return targetValue;
285
322
  }
286
323
  let callback;
@@ -300,7 +337,7 @@ const animateSnapshotValue = (key, implicitKey, targetValue, config, previouslyE
300
337
  (0, import_react_native_reanimated.runOnJS)(markUpdateKeyDone)(key, updateCycleId, finished ?? false);
301
338
  };
302
339
  }
303
- return applyAnimation(targetValue, config, callback, previouslyEmitted ? void 0 : seedValue ?? getImplicitDefault(implicitKey, targetValue), validateStartAsColor);
340
+ return applyAnimation(targetValue, config, callback, previouslyEmitted ? void 0 : seedValue ?? getImplicitDefault(implicitKey, targetValue), validateStartAsColor, publishTo);
304
341
  };
305
342
  const ANIMATABLE_PROPERTIES = {
306
343
  transform: true,
@@ -357,20 +394,18 @@ const ANIMATABLE_PROPERTIES = {
357
394
  flexGrow: true,
358
395
  flexShrink: true
359
396
  };
360
- const canAnimateProperty = (key, value, animateOnly) => {
397
+ const canAnimateProperty = (key, value) => {
361
398
  if (!ANIMATABLE_PROPERTIES[key]) return false;
362
- if (value === "auto") return false;
363
- if (typeof value === "string" && value.startsWith("calc")) return false;
364
- if (animateOnly && !animateOnly.includes(key)) return false;
399
+ if (typeof value === "string" && NON_ANIMATABLE_VALUE.test(value)) return false;
365
400
  return true;
366
401
  };
367
- const splitAnimationStyles = (style, isDark, disableAnimation, animateOnly) => {
402
+ const splitAnimationStyles = (style, isDark, disableAnimation) => {
368
403
  const animated = {};
369
404
  const statics = {};
370
405
  for (const key in style) {
371
406
  const value = resolveDynamicValue(style[key], isDark);
372
407
  if (value === void 0) continue;
373
- if (!disableAnimation && canAnimateProperty(key, value, animateOnly)) {
408
+ if (!disableAnimation && canAnimateProperty(key, value)) {
374
409
  animated[key] = cloneAnimationValue(value);
375
410
  } else {
376
411
  statics[key] = cloneAnimationValue(value);
@@ -409,49 +444,72 @@ function createWebAnimatedComponent(defaultTag) {
409
444
  }
410
445
  const AnimatedView = createWebAnimatedComponent("div");
411
446
  const AnimatedText = createWebAnimatedComponent("span");
412
- function buildTransitionConfig(transition, animations, animationState, styleKeys) {
413
- const normalized = (0, import_animation_helpers.normalizeTransition)(transition);
414
- const effectiveKey = (0, import_animation_helpers.getEffectiveAnimation)(normalized, animationState);
415
- let base = cloneTransitionConfig(effectiveKey ? animations[effectiveKey] ?? { type: "spring" } : { type: "spring" });
416
- if (normalized.delay) {
417
- base = cloneTransitionConfig({
418
- ...base,
419
- delay: normalized.delay
420
- });
447
+ let animatedTextInput;
448
+ const defaultConfig = { type: "spring" };
449
+ const bezierEasings = /* @__PURE__ */ new Map();
450
+ function bezierEasing(easing) {
451
+ if (!bezierEasings.has(easing)) {
452
+ const bezier = (0, import_animation_helpers.easingToBezier)(easing);
453
+ bezierEasings.set(easing, bezier ? import_react_native_reanimated.Easing.bezier(bezier[0], bezier[1], bezier[2], bezier[3]) : void 0);
421
454
  }
422
- if (normalized.config) {
423
- base = cloneTransitionConfig({
424
- ...base,
425
- ...normalized.config
426
- });
427
- if (base.type !== "timing" && normalized.config.duration !== void 0 && normalized.config.damping === void 0 && normalized.config.stiffness === void 0 && normalized.config.mass === void 0) {
428
- base = cloneTransitionConfig({
429
- ...base,
430
- type: "timing"
431
- });
432
- }
455
+ return bezierEasings.get(easing);
456
+ }
457
+ function entryToReanimated(entry) {
458
+ if (entry.timing.kind === "spring") {
459
+ return {
460
+ type: "spring",
461
+ stiffness: entry.timing.stiffness,
462
+ damping: entry.timing.damping,
463
+ mass: entry.timing.mass,
464
+ delay: entry.delayMs || void 0
465
+ };
433
466
  }
467
+ return {
468
+ type: "timing",
469
+ duration: entry.timing.durationMs,
470
+ easing: bezierEasing(entry.timing.easing),
471
+ delay: entry.delayMs || void 0
472
+ };
473
+ }
474
+ function buildTransitionConfig(transition, animations, animationState, styleKeys) {
475
+ const resolved = (0, import_animation_helpers.forAnimationState)((0, import_animation_helpers.resolveTransition)(transition, { animations }), animationState);
476
+ const base = resolved.all ? entryToReanimated(resolved.all) : { ...defaultConfig };
434
477
  const propertyConfigs = {};
435
478
  for (const key of styleKeys) {
436
- const propAnimation = normalized.properties[key];
437
- if (typeof propAnimation === "string") {
438
- propertyConfigs[key] = cloneTransitionConfig(animations[propAnimation] ?? base);
439
- } else if (propAnimation && typeof propAnimation === "object") {
440
- const configType = propAnimation.type;
441
- const baseForProp = configType ? animations[configType] ?? base : base;
442
- propertyConfigs[key] = cloneTransitionConfig({
443
- ...baseForProp,
444
- ...propAnimation
445
- });
446
- } else {
447
- propertyConfigs[key] = cloneTransitionConfig(base);
448
- }
479
+ const entry = (0, import_animation_helpers.getTransitionForKey)(resolved, key);
480
+ propertyConfigs[key] = entry ? entryToReanimated(entry) : instantConfig();
449
481
  }
450
482
  return {
451
483
  baseConfig: base,
452
- propertyConfigs
484
+ propertyConfigs,
485
+ resolved
453
486
  };
454
487
  }
488
+ const instantConfig = () => ({
489
+ type: "timing",
490
+ duration: 0
491
+ });
492
+ function sameTransitionConfig(a, b) {
493
+ const aKeys = Object.keys(a);
494
+ if (aKeys.length !== Object.keys(b).length) return false;
495
+ for (const key of aKeys) {
496
+ if (a[key] !== b[key]) return false;
497
+ }
498
+ return true;
499
+ }
500
+ function sameDriverConfig(a, b) {
501
+ if (a.disableAnimation !== b.disableAnimation || a.isHydrating !== b.isHydrating) {
502
+ return false;
503
+ }
504
+ if (!sameTransitionConfig(a.baseConfig, b.baseConfig)) return false;
505
+ const aKeys = Object.keys(a.propertyConfigs);
506
+ if (aKeys.length !== Object.keys(b.propertyConfigs).length) return false;
507
+ for (const key of aKeys) {
508
+ const other = b.propertyConfigs[key];
509
+ if (!other || !sameTransitionConfig(a.propertyConfigs[key], other)) return false;
510
+ }
511
+ return true;
512
+ }
455
513
  function getStyleKeys(style) {
456
514
  const keys = new Set(Object.keys(style));
457
515
  if (style.transform && Array.isArray(style.transform)) {
@@ -463,14 +521,7 @@ function getStyleKeys(style) {
463
521
  }
464
522
  return keys;
465
523
  }
466
- function createAnimations(animationsConfig) {
467
- const animations = {};
468
- for (const key in animationsConfig) {
469
- animations[key] = cloneTransitionConfig({
470
- type: "spring",
471
- ...animationsConfig[key]
472
- });
473
- }
524
+ function createAnimations(animations) {
474
525
  return {
475
526
  needsCustomComponent: true,
476
527
  View: import_core.isWeb ? AnimatedView : Animated.View,
@@ -481,6 +532,31 @@ function createAnimations(animationsConfig) {
481
532
  animations,
482
533
  usePresence: import_use_presence.usePresence,
483
534
  ResetPresence: import_use_presence.ResetPresence,
535
+ useTextMetrics: ({ inheritedText, lineHeight }) => {
536
+ const node = inheritedText?.driver === "reanimated" ? inheritedText.fontSize : null;
537
+ const ratio = typeof lineHeight === "number" ? lineHeight : null;
538
+ const animatedStyle = (0, import_react_native_reanimated.useAnimatedStyle)(() => {
539
+ "worklet";
540
+ if (!node) return {};
541
+ const fontSize = node.value;
542
+ return ratio === null ? { fontSize } : {
543
+ fontSize,
544
+ lineHeight: fontSize * ratio
545
+ };
546
+ }, [node, ratio]);
547
+ const textChannel = inheritedText ?? null;
548
+ if (!node || !import_textInputHost.TextInputHost) return {
549
+ style: null,
550
+ textChannel
551
+ };
552
+ animatedTextInput ||= Animated.createAnimatedComponent(import_textInputHost.TextInputHost);
553
+ return {
554
+ style: animatedStyle,
555
+ textChannel,
556
+ Text: import_core.isWeb ? AnimatedText : Animated.Text,
557
+ TextInput: animatedTextInput
558
+ };
559
+ },
484
560
  useAnimatedNumber(initial) {
485
561
  const sharedValue = (0, import_react_native_reanimated.useSharedValue)(initial);
486
562
  return (0, import_react.useMemo)(() => ({
@@ -557,7 +633,7 @@ function createAnimations(animationsConfig) {
557
633
  return animatedStyle;
558
634
  },
559
635
  useAnimations(animationProps) {
560
- const { props, presence, style, componentState, useStyleEmitter, themeName, stateRef, styleState, onTransition } = animationProps;
636
+ const { props, presence, style, componentState, useStyleEmitter, themeName, stateRef, styleState, onTransition, inheritedText } = animationProps;
561
637
  const isHydrating = componentState.unmounted === true;
562
638
  const isMounting = componentState.unmounted === "should-enter";
563
639
  const isEntering = !!componentState.unmounted;
@@ -568,10 +644,27 @@ function createAnimations(animationsConfig) {
568
644
  wasEnteringRef.current = isEntering;
569
645
  });
570
646
  const effectiveTransition = styleState?.effectiveTransition ?? props.transition;
571
- const normalized = (0, import_animation_helpers.normalizeTransition)(effectiveTransition);
647
+ const leadingRatio = getLeadingRatio(styleState?.nativeTextMetrics);
572
648
  const animationState = isExiting ? "exit" : isMounting || justFinishedEntering ? "enter" : "default";
573
- const animationKey = (0, import_animation_helpers.getEffectiveAnimation)(normalized, animationState);
574
- const disableAnimation = isHydrating || !animationKey;
649
+ const resolvedTransition = (0, import_animation_helpers.forAnimationState)((0, import_animation_helpers.resolveTransition)(effectiveTransition, { animations }), animationState);
650
+ const disableAnimation = isHydrating || !(0, import_animation_helpers.hasTransition)(resolvedTransition);
651
+ const fontSizeShared = (0, import_react_native_reanimated.useSharedValue)(typeof style.fontSize === "number" ? style.fontSize : 0);
652
+ const publishesFontSize = !disableAnimation && typeof style.fontSize === "number";
653
+ const ownTextChannel = (0, import_react.useMemo)(() => ({
654
+ fontSize: fontSizeShared,
655
+ driver: "reanimated"
656
+ }), [fontSizeShared]);
657
+ const inheritedFontSize = leadingRatio !== void 0 && typeof style.fontSize !== "number" && inheritedText?.driver === "reanimated" ? inheritedText.fontSize : null;
658
+ const leadingFontSize = inheritedFontSize ?? fontSizeShared;
659
+ const leadingRatioShared = (0, import_react_native_reanimated.useSharedValue)(leadingRatio ?? 0);
660
+ const paintedRatioRef = (0, import_react.useRef)(leadingRatio);
661
+ const applyLeadingRatio = (ratio, config) => {
662
+ const previous = paintedRatioRef.current;
663
+ if (previous === ratio) return;
664
+ paintedRatioRef.current = ratio;
665
+ if (ratio === void 0) return;
666
+ leadingRatioShared.value = previous === void 0 || disableAnimation ? ratio : applyAnimation(ratio, config);
667
+ };
575
668
  const isDark = themeName?.startsWith("dark") || false;
576
669
  const sendExitComplete = presence?.[1];
577
670
  const onTransitionRef = (0, import_react.useRef)(onTransition);
@@ -596,16 +689,18 @@ function createAnimations(animationsConfig) {
596
689
  const updateCompletedRef = (0, import_react.useRef)(false);
597
690
  const isCompletingUpdateRef = (0, import_react_native_reanimated.useSharedValue)(false);
598
691
  const updateCycleIdShared = (0, import_react_native_reanimated.useSharedValue)(0);
692
+ const updateInterruptedRef = (0, import_react.useRef)(false);
599
693
  const markUpdateKeyDone = (0, import_core.useEvent)((key, cycleId, finished) => {
600
- if (!finished || cycleId !== updateCycleIdRef.current) return;
694
+ if (cycleId !== updateCycleIdRef.current) return;
601
695
  if (updateCompletedRef.current) return;
696
+ if (!finished) updateInterruptedRef.current = true;
602
697
  pendingUpdateKeysRef.current.delete(key);
603
698
  if (pendingUpdateKeysRef.current.size === 0) {
604
699
  updateCompletedRef.current = true;
605
700
  isCompletingUpdateRef.value = false;
606
701
  if (updateStartedRef.current) {
607
702
  updateStartedRef.current = false;
608
- emit("end", "update", true);
703
+ emit("end", "update", !updateInterruptedRef.current);
609
704
  }
610
705
  }
611
706
  });
@@ -690,16 +785,45 @@ function createAnimations(animationsConfig) {
690
785
  const committedRenderKeysRef = (0, import_react.useRef)(/* @__PURE__ */ new Set());
691
786
  const lastPaintedRef = (0, import_react.useRef)({});
692
787
  const carryRef = (0, import_react.useRef)({});
788
+ const exitSeedRef = (0, import_react.useRef)(null);
789
+ const paintedPredecessor = isExiting ? exitSeedRef.current ?? lastPaintedRef.current : lastPaintedRef.current;
790
+ (0, import_core.useIsomorphicLayoutEffect)(() => {
791
+ exitSeedRef.current = isExiting ? paintedPredecessor : null;
792
+ }, [isExiting, paintedPredecessor]);
693
793
  const { animatedStyles, staticStyles, nextCarry } = (0, import_react.useMemo)(() => {
694
- const animateOnly = props.animateOnly;
695
- const { animated, statics } = splitAnimationStyles(style, isDark, disableAnimation, animateOnly);
794
+ const { animated, statics } = splitAnimationStyles(style, isDark, disableAnimation);
795
+ if (isExiting && animated.transform) {
796
+ const remaining = getAnimatedTransforms(animated.transform).map(cloneStyleRecord);
797
+ const previous = getAnimatedTransforms(paintedPredecessor.transform);
798
+ if (previous.length) {
799
+ animated.transform = previous.map((entry) => {
800
+ const key = Object.keys(entry)[0];
801
+ const index = remaining.findIndex((target) => key in target);
802
+ return index < 0 ? cloneStyleRecord(entry) : remaining.splice(index, 1)[0];
803
+ }).concat(remaining);
804
+ }
805
+ }
696
806
  const nextCarry2 = cloneStyleRecord(carryRef.current);
697
807
  for (const key in nextCarry2) {
698
808
  if (!(key in animated)) delete nextCarry2[key];
699
809
  }
700
810
  for (const key in animated) {
701
811
  if (!(key in nextCarry2)) {
702
- nextCarry2[key] = cloneAnimationValue(animated[key]);
812
+ const target = animated[key];
813
+ if (isExiting && key === "transform") {
814
+ nextCarry2[key] = getAnimatedTransforms(target).map((transform) => {
815
+ const initial = cloneStyleRecord(transform);
816
+ for (const part in initial) {
817
+ const value = initial[part];
818
+ if (typeof value === "number" || typeof value === "string") {
819
+ initial[part] = paintedPredecessor[`transform:${part}`] ?? getImplicitDefault(part, value);
820
+ }
821
+ }
822
+ return initial;
823
+ });
824
+ } else {
825
+ nextCarry2[key] = cloneAnimationValue(isExiting && (typeof target === "number" || typeof target === "string") ? paintedPredecessor[key] ?? getImplicitDefault(key, target) : target);
826
+ }
703
827
  }
704
828
  statics[key] = nextCarry2[key];
705
829
  }
@@ -712,14 +836,22 @@ function createAnimations(animationsConfig) {
712
836
  disableAnimation,
713
837
  style,
714
838
  isDark,
715
- props.animateOnly
839
+ isExiting,
840
+ isExiting ? paintedPredecessor : null
841
+ ]);
842
+ const derivesLeading = derivesLeadingProduct(animatedStyles, leadingRatio, !!inheritedFontSize, resolvedTransition);
843
+ const renderSnapshot = (0, import_react.useMemo)(() => buildSnapshot(animatedStyles, staticStyles, getAnimatedTransforms(animatedStyles.transform), committedRenderKeysRef.current, paintedPredecessor, derivesLeading), [
844
+ animatedStyles,
845
+ staticStyles,
846
+ isExiting ? paintedPredecessor : null,
847
+ derivesLeading
716
848
  ]);
717
- const renderSnapshot = (0, import_react.useMemo)(() => buildSnapshot(animatedStyles, staticStyles, getAnimatedTransforms(animatedStyles.transform), committedRenderKeysRef.current, lastPaintedRef.current), [animatedStyles, staticStyles]);
718
849
  const renderSnapshotRef = (0, import_react_native_reanimated.useSharedValue)({
719
850
  animated: {},
720
851
  statics: {},
721
852
  transforms: [],
722
853
  gatedKeys: {},
854
+ derivesLeading: false,
723
855
  seeds: {},
724
856
  removedKeys: {},
725
857
  removeTransform: false,
@@ -772,40 +904,49 @@ function createAnimations(animationsConfig) {
772
904
  animatedStyles,
773
905
  animationState
774
906
  ]);
775
- const configRef = (0, import_react_native_reanimated.useSharedValue)({
907
+ const renderConfig = {
776
908
  baseConfig,
777
909
  propertyConfigs,
778
910
  disableAnimation,
779
911
  isHydrating
780
- });
912
+ };
913
+ const configRef = (0, import_react_native_reanimated.useSharedValue)(renderConfig);
914
+ const writtenConfigRef = (0, import_react.useRef)(renderConfig);
915
+ const writeConfig = (next) => {
916
+ if (sameDriverConfig(writtenConfigRef.current, next)) return;
917
+ writtenConfigRef.current = next;
918
+ configRef.value = next;
919
+ };
781
920
  (0, import_core.useIsomorphicLayoutEffect)(() => {
782
- configRef.value = {
783
- baseConfig,
784
- propertyConfigs,
785
- disableAnimation,
786
- isHydrating
787
- };
921
+ writeConfig(renderConfig);
788
922
  }, [
789
923
  baseConfig,
790
924
  propertyConfigs,
791
925
  disableAnimation,
792
926
  isHydrating
793
927
  ]);
794
- useStyleEmitter?.((nextStyle, effectiveTransition2, pseudoActive) => {
928
+ (0, import_core.useIsomorphicLayoutEffect)(() => {
929
+ if (emitterSnapshotRef.value !== null) return;
930
+ applyLeadingRatio(leadingRatio, propertyConfigs.lineHeight ?? baseConfig);
931
+ });
932
+ const onEmittedStyle = (nextStyle, effectiveTransition2, pseudoActive, nextMetrics) => {
795
933
  if (isExitingJSRef.current) return;
796
934
  pseudoActiveRef.current = pseudoActive === true;
797
- const animateOnly = props.animateOnly;
798
935
  const transitionToUse = effectiveTransition2 ?? props.transition;
799
- const { baseConfig: newBase, propertyConfigs: newPropertyConfigs } = buildTransitionConfig(transitionToUse, animations, animationState, getStyleKeys(nextStyle));
800
- configRef.value = {
936
+ const emittedLeadingRatio = getLeadingRatio(nextMetrics);
937
+ const { baseConfig: newBase, propertyConfigs: newPropertyConfigs, resolved: emittedResolved } = buildTransitionConfig(transitionToUse, animations, animationState, getStyleKeys(nextStyle));
938
+ const written = writtenConfigRef.current;
939
+ writeConfig({
801
940
  baseConfig: newBase,
802
941
  propertyConfigs: newPropertyConfigs,
803
- disableAnimation: configRef.value.disableAnimation,
804
- isHydrating: configRef.value.isHydrating
805
- };
942
+ disableAnimation: written.disableAnimation,
943
+ isHydrating: written.isHydrating
944
+ });
806
945
  const previousKeys = emitterKeysRef.current ?? committedRenderKeysRef.current;
807
- const { animated, statics } = splitAnimationStyles(nextStyle, isDark, configRef.value.disableAnimation, animateOnly);
808
- const snapshot = buildSnapshot(animated, statics, getAnimatedTransforms(animated.transform), previousKeys, lastPaintedRef.current);
946
+ const { animated, statics } = splitAnimationStyles(nextStyle, isDark, written.disableAnimation);
947
+ const emittedDerivesLeading = derivesLeadingProduct(animated, emittedLeadingRatio, !!inheritedFontSize, emittedResolved);
948
+ applyLeadingRatio(emittedLeadingRatio, newPropertyConfigs.lineHeight ?? newBase);
949
+ const snapshot = buildSnapshot(animated, statics, getAnimatedTransforms(animated.transform), previousKeys, lastPaintedRef.current, emittedDerivesLeading);
809
950
  emitterSnapshotRef.value = snapshot.value;
810
951
  emitterKeysRef.current = snapshot.keys;
811
952
  lastPaintedRef.current = snapshot.painted;
@@ -816,7 +957,8 @@ function createAnimations(animationsConfig) {
816
957
  transforms: snapshot.value.transforms
817
958
  });
818
959
  }
819
- });
960
+ };
961
+ useStyleEmitter?.(onEmittedStyle);
820
962
  const exitKeysRegistered = (0, import_react.useRef)(false);
821
963
  const enterKeysRegistered = (0, import_react.useRef)(false);
822
964
  if (!isExiting && justFinishedEntering && onTransition) {
@@ -847,6 +989,7 @@ function createAnimations(animationsConfig) {
847
989
  if (!shouldRegisterUpdate || !previousStyleStillCurrent) return;
848
990
  const cycleId = ++updateCycleIdRef.current;
849
991
  pendingUpdateKeysRef.current = new Set(updateKeys);
992
+ updateInterruptedRef.current = false;
850
993
  updateCompletedRef.current = updateKeys.length === 0;
851
994
  isCompletingUpdateRef.value = updateKeys.length > 0;
852
995
  updateCycleIdShared.value = cycleId;
@@ -905,17 +1048,22 @@ function createAnimations(animationsConfig) {
905
1048
  justFinishedEntering,
906
1049
  onTransition
907
1050
  ]);
908
- const mapperStateRef = (0, import_react_native_reanimated.useSharedValue)({ emitted: {} });
1051
+ const mapperStateRef = (0, import_react_native_reanimated.useSharedValue)({
1052
+ emitted: {},
1053
+ ownedStatics: {}
1054
+ });
909
1055
  const animatedStyle = (0, import_react_native_reanimated.useAnimatedStyle)(() => {
910
1056
  "worklet";
911
1057
  const mapperState = mapperStateRef.value;
912
1058
  const config = configRef.value;
913
1059
  if (config.disableAnimation || config.isHydrating) {
914
- updateMapperState(mapperStateRef, {});
1060
+ updateMapperState(mapperStateRef, {}, {});
915
1061
  return {};
916
1062
  }
917
1063
  const previouslyEmitted = mapperState.emitted;
918
1064
  const emitted = {};
1065
+ const previouslyOwned = mapperState.ownedStatics;
1066
+ const ownedStatics = {};
919
1067
  const result = {};
920
1068
  const emitterSnapshot = emitterSnapshotRef.value;
921
1069
  const snapshot = emitterSnapshot ?? renderSnapshotRef.value;
@@ -928,6 +1076,8 @@ function createAnimations(animationsConfig) {
928
1076
  const currentlyCompletingUpdate = isCompletingUpdateRef.value;
929
1077
  const currentUpdateCycleId = updateCycleIdShared.value;
930
1078
  for (const key in staticValues) {
1079
+ if (!emitterSnapshot && !previouslyOwned[key]) continue;
1080
+ ownedStatics[key] = true;
931
1081
  result[key] = staticValues[key];
932
1082
  }
933
1083
  for (const key in snapshot.removedKeys) {
@@ -939,11 +1089,16 @@ function createAnimations(animationsConfig) {
939
1089
  if (key === "transform") continue;
940
1090
  const targetValue = animatedValues[key];
941
1091
  emitted[key] = true;
1092
+ if (key === "lineHeight" && snapshot.derivesLeading) continue;
942
1093
  if (typeof targetValue !== "number" && typeof targetValue !== "string") {
943
1094
  result[key] = targetValue;
944
1095
  continue;
945
1096
  }
946
- result[key] = animateSnapshotValue(key, key, targetValue, config.propertyConfigs[key] ?? config.baseConfig, !!previouslyEmitted[key], snapshot.seeds[key], !!snapshot.gatedKeys[key], currentlyExiting, currentCycleId, currentlyCompletingEnter, currentEnterCycleId, currentlyCompletingUpdate, currentUpdateCycleId, markExitKeyDone, markEnterKeyDone, markUpdateKeyDone, !!COLOR_STYLE_KEYS[key]);
1097
+ result[key] = animateSnapshotValue(key, key, targetValue, config.propertyConfigs[key] ?? config.baseConfig, !!previouslyEmitted[key], snapshot.seeds[key], !!snapshot.gatedKeys[key], currentlyExiting, currentCycleId, currentlyCompletingEnter, currentEnterCycleId, currentlyCompletingUpdate, currentUpdateCycleId, markExitKeyDone, markEnterKeyDone, markUpdateKeyDone, !!COLOR_STYLE_KEYS[key], key === "fontSize" ? fontSizeShared : void 0);
1098
+ }
1099
+ if (snapshot.derivesLeading) {
1100
+ emitted.lineHeight = true;
1101
+ result.lineHeight = leadingFontSize.value * leadingRatioShared.value;
947
1102
  }
948
1103
  const transforms = snapshot.transforms;
949
1104
  if (transforms.length > 0 || snapshot.removeTransform) {
@@ -967,7 +1122,7 @@ function createAnimations(animationsConfig) {
967
1122
  result.transform = validTransforms;
968
1123
  }
969
1124
  }
970
- updateMapperState(mapperStateRef, emitted);
1125
+ updateMapperState(mapperStateRef, emitted, ownedStatics);
971
1126
  return result;
972
1127
  }, import_core.isWeb ? [
973
1128
  renderSnapshotRef,
@@ -981,12 +1136,15 @@ function createAnimations(animationsConfig) {
981
1136
  markEnterKeyDone,
982
1137
  isCompletingUpdateRef,
983
1138
  updateCycleIdShared,
984
- markUpdateKeyDone
1139
+ markUpdateKeyDone,
1140
+ fontSizeShared,
1141
+ leadingRatioShared,
1142
+ inheritedFontSize
985
1143
  ] : void 0);
986
1144
  silenceAnimatedComponentDevCheck(animatedStyle);
987
1145
  if (process.env.NODE_ENV === "development" && props.debug && props.debug !== "profile") {
988
1146
  console.info("[animations-reanimated] useAnimations", {
989
- animationKey,
1147
+ resolvedTransition,
990
1148
  componentState,
991
1149
  isExiting,
992
1150
  animatedStyles,
@@ -995,7 +1153,10 @@ function createAnimations(animationsConfig) {
995
1153
  propertyConfigs
996
1154
  });
997
1155
  }
998
- return { style: [staticStyles, animatedStyle] };
1156
+ return {
1157
+ style: [staticStyles, animatedStyle],
1158
+ textChannel: publishesFontSize ? ownTextChannel : typeof style.fontSize === "number" ? null : inheritedText ?? null
1159
+ };
999
1160
  }
1000
1161
  };
1001
1162
  }