{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"drip-range","type":"registry:ui","title":"Drip Range","description":"Range slider whose thumb stretches with drag speed and drips a gooey value bubble while you slide.","dependencies":["motion"],"registryDependencies":[],"files":[{"path":"registry/ui/drip-range.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useId, useState } from \"react\";\nimport { motion, useMotionValue, useReducedMotion, useSpring, useTransform, useVelocity } from \"motion/react\";\n\nexport interface DripRangeProps {\n  min?: number;\n  max?: number;\n  step?: number;\n  /** Controlled value. */\n  value?: number;\n  /** Initial value when uncontrolled. */\n  defaultValue?: number;\n  onChange?: (value: number) => void;\n  /** Thumb, fill and bubble color. */\n  color?: string;\n  /** Value text color inside the bubble. */\n  textColor?: string;\n  /** Text appended to the value, e.g. \"%\". */\n  suffix?: string;\n  /** Accessible name. */\n  label?: string;\n  className?: string;\n}\n\nexport function DripRange({\n  min = 0,\n  max = 100,\n  step = 1,\n  value,\n  defaultValue = 42,\n  onChange,\n  color = \"#c6ff3d\",\n  textColor = \"#0a0a0a\",\n  suffix = \"%\",\n  label = \"Value\",\n  className,\n}: DripRangeProps) {\n  const [inner, setInner] = useState(defaultValue);\n  const [active, setActive] = useState(false);\n  const reduce = useReducedMotion();\n  const goo = `goo-${useId().replace(/:/g, \"\")}`;\n  const v = Math.min(max, Math.max(min, value ?? inner));\n  const pct = max > min ? ((v - min) / (max - min)) * 100 : 0;\n\n  // Position in % drives the thumb; its velocity stretches the thumb and tilts the bubble.\n  const pos = useMotionValue(pct);\n  useEffect(() => {\n    pos.set(pct);\n  }, [pct, pos]);\n  const left = useSpring(pos, { stiffness: 700, damping: 45 });\n  const leftCss = useTransform(left, (p) => `${p}%`);\n  const vel = useVelocity(left);\n  const stretch = useSpring(useTransform(vel, [-600, 0, 600], [1.6, 1, 1.6]), { stiffness: 400, damping: 30 });\n  const squash = useTransform(stretch, (s) => 1 / Math.sqrt(s));\n  const tilt = useSpring(useTransform(vel, [-600, 600], [22, -22]), { stiffness: 300, damping: 20 });\n\n  const set = (n: number) => {\n    if (value === undefined) setInner(n);\n    onChange?.(n);\n  };\n\n  const bubble = active && !reduce;\n  const pop = { type: \"spring\" as const, stiffness: 420, damping: bubble ? 16 : 26 };\n\n  return (\n    <div className={`relative w-full max-w-md select-none py-16 ${className ?? \"\"}`}>\n      <svg aria-hidden className=\"absolute size-0\">\n        <filter id={goo} x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\">\n          <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"5\" result=\"b\" />\n          <feColorMatrix in=\"b\" values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 22 -9\" />\n        </filter>\n      </svg>\n\n      {/* Track + fill */}\n      <div className=\"relative h-2 rounded-full bg-muted\">\n        <motion.div className=\"absolute inset-y-0 left-0 rounded-full\" style={{ width: leftCss, background: color }} />\n      </div>\n\n      {/* Gooey layer: the bubble drips out of the thumb and melts back in. */}\n      <div aria-hidden className=\"pointer-events-none absolute inset-0\" style={{ filter: `url(#${goo})` }}>\n        <motion.div className=\"absolute top-1/2\" style={{ left: leftCss }}>\n          <motion.div\n            className=\"absolute -translate-x-1/2 -translate-y-1/2 rounded-full\"\n            style={{ background: color, scaleX: stretch, scaleY: squash }}\n            initial={false}\n            animate={{ width: active ? 30 : 22, height: active ? 30 : 22 }}\n            transition={pop}\n          />\n          <motion.div\n            className=\"absolute size-11 -translate-x-1/2 -translate-y-1/2 rounded-full\"\n            style={{ background: color, rotate: tilt }}\n            initial={false}\n            animate={{ y: bubble ? -46 : 0, scale: bubble ? 1 : 0.3 }}\n            transition={pop}\n          />\n        </motion.div>\n      </div>\n\n      {/* Crisp overlays: thumb core + value text (outside the filter so they stay sharp). */}\n      <div aria-hidden className=\"pointer-events-none absolute inset-0\">\n        <motion.div className=\"absolute top-1/2\" style={{ left: leftCss }}>\n          <motion.span\n            className=\"absolute size-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full\"\n            style={{ background: textColor, opacity: 0.75 }}\n            initial={false}\n            animate={{ scale: active ? 0.6 : 1 }}\n          />\n          <motion.span\n            className=\"absolute -translate-x-1/2 -translate-y-1/2 whitespace-nowrap text-sm font-semibold tabular-nums\"\n            style={{ color: textColor }}\n            initial={false}\n            animate={{ y: bubble ? -46 : 0, opacity: bubble ? 1 : 0, scale: bubble ? 1 : 0.5 }}\n            transition={pop}\n          >\n            {v}\n            {suffix}\n          </motion.span>\n        </motion.div>\n      </div>\n\n      {/* Native range input: pointer, touch and keyboard for free. */}\n      <input\n        type=\"range\"\n        aria-label={label}\n        min={min}\n        max={max}\n        step={step}\n        value={v}\n        onChange={(e) => set(Number(e.target.value))}\n        onPointerDown={() => {\n          setActive(true);\n          window.addEventListener(\"pointerup\", () => setActive(false), { once: true });\n        }}\n        onKeyDown={() => setActive(true)}\n        onKeyUp={() => setActive(false)}\n        onBlur={() => setActive(false)}\n        className=\"peer absolute inset-x-0 top-1/2 h-10 -translate-y-1/2 cursor-grab appearance-none opacity-0 active:cursor-grabbing [&::-moz-range-thumb]:h-10 [&::-moz-range-thumb]:w-px [&::-webkit-slider-thumb]:h-10 [&::-webkit-slider-thumb]:w-px [&::-webkit-slider-thumb]:appearance-none\"\n      />\n      <motion.span\n        aria-hidden\n        className=\"pointer-events-none absolute top-1/2 hidden size-9 -translate-x-1/2 -translate-y-1/2 rounded-full ring-2 ring-ring ring-offset-2 ring-offset-background peer-focus-visible:block\"\n        style={{ left: leftCss }}\n      />\n    </div>\n  );\n}\n"}]}