{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slider-allocation-wire",
  "title": "Slider Allocation Wire",
  "description": "Two-way allocation slider built as a slack wire between two labeled anchors: a bead sliding along the wire sets the ratio while the wire's own sag encodes the unallocated remainder, so a taut straight wire means fully committed and a deep droop means budget left on the table.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/slider-allocation-wire/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\n// ---------------------------------------------------------------------------\n// CatenarySplit — a two-way allocation slider built as a slack wire between\n// two labeled anchors. The bead's position along the wire sets the RATIO\n// (left share vs right share); the wire's SAG encodes a second, independent\n// variable — the unallocated remainder — so a taut straight wire reads as\n// \"fully committed\" and a deep droop reads as \"budget still on the table\".\n// Mechanism: an SVG quadratic bezier whose control-point depth is\n// proportional to the unallocated fraction, rebuilt every frame; the bead is\n// positioned by genuine arc-length parameterization (sampled table, not raw\n// parametric t) so it slides evenly along whatever shape the wire currently\n// has. A wheel/pinch on the wire, or dragging the paired \"Total allocated\"\n// track, winches the total; easing toward a taut wire plays two decaying\n// tension oscillations when it lands fully committed. Direct-DOM rAF hot\n// path — React state only holds the committed integer values, per-frame\n// geometry lives in refs/locals and is written straight to the DOM.\n// ---------------------------------------------------------------------------\n\nconst VB_W = 400; // svg viewBox width (units, not px — scaled to the box)\nconst VB_H = 120;\nconst ANCHOR_X = 30; // inset of each anchor from the viewBox edges\nconst WIRE_Y = 46; // baseline y both anchors sit on\nconst MAX_DEPTH = 44; // control-point sag at fully unallocated (total = 0)\nconst SAMPLES = 48; // arc-length table resolution\n\nconst RATIO_K = 220; // keyboard/glide spring, s^-2 (critically-ish damped)\nconst RATIO_ZETA = 0.9;\nconst DEPTH_K = 130; // sag spring when NOT snapping taut\nconst DEPTH_ZETA = 1.0;\nconst TAUT_MS = 900; // scripted tension-wobble window when total hits 100\nconst TAUT_DECAY = 3.4;\nconst TAUT_CYCLES = 2; // exactly two damped oscillations\nconst SETTLE_MS = 1200; // forced-settle deadline for any pending motion\n\nfunction clamp(v: number, lo: number, hi: number) {\n  return Math.min(hi, Math.max(lo, v));\n}\n\ninterface Pt {\n  x: number;\n  y: number;\n}\n\nfunction quadPoint(p0: Pt, p1: Pt, p2: Pt, t: number): Pt {\n  const mt = 1 - t;\n  return {\n    x: mt * mt * p0.x + 2 * mt * t * p1.x + t * t * p2.x,\n    y: mt * mt * p0.y + 2 * mt * t * p1.y + t * t * p2.y,\n  };\n}\n\ninterface ArcTable {\n  xs: number[];\n  ys: number[];\n  lens: number[];\n  total: number;\n}\n\n// Samples the current quadratic bezier into an arc-length lookup table. Since\n// both anchors sit at the same y and the control point's x is always their\n// midpoint, x(t) is monotonic — the table doubles as an x -> arc-length map.\nfunction buildTable(p0: Pt, p1: Pt, p2: Pt): ArcTable {\n  const xs: number[] = [p0.x];\n  const ys: number[] = [p0.y];\n  const lens: number[] = [0];\n  let prevX = p0.x;\n  let prevY = p0.y;\n  let acc = 0;\n  for (let i = 1; i <= SAMPLES; i++) {\n    const { x, y } = quadPoint(p0, p1, p2, i / SAMPLES);\n    acc += Math.hypot(x - prevX, y - prevY);\n    xs.push(x);\n    ys.push(y);\n    lens.push(acc);\n    prevX = x;\n    prevY = y;\n  }\n  return { xs, ys, lens, total: Math.max(1e-6, acc) };\n}\n\n// Point at a given arc-length fraction (0..1) — genuine arc-length walk, not\n// a lerp on parametric t.\nfunction pointAtFraction(table: ArcTable, f: number): Pt {\n  const target = clamp(f, 0, 1) * table.total;\n  const { xs, ys, lens } = table;\n  for (let i = 1; i < lens.length; i++) {\n    if (lens[i] >= target) {\n      const span = lens[i] - lens[i - 1];\n      const u = span > 1e-6 ? (target - lens[i - 1]) / span : 0;\n      return {\n        x: xs[i - 1] + (xs[i] - xs[i - 1]) * u,\n        y: ys[i - 1] + (ys[i] - ys[i - 1]) * u,\n      };\n    }\n  }\n  const last = lens.length - 1;\n  return { x: xs[last], y: ys[last] };\n}\n\n// Inverse: arc-length fraction nearest a given x (binary search — x is\n// monotonic by construction). Used to map a pointer position on the wire\n// back to a ratio.\nfunction fractionForX(table: ArcTable, x: number): number {\n  const { xs, lens } = table;\n  const hi0 = xs.length - 1;\n  if (x <= xs[0]) return 0;\n  if (x >= xs[hi0]) return 1;\n  let lo = 0;\n  let hi = hi0;\n  while (hi - lo > 1) {\n    const mid = (lo + hi) >> 1;\n    if (xs[mid] < x) lo = mid;\n    else hi = mid;\n  }\n  const span = xs[hi] - xs[lo];\n  const u = span > 1e-6 ? (x - xs[lo]) / span : 0;\n  const len = lens[lo] + (lens[hi] - lens[lo]) * u;\n  return clamp(len / table.total, 0, 1);\n}\n\nexport interface CatenarySplitProps {\n  /** label for the left anchor, e.g. \"Compute\" */\n  leftLabel?: string;\n  /** label for the right anchor, e.g. \"Storage\" */\n  rightLabel?: string;\n  /** controlled split ratio, 0-100 (% of the committed total given to left) */\n  ratio?: number;\n  /** uncontrolled initial split ratio */\n  defaultRatio?: number;\n  /** controlled total committed, 0-100 (% of budget not left on the table) */\n  total?: number;\n  /** uncontrolled initial total committed */\n  defaultTotal?: number;\n  /** called with the new ratio on every drag */\n  onRatioChange?: (v: number) => void;\n  /** called with the new total on every drag */\n  onTotalChange?: (v: number) => void;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nexport function CatenarySplit({\n  leftLabel = \"Compute\",\n  rightLabel = \"Storage\",\n  ratio,\n  defaultRatio = 60,\n  total,\n  defaultTotal = 65,\n  onRatioChange,\n  onTotalChange,\n  className = \"\",\n}: CatenarySplitProps) {\n  const isRatioControlled = ratio !== undefined;\n  const [ratioInternal, setRatioInternal] = useState(() =>\n    clamp(defaultRatio, 0, 100)\n  );\n  const ratioValue = isRatioControlled ? (ratio as number) : ratioInternal;\n  const ratioRef = useRef(ratioValue);\n  ratioRef.current = ratioValue;\n\n  const isTotalControlled = total !== undefined;\n  const [totalInternal, setTotalInternal] = useState(() =>\n    clamp(defaultTotal, 0, 100)\n  );\n  const totalValue = isTotalControlled ? (total as number) : totalInternal;\n  const totalRef = useRef(totalValue);\n  totalRef.current = totalValue;\n\n  const [ratioActive, setRatioActive] = useState(false); // focus or drag\n  const [totalActive, setTotalActive] = useState(false);\n\n  const commitRatioRef = useRef<(v: number) => void>(() => {});\n  commitRatioRef.current = (v: number) => {\n    const q = clamp(Math.round(v), 0, 100);\n    if (q === ratioRef.current) return;\n    if (!isRatioControlled) setRatioInternal(q);\n    onRatioChange?.(q);\n  };\n  const commitTotalRef = useRef<(v: number) => void>(() => {});\n  commitTotalRef.current = (v: number) => {\n    const q = clamp(Math.round(v), 0, 100);\n    if (q === totalRef.current) return;\n    if (!isTotalControlled) setTotalInternal(q);\n    onTotalChange?.(q);\n  };\n\n  const wireBoxRef = useRef<HTMLDivElement>(null);\n  const pathRef = useRef<SVGPathElement>(null);\n  const beadRef = useRef<HTMLDivElement>(null);\n  const leftPctRef = useRef<HTMLSpanElement>(null);\n  const rightPctRef = useRef<HTMLSpanElement>(null);\n  const totalTrackRef = useRef<HTMLDivElement>(null);\n  const totalFillRef = useRef<HTMLDivElement>(null);\n  const totalHandleRef = useRef<HTMLDivElement>(null);\n  const totalCaptionRef = useRef<HTMLSpanElement>(null);\n\n  const engineRef = useRef<{\n    beadDragStart: (clientX: number) => void;\n    beadDragMove: (clientX: number) => void;\n    beadDragEnd: () => void;\n    totalDragStart: (clientX: number) => void;\n    totalDragMove: (clientX: number) => void;\n    totalDragEnd: () => void;\n    wheelTotal: (deltaPercent: number) => void;\n    syncRatio: (v: number) => void;\n    syncTotal: (v: number) => void;\n  } | null>(null);\n\n  useEffect(() => {\n    const wireBox = wireBoxRef.current;\n    const path = pathRef.current;\n    const bead = beadRef.current;\n    const leftPct = leftPctRef.current;\n    const rightPct = rightPctRef.current;\n    const totalTrack = totalTrackRef.current;\n    const totalFill = totalFillRef.current;\n    const totalHandle = totalHandleRef.current;\n    const totalCaption = totalCaptionRef.current;\n    if (\n      !wireBox ||\n      !path ||\n      !bead ||\n      !leftPct ||\n      !rightPct ||\n      !totalTrack ||\n      !totalFill ||\n      !totalHandle ||\n      !totalCaption\n    )\n      return;\n\n    const reduced = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\"\n    ).matches;\n\n    let raf = 0;\n    let last = 0;\n    let visible = true;\n    let scaleX = 1;\n    let scaleY = 1;\n    let sized = false;\n\n    // -- hot-path state ------------------------------------------------------\n    let aRatio = ratioRef.current; // animated ratio (0-100), what the bead reads\n    let rv = 0; // ratio velocity\n    let ratioTarget = ratioRef.current;\n    let draggingRatio = false;\n    let committedRatio = ratioRef.current; // dedupes onRatioChange during a drag\n\n    let aDepth = MAX_DEPTH * (1 - totalRef.current / 100);\n    let dv = 0; // depth velocity\n    let depthTarget = aDepth;\n    let tautAt = -1; // performance.now() when a taut-snap wobble started, -1 = none\n    let tautFrom = 0;\n    let draggingTotal = false;\n    // hot-path mirror of the committed total, for immediate render feedback —\n    // kept separate from totalRef (the React-state mirror) so drag/wheel\n    // handlers never race the commit function's own dedup check\n    let liveTotal = totalRef.current;\n\n    let deadline = 0;\n\n    const anchors = () => ({\n      p0: { x: ANCHOR_X, y: WIRE_Y },\n      p2: { x: VB_W - ANCHOR_X, y: WIRE_Y },\n    });\n\n    const tableFor = (depth: number) => {\n      const { p0, p2 } = anchors();\n      const p1 = { x: (p0.x + p2.x) / 2, y: WIRE_Y + depth };\n      return { table: buildTable(p0, p1, p2), p0, p1, p2 };\n    };\n\n    const render = () => {\n      const { table, p0, p1, p2 } = tableFor(aDepth);\n      path.setAttribute(\n        \"d\",\n        `M ${p0.x} ${p0.y} Q ${p1.x} ${p1.y} ${p2.x} ${p2.y}`\n      );\n      const bp = pointAtFraction(table, aRatio / 100);\n      bead.style.transform = `translate3d(${(bp.x * scaleX).toFixed(2)}px, ${(\n        bp.y * scaleY\n      ).toFixed(2)}px, 0) translate(-50%, -50%)`;\n\n      const leftRounded = Math.round(aRatio);\n      leftPct.textContent = `${leftRounded}%`;\n      rightPct.textContent = `${100 - leftRounded}%`;\n\n      const totalRounded = clamp(Math.round(liveTotal), 0, 100);\n      totalCaption.textContent = `${totalRounded}%`;\n      totalFill.style.width = `${totalRounded}%`;\n      totalHandle.style.left = `${totalRounded}%`;\n    };\n\n    const resize = () => {\n      const rect = wireBox.getBoundingClientRect();\n      if (rect.width < 40 || rect.height < 20) {\n        sized = false;\n        return;\n      }\n      scaleX = rect.width / VB_W;\n      scaleY = rect.height / VB_H;\n      sized = true;\n      render();\n    };\n    resize();\n\n    const loop = (now: number) => {\n      raf = 0;\n      const dt = last ? Math.min(0.033, (now - last) / 1000) : 1 / 60;\n      last = now;\n      let active = false;\n\n      // ratio: pinned to pointer while dragging, otherwise a plain glide\n      if (!draggingRatio) {\n        const c = 2 * RATIO_ZETA * Math.sqrt(RATIO_K);\n        rv += (-RATIO_K * (aRatio - ratioTarget) - c * rv) * dt;\n        aRatio += rv * dt;\n        if (Math.abs(aRatio - ratioTarget) < 0.05 && Math.abs(rv) < 0.5) {\n          aRatio = ratioTarget;\n          rv = 0;\n        } else active = true;\n      }\n\n      // depth: either the scripted taut-snap wobble, or a plain damped glide\n      if (tautAt >= 0) {\n        const p = clamp((now - tautAt) / TAUT_MS, 0, 1);\n        if (p >= 1) {\n          aDepth = 0;\n          dv = 0;\n          tautAt = -1;\n        } else {\n          aDepth =\n            tautFrom *\n            Math.exp(-TAUT_DECAY * p) *\n            Math.cos(2 * Math.PI * TAUT_CYCLES * p);\n          active = true;\n        }\n      } else {\n        // eases toward depthTarget on EVERY total-changing path, including a\n        // live drag on the paired track — winching reads as mechanical drag,\n        // never a jump cut, so the spring keeps running while draggingTotal\n        // is true instead of freezing until pointer-up\n        const c = 2 * DEPTH_ZETA * Math.sqrt(DEPTH_K);\n        dv += (-DEPTH_K * (aDepth - depthTarget) - c * dv) * dt;\n        aDepth += dv * dt;\n        if (Math.abs(aDepth - depthTarget) < 0.05 && Math.abs(dv) < 0.5) {\n          aDepth = depthTarget;\n          dv = 0;\n        } else active = true;\n      }\n\n      if (active && now > deadline) {\n        aRatio = ratioTarget;\n        rv = 0;\n        if (tautAt < 0) {\n          aDepth = depthTarget;\n          dv = 0;\n        }\n        active = false;\n      }\n\n      if (sized) render();\n      if (active && visible && !document.hidden) {\n        raf = requestAnimationFrame(loop);\n      }\n    };\n\n    const wake = () => {\n      if (!raf && sized) {\n        last = 0;\n        raf = requestAnimationFrame(loop);\n      }\n    };\n\n    const armDeadline = () => {\n      deadline = performance.now() + SETTLE_MS;\n    };\n\n    const setRatioTarget = (v: number) => {\n      ratioTarget = v;\n      if (reduced) {\n        aRatio = v;\n        rv = 0;\n        if (sized) render();\n        return;\n      }\n      armDeadline();\n      wake();\n    };\n\n    // Every path that changes the total (drag on the paired track, wheel/\n    // pinch on the wire, keyboard) routes through here so the sag always\n    // eases rather than teleporting — including while actively dragging the\n    // total track, so \"winching\" reads as mechanical, not instant. That is\n    // also what lets the taut-snap wobble fire on drag: the transition is\n    // detected the moment the eased depth's TARGET reaches zero, while the\n    // animated depth itself is still mid-glide and has real energy to decay.\n    const setTotalTarget = (v: number) => {\n      const wasFull = depthTarget <= 0.01;\n      depthTarget = MAX_DEPTH * (1 - v / 100);\n      const nowFull = depthTarget <= 0.01;\n      if (reduced) {\n        aDepth = depthTarget;\n        dv = 0;\n        tautAt = -1;\n        if (sized) render();\n        return;\n      }\n      if (nowFull && !wasFull && tautAt < 0) {\n        // landing on a fully-committed total plucks the wire taut — two\n        // decaying tension oscillations from wherever the sag currently is\n        tautFrom = aDepth;\n        tautAt = performance.now();\n      }\n      armDeadline();\n      wake();\n    };\n\n    // -- bead drag: arc-length projection of the pointer onto the CURRENT\n    // curve shape, so the bead genuinely rides whatever sag is on screen --\n    const beadDragStart = (clientX: number) => {\n      if (!sized) return;\n      draggingRatio = true;\n      beadDragMove(clientX);\n    };\n    const beadDragMove = (clientX: number) => {\n      if (!sized || !draggingRatio) return;\n      const rect = wireBox.getBoundingClientRect();\n      const x = (clientX - rect.left) / scaleX;\n      const { table } = tableFor(aDepth);\n      const f = fractionForX(table, x);\n      aRatio = f * 100;\n      rv = 0;\n      const q = clamp(Math.round(aRatio), 0, 100);\n      ratioTarget = q;\n      if (q !== committedRatio) {\n        committedRatio = q;\n        commitRatioRef.current(q);\n      }\n      armDeadline();\n      if (sized) render();\n    };\n    const beadDragEnd = () => {\n      if (!draggingRatio) return;\n      draggingRatio = false;\n      ratioTarget = clamp(Math.round(aRatio), 0, 100);\n      aRatio = ratioTarget;\n      armDeadline();\n      wake();\n    };\n\n    // -- total drag: linear along the paired track ---------------------------\n    const totalDragStart = (clientX: number) => {\n      if (!sized) return;\n      draggingTotal = true;\n      totalDragMove(clientX);\n    };\n    const totalDragMove = (clientX: number) => {\n      if (!draggingTotal) return;\n      const rect = totalTrack.getBoundingClientRect();\n      const f = rect.width > 0 ? (clientX - rect.left) / rect.width : 0;\n      const v = clamp(Math.round(f * 100), 0, 100);\n      liveTotal = v; // reflected immediately for the caption/fill/handle\n      commitTotalRef.current(v);\n      setTotalTarget(v); // eases like every other total-changing path\n    };\n    const totalDragEnd = () => {\n      if (!draggingTotal) return;\n      draggingTotal = false;\n      setTotalTarget(liveTotal); // idempotent — already the drag target\n    };\n\n    const wheelTotal = (deltaPercent: number) => {\n      const next = clamp(Math.round(liveTotal + deltaPercent), 0, 100);\n      if (next === liveTotal) return;\n      liveTotal = next;\n      commitTotalRef.current(next);\n      setTotalTarget(next);\n    };\n\n    // external/keyboard commits: glide to the new value unless that axis is\n    // mid-drag, in which case the pointer already owns it this frame\n    const syncRatio = (v: number) => {\n      if (draggingRatio) return;\n      committedRatio = v;\n      setRatioTarget(v);\n    };\n    const syncTotal = (v: number) => {\n      if (draggingTotal) return;\n      liveTotal = v;\n      setTotalTarget(v);\n    };\n\n    engineRef.current = {\n      beadDragStart,\n      beadDragMove,\n      beadDragEnd,\n      totalDragStart,\n      totalDragMove,\n      totalDragEnd,\n      wheelTotal,\n      syncRatio,\n      syncTotal,\n    };\n\n    const ro = new ResizeObserver(resize);\n    ro.observe(wireBox);\n    const io = new IntersectionObserver((entries) => {\n      visible = entries[0]?.isIntersecting ?? true;\n      if (visible) wake();\n    });\n    io.observe(wireBox);\n    const onVis = () => {\n      if (!document.hidden) wake();\n    };\n    document.addEventListener(\"visibilitychange\", onVis);\n\n    // React registers wheel listeners as passive by default, so a synthetic\n    // onWheel prop can never call preventDefault without throwing — bind a\n    // real non-passive listener instead so scroll/pinch on the wire can\n    // actually consume the gesture instead of also scrolling the page.\n    const onWheel = (e: WheelEvent) => {\n      e.preventDefault();\n      // trackpad pinch reports as a wheel event with ctrlKey set\n      const scale = e.ctrlKey ? 0.6 : 0.15;\n      engineRef.current?.wheelTotal(-e.deltaY * scale);\n    };\n    wireBox.addEventListener(\"wheel\", onWheel, { passive: false });\n\n    // keep the animated targets aligned with external/committed value changes\n    setRatioTarget(ratioRef.current);\n    setTotalTarget(totalRef.current);\n\n    return () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      io.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVis);\n      wireBox.removeEventListener(\"wheel\", onWheel);\n      engineRef.current = null;\n    };\n    // mount-only: geometry constants are module-level, external value syncs\n    // are handled by the effects below via engineRef.syncRatio/syncTotal\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  // external/keyboard ratio and total changes glide toward the new value\n  // (a live drag on that same axis already owns the frame and is skipped)\n  useEffect(() => {\n    engineRef.current?.syncRatio(ratioValue);\n  }, [ratioValue]);\n  useEffect(() => {\n    engineRef.current?.syncTotal(totalValue);\n  }, [totalValue]);\n\n  // -- keyboard: bead (\"Split\") ----------------------------------------------\n  const onBeadKeyDown = (e: React.KeyboardEvent) => {\n    const v = ratioRef.current;\n    let next: number | null = null;\n    switch (e.key) {\n      case \"ArrowRight\":\n      case \"ArrowUp\":\n        next = v + 1;\n        break;\n      case \"ArrowLeft\":\n      case \"ArrowDown\":\n        next = v - 1;\n        break;\n      case \"PageUp\":\n        next = v + 10;\n        break;\n      case \"PageDown\":\n        next = v - 10;\n        break;\n      case \"Home\":\n        next = 0;\n        break;\n      case \"End\":\n        next = 100;\n        break;\n      default:\n        return;\n    }\n    e.preventDefault();\n    next = clamp(next, 0, 100);\n    if (next !== v) commitRatioRef.current(next);\n  };\n\n  // -- keyboard: \"Total allocated\" -------------------------------------------\n  const onTotalKeyDown = (e: React.KeyboardEvent) => {\n    const v = totalRef.current;\n    let next: number | null = null;\n    switch (e.key) {\n      case \"ArrowRight\":\n      case \"ArrowUp\":\n        next = v + 1;\n        break;\n      case \"ArrowLeft\":\n      case \"ArrowDown\":\n        next = v - 1;\n        break;\n      case \"PageUp\":\n        next = v + 10;\n        break;\n      case \"PageDown\":\n        next = v - 10;\n        break;\n      case \"Home\":\n        next = 0;\n        break;\n      case \"End\":\n        next = 100;\n        break;\n      default:\n        return;\n    }\n    e.preventDefault();\n    next = clamp(next, 0, 100);\n    if (next !== v) commitTotalRef.current(next);\n  };\n\n  const leftPctNow = Math.round(ratioValue);\n  const rightPctNow = 100 - leftPctNow;\n  const totalNow = Math.round(totalValue);\n  const beadValueText = `${leftPctNow}% ${leftLabel.toLowerCase()}, ${rightPctNow}% ${rightLabel.toLowerCase()}`;\n  const totalValueText = `${totalNow}% of budget allocated`;\n\n  return (\n    <div className={`w-full select-none ${className}`}>\n      <div\n        ref={wireBoxRef}\n        className=\"group relative h-28 w-full touch-none\"\n        onPointerDown={(e) => {\n          e.preventDefault();\n          e.currentTarget.setPointerCapture(e.pointerId);\n          beadRef.current?.focus({ preventScroll: true });\n          engineRef.current?.beadDragStart(e.clientX);\n          setRatioActive(true);\n        }}\n        onPointerMove={(e) => engineRef.current?.beadDragMove(e.clientX)}\n        onPointerUp={() => {\n          engineRef.current?.beadDragEnd();\n          setRatioActive(false);\n        }}\n        onPointerCancel={() => {\n          engineRef.current?.beadDragEnd();\n          setRatioActive(false);\n        }}\n      >\n        <svg\n          aria-hidden\n          viewBox={`0 0 ${VB_W} ${VB_H}`}\n          preserveAspectRatio=\"none\"\n          className=\"absolute inset-0 h-full w-full cursor-ew-resize\"\n        >\n          <path\n            ref={pathRef}\n            fill=\"none\"\n            stroke=\"var(--ns-muted)\"\n            strokeWidth={1.5}\n            strokeLinecap=\"round\"\n          />\n          <circle cx={ANCHOR_X} cy={WIRE_Y} r={3} fill=\"var(--border)\" />\n          <circle\n            cx={VB_W - ANCHOR_X}\n            cy={WIRE_Y}\n            r={3}\n            fill=\"var(--border)\"\n          />\n        </svg>\n\n        <div\n          ref={beadRef}\n          role=\"slider\"\n          tabIndex={0}\n          aria-label=\"Split\"\n          aria-orientation=\"horizontal\"\n          aria-valuemin={0}\n          aria-valuemax={100}\n          aria-valuenow={leftPctNow}\n          aria-valuetext={beadValueText}\n          onKeyDown={onBeadKeyDown}\n          onFocus={() => setRatioActive(true)}\n          onBlur={() => setRatioActive(false)}\n          className=\"absolute left-0 top-0 h-7 w-7 cursor-grab rounded-full outline-none will-change-transform active:cursor-grabbing focus-visible:ring-2 focus-visible:ring-ns-accent focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n        >\n          <span\n            aria-hidden\n            data-active={ratioActive}\n            className=\"absolute inset-[3px] rounded-full border border-border bg-background transition-colors duration-150 group-hover:border-foreground/40 data-[active=true]:border-ns-accent\"\n          />\n          <span\n            aria-hidden\n            className=\"absolute left-1/2 top-1/2 h-2 w-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-foreground\"\n          />\n        </div>\n      </div>\n\n      <div className=\"flex items-start justify-between px-0.5 pt-1 font-mono\">\n        <div className=\"flex flex-col gap-0.5\">\n          <span className=\"text-xs text-ns-muted\">{leftLabel}</span>\n          <span\n            ref={leftPctRef}\n            className=\"tabular-nums text-sm text-foreground\"\n          >\n            {leftPctNow}%\n          </span>\n        </div>\n        <div className=\"flex flex-col items-end gap-0.5\">\n          <span className=\"text-xs text-ns-muted\">{rightLabel}</span>\n          <span\n            ref={rightPctRef}\n            className=\"tabular-nums text-sm text-foreground\"\n          >\n            {rightPctNow}%\n          </span>\n        </div>\n      </div>\n\n      <div className=\"mt-5\">\n        <div className=\"flex items-center justify-between pb-1.5\">\n          <span className=\"font-mono text-xs text-ns-muted\">\n            Total allocated\n          </span>\n          <span\n            ref={totalCaptionRef}\n            className=\"font-mono text-xs tabular-nums text-ns-muted\"\n          >\n            {totalNow}%\n          </span>\n        </div>\n        <div\n          ref={totalTrackRef}\n          role=\"slider\"\n          tabIndex={0}\n          aria-label=\"Total allocated\"\n          aria-orientation=\"horizontal\"\n          aria-valuemin={0}\n          aria-valuemax={100}\n          aria-valuenow={totalNow}\n          aria-valuetext={totalValueText}\n          onKeyDown={onTotalKeyDown}\n          onFocus={() => setTotalActive(true)}\n          onBlur={() => setTotalActive(false)}\n          onPointerDown={(e) => {\n            e.preventDefault();\n            e.currentTarget.setPointerCapture(e.pointerId);\n            engineRef.current?.totalDragStart(e.clientX);\n            setTotalActive(true);\n          }}\n          onPointerMove={(e) => engineRef.current?.totalDragMove(e.clientX)}\n          onPointerUp={() => {\n            engineRef.current?.totalDragEnd();\n            setTotalActive(false);\n          }}\n          onPointerCancel={() => {\n            engineRef.current?.totalDragEnd();\n            setTotalActive(false);\n          }}\n          className=\"relative h-4 w-full cursor-ew-resize touch-none outline-none focus-visible:ring-2 focus-visible:ring-ns-accent focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n        >\n          <span className=\"absolute left-0 top-1/2 h-1 w-full -translate-y-1/2 rounded-full bg-border\" />\n          <div\n            ref={totalFillRef}\n            className=\"absolute left-0 top-1/2 h-1 -translate-y-1/2 rounded-full bg-foreground/70\"\n            style={{ width: `${totalNow}%` }}\n          />\n          <div\n            ref={totalHandleRef}\n            data-active={totalActive}\n            style={{ left: `${totalNow}%` }}\n            className=\"absolute top-1/2 h-3 w-3 -translate-x-1/2 -translate-y-1/2 rounded-full border border-border bg-background transition-colors duration-150 data-[active=true]:border-ns-accent\"\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/slider-allocation-wire.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "slider",
      "allocation",
      "budget",
      "range",
      "svg",
      "bezier",
      "two-variable",
      "input",
      "form"
    ],
    "instruction": "Build a two-way allocation slider drawn as a slack wire strung between two labeled anchors (default 'Compute' / 'Storage', both overridable via leftLabel/rightLabel props). Two independent 0-100 values live in the geometry: `ratio` is the bead's position along the wire (share going to the left anchor) and `total` is how much of the budget is actually committed, expressed as the wire's SAG — total=100 pulls it dead straight, total=0 sags to a deep, clearly visible droop. RENDER: an SVG quadratic bezier (`M p0 Q control p2`, viewBox 400x120, preserveAspectRatio=none so it maps 1:1 to the container box) whose control point sits at the anchors' x-midpoint and a y-depth of `MAX_DEPTH * (1 - total/100)`, rebuilt every frame from a freshly-sampled 48-point arc-length lookup table (never a cached shape) so the curve, and everything derived from it, is always current. The bead is a real DOM node (not an SVG shape) layered on top, positioned every frame by walking that SAME arc-length table to the point at `ratio`% of total arc length — genuine arc-length parameterization, not a lerp on the bezier's raw parametric t, so the bead visibly slides evenly along whatever shape the wire currently has, speeding up/slowing down across the sag exactly like a bead on a real slack cord would. Stroke is `var(--ns-muted)`; the bead is a --border-ringed dot that swaps to --ns-accent only while focused or dragging (never at rest); two small --border anchor dots mark the wire's ends; the curve, dots, and wire are all `aria-hidden` — every bit of state lives in the two slider values, never in the drawing. INTERACTION: (1) dragging anywhere on the wire area projects the pointer's x onto the CURRENT curve's arc-length table to set the ratio directly, pinned 1:1, no lag; (2) a second, separate 'Total allocated' track below the anchors' numbers is a paired linear slider (drag it directly, same 0-100 range) that winches the total; (3) wheel or trackpad-pinch (pinch reports as a wheel event with ctrlKey set) anywhere on the wire ALSO winches the total, so the wire itself doubles as a scroll-to-commit control. Every total-changing path — the paired track's drag, wheel/pinch, and keyboard — eases the sag through the same damped spring (critically damped, ~130 s^-2) rather than teleporting, so 'winching' reads as mechanical drag, not a jump cut; when the eased sag's TARGET newly lands at zero (total reaches exactly 100, wire fully taut) the depth is handed off to a scripted decaying cosine — `depth(t) = fromDepth * e^(-3.4p) * cos(4*pi*p)` over 900ms, exactly two visible oscillations — so the wire visibly twangs taut instead of just stopping, then falls back to the normal spring. Loosening back off from 100 never wobbles, only landing on fully-committed does. A forced-settle deadline (1.2s) guarantees any pending glide or wobble always resolves. Per-anchor split numbers (Geist Mono, tabular-nums) sit directly beneath the wire and are read straight off the animated bead position every frame, so they visibly count as the bead (or the whole curve, during a total change) moves; a third mono readout shows the live total percentage above its paired track. ACCESSIBILITY: two independent role=slider controls, nothing else holds state. The bead: aria-label 'Split', aria-valuemin/max 0/100, aria-valuenow the left anchor's percentage, aria-valuetext like '60% compute, 40% storage' built from the actual anchor labels; ArrowLeft/Right (and Up/Down) step 1%, PageUp/PageDown step 10%, Home/End jump to 0/100. The paired track: aria-label 'Total allocated', same aria-valuemin/max/now/key model, aria-valuetext like '85% of budget allocated'. Both are real focusable elements (tabIndex 0) reachable by Tab in document order, both use `outline-none` paired ONLY with `focus-visible:ring-2 focus-visible:ring-ns-accent` (never `focus-visible:outline-*` on the same element) so the focus ring is never invisible. REDUCED MOTION: both springs and the taut-snap wobble are skipped entirely — ratio and total jump straight to their target every time — while dragging still tracks the pointer 1:1 exactly as before, so the component stays fully usable and legible with zero animation. PERF: direct-DOM rAF hot path — React state holds only the two committed integers (controlled/uncontrolled, each with its own onChange), all per-frame geometry (bead transform, path `d`, number text, track fill/handle position) is written straight to refs, never through React re-render; the loop sleeps at a velocity/position epsilon, is paused by an IntersectionObserver when offscreen and on document.hidden, and a ResizeObserver keeps the SVG-to-container scale factors current. No dependencies, no canvas — pure SVG + DOM + CSS, tokens only (--background --foreground --ns-muted --border --ns-accent), correct in both themes."
  },
  "type": "registry:ui"
}