import React, { useRef, useCallback } from 'react'; import { View, Text, StyleSheet, PanResponder, Dimensions, Platform, } from 'react-native'; import { useDubsTheme } from '../theme'; const THUMB_SIZE = 32; const TRACK_HEIGHT = 6; const TICK_INTERVAL = 0.01; // haptic every 0.01 SOL export interface SolSliderProps { /** Current value in SOL */ value: number; /** Min SOL (default 0.01) */ min?: number; /** Max SOL (default 5) */ max?: number; /** Step size in SOL (default 0.01) */ step?: number; /** Accent color for filled track + thumb glow */ accentColor?: string; /** Called on every drag frame */ onValueChange: (value: number) => void; /** Called when user lifts finger */ onSlidingComplete?: (value: number) => void; /** Called on each tick for haptics/sound */ onTick?: (value: number) => void; /** Disabled state */ disabled?: boolean; } export function SolSlider({ value, min = 0.01, max = 5, step = 0.01, accentColor, onValueChange, onSlidingComplete, onTick, disabled = false, }: SolSliderProps) { const t = useDubsTheme(); const accent = accentColor || t.accent; const trackRef = useRef(null); const trackWidth = useRef(0); const lastTickValue = useRef(value); const clamp = (v: number) => { const stepped = Math.round(v / step) * step; return Math.max(min, Math.min(max, parseFloat(stepped.toFixed(4)))); }; const valueToPosition = (v: number) => { const ratio = (v - min) / (max - min); return ratio * trackWidth.current; }; const positionToValue = (x: number) => { const ratio = Math.max(0, Math.min(1, x / trackWidth.current)); return clamp(min + ratio * (max - min)); }; const panResponder = useRef( PanResponder.create({ onStartShouldSetPanResponder: () => !disabled, onMoveShouldSetPanResponder: () => !disabled, onPanResponderGrant: (_, gestureState) => { lastTickValue.current = value; }, onPanResponderMove: (evt, gestureState) => { // Calculate position relative to track const touchX = gestureState.moveX; trackRef.current?.measureInWindow((trackX) => { const relX = touchX - trackX; const newVal = positionToValue(relX); // Fire tick if we crossed a tick boundary const tickDelta = Math.abs(newVal - lastTickValue.current); if (tickDelta >= TICK_INTERVAL) { lastTickValue.current = newVal; onTick?.(newVal); } onValueChange(newVal); }); }, onPanResponderRelease: () => { onSlidingComplete?.(value); }, }) ).current; const ratio = (value - min) / (max - min); const filledWidth = `${ratio * 100}%`; // Snap points for visual markers const markers = [min, max * 0.25, max * 0.5, max * 0.75, max]; return ( {/* Track */} { trackWidth.current = e.nativeEvent.layout.width; }} {...panResponder.panHandlers} > {/* Background track */} {/* Filled track */} {/* Tick markers */} {markers.map((m, i) => { const mRatio = (m - min) / (max - min); return ( = m ? accent : '#3A3A3C', }, ]} /> ); })} {/* Thumb */} {/* Min/Max labels */} {min} {max} SOL ); } const styles = StyleSheet.create({ container: { paddingVertical: 4, }, trackContainer: { height: THUMB_SIZE + 16, justifyContent: 'center', paddingHorizontal: THUMB_SIZE / 2, }, track: { height: TRACK_HEIGHT, borderRadius: TRACK_HEIGHT / 2, overflow: 'hidden', }, trackFilled: { height: '100%', borderRadius: TRACK_HEIGHT / 2, }, markerRow: { position: 'absolute', left: THUMB_SIZE / 2, right: THUMB_SIZE / 2, height: TRACK_HEIGHT, top: (THUMB_SIZE + 16 - TRACK_HEIGHT) / 2, }, marker: { position: 'absolute', width: 3, height: 12, borderRadius: 1.5, top: -3, marginLeft: -1.5, }, thumb: { position: 'absolute', width: THUMB_SIZE, height: THUMB_SIZE, borderRadius: THUMB_SIZE / 2, top: 8, marginLeft: 0, alignItems: 'center', justifyContent: 'center', ...Platform.select({ ios: { shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0.6, shadowRadius: 10, }, android: { elevation: 8, }, }), }, thumbInner: { width: 12, height: 12, borderRadius: 6, backgroundColor: '#FFFFFF', }, rangeRow: { flexDirection: 'row', justifyContent: 'space-between', paddingHorizontal: THUMB_SIZE / 2, marginTop: 4, }, rangeText: { color: '#5A5A5E', fontSize: 11, fontWeight: '600', }, });