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 (
);
}