{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "slider", "type": "registry:ui", "title": "Slider", "description": "PanResponder slider — no gesture-handler dep.", "registryDependencies": [ "https://raw.githubusercontent.com/truongnat/rnui/main/registry/dist/nativewind/utils.json", "https://raw.githubusercontent.com/truongnat/rnui/main/registry/dist/nativewind/theme.json" ], "files": [ { "path": "registry/shared/ui/slider.tsx", "type": "registry:ui", "target": "components/ui/slider.tsx", "content": "import { useContext, useRef, useState } from 'react';\nimport {\n PanResponder,\n StyleSheet,\n View,\n type ViewProps,\n} from 'react-native';\nimport { cn, FormFieldContext, useThemeColor } from '@/lib/utils';\n\nexport interface SliderProps extends ViewProps {\n /** Controlled value. Omit and use `defaultValue` for uncontrolled. */\n value?: number;\n defaultValue?: number;\n min?: number;\n max?: number;\n step?: number;\n onValueChange?: (value: number) => void;\n onSlidingStart?: (value: number) => void;\n onSlidingComplete?: (value: number) => void;\n disabled?: boolean;\n /** Mirrors shadcn `aria-invalid` — destructive tint. Auto-detected from FormField error. */\n invalid?: boolean;\n className?: string;\n trackClassName?: string;\n thumbClassName?: string;\n}\n\nexport function Slider({\n value,\n defaultValue = 0,\n min = 0,\n max = 100,\n step = 1,\n onValueChange,\n onSlidingStart,\n onSlidingComplete,\n disabled = false,\n invalid,\n className,\n trackClassName,\n thumbClassName,\n style,\n ...props\n}: SliderProps) {\n const [trackWidth, setTrackWidth] = useState(0);\n const [innerValue, setInnerValue] = useState(defaultValue);\n const containerRef = useRef(null);\n const trackWidthRef = useRef(0);\n trackWidthRef.current = trackWidth;\n\n const field = useContext(FormFieldContext);\n const colors = useThemeColor();\n const isInvalid = invalid ?? !!field?.error;\n\n const current = value ?? innerValue;\n const clamp = (v: number) => Math.min(max, Math.max(min, v));\n const snap = (v: number) => {\n const stepped = Math.round((v - min) / step) * step + min;\n return Math.min(max, Math.max(min, stepped));\n };\n\n const pct =\n trackWidth > 0\n ? ((clamp(current) - min) / (max - min)) * 100\n : 0;\n\n const latestValueRef = useRef(current);\n latestValueRef.current = current;\n\n const startValueRef = useRef(current);\n const disabledRef = useRef(disabled);\n disabledRef.current = disabled;\n\n const commitValue = (val: number) => {\n const clamped = snap(clamp(val));\n if (value === undefined) {\n setInnerValue(clamped);\n }\n onValueChange?.(clamped);\n };\n\n const panResponder = useRef(\n PanResponder.create({\n // Capture touch immediately on start to prevent parent ScrollView / iOS swipe from canceling\n onStartShouldSetPanResponder: () => !disabledRef.current,\n onStartShouldSetPanResponderCapture: () => !disabledRef.current,\n\n onMoveShouldSetPanResponder: (_, gestureState) =>\n !disabledRef.current &&\n Math.abs(gestureState.dx) > Math.abs(gestureState.dy),\n onMoveShouldSetPanResponderCapture: (_, gestureState) =>\n !disabledRef.current &&\n Math.abs(gestureState.dx) > Math.abs(gestureState.dy),\n\n onPanResponderGrant: (e) => {\n if (disabledRef.current) return;\n startValueRef.current = latestValueRef.current;\n onSlidingStart?.(latestValueRef.current);\n },\n\n onPanResponderMove: (_, gestureState) => {\n if (disabledRef.current || trackWidthRef.current <= 0) return;\n const deltaValue =\n (gestureState.dx / trackWidthRef.current) * (max - min);\n const nextValue = snap(clamp(startValueRef.current + deltaValue));\n commitValue(nextValue);\n },\n\n // Crucial: lock touch responder so parent ScrollView / iOS back gesture cannot steal it\n onPanResponderTerminationRequest: () => false,\n\n onPanResponderRelease: (e, gestureState) => {\n if (disabledRef.current) return;\n\n // If it was a quick tap with minimal drag (dx < 3), calculate tap target position\n if (\n Math.abs(gestureState.dx) < 3 &&\n trackWidthRef.current > 0 &&\n containerRef.current\n ) {\n containerRef.current.measure((_x, _y, w, _h, pageX) => {\n const tapX = e.nativeEvent.pageX - pageX;\n const ratio = Math.min(1, Math.max(0, tapX / w));\n const tapValue = snap(min + ratio * (max - min));\n commitValue(tapValue);\n onSlidingComplete?.(tapValue);\n });\n } else {\n onSlidingComplete?.(latestValueRef.current);\n }\n },\n\n onPanResponderTerminate: () => {\n onSlidingComplete?.(latestValueRef.current);\n },\n })\n ).current;\n\n const activeColor = isInvalid\n ? colors.destructive\n : colors.primary;\n\n return (\n {\n const w = e.nativeEvent.layout.width;\n setTrackWidth(w);\n trackWidthRef.current = w;\n }}\n {...panResponder.panHandlers}\n {...props}\n >\n {/* Background Track */}\n \n {/* Active Filled Range */}\n \n \n\n {/* Draggable Thumb */}\n \n \n );\n}\n\nconst styles = StyleSheet.create({\n container: {\n height: 38,\n justifyContent: 'center',\n },\n});\n" } ] }