{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "fine-tune-slider", "type": "registry:ui", "title": "Fine Tune Slider", "description": "A slider with pull-away precision mode — drag normally, pull away from the track to fine-tune values.", "dependencies": [ "motion" ], "files": [ { "path": "registry/ruixenui/fine-tune-slider.tsx", "content": "\"use client\";\n\nimport { motion, AnimatePresence } from \"motion/react\";\nimport { useState, useRef, useCallback } from \"react\";\n\n/**\n * Fine Tune Slider — Rauno Freiberg craft.\n *\n * Hold and drag to scrub. Pull away from the track to fine-tune.\n * Elastic tether visualizes precision depth. Layered thumb shadows.\n * Staggered tick marks. Track glow. Cursor glow under glass.\n */\n\n/* ── Springs — tuned per visual weight ── */\n\nconst spring = {\n thumb: { type: \"spring\" as const, stiffness: 500, damping: 30 },\n content: { type: \"spring\" as const, stiffness: 340, damping: 28 },\n icon: { type: \"spring\" as const, stiffness: 440, damping: 24 },\n tether: { type: \"spring\" as const, stiffness: 500, damping: 40 },\n};\n\n/* ── Types ── */\n\ninterface FineTuneSliderProps {\n min?: number;\n max?: number;\n step?: number;\n defaultValue?: number;\n onChange?: (value: number) => void;\n}\n\n/* ── Helper ── */\n\nfunction snap(v: number, step: number, min: number, max: number) {\n const s = Math.round(v / step) * step;\n const d = step < 1 ? (String(step).split(\".\")[1]?.length ?? 0) : 0;\n return Math.min(max, Math.max(min, Number(s.toFixed(d))));\n}\n\n/* ── Tick positions ── */\n\nconst TICKS = [0, 25, 50, 75, 100];\n\n/* ── Component ── */\n\nexport function FineTuneSlider({\n min = 0,\n max = 100,\n step = 1,\n defaultValue = 50,\n onChange,\n}: FineTuneSliderProps) {\n const [value, setValue] = useState(defaultValue);\n const [dragging, setDragging] = useState(false);\n const [hovered, setHovered] = useState(false);\n const [precision, setPrecision] = useState(1);\n const [cursorOffset, setCursorOffset] = useState(0);\n const trackRef = useRef(null);\n const drag = useRef({\n startX: 0,\n startVal: 0,\n rect: null as DOMRect | null,\n });\n\n /* Cursor glow */\n const [glowX, setGlowX] = useState(0);\n const [glowOn, setGlowOn] = useState(false);\n\n const doSnap = useCallback(\n (v: number) => snap(v, step, min, max),\n [min, max, step],\n );\n\n /* ── Pointer handlers ── */\n\n const onDown = useCallback(\n (e: React.PointerEvent) => {\n if (!trackRef.current) return;\n e.preventDefault();\n e.currentTarget.setPointerCapture(e.pointerId);\n\n const rect = trackRef.current.getBoundingClientRect();\n const pct = Math.max(\n 0,\n Math.min(1, (e.clientX - rect.left) / rect.width),\n );\n const jumped = doSnap(min + pct * (max - min));\n\n setValue(jumped);\n onChange?.(jumped);\n drag.current = { startX: e.clientX, startVal: jumped, rect };\n setDragging(true);\n },\n [min, max, doSnap, onChange],\n );\n\n const onMove = useCallback(\n (e: React.PointerEvent) => {\n if (!dragging || !drag.current.rect) return;\n const { startX, startVal, rect } = drag.current;\n if (rect.width === 0) return;\n\n const trackCenterY = rect.top + rect.height / 2;\n const vDist = e.clientY - trackCenterY;\n setCursorOffset(vDist);\n\n const p = 1 + Math.abs(vDist) / 60;\n setPrecision(p);\n\n const dx = (e.clientX - startX) / p;\n const dv = (dx / rect.width) * (max - min);\n const next = doSnap(startVal + dv);\n\n setValue(next);\n onChange?.(next);\n },\n [dragging, max, min, doSnap, onChange],\n );\n\n const onUp = useCallback(() => {\n setDragging(false);\n setPrecision(1);\n setCursorOffset(0);\n }, []);\n\n const pct = max > min ? ((value - min) / (max - min)) * 100 : 0;\n const inPrecision = dragging && precision > 1.4;\n const active = dragging || hovered;\n\n /* Tether geometry */\n const tetherLen = Math.min(Math.abs(cursorOffset), 140);\n const tetherDown = cursorOffset > 0;\n\n return (\n
\n
\n {/* Surface */}\n {\n const r = e.currentTarget.getBoundingClientRect();\n setGlowX(e.clientX - r.left);\n if (!glowOn) setGlowOn(true);\n }}\n onMouseLeave={() => {\n setGlowOn(false);\n if (!dragging) setHovered(false);\n }}\n onMouseEnter={() => setHovered(true)}\n >\n {/* ── Cursor glow — light-under-glass ── */}\n \n\n {/* ── Value row ── */}\n
\n \n {value}\n \n\n \n {inPrecision && (\n \n \n \n {precision.toFixed(1)}×\n \n \n )}\n \n
\n\n {/* ── Track zone — overflow visible for tether ── */}\n
\n {/* ── Elastic tether — precision depth visual ── */}\n \n {inPrecision && (\n \n \n {/* Endpoint dot */}\n \n \n )}\n \n\n {/* ── Track ── */}\n \n {/* Filled portion — glows in precision mode */}\n \n\n {/* Tick marks — stagger in on interaction */}\n {TICKS.map((t, i) => (\n \n ))}\n\n {/* Thumb */}\n \n \n \n
\n\n {/* Min / Max labels — reveal on interaction */}\n \n \n {min}\n \n \n {max}\n \n \n
\n \n
\n \n );\n}\n\n/* ── Icons ── */\n\nfunction CrosshairIcon() {\n return (\n \n \n \n \n \n \n \n );\n}\n\nexport default FineTuneSlider;\n", "type": "registry:ui", "target": "components/ruixen/fine-tune-slider.tsx" } ] }