import { isValidElement, type ReactElement, type ReactNode, useEffect, useReducer, useRef, } from "react"; import { StyleSheet, View, type ViewStyle } from "react-native"; import Animated, { useAnimatedStyle, useSharedValue, withTiming, } from "react-native-reanimated"; export interface LiveChartTransitionProps { /** Key of the active child to show. Must match a child's `key`. */ active: string; /** Chart elements, each with a unique `key`. */ children: ReactNode; /** Cross-fade duration in ms. Default `300`. */ duration?: number; /** * Keep every child mounted for the lifetime of the transition (only the * opacity animates). Each chart's engine then settles its y-range once, up * front, so switching is a pure cross-fade with no reveal/range re-animation. * Costs more (all engines run continuously). Default `false` (mount the * active child + unmount the outgoing one after the fade). */ keepMounted?: boolean; /** Container style. */ style?: ViewStyle; } function FadeLayer({ visible, duration, children, }: { visible: boolean; duration: number; children: ReactNode; }) { const opacity = useSharedValue(visible ? 1 : 0); useEffect(() => { opacity.set(withTiming(visible ? 1 : 0, { duration })); }, [visible, duration, opacity]); const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.get() })); return ( {children} ); } type MountedAction = | { type: "mount"; key: string } | { type: "unmount"; key: string }; /** Tracks which child keys are currently mounted during a cross-fade. */ function mountedReducer( mounted: Set, action: MountedAction, ): Set { switch (action.type) { case "mount": if (mounted.has(action.key)) return mounted; return new Set(mounted).add(action.key); case "unmount": { if (!mounted.has(action.key)) return mounted; const next = new Set(mounted); next.delete(action.key); return next; } } } /** * Cross-fades between chart instances by `key` (e.g. line ↔ candle). Mounts the * active child plus any outgoing child still fading out; unmounts the outgoing * child once the fade completes. RN/Reanimated analogue of liveline's * `LivelineTransition`. */ export function LiveChartTransition({ active, children, duration = 300, keepMounted = false, style, }: LiveChartTransitionProps) { // Use the raw children (not Children.toArray, which rewrites keys to ".$key"). const childArray = ( Array.isArray(children) ? children : [children] ).filter(isValidElement) as ReactElement[]; const [mounted, dispatch] = useReducer( mountedReducer, active, (initial) => new Set([initial]), ); const prevRef = useRef(active); useEffect(() => { if (keepMounted) return; if (active === prevRef.current) return; const outgoing = prevRef.current; prevRef.current = active; dispatch({ type: "mount", key: active }); const timer = setTimeout(() => { dispatch({ type: "unmount", key: outgoing }); }, duration + 50); return () => clearTimeout(timer); }, [active, duration, keepMounted]); return ( {childArray.map((child) => { const key = String(child.key ?? ""); if (!keepMounted && !mounted.has(key)) return null; return ( {child} ); })} ); } const styles = StyleSheet.create({ root: { flex: 1, position: "relative" }, });