@tamagui/animations-reanimated 2.0.0-1768636514428 → 2.0.0-1768696252732

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.
@@ -1,16 +1,17 @@
1
1
  import { normalizeTransition } from '@tamagui/animation-helpers'
2
- import { ResetPresence, usePresence } from '@tamagui/use-presence'
3
2
  import {
4
3
  getSplitStyles,
5
4
  hooks,
6
5
  isWeb,
7
6
  Text,
8
7
  useComposedRefs,
8
+ useIsomorphicLayoutEffect,
9
9
  useThemeWithState,
10
10
  View,
11
11
  type AnimationDriver,
12
12
  type UniversalAnimatedNumber,
13
13
  } from '@tamagui/core'
14
+ import { ResetPresence, usePresence } from '@tamagui/use-presence'
14
15
  import React, { forwardRef, useMemo, useRef } from 'react'
15
16
  import type { SharedValue } from 'react-native-reanimated'
16
17
  import Animated_, {
@@ -66,9 +67,6 @@ type TimingConfig = {
66
67
  /** Combined animation configuration type */
67
68
  export type TransitionConfig = SpringConfig | TimingConfig
68
69
 
69
- /** Per-property animation overrides */
70
- type PropertyOverrides = Record<string, TransitionConfig | string>
71
-
72
70
  // =============================================================================
73
71
  // Utility Functions
74
72
  // =============================================================================
@@ -113,62 +111,6 @@ const applyAnimation = (
113
111
  return animatedValue
114
112
  }
115
113
 
116
- /**
117
- * Estimate spring animation duration based on physics parameters
118
- * Uses underdamped harmonic oscillator settling time formula
119
- *
120
- * Adds 15% buffer to ensure animation visually completes before exit callback
121
- */
122
- const estimateSpringDuration = (config: SpringConfig): number => {
123
- const stiffness = config.stiffness ?? 100
124
- const damping = config.damping ?? 10
125
- const mass = config.mass ?? 1
126
-
127
- // Guard against invalid parameters that would cause division by zero or NaN
128
- if (mass <= 0 || stiffness <= 0) {
129
- return 400 // sensible default
130
- }
131
-
132
- // Natural frequency: ω₀ = √(k/m)
133
- const omega0 = Math.sqrt(stiffness / mass)
134
- // Damping ratio: ζ = c / (2√(km))
135
- const zeta = damping / (2 * Math.sqrt(stiffness * mass))
136
-
137
- let duration: number
138
- if (zeta < 1 && zeta > 0 && omega0 > 0) {
139
- // Underdamped: oscillates, settling time ≈ 4 / (ζω₀)
140
- duration = (4 / (zeta * omega0)) * 1000
141
- } else if (omega0 > 0) {
142
- // Overdamped or critically damped
143
- duration = (2 / omega0) * 1000
144
- } else {
145
- duration = 400 // fallback
146
- }
147
-
148
- // Guard against NaN/Infinity from edge cases
149
- if (!Number.isFinite(duration)) {
150
- return 400
151
- }
152
-
153
- // Clamp and add 15% buffer to prevent premature exit callbacks
154
- return Math.ceil(Math.min(2000, Math.max(200, duration)) * 1.15)
155
- }
156
-
157
- /**
158
- * Get total animation duration including delay
159
- * Adds 50ms buffer for timing animations to ensure completion before callbacks
160
- */
161
- const getAnimationDuration = (config: TransitionConfig): number => {
162
- const delay = Math.max(0, config.delay ?? 0)
163
-
164
- if (config.type === 'timing') {
165
- const duration = Math.max(0, (config as TimingConfig).duration ?? 300)
166
- return duration + delay + 50
167
- }
168
-
169
- return estimateSpringDuration(config as SpringConfig) + delay
170
- }
171
-
172
114
  // =============================================================================
173
115
  // Animatable Properties
174
116
  // =============================================================================
@@ -269,7 +211,7 @@ function createWebAnimatedComponent(defaultTag: 'div' | 'span') {
269
211
 
270
212
  const Component = Animated.createAnimatedComponent(
271
213
  forwardRef((propsIn: any, ref) => {
272
- const { forwardedRef, tag = defaultTag, ...rest } = propsIn
214
+ const { forwardedRef, render = defaultTag, ...rest } = propsIn
273
215
  const hostRef = useRef<HTMLElement>(null)
274
216
  const composedRefs = useComposedRefs(forwardedRef, ref, hostRef)
275
217
 
@@ -291,9 +233,9 @@ function createWebAnimatedComponent(defaultTag: 'div' | 'span') {
291
233
  )
292
234
 
293
235
  const viewProps = result?.viewProps ?? {}
294
- const Element = tag
236
+ const Element = render
295
237
  const transformedProps = hooks.usePropsTransform?.(
296
- tag,
238
+ render,
297
239
  viewProps,
298
240
  stateRef as any,
299
241
  false
@@ -444,26 +386,29 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
444
386
  // useAnimations - Main animation hook for components
445
387
  // =========================================================================
446
388
  useAnimations(animationProps) {
447
- const { props, presence, style, componentState, useStyleEmitter } = animationProps
389
+ const { props, presence, style, componentState, useStyleEmitter, themeName } =
390
+ animationProps
448
391
 
449
- // Extract animation key
450
- const animationKey = Array.isArray(props.transition)
451
- ? props.transition[0]
452
- : props.transition
392
+ // Extract animation key from normalized transition
393
+ // props.transition can be: string | [string, config] | { default: string, ... }
394
+ const normalized = normalizeTransition(props.transition)
395
+ const animationKey = normalized.default
453
396
 
454
397
  // State flags
455
398
  const isHydrating = componentState.unmounted === true
456
399
  const isMounting = componentState.unmounted === 'should-enter'
457
400
  const disableAnimation = isHydrating || !animationKey
458
401
 
459
- // Theme state for dynamic values
460
- const [, themeState] = useThemeWithState({})
461
- const isDark = themeState?.scheme === 'dark' || themeState?.name?.startsWith('dark')
402
+ // Theme state for dynamic values - use themeName from props instead of hook
403
+ const isDark = themeName?.startsWith('dark') || false
462
404
 
463
405
  // Presence state for exit animations
464
406
  const isExiting = presence?.[0] === false
465
407
  const sendExitComplete = presence?.[1]
466
408
 
409
+ // Track exit animation progress (0 = not started, 1 = complete)
410
+ const exitProgress = useSharedValue(0)
411
+
467
412
  // =========================================================================
468
413
  // avoidRerenders: SharedValues for style updates without re-renders
469
414
  // =========================================================================
@@ -533,16 +478,15 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
533
478
  // Build per-property overrides from normalized properties
534
479
  const overrides: Record<string, TransitionConfig> = {}
535
480
 
536
- for (const [key, animationNameOrConfig] of Object.entries(
537
- normalized.properties
538
- )) {
481
+ for (const key in normalized.properties) {
482
+ const animationNameOrConfig = normalized.properties[key]
539
483
  if (typeof animationNameOrConfig === 'string') {
540
484
  // Property override referencing a named animation: { x: 'quick' }
541
485
  overrides[key] =
542
486
  animations[animationNameOrConfig as keyof typeof animations] ?? base
543
487
  } else if (animationNameOrConfig && typeof animationNameOrConfig === 'object') {
544
488
  // Property override with inline config: { x: { type: 'quick', delay: 100 } }
545
- const configType = animationNameOrConfig.type
489
+ const configType = (animationNameOrConfig as any).type
546
490
  const baseForProp = configType
547
491
  ? (animations[configType as keyof typeof animations] ?? base)
548
492
  : base
@@ -572,14 +516,18 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
572
516
  return { baseConfig: base, propertyConfigs: configs }
573
517
  }, [isHydrating, props.transition, animatedStyles])
574
518
 
575
- // Store config in ref for worklet access
576
- const configRef = useRef({
519
+ // Store config in SharedValue for worklet access (concurrent-safe)
520
+ // Using useEffect to avoid writing to shared value during render
521
+ const configRef = useSharedValue({
577
522
  baseConfig,
578
523
  propertyConfigs,
579
524
  disableAnimation,
580
525
  isHydrating,
581
526
  })
582
- configRef.current = { baseConfig, propertyConfigs, disableAnimation, isHydrating }
527
+
528
+ useIsomorphicLayoutEffect(() => {
529
+ configRef.set({ baseConfig, propertyConfigs, disableAnimation, isHydrating })
530
+ }, [baseConfig, propertyConfigs, disableAnimation, isHydrating])
583
531
 
584
532
  // =========================================================================
585
533
  // avoidRerenders: Register style emitter callback
@@ -595,9 +543,9 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
595
543
  const rawValue = nextStyle[key]
596
544
  const value = resolveDynamicValue(rawValue, isDark)
597
545
 
598
- if (value === undefined) continue
546
+ if (value == undefined) continue
599
547
 
600
- if (configRef.current.disableAnimation) {
548
+ if (configRef.get().disableAnimation) {
601
549
  statics[key] = value
602
550
  continue
603
551
  }
@@ -623,9 +571,10 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
623
571
  }
624
572
 
625
573
  // Update SharedValues - this triggers worklet without React re-render
626
- animatedTargetsRef.value = animated
627
- staticTargetsRef.value = statics
628
- transformTargetsRef.value = transforms
574
+ // Using .set() method for concurrent-safe updates
575
+ animatedTargetsRef.set(animated)
576
+ staticTargetsRef.set(statics)
577
+ transformTargetsRef.set(transforms)
629
578
 
630
579
  if (
631
580
  process.env.NODE_ENV === 'development' &&
@@ -640,16 +589,41 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
640
589
  }
641
590
  })
642
591
 
643
- // Handle exit animation completion
644
- // Use timeout based on calculated animation duration
592
+ // Handle exit animation completion using reanimated's native callback
593
+ // Animate exitProgress from 0 to 1 during exit, call sendExitComplete on completion
645
594
  React.useEffect(() => {
646
595
  if (!isExiting || !sendExitComplete) return
647
596
 
648
- const duration = getAnimationDuration(baseConfig)
649
- const timeoutId = setTimeout(sendExitComplete, duration)
597
+ // Use ref to get current config without adding to deps
598
+ const config = configRef.get().baseConfig
599
+
600
+ // Animate exitProgress to 1, which triggers sendExitComplete on completion
601
+ // Using .set() for React Compiler compatibility
602
+ if (config.type === 'timing') {
603
+ exitProgress.set(
604
+ withTiming(1, config as WithTimingConfig, (finished) => {
605
+ 'worklet'
606
+ if (finished) {
607
+ runOnJS(sendExitComplete)()
608
+ }
609
+ })
610
+ )
611
+ } else {
612
+ exitProgress.set(
613
+ withSpring(1, config as WithSpringConfig, (finished) => {
614
+ 'worklet'
615
+ if (finished) {
616
+ runOnJS(sendExitComplete)()
617
+ }
618
+ })
619
+ )
620
+ }
650
621
 
651
- return () => clearTimeout(timeoutId)
652
- }, [isExiting, sendExitComplete, baseConfig])
622
+ return () => {
623
+ // Cancel the exit animation if component unmounts early
624
+ cancelAnimation(exitProgress)
625
+ }
626
+ }, [isExiting, sendExitComplete])
653
627
 
654
628
  // Create animated style
655
629
  const animatedStyle = useAnimatedStyle(() => {
@@ -660,16 +634,16 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
660
634
  }
661
635
 
662
636
  const result: Record<string, any> = {}
663
- const config = configRef.current
637
+ const config = configRef.get()
664
638
 
665
639
  // Check if we have avoidRerenders updates
666
- const hasEmitterUpdates = animatedTargetsRef.value !== null
640
+ // Using .get() method for concurrent-safe reads in worklets
641
+ const emitterAnimated = animatedTargetsRef.get()
642
+ const hasEmitterUpdates = emitterAnimated !== null
667
643
 
668
644
  // Use emitter values if available, otherwise use React state values
669
- const animatedValues = hasEmitterUpdates
670
- ? animatedTargetsRef.value!
671
- : animatedStyles
672
- const staticValues = hasEmitterUpdates ? staticTargetsRef.value! : {}
645
+ const animatedValues = hasEmitterUpdates ? emitterAnimated! : animatedStyles
646
+ const staticValues = hasEmitterUpdates ? staticTargetsRef.get()! : {}
673
647
 
674
648
  // Include static values from emitter (for hover/press style changes)
675
649
  for (const key in staticValues) {
@@ -687,26 +661,27 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
687
661
 
688
662
  // Handle transforms
689
663
  const transforms = hasEmitterUpdates
690
- ? transformTargetsRef.value
664
+ ? transformTargetsRef.get()
691
665
  : animatedStyles.transform
692
666
 
693
667
  // Animate transform properties with validation
694
668
  if (transforms && Array.isArray(transforms)) {
695
- const validTransforms = (transforms as Record<string, unknown>[])
696
- .filter((t) => {
697
- // Validate transform object has at least one key with a numeric value
698
- if (!t || typeof t !== 'object') return false
699
- const keys = Object.keys(t)
700
- if (keys.length === 0) return false
701
- const value = t[keys[0]]
702
- return typeof value === 'number' || typeof value === 'string'
703
- })
704
- .map((t) => {
669
+ const validTransforms: Record<string, unknown>[] = []
670
+
671
+ for (const t of transforms) {
672
+ if (!t) continue
673
+ const keys = Object.keys(t)
674
+ if (keys.length === 0) continue
675
+ const value = t[keys[0]]
676
+ if (typeof value === 'number' || typeof value === 'string') {
705
677
  const transformKey = Object.keys(t)[0]
706
678
  const targetValue = t[transformKey]
707
679
  const propConfig = config.propertyConfigs[transformKey] ?? config.baseConfig
708
- return { [transformKey]: applyAnimation(targetValue as number, propConfig) }
709
- })
680
+ validTransforms.push({
681
+ [transformKey]: applyAnimation(targetValue as number, propConfig),
682
+ })
683
+ }
684
+ }
710
685
 
711
686
  if (validTransforms.length > 0) {
712
687
  result.transform = validTransforms
@@ -1,11 +1,11 @@
1
1
  {
2
- "mappings": "AAEA,cAQO,uBAEA;AAGP,cAUO,uBACA,wBACA;;KA2BF,eAAe;CAClB,OAAO;CACP;IACE,QAAQ;;KAGP,eAAe;CAClB,MAAM;CACN;IACE,QAAQ;;AAGZ,YAAY,mBAAmB,eAAe;;;;;;;;;;;;;;;;;;;;AA4Q9C,OAAO,iBAAS,iBAAiB,UAAU,eAAe,mBACxD,kBAAkB,IACjB,gBAAgB",
2
+ "mappings": "AACA,cAQO,uBAEA;AAIP,cAUO,uBACA,wBACA;;KA2BF,eAAe;CAClB,OAAO;CACP;IACE,QAAQ;;KAGP,eAAe;CAClB,MAAM;CACN;IACE,QAAQ;;AAGZ,YAAY,mBAAmB,eAAe;;;;;;;;;;;;;;;;;;;;AAiN9C,OAAO,iBAAS,iBAAiB,UAAU,eAAe,mBACxD,kBAAkB,IACjB,gBAAgB",
3
3
  "names": [],
4
4
  "sources": [
5
5
  "src/createAnimations.tsx"
6
6
  ],
7
7
  "sourcesContent": [
8
- "import { normalizeTransition } from '@tamagui/animation-helpers'\nimport { ResetPresence, usePresence } from '@tamagui/use-presence'\nimport {\n getSplitStyles,\n hooks,\n isWeb,\n Text,\n useComposedRefs,\n useThemeWithState,\n View,\n type AnimationDriver,\n type UniversalAnimatedNumber,\n} from '@tamagui/core'\nimport React, { forwardRef, useMemo, useRef } from 'react'\nimport type { SharedValue } from 'react-native-reanimated'\nimport Animated_, {\n cancelAnimation,\n runOnJS,\n useAnimatedReaction,\n useAnimatedStyle,\n useDerivedValue,\n useSharedValue,\n withDelay,\n withSpring,\n withTiming,\n type WithSpringConfig,\n type WithTimingConfig,\n} from 'react-native-reanimated'\n\n// =============================================================================\n// ESM/CJS compatibility\n// =============================================================================\n\n/**\n * Handle ESM/CJS module interop for react-native-reanimated\n * @see https://github.com/evanw/esbuild/issues/2480#issuecomment-1833104754\n */\nconst getDefaultExport = <T,>(module: T | { default: T }): T => {\n const mod = module as any\n if (mod.__esModule || mod[Symbol.toStringTag] === 'Module') {\n return mod.default || mod\n }\n return mod\n}\n\nconst Animated = getDefaultExport(Animated_)\n\n// =============================================================================\n// Types\n// =============================================================================\n\ntype ReanimatedAnimatedNumber = SharedValue<number>\n\n/** Spring animation configuration */\ntype SpringConfig = {\n type?: 'spring'\n delay?: number\n} & Partial<WithSpringConfig>\n\n/** Timing animation configuration */\ntype TimingConfig = {\n type: 'timing'\n delay?: number\n} & Partial<WithTimingConfig>\n\n/** Combined animation configuration type */\nexport type TransitionConfig = SpringConfig | TimingConfig\n\n/** Per-property animation overrides */\ntype PropertyOverrides = Record<string, TransitionConfig | string>\n\n// =============================================================================\n// Utility Functions\n// =============================================================================\n\n/**\n * Resolve dynamic theme values (e.g., `{dynamic: {dark: \"value\", light: \"value\"}}`)\n */\nconst resolveDynamicValue = (value: unknown, isDark: boolean): unknown => {\n if (\n value !== null &&\n typeof value === 'object' &&\n 'dynamic' in value &&\n typeof (value as any).dynamic === 'object'\n ) {\n const dynamic = (value as any).dynamic\n return isDark ? dynamic.dark : dynamic.light\n }\n return value\n}\n\n/**\n * Apply animation to a value based on config\n */\nconst applyAnimation = (\n targetValue: number | string,\n config: TransitionConfig\n): number | string => {\n 'worklet'\n const delay = config.delay\n\n let animatedValue: any\n if (config.type === 'timing') {\n animatedValue = withTiming(targetValue as number, config as WithTimingConfig)\n } else {\n animatedValue = withSpring(targetValue as number, config as WithSpringConfig)\n }\n\n if (delay && delay > 0) {\n animatedValue = withDelay(delay, animatedValue)\n }\n\n return animatedValue\n}\n\n/**\n * Estimate spring animation duration based on physics parameters\n * Uses underdamped harmonic oscillator settling time formula\n *\n * Adds 15% buffer to ensure animation visually completes before exit callback\n */\nconst estimateSpringDuration = (config: SpringConfig): number => {\n const stiffness = config.stiffness ?? 100\n const damping = config.damping ?? 10\n const mass = config.mass ?? 1\n\n // Guard against invalid parameters that would cause division by zero or NaN\n if (mass <= 0 || stiffness <= 0) {\n return 400 // sensible default\n }\n\n // Natural frequency: ω₀ = √(k/m)\n const omega0 = Math.sqrt(stiffness / mass)\n // Damping ratio: ζ = c / (2√(km))\n const zeta = damping / (2 * Math.sqrt(stiffness * mass))\n\n let duration: number\n if (zeta < 1 && zeta > 0 && omega0 > 0) {\n // Underdamped: oscillates, settling time ≈ 4 / (ζω₀)\n duration = (4 / (zeta * omega0)) * 1000\n } else if (omega0 > 0) {\n // Overdamped or critically damped\n duration = (2 / omega0) * 1000\n } else {\n duration = 400 // fallback\n }\n\n // Guard against NaN/Infinity from edge cases\n if (!Number.isFinite(duration)) {\n return 400\n }\n\n // Clamp and add 15% buffer to prevent premature exit callbacks\n return Math.ceil(Math.min(2000, Math.max(200, duration)) * 1.15)\n}\n\n/**\n * Get total animation duration including delay\n * Adds 50ms buffer for timing animations to ensure completion before callbacks\n */\nconst getAnimationDuration = (config: TransitionConfig): number => {\n const delay = Math.max(0, config.delay ?? 0)\n\n if (config.type === 'timing') {\n const duration = Math.max(0, (config as TimingConfig).duration ?? 300)\n return duration + delay + 50\n }\n\n return estimateSpringDuration(config as SpringConfig) + delay\n}\n\n// =============================================================================\n// Animatable Properties\n// =============================================================================\n\nconst ANIMATABLE_PROPERTIES: Record<string, boolean> = {\n // Transform\n transform: true,\n // Opacity\n opacity: true,\n // Dimensions\n height: true,\n width: true,\n minWidth: true,\n minHeight: true,\n maxWidth: true,\n maxHeight: true,\n // Background\n backgroundColor: true,\n // Border colors\n borderColor: true,\n borderLeftColor: true,\n borderRightColor: true,\n borderTopColor: true,\n borderBottomColor: true,\n // Border radius\n borderRadius: true,\n borderTopLeftRadius: true,\n borderTopRightRadius: true,\n borderBottomLeftRadius: true,\n borderBottomRightRadius: true,\n // Border width\n borderWidth: true,\n borderLeftWidth: true,\n borderRightWidth: true,\n borderTopWidth: true,\n borderBottomWidth: true,\n // Text\n color: true,\n fontSize: true,\n fontWeight: true,\n lineHeight: true,\n letterSpacing: true,\n // Position\n left: true,\n right: true,\n top: true,\n bottom: true,\n // Margin\n margin: true,\n marginTop: true,\n marginBottom: true,\n marginLeft: true,\n marginRight: true,\n marginHorizontal: true,\n marginVertical: true,\n // Padding\n padding: true,\n paddingTop: true,\n paddingBottom: true,\n paddingLeft: true,\n paddingRight: true,\n paddingHorizontal: true,\n paddingVertical: true,\n // Flex/Gap\n gap: true,\n rowGap: true,\n columnGap: true,\n flex: true,\n flexGrow: true,\n flexShrink: true,\n}\n\n/**\n * Check if a style property can be animated\n */\nconst canAnimateProperty = (\n key: string,\n value: unknown,\n animateOnly?: string[]\n): boolean => {\n if (!ANIMATABLE_PROPERTIES[key]) return false\n if (value === 'auto') return false\n if (typeof value === 'string' && value.startsWith('calc')) return false\n if (animateOnly && !animateOnly.includes(key)) return false\n return true\n}\n\n// =============================================================================\n// Animated Components (Web)\n// =============================================================================\n\n/**\n * Create a Tamagui-compatible animated component for web\n * Supports data- attributes, className, and proper style handling\n */\nfunction createWebAnimatedComponent(defaultTag: 'div' | 'span') {\n const isText = defaultTag === 'span'\n\n const Component = Animated.createAnimatedComponent(\n forwardRef((propsIn: any, ref) => {\n const { forwardedRef, tag = defaultTag, ...rest } = propsIn\n const hostRef = useRef<HTMLElement>(null)\n const composedRefs = useComposedRefs(forwardedRef, ref, hostRef)\n\n const stateRef = useRef<{ host: HTMLElement | null }>({\n get host() {\n return hostRef.current\n },\n })\n\n const [, themeState] = useThemeWithState({})\n\n const result = getSplitStyles(\n rest,\n isText ? Text.staticConfig : View.staticConfig,\n themeState?.theme ?? {},\n themeState?.name ?? '',\n { unmounted: false } as any,\n { isAnimated: false, noClass: true }\n )\n\n const viewProps = result?.viewProps ?? {}\n const Element = tag\n const transformedProps = hooks.usePropsTransform?.(\n tag,\n viewProps,\n stateRef as any,\n false\n )\n\n return <Element {...transformedProps} ref={composedRefs} />\n })\n )\n ;(Component as any).acceptTagProp = true\n return Component\n}\n\nconst AnimatedView = createWebAnimatedComponent('div')\nconst AnimatedText = createWebAnimatedComponent('span')\n\n// =============================================================================\n// Animation Driver Factory\n// =============================================================================\n\n/**\n * Create a Reanimated-based animation driver for Tamagui.\n *\n * This is a native Reanimated implementation without Moti dependency.\n * It provides smooth spring and timing animations with full support for:\n * - Per-property animation configurations\n * - Exit animations with proper completion callbacks\n * - Dynamic theme value resolution\n * - Transform property animations\n * - avoidReRenders optimization for hover/press/focus states\n *\n * @example\n * ```tsx\n * const animations = createAnimations({\n * fast: { type: 'spring', damping: 20, stiffness: 250 },\n * slow: { type: 'timing', duration: 500 },\n * })\n * ```\n */\nexport function createAnimations<A extends Record<string, TransitionConfig>>(\n animationsConfig: A\n): AnimationDriver<A> {\n // Normalize animation configs - default to spring if not specified\n // This matches behavior of moti and motion drivers\n const animations = {} as A\n for (const key in animationsConfig) {\n animations[key] = {\n type: 'spring',\n ...animationsConfig[key],\n } as A[typeof key]\n }\n\n return {\n View: isWeb ? AnimatedView : Animated.View,\n Text: isWeb ? AnimatedText : Animated.Text,\n isReactNative: true,\n supportsCSS: false,\n avoidReRenders: true,\n animations,\n usePresence,\n ResetPresence,\n\n // =========================================================================\n // useAnimatedNumber - For imperative animated values\n // =========================================================================\n useAnimatedNumber(initial): UniversalAnimatedNumber<ReanimatedAnimatedNumber> {\n const sharedValue = useSharedValue(initial)\n\n return useMemo(\n () => ({\n getInstance() {\n 'worklet'\n return sharedValue\n },\n\n getValue() {\n 'worklet'\n return sharedValue.value\n },\n\n setValue(next, config = { type: 'spring' }, onFinish) {\n 'worklet'\n const handleFinish = onFinish\n ? () => {\n 'worklet'\n runOnJS(onFinish)()\n }\n : undefined\n\n if (config.type === 'direct') {\n sharedValue.value = next\n onFinish?.()\n } else if (config.type === 'spring') {\n sharedValue.value = withSpring(\n next,\n config as WithSpringConfig,\n handleFinish\n )\n } else {\n sharedValue.value = withTiming(\n next,\n config as WithTimingConfig,\n handleFinish\n )\n }\n },\n\n stop() {\n 'worklet'\n cancelAnimation(sharedValue)\n },\n }),\n [sharedValue]\n )\n },\n\n // =========================================================================\n // useAnimatedNumberReaction - React to animated value changes\n // =========================================================================\n useAnimatedNumberReaction({ value }, onValue) {\n const instance = value.getInstance()\n\n return useAnimatedReaction(\n () => instance.value,\n (next, prev) => {\n if (prev !== next) {\n runOnJS(onValue)(next)\n }\n },\n [onValue, instance]\n )\n },\n\n // =========================================================================\n // useAnimatedNumberStyle - Create animated styles from values\n // =========================================================================\n useAnimatedNumberStyle(val, getStyle) {\n const instance = val.getInstance()\n\n const derivedValue = useDerivedValue(() => instance.value, [instance, getStyle])\n\n return useAnimatedStyle(\n () => getStyle(derivedValue.value),\n [val, getStyle, derivedValue, instance]\n )\n },\n\n // =========================================================================\n // useAnimations - Main animation hook for components\n // =========================================================================\n useAnimations(animationProps) {\n const { props, presence, style, componentState, useStyleEmitter } = animationProps\n\n // Extract animation key\n const animationKey = Array.isArray(props.transition)\n ? props.transition[0]\n : props.transition\n\n // State flags\n const isHydrating = componentState.unmounted === true\n const isMounting = componentState.unmounted === 'should-enter'\n const disableAnimation = isHydrating || !animationKey\n\n // Theme state for dynamic values\n const [, themeState] = useThemeWithState({})\n const isDark = themeState?.scheme === 'dark' || themeState?.name?.startsWith('dark')\n\n // Presence state for exit animations\n const isExiting = presence?.[0] === false\n const sendExitComplete = presence?.[1]\n\n // =========================================================================\n // avoidRerenders: SharedValues for style updates without re-renders\n // =========================================================================\n const animatedTargetsRef = useSharedValue<Record<string, unknown> | null>(null)\n const staticTargetsRef = useSharedValue<Record<string, unknown> | null>(null)\n const transformTargetsRef = useSharedValue<Array<Record<string, unknown>> | null>(\n null\n )\n\n // Separate styles into animated and static\n const { animatedStyles, staticStyles } = useMemo(() => {\n const animated: Record<string, unknown> = {}\n const staticStyles: Record<string, unknown> = {}\n const animateOnly = props.animateOnly as string[] | undefined\n\n for (const key in style) {\n const rawValue = (style as Record<string, unknown>)[key]\n const value = resolveDynamicValue(rawValue, isDark)\n\n if (value === undefined) continue\n\n if (disableAnimation) {\n staticStyles[key] = value\n continue\n }\n\n if (canAnimateProperty(key, value, animateOnly)) {\n animated[key] = value\n } else {\n staticStyles[key] = value\n }\n }\n\n // During mount, include animated values in static to prevent flicker\n if (isMounting) {\n for (const key in animated) {\n staticStyles[key] = animated[key]\n }\n }\n\n return { animatedStyles: animated, staticStyles }\n }, [disableAnimation, style, isDark, isMounting, props.animateOnly])\n\n // Build animation config with per-property overrides using normalized transition\n const { baseConfig, propertyConfigs } = useMemo(() => {\n if (isHydrating) {\n return {\n baseConfig: { type: 'timing' as const, duration: 0 },\n propertyConfigs: {} as Record<string, TransitionConfig>,\n }\n }\n\n // Normalize the transition prop to a consistent format\n const normalized = normalizeTransition(props.transition)\n\n // Get base animation config from default animation key\n let base = normalized.default\n ? (animations[normalized.default as keyof typeof animations] ??\n ({ type: 'spring' } as TransitionConfig))\n : ({ type: 'spring' } as TransitionConfig)\n\n // Apply global delay to base config if present\n if (normalized.delay) {\n base = { ...base, delay: normalized.delay }\n }\n\n // Build per-property overrides from normalized properties\n const overrides: Record<string, TransitionConfig> = {}\n\n for (const [key, animationNameOrConfig] of Object.entries(\n normalized.properties\n )) {\n if (typeof animationNameOrConfig === 'string') {\n // Property override referencing a named animation: { x: 'quick' }\n overrides[key] =\n animations[animationNameOrConfig as keyof typeof animations] ?? base\n } else if (animationNameOrConfig && typeof animationNameOrConfig === 'object') {\n // Property override with inline config: { x: { type: 'quick', delay: 100 } }\n const configType = animationNameOrConfig.type\n const baseForProp = configType\n ? (animations[configType as keyof typeof animations] ?? base)\n : base\n // Cast to TransitionConfig since we're merging compatible animation configs\n overrides[key] = {\n ...baseForProp,\n ...animationNameOrConfig,\n } as TransitionConfig\n }\n }\n\n // Build per-property config map\n const configs: Record<string, TransitionConfig> = {}\n\n // Get all animated property keys including transform sub-properties\n const allKeys = new Set(Object.keys(animatedStyles))\n if (animatedStyles.transform && Array.isArray(animatedStyles.transform)) {\n for (const t of animatedStyles.transform as Record<string, unknown>[]) {\n allKeys.add(Object.keys(t)[0])\n }\n }\n\n for (const key of allKeys) {\n configs[key] = overrides[key] ?? base\n }\n\n return { baseConfig: base, propertyConfigs: configs }\n }, [isHydrating, props.transition, animatedStyles])\n\n // Store config in ref for worklet access\n const configRef = useRef({\n baseConfig,\n propertyConfigs,\n disableAnimation,\n isHydrating,\n })\n configRef.current = { baseConfig, propertyConfigs, disableAnimation, isHydrating }\n\n // =========================================================================\n // avoidRerenders: Register style emitter callback\n // When hover/press/etc state changes, this is called instead of re-rendering\n // =========================================================================\n useStyleEmitter?.((nextStyle: Record<string, unknown>) => {\n const animateOnly = props.animateOnly as string[] | undefined\n const animated: Record<string, unknown> = {}\n const statics: Record<string, unknown> = {}\n const transforms: Array<Record<string, unknown>> = []\n\n for (const key in nextStyle) {\n const rawValue = nextStyle[key]\n const value = resolveDynamicValue(rawValue, isDark)\n\n if (value === undefined) continue\n\n if (configRef.current.disableAnimation) {\n statics[key] = value\n continue\n }\n\n if (key === 'transform' && Array.isArray(value)) {\n for (const t of value as Record<string, unknown>[]) {\n if (t && typeof t === 'object') {\n const tKey = Object.keys(t)[0]\n const tVal = t[tKey]\n if (typeof tVal === 'number' || typeof tVal === 'string') {\n transforms.push(t)\n }\n }\n }\n continue\n }\n\n if (canAnimateProperty(key, value, animateOnly)) {\n animated[key] = value\n } else {\n statics[key] = value\n }\n }\n\n // Update SharedValues - this triggers worklet without React re-render\n animatedTargetsRef.value = animated\n staticTargetsRef.value = statics\n transformTargetsRef.value = transforms\n\n if (\n process.env.NODE_ENV === 'development' &&\n props.debug &&\n props.debug !== 'profile'\n ) {\n console.info('[animations-reanimated] useStyleEmitter update', {\n animated,\n statics,\n transforms,\n })\n }\n })\n\n // Handle exit animation completion\n // Use timeout based on calculated animation duration\n React.useEffect(() => {\n if (!isExiting || !sendExitComplete) return\n\n const duration = getAnimationDuration(baseConfig)\n const timeoutId = setTimeout(sendExitComplete, duration)\n\n return () => clearTimeout(timeoutId)\n }, [isExiting, sendExitComplete, baseConfig])\n\n // Create animated style\n const animatedStyle = useAnimatedStyle(() => {\n 'worklet'\n\n if (disableAnimation || isHydrating) {\n return {}\n }\n\n const result: Record<string, any> = {}\n const config = configRef.current\n\n // Check if we have avoidRerenders updates\n const hasEmitterUpdates = animatedTargetsRef.value !== null\n\n // Use emitter values if available, otherwise use React state values\n const animatedValues = hasEmitterUpdates\n ? animatedTargetsRef.value!\n : animatedStyles\n const staticValues = hasEmitterUpdates ? staticTargetsRef.value! : {}\n\n // Include static values from emitter (for hover/press style changes)\n for (const key in staticValues) {\n result[key] = staticValues[key]\n }\n\n // Animate regular properties\n for (const key in animatedValues) {\n if (key === 'transform') continue\n\n const targetValue = animatedValues[key]\n const propConfig = config.propertyConfigs[key] ?? config.baseConfig\n result[key] = applyAnimation(targetValue as number, propConfig)\n }\n\n // Handle transforms\n const transforms = hasEmitterUpdates\n ? transformTargetsRef.value\n : animatedStyles.transform\n\n // Animate transform properties with validation\n if (transforms && Array.isArray(transforms)) {\n const validTransforms = (transforms as Record<string, unknown>[])\n .filter((t) => {\n // Validate transform object has at least one key with a numeric value\n if (!t || typeof t !== 'object') return false\n const keys = Object.keys(t)\n if (keys.length === 0) return false\n const value = t[keys[0]]\n return typeof value === 'number' || typeof value === 'string'\n })\n .map((t) => {\n const transformKey = Object.keys(t)[0]\n const targetValue = t[transformKey]\n const propConfig = config.propertyConfigs[transformKey] ?? config.baseConfig\n return { [transformKey]: applyAnimation(targetValue as number, propConfig) }\n })\n\n if (validTransforms.length > 0) {\n result.transform = validTransforms\n }\n }\n\n return result\n }, [animatedStyles, baseConfig, propertyConfigs, disableAnimation, isHydrating])\n\n // Debug logging\n if (\n process.env.NODE_ENV === 'development' &&\n props.debug &&\n props.debug !== 'profile'\n ) {\n console.info('[animations-reanimated] useAnimations', {\n animationKey,\n componentState,\n isExiting,\n animatedStyles,\n staticStyles,\n baseConfig,\n propertyConfigs,\n })\n }\n\n return {\n style: [staticStyles, animatedStyle],\n }\n },\n }\n}\n"
8
+ "import { normalizeTransition } from '@tamagui/animation-helpers'\nimport {\n getSplitStyles,\n hooks,\n isWeb,\n Text,\n useComposedRefs,\n useThemeWithState,\n View,\n type AnimationDriver,\n type UniversalAnimatedNumber,\n} from '@tamagui/core'\nimport { ResetPresence, usePresence } from '@tamagui/use-presence'\nimport React, { forwardRef, useMemo, useRef } from 'react'\nimport type { SharedValue } from 'react-native-reanimated'\nimport Animated_, {\n cancelAnimation,\n runOnJS,\n useAnimatedReaction,\n useAnimatedStyle,\n useDerivedValue,\n useSharedValue,\n withDelay,\n withSpring,\n withTiming,\n type WithSpringConfig,\n type WithTimingConfig,\n} from 'react-native-reanimated'\n\n// =============================================================================\n// ESM/CJS compatibility\n// =============================================================================\n\n/**\n * Handle ESM/CJS module interop for react-native-reanimated\n * @see https://github.com/evanw/esbuild/issues/2480#issuecomment-1833104754\n */\nconst getDefaultExport = <T,>(module: T | { default: T }): T => {\n const mod = module as any\n if (mod.__esModule || mod[Symbol.toStringTag] === 'Module') {\n return mod.default || mod\n }\n return mod\n}\n\nconst Animated = getDefaultExport(Animated_)\n\n// =============================================================================\n// Types\n// =============================================================================\n\ntype ReanimatedAnimatedNumber = SharedValue<number>\n\n/** Spring animation configuration */\ntype SpringConfig = {\n type?: 'spring'\n delay?: number\n} & Partial<WithSpringConfig>\n\n/** Timing animation configuration */\ntype TimingConfig = {\n type: 'timing'\n delay?: number\n} & Partial<WithTimingConfig>\n\n/** Combined animation configuration type */\nexport type TransitionConfig = SpringConfig | TimingConfig\n\n// =============================================================================\n// Utility Functions\n// =============================================================================\n\n/**\n * Resolve dynamic theme values (e.g., `{dynamic: {dark: \"value\", light: \"value\"}}`)\n */\nconst resolveDynamicValue = (value: unknown, isDark: boolean): unknown => {\n if (\n value !== null &&\n typeof value === 'object' &&\n 'dynamic' in value &&\n typeof (value as any).dynamic === 'object'\n ) {\n const dynamic = (value as any).dynamic\n return isDark ? dynamic.dark : dynamic.light\n }\n return value\n}\n\n/**\n * Apply animation to a value based on config\n */\nconst applyAnimation = (\n targetValue: number | string,\n config: TransitionConfig\n): number | string => {\n 'worklet'\n const delay = config.delay\n\n let animatedValue: any\n if (config.type === 'timing') {\n animatedValue = withTiming(targetValue as number, config as WithTimingConfig)\n } else {\n animatedValue = withSpring(targetValue as number, config as WithSpringConfig)\n }\n\n if (delay && delay > 0) {\n animatedValue = withDelay(delay, animatedValue)\n }\n\n return animatedValue\n}\n\n// =============================================================================\n// Animatable Properties\n// =============================================================================\n\nconst ANIMATABLE_PROPERTIES: Record<string, boolean> = {\n // Transform\n transform: true,\n // Opacity\n opacity: true,\n // Dimensions\n height: true,\n width: true,\n minWidth: true,\n minHeight: true,\n maxWidth: true,\n maxHeight: true,\n // Background\n backgroundColor: true,\n // Border colors\n borderColor: true,\n borderLeftColor: true,\n borderRightColor: true,\n borderTopColor: true,\n borderBottomColor: true,\n // Border radius\n borderRadius: true,\n borderTopLeftRadius: true,\n borderTopRightRadius: true,\n borderBottomLeftRadius: true,\n borderBottomRightRadius: true,\n // Border width\n borderWidth: true,\n borderLeftWidth: true,\n borderRightWidth: true,\n borderTopWidth: true,\n borderBottomWidth: true,\n // Text\n color: true,\n fontSize: true,\n fontWeight: true,\n lineHeight: true,\n letterSpacing: true,\n // Position\n left: true,\n right: true,\n top: true,\n bottom: true,\n // Margin\n margin: true,\n marginTop: true,\n marginBottom: true,\n marginLeft: true,\n marginRight: true,\n marginHorizontal: true,\n marginVertical: true,\n // Padding\n padding: true,\n paddingTop: true,\n paddingBottom: true,\n paddingLeft: true,\n paddingRight: true,\n paddingHorizontal: true,\n paddingVertical: true,\n // Flex/Gap\n gap: true,\n rowGap: true,\n columnGap: true,\n flex: true,\n flexGrow: true,\n flexShrink: true,\n}\n\n/**\n * Check if a style property can be animated\n */\nconst canAnimateProperty = (\n key: string,\n value: unknown,\n animateOnly?: string[]\n): boolean => {\n if (!ANIMATABLE_PROPERTIES[key]) return false\n if (value === 'auto') return false\n if (typeof value === 'string' && value.startsWith('calc')) return false\n if (animateOnly && !animateOnly.includes(key)) return false\n return true\n}\n\n// =============================================================================\n// Animated Components (Web)\n// =============================================================================\n\n/**\n * Create a Tamagui-compatible animated component for web\n * Supports data- attributes, className, and proper style handling\n */\nfunction createWebAnimatedComponent(defaultTag: 'div' | 'span') {\n const isText = defaultTag === 'span'\n\n const Component = Animated.createAnimatedComponent(\n forwardRef((propsIn: any, ref) => {\n const { forwardedRef, render = defaultTag, ...rest } = propsIn\n const hostRef = useRef<HTMLElement>(null)\n const composedRefs = useComposedRefs(forwardedRef, ref, hostRef)\n\n const stateRef = useRef<{ host: HTMLElement | null }>({\n get host() {\n return hostRef.current\n },\n })\n\n const [, themeState] = useThemeWithState({})\n\n const result = getSplitStyles(\n rest,\n isText ? Text.staticConfig : View.staticConfig,\n themeState?.theme ?? {},\n themeState?.name ?? '',\n { unmounted: false } as any,\n { isAnimated: false, noClass: true }\n )\n\n const viewProps = result?.viewProps ?? {}\n const Element = render\n const transformedProps = hooks.usePropsTransform?.(\n render,\n viewProps,\n stateRef as any,\n false\n )\n\n return <Element {...transformedProps} ref={composedRefs} />\n })\n )\n ;(Component as any).acceptTagProp = true\n return Component\n}\n\nconst AnimatedView = createWebAnimatedComponent('div')\nconst AnimatedText = createWebAnimatedComponent('span')\n\n// =============================================================================\n// Animation Driver Factory\n// =============================================================================\n\n/**\n * Create a Reanimated-based animation driver for Tamagui.\n *\n * This is a native Reanimated implementation without Moti dependency.\n * It provides smooth spring and timing animations with full support for:\n * - Per-property animation configurations\n * - Exit animations with proper completion callbacks\n * - Dynamic theme value resolution\n * - Transform property animations\n * - avoidReRenders optimization for hover/press/focus states\n *\n * @example\n * ```tsx\n * const animations = createAnimations({\n * fast: { type: 'spring', damping: 20, stiffness: 250 },\n * slow: { type: 'timing', duration: 500 },\n * })\n * ```\n */\nexport function createAnimations<A extends Record<string, TransitionConfig>>(\n animationsConfig: A\n): AnimationDriver<A> {\n // Normalize animation configs - default to spring if not specified\n // This matches behavior of moti and motion drivers\n const animations = {} as A\n for (const key in animationsConfig) {\n animations[key] = {\n type: 'spring',\n ...animationsConfig[key],\n } as A[typeof key]\n }\n\n return {\n View: isWeb ? AnimatedView : Animated.View,\n Text: isWeb ? AnimatedText : Animated.Text,\n isReactNative: true,\n supportsCSS: false,\n avoidReRenders: true,\n animations,\n usePresence,\n ResetPresence,\n\n // =========================================================================\n // useAnimatedNumber - For imperative animated values\n // =========================================================================\n useAnimatedNumber(initial): UniversalAnimatedNumber<ReanimatedAnimatedNumber> {\n const sharedValue = useSharedValue(initial)\n\n return useMemo(\n () => ({\n getInstance() {\n 'worklet'\n return sharedValue\n },\n\n getValue() {\n 'worklet'\n return sharedValue.value\n },\n\n setValue(next, config = { type: 'spring' }, onFinish) {\n 'worklet'\n const handleFinish = onFinish\n ? () => {\n 'worklet'\n runOnJS(onFinish)()\n }\n : undefined\n\n if (config.type === 'direct') {\n sharedValue.value = next\n onFinish?.()\n } else if (config.type === 'spring') {\n sharedValue.value = withSpring(\n next,\n config as WithSpringConfig,\n handleFinish\n )\n } else {\n sharedValue.value = withTiming(\n next,\n config as WithTimingConfig,\n handleFinish\n )\n }\n },\n\n stop() {\n 'worklet'\n cancelAnimation(sharedValue)\n },\n }),\n [sharedValue]\n )\n },\n\n // =========================================================================\n // useAnimatedNumberReaction - React to animated value changes\n // =========================================================================\n useAnimatedNumberReaction({ value }, onValue) {\n const instance = value.getInstance()\n\n return useAnimatedReaction(\n () => instance.value,\n (next, prev) => {\n if (prev !== next) {\n runOnJS(onValue)(next)\n }\n },\n [onValue, instance]\n )\n },\n\n // =========================================================================\n // useAnimatedNumberStyle - Create animated styles from values\n // =========================================================================\n useAnimatedNumberStyle(val, getStyle) {\n const instance = val.getInstance()\n\n const derivedValue = useDerivedValue(() => instance.value, [instance, getStyle])\n\n return useAnimatedStyle(\n () => getStyle(derivedValue.value),\n [val, getStyle, derivedValue, instance]\n )\n },\n\n // =========================================================================\n // useAnimations - Main animation hook for components\n // =========================================================================\n useAnimations(animationProps) {\n const { props, presence, style, componentState, useStyleEmitter, themeName } =\n animationProps\n\n // Extract animation key from normalized transition\n // props.transition can be: string | [string, config] | { default: string, ... }\n const normalized = normalizeTransition(props.transition)\n const animationKey = normalized.default\n\n // State flags\n const isHydrating = componentState.unmounted === true\n const isMounting = componentState.unmounted === 'should-enter'\n const disableAnimation = isHydrating || !animationKey\n\n // Theme state for dynamic values - use themeName from props instead of hook\n const isDark = themeName?.startsWith('dark') || false\n\n // Presence state for exit animations\n const isExiting = presence?.[0] === false\n const sendExitComplete = presence?.[1]\n\n // Track exit animation progress (0 = not started, 1 = complete)\n const exitProgress = useSharedValue(0)\n\n // =========================================================================\n // avoidRerenders: SharedValues for style updates without re-renders\n // =========================================================================\n const animatedTargetsRef = useSharedValue<Record<string, unknown> | null>(null)\n const staticTargetsRef = useSharedValue<Record<string, unknown> | null>(null)\n const transformTargetsRef = useSharedValue<Array<Record<string, unknown>> | null>(\n null\n )\n\n // Separate styles into animated and static\n const { animatedStyles, staticStyles } = useMemo(() => {\n const animated: Record<string, unknown> = {}\n const staticStyles: Record<string, unknown> = {}\n const animateOnly = props.animateOnly as string[] | undefined\n\n for (const key in style) {\n const rawValue = (style as Record<string, unknown>)[key]\n const value = resolveDynamicValue(rawValue, isDark)\n\n if (value === undefined) continue\n\n if (disableAnimation) {\n staticStyles[key] = value\n continue\n }\n\n if (canAnimateProperty(key, value, animateOnly)) {\n animated[key] = value\n } else {\n staticStyles[key] = value\n }\n }\n\n // During mount, include animated values in static to prevent flicker\n if (isMounting) {\n for (const key in animated) {\n staticStyles[key] = animated[key]\n }\n }\n\n return { animatedStyles: animated, staticStyles }\n }, [disableAnimation, style, isDark, isMounting, props.animateOnly])\n\n // Build animation config with per-property overrides using normalized transition\n const { baseConfig, propertyConfigs } = useMemo(() => {\n if (isHydrating) {\n return {\n baseConfig: { type: 'timing' as const, duration: 0 },\n propertyConfigs: {} as Record<string, TransitionConfig>,\n }\n }\n\n // Normalize the transition prop to a consistent format\n const normalized = normalizeTransition(props.transition)\n\n // Get base animation config from default animation key\n let base = normalized.default\n ? (animations[normalized.default as keyof typeof animations] ??\n ({ type: 'spring' } as TransitionConfig))\n : ({ type: 'spring' } as TransitionConfig)\n\n // Apply global delay to base config if present\n if (normalized.delay) {\n base = { ...base, delay: normalized.delay }\n }\n\n // Build per-property overrides from normalized properties\n const overrides: Record<string, TransitionConfig> = {}\n\n for (const key in normalized.properties) {\n const animationNameOrConfig = normalized.properties[key]\n if (typeof animationNameOrConfig === 'string') {\n // Property override referencing a named animation: { x: 'quick' }\n overrides[key] =\n animations[animationNameOrConfig as keyof typeof animations] ?? base\n } else if (animationNameOrConfig && typeof animationNameOrConfig === 'object') {\n // Property override with inline config: { x: { type: 'quick', delay: 100 } }\n const configType = (animationNameOrConfig as any).type\n const baseForProp = configType\n ? (animations[configType as keyof typeof animations] ?? base)\n : base\n // Cast to TransitionConfig since we're merging compatible animation configs\n overrides[key] = {\n ...baseForProp,\n ...animationNameOrConfig,\n } as TransitionConfig\n }\n }\n\n // Build per-property config map\n const configs: Record<string, TransitionConfig> = {}\n\n // Get all animated property keys including transform sub-properties\n const allKeys = new Set(Object.keys(animatedStyles))\n if (animatedStyles.transform && Array.isArray(animatedStyles.transform)) {\n for (const t of animatedStyles.transform as Record<string, unknown>[]) {\n allKeys.add(Object.keys(t)[0])\n }\n }\n\n for (const key of allKeys) {\n configs[key] = overrides[key] ?? base\n }\n\n return { baseConfig: base, propertyConfigs: configs }\n }, [isHydrating, props.transition, animatedStyles])\n\n // Store config in SharedValue for worklet access (concurrent-safe)\n // Using useEffect to avoid writing to shared value during render\n const configRef = useSharedValue({\n baseConfig,\n propertyConfigs,\n disableAnimation,\n isHydrating,\n })\n\n React.useEffect(() => {\n configRef.set({ baseConfig, propertyConfigs, disableAnimation, isHydrating })\n }, [baseConfig, propertyConfigs, disableAnimation, isHydrating])\n\n // =========================================================================\n // avoidRerenders: Register style emitter callback\n // When hover/press/etc state changes, this is called instead of re-rendering\n // =========================================================================\n useStyleEmitter?.((nextStyle: Record<string, unknown>) => {\n const animateOnly = props.animateOnly as string[] | undefined\n const animated: Record<string, unknown> = {}\n const statics: Record<string, unknown> = {}\n const transforms: Array<Record<string, unknown>> = []\n\n for (const key in nextStyle) {\n const rawValue = nextStyle[key]\n const value = resolveDynamicValue(rawValue, isDark)\n\n if (value == undefined) continue\n\n if (configRef.get().disableAnimation) {\n statics[key] = value\n continue\n }\n\n if (key === 'transform' && Array.isArray(value)) {\n for (const t of value as Record<string, unknown>[]) {\n if (t && typeof t === 'object') {\n const tKey = Object.keys(t)[0]\n const tVal = t[tKey]\n if (typeof tVal === 'number' || typeof tVal === 'string') {\n transforms.push(t)\n }\n }\n }\n continue\n }\n\n if (canAnimateProperty(key, value, animateOnly)) {\n animated[key] = value\n } else {\n statics[key] = value\n }\n }\n\n // Update SharedValues - this triggers worklet without React re-render\n // Using .set() method for concurrent-safe updates\n animatedTargetsRef.set(animated)\n staticTargetsRef.set(statics)\n transformTargetsRef.set(transforms)\n\n if (\n process.env.NODE_ENV === 'development' &&\n props.debug &&\n props.debug !== 'profile'\n ) {\n console.info('[animations-reanimated] useStyleEmitter update', {\n animated,\n statics,\n transforms,\n })\n }\n })\n\n // Store baseConfig in a ref so the exit effect doesn't re-run when config changes\n const baseConfigRef = useRef(baseConfig)\n baseConfigRef.current = baseConfig\n\n // Handle exit animation completion using reanimated's native callback\n // Animate exitProgress from 0 to 1 during exit, call sendExitComplete on completion\n React.useEffect(() => {\n if (!isExiting || !sendExitComplete) return\n\n // Use ref to get current config without adding to deps\n const config = baseConfigRef.current\n\n // Animate exitProgress to 1, which triggers sendExitComplete on completion\n // Using .set() for React Compiler compatibility\n if (config.type === 'timing') {\n exitProgress.set(\n withTiming(1, config as WithTimingConfig, (finished) => {\n 'worklet'\n if (finished) {\n runOnJS(sendExitComplete)()\n }\n })\n )\n } else {\n exitProgress.set(\n withSpring(1, config as WithSpringConfig, (finished) => {\n 'worklet'\n if (finished) {\n runOnJS(sendExitComplete)()\n }\n })\n )\n }\n\n return () => {\n // Cancel the exit animation if component unmounts early\n cancelAnimation(exitProgress)\n }\n }, [isExiting, sendExitComplete])\n\n // Create animated style\n const animatedStyle = useAnimatedStyle(() => {\n 'worklet'\n\n if (disableAnimation || isHydrating) {\n return {}\n }\n\n const result: Record<string, any> = {}\n const config = configRef.get()\n\n // Check if we have avoidRerenders updates\n // Using .get() method for concurrent-safe reads in worklets\n const emitterAnimated = animatedTargetsRef.get()\n const hasEmitterUpdates = emitterAnimated !== null\n\n // Use emitter values if available, otherwise use React state values\n const animatedValues = hasEmitterUpdates ? emitterAnimated! : animatedStyles\n const staticValues = hasEmitterUpdates ? staticTargetsRef.get()! : {}\n\n // Include static values from emitter (for hover/press style changes)\n for (const key in staticValues) {\n result[key] = staticValues[key]\n }\n\n // Animate regular properties\n for (const key in animatedValues) {\n if (key === 'transform') continue\n\n const targetValue = animatedValues[key]\n const propConfig = config.propertyConfigs[key] ?? config.baseConfig\n result[key] = applyAnimation(targetValue as number, propConfig)\n }\n\n // Handle transforms\n const transforms = hasEmitterUpdates\n ? transformTargetsRef.get()\n : animatedStyles.transform\n\n // Animate transform properties with validation\n if (transforms && Array.isArray(transforms)) {\n const validTransforms: Record<string, unknown>[] = []\n\n for (const t of transforms) {\n if (!t) continue\n const keys = Object.keys(t)\n if (keys.length === 0) continue\n const value = t[keys[0]]\n if (typeof value === 'number' || typeof value === 'string') {\n const transformKey = Object.keys(t)[0]\n const targetValue = t[transformKey]\n const propConfig = config.propertyConfigs[transformKey] ?? config.baseConfig\n validTransforms.push({\n [transformKey]: applyAnimation(targetValue as number, propConfig),\n })\n }\n }\n\n if (validTransforms.length > 0) {\n result.transform = validTransforms\n }\n }\n\n return result\n }, [animatedStyles, baseConfig, propertyConfigs, disableAnimation, isHydrating])\n\n // Debug logging\n if (\n process.env.NODE_ENV === 'development' &&\n props.debug &&\n props.debug !== 'profile'\n ) {\n console.info('[animations-reanimated] useAnimations', {\n animationKey,\n componentState,\n isExiting,\n animatedStyles,\n staticStyles,\n baseConfig,\n propertyConfigs,\n })\n }\n\n return {\n style: [staticStyles, animatedStyle],\n }\n },\n }\n}\n"
9
9
  ],
10
10
  "version": 3
11
11
  }