import { View, StyleSheet, ViewStyle } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import Animated, { useSharedValue, useAnimatedStyle, runOnJS, interpolate, Extrapolation, } from 'react-native-reanimated'; export type SliderProps = { /** Minimum value */ minimumValue: number; /** Maximum value */ maximumValue: number; /** Current value */ value: number; /** Callback when value changes */ onValueChange: (value: number) => void; /** Color of the filled track */ minimumTrackTintColor?: string; /** Color of the unfilled track */ maximumTrackTintColor?: string; /** Color of the thumb */ thumbTintColor?: string; /** Additional styles */ style?: ViewStyle; }; /** * Interactive slider component built with react-native-reanimated and gesture-handler. * * @example * ```tsx * const [value, setValue] = useState(50); * * * ``` */ export function Slider({ minimumValue, maximumValue, value, onValueChange, minimumTrackTintColor = '#6366f1', maximumTrackTintColor = '#374151', thumbTintColor = '#818cf8', style, }: SliderProps) { const trackWidth = useSharedValue(0); const thumbPosition = useSharedValue( ((value - minimumValue) / (maximumValue - minimumValue)) * 100 ); const updateValue = (position: number) => { const percentage = Math.max(0, Math.min(position / trackWidth.value, 1)); const newValue = minimumValue + percentage * (maximumValue - minimumValue); onValueChange(newValue); }; const panGesture = Gesture.Pan() .onStart((e) => { const newPosition = e.x; thumbPosition.value = (newPosition / trackWidth.value) * 100; runOnJS(updateValue)(newPosition); }) .onChange((e) => { const newPosition = Math.max(0, Math.min(e.x, trackWidth.value)); thumbPosition.value = (newPosition / trackWidth.value) * 100; runOnJS(updateValue)(newPosition); }); const tapGesture = Gesture.Tap().onStart((e) => { const newPosition = e.x; thumbPosition.value = (newPosition / trackWidth.value) * 100; runOnJS(updateValue)(newPosition); }); const combinedGesture = Gesture.Race(panGesture, tapGesture); const thumbStyle = useAnimatedStyle(() => { const left = interpolate( thumbPosition.value, [0, 100], [0, trackWidth.value], Extrapolation.CLAMP ); return { transform: [{ translateX: left - 12 }], }; }); const fillStyle = useAnimatedStyle(() => { return { width: `${thumbPosition.value}%`, }; }); // Update thumb position when value prop changes const currentPercentage = ((value - minimumValue) / (maximumValue - minimumValue)) * 100; if (Math.abs(thumbPosition.value - currentPercentage) > 0.1) { thumbPosition.value = currentPercentage; } return ( { trackWidth.value = e.nativeEvent.layout.width; }} > ); } const styles = StyleSheet.create({ container: { height: 40, justifyContent: 'center', }, track: { height: 4, position: 'relative', justifyContent: 'center', }, trackBackground: { height: 4, borderRadius: 2, position: 'absolute', width: '100%', }, trackFill: { height: 4, borderRadius: 2, position: 'absolute', }, thumb: { width: 24, height: 24, borderRadius: 12, position: 'absolute', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.25, shadowRadius: 3.84, elevation: 5, }, });