{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tabs-slack-cable",
  "title": "Tabs Slack Cable",
  "description": "Classic tabs whose active-indicator is a single SVG path drawn as a slack cable instead of a rigid bar: it sags mid-jump proportional to distance, tautens flat with a small spring snap, and previews the destination with a 3px tug when you hover a neighboring tab.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/tabs-slack-cable/component.tsx",
      "content": "\"use client\";\n\nimport {\n  useCallback,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n  type KeyboardEvent,\n  type ReactNode,\n} from \"react\";\n\n// ---------------------------------------------------------------------------\n// SlackRail — classic tabs whose active-indicator is a single SVG path drawn\n// as a slack cable instead of a rigid underline bar. At rest it is a taut 2px\n// line under the active tab. On selection change both endpoints spring\n// toward the new tab's edges (mass scaled by jump distance, so long jumps\n// accelerate slower and settle with a touch more overshoot — \"feel heavier\")\n// while the path's control point drops into a quadratic sag proportional to\n// how far the endpoints still have left to travel, flattening back to a\n// straight line as they arrive with a small underdamped snap. Hovering a\n// non-active tab tugs whichever endpoint is spatially nearest it 3px in that\n// direction, previewing where the cable would go. Full WAI-ARIA tabs pattern\n// (roving tabindex, automatic activation on Left/Right/Home/End); the cable\n// itself is aria-hidden and purely decorative. No canvas — DOM + SVG + CSS.\n// ---------------------------------------------------------------------------\n\nexport interface SlackRailItem {\n  value: string;\n  label: string;\n  content: ReactNode;\n}\n\nexport interface SlackRailProps {\n  /** the tabs, in order */\n  items: SlackRailItem[];\n  /** controlled selected value; omit for uncontrolled */\n  value?: string;\n  /** uncontrolled initial selected value */\n  defaultValue?: string;\n  /** called with the new value when a different tab is selected */\n  onValueChange?: (value: string) => void;\n  /** accessible name for the tab list */\n  \"aria-label\": string;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nfunction useReducedMotion() {\n  const [reduced, setReduced] = useState(false);\n  useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    setReduced(mq.matches);\n    const onChange = () => setReduced(mq.matches);\n    mq.addEventListener(\"change\", onChange);\n    return () => mq.removeEventListener(\"change\", onChange);\n  }, []);\n  return reduced;\n}\n\nfunction clamp(n: number, lo: number, hi: number) {\n  return Math.min(hi, Math.max(lo, n));\n}\n\nconst BASELINE_Y = 7; // rail SVG viewport y for the resting line\nconst RAIL_H = 14; // rail SVG viewport height (room for the sag to drop into)\nconst STROKE_W = 2;\nconst SAG_DIVISOR = 8;\nconst SAG_MAX = 6;\nconst TUG_PX = 3;\nconst SETTLE_EPS = 0.04;\nconst FORCE_SETTLE_MS = 900;\nconst SPRING_K = 220; // stiffness, s^-2\nconst SPRING_C = 26.4; // damping — combined with mass this gives near-critical\n// damping on short hops and a visibly underdamped small overshoot on long\n// ones, since mass grows with travel distance while c/k stay fixed.\nconst MASS_PER_PX = 1 / 260;\nconst TUG_TAU = 0.09; // exponential ease-toward time constant, seconds\n\ninterface Endpoint {\n  x: number;\n  v: number;\n  target: number;\n}\n\nexport function SlackRail({\n  items,\n  value,\n  defaultValue,\n  onValueChange,\n  className,\n  \"aria-label\": ariaLabel,\n  ...rest\n}: SlackRailProps) {\n  const baseId = useId();\n  const reduced = useReducedMotion();\n\n  const isControlled = value !== undefined;\n  const [internalValue, setInternalValue] = useState(\n    () => value ?? defaultValue ?? items[0]?.value,\n  );\n  const activeValue = isControlled ? (value as string) : internalValue;\n  const activeIndex = Math.max(\n    0,\n    items.findIndex((it) => it.value === activeValue),\n  );\n\n  const select = useCallback(\n    (next: string) => {\n      if (!isControlled) setInternalValue(next);\n      onValueChange?.(next);\n    },\n    [isControlled, onValueChange],\n  );\n\n  const tablistRef = useRef<HTMLDivElement | null>(null);\n  const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);\n\n  const left = useRef<Endpoint>({ x: 0, v: 0, target: 0 });\n  const right = useRef<Endpoint>({ x: 0, v: 0, target: 0 });\n  const travelDistance = useRef(0);\n  const transitionStart = useRef(0);\n  const tugLeft = useRef(0);\n  const tugRight = useRef(0);\n  const tugTargetLeft = useRef(0);\n  const tugTargetRight = useRef(0);\n  const laidOut = useRef(false);\n\n  const rafId = useRef<number | null>(null);\n  const lastTick = useRef(0);\n\n  const pathRef = useRef<SVGPathElement | null>(null);\n  const [hoverIndex, setHoverIndex] = useState<number | null>(null);\n  const [fadeVisible, setFadeVisible] = useState(true);\n\n  const measure = useCallback((index: number) => {\n    const list = tablistRef.current;\n    const btn = tabRefs.current[index];\n    if (!list || !btn) return null;\n    const listRect = list.getBoundingClientRect();\n    const btnRect = btn.getBoundingClientRect();\n    return {\n      left: btnRect.left - listRect.left,\n      right: btnRect.right - listRect.left,\n    };\n  }, []);\n\n  const applyPath = useCallback(() => {\n    const p = pathRef.current;\n    if (!p) return;\n    const finalLeft = left.current.x + tugLeft.current;\n    const finalRight = right.current.x + tugRight.current;\n    const totalTravel = travelDistance.current;\n    const remainL =\n      totalTravel > 0.5\n        ? clamp(Math.abs(left.current.target - left.current.x) / totalTravel, 0, 1)\n        : 0;\n    const remainR =\n      totalTravel > 0.5\n        ? clamp(Math.abs(right.current.target - right.current.x) / totalTravel, 0, 1)\n        : 0;\n    const sagFrac = Math.max(remainL, remainR);\n    const sagMax = Math.min(totalTravel / SAG_DIVISOR, SAG_MAX);\n    const sag = sagMax * sagFrac;\n    const controlX = (finalLeft + finalRight) / 2;\n    const controlY = BASELINE_Y + sag;\n    p.setAttribute(\n      \"d\",\n      `M ${finalLeft} ${BASELINE_Y} Q ${controlX} ${controlY} ${finalRight} ${BASELINE_Y}`,\n    );\n  }, []);\n\n  const tick = useCallback(() => {\n    const now = performance.now();\n    const dt = clamp((now - lastTick.current) / 1000, 0, 0.05);\n    lastTick.current = now;\n\n    const mass = 1 + travelDistance.current * MASS_PER_PX;\n\n    let stillMoving = false;\n    for (const ep of [left.current, right.current]) {\n      const a = (SPRING_K * (ep.target - ep.x) - SPRING_C * ep.v) / mass;\n      ep.v += a * dt;\n      ep.x += ep.v * dt;\n      if (Math.abs(ep.target - ep.x) > SETTLE_EPS || Math.abs(ep.v) > SETTLE_EPS) {\n        stillMoving = true;\n      }\n    }\n\n    if (now - transitionStart.current > FORCE_SETTLE_MS) {\n      left.current.x = left.current.target;\n      left.current.v = 0;\n      right.current.x = right.current.target;\n      right.current.v = 0;\n      stillMoving = false;\n    }\n\n    let tugMoving = false;\n    const easeTo = (cur: number, target: number) => {\n      const next = cur + (target - cur) * (1 - Math.exp(-dt / TUG_TAU));\n      if (Math.abs(target - next) > 0.02) tugMoving = true;\n      return next;\n    };\n    tugLeft.current = easeTo(tugLeft.current, tugTargetLeft.current);\n    tugRight.current = easeTo(tugRight.current, tugTargetRight.current);\n\n    applyPath();\n\n    if (stillMoving || tugMoving) {\n      rafId.current = requestAnimationFrame(tick);\n    } else {\n      rafId.current = null;\n    }\n  }, [applyPath]);\n\n  const wake = useCallback(() => {\n    if (rafId.current == null) {\n      lastTick.current = performance.now();\n      rafId.current = requestAnimationFrame(tick);\n    }\n  }, [tick]);\n\n  // Lay out / retarget the rail whenever the active tab or the tab layout\n  // (resize, font load) changes.\n  useLayoutEffect(() => {\n    const pos = measure(activeIndex);\n    if (!pos) return;\n\n    if (!laidOut.current) {\n      // First layout: snap in place, no travel, no sag.\n      left.current = { x: pos.left, v: 0, target: pos.left };\n      right.current = { x: pos.right, v: 0, target: pos.right };\n      travelDistance.current = 0;\n      laidOut.current = true;\n      applyPath();\n      return;\n    }\n\n    const oldCenter = (left.current.x + right.current.x) / 2;\n    const newCenter = (pos.left + pos.right) / 2;\n    travelDistance.current = Math.abs(newCenter - oldCenter);\n    left.current.target = pos.left;\n    right.current.target = pos.right;\n    transitionStart.current = performance.now();\n\n    if (reduced) {\n      left.current = { x: pos.left, v: 0, target: pos.left };\n      right.current = { x: pos.right, v: 0, target: pos.right };\n      travelDistance.current = 0;\n      applyPath();\n      setFadeVisible(false);\n      requestAnimationFrame(() => requestAnimationFrame(() => setFadeVisible(true)));\n      return;\n    }\n\n    wake();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [activeIndex, reduced]);\n\n  // Re-measure on container resize (font load, responsive width changes) and\n  // keep the SVG's viewBox in lockstep with the tablist's actual pixel width\n  // so path coordinates (measured in real px) map 1:1 onto the rendered rail.\n  const [railWidthPx, setRailWidthPx] = useState(0);\n  useEffect(() => {\n    const list = tablistRef.current;\n    if (!list) return;\n    const resnap = () => {\n      setRailWidthPx(list.getBoundingClientRect().width);\n      const pos = measure(activeIndex);\n      if (!pos) return;\n      left.current = { x: pos.left, v: 0, target: pos.left };\n      right.current = { x: pos.right, v: 0, target: pos.right };\n      travelDistance.current = 0;\n      applyPath();\n    };\n    resnap();\n    const ro = new ResizeObserver(resnap);\n    ro.observe(list);\n    return () => ro.disconnect();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  useEffect(() => {\n    return () => {\n      if (rafId.current != null) cancelAnimationFrame(rafId.current);\n    };\n  }, []);\n\n  // Hover tug: nudge whichever endpoint is spatially nearest the hovered,\n  // non-active tab 3px toward it.\n  useEffect(() => {\n    if (reduced) return;\n    if (hoverIndex === null || hoverIndex === activeIndex) {\n      tugTargetLeft.current = 0;\n      tugTargetRight.current = 0;\n      wake();\n      return;\n    }\n    const pos = measure(hoverIndex);\n    if (!pos) return;\n    const hoverCenter = (pos.left + pos.right) / 2;\n    const distL = Math.abs(hoverCenter - left.current.x);\n    const distR = Math.abs(hoverCenter - right.current.x);\n    if (distL <= distR) {\n      tugTargetLeft.current = Math.sign(hoverCenter - left.current.x) * TUG_PX;\n      tugTargetRight.current = 0;\n    } else {\n      tugTargetRight.current = Math.sign(hoverCenter - right.current.x) * TUG_PX;\n      tugTargetLeft.current = 0;\n    }\n    wake();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [hoverIndex, reduced]);\n\n  const focusTab = (index: number) => {\n    tabRefs.current[index]?.focus();\n  };\n\n  const onTabKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {\n    const count = items.length;\n    let next: number | null = null;\n    switch (e.key) {\n      case \"ArrowRight\":\n        next = (activeIndex + 1) % count;\n        break;\n      case \"ArrowLeft\":\n        next = (activeIndex - 1 + count) % count;\n        break;\n      case \"Home\":\n        next = 0;\n        break;\n      case \"End\":\n        next = count - 1;\n        break;\n      default:\n        return;\n    }\n    e.preventDefault();\n    focusTab(next);\n    select(items[next].value);\n  };\n\n  return (\n    <div className={className} {...rest}>\n      <div\n        ref={tablistRef}\n        role=\"tablist\"\n        aria-label={ariaLabel}\n        className=\"relative flex gap-1\"\n      >\n        {items.map((item, i) => {\n          const isActive = i === activeIndex;\n          return (\n            <button\n              key={item.value}\n              ref={(el) => {\n                tabRefs.current[i] = el;\n              }}\n              role=\"tab\"\n              id={`${baseId}-tab-${item.value}`}\n              aria-selected={isActive}\n              aria-controls={`${baseId}-panel-${item.value}`}\n              tabIndex={isActive ? 0 : -1}\n              onClick={() => select(item.value)}\n              onKeyDown={onTabKeyDown}\n              onPointerEnter={() => setHoverIndex(i)}\n              onPointerLeave={() =>\n                setHoverIndex((cur) => (cur === i ? null : cur))\n              }\n              className={`rounded-sm px-3 py-2 text-sm transition-colors duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent ${\n                isActive\n                  ? \"font-medium text-foreground\"\n                  : \"text-ns-muted hover:text-foreground\"\n              }`}\n            >\n              {item.label}\n            </button>\n          );\n        })}\n        <svg\n          aria-hidden\n          focusable=\"false\"\n          viewBox={`0 0 ${Math.max(railWidthPx, 1)} ${RAIL_H}`}\n          preserveAspectRatio=\"none\"\n          width={railWidthPx}\n          height={RAIL_H}\n          className=\"pointer-events-none absolute left-0 top-full mt-1 overflow-visible\"\n          style={\n            reduced\n              ? { opacity: fadeVisible ? 1 : 0, transition: \"opacity 120ms ease-out\" }\n              : undefined\n          }\n        >\n          <path\n            ref={pathRef}\n            fill=\"none\"\n            stroke=\"var(--foreground)\"\n            strokeWidth={STROKE_W}\n            strokeLinecap=\"round\"\n            vectorEffect=\"non-scaling-stroke\"\n          />\n        </svg>\n      </div>\n      {items.map((item, i) => (\n        <div\n          key={item.value}\n          role=\"tabpanel\"\n          id={`${baseId}-panel-${item.value}`}\n          aria-labelledby={`${baseId}-tab-${item.value}`}\n          tabIndex={0}\n          hidden={i !== activeIndex}\n          className=\"pt-4 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent\"\n        >\n          {item.content}\n        </div>\n      ))}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/tabs-slack-cable.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": [
      "tabs",
      "navigation",
      "svg",
      "spring",
      "physics",
      "accessibility"
    ],
    "instruction": "A full WAI-ARIA tabs widget (role=tablist/tab/tabpanel, roving tabindex, automatic activation on ArrowLeft/ArrowRight/Home/End) whose selection indicator is one aria-hidden SVG <path> under the tab row instead of a CSS-transitioned bar. At rest the path is a flat 2px --foreground stroke spanning exactly the active tab's measured left/right edges. On selection change both endpoints are independently driven by a damped spring (stiffness 220 s^-2, damping 26.4, integrated semi-implicit-Euler on a single rAF loop with zero React state on the hot path) toward the new tab's edges; the spring's effective mass is 1 + travelDistance/260, where travelDistance is the old-to-new active-tab center jump measured once at transition start — so a short hop between adjacent tabs stays near-critically damped while a long jump across the row accelerates and settles more slowly and with a touch more overshoot, reading as heavier. Each frame the path is rebuilt as a quadratic Bezier: the control point's y drops below the flat baseline by min(travelDistance/8, 6)px scaled by how much of the total travel the slower endpoint still has left (1 at the instant the jump starts, 0 once both endpoints arrive), so the cable visibly pays out and sags mid-flight and flattens back to a straight line exactly as it arrives, with the spring's own slight overshoot read as the small snap into place. Hovering a non-active tab (pointerenter/pointerleave) nudges whichever endpoint sits spatially nearest that tab 3px in its direction, eased in with an exponential ease-toward (tau 90ms, the ease-out-expo family) and eased back to 0 on pointerleave — a preview of where the cable would travel, never committing selection. A ResizeObserver keeps the SVG's viewBox locked 1:1 to the tablist's pixel width and re-snaps the rail instantly (no animation) on layout change, e.g. font load or container resize. Tab labels are --ns-muted at rest, --foreground with a hover transition when active or hovered; focus uses focus-visible:outline-ns-accent, never paired with a base outline-none. Panels are real DOM nodes linked by aria-controls/aria-labelledby and kept mounted with the hidden attribute so ids always resolve. prefers-reduced-motion collapses the whole spring/sag mechanism: a selection change snaps both endpoints straight to the destination with zero sag, the hover tug is disabled outright, and the path instead plays a 120ms opacity crossfade so the reposition still reads as a change rather than a silent jump. Pure DOM + SVG + CSS — no canvas anywhere."
  },
  "type": "registry:ui"
}