import React, { useCallback, useEffect } from 'react' import { LayoutChangeEvent, View } from 'react-native' import { CollapseAnimationConfig, CollapseProps } from './types' import Animated, { Easing, useSharedValue, useAnimatedStyle, withTiming, } from 'react-native-reanimated' export * from './types' export const Collapse = (props: CollapseProps) => { const { open, children, style, contentContainerStyle, animationConfig, ...rest } = { ...Collapse.defaultProps, ...props, } /** Two separate SharedValues are required: `height` is the measured natural height (written once and never animated), while `animatedHeight` is the value that actually drives the transition. Keeping them separate avoids re-triggering `onLayout` effects when the animation runs. */ const height = useSharedValue(0) const animatedHeight = useSharedValue(0) const onLayout = useCallback((event: LayoutChangeEvent) => { const measuredHeight = event.nativeEvent.layout.height /** Guard prevents overwriting the measured height if children re-trigger layout (e.g. after a font-load or image decode). */ if (height.value != 0) return if (measuredHeight) { height.value = measuredHeight if (open) { animatedHeight.value = withTiming(measuredHeight, animationConfig) } } }, [open]) /** `maxHeight` is animated rather than `height` so the inner View can still measure its natural size via `onLayout`; animating `height` directly would freeze the measurement. The `'auto'` fallback keeps content visible before the first layout completes. */ const animatedStyle = useAnimatedStyle(() => { return { maxHeight: height.value === 0 ? 'auto' : animatedHeight.value, overflow: 'hidden', } }) useEffect(() => { animatedHeight.value = withTiming(open ? height.value : 0, animationConfig) }, [open]) return ( {children} ) } const defaultAnimationConfig: CollapseAnimationConfig = { duration: 300, easing: Easing.inOut(Easing.quad), } Collapse.defaultProps = { animationConfig: defaultAnimationConfig, } as CollapseProps