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.
npx shadcn add https://design.helpmarq.com /r/slider-allocation-wire.jsonregistry/core/slider-allocation-wire/component.tsx"use client";
import { useEffect, useRef, useState } from "react";
// ---------------------------------------------------------------------------
// CatenarySplit — a two-way allocation slider built as a slack wire between
// two labeled anchors. The bead's position along the wire sets the RATIO
// (left share vs right share); the wire's SAG encodes a second, independent
// variable — the unallocated remainder — so a taut straight wire reads as
// "fully committed" and a deep droop reads as "budget still on the table".
// Mechanism: an SVG quadratic bezier whose control-point depth is
// proportional to the unallocated fraction, rebuilt every frame; the bead is
// positioned by genuine arc-length parameterization (sampled table, not raw
// parametric t) so it slides evenly along whatever shape the wire currently
// has. A wheel/pinch on the wire, or dragging the paired "Total allocated"
// track, winches the total; easing toward a taut wire plays two decaying
// tension oscillations when it lands fully committed. Direct-DOM rAF hot
// path — React state only holds the committed integer values, per-frame
// geometry lives in refs/locals and is written straight to the DOM.
// ---------------------------------------------------------------------------
const VB_W = 400; // svg viewBox width (units, not px — scaled to the box)
const VB_H = 120;
const ANCHOR_X = 30; // inset of each anchor from the viewBox edges
const WIRE_Y = 46; // baseline y both anchors sit on
const MAX_DEPTH = 44; // control-point sag at fully unallocated (total = 0)
const SAMPLES = 48; // arc-length table resolution
const RATIO_K = 220; // keyboard/glide spring, s^-2 (critically-ish damped)
const RATIO_ZETA = 0.9;
const DEPTH_K = 130; // sag spring when NOT snapping taut
const DEPTH_ZETA = 1.0;
const TAUT_MS = 900; // scripted tension-wobble window when total hits 100
const TAUT_DECAY = 3.4;
const TAUT_CYCLES = 2; // exactly two damped oscillations
const SETTLE_MS = 1200; // forced-settle deadline for any pending motion
function clamp(v: number, lo: number, hi: number) {
return Math.min(hi, Math.max(lo, v));
}
interface Pt {
x: number;
y: number;
}
function quadPoint(p0: Pt, p1: Pt, p2: Pt, t: number): Pt {
const mt = 1 - t;
return {
x: mt * mt * p0.x + 2 * mt * t * p1.x + t * t * p2.x,
y: mt * mt * p0.y + 2 * mt * t * p1.y + t * t * p2.y,
};
}
interface ArcTable {
xs: number[];
ys: number[];
lens: number[];
total: number;
}
// Samples the current quadratic bezier into an arc-length lookup table. Since
// both anchors sit at the same y and the control point's x is always their
// midpoint, x(t) is monotonic — the table doubles as an x -> arc-length map.
function buildTable(p0: Pt, p1: Pt, p2: Pt): ArcTable {
const xs: number[] = [p0.x];
const ys: number[] = [p0.y];
const lens: number[] = [0];
let prevX = p0.x;
let prevY = p0.y;
let acc = 0;
for (let i = 1; i <= SAMPLES; i++) {
const { x, y } = quadPoint(p0, p1, p2, i / SAMPLES);
acc += Math.hypot(x - prevX, y - prevY);
xs.push(x);
ys.push(y);
lens.push(acc);
prevX = x;
prevY = y;
}
return { xs, ys, lens, total: Math.max(1e-6, acc) };
}
// Point at a given arc-length fraction (0..1) — genuine arc-length walk, not
// a lerp on parametric t.
function pointAtFraction(table: ArcTable, f: number): Pt {
const target = clamp(f, 0, 1) * table.total;
const { xs, ys, lens } = table;
for (let i = 1; i < lens.length; i++) {
if (lens[i] >= target) {
const span = lens[i] - lens[i - 1];
const u = span > 1e-6 ? (target - lens[i - 1]) / span : 0;
return {
x: xs[i - 1] + (xs[i] - xs[i - 1]) * u,
y: ys[i - 1] + (ys[i] - ys[i - 1]) * u,
};
}
}
const last = lens.length - 1;
return { x: xs[last], y: ys[last] };
}
// Inverse: arc-length fraction nearest a given x (binary search — x is
// monotonic by construction). Used to map a pointer position on the wire
// back to a ratio.
function fractionForX(table: ArcTable, x: number): number {
const { xs, lens } = table;
const hi0 = xs.length - 1;
if (x <= xs[0]) return 0;
if (x >= xs[hi0]) return 1;
let lo = 0;
let hi = hi0;
while (hi - lo > 1) {
const mid = (lo + hi) >> 1;
if (xs[mid] < x) lo = mid;
else hi = mid;
}
const span = xs[hi] - xs[lo];
const u = span > 1e-6 ? (x - xs[lo]) / span : 0;
const len = lens[lo] + (lens[hi] - lens[lo]) * u;
return clamp(len / table.total, 0, 1);
}
export interface CatenarySplitProps {
/** label for the left anchor, e.g. "Compute" */
leftLabel?: string;
/** label for the right anchor, e.g. "Storage" */
rightLabel?: string;
/** controlled split ratio, 0-100 (% of the committed total given to left) */
ratio?: number;
defaultRatio?: number;
/** controlled total committed, 0-100 (% of budget not left on the table) */
total?: number;
defaultTotal?: number;
onRatioChange?: (v: number) => void;
onTotalChange?: (v: number) => void;
className?: string;
}
export function CatenarySplit({
leftLabel = "Compute",
rightLabel = "Storage",
ratio,
defaultRatio = 60,
total,
defaultTotal = 65,
onRatioChange,
onTotalChange,
className = "",
}: CatenarySplitProps) {
const isRatioControlled = ratio !== undefined;
const [ratioInternal, setRatioInternal] = useState(() =>
clamp(defaultRatio, 0, 100)
);
const ratioValue = isRatioControlled ? (ratio as number) : ratioInternal;
const ratioRef = useRef(ratioValue);
ratioRef.current = ratioValue;
const isTotalControlled = total !== undefined;
const [totalInternal, setTotalInternal] = useState(() =>
clamp(defaultTotal, 0, 100)
);
const totalValue = isTotalControlled ? (total as number) : totalInternal;
const totalRef = useRef(totalValue);
totalRef.current = totalValue;
const [ratioActive, setRatioActive] = useState(false); // focus or drag
const [totalActive, setTotalActive] = useState(false);
const commitRatioRef = useRef<(v: number) => void>(() => {});
commitRatioRef.current = (v: number) => {
const q = clamp(Math.round(v), 0, 100);
if (q === ratioRef.current) return;
if (!isRatioControlled) setRatioInternal(q);
onRatioChange?.(q);
};
const commitTotalRef = useRef<(v: number) => void>(() => {});
commitTotalRef.current = (v: number) => {
const q = clamp(Math.round(v), 0, 100);
if (q === totalRef.current) return;
if (!isTotalControlled) setTotalInternal(q);
onTotalChange?.(q);
};
const wireBoxRef = useRef<HTMLDivElement>(null);
const pathRef = useRef<SVGPathElement>(null);
const beadRef = useRef<HTMLDivElement>(null);
const leftPctRef = useRef<HTMLSpanElement>(null);
const rightPctRef = useRef<HTMLSpanElement>(null);
const totalTrackRef = useRef<HTMLDivElement>(null);
const totalFillRef = useRef<HTMLDivElement>(null);
const totalHandleRef = useRef<HTMLDivElement>(null);
const totalCaptionRef = useRef<HTMLSpanElement>(null);
const engineRef = useRef<{
beadDragStart: (clientX: number) => void;
beadDragMove: (clientX: number) => void;
beadDragEnd: () => void;
totalDragStart: (clientX: number) => void;
totalDragMove: (clientX: number) => void;
totalDragEnd: () => void;
wheelTotal: (deltaPercent: number) => void;
syncRatio: (v: number) => void;
syncTotal: (v: number) => void;
} | null>(null);
useEffect(() => {
const wireBox = wireBoxRef.current;
const path = pathRef.current;
const bead = beadRef.current;
const leftPct = leftPctRef.current;
const rightPct = rightPctRef.current;
const totalTrack = totalTrackRef.current;
const totalFill = totalFillRef.current;
const totalHandle = totalHandleRef.current;
const totalCaption = totalCaptionRef.current;
if (
!wireBox ||
!path ||
!bead ||
!leftPct ||
!rightPct ||
!totalTrack ||
!totalFill ||
!totalHandle ||
!totalCaption
)
return;
const reduced = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
let raf = 0;
let last = 0;
let visible = true;
let scaleX = 1;
let scaleY = 1;
let sized = false;
// -- hot-path state ------------------------------------------------------
let aRatio = ratioRef.current; // animated ratio (0-100), what the bead reads
let rv = 0; // ratio velocity
let ratioTarget = ratioRef.current;
let draggingRatio = false;
let committedRatio = ratioRef.current; // dedupes onRatioChange during a drag
let aDepth = MAX_DEPTH * (1 - totalRef.current / 100);
let dv = 0; // depth velocity
let depthTarget = aDepth;
let tautAt = -1; // performance.now() when a taut-snap wobble started, -1 = none
let tautFrom = 0;
let draggingTotal = false;
// hot-path mirror of the committed total, for immediate render feedback —
// kept separate from totalRef (the React-state mirror) so drag/wheel
// handlers never race the commit function's own dedup check
let liveTotal = totalRef.current;
let deadline = 0;
const anchors = () => ({
p0: { x: ANCHOR_X, y: WIRE_Y },
p2: { x: VB_W - ANCHOR_X, y: WIRE_Y },
});
const tableFor = (depth: number) => {
const { p0, p2 } = anchors();
const p1 = { x: (p0.x + p2.x) / 2, y: WIRE_Y + depth };
return { table: buildTable(p0, p1, p2), p0, p1, p2 };
};
const render = () => {
const { table, p0, p1, p2 } = tableFor(aDepth);
path.setAttribute(
"d",
`M ${p0.x} ${p0.y} Q ${p1.x} ${p1.y} ${p2.x} ${p2.y}`
);
const bp = pointAtFraction(table, aRatio / 100);
bead.style.transform = `translate3d(${(bp.x * scaleX).toFixed(2)}px, ${(
bp.y * scaleY
).toFixed(2)}px, 0) translate(-50%, -50%)`;
const leftRounded = Math.round(aRatio);
leftPct.textContent = `${leftRounded}%`;
rightPct.textContent = `${100 - leftRounded}%`;
const totalRounded = clamp(Math.round(liveTotal), 0, 100);
totalCaption.textContent = `${totalRounded}%`;
totalFill.style.width = `${totalRounded}%`;
totalHandle.style.left = `${totalRounded}%`;
};
const resize = () => {
const rect = wireBox.getBoundingClientRect();
if (rect.width < 40 || rect.height < 20) {
sized = false;
return;
}
scaleX = rect.width / VB_W;
scaleY = rect.height / VB_H;
sized = true;
render();
};
resize();
const loop = (now: number) => {
raf = 0;
const dt = last ? Math.min(0.033, (now - last) / 1000) : 1 / 60;
last = now;
let active = false;
// ratio: pinned to pointer while dragging, otherwise a plain glide
if (!draggingRatio) {
const c = 2 * RATIO_ZETA * Math.sqrt(RATIO_K);
rv += (-RATIO_K * (aRatio - ratioTarget) - c * rv) * dt;
aRatio += rv * dt;
if (Math.abs(aRatio - ratioTarget) < 0.05 && Math.abs(rv) < 0.5) {
aRatio = ratioTarget;
rv = 0;
} else active = true;
}
// depth: either the scripted taut-snap wobble, or a plain damped glide
if (tautAt >= 0) {
const p = clamp((now - tautAt) / TAUT_MS, 0, 1);
if (p >= 1) {
aDepth = 0;
dv = 0;
tautAt = -1;
} else {
aDepth =
tautFrom *
Math.exp(-TAUT_DECAY * p) *
Math.cos(2 * Math.PI * TAUT_CYCLES * p);
active = true;
}
} else {
// eases toward depthTarget on EVERY total-changing path, including a
// live drag on the paired track — winching reads as mechanical drag,
// never a jump cut, so the spring keeps running while draggingTotal
// is true instead of freezing until pointer-up
const c = 2 * DEPTH_ZETA * Math.sqrt(DEPTH_K);
dv += (-DEPTH_K * (aDepth - depthTarget) - c * dv) * dt;
aDepth += dv * dt;
if (Math.abs(aDepth - depthTarget) < 0.05 && Math.abs(dv) < 0.5) {
aDepth = depthTarget;
dv = 0;
} else active = true;
}
if (active && now > deadline) {
aRatio = ratioTarget;
rv = 0;
if (tautAt < 0) {
aDepth = depthTarget;
dv = 0;
}
active = false;
}
if (sized) render();
if (active && visible && !document.hidden) {
raf = requestAnimationFrame(loop);
}
};
const wake = () => {
if (!raf && sized) {
last = 0;
raf = requestAnimationFrame(loop);
}
};
const armDeadline = () => {
deadline = performance.now() + SETTLE_MS;
};
const setRatioTarget = (v: number) => {
ratioTarget = v;
if (reduced) {
aRatio = v;
rv = 0;
if (sized) render();
return;
}
armDeadline();
wake();
};
// Every path that changes the total (drag on the paired track, wheel/
// pinch on the wire, keyboard) routes through here so the sag always
// eases rather than teleporting — including while actively dragging the
// total track, so "winching" reads as mechanical, not instant. That is
// also what lets the taut-snap wobble fire on drag: the transition is
// detected the moment the eased depth's TARGET reaches zero, while the
// animated depth itself is still mid-glide and has real energy to decay.
const setTotalTarget = (v: number) => {
const wasFull = depthTarget <= 0.01;
depthTarget = MAX_DEPTH * (1 - v / 100);
const nowFull = depthTarget <= 0.01;
if (reduced) {
aDepth = depthTarget;
dv = 0;
tautAt = -1;
if (sized) render();
return;
}
if (nowFull && !wasFull && tautAt < 0) {
// landing on a fully-committed total plucks the wire taut — two
// decaying tension oscillations from wherever the sag currently is
tautFrom = aDepth;
tautAt = performance.now();
}
armDeadline();
wake();
};
// -- bead drag: arc-length projection of the pointer onto the CURRENT
// curve shape, so the bead genuinely rides whatever sag is on screen --
const beadDragStart = (clientX: number) => {
if (!sized) return;
draggingRatio = true;
beadDragMove(clientX);
};
const beadDragMove = (clientX: number) => {
if (!sized || !draggingRatio) return;
const rect = wireBox.getBoundingClientRect();
const x = (clientX - rect.left) / scaleX;
const { table } = tableFor(aDepth);
const f = fractionForX(table, x);
aRatio = f * 100;
rv = 0;
const q = clamp(Math.round(aRatio), 0, 100);
ratioTarget = q;
if (q !== committedRatio) {
committedRatio = q;
commitRatioRef.current(q);
}
armDeadline();
if (sized) render();
};
const beadDragEnd = () => {
if (!draggingRatio) return;
draggingRatio = false;
ratioTarget = clamp(Math.round(aRatio), 0, 100);
aRatio = ratioTarget;
armDeadline();
wake();
};
// -- total drag: linear along the paired track ---------------------------
const totalDragStart = (clientX: number) => {
if (!sized) return;
draggingTotal = true;
totalDragMove(clientX);
};
const totalDragMove = (clientX: number) => {
if (!draggingTotal) return;
const rect = totalTrack.getBoundingClientRect();
const f = rect.width > 0 ? (clientX - rect.left) / rect.width : 0;
const v = clamp(Math.round(f * 100), 0, 100);
liveTotal = v; // reflected immediately for the caption/fill/handle
commitTotalRef.current(v);
setTotalTarget(v); // eases like every other total-changing path
};
const totalDragEnd = () => {
if (!draggingTotal) return;
draggingTotal = false;
setTotalTarget(liveTotal); // idempotent — already the drag target
};
const wheelTotal = (deltaPercent: number) => {
const next = clamp(Math.round(liveTotal + deltaPercent), 0, 100);
if (next === liveTotal) return;
liveTotal = next;
commitTotalRef.current(next);
setTotalTarget(next);
};
// external/keyboard commits: glide to the new value unless that axis is
// mid-drag, in which case the pointer already owns it this frame
const syncRatio = (v: number) => {
if (draggingRatio) return;
committedRatio = v;
setRatioTarget(v);
};
const syncTotal = (v: number) => {
if (draggingTotal) return;
liveTotal = v;
setTotalTarget(v);
};
engineRef.current = {
beadDragStart,
beadDragMove,
beadDragEnd,
totalDragStart,
totalDragMove,
totalDragEnd,
wheelTotal,
syncRatio,
syncTotal,
};
const ro = new ResizeObserver(resize);
ro.observe(wireBox);
const io = new IntersectionObserver((entries) => {
visible = entries[0]?.isIntersecting ?? true;
if (visible) wake();
});
io.observe(wireBox);
const onVis = () => {
if (!document.hidden) wake();
};
document.addEventListener("visibilitychange", onVis);
// React registers wheel listeners as passive by default, so a synthetic
// onWheel prop can never call preventDefault without throwing — bind a
// real non-passive listener instead so scroll/pinch on the wire can
// actually consume the gesture instead of also scrolling the page.
const onWheel = (e: WheelEvent) => {
e.preventDefault();
// trackpad pinch reports as a wheel event with ctrlKey set
const scale = e.ctrlKey ? 0.6 : 0.15;
engineRef.current?.wheelTotal(-e.deltaY * scale);
};
wireBox.addEventListener("wheel", onWheel, { passive: false });
// keep the animated targets aligned with external/committed value changes
setRatioTarget(ratioRef.current);
setTotalTarget(totalRef.current);
return () => {
cancelAnimationFrame(raf);
ro.disconnect();
io.disconnect();
document.removeEventListener("visibilitychange", onVis);
wireBox.removeEventListener("wheel", onWheel);
engineRef.current = null;
};
// mount-only: geometry constants are module-level, external value syncs
// are handled by the effects below via engineRef.syncRatio/syncTotal
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// external/keyboard ratio and total changes glide toward the new value
// (a live drag on that same axis already owns the frame and is skipped)
useEffect(() => {
engineRef.current?.syncRatio(ratioValue);
}, [ratioValue]);
useEffect(() => {
engineRef.current?.syncTotal(totalValue);
}, [totalValue]);
// -- keyboard: bead ("Split") ----------------------------------------------
const onBeadKeyDown = (e: React.KeyboardEvent) => {
const v = ratioRef.current;
let next: number | null = null;
switch (e.key) {
case "ArrowRight":
case "ArrowUp":
next = v + 1;
break;
case "ArrowLeft":
case "ArrowDown":
next = v - 1;
break;
case "PageUp":
next = v + 10;
break;
case "PageDown":
next = v - 10;
break;
case "Home":
next = 0;
break;
case "End":
next = 100;
break;
default:
return;
}
e.preventDefault();
next = clamp(next, 0, 100);
if (next !== v) commitRatioRef.current(next);
};
// -- keyboard: "Total allocated" -------------------------------------------
const onTotalKeyDown = (e: React.KeyboardEvent) => {
const v = totalRef.current;
let next: number | null = null;
switch (e.key) {
case "ArrowRight":
case "ArrowUp":
next = v + 1;
break;
case "ArrowLeft":
case "ArrowDown":
next = v - 1;
break;
case "PageUp":
next = v + 10;
break;
case "PageDown":
next = v - 10;
break;
case "Home":
next = 0;
break;
case "End":
next = 100;
break;
default:
return;
}
e.preventDefault();
next = clamp(next, 0, 100);
if (next !== v) commitTotalRef.current(next);
};
const leftPctNow = Math.round(ratioValue);
const rightPctNow = 100 - leftPctNow;
const totalNow = Math.round(totalValue);
const beadValueText = `${leftPctNow}% ${leftLabel.toLowerCase()}, ${rightPctNow}% ${rightLabel.toLowerCase()}`;
const totalValueText = `${totalNow}% of budget allocated`;
return (
<div className={`w-full select-none ${className}`}>
<div
ref={wireBoxRef}
className="group relative h-28 w-full touch-none"
onPointerDown={(e) => {
e.preventDefault();
e.currentTarget.setPointerCapture(e.pointerId);
beadRef.current?.focus({ preventScroll: true });
engineRef.current?.beadDragStart(e.clientX);
setRatioActive(true);
}}
onPointerMove={(e) => engineRef.current?.beadDragMove(e.clientX)}
onPointerUp={() => {
engineRef.current?.beadDragEnd();
setRatioActive(false);
}}
onPointerCancel={() => {
engineRef.current?.beadDragEnd();
setRatioActive(false);
}}
>
<svg
aria-hidden
viewBox={`0 0 ${VB_W} ${VB_H}`}
preserveAspectRatio="none"
className="absolute inset-0 h-full w-full cursor-ew-resize"
>
<path
ref={pathRef}
fill="none"
stroke="var(--muted)"
strokeWidth={1.5}
strokeLinecap="round"
/>
<circle cx={ANCHOR_X} cy={WIRE_Y} r={3} fill="var(--border)" />
<circle
cx={VB_W - ANCHOR_X}
cy={WIRE_Y}
r={3}
fill="var(--border)"
/>
</svg>
<div
ref={beadRef}
role="slider"
tabIndex={0}
aria-label="Split"
aria-orientation="horizontal"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={leftPctNow}
aria-valuetext={beadValueText}
onKeyDown={onBeadKeyDown}
onFocus={() => setRatioActive(true)}
onBlur={() => setRatioActive(false)}
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-accent focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
<span
aria-hidden
data-active={ratioActive}
className="absolute inset-[3px] rounded-full border border-border bg-background transition-colors duration-150 group-hover:border-foreground/40 data-[active=true]:border-accent"
/>
<span
aria-hidden
className="absolute left-1/2 top-1/2 h-2 w-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-foreground"
/>
</div>
</div>
<div className="flex items-start justify-between px-0.5 pt-1 font-mono">
<div className="flex flex-col gap-0.5">
<span className="text-xs text-muted">{leftLabel}</span>
<span
ref={leftPctRef}
className="tabular-nums text-sm text-foreground"
>
{leftPctNow}%
</span>
</div>
<div className="flex flex-col items-end gap-0.5">
<span className="text-xs text-muted">{rightLabel}</span>
<span
ref={rightPctRef}
className="tabular-nums text-sm text-foreground"
>
{rightPctNow}%
</span>
</div>
</div>
<div className="mt-5">
<div className="flex items-center justify-between pb-1.5">
<span className="font-mono text-xs text-muted">
Total allocated
</span>
<span
ref={totalCaptionRef}
className="font-mono text-xs tabular-nums text-muted"
>
{totalNow}%
</span>
</div>
<div
ref={totalTrackRef}
role="slider"
tabIndex={0}
aria-label="Total allocated"
aria-orientation="horizontal"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={totalNow}
aria-valuetext={totalValueText}
onKeyDown={onTotalKeyDown}
onFocus={() => setTotalActive(true)}
onBlur={() => setTotalActive(false)}
onPointerDown={(e) => {
e.preventDefault();
e.currentTarget.setPointerCapture(e.pointerId);
engineRef.current?.totalDragStart(e.clientX);
setTotalActive(true);
}}
onPointerMove={(e) => engineRef.current?.totalDragMove(e.clientX)}
onPointerUp={() => {
engineRef.current?.totalDragEnd();
setTotalActive(false);
}}
onPointerCancel={() => {
engineRef.current?.totalDragEnd();
setTotalActive(false);
}}
className="relative h-4 w-full cursor-ew-resize touch-none outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
<span className="absolute left-0 top-1/2 h-1 w-full -translate-y-1/2 rounded-full bg-border" />
<div
ref={totalFillRef}
className="absolute left-0 top-1/2 h-1 -translate-y-1/2 rounded-full bg-foreground/70"
style={{ width: `${totalNow}%` }}
/>
<div
ref={totalHandleRef}
data-active={totalActive}
style={{ left: `${totalNow}%` }}
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-accent"
/>
</div>
</div>
</div>
);
}
an allocation control where BOTH a ratio and a total-committed amount matter at once — budget splits, traffic weighting, quota assignment — and the wire's own sag is the second variable's readout, not just a ratio rail. Reach for slider-loupe instead for a single precise value read through a magnifying lens (audio scrub, zoom level), and pricing-scale for a literal two-option weighing/comparison rather than a continuous split-plus-commitment.
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(--muted)`; the bead is a --border-ringed dot that swaps to --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-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 --muted --border --accent), correct in both themes.