import React, { useRef, useState, useCallback } from 'react'; import { View, PanResponder, Animated, Text, StyleSheet, } from 'react-native'; import { useTheme } from '../../theme/useTheme'; import type { SliderProps, SliderColor } from './Slider.types'; const THUMB_SIZE = 24; const TRACK_HEIGHT = 4; const colorKeyMap: Record = { primary: 'primary', secondary: 'secondary', success: 'success', warning: 'warning', error: 'error', }; export const Slider: React.FC = ({ value, onValueChange, onSlidingComplete, min = 0, max = 1, step, disabled = false, color = 'primary', showValue = false, style, }) => { const theme = useTheme(); const [trackWidth, setTrackWidth] = useState(0); const thumbPosition = useRef(new Animated.Value(0)).current; const colorValue = theme.colors[colorKeyMap[color]]; const range = max - min; const clampedValue = Math.min(max, Math.max(min, value)); const normalizedValue = range > 0 ? (clampedValue - min) / range : 0; const updatePosition = useCallback( (rawValue: number) => { let val = rawValue; if (step && step > 0) { const steps = Math.round(val / step); val = steps * step; } val = Math.min(1, Math.max(0, val)); const actualValue = min + val * range; onValueChange(actualValue); }, [min, range, step, onValueChange] ); const trackRef = useRef(null); const trackLayoutRef = useRef({ x: 0, width: 0 }); const handleTouch = useCallback( (pageX: number) => { const { x, width } = trackLayoutRef.current; if (width <= 0) return; const val = Math.min(1, Math.max(0, (pageX - x - THUMB_SIZE / 2) / (width - THUMB_SIZE))); updatePosition(val); }, [updatePosition] ); const panResponder = useRef( PanResponder.create({ onStartShouldSetPanResponder: () => !disabled, onMoveShouldSetPanResponder: () => !disabled, onPanResponderGrant: (evt) => { if (disabled) return; trackRef.current?.measureInWindow((winX, _y, winWidth) => { trackLayoutRef.current.x = winX; if (winWidth > 0) trackLayoutRef.current.width = winWidth; handleTouch(evt.nativeEvent.pageX); }); }, onPanResponderMove: (evt) => { if (disabled) return; handleTouch(evt.nativeEvent.pageX); }, onPanResponderRelease: (evt) => { if (disabled) return; handleTouch(evt.nativeEvent.pageX); const { x, width } = trackLayoutRef.current; if (width > 0) { const val = Math.min(1, Math.max(0, (evt.nativeEvent.pageX - x - THUMB_SIZE / 2) / (width - THUMB_SIZE))); const actualValue = min + val * range; onSlidingComplete?.(actualValue); } }, }) ).current; React.useEffect(() => { if (trackWidth > 0) { const position = normalizedValue * (trackWidth - THUMB_SIZE); thumbPosition.setValue(position); } }, [normalizedValue, trackWidth, thumbPosition]); const onTrackLayout = useCallback((e: { nativeEvent: { layout: { width: number } } }) => { const { width } = e.nativeEvent.layout; setTrackWidth(width); trackLayoutRef.current.width = width; }, []); const fillWidth = trackWidth > 0 ? normalizedValue * (trackWidth - THUMB_SIZE) + THUMB_SIZE / 2 : 0; return ( {showValue && ( {clampedValue.toFixed(step ? 2 : 1)} )} ); }; const styles = StyleSheet.create({ wrapper: {}, disabled: { opacity: 0.5, }, valueText: {}, trackContainer: { justifyContent: 'center', position: 'relative', }, track: { width: '100%', overflow: 'hidden', }, fill: { position: 'absolute', left: 0, top: 0, }, thumb: { position: 'absolute', left: 0, top: 0, }, });