1import { useEffect, useState } from "react";
2import { View } from "react-native";
3import Animated, {
4 cancelAnimation,
5 Easing,
6 useAnimatedStyle,
7 useSharedValue,
8 withRepeat,
9 withTiming,
10} from "react-native-reanimated";
11
12const INDICATOR_WIDTH_FRACTION = 0.3;
13const MIN_INDICATOR_WIDTH = 48;
14
15function LoadingStripFrame(props: {
16 readonly children: React.ReactNode;
17 readonly onLayout?: (width: number) => void;
18}) {
19 return (
20 <View
21 pointerEvents="none"
22 accessibilityElementsHidden
23 importantForAccessibility="no-hide-descendants"
24 className="absolute inset-x-0 top-0 z-10 h-0.5 overflow-hidden"
25 onLayout={
26 props.onLayout
27 ? (event) => {
28 props.onLayout?.(event.nativeEvent.layout.width);
29 }
30 : undefined
31 }
32 >
33 {props.children}
34 </View>
35 );
36}
37
38function IndeterminateLoadingStrip() {
39 const [containerWidth, setContainerWidth] = useState(0);
40 const travelProgress = useSharedValue(0);
41 const indicatorWidth = Math.max(MIN_INDICATOR_WIDTH, containerWidth * INDICATOR_WIDTH_FRACTION);
42
43 useEffect(() => {
44 travelProgress.value = 0;
45 travelProgress.value = withRepeat(
46 withTiming(1, {
47 duration: 1100,
48 easing: Easing.inOut(Easing.quad),
49 }),
50 -1,
51 false,
52 );
53
54 return () => {
55 cancelAnimation(travelProgress);
56 };
57 }, [travelProgress]);
58
59 const indicatorStyle = useAnimatedStyle(
60 () => ({
61 transform: [
62 {
63 translateX: (containerWidth + indicatorWidth) * travelProgress.value - indicatorWidth,
64 },
65 ],
66 width: indicatorWidth,
67 }),
68 [containerWidth, indicatorWidth],
69 );
70
71 return (
72 <LoadingStripFrame onLayout={setContainerWidth}>
73 <Animated.View className="h-full rounded-full bg-primary" style={indicatorStyle} />
74 </LoadingStripFrame>
75 );
76}
77
78export function LoadingStrip(props: { readonly progress?: number }) {
79 if (props.progress === undefined) {
80 return <IndeterminateLoadingStrip />;
81 }
82
83 const clampedProgress = Math.min(1, Math.max(0, props.progress));
84
85 return (
86 <LoadingStripFrame>
87 <View
88 className="h-full rounded-r-full bg-primary"
89 style={{ width: `${clampedProgress * 100}%` }}
90 />
91 </LoadingStripFrame>
92 );
93}