import { useEffect } from "react"; import { View } from "react-native"; import Animated, { Easing, cancelAnimation, useAnimatedStyle, useSharedValue, withTiming, } from "react-native-reanimated"; interface PlaybackPageIndicatorProps { total: number; current: number; /** 0–1 progress for the active segment. Source ticks at ~100ms. */ progress: number; paused: boolean; } const SEGMENT_GAP = 3; const SEGMENT_HEIGHT = 2.5; const SMOOTHING_MS = 300; /** * Snapchat-style segmented progress bar. Past segments full, future empty, * active segment animated. The 300ms linear smoothing absorbs the 100ms * tick from the particle view source so motion looks continuous at 60fps. */ export function PlaybackPageIndicator({ total, current, progress, paused, }: PlaybackPageIndicatorProps) { if (total === 0) return null; return ( {Array.from({ length: total }).map((_, i) => ( ))} ); } interface SegmentProps { isActive: boolean; isPast: boolean; progress: number; paused: boolean; } function Segment({ isActive, isPast, progress, paused }: SegmentProps) { // Each segment owns its own width animation. Past = 1, future = 0, // active = animated toward `progress`. Reanimated keeps the tween on the // UI thread so JS thread stalls (e.g. the 100ms text tick re-render) // can't drop frames here. const fill = useSharedValue(isPast ? 1 : 0); useEffect(() => { if (isPast) { cancelAnimation(fill); fill.value = withTiming(1, { duration: 120, easing: Easing.out(Easing.cubic) }); return; } if (!isActive) { cancelAnimation(fill); fill.value = 0; return; } if (paused) { cancelAnimation(fill); return; } fill.value = withTiming(progress, { duration: SMOOTHING_MS, easing: Easing.linear, }); }, [isPast, isActive, progress, paused, fill]); const fillStyle = useAnimatedStyle(() => ({ width: `${Math.min(Math.max(fill.value, 0), 1) * 100}%`, })); return ( ); }