{"$schema":"https://ui.shadcn.com/schema/registry.json","author":"Kaiyu Hsu <uicapsule@kyh.io>","categories":["controls","cyberpunk"],"dependencies":["@number-flow/react","motion"],"description":"Rotary dial slider — drag sideways to spin a ring of tick marks under a fixed needle; release and it springs to the nearest tick while the number readout rolls.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { useState } from \"react\";\nimport NumberFlow from \"@number-flow/react\";\n\nimport { RadialSlider } from \"./radial-slider\";\n\nconst Preview = () => {\n  const [value, setValue] = useState(0);\n\n  return (\n    <main className=\"flex h-dvh items-center justify-center bg-zinc-900 p-5\">\n      <div className=\"relative h-36 w-96 overflow-hidden rounded-3xl bg-black p-4 shadow-2xl\">\n        <div className=\"pointer-events-none absolute right-0 bottom-0 left-0 z-30 h-[20%] bg-gradient-to-b from-transparent to-black\" />\n        <div className=\"mb-6 flex items-center justify-center text-white\">\n          <NumberFlow value={value} className=\"text-xl\" />\n        </div>\n        <div className=\"pointer-events-none absolute left-1/2 z-20 h-full w-1 -translate-x-1/2 -translate-y-3 rounded-full bg-white\" />\n        <RadialSlider onChange={setValue} />\n      </div>\n    </main>\n  );\n};\n\nexport default Preview;\n","path":"/preview.tsx","target":"uicapsule/radial-slider/preview.tsx","type":"registry:file"},{"content":"import { useMemo, useRef } from \"react\";\nimport { animate, motion, useMotionValue } from \"motion/react\";\n\n/** Horizontal drag distance, in px, that advances the dial by one degree. */\nconst PIXELS_PER_DEGREE = 5;\n\n/**\n * Rounds a rotation to the nearest tick, expressed as an angle in [0, 360].\n * 360 is a valid result: it means \"snap forward to the next full turn\".\n */\nconst nearestTickAngle = (rotation: number, snapAngle: number) => {\n  const normalized = ((rotation % 360) + 360) % 360;\n  return Math.round(normalized / snapAngle) * snapAngle;\n};\n\ninterface RadialSliderProps {\n  onChange: (value: number) => void;\n  maxValue?: number;\n}\n\nexport const RadialSlider = ({ onChange, maxValue = 100 }: RadialSliderProps) => {\n  const rotate = useMotionValue(0);\n  const accumulatedRotation = useRef(0);\n\n  const ticks = useMemo(\n    () =>\n      Array.from({ length: maxValue }, (_, tick) => tick * (360 / maxValue)).map((angle) => (\n        <div\n          key={angle}\n          className=\"absolute inset-0 flex justify-center overflow-hidden rounded-full\"\n          style={{\n            transform: `rotate(${angle}deg)`,\n          }}\n        >\n          <div className=\"absolute h-8 w-0.5 bg-gray-200\" />\n        </div>\n      )),\n    [maxValue],\n  );\n\n  return (\n    <>\n      <motion.div\n        drag=\"x\"\n        dragConstraints={{ left: 0, right: 0 }}\n        dragElastic={0}\n        onDrag={(_event, info) => {\n          rotate.set(accumulatedRotation.current + info.offset.x / PIXELS_PER_DEGREE);\n\n          const snapAngle = 360 / maxValue;\n          const nearestMultiple = nearestTickAngle(rotate.get(), snapAngle);\n\n          const topIndex = (maxValue - nearestMultiple / snapAngle) % maxValue;\n          onChange(Math.floor(topIndex));\n        }}\n        onDragEnd={() => {\n          const currentRotation = rotate.get();\n\n          const snapAngle = 360 / maxValue;\n          const nearestMultiple = nearestTickAngle(currentRotation, snapAngle);\n\n          const fullRotations = Math.floor(currentRotation / 360) * 360;\n          const finalRotation = fullRotations + nearestMultiple;\n\n          accumulatedRotation.current = finalRotation;\n\n          animate(rotate, finalRotation, {\n            damping: 30,\n            stiffness: 300,\n            type: \"spring\",\n          });\n        }}\n        style={{ rotate }}\n        className=\"relative aspect-square w-full cursor-grab rounded-full bg-transparent active:cursor-grabbing\"\n      >\n        {ticks}\n      </motion.div>\n      <div className=\"pointer-events-none absolute right-0 bottom-0 left-0 z-10 h-[calc(50%+1rem)] bg-gradient-to-r from-blue-500 to-yellow-500 mix-blend-multiply\" />\n    </>\n  );\n};\n","path":"/radial-slider.tsx","target":"uicapsule/radial-slider/radial-slider.tsx","type":"registry:file"}],"homepage":"https://www.uicapsule.com/ui/radial-slider","name":"radial-slider","registryDependencies":[],"title":"Radial Slider","type":"registry:block"}