140 lines
3.4 KiB
TypeScript
140 lines
3.4 KiB
TypeScript
import { createContext, useContext } from "react";
|
|
import { Dimensions, View } from "react-native";
|
|
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
|
import Animated, {
|
|
useSharedValue,
|
|
useAnimatedScrollHandler,
|
|
useAnimatedStyle,
|
|
interpolate,
|
|
Extrapolation,
|
|
} from "react-native-reanimated";
|
|
|
|
import { cn } from "@turbostarter/ui";
|
|
|
|
import type { FlatList } from "react-native";
|
|
import type { ViewProps } from "react-native";
|
|
import type {
|
|
SharedValue,
|
|
ScrollHandlerProcessed,
|
|
FlatListPropsWithLayout,
|
|
} from "react-native-reanimated";
|
|
|
|
interface SliderContextType {
|
|
threshold: number;
|
|
scrollX: SharedValue<number> | null;
|
|
onScroll: ScrollHandlerProcessed<Record<string, unknown>>;
|
|
}
|
|
|
|
const SliderContext = createContext<SliderContextType>({
|
|
threshold: Dimensions.get("window").width,
|
|
scrollX: null,
|
|
onScroll: () => null,
|
|
});
|
|
|
|
const Slider = ({
|
|
className,
|
|
threshold = Dimensions.get("window").width,
|
|
...props
|
|
}: ViewProps & { threshold?: number }) => {
|
|
const scrollX = useSharedValue(0);
|
|
|
|
const onScroll = useAnimatedScrollHandler({
|
|
onScroll: ({ contentOffset: { x } }) => {
|
|
scrollX.value = x;
|
|
},
|
|
});
|
|
|
|
return (
|
|
<SliderContext.Provider value={{ scrollX, onScroll, threshold }}>
|
|
<View {...props} className={cn("flex-1 items-center gap-4", className)} />
|
|
</SliderContext.Provider>
|
|
);
|
|
};
|
|
|
|
const SliderList = <ItemT,>(
|
|
props: FlatListPropsWithLayout<ItemT> & {
|
|
ref?: React.ForwardedRef<FlatList>;
|
|
},
|
|
) => {
|
|
const { onScroll, threshold } = useContext(SliderContext);
|
|
const native = Gesture.Native();
|
|
|
|
return (
|
|
<GestureDetector gesture={native}>
|
|
<Animated.FlatList
|
|
onScroll={onScroll}
|
|
horizontal
|
|
showsHorizontalScrollIndicator={false}
|
|
initialNumToRender={1}
|
|
maxToRenderPerBatch={1}
|
|
scrollEventThrottle={16}
|
|
decelerationRate="fast"
|
|
snapToAlignment="start"
|
|
pagingEnabled
|
|
snapToInterval={threshold}
|
|
{...props}
|
|
/>
|
|
</GestureDetector>
|
|
);
|
|
};
|
|
|
|
const SliderListItem = ({
|
|
index,
|
|
style,
|
|
...props
|
|
}: ViewProps & { index: number }) => {
|
|
const { scrollX, threshold } = useContext(SliderContext);
|
|
const opacity = useAnimatedStyle(() => ({
|
|
opacity: interpolate(
|
|
scrollX?.value ?? 0,
|
|
[(index - 1) * threshold, index * threshold, (index + 1) * threshold],
|
|
[0, 1, 0],
|
|
Extrapolation.CLAMP,
|
|
),
|
|
}));
|
|
|
|
return <Animated.View style={[style, opacity]} {...props} />;
|
|
};
|
|
|
|
const SliderPaginationDots = ({ className, ...props }: ViewProps) => {
|
|
return <View className={cn("flex-row gap-1.5", className)} {...props} />;
|
|
};
|
|
|
|
const SliderPaginationDot = ({
|
|
className,
|
|
index = 0,
|
|
...props
|
|
}: ViewProps & { index?: number }) => {
|
|
const { scrollX, threshold } = useContext(SliderContext);
|
|
|
|
const animatedDot = useAnimatedStyle(() => ({
|
|
opacity: interpolate(
|
|
scrollX?.value ?? 0,
|
|
[(index - 1) * threshold, index * threshold, (index + 1) * threshold],
|
|
[0.7, 1, 0.7],
|
|
),
|
|
width: interpolate(
|
|
scrollX?.value ?? 0,
|
|
[(index - 1) * threshold, index * threshold, (index + 1) * threshold],
|
|
[11, 28, 11],
|
|
Extrapolation.CLAMP,
|
|
),
|
|
}));
|
|
|
|
return (
|
|
<Animated.View
|
|
className={cn("bg-primary h-3 rounded-full opacity-50", className)}
|
|
style={animatedDot}
|
|
{...props}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export {
|
|
Slider,
|
|
SliderList,
|
|
SliderListItem,
|
|
SliderPaginationDots,
|
|
SliderPaginationDot,
|
|
};
|