@tamagui/animations-reanimated 2.0.0-1768696252732 → 2.0.0-1768700139740
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tamagui/animations-reanimated",
|
|
3
|
-
"version": "2.0.0-
|
|
3
|
+
"version": "2.0.0-1768700139740",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"source": "src/index.ts",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,12 +28,12 @@
|
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@tamagui/animation-helpers": "2.0.0-
|
|
32
|
-
"@tamagui/core": "2.0.0-
|
|
33
|
-
"@tamagui/use-presence": "2.0.0-
|
|
31
|
+
"@tamagui/animation-helpers": "2.0.0-1768700139740",
|
|
32
|
+
"@tamagui/core": "2.0.0-1768700139740",
|
|
33
|
+
"@tamagui/use-presence": "2.0.0-1768700139740"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@tamagui/build": "2.0.0-
|
|
36
|
+
"@tamagui/build": "2.0.0-1768700139740",
|
|
37
37
|
"react": "*",
|
|
38
38
|
"react-native": "0.81.5",
|
|
39
39
|
"react-native-reanimated": "~4.1.1"
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"mappings": "AACA,
|
|
2
|
+
"mappings": "AACA,cASO,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 {\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"
|
|
8
|
+
"import { normalizeTransition } from '@tamagui/animation-helpers'\nimport {\n getSplitStyles,\n hooks,\n isWeb,\n Text,\n useComposedRefs,\n useIsomorphicLayoutEffect,\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 useIsomorphicLayoutEffect(() => {\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 // 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 = configRef.get().baseConfig\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
|
}
|