@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.
- package/dist/cjs/createAnimations.cjs +67 -64
- package/dist/cjs/createAnimations.js +46 -39
- package/dist/cjs/createAnimations.js.map +2 -2
- package/dist/cjs/createAnimations.native.js +107 -115
- package/dist/cjs/createAnimations.native.js.map +1 -1
- package/dist/esm/createAnimations.js +47 -39
- package/dist/esm/createAnimations.js.map +2 -2
- package/dist/esm/createAnimations.mjs +67 -64
- package/dist/esm/createAnimations.mjs.map +1 -1
- package/dist/esm/createAnimations.native.js +107 -115
- package/dist/esm/createAnimations.native.js.map +1 -1
- package/package.json +5 -5
- package/src/createAnimations.tsx +83 -108
- package/types/createAnimations.d.ts.map +2 -2
package/src/createAnimations.tsx
CHANGED
|
@@ -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,
|
|
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 =
|
|
236
|
+
const Element = render
|
|
295
237
|
const transformedProps = hooks.usePropsTransform?.(
|
|
296
|
-
|
|
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 } =
|
|
389
|
+
const { props, presence, style, componentState, useStyleEmitter, themeName } =
|
|
390
|
+
animationProps
|
|
448
391
|
|
|
449
|
-
// Extract animation key
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
|
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
|
|
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
|
|
576
|
-
|
|
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
|
-
|
|
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
|
|
546
|
+
if (value == undefined) continue
|
|
599
547
|
|
|
600
|
-
if (configRef.
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
649
|
-
const
|
|
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 () =>
|
|
652
|
-
|
|
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.
|
|
637
|
+
const config = configRef.get()
|
|
664
638
|
|
|
665
639
|
// Check if we have avoidRerenders updates
|
|
666
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
-
|
|
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": "
|
|
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
|
}
|