{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "slider", "title": "Slider", "description": "A hand-drawn range slider with a sketchy track and circle thumb.", "dependencies": [ "roughjs" ], "registryDependencies": [ "https://www.bydefaulthuman.fun/r/use-rough.json", "https://www.bydefaulthuman.fun/r/rough-lib.json", "https://www.bydefaulthuman.fun/r/crumble-theme.json", "utils" ], "files": [ { "path": "registry/new-york/ui/slider.tsx", "content": "\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useRef,\n useState,\n type ChangeEvent,\n type InputHTMLAttributes,\n} from \"react\";\nimport { useRough } from \"@/hooks/use-rough\";\nimport {\n resolveRoughVars,\n stableSeed,\n type CrumbleColorProps,\n type CrumbleTheme,\n} from \"@/lib/rough\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface SliderProps\n extends Omit, \"type\">,\n CrumbleColorProps {\n formatValue?: (value: number) => string;\n label?: string;\n showValue?: boolean;\n theme?: CrumbleTheme;\n}\n\nconst TRACK_HEIGHT = 6;\nconst THUMB_SIZE = 20;\n\nexport function Slider({\n className,\n defaultValue,\n fill,\n formatValue,\n id,\n label,\n max = 100,\n min = 0,\n onChange,\n showValue = true,\n stroke,\n strokeMuted,\n theme: themeProp,\n value,\n ...props\n}: SliderProps) {\n const [current, setCurrent] = useState(Number(value ?? defaultValue ?? min));\n const wrapperRef = useRef(null);\n const trackSvgRef = useRef(null);\n const thumbSvgRef = useRef(null);\n const sliderId =\n id ?? `slider-${label?.toLowerCase().replace(/\\s+/g, \"-\") ?? \"field\"}`;\n const pct = (current - Number(min)) / (Number(max) - Number(min));\n const roughStyle = resolveRoughVars({ stroke, strokeMuted, fill });\n const { drawLine, theme } = useRough({\n stableId: sliderId,\n svgRef: trackSvgRef,\n theme: themeProp,\n variant: \"border\",\n });\n const { drawCircle } = useRough({\n stableId: `${sliderId}-thumb`,\n svgRef: thumbSvgRef,\n theme: themeProp,\n variant: \"interactive\",\n });\n\n const drawAll = useCallback(() => {\n const thumbSvg = thumbSvgRef.current;\n const trackSvg = trackSvgRef.current;\n const wrapper = wrapperRef.current;\n\n if (!thumbSvg || !trackSvg || !wrapper) return;\n\n const width = wrapper.offsetWidth;\n const trackHeight = TRACK_HEIGHT + 4;\n\n trackSvg.replaceChildren();\n trackSvg.setAttribute(\"width\", String(width));\n trackSvg.setAttribute(\"height\", String(trackHeight));\n trackSvg.setAttribute(\"viewBox\", `0 0 ${width} ${trackHeight}`);\n\n const baseStrokeWidth = TRACK_HEIGHT;\n const track = drawLine(0, TRACK_HEIGHT / 2 + 2, width, TRACK_HEIGHT / 2 + 2, {\n seed: stableSeed(`${sliderId}-track`),\n stroke: \"hsl(var(--border))\",\n strokeWidth: baseStrokeWidth,\n });\n if (track) trackSvg.appendChild(track);\n\n if (pct > 0) {\n const fillLine = drawLine(\n 0,\n TRACK_HEIGHT / 2 + 2,\n width * pct,\n TRACK_HEIGHT / 2 + 2,\n {\n seed: stableSeed(`${sliderId}-fill`),\n stroke: \"var(--cr-stroke)\",\n strokeWidth: baseStrokeWidth,\n },\n );\n if (fillLine) trackSvg.appendChild(fillLine);\n }\n\n thumbSvg.replaceChildren();\n const thumb = drawCircle(THUMB_SIZE / 2, THUMB_SIZE / 2, THUMB_SIZE - 2, {\n fill: \"hsl(var(--background))\",\n fillStyle: \"solid\",\n stroke: \"var(--cr-stroke)\",\n });\n if (thumb) thumbSvg.appendChild(thumb);\n }, [drawCircle, drawLine, pct, sliderId]);\n\n useEffect(() => {\n drawAll();\n }, [drawAll]);\n\n useEffect(() => {\n const wrapper = wrapperRef.current;\n if (!wrapper) return;\n\n const observer = new ResizeObserver(() => drawAll());\n observer.observe(wrapper);\n return () => observer.disconnect();\n }, [drawAll]);\n\n const handleChange = (event: ChangeEvent) => {\n setCurrent(Number(event.target.value));\n onChange?.(event);\n };\n\n return (\n
\n {label || showValue ? (\n
\n {label ? (\n \n {label}\n \n ) : null}\n {showValue ? (\n \n {formatValue ? formatValue(current) : String(current)}\n \n ) : null}\n
\n ) : null}\n
\n \n \n \n
\n
\n );\n}\n", "type": "registry:ui", "target": "components/crumble/ui/slider.tsx" } ], "type": "registry:ui" }