@hanzogui/animations-reanimated 3.0.6 → 4.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/createAnimations.cjs +484 -334
- package/dist/cjs/createAnimations.native.js +568 -409
- package/dist/cjs/createAnimations.native.js.map +1 -1
- package/dist/cjs/index.cjs +7 -5
- package/dist/cjs/index.native.js +7 -5
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/esm/createAnimations.mjs +456 -308
- package/dist/esm/createAnimations.mjs.map +1 -1
- package/dist/esm/createAnimations.native.js +540 -383
- package/dist/esm/createAnimations.native.js.map +1 -1
- package/package.json +1 -1
- package/src/createAnimations.tsx +172 -124
- package/types/createAnimations.d.ts.map +2 -2
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"mappings": "AACA,
|
|
2
|
+
"mappings": "AACA,cAUO,uBAEA;AAIP,cAUO,uBACA,wBACA;;KA2BF,eAAe;CAClB,OAAO;CACP;IACE,QAAQ;;KAGP,eAAe;CAClB,MAAM;CACN;IACE,QAAQ;;AAGZ,YAAY,mBAAmB,eAAe;;AAG9C,YAAY,0BAA0B;;;;;;;;;;;;;;;;;;;;;AAkTtC,OAAO,iBAAS,iBAAiB,UAAU,eAAe,mBACxD,kBAAkB,IACjB,gBAAgB",
|
|
3
3
|
"names": [],
|
|
4
4
|
"sources": [
|
|
5
5
|
"src/createAnimations.tsx"
|
|
6
6
|
],
|
|
7
7
|
"version": 3,
|
|
8
8
|
"sourcesContent": [
|
|
9
|
-
"import { normalizeTransition, getEffectiveAnimation } from '@hanzogui/animation-helpers'\nimport {\n getSplitStyles,\n hooks,\n isWeb,\n type PseudoTransitions,\n Text,\n useComposedRefs,\n useEvent,\n useIsomorphicLayoutEffect,\n useThemeWithState,\n View,\n type AnimationDriver,\n type UniversalAnimatedNumber,\n} from '@hanzogui/core'\nimport { ResetPresence, usePresence } from '@hanzogui/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/** Options for createAnimations (reserved for future use) */\nexport type CreateAnimationsOptions = {}\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/** Animation completion callback type */\ntype AnimationCallback = (finished?: boolean) => void\n\n/**\n * Apply animation to a value based on config, with optional completion callback\n */\nconst applyAnimation = (\n targetValue: number | string,\n config: TransitionConfig,\n callback?: AnimationCallback\n): number | string => {\n 'worklet'\n const delay = config.delay\n\n let animatedValue: any\n if (config.type === 'timing') {\n animatedValue = withTiming(\n targetValue as number,\n config as WithTimingConfig,\n callback\n )\n } else {\n animatedValue = withSpring(\n targetValue as number,\n config as WithSpringConfig,\n callback\n )\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 Gui-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).acceptRenderProp = true\n return Component\n}\n\nconst AnimatedView = createWebAnimatedComponent('div')\nconst AnimatedText = createWebAnimatedComponent('span')\n\n// =============================================================================\n// Transition Config Builder\n// =============================================================================\n\ntype TransitionConfigResult = {\n baseConfig: TransitionConfig\n propertyConfigs: Record<string, TransitionConfig>\n}\n\n/**\n * Builds animation config from a transition prop.\n * Shared logic used in both initial render and style emitter updates.\n */\nfunction buildTransitionConfig<A extends Record<string, TransitionConfig>>(\n transition: any,\n animations: A,\n animationState: 'enter' | 'exit' | 'default',\n styleKeys: Set<string>\n): TransitionConfigResult {\n const normalized = normalizeTransition(transition)\n const effectiveKey = getEffectiveAnimation(normalized, animationState)\n\n let base = effectiveKey\n ? (animations[effectiveKey as keyof typeof animations] ??\n ({ type: 'spring' } as TransitionConfig))\n : ({ type: 'spring' } as TransitionConfig)\n\n if (normalized.delay) {\n base = { ...base, delay: normalized.delay }\n }\n\n if (normalized.config) {\n base = { ...base, ...normalized.config }\n // infer type: 'timing' if duration is provided without spring params\n if (\n base.type !== 'timing' &&\n normalized.config.duration !== undefined &&\n normalized.config.damping === undefined &&\n normalized.config.stiffness === undefined &&\n normalized.config.mass === undefined\n ) {\n base = { ...base, type: 'timing' }\n }\n }\n\n // build per-property configs\n const propertyConfigs: Record<string, TransitionConfig> = {}\n\n for (const key of styleKeys) {\n const propAnimation = normalized.properties[key]\n if (typeof propAnimation === 'string') {\n propertyConfigs[key] = animations[propAnimation as keyof typeof animations] ?? base\n } else if (propAnimation && typeof propAnimation === 'object') {\n const configType = (propAnimation as any).type\n const baseForProp = configType\n ? (animations[configType as keyof typeof animations] ?? base)\n : base\n propertyConfigs[key] = {\n ...baseForProp,\n ...propAnimation,\n } as TransitionConfig\n } else {\n propertyConfigs[key] = base\n }\n }\n\n return { baseConfig: base, propertyConfigs }\n}\n\n/**\n * Extracts all style keys including transform sub-properties.\n */\nfunction getStyleKeys(style: Record<string, unknown>): Set<string> {\n const keys = new Set(Object.keys(style))\n if (style.transform && Array.isArray(style.transform)) {\n for (const t of style.transform as Record<string, unknown>[]) {\n if (t && typeof t === 'object') {\n keys.add(Object.keys(t)[0])\n }\n }\n }\n return keys\n}\n\n// =============================================================================\n// Animation Driver Factory\n// =============================================================================\n\n/**\n * Create a Reanimated-based animation driver for Gui.\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 * ```\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 needsCustomComponent: true,\n View: isWeb ? AnimatedView : Animated.View,\n Text: isWeb ? AnimatedText : Animated.Text,\n isReactNative: true,\n inputStyle: 'value',\n outputStyle: 'inline',\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 {\n props,\n presence,\n style,\n componentState,\n useStyleEmitter,\n themeName,\n stateRef,\n styleState,\n } = animationProps\n\n // State flags\n const isHydrating = componentState.unmounted === true\n const isMounting = componentState.unmounted === 'should-enter'\n const isEntering = !!componentState.unmounted\n\n // Presence state for exit animations\n const isExiting = presence?.[0] === false\n\n // Track if we just finished entering (transition from entering to not entering)\n const wasEnteringRef = useRef(isEntering)\n const justFinishedEntering = wasEnteringRef.current && !isEntering\n React.useEffect(() => {\n wasEnteringRef.current = isEntering\n })\n\n // Use effectiveTransition computed by createComponent (single source of truth)\n const effectiveTransition = styleState?.effectiveTransition ?? props.transition\n const normalized = normalizeTransition(effectiveTransition)\n // Use 'enter' if we're mounting OR if we just finished entering\n const animationState: 'enter' | 'exit' | 'default' = isExiting\n ? 'exit'\n : isMounting || justFinishedEntering\n ? 'enter'\n : 'default'\n const animationKey = getEffectiveAnimation(normalized, animationState)\n\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 // Get sendExitComplete callback from presence\n const sendExitComplete = presence?.[1]\n\n // =========================================================================\n // Exit cycle state for deterministic per-property completion tracking\n // =========================================================================\n const exitCycleIdRef = useRef(0)\n const pendingExitKeysRef = useRef<Set<string>>(new Set())\n const exitCompletedRef = useRef(false)\n const wasExitingRef = useRef(false)\n\n // detect transition into/out of exiting state\n const justStartedExiting = isExiting && !wasExitingRef.current\n const justStoppedExiting = !isExiting && wasExitingRef.current\n\n // stable callback to mark a property as done (called from worklet via runOnJS)\n const markExitKeyDone = useEvent(\n (key: string, cycleId: number, finished: boolean) => {\n // ignore callbacks from stale cycles\n if (cycleId !== exitCycleIdRef.current) return\n // ignore if already completed\n if (exitCompletedRef.current) return\n // count both finished and canceled animations as \"done\" during exit\n // (element is leaving anyway, canceled animations shouldn't block completion)\n\n pendingExitKeysRef.current.delete(key)\n\n // check if all exit animations are done\n if (pendingExitKeysRef.current.size === 0) {\n exitCompletedRef.current = true\n sendExitComplete?.()\n }\n }\n )\n\n // SharedValue to pass exit state into worklet\n const isExitingRef = useSharedValue(isExiting)\n const exitCycleIdShared = useSharedValue(exitCycleIdRef.current)\n\n // start new exit cycle only on transition INTO exiting (not every render while exiting)\n if (justStartedExiting) {\n exitCycleIdRef.current++\n exitCompletedRef.current = false\n pendingExitKeysRef.current.clear()\n }\n // invalidate pending callbacks when exit is canceled/interrupted\n if (justStoppedExiting) {\n exitCycleIdRef.current++\n pendingExitKeysRef.current.clear()\n }\n\n // update shared values (using effect to avoid writing during render)\n useIsomorphicLayoutEffect(() => {\n isExitingRef.value = isExiting\n exitCycleIdShared.value = exitCycleIdRef.current\n }, [isExiting, exitCycleIdRef.current])\n\n // track previous exiting state\n React.useEffect(() => {\n wasExitingRef.current = isExiting\n })\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 return buildTransitionConfig(\n props.transition,\n animations,\n animationState,\n getStyleKeys(animatedStyles)\n )\n }, [isHydrating, props.transition, animatedStyles, animationState])\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>, effectiveTransition) => {\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 // effectiveTransition is computed in createComponent based on entering/exiting pseudo states\n // rebuild config whenever transition changes (entering OR exiting pseudo states)\n const transitionToUse = effectiveTransition ?? props.transition\n const { baseConfig: newBase, propertyConfigs: newPropertyConfigs } =\n buildTransitionConfig(\n transitionToUse,\n animations,\n animationState,\n getStyleKeys(nextStyle)\n )\n\n // update configRef with the new config\n configRef.set({\n baseConfig: newBase,\n propertyConfigs: newPropertyConfigs,\n disableAnimation: configRef.get().disableAnimation,\n isHydrating: configRef.get().isHydrating,\n })\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 - on web, the mapper watches these if passed in dependencies\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 // Compute and register exit keys synchronously during render to avoid race conditions\n // This must happen BEFORE useAnimatedStyle runs so callbacks have a populated set\n const exitKeysRegistered = useRef(false)\n if (justStartedExiting && sendExitComplete) {\n const exitKeys: string[] = []\n const animateOnly = props.animateOnly as string[] | undefined\n\n // regular animated properties\n for (const key in animatedStyles) {\n if (key === 'transform') continue\n if (canAnimateProperty(key, animatedStyles[key], animateOnly)) {\n exitKeys.push(key)\n }\n }\n\n // transform sub-keys (filter by animateOnly if specified)\n const transforms = animatedStyles.transform\n if (transforms && Array.isArray(transforms)) {\n for (const t of transforms) {\n if (!t) continue\n const tKey = Object.keys(t)[0]\n if (tKey) {\n // check animateOnly filter for transform sub-keys\n if (animateOnly && !animateOnly.includes(tKey)) {\n continue\n }\n exitKeys.push(`transform:${tKey}`)\n }\n }\n }\n\n // register keys for this cycle\n pendingExitKeysRef.current = new Set(exitKeys)\n exitKeysRegistered.current = exitKeys.length > 0\n }\n\n // handle zero-animation case in effect (after render commit)\n React.useEffect(() => {\n if (!justStartedExiting || !sendExitComplete) return\n\n // if no keys were registered, complete immediately\n if (!exitKeysRegistered.current && pendingExitKeysRef.current.size === 0) {\n if (!exitCompletedRef.current) {\n exitCompletedRef.current = true\n sendExitComplete()\n }\n }\n }, [justStartedExiting, 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 from useStyleEmitter\n const emitterAnimated = animatedTargetsRef.value\n const emitterStatic = staticTargetsRef.value\n const emitterTransforms = transformTargetsRef.value\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 ? emitterStatic! : {}\n\n // read exit state from shared values\n const currentlyExiting = isExitingRef.value\n const currentCycleId = exitCycleIdShared.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\n // create callback for exit tracking\n let callback: AnimationCallback | undefined\n if (currentlyExiting) {\n const capturedKey = key\n const capturedCycleId = currentCycleId\n callback = (finished) => {\n 'worklet'\n runOnJS(markExitKeyDone)(capturedKey, capturedCycleId, finished ?? false)\n }\n }\n\n result[key] = applyAnimation(targetValue as number, propConfig, callback)\n }\n\n // Handle transforms\n const transforms = hasEmitterUpdates\n ? emitterTransforms\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\n // create callback for exit tracking (transform sub-key)\n let callback: AnimationCallback | undefined\n if (currentlyExiting) {\n const capturedKey = `transform:${transformKey}`\n const capturedCycleId = currentCycleId\n callback = (finished) => {\n 'worklet'\n runOnJS(markExitKeyDone)(\n capturedKey,\n capturedCycleId,\n finished ?? false\n )\n }\n }\n\n validTransforms.push({\n [transformKey]: applyAnimation(\n targetValue as number,\n propConfig,\n callback\n ),\n })\n }\n }\n\n if (validTransforms.length > 0) {\n result.transform = validTransforms\n }\n }\n\n return result\n }, [\n animatedStyles,\n baseConfig,\n propertyConfigs,\n disableAnimation,\n isHydrating,\n // Pass SharedValues so the mapper watches them on web (see useAnimatedStyle.ts line 470-472)\n animatedTargetsRef,\n staticTargetsRef,\n transformTargetsRef,\n isExitingRef,\n exitCycleIdShared,\n markExitKeyDone,\n ])\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
|
+
"import { getEffectiveAnimation, normalizeTransition } from '@hanzogui/animation-helpers'\nimport {\n getSplitStyles,\n hooks,\n isWeb,\n Text,\n useComposedRefs,\n useEvent,\n useIsomorphicLayoutEffect,\n useThemeWithState,\n View,\n type AnimationDriver,\n type UniversalAnimatedNumber,\n} from '@hanzogui/core'\nimport { ResetPresence, usePresence } from '@hanzogui/use-presence'\nimport React, { forwardRef, useMemo, useRef } from 'react'\nimport type { SharedValue } from 'react-native-reanimated'\nimport Animated_, {\n cancelAnimation,\n runOnJS,\n runOnUI,\n useAnimatedReaction,\n useAnimatedStyle,\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/** Options for createAnimations (reserved for future use) */\nexport type CreateAnimationsOptions = {}\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/** Animation completion callback type */\ntype AnimationCallback = (finished?: boolean) => void\n\n/**\n * Apply animation to a value based on config, with optional completion callback\n */\nconst applyAnimation = (\n targetValue: number | string,\n config: TransitionConfig,\n callback?: AnimationCallback\n): number | string => {\n 'worklet'\n const delay = config.delay\n\n let animatedValue: any\n if (config.type === 'timing') {\n animatedValue = withTiming(\n targetValue as number,\n config as WithTimingConfig,\n callback\n )\n } else {\n animatedValue = withSpring(\n targetValue as number,\n config as WithSpringConfig,\n callback\n )\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 Gui-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).acceptRenderProp = true\n return Component\n}\n\nconst AnimatedView = createWebAnimatedComponent('div')\nconst AnimatedText = createWebAnimatedComponent('span')\n\n// =============================================================================\n// Transition Config Builder\n// =============================================================================\n\ntype TransitionConfigResult = {\n baseConfig: TransitionConfig\n propertyConfigs: Record<string, TransitionConfig>\n}\n\n/**\n * Builds animation config from a transition prop.\n * Shared logic used in both initial render and style emitter updates.\n */\nfunction buildTransitionConfig<A extends Record<string, TransitionConfig>>(\n transition: any,\n animations: A,\n animationState: 'enter' | 'exit' | 'default',\n styleKeys: Set<string>\n): TransitionConfigResult {\n const normalized = normalizeTransition(transition)\n const effectiveKey = getEffectiveAnimation(normalized, animationState)\n\n let base = effectiveKey\n ? (animations[effectiveKey as keyof typeof animations] ??\n ({ type: 'spring' } as TransitionConfig))\n : ({ type: 'spring' } as TransitionConfig)\n\n if (normalized.delay) {\n base = { ...base, delay: normalized.delay }\n }\n\n if (normalized.config) {\n base = { ...base, ...normalized.config }\n // infer type: 'timing' if duration is provided without spring params\n if (\n base.type !== 'timing' &&\n normalized.config.duration !== undefined &&\n normalized.config.damping === undefined &&\n normalized.config.stiffness === undefined &&\n normalized.config.mass === undefined\n ) {\n base = { ...base, type: 'timing' }\n }\n }\n\n // build per-property configs\n const propertyConfigs: Record<string, TransitionConfig> = {}\n\n for (const key of styleKeys) {\n const propAnimation = normalized.properties[key]\n if (typeof propAnimation === 'string') {\n propertyConfigs[key] = animations[propAnimation as keyof typeof animations] ?? base\n } else if (propAnimation && typeof propAnimation === 'object') {\n const configType = (propAnimation as any).type\n const baseForProp = configType\n ? (animations[configType as keyof typeof animations] ?? base)\n : base\n propertyConfigs[key] = {\n ...baseForProp,\n ...propAnimation,\n } as TransitionConfig\n } else {\n propertyConfigs[key] = base\n }\n }\n\n return { baseConfig: base, propertyConfigs }\n}\n\n/**\n * Extracts all style keys including transform sub-properties.\n */\nfunction getStyleKeys(style: Record<string, unknown>): Set<string> {\n const keys = new Set(Object.keys(style))\n if (style.transform && Array.isArray(style.transform)) {\n for (const t of style.transform as Record<string, unknown>[]) {\n if (t && typeof t === 'object') {\n keys.add(Object.keys(t)[0])\n }\n }\n }\n return keys\n}\n\n// =============================================================================\n// Animation Driver Factory\n// =============================================================================\n\n/**\n * Create a Reanimated-based animation driver for Gui.\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 * ```\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 needsCustomComponent: true,\n View: isWeb ? AnimatedView : Animated.View,\n Text: isWeb ? AnimatedText : Animated.Text,\n isReactNative: true,\n inputStyle: 'value',\n outputStyle: 'inline',\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 if (config.type === 'direct') {\n sharedValue.value = next\n onFinish?.()\n } else {\n // animations must start on the UI thread in Reanimated 4.x —\n // withSpring/withTiming return animation descriptors that only\n // work when assigned to a SharedValue inside the UI runtime\n const cb = onFinish\n ? () => {\n 'worklet'\n runOnJS(onFinish)()\n }\n : undefined\n\n if (isWeb) {\n // on web there's no UI thread — set directly\n sharedValue.value =\n config.type === 'spring'\n ? withSpring(next, config as WithSpringConfig, cb)\n : withTiming(next, config as WithTimingConfig, cb)\n } else {\n runOnUI(() => {\n 'worklet'\n sharedValue.value =\n config.type === 'spring'\n ? withSpring(next, config as WithSpringConfig, cb)\n : withTiming(next, config as WithTimingConfig, cb)\n })()\n }\n }\n },\n\n stop() {\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 if (isWeb) {\n // web: must pass explicit deps (no babel plugin)\n return useAnimatedStyle(() => {\n 'worklet'\n return getStyle(instance.value)\n }, [instance, getStyle])\n }\n\n // native: use useAnimatedReaction to watch instance changes and\n // write computed style to a dedicated SharedValue. useAnimatedStyle\n // then reads only that SharedValue — pure closure, no non-SV items\n // to confuse startMapper's dependency tracking.\n const styleVal = useSharedValue(getStyle(instance.value))\n\n useAnimatedReaction(\n () => {\n 'worklet'\n return instance.value\n },\n (current, previous) => {\n 'worklet'\n if (current !== previous) {\n styleVal.value = getStyle(current)\n }\n }\n )\n\n return useAnimatedStyle(() => {\n 'worklet'\n return styleVal.value\n })\n },\n\n useAnimatedNumbersStyle(vals, getStyle) {\n const instances = vals.map((v) => v.getInstance())\n\n return useAnimatedStyle(\n () => {\n 'worklet'\n const currentValues = instances.map((inst) => inst.value)\n return getStyle(...currentValues)\n },\n isWeb ? [getStyle, ...instances] : undefined\n )\n },\n\n // =========================================================================\n // useAnimations - Main animation hook for components\n // =========================================================================\n useAnimations(animationProps) {\n const {\n props,\n presence,\n style,\n componentState,\n useStyleEmitter,\n themeName,\n stateRef,\n styleState,\n } = animationProps\n\n // State flags\n const isHydrating = componentState.unmounted === true\n const isMounting = componentState.unmounted === 'should-enter'\n const isEntering = !!componentState.unmounted\n\n // Presence state for exit animations\n const isExiting = presence?.[0] === false\n\n // Track if we just finished entering (transition from entering to not entering)\n const wasEnteringRef = useRef(isEntering)\n const justFinishedEntering = wasEnteringRef.current && !isEntering\n React.useEffect(() => {\n wasEnteringRef.current = isEntering\n })\n\n // Use effectiveTransition computed by createComponent (single source of truth)\n const effectiveTransition = styleState?.effectiveTransition ?? props.transition\n const normalized = normalizeTransition(effectiveTransition)\n // Use 'enter' if we're mounting OR if we just finished entering\n const animationState: 'enter' | 'exit' | 'default' = isExiting\n ? 'exit'\n : isMounting || justFinishedEntering\n ? 'enter'\n : 'default'\n const animationKey = getEffectiveAnimation(normalized, animationState)\n\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 // Get sendExitComplete callback from presence\n const sendExitComplete = presence?.[1]\n\n // =========================================================================\n // Exit cycle state for deterministic per-property completion tracking\n // =========================================================================\n const exitCycleIdRef = useRef(0)\n const pendingExitKeysRef = useRef<Set<string>>(new Set())\n const exitCompletedRef = useRef(false)\n const wasExitingRef = useRef(false)\n\n // detect transition into/out of exiting state\n const justStartedExiting = isExiting && !wasExitingRef.current\n const justStoppedExiting = !isExiting && wasExitingRef.current\n\n // stable callback to mark a property as done (called from worklet via runOnJS)\n const markExitKeyDone = useEvent(\n (key: string, cycleId: number, finished: boolean) => {\n // ignore callbacks from stale cycles\n if (cycleId !== exitCycleIdRef.current) return\n // ignore if already completed\n if (exitCompletedRef.current) return\n // count both finished and canceled animations as \"done\" during exit\n // (element is leaving anyway, canceled animations shouldn't block completion)\n\n pendingExitKeysRef.current.delete(key)\n\n // check if all exit animations are done\n if (pendingExitKeysRef.current.size === 0) {\n exitCompletedRef.current = true\n sendExitComplete?.()\n }\n }\n )\n\n // SharedValue to pass exit state into worklet\n const isExitingRef = useSharedValue(isExiting)\n const exitCycleIdShared = useSharedValue(exitCycleIdRef.current)\n\n // start new exit cycle only on transition INTO exiting (not every render while exiting)\n if (justStartedExiting) {\n exitCycleIdRef.current++\n exitCompletedRef.current = false\n pendingExitKeysRef.current.clear()\n }\n // invalidate pending callbacks when exit is canceled/interrupted\n if (justStoppedExiting) {\n exitCycleIdRef.current++\n pendingExitKeysRef.current.clear()\n }\n\n // update shared values (using effect to avoid writing during render)\n useIsomorphicLayoutEffect(() => {\n isExitingRef.value = isExiting\n exitCycleIdShared.value = exitCycleIdRef.current\n }, [isExiting, exitCycleIdRef.current])\n\n // track previous exiting state\n React.useEffect(() => {\n wasExitingRef.current = isExiting\n })\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 return buildTransitionConfig(\n props.transition,\n animations,\n animationState,\n getStyleKeys(animatedStyles)\n )\n }, [isHydrating, props.transition, animatedStyles, animationState])\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>, effectiveTransition) => {\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 // effectiveTransition is computed in createComponent based on entering/exiting pseudo states\n // rebuild config whenever transition changes (entering OR exiting pseudo states)\n const transitionToUse = effectiveTransition ?? props.transition\n const { baseConfig: newBase, propertyConfigs: newPropertyConfigs } =\n buildTransitionConfig(\n transitionToUse,\n animations,\n animationState,\n getStyleKeys(nextStyle)\n )\n\n // update configRef with the new config\n configRef.set({\n baseConfig: newBase,\n propertyConfigs: newPropertyConfigs,\n disableAnimation: configRef.get().disableAnimation,\n isHydrating: configRef.get().isHydrating,\n })\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 - on web, the mapper watches these if passed in dependencies\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 // Compute and register exit keys synchronously during render to avoid race conditions\n // This must happen BEFORE useAnimatedStyle runs so callbacks have a populated set\n const exitKeysRegistered = useRef(false)\n if (justStartedExiting && sendExitComplete) {\n const exitKeys: string[] = []\n const animateOnly = props.animateOnly as string[] | undefined\n\n // regular animated properties\n for (const key in animatedStyles) {\n if (key === 'transform') continue\n if (canAnimateProperty(key, animatedStyles[key], animateOnly)) {\n exitKeys.push(key)\n }\n }\n\n // transform sub-keys (filter by animateOnly if specified)\n const transforms = animatedStyles.transform\n if (transforms && Array.isArray(transforms)) {\n for (const t of transforms) {\n if (!t) continue\n const tKey = Object.keys(t)[0]\n if (tKey) {\n // check animateOnly filter for transform sub-keys\n if (animateOnly && !animateOnly.includes(tKey)) {\n continue\n }\n exitKeys.push(`transform:${tKey}`)\n }\n }\n }\n\n // register keys for this cycle\n pendingExitKeysRef.current = new Set(exitKeys)\n exitKeysRegistered.current = exitKeys.length > 0\n }\n\n // handle zero-animation case in effect (after render commit)\n React.useEffect(() => {\n if (!justStartedExiting || !sendExitComplete) return\n\n // if no keys were registered, complete immediately\n if (!exitKeysRegistered.current && pendingExitKeysRef.current.size === 0) {\n if (!exitCompletedRef.current) {\n exitCompletedRef.current = true\n sendExitComplete()\n }\n }\n }, [justStartedExiting, sendExitComplete])\n\n // Create animated style\n const animatedStyle = useAnimatedStyle(\n () => {\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 from useStyleEmitter\n const emitterAnimated = animatedTargetsRef.value\n const emitterStatic = staticTargetsRef.value\n const emitterTransforms = transformTargetsRef.value\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 ? emitterStatic! : {}\n\n // read exit state from shared values\n const currentlyExiting = isExitingRef.value\n const currentCycleId = exitCycleIdShared.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\n // create callback for exit tracking\n let callback: AnimationCallback | undefined\n if (currentlyExiting) {\n const capturedKey = key\n const capturedCycleId = currentCycleId\n callback = (finished) => {\n 'worklet'\n runOnJS(markExitKeyDone)(capturedKey, capturedCycleId, finished ?? false)\n }\n }\n\n result[key] = applyAnimation(targetValue as number, propConfig, callback)\n }\n\n // Handle transforms\n const transforms = hasEmitterUpdates\n ? emitterTransforms\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 =\n config.propertyConfigs[transformKey] ?? config.baseConfig\n\n // create callback for exit tracking (transform sub-key)\n let callback: AnimationCallback | undefined\n if (currentlyExiting) {\n const capturedKey = `transform:${transformKey}`\n const capturedCycleId = currentCycleId\n callback = (finished) => {\n 'worklet'\n runOnJS(markExitKeyDone)(\n capturedKey,\n capturedCycleId,\n finished ?? false\n )\n }\n }\n\n validTransforms.push({\n [transformKey]: applyAnimation(\n targetValue as number,\n propConfig,\n callback\n ),\n })\n }\n }\n\n if (validTransforms.length > 0) {\n result.transform = validTransforms\n }\n }\n\n return result\n },\n isWeb\n ? [\n animatedStyles,\n baseConfig,\n propertyConfigs,\n disableAnimation,\n isHydrating,\n // pass SharedValues so the mapper watches them on web (no babel plugin)\n animatedTargetsRef,\n staticTargetsRef,\n transformTargetsRef,\n isExitingRef,\n exitCycleIdShared,\n markExitKeyDone,\n ]\n : undefined\n )\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"
|
|
10
10
|
]
|
|
11
11
|
}
|