{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "liquid-slider",
  "type": "registry:component",
  "title": "Liquid Slider",
  "description": "A range slider whose thumb is a droplet: it stretches when you drag fast, melts into the filled track, and buds off a little value bubble while you hold it.",
  "categories": [
    "component",
    "Liquid"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/liquid-slider.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { motion, useMotionValue, useReducedMotion, useSpring, useTransform, useVelocity } from \"motion/react\";\nimport { type ReactNode, useEffect, useId, useLayoutEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst TONES = {\n  primary: \"[--liquid:var(--color-primary)] [--liquid-ink:var(--color-primary-foreground)]\",\n  info: \"[--liquid:var(--color-info)] [--liquid-ink:var(--color-white)]\",\n  success: \"[--liquid:var(--color-success)] [--liquid-ink:var(--color-white)]\",\n  destructive: \"[--liquid:var(--color-destructive)] [--liquid-ink:var(--color-white)]\",\n} as const;\n\nconst THUMB = 22;\nconst BAR = 8;\nconst HIT = 32;\nconst BUBBLE_H = 26;\nconst BUBBLE_RISE = 40;\nconst TETHER = THUMB * 0.5;\nconst BUD = THUMB * 0.7;\nconst ease = (v: number, a: number, b: number) => {\n  const t = Math.min(Math.max((v - a) / (b - a), 0), 1);\n  return t * t * (3 - 2 * t);\n};\n\nexport interface LiquidSliderProps {\n  value?: number;\n  defaultValue?: number;\n  onValueChange?: (value: number) => void;\n  min?: number;\n  max?: number;\n  step?: number;\n  label?: ReactNode;\n  icon?: ReactNode;\n  formatValue?: (value: number) => string;\n  bubble?: boolean;\n  tone?: keyof typeof TONES;\n  disabled?: boolean;\n  name?: string;\n  className?: string;\n  \"aria-label\"?: string;\n}\n\nexport function LiquidSlider({\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  min = 0,\n  max = 100,\n  step = 1,\n  label,\n  icon,\n  formatValue = (v) => String(v),\n  bubble = true,\n  tone = \"primary\",\n  disabled,\n  name,\n  className,\n  \"aria-label\": ariaLabel,\n}: LiquidSliderProps) {\n  const [inner, setInner] = useState(defaultValue ?? min);\n  const value = valueProp ?? inner;\n  const [width, setWidth] = useState(0);\n  const [dragging, setDragging] = useState(false);\n  const [nudged, setNudged] = useState(false);\n  const nudgeTimer = useRef<number | undefined>(undefined);\n  const trackRef = useRef<HTMLDivElement>(null);\n  const reduce = useReducedMotion();\n  const id = useId();\n  const filterId = `liquid-slider-${id.replace(/:/g, \"\")}`;\n\n  useLayoutEffect(() => {\n    const track = trackRef.current;\n    if (!track) return;\n    const measure = () => setWidth(track.offsetWidth);\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(track);\n    return () => observer.disconnect();\n  }, []);\n\n  useEffect(() => () => window.clearTimeout(nudgeTimer.current), []);\n\n  const pct = max > min ? (Math.min(Math.max(value, min), max) - min) / (max - min) : 0;\n  const centre = THUMB / 2 + pct * Math.max(width - THUMB, 0);\n\n  const target = useMotionValue(centre);\n  const x = useSpring(target, { stiffness: 900, damping: 56, mass: 0.6 });\n  const lag = useSpring(target, { stiffness: 260, damping: 26 });\n  const trail = useTransform(() => x.get() + Math.max(-TETHER, Math.min(TETHER, lag.get() - x.get())));\n  const bubbleX = useSpring(target, { stiffness: 380, damping: 30 });\n  const velocity = useVelocity(x);\n  const scaleX = useTransform(velocity, [-2400, 0, 2400], [1.38, 1, 1.38], { clamp: true });\n  const scaleY = useTransform(velocity, [-2400, 0, 2400], [0.8, 1, 0.8], { clamp: true });\n  const fillClip = useTransform(x, (cx) => `inset(0 ${Math.max(width - cx, 0)}px 0 0 round ${BAR}px)`);\n\n  const showBubble = bubble && !disabled && (dragging || nudged);\n  const rise = useSpring(0, { stiffness: 420, damping: 24, mass: 0.7 });\n  const labelOpacity = useTransform(rise, [BUBBLE_RISE * 0.72, BUBBLE_RISE * 0.92], [0, 1]);\n\n  useLayoutEffect(() => {\n    if (!width) return;\n    if (reduce || target.get() === 0) {\n      for (const v of [target, x, lag, bubbleX]) v.jump(centre);\n    } else target.set(centre);\n  }, [centre, width, reduce, target, x, lag, bubbleX]);\n\n  useEffect(() => {\n    const to = showBubble ? BUBBLE_RISE : 0;\n    if (reduce) rise.jump(to);\n    else rise.set(to);\n  }, [showBubble, reduce, rise]);\n\n  const bubbleY = useTransform(rise, (r) => -r);\n  const longest = Math.max(formatValue(min).length, formatValue(max).length, formatValue(value).length);\n  const bubbleW = Math.max(BUBBLE_H + 4, Math.round(longest * 7.2 + 18));\n  const budW = useTransform(rise, (r) => BUD + (bubbleW - BUD) * ease(r, BUBBLE_RISE * 0.45, BUBBLE_RISE * 0.9));\n  const budH = useTransform(rise, (r) => BUD + (BUBBLE_H - BUD) * ease(r, BUBBLE_RISE * 0.25, BUBBLE_RISE * 0.75));\n  const budMarginX = useTransform(budW, (w) => -w / 2);\n  const budMarginY = useTransform(budH, (h) => -h / 2);\n\n  const set = (next: number) => {\n    if (valueProp === undefined) setInner(next);\n    onValueChange?.(next);\n  };\n\n  const nudge = () => {\n    setNudged(true);\n    window.clearTimeout(nudgeTimer.current);\n    nudgeTimer.current = window.setTimeout(() => setNudged(false), 900);\n  };\n\n  // Drawn twice: under the goo filter for the necks, then crisp on top so edges stay sharp.\n  const shapes = (crisp: boolean) => (\n    <>\n      <motion.span\n        className=\"absolute top-1/2 left-0 w-full rounded-full bg-(--liquid)\"\n        style={{ height: BAR, marginTop: -BAR / 2, clipPath: fillClip }}\n      />\n      {!crisp && (\n        <motion.span\n          className=\"absolute top-1/2 left-0 rounded-full bg-(--liquid)\"\n          style={{ x: trail, width: THUMB * 0.62, height: THUMB * 0.62, marginLeft: -THUMB * 0.31, marginTop: -THUMB * 0.31 }}\n        />\n      )}\n      {!crisp && bubble && (\n        <motion.span\n          className=\"absolute top-1/2 left-0 rounded-full bg-(--liquid)\"\n          style={{ x: bubbleX, y: bubbleY, width: budW, height: budH, marginLeft: budMarginX, marginTop: budMarginY }}\n        />\n      )}\n      <motion.span\n        className={cn(\n          \"absolute top-1/2 left-0 rounded-full bg-(--liquid)\",\n          crisp &&\n            \"ring-offset-background transition-shadow duration-150 group-has-[input:focus-visible]/slider:ring-2 group-has-[input:focus-visible]/slider:ring-ring group-has-[input:focus-visible]/slider:ring-offset-2\",\n        )}\n        style={{ x, scaleX, scaleY, width: THUMB, height: THUMB, marginLeft: -THUMB / 2, marginTop: -THUMB / 2 }}\n      >\n        {crisp && (\n          <span className=\"absolute inset-[7px] rounded-full bg-(--liquid-ink) opacity-90 transition-[scale,opacity] duration-150 ease-out group-has-[input:active]/slider:scale-50 group-has-[input:active]/slider:opacity-0\" />\n        )}\n      </motion.span>\n    </>\n  );\n\n  const valueText = formatValue(value);\n\n  return (\n    <div className={cn(\"grid w-full gap-2\", TONES[tone], disabled && \"opacity-64\", className)}>\n      {label && (\n        <div\n          className={cn(\n            \"flex items-center justify-between gap-3 text-sm transition-opacity duration-150 ease-out\",\n            showBubble ? \"opacity-0\" : \"delay-75\",\n          )}\n        >\n          <label className=\"flex items-center gap-2 font-medium [&_svg]:size-4 [&_svg]:text-muted-foreground\" htmlFor={id}>\n            {icon}\n            {label}\n          </label>\n          <output className=\"font-mono text-muted-foreground text-xs tabular-nums\" htmlFor={id}>\n            {valueText}\n          </output>\n        </div>\n      )}\n      <div className=\"group/slider relative\" ref={trackRef} style={{ height: HIT }}>\n        <input\n          aria-label={label ? undefined : ariaLabel}\n          aria-valuetext={valueText}\n          className={cn(\n            \"absolute inset-0 z-10 m-0 w-full cursor-grab appearance-none bg-transparent opacity-0 outline-none active:cursor-grabbing disabled:cursor-not-allowed\",\n            \"[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:size-[22px] [&::-moz-range-thumb]:size-[22px] [&::-moz-range-thumb]:border-0\",\n          )}\n          disabled={disabled}\n          id={id}\n          max={max}\n          min={min}\n          name={name}\n          onBlur={() => setDragging(false)}\n          onChange={(event) => set(Number(event.currentTarget.value))}\n          onKeyDown={(event) => {\n            if (event.key.startsWith(\"Arrow\") || event.key === \"Home\" || event.key === \"End\" || event.key.startsWith(\"Page\")) nudge();\n          }}\n          onPointerCancel={() => setDragging(false)}\n          onPointerDown={() => setDragging(true)}\n          onPointerUp={() => setDragging(false)}\n          step={step}\n          type=\"range\"\n          value={value}\n        />\n        <span aria-hidden className=\"absolute inset-x-0 top-1/2 rounded-full bg-input\" style={{ height: BAR, marginTop: -BAR / 2 }} />\n        <svg aria-hidden className=\"absolute size-0\">\n          <filter colorInterpolationFilters=\"sRGB\" height=\"600%\" id={filterId} width=\"140%\" x=\"-20%\" y=\"-350%\">\n            <feGaussianBlur in=\"SourceGraphic\" result=\"blur\" stdDeviation=\"4\" />\n            <feColorMatrix in=\"blur\" values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 24 -10\" />\n          </filter>\n        </svg>\n        {width > 0 && (\n          <>\n            <span aria-hidden className=\"pointer-events-none absolute inset-0\" style={{ filter: `url(#${filterId})` }}>\n              {shapes(false)}\n            </span>\n            <span aria-hidden className=\"pointer-events-none absolute inset-0\">\n              {shapes(true)}\n            </span>\n            {bubble && (\n              <motion.span\n                aria-hidden\n                className=\"pointer-events-none absolute top-1/2 left-0 flex items-center justify-center rounded-full bg-(--liquid) font-medium text-(--liquid-ink) text-xs tabular-nums\"\n                style={{\n                  x: bubbleX,\n                  y: bubbleY,\n                  opacity: labelOpacity,\n                  width: bubbleW,\n                  height: BUBBLE_H,\n                  marginLeft: -bubbleW / 2,\n                  marginTop: -BUBBLE_H / 2,\n                }}\n              >\n                {valueText}\n              </motion.span>\n            )}\n          </>\n        )}\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}