{"$schema":"https://ui.shadcn.com/schema/registry.json","author":"Kaiyu Hsu <uicapsule@kyh.io>","categories":["controls","skeuomorphism"],"dependencies":["@radix-ui/react-slider","motion"],"description":"iOS-style vertical volume slider — drag past the top and the bar stretches, past the bottom it squashes, with sigmoid rubber-band resistance and a spring back.","devDependencies":[],"files":[{"content":"import { useRef, useState } from \"react\";\nimport * as Slider from \"@radix-ui/react-slider\";\nimport { animate, motion, useMotionValue, useMotionValueEvent, useTransform } from \"motion/react\";\n\n/** Height of the track in px. Must stay in sync with the `h-[200px]` class below. */\nconst TRACK_HEIGHT = 200;\n/** How far the bar may rubber-band past either end of the track, in px. */\nconst OVERSCROLL = 50;\n\n/** Sigmoid falloff: maps unbounded overshoot into (-max, max). */\nconst decay = (value: number, max: number) => {\n  const entry = value / max;\n  const sigmoid = 2 / (1 + Math.exp(-entry)) - 1;\n  const exit = sigmoid * max;\n\n  return exit;\n};\n\nexport const Volume = () => {\n  const [value, setValue] = useState([0]);\n  const [position, setPosition] = useState<\"top\" | \"middle\" | \"bottom\">(\"middle\");\n\n  const clientY = useMotionValue(0);\n  const y = useMotionValue(1);\n\n  const ref = useRef<HTMLDivElement>(null);\n\n  useMotionValueEvent(clientY, \"change\", (latestValue) => {\n    if (!ref.current) {\n      return;\n    }\n\n    const overflow = latestValue - ref.current.getBoundingClientRect().top;\n\n    if (overflow < 0) {\n      y.jump(decay(overflow, OVERSCROLL));\n      setPosition(\"bottom\");\n    } else if (overflow > TRACK_HEIGHT) {\n      y.jump(decay(overflow - TRACK_HEIGHT, OVERSCROLL));\n      setPosition(\"top\");\n    } else {\n      y.jump(1);\n      setPosition(\"middle\");\n    }\n  });\n\n  // Negative `y` stretches the bar past the track, positive `y` squashes it.\n  const scaleY = useTransform(() => (TRACK_HEIGHT - y.get()) / TRACK_HEIGHT);\n  const yReadout = useTransform(() => Math.floor(y.get()));\n\n  return (\n    <div className=\"flex h-96 flex-col items-center justify-center gap-12\">\n      <Slider.Root\n        ref={ref}\n        className=\"relative flex h-[200px] w-[60px] touch-none flex-col items-center select-none\"\n        value={value}\n        onValueChange={setValue}\n        max={100}\n        step={1}\n        orientation=\"vertical\"\n        asChild\n      >\n        <motion.div\n          style={{\n            scaleY,\n            // Both overscroll directions pivot on the track's bottom edge, which is\n            // the bar's fixed physical anchor.\n            transformOrigin: position === \"middle\" ? \"center\" : \"bottom\",\n          }}\n          onPointerMove={(events) => {\n            if (events.buttons > 0) {\n              clientY.set(events.clientY);\n            }\n          }}\n          onLostPointerCapture={() => {\n            animate(y, 1, { bounce: 0.5, type: \"spring\" });\n          }}\n        >\n          <Slider.Track className=\"relative w-full grow overflow-hidden rounded-xl bg-neutral-800\">\n            <Slider.Range className=\"absolute w-full bg-white\" />\n          </Slider.Track>\n        </motion.div>\n      </Slider.Root>\n\n      <div className=\"flex w-96 flex-col items-center gap-1\">\n        <span className=\"text-neutral-300\">value: {value[0]}</span>\n        <span className=\"text-neutral-300\">\n          y: <motion.span>{yReadout}</motion.span>\n        </span>\n      </div>\n    </div>\n  );\n};\n","path":"/ios-volume-slider.tsx","target":"uicapsule/ios-volume-slider/ios-volume-slider.tsx","type":"registry:file"},{"content":"\"use client\";\n\nimport { Volume } from \"./ios-volume-slider\";\n\nconst Preview = () => (\n  <main className=\"flex h-dvh items-center justify-center bg-neutral-900\">\n    <Volume />\n  </main>\n);\n\nexport default Preview;\n","path":"/preview.tsx","target":"uicapsule/ios-volume-slider/preview.tsx","type":"registry:file"}],"homepage":"https://www.uicapsule.com/ui/ios-volume-slider","name":"ios-volume-slider","registryDependencies":[],"title":"iOS Volume Slider","type":"registry:block"}