{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "segmented-control-fling",
  "title": "Segmented Control Fling",
  "description": "Segmented control with a visibly grabbable pill (grip dots, hover lift): fling it, it coasts on real release velocity, rubber-bands off the ends, and snaps into the nearest detent. Optional one-shot intro fling demos the mechanic on first view.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/segmented-control-fling/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\n// ---------------------------------------------------------------------------\n// FlingSegment — segmented control whose selection pill is grabbable: drag\n// and release it anywhere along the track and it coasts on real release\n// velocity under exponential friction, rubber-bands off the track edges, and\n// captures into the nearest segment detent with one small overshoot. Click\n// and keyboard remain instant no-drag paths (critically damped glide across\n// both x and width — segments may differ in width). Pure DOM: the pill is an\n// absolutely-positioned node driven by OFFSET transforms relative to its\n// layout slot on a direct-DOM rAF loop — zero React state on the hot path,\n// the loop sleeps at a velocity epsilon, pauses offscreen, and a 1 s\n// forced-settle deadline guarantees physics never jitters forever. All ink\n// is token-relative CSS (background/surface/border/foreground/accent), so\n// both themes restyle live with nothing to re-derive.\n// ---------------------------------------------------------------------------\n\nconst FRICTION = 4.5; // exponential coast friction, s^-1\nconst CAPTURE_V = 300; // px/s — below this the nearest detent captures\nconst DETENT_K = 280; // detent capture spring, s^-2\nconst DETENT_ZETA = 0.8; // one small overshoot\nconst EDGE_K = 320; // edge rubber-band spring, s^-2\nconst EDGE_ZETA = 0.9;\nconst GLIDE_K = 220; // click/keyboard glide — critically damped, ~260 ms\nconst GLIDE_ZETA = 1.0;\nconst RUBBER = 0.35; // beyond-track displacement scale\nconst SETTLE_MS = 1000; // forced-settle deadline\nconst V_WINDOW = 80; // ms of pointer samples averaged into release velocity\nconst INTRO_DELAY_MS = 2500; // one-shot intro fling — a beat after first paint\n\nexport interface FlingSegmentOption {\n  value: string;\n  label: string;\n}\n\nconst DEFAULT_OPTIONS: FlingSegmentOption[] = [\n  { value: \"list\", label: \"List\" },\n  { value: \"board\", label: \"Board\" },\n  { value: \"timeline\", label: \"Timeline\" },\n];\n\nexport function FlingSegment({\n  options = DEFAULT_OPTIONS,\n  value,\n  defaultValue,\n  onValueChange,\n  introFling = false,\n  className = \"\",\n  \"aria-label\": ariaLabel = \"View\",\n}: {\n  /** the segments */\n  options?: FlingSegmentOption[];\n  /** controlled value; omit for uncontrolled */\n  value?: string;\n  /** uncontrolled initial value */\n  defaultValue?: string;\n  /** fires once per selection change — on detent commit, never per-frame */\n  onValueChange?: (value: string) => void;\n  /**\n   * opt-in one-shot self-demo: a beat after mount the pill is flung to the\n   * far segment, coasts, and captures into the detent. Plays exactly once,\n   * never loops, is skipped under reduced motion, and is permanently\n   * cancelled by any pointer or keyboard interaction.\n   */\n  introFling?: boolean;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n  /** accessible name for the control. Default \"View\". */\n  \"aria-label\"?: string;\n}) {\n  const trackRef = useRef<HTMLDivElement>(null);\n  const pillRef = useRef<HTMLSpanElement>(null);\n  const btnRefs = useRef<(HTMLButtonElement | null)[]>([]);\n  const engineRef = useRef<{\n    goTo: (index: number, animate: boolean) => void;\n  } | null>(null);\n  const firstRef = useRef(true);\n\n  const isControlled = value !== undefined;\n  const [internal, setInternal] = useState(\n    defaultValue ?? options[0]?.value ?? \"\"\n  );\n  const selectedValue = isControlled ? value : internal;\n  let selectedIndex = options.findIndex((o) => o.value === selectedValue);\n  if (selectedIndex < 0) selectedIndex = 0;\n\n  const selectedRef = useRef(selectedIndex);\n  selectedRef.current = selectedIndex;\n  const optsLenRef = useRef(options.length);\n  optsLenRef.current = options.length;\n  const introRef = useRef(introFling);\n  introRef.current = introFling;\n  const introDoneRef = useRef(false); // survives option changes — plays once ever\n\n  // instant commit path shared by click, keyboard, and the physics engine's\n  // detent settle — fires onValueChange exactly once per selection change\n  const setIndex = (i: number) => {\n    const opt = options[i];\n    if (!opt) return;\n    if (!isControlled) setInternal(opt.value);\n    if (opt.value !== selectedValue) onValueChange?.(opt.value);\n  };\n  const commitRef = useRef<(i: number) => void>(() => {});\n  commitRef.current = setIndex;\n\n  const optKey = options.map((o) => o.value).join(\"\u0000\");\n\n  useEffect(() => {\n    const track = trackRef.current;\n    const pill = pillRef.current;\n    if (!track || !pill) return;\n    const reduced = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\"\n    ).matches;\n\n    // -- hot-path state: locals only, never React state --------------------\n    let segX: number[] = []; // offsetLeft per segment (track-relative)\n    let segW: number[] = []; // offsetWidth per segment\n    let minX = 0;\n    let maxX = 0;\n    let sized = false;\n    let x = 0; // pill offset within the track's layout, px\n    let v = 0; // px/s\n    let w = 0; // pill width, px\n    let wv = 0;\n    let mode: 0 | 1 | 2 | 3 | 4 = 0; // idle | drag | coast | detent | glide\n    let targetI = selectedRef.current;\n    let deadline = 0;\n    let raf = 0;\n    let last = 0;\n    let visible = true;\n\n    // drag bookkeeping\n    let pointerId = -1;\n    let downX = 0;\n    let basePillX = 0;\n    let pointerX = 0;\n    let samples: { t: number; x: number }[] = [];\n\n    const render = () => {\n      // beyond-track displacement renders scaled ×RUBBER (rubber band); the\n      // physical x stays unscaled so the edge spring sees true displacement\n      let rx = x;\n      if (rx < minX) rx = minX + (rx - minX) * RUBBER;\n      else if (rx > maxX) rx = maxX + (rx - maxX) * RUBBER;\n      pill.style.width = `${w.toFixed(2)}px`;\n      pill.style.transform = `translateX(${rx.toFixed(2)}px)`;\n    };\n\n    const nearest = () => {\n      const center = x + w / 2;\n      let best = 0;\n      let bd = Infinity;\n      for (let i = 0; i < segX.length; i++) {\n        const d = Math.abs((segX[i] ?? 0) + (segW[i] ?? 0) / 2 - center);\n        if (d < bd) {\n          bd = d;\n          best = i;\n        }\n      }\n      return best;\n    };\n\n    const measure = () => {\n      const btns: HTMLButtonElement[] = [];\n      for (let i = 0; i < optsLenRef.current; i++) {\n        const b = btnRefs.current[i];\n        if (b) btns.push(b);\n      }\n      const rect = track.getBoundingClientRect();\n      const first = btns[0];\n      if (!first || rect.width < 8 || rect.height < 8) {\n        sized = false; // zero-size guard — no physics into nothing\n        pill.style.opacity = \"0\";\n        return;\n      }\n      segX = btns.map((b) => b.offsetLeft);\n      segW = btns.map((b) => b.offsetWidth);\n      minX = segX[0] ?? 0;\n      maxX = segX[segX.length - 1] ?? 0;\n      pill.style.top = `${first.offsetTop}px`;\n      pill.style.height = `${first.offsetHeight}px`;\n      pill.style.opacity = \"1\";\n      sized = true;\n      if (mode === 0) {\n        targetI = Math.min(targetI, segX.length - 1);\n        x = segX[targetI] ?? 0;\n        w = segW[targetI] ?? 0;\n        v = wv = 0;\n        render();\n      }\n    };\n\n    const settle = () => {\n      mode = 0;\n      render();\n      track.style.cursor = \"\";\n      commitRef.current(targetI);\n    };\n\n    const loop = (now: number) => {\n      raf = 0;\n      const dt = last ? Math.min(0.032, (now - last) / 1000) : 1 / 60;\n      last = now;\n      if (!sized || mode === 0) return;\n      let running = true;\n\n      if (mode === 1) {\n        // drag: pill tracks the pointer 1:1; width springs toward whichever\n        // segment is currently nearest so unequal widths morph mid-drag\n        x = basePillX + (pointerX - downX);\n        const tw = segW[nearest()] ?? w;\n        const c = 2 * DETENT_ZETA * Math.sqrt(DETENT_K);\n        wv += (-DETENT_K * (w - tw) - c * wv) * dt;\n        w += wv * dt;\n      } else if (mode === 2) {\n        if (x < minX || x > maxX) {\n          // overshot an end — rubber-band spring hauls it back\n          const edge = x < minX ? minX : maxX;\n          const c = 2 * EDGE_ZETA * Math.sqrt(EDGE_K);\n          v += (-EDGE_K * (x - edge) - c * v) * dt;\n          x += v * dt;\n          if (x >= minX && x <= maxX && Math.abs(v) < CAPTURE_V) {\n            mode = 3;\n            targetI = nearest();\n          }\n        } else {\n          v *= Math.exp(-FRICTION * dt);\n          x += v * dt;\n          if (Math.abs(v) < CAPTURE_V) {\n            mode = 3;\n            targetI = nearest();\n          }\n        }\n        const tw = segW[nearest()] ?? w;\n        const c = 2 * DETENT_ZETA * Math.sqrt(DETENT_K);\n        wv += (-DETENT_K * (w - tw) - c * wv) * dt;\n        w += wv * dt;\n      } else {\n        // detent capture (one small overshoot) or click/keyboard glide\n        const k = mode === 3 ? DETENT_K : GLIDE_K;\n        const z = mode === 3 ? DETENT_ZETA : GLIDE_ZETA;\n        const tx = segX[targetI] ?? 0;\n        const tw = segW[targetI] ?? w;\n        const c = 2 * z * Math.sqrt(k);\n        v += (-k * (x - tx) - c * v) * dt;\n        x += v * dt;\n        wv += (-k * (w - tw) - c * wv) * dt;\n        w += wv * dt;\n        if (\n          Math.abs(x - tx) < 0.3 &&\n          Math.abs(v) < 8 &&\n          Math.abs(w - tw) < 0.3 &&\n          Math.abs(wv) < 8\n        ) {\n          x = tx;\n          w = tw;\n          v = wv = 0;\n          running = false; // velocity-epsilon sleep\n        }\n      }\n\n      // forced-settle deadline — physics never jitters forever\n      if (mode !== 1 && now > deadline) {\n        if (mode === 2) targetI = nearest();\n        x = segX[targetI] ?? x;\n        w = segW[targetI] ?? w;\n        v = wv = 0;\n        running = false;\n      }\n\n      if (!running) {\n        settle();\n        return;\n      }\n      render();\n      if (visible && !document.hidden) raf = requestAnimationFrame(loop);\n    };\n\n    const wake = () => {\n      if (!raf && !reduced && sized && mode !== 0 && visible && !document.hidden) {\n        last = 0;\n        raf = requestAnimationFrame(loop);\n      }\n    };\n\n    const goTo = (i: number, animate: boolean) => {\n      if (!sized) {\n        targetI = i;\n        return;\n      }\n      if (mode === 1) return; // user owns the pill mid-drag\n      targetI = Math.max(0, Math.min(i, segX.length - 1));\n      if (reduced || !animate) {\n        x = segX[targetI] ?? 0;\n        w = segW[targetI] ?? 0;\n        v = wv = 0;\n        mode = 0;\n        render();\n        return;\n      }\n      if (mode === 0 && Math.abs(x - (segX[targetI] ?? 0)) < 0.5) return;\n      mode = 4;\n      deadline = performance.now() + SETTLE_MS;\n      wake();\n    };\n\n    const finishDrag = (releaseV: number) => {\n      pointerId = -1;\n      if (mode !== 1) return;\n      track.style.cursor = \"\";\n      if (reduced) {\n        // no coast — release selects the nearest segment instantly\n        targetI = nearest();\n        x = segX[targetI] ?? x;\n        w = segW[targetI] ?? w;\n        v = wv = 0;\n        mode = 0;\n        render();\n        commitRef.current(targetI);\n        return;\n      }\n      v = releaseV;\n      x = basePillX + (pointerX - downX);\n      mode = 2;\n      deadline = performance.now() + SETTLE_MS;\n      wake();\n    };\n\n    // -- one-shot intro fling (opt-in via introFling) -----------------------\n    // scripted self-demo: the pill gets flung toward the far segment, coasts\n    // on friction, nudges the end rubber band, and captures into the detent —\n    // so a first-time viewer instantly sees the mechanic. Runs once shortly\n    // after mount, never loops, skipped under reduced motion, permanently\n    // cancelled by any pointer or keyboard interaction.\n    let introTimer = 0;\n    const cancelIntro = () => {\n      introDoneRef.current = true;\n      if (introTimer) {\n        window.clearTimeout(introTimer);\n        introTimer = 0;\n      }\n    };\n    if (introRef.current && !reduced && !introDoneRef.current) {\n      introTimer = window.setTimeout(() => {\n        introTimer = 0;\n        if (introDoneRef.current || !sized || mode !== 0) return;\n        introDoneRef.current = true;\n        const to = selectedRef.current === segX.length - 1 ? 0 : segX.length - 1;\n        const d = (segX[to] ?? 0) - x;\n        if (Math.abs(d) < 4) return;\n        // release velocity sized so friction bleeds it to capture speed just\n        // past the target — a visible coast plus one rubber-band nudge\n        v = Math.sign(d) * (CAPTURE_V + FRICTION * Math.abs(d) + 60);\n        mode = 2;\n        deadline = performance.now() + SETTLE_MS;\n        wake();\n      }, INTRO_DELAY_MS);\n    }\n\n    const onDown = (e: PointerEvent) => {\n      cancelIntro(); // any real interaction retires the self-demo for good\n      if (e.button !== 0 || !sized || mode === 1) return;\n      // is the press inside the pill's rendered span? (track coords; the 1px\n      // track border is absorbed by the ±2px tolerance)\n      const px = e.clientX - track.getBoundingClientRect().left;\n      let rx = x;\n      if (rx < minX) rx = minX + (rx - minX) * RUBBER;\n      else if (rx > maxX) rx = maxX + (rx - maxX) * RUBBER;\n      if (px < rx - 2 || px > rx + w + 2) return; // segment click path instead\n      pointerId = e.pointerId;\n      downX = e.clientX;\n      pointerX = e.clientX;\n      basePillX = x;\n      v = wv = 0;\n      samples = [{ t: e.timeStamp, x: e.clientX }];\n      mode = 1; // catch the pill — even mid-flight\n      track.style.cursor = \"grabbing\";\n      try {\n        track.setPointerCapture(e.pointerId);\n      } catch {\n        /* pointer already gone */\n      }\n      if (reduced) render();\n      else wake();\n    };\n\n    const onMove = (e: PointerEvent) => {\n      if (mode !== 1 || e.pointerId !== pointerId) return;\n      pointerX = e.clientX;\n      samples.push({ t: e.timeStamp, x: e.clientX });\n      while (samples.length > 1 && e.timeStamp - (samples[0]?.t ?? 0) > 120)\n        samples.shift();\n      if (reduced) {\n        x = Math.min(maxX, Math.max(minX, basePillX + (pointerX - downX)));\n        w = segW[nearest()] ?? w;\n        render();\n      }\n    };\n\n    const onUp = (e: PointerEvent) => {\n      if (e.pointerId !== pointerId) return;\n      // release velocity = mean of pointer samples from the last 80 ms\n      const cutoff = e.timeStamp - V_WINDOW;\n      let i0 = 0;\n      while (i0 < samples.length - 1 && (samples[i0]?.t ?? 0) < cutoff) i0++;\n      const a = samples[i0];\n      const b = samples[samples.length - 1];\n      let rv = 0;\n      if (a && b && b.t - a.t > 8) rv = ((b.x - a.x) / (b.t - a.t)) * 1000;\n      finishDrag(rv);\n    };\n\n    const onCancel = (e: PointerEvent) => {\n      if (e.pointerId !== pointerId) return;\n      finishDrag(0);\n    };\n\n    measure();\n    engineRef.current = { goTo };\n\n    // -- observers ----------------------------------------------------------\n    const ro = new ResizeObserver(measure);\n    ro.observe(track);\n    const io = new IntersectionObserver((entries) => {\n      visible = entries[0]?.isIntersecting ?? true;\n      if (visible) wake();\n      else {\n        cancelAnimationFrame(raf);\n        raf = 0; // offscreen — loop pauses, resumes on re-entry\n      }\n    });\n    io.observe(track);\n    const onVis = () => {\n      if (!document.hidden) wake();\n    };\n    document.addEventListener(\"visibilitychange\", onVis);\n    track.addEventListener(\"keydown\", cancelIntro, true);\n    track.addEventListener(\"pointerdown\", onDown);\n    track.addEventListener(\"pointermove\", onMove);\n    track.addEventListener(\"pointerup\", onUp);\n    track.addEventListener(\"pointercancel\", onCancel);\n\n    return () => {\n      cancelAnimationFrame(raf);\n      if (introTimer) window.clearTimeout(introTimer); // teardown ≠ played\n      ro.disconnect();\n      io.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVis);\n      track.removeEventListener(\"keydown\", cancelIntro, true);\n      track.removeEventListener(\"pointerdown\", onDown);\n      track.removeEventListener(\"pointermove\", onMove);\n      track.removeEventListener(\"pointerup\", onUp);\n      track.removeEventListener(\"pointercancel\", onCancel);\n      track.style.cursor = \"\";\n      engineRef.current = null;\n    };\n  }, [optKey]);\n\n  // selection changes glide the pill; drag commits land here already parked\n  // at the detent, so the glide is a no-op for them\n  useEffect(() => {\n    if (firstRef.current) {\n      firstRef.current = false;\n      return;\n    }\n    engineRef.current?.goTo(selectedIndex, true);\n  }, [selectedIndex, optKey]);\n\n  const onKeyDown = (e: React.KeyboardEvent) => {\n    const n = options.length;\n    if (n === 0) return;\n    let next = -1;\n    switch (e.key) {\n      case \"ArrowRight\":\n      case \"ArrowDown\":\n        next = (selectedIndex + 1) % n;\n        break;\n      case \"ArrowLeft\":\n      case \"ArrowUp\":\n        next = (selectedIndex - 1 + n) % n;\n        break;\n      case \"Home\":\n        next = 0;\n        break;\n      case \"End\":\n        next = n - 1;\n        break;\n      case \" \":\n      case \"Enter\":\n        next = selectedIndex; // commit focused\n        break;\n      default:\n        return;\n    }\n    e.preventDefault();\n    setIndex(next);\n    btnRefs.current[next]?.focus();\n  };\n\n  return (\n    <div\n      ref={trackRef}\n      role=\"radiogroup\"\n      aria-label={ariaLabel}\n      onKeyDown={onKeyDown}\n      className={`group/fling relative inline-flex touch-pan-y select-none items-center rounded-sm border border-border bg-foreground/[0.04] p-1 ${className}`}\n    >\n      <span\n        ref={pillRef}\n        aria-hidden\n        className=\"absolute left-0 top-0 rounded-[5px] border border-border bg-background opacity-0 shadow-[0_1px_2px_rgba(0,0,0,0.12)] transition-shadow duration-150 will-change-transform group-has-[button[aria-checked=true]:hover]/fling:shadow-[0_3px_8px_rgba(0,0,0,0.24)]\"\n      >\n        {/* grip affordance — six dots hugging the pill's right edge say\n            \"this is a handle\"; they brighten when the pill is hovered */}\n        <span className=\"absolute right-[5px] top-1/2 grid -translate-y-1/2 grid-cols-2 gap-[3px]\">\n          {[0, 1, 2, 3, 4, 5].map((i) => (\n            <span\n              key={i}\n              className=\"h-[2px] w-[2px] rounded-full bg-foreground/25 transition-colors duration-150 group-has-[button[aria-checked=true]:hover]/fling:bg-foreground/50\"\n            />\n          ))}\n        </span>\n      </span>\n      {options.map((opt, i) => {\n        const selected = i === selectedIndex;\n        return (\n          <button\n            key={opt.value}\n            ref={(el) => {\n              btnRefs.current[i] = el;\n            }}\n            type=\"button\"\n            role=\"radio\"\n            aria-checked={selected}\n            tabIndex={selected ? 0 : -1}\n            onClick={() => setIndex(i)}\n            className={`relative rounded-[5px] px-3.5 py-1.5 text-sm font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ns-accent ${\n              selected\n                ? \"cursor-grab text-foreground active:cursor-grabbing\"\n                : \"cursor-pointer text-ns-muted hover:text-foreground\"\n            }`}\n          >\n            {opt.label}\n          </button>\n        );\n      })}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/segmented-control-fling.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": [
      "control",
      "form",
      "segmented",
      "physics",
      "drag"
    ],
    "instruction": "Segmented control whose selection pill is grabbable: drag and release it anywhere along the track and it coasts on real release velocity (mean of pointer samples from the last 80 ms) under exponential friction v*=exp(-4.5*dt), rubber-bands off the track edges (beyond-track displacement rendered scaled 0.35, spring back k=320 zeta=0.9), and below 300 px/s captures into the nearest segment detent via a spring k=280 zeta=0.8 with one small overshoot. Click and keyboard remain instant no-drag paths: measure the target segment's width first (segments may differ) and glide both x and width with a critically damped spring k=220 zeta=1, ~260 ms. Pure DOM — the pill is an absolutely-positioned node driven by offset transforms relative to its layout slot on a direct-DOM rAF loop, zero React state on the hot path; a 1 s forced-settle deadline guarantees physics never jitters forever; the loop sleeps at a velocity epsilon and pauses offscreen via IntersectionObserver. role=radiogroup of role=radio buttons: ArrowLeft/Right/Up/Down move selection, Home/End jump to first/last, Space/Enter commit the focused segment, focus ring uses the accent token, and onValueChange fires once on detent commit, never per-frame. Reduced motion: the pill repositions instantly and drag release selects the nearest segment with no coast. Grabbability is advertised visually: the pill carries a six-dot grip affordance at its right edge and lifts (deeper shadow, brighter dots) when the selected segment is hovered, with cursor-grab/grabbing. Optional introFling prop plays a one-shot scripted self-demo ~2.5 s after mount — the pill is flung to the far segment, coasts, nudges the end rubber band, and captures into the detent; it never loops, is skipped under reduced motion, and is permanently cancelled by any pointer or keyboard interaction. All ink is token-relative CSS — no canvas."
  },
  "type": "registry:ui"
}