{
  "name": "liquid-slider",
  "type": "registry:component",
  "dependencies": [],
  "files": [
    {
      "type": "registry:component",
      "content": "\"use client\";\n\n/**\n * @author: @dorianbaffier\n * @description: Liquid Slider\n * @version: 1.0.0\n * @date: 2026-10-14\n * @license: MIT\n * @website: https://kokonutui.com\n * @github: https://github.com/kokonut-labs/kokonutui\n */\n\nimport {\n  type KeyboardEvent,\n  type PointerEvent,\n  useCallback,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst WIDTH = 280;\nconst HEIGHT = 360;\n// The glass is a cylinder seen from a little above: its rim and floor are\n// ellipses RY tall, its walls run from LEFT to RIGHT.\nconst LEFT = 84;\nconst RIGHT = 196;\nconst CX = (LEFT + RIGHT) / 2;\nconst RADIUS = (RIGHT - LEFT) / 2;\nconst RY = 9;\nconst RIM_Y = 104;\nconst FLOOR_Y = 312;\n// Where the surface sits when the glass is empty and when it is full.\nconst EMPTY_Y = FLOOR_Y;\nconst FULL_Y = RIM_Y + 16;\n\n// The surface is a row of columns that each bob up and down and pull on\n// their neighbours: a 1D wave. Heights in px, time in seconds.\nconst COLUMNS = 34;\nconst STEP = 1 / 120;\nconst SPREAD = 2600;\nconst TENSION = 30;\nconst DAMPING = 2.4;\n// Neighbouring columns drag on each other's speed, which smooths out\n// the short, jagged ripples and leaves the long sloshes.\nconst VISCOSITY = 22;\nconst MAX_WAVE = 16;\n// While dragging, the surface under the finger is pulled towards it this\n// hard, and the finger's sideways speed piles the liquid up ahead of it.\nconst GRAB = 900;\nconst GRAB_REACH = 2.6;\nconst GRAB_MAX = 14;\nconst STIR = 0.55;\n// The level chases its target on a spring: fast, but not instant, so a quick\n// drag leaves the liquid behind the finger for a moment.\nconst LEVEL_STIFFNESS = 220;\nconst LEVEL_DAMPING = 26;\nconst MAX_BUBBLES = 14;\nconst REST_EPSILON = 0.02;\n\ninterface Bubble {\n  x: number;\n  y: number;\n  r: number;\n  speed: number;\n  phase: number;\n}\n\nfunction clamp(value: number, min: number, max: number) {\n  return Math.min(Math.max(value, min), max);\n}\n\nfunction haptic() {\n  if (typeof navigator !== \"undefined\" && \"vibrate\" in navigator) {\n    navigator.vibrate(4);\n  }\n}\n\n// How far in front of / behind the centre line the surface ellipse reaches at\n// each column, precomputed and rounded so server and browser markup match.\nconst COLUMN_X: number[] = [];\nconst DEPTH: number[] = [];\nfor (let i = 0; i < COLUMNS; i++) {\n  const t = i / (COLUMNS - 1);\n  const x = LEFT + t * (RIGHT - LEFT);\n  COLUMN_X.push(Math.round(x * 100) / 100);\n  const u = (x - CX) / RADIUS;\n  DEPTH.push(Math.round(RY * Math.sqrt(Math.max(0, 1 - u * u)) * 100) / 100);\n}\n\nfunction levelToY(level: number) {\n  return EMPTY_Y - level * (EMPTY_Y - FULL_Y);\n}\n\nfunction f(n: number) {\n  return n.toFixed(2);\n}\n\n/** The top of the liquid: from the back edge across, then the front back. */\nfunction surfacePath(levelY: number, heights: Float64Array) {\n  let d = \"\";\n  for (let i = 0; i < COLUMNS; i++) {\n    const y = Math.min(levelY + heights[i] - DEPTH[i], FLOOR_Y);\n    d += `${i === 0 ? \"M\" : \"L\"}${f(COLUMN_X[i])} ${f(y)}`;\n  }\n  for (let i = COLUMNS - 1; i >= 0; i--) {\n    const y = Math.min(levelY + heights[i] + DEPTH[i], FLOOR_Y + DEPTH[i]);\n    d += `L${f(COLUMN_X[i])} ${f(y)}`;\n  }\n  return `${d}Z`;\n}\n\n/** The body: the front edge of the surface, down the wall, along the floor. */\nfunction bodyPath(levelY: number, heights: Float64Array) {\n  let d = \"\";\n  for (let i = 0; i < COLUMNS; i++) {\n    const y = Math.min(levelY + heights[i] + DEPTH[i], FLOOR_Y + DEPTH[i]);\n    d += `${i === 0 ? \"M\" : \"L\"}${f(COLUMN_X[i])} ${f(y)}`;\n  }\n  d += `L${RIGHT} ${FLOOR_Y} A${RADIUS} ${RY} 0 0 1 ${LEFT} ${FLOOR_Y}Z`;\n  return d;\n}\n\n/** A thin line along the front lip, where light catches the surface. */\nfunction lipPath(levelY: number, heights: Float64Array) {\n  let d = \"\";\n  for (let i = 1; i < COLUMNS - 1; i++) {\n    const y = Math.min(levelY + heights[i] + DEPTH[i], FLOOR_Y + DEPTH[i]);\n    d += `${i === 1 ? \"M\" : \"L\"}${f(COLUMN_X[i])} ${f(y)}`;\n  }\n  return d;\n}\n\n/** Surface height (px from the level) at an x, between two columns. */\nfunction heightAt(heights: Float64Array, x: number) {\n  const t = clamp((x - LEFT) / (RIGHT - LEFT), 0, 1) * (COLUMNS - 1);\n  const i = Math.floor(t);\n  const j = Math.min(i + 1, COLUMNS - 1);\n  return heights[i] + (heights[j] - heights[i]) * (t - i);\n}\n\nconst FLAT = new Float64Array(COLUMNS);\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  /** Shown after the number. */\n  unit?: string;\n  label?: string;\n  /** Formats the readout and `aria-valuetext`. */\n  formatValue?: (value: number) => string;\n  className?: string;\n}\n\nconst defaultFormat = (value: number) => value.toLocaleString(\"en-US\");\n\nexport default function LiquidSlider({\n  value: valueProp,\n  defaultValue = 1250,\n  onValueChange,\n  min = 0,\n  max = 2000,\n  step = 50,\n  unit = \"ml\",\n  label = \"Water today\",\n  formatValue = defaultFormat,\n  className,\n}: LiquidSliderProps) {\n  const [internal, setInternal] = useState(defaultValue);\n  const value = clamp(valueProp ?? internal, min, max);\n  const [dragging, setDragging] = useState(false);\n  const labelId = useId();\n\n  const surfaceRef = useRef<SVGPathElement>(null);\n  const bodyRef = useRef<SVGPathElement>(null);\n  const lipRef = useRef<SVGPathElement>(null);\n  const bubblesRef = useRef<(SVGCircleElement | null)[]>([]);\n  const svgRef = useRef<SVGSVGElement>(null);\n\n  const fraction = (max - min > 0 ? (value - min) / (max - min) : 0) || 0;\n  const initialY = levelToY(fraction);\n\n  const sim = useRef({\n    heights: new Float64Array(COLUMNS),\n    speeds: new Float64Array(COLUMNS),\n    accels: new Float64Array(COLUMNS),\n    level: fraction,\n    levelSpeed: 0,\n    target: fraction,\n    bubbles: [] as Bubble[],\n    raf: 0,\n    last: 0,\n    carry: 0,\n    // Pointer, while held: where it is, in drawing units, and its sideways\n    // speed.\n    held: false,\n    px: 0,\n    py: 0,\n    pvx: 0,\n    lastPx: 0,\n    lastT: 0,\n    reduceMotion: false,\n    seed: 1,\n  });\n\n  // Handlers outlive the render they came from; read the props here.\n  const latest = useRef({ value, min, max, step, onValueChange });\n  latest.current = { value, min, max, step, onValueChange };\n\n  const commit = useCallback(\n    (next: number) => {\n      const { min: lo, max: hi, step: inc } = latest.current;\n      const snapped = clamp(\n        Number((Math.round((next - lo) / inc) * inc + lo).toFixed(10)),\n        lo,\n        hi\n      );\n      if (snapped === latest.current.value) {\n        return;\n      }\n      // A light tick each time the level crosses a quarter.\n      const quarter = (hi - lo) / 4;\n      if (\n        Math.floor((snapped - lo) / quarter) !==\n        Math.floor((latest.current.value - lo) / quarter)\n      ) {\n        haptic();\n      }\n      latest.current.value = snapped;\n      if (valueProp === undefined) {\n        setInternal(snapped);\n      }\n      latest.current.onValueChange?.(snapped);\n    },\n    [valueProp]\n  );\n\n  const random = useCallback(() => {\n    // Deterministic, so bubbles never depend on Math.random during render.\n    const s = sim.current;\n    s.seed = (s.seed * 16_807) % 2_147_483_647;\n    return s.seed / 2_147_483_647;\n  }, []);\n\n  const draw = useCallback(() => {\n    const s = sim.current;\n    const y = levelToY(s.level);\n    surfaceRef.current?.setAttribute(\"d\", surfacePath(y, s.heights));\n    bodyRef.current?.setAttribute(\"d\", bodyPath(y, s.heights));\n    lipRef.current?.setAttribute(\"d\", lipPath(y, s.heights));\n    for (let i = 0; i < MAX_BUBBLES; i++) {\n      const el = bubblesRef.current[i];\n      const b = s.bubbles[i];\n      if (!el) {\n        continue;\n      }\n      if (b) {\n        const wobble = Math.sin(b.phase) * 1.6;\n        el.setAttribute(\"cx\", f(b.x + wobble));\n        el.setAttribute(\"cy\", f(b.y));\n        el.setAttribute(\"r\", f(b.r));\n        el.style.opacity = \"1\";\n      } else {\n        el.style.opacity = \"0\";\n      }\n    }\n  }, []);\n\n  // The level rides a spring towards its target. When it rises fast, the\n  // pour drags bubbles in from the floor.\n  const stepLevel = useCallback(() => {\n    const s = sim.current;\n    const force =\n      (s.target - s.level) * LEVEL_STIFFNESS - s.levelSpeed * LEVEL_DAMPING;\n    s.levelSpeed += force * STEP;\n    s.level += s.levelSpeed * STEP;\n    if (\n      s.levelSpeed > 0.5 &&\n      s.bubbles.length < MAX_BUBBLES &&\n      random() < s.levelSpeed * 0.05\n    ) {\n      s.bubbles.push({\n        x: LEFT + 10 + random() * (RIGHT - LEFT - 20),\n        y: FLOOR_Y + 2,\n        r: 1.2 + random() * 2,\n        speed: 40 + random() * 50,\n        phase: random() * Math.PI * 2,\n      });\n    }\n  }, [random]);\n\n  // One step of the wave: each column accelerates towards its neighbours'\n  // average and back to rest, then the volume is re-levelled so the waves\n  // only move liquid around.\n  const stepWaves = useCallback(() => {\n    const s = sim.current;\n    const { heights: h, speeds: v, accels: a } = s;\n    const levelY = levelToY(s.level);\n    let mean = 0;\n    for (let i = 0; i < COLUMNS; i++) {\n      const l = i === 0 ? 0 : i - 1;\n      const r = i === COLUMNS - 1 ? i : i + 1;\n      let accel =\n        SPREAD * (h[l] + h[r] - 2 * h[i]) +\n        VISCOSITY * (v[l] + v[r] - 2 * v[i]) -\n        TENSION * h[i] -\n        DAMPING * v[i];\n      if (s.held) {\n        const reach = (COLUMN_X[i] - s.px) / ((RIGHT - LEFT) / COLUMNS);\n        const weight = Math.exp(-(reach * reach) / (GRAB_REACH * GRAB_REACH));\n        const pull = clamp(s.py - (levelY + h[i]), -GRAB_MAX, GRAB_MAX);\n        accel += GRAB * pull * weight * 0.05;\n        accel -= STIR * s.pvx * ((i - (COLUMNS - 1) / 2) / COLUMNS) * 60;\n      }\n      a[i] = accel;\n    }\n    for (let i = 0; i < COLUMNS; i++) {\n      v[i] += a[i] * STEP;\n      h[i] = clamp(h[i] + v[i] * STEP, -MAX_WAVE, MAX_WAVE);\n      mean += h[i];\n    }\n    mean /= COLUMNS;\n    let energy = 0;\n    for (let i = 0; i < COLUMNS; i++) {\n      h[i] -= mean;\n      energy += Math.abs(h[i]) + Math.abs(v[i]) * STEP;\n    }\n    return energy / COLUMNS;\n  }, []);\n\n  // Bubbles rise with a wobble and pop at the surface, leaving a ripple.\n  const stepBubbles = useCallback(() => {\n    const s = sim.current;\n    const levelY = levelToY(s.level);\n    for (let i = s.bubbles.length - 1; i >= 0; i--) {\n      const b = s.bubbles[i];\n      b.y -= b.speed * STEP;\n      b.phase += STEP * 9;\n      if (b.y <= levelY + heightAt(s.heights, b.x) + 1) {\n        const col = Math.round(((b.x - LEFT) / (RIGHT - LEFT)) * (COLUMNS - 1));\n        s.speeds[clamp(col, 0, COLUMNS - 1)] -= 30 * b.r;\n        s.bubbles.splice(i, 1);\n      }\n    }\n  }, []);\n\n  const tick = useCallback(\n    (now: number) => {\n      const s = sim.current;\n      s.raf = 0;\n      const elapsed = s.last ? Math.min((now - s.last) / 1000, 0.05) : STEP;\n      s.last = now;\n      s.carry += elapsed;\n      let energy = 0;\n      while (s.carry >= STEP) {\n        stepLevel();\n        energy = stepWaves();\n        stepBubbles();\n        s.carry -= STEP;\n      }\n      // The sideways speed fades when the pointer stops moving.\n      s.pvx *= 0.85;\n      draw();\n      const settled =\n        !s.held &&\n        energy < REST_EPSILON &&\n        Math.abs(s.target - s.level) < 0.0005 &&\n        Math.abs(s.levelSpeed) < 0.001 &&\n        s.bubbles.length === 0;\n      if (settled) {\n        s.level = s.target;\n        s.heights.fill(0);\n        s.speeds.fill(0);\n        s.last = 0;\n        draw();\n        return;\n      }\n      s.raf = requestAnimationFrame(tick);\n    },\n    [draw, stepBubbles, stepLevel, stepWaves]\n  );\n\n  const wake = useCallback(() => {\n    const s = sim.current;\n    if (s.reduceMotion) {\n      s.level = s.target;\n      s.levelSpeed = 0;\n      s.heights.fill(0);\n      s.speeds.fill(0);\n      s.bubbles.length = 0;\n      draw();\n      return;\n    }\n    if (!s.raf) {\n      s.raf = requestAnimationFrame(tick);\n    }\n  }, [draw, tick]);\n\n  useEffect(() => {\n    const s = sim.current;\n    const query = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    s.reduceMotion = query.matches;\n    const onChange = () => {\n      s.reduceMotion = query.matches;\n    };\n    query.addEventListener(\"change\", onChange);\n    // Pause while the tab is hidden; the loop picks up again on return.\n    const onVisibility = () => {\n      if (document.hidden && s.raf) {\n        cancelAnimationFrame(s.raf);\n        s.raf = 0;\n        s.last = 0;\n      } else if (!document.hidden) {\n        wake();\n      }\n    };\n    document.addEventListener(\"visibilitychange\", onVisibility);\n    return () => {\n      query.removeEventListener(\"change\", onChange);\n      document.removeEventListener(\"visibilitychange\", onVisibility);\n      cancelAnimationFrame(s.raf);\n      s.raf = 0;\n    };\n  }, [wake]);\n\n  // A value set from outside (a controlled prop, or a keypress) moves the\n  // target; the liquid follows on its own.\n  useEffect(() => {\n    const s = sim.current;\n    if (s.held) {\n      return;\n    }\n    if (Math.abs(s.target - fraction) > 1e-6) {\n      s.target = fraction;\n      wake();\n    }\n  }, [fraction, wake]);\n\n  const localPoint = (event: PointerEvent) => {\n    const rect = svgRef.current?.getBoundingClientRect();\n    if (!rect) {\n      return { x: 0, y: 0 };\n    }\n    return {\n      x: ((event.clientX - rect.left) / rect.width) * WIDTH,\n      y: ((event.clientY - rect.top) / rect.height) * HEIGHT,\n    };\n  };\n\n  const follow = (event: PointerEvent) => {\n    const s = sim.current;\n    const point = localPoint(event);\n    const now = event.timeStamp;\n    if (s.lastT && now > s.lastT) {\n      const vx = ((point.x - s.lastPx) / (now - s.lastT)) * 1000;\n      s.pvx = s.pvx * 0.6 + clamp(vx, -900, 900) * 0.4;\n    }\n    s.lastPx = point.x;\n    s.lastT = now;\n    s.px = clamp(point.x, LEFT, RIGHT);\n    s.py = clamp(point.y, FULL_Y, EMPTY_Y);\n    const raw = clamp((EMPTY_Y - point.y) / (EMPTY_Y - FULL_Y), 0, 1);\n    s.target = raw;\n    const { min: lo, max: hi } = latest.current;\n    commit(lo + raw * (hi - lo));\n    wake();\n  };\n\n  const handlePointerDown = (event: PointerEvent<SVGSVGElement>) => {\n    if (event.pointerType === \"mouse\" && event.button !== 0) {\n      return;\n    }\n    event.preventDefault();\n    // Focus without scrolling, so the keyboard picks up where the hand left.\n    (event.currentTarget.parentElement as HTMLElement | null)?.focus({\n      preventScroll: true,\n    });\n    event.currentTarget.setPointerCapture(event.pointerId);\n    const s = sim.current;\n    s.held = true;\n    s.lastT = 0;\n    s.pvx = 0;\n    setDragging(true);\n    follow(event);\n  };\n\n  const handlePointerMove = (event: PointerEvent<SVGSVGElement>) => {\n    if (sim.current.held) {\n      follow(event);\n    }\n  };\n\n  const handlePointerUp = () => {\n    const s = sim.current;\n    if (!s.held) {\n      return;\n    }\n    s.held = false;\n    setDragging(false);\n    // Settle on the step the readout shows.\n    const { value: v, min: lo, max: hi } = latest.current;\n    s.target = hi - lo > 0 ? (v - lo) / (hi - lo) : 0;\n    wake();\n  };\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    const { min: lo, max: hi, step: inc } = latest.current;\n    const big = Math.max(inc, (hi - lo) / 10);\n    const moves: Record<string, number> = {\n      ArrowUp: value + inc,\n      ArrowRight: value + inc,\n      ArrowDown: value - inc,\n      ArrowLeft: value - inc,\n      PageUp: value + big,\n      PageDown: value - big,\n      Home: lo,\n      End: hi,\n    };\n    const next = Object.hasOwn(moves, event.key) ? moves[event.key] : null;\n    if (next === null) {\n      return;\n    }\n    event.preventDefault();\n    commit(next);\n    // A drop in the middle, so the key gets a ripple too.\n    const s = sim.current;\n    if (!s.reduceMotion) {\n      s.speeds[Math.floor(COLUMNS / 2)] += next > value ? -90 : 90;\n      wake();\n    }\n  };\n\n  const ticks = [0, 0.25, 0.5, 0.75, 1];\n  const initialSurface = surfacePath(initialY, FLAT);\n  const initialBody = bodyPath(initialY, FLAT);\n  const initialLip = lipPath(initialY, FLAT);\n\n  return (\n    <div\n      aria-labelledby={labelId}\n      aria-orientation=\"vertical\"\n      aria-valuemax={max}\n      aria-valuemin={min}\n      aria-valuenow={value}\n      aria-valuetext={`${formatValue(value)} ${unit}`.trim()}\n      className={cn(\n        \"relative select-none overflow-hidden rounded-3xl border border-zinc-200 bg-white text-zinc-900 outline-none focus-visible:outline-2 focus-visible:outline-zinc-400 focus-visible:outline-offset-2 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100\",\n        className\n      )}\n      onKeyDown={handleKeyDown}\n      role=\"slider\"\n      style={{ width: WIDTH, height: HEIGHT }}\n      tabIndex={0}\n    >\n      <div className=\"pointer-events-none absolute inset-x-0 top-0 p-6\">\n        <p className=\"text-sm text-zinc-500\" id={labelId}>\n          {label}\n        </p>\n        <p className=\"mt-0.5 font-medium text-2xl tabular-nums tracking-tight\">\n          {formatValue(value)}\n          <span className=\"ml-1 text-base text-zinc-400\">{unit}</span>\n        </p>\n      </div>\n\n      <svg\n        aria-hidden=\"true\"\n        className={cn(\n          \"absolute inset-0 touch-none\",\n          dragging ? \"cursor-grabbing\" : \"cursor-grab\"\n        )}\n        height={HEIGHT}\n        onLostPointerCapture={handlePointerUp}\n        onPointerCancel={handlePointerUp}\n        onPointerDown={handlePointerDown}\n        onPointerMove={handlePointerMove}\n        onPointerUp={handlePointerUp}\n        ref={svgRef}\n        viewBox={`0 0 ${WIDTH} ${HEIGHT}`}\n        width={WIDTH}\n      >\n        <defs>\n          <clipPath id={`${labelId}-glass`}>\n            <path\n              d={`M${LEFT} ${RIM_Y} L${RIGHT} ${RIM_Y} L${RIGHT} ${FLOOR_Y} A${RADIUS} ${RY} 0 0 1 ${LEFT} ${FLOOR_Y} Z`}\n            />\n          </clipPath>\n        </defs>\n\n        {/* The back of the glass. */}\n        <path\n          className=\"fill-zinc-50 dark:fill-zinc-900/60\"\n          d={`M${LEFT} ${RIM_Y} A${RADIUS} ${RY} 0 0 1 ${RIGHT} ${RIM_Y} L${RIGHT} ${FLOOR_Y} A${RADIUS} ${RY} 0 0 1 ${LEFT} ${FLOOR_Y} Z`}\n        />\n        <path\n          className=\"fill-none stroke-zinc-200 dark:stroke-zinc-800\"\n          d={`M${LEFT} ${FLOOR_Y} A${RADIUS} ${RY} 0 0 1 ${RIGHT} ${FLOOR_Y}`}\n          strokeWidth={1}\n        />\n\n        <g clipPath={`url(#${labelId}-glass)`}>\n          <path className=\"fill-sky-500\" d={initialBody} ref={bodyRef} />\n          {Array.from({ length: MAX_BUBBLES }, (_, i) => (\n            <circle\n              className=\"fill-white/25 stroke-white/70\"\n              cx={CX}\n              cy={FLOOR_Y}\n              // biome-ignore lint/suspicious/noArrayIndexKey: a fixed pool of circles reused by index\n              key={i}\n              r={1}\n              ref={(el) => {\n                bubblesRef.current[i] = el;\n              }}\n              strokeWidth={0.75}\n              style={{ opacity: 0 }}\n            />\n          ))}\n          <path\n            className=\"fill-sky-300 dark:fill-sky-400\"\n            d={initialSurface}\n            ref={surfaceRef}\n          />\n          <path\n            className=\"fill-none stroke-white/60\"\n            d={initialLip}\n            ref={lipRef}\n            strokeLinecap=\"round\"\n            strokeWidth={1}\n          />\n        </g>\n\n        {/* Graduations, printed on the front of the glass. */}\n        {ticks.map((t) => {\n          const y = Math.round(levelToY(t) + RY * 0.55);\n          const tickValue = min + t * (max - min);\n          return (\n            <g key={t}>\n              <line\n                className=\"stroke-zinc-900/25 dark:stroke-white/30\"\n                strokeWidth={1}\n                x1={RIGHT - 14}\n                x2={RIGHT - 4}\n                y1={y}\n                y2={y}\n              />\n              <text\n                className=\"fill-zinc-400 text-[10px] tabular-nums dark:fill-zinc-500\"\n                dominantBaseline=\"middle\"\n                x={RIGHT + 8}\n                y={y}\n              >\n                {formatValue(tickValue)}\n              </text>\n            </g>\n          );\n        })}\n\n        {/* The glass itself: rim, walls and the front of the floor. */}\n        <ellipse\n          className=\"fill-none stroke-zinc-300 dark:stroke-zinc-700\"\n          cx={CX}\n          cy={RIM_Y}\n          rx={RADIUS}\n          ry={RY}\n          strokeWidth={1.5}\n        />\n        <path\n          className=\"fill-none stroke-zinc-300 dark:stroke-zinc-700\"\n          d={`M${LEFT} ${RIM_Y} L${LEFT} ${FLOOR_Y} A${RADIUS} ${RY} 0 0 0 ${RIGHT} ${FLOOR_Y} L${RIGHT} ${RIM_Y}`}\n          strokeWidth={1.5}\n        />\n        {/* A strip of reflection down the left wall. */}\n        <line\n          className=\"stroke-white/50 dark:stroke-white/10\"\n          strokeLinecap=\"round\"\n          strokeWidth={3}\n          x1={LEFT + 9}\n          x2={LEFT + 9}\n          y1={RIM_Y + 22}\n          y2={FLOOR_Y - 14}\n        />\n      </svg>\n    </div>\n  );\n}\n",
      "path": "/components/kokonutui/liquid-slider.tsx",
      "target": "components/kokonutui/liquid-slider.tsx"
    }
  ]
}