{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "scrub-input", "type": "registry:ui", "title": "Scrub Input", "description": "An inline slider input styled as a pill, perfect for adjusting variables smoothly.", "dependencies": [], "devDependencies": [], "registryDependencies": [], "files": [ { "path": "components/ui/scrub-input.tsx", "type": "registry:ui", "content": "\"use client\";\n\nimport React, { useRef, useState } from \"react\";\nimport { cn } from \"@workspace/ui/lib/utils\";\n\nexport interface ScrubInputProps {\n value?: number;\n defaultValue?: number;\n onChange?: (value: number) => void;\n label: string;\n min?: number;\n max?: number;\n step?: number;\n className?: string;\n}\n\nexport function ScrubInput({\n value: controlledValue,\n defaultValue = 0,\n onChange,\n label,\n min = 0,\n max = 100,\n step = 1,\n className,\n}: ScrubInputProps) {\n const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);\n const isControlled = controlledValue !== undefined;\n const value = isControlled ? controlledValue : uncontrolledValue;\n\n const [isDragging, setIsDragging] = useState(false);\n const containerRef = useRef(null);\n\n const handleValueChange = (newValue: number) => {\n if (!isControlled) setUncontrolledValue(newValue);\n onChange?.(newValue);\n };\n\n const updateValueFromPointer = (clientX: number) => {\n if (!containerRef.current) return;\n const rect = containerRef.current.getBoundingClientRect();\n const x = clientX - rect.left;\n let percentage = x / rect.width;\n percentage = Math.max(0, Math.min(1, percentage));\n\n let newValue = min + percentage * (max - min);\n newValue = Math.round(newValue / step) * step;\n\n // Ensure value stays within bounds due to rounding\n newValue = Math.max(min, Math.min(max, newValue));\n\n if (newValue !== value) {\n handleValueChange(newValue);\n }\n };\n\n const onPointerDown = (e: React.PointerEvent) => {\n containerRef.current?.setPointerCapture(e.pointerId);\n setIsDragging(true);\n updateValueFromPointer(e.clientX);\n };\n\n const onPointerMove = (e: React.PointerEvent) => {\n if (!isDragging) return;\n updateValueFromPointer(e.clientX);\n };\n\n const onPointerUp = (e: React.PointerEvent) => {\n containerRef.current?.releasePointerCapture(e.pointerId);\n setIsDragging(false);\n };\n\n const percentage = max > min ? ((value - min) / (max - min)) * 100 : 0;\n\n // Create a minimum width so the fill doesn't look too weird when value is 0\n const fillWidth = Math.max(20, percentage);\n\n return (\n \n {/* The fill / thumb */}\n \n {/* Handle mark */}\n
\n
\n\n {/* Label and Value (Always on top to be visible whether the thumb covers them or not) */}\n
\n \n {label}\n \n \n {value}\n \n
\n \n );\n}\n" } ], "tailwind": {}, "cssVars": {}, "meta": {} }