import { useEffect, forwardRef, useMemo } from 'react'; import { View } from 'react-native'; import Animated, { useSharedValue, useAnimatedStyle, withRepeat, withSequence, withTiming, Easing, } from 'react-native-reanimated'; import { skeletonStyles } from './Skeleton.styles'; import type { SkeletonProps, SkeletonGroupProps } from './types'; import { getNativeLiveRegionAccessibilityProps } from '../utils/accessibility'; import type { IdealystElement } from '../utils/refTypes'; const Skeleton = forwardRef(({ width: _width = '100%', height: _height = 20, shape = 'rectangle', borderRadius, animation = 'pulse', style, testID, id, // Accessibility props accessibilityLabel, accessibilityLive, accessibilityBusy, }, ref) => { // Generate native accessibility props const nativeA11yProps = useMemo(() => { return getNativeLiveRegionAccessibilityProps({ accessibilityLabel: accessibilityLabel ?? 'Loading content', accessibilityLive: accessibilityLive ?? 'polite', accessibilityBusy: accessibilityBusy ?? true, }); }, [accessibilityLabel, accessibilityLive, accessibilityBusy]); skeletonStyles.useVariants({ shape, animation, }); const animatedValue = useSharedValue(0); useEffect(() => { if (animation === 'pulse') { // Pulse animation: opacity from 1 -> 0.5 -> 1 animatedValue.value = withRepeat( withSequence( withTiming(1, { duration: 750, easing: Easing.inOut(Easing.ease) }), withTiming(0, { duration: 750, easing: Easing.inOut(Easing.ease) }) ), -1, // infinite false ); } else if (animation === 'wave') { // Wave animation: translateX from -100% -> 100% animatedValue.value = withRepeat( withTiming(1, { duration: 1500, easing: Easing.inOut(Easing.ease) }), -1, // infinite false ); } }, [animation]); const pulseAnimatedStyle = useAnimatedStyle(() => { if (animation === 'pulse') { return { opacity: animatedValue.value === 0 ? 1 : 1 - animatedValue.value * 0.5, }; } return {}; }); const waveAnimatedStyle = useAnimatedStyle(() => { if (animation === 'wave') { return { transform: [ { translateX: (animatedValue.value - 0.5) * 400, // -200 to 200 }, ], }; } return {}; }); const customStyles = { ...(shape === 'rounded' && borderRadius ? { borderRadius } : {}), ...(shape === 'circle' ? { aspectRatio: 1 } : {}), }; return ( {animation === 'wave' && ( )} ); }); Skeleton.displayName = 'Skeleton'; export const SkeletonGroup: React.FC = ({ count = 3, spacing = 12, skeletonProps, style, testID, id, }) => { skeletonStyles.useVariants({}); return ( {Array.from({ length: count }).map((_, index) => ( ))} ); }; export default Skeleton;