{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "invert-tabs",
  "type": "registry:ui",
  "title": "Invert Tabs",
  "description": "mix-blend-mode exclusion on a white pill — text auto-inverts in both light and dark themes. Spring physics. Audio tick.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/ruixenui/invert-tabs.tsx",
      "content": "\"use client\";\n\nimport { useState, useRef, useCallback, useEffect } from \"react\";\nimport { motion, useMotionValue, useSpring } from \"motion/react\";\n\n/**\n * Invert Tabs — inspired by rauno.me/craft.\n *\n * mix-blend-mode: exclusion on a white pill indicator.\n * Text auto-inverts under the pill in both light and dark themes.\n * Pure tab selector. Spring physics. Audio tick on change.\n */\n\n/* ── Audio singleton ── */\n\nlet _a: AudioContext | null = null;\nlet _b: AudioBuffer | null = null;\n\nfunction getCtx(): AudioContext {\n  if (!_a)\n    _a = new (window.AudioContext ||\n      (window as unknown as { webkitAudioContext: typeof AudioContext })\n        .webkitAudioContext)();\n  if (_a.state === \"suspended\") _a.resume();\n  return _a;\n}\n\nfunction getBuf(ac: AudioContext): AudioBuffer {\n  if (_b && _b.sampleRate === ac.sampleRate) return _b;\n  const len = Math.floor(ac.sampleRate * 0.003);\n  const buf = ac.createBuffer(1, len, ac.sampleRate);\n  const ch = buf.getChannelData(0);\n  for (let i = 0; i < len; i++)\n    ch[i] = (Math.random() * 2 - 1) * (1 - i / len) ** 4;\n  _b = buf;\n  return buf;\n}\n\nfunction tick(ref: React.MutableRefObject<number>) {\n  const now = performance.now();\n  if (now - ref.current < 25) return;\n  ref.current = now;\n  try {\n    const ac = getCtx();\n    const src = ac.createBufferSource();\n    const g = ac.createGain();\n    src.buffer = getBuf(ac);\n    g.gain.value = 0.06;\n    src.connect(g).connect(ac.destination);\n    src.start();\n  } catch {\n    /* silent */\n  }\n}\n\n/* ── Types ── */\n\nexport interface InvertTabItem {\n  value: string;\n  label: string;\n}\n\ninterface InvertTabsProps {\n  items?: InvertTabItem[];\n  defaultValue?: string;\n  onChange?: (value: string) => void;\n  sound?: boolean;\n  className?: string;\n}\n\n/* ── CSS ── */\n\nconst CSS = `.it{--it-bg:#f0f0f0;--it-text:rgba(0,0,0,.5);--it-pill:#fff}.dark .it,[data-theme=\"dark\"] .it{--it-bg:#1a1a1c;--it-text:rgba(255,255,255,.45);--it-pill:#fff}`;\n\n/* ── Constants ── */\n\nconst SPRING = { stiffness: 420, damping: 28 };\n\n/* ── Component ── */\n\nexport function InvertTabs({\n  items = [\n    { value: \"all\", label: \"All Posts\" },\n    { value: \"engineering\", label: \"Engineering\" },\n    { value: \"community\", label: \"Community\" },\n    { value: \"press\", label: \"Press\" },\n    { value: \"changelog\", label: \"Changelog\" },\n  ],\n  defaultValue,\n  onChange,\n  sound = true,\n  className,\n}: InvertTabsProps) {\n  const [active, setActive] = useState(defaultValue || items[0]?.value || \"\");\n  const lastSound = useRef(0);\n  const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);\n  const barRef = useRef<HTMLDivElement | null>(null);\n  const measured = useRef(false);\n\n  const pillX = useMotionValue(0);\n  const pillW = useMotionValue(0);\n  const springX = useSpring(pillX, SPRING);\n  const springW = useSpring(pillW, SPRING);\n\n  const measure = useCallback(() => {\n    const bar = barRef.current;\n    if (!bar) return;\n    const idx = items.findIndex((t) => t.value === active);\n    const btn = tabRefs.current[idx];\n    if (!btn) return;\n    // Use offsetLeft/offsetWidth — immune to ancestor CSS transforms (e.g. scale)\n    const x = btn.offsetLeft;\n    const w = btn.offsetWidth;\n    if (!measured.current) {\n      pillX.jump(x);\n      pillW.jump(w);\n      measured.current = true;\n    } else {\n      pillX.set(x);\n      pillW.set(w);\n    }\n  }, [active, items, pillX, pillW]);\n\n  useEffect(() => {\n    measure();\n    const ro = new ResizeObserver(measure);\n    if (barRef.current) ro.observe(barRef.current);\n    return () => ro.disconnect();\n  }, [measure]);\n\n  const go = useCallback(\n    (value: string) => {\n      if (value === active) return;\n      if (sound) tick(lastSound);\n      setActive(value);\n      onChange?.(value);\n    },\n    [active, onChange, sound],\n  );\n\n  return (\n    <div className={`it${className ? ` ${className}` : \"\"}`}>\n      <style dangerouslySetInnerHTML={{ __html: CSS }} />\n\n      <div\n        ref={barRef}\n        style={{\n          position: \"relative\",\n          display: \"inline-flex\",\n          alignItems: \"center\",\n          gap: 2,\n          padding: 4,\n          background: \"var(--it-bg)\",\n          borderRadius: 9999,\n          isolation: \"isolate\",\n          overflow: \"hidden\",\n        }}\n      >\n        {items.map((item, i) => (\n          <button\n            key={item.value}\n            ref={(el) => {\n              tabRefs.current[i] = el;\n            }}\n            onClick={() => go(item.value)}\n            style={{\n              position: \"relative\",\n              zIndex: 1,\n              display: \"flex\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n              height: 32,\n              border: \"none\",\n              background: \"none\",\n              padding: \"0 14px\",\n              fontSize: 14,\n              fontWeight: 500,\n              fontFamily: \"inherit\",\n              color: \"var(--it-text)\",\n              cursor: \"pointer\",\n              whiteSpace: \"nowrap\",\n              lineHeight: 1,\n              borderRadius: 9999,\n            }}\n          >\n            {item.label}\n          </button>\n        ))}\n\n        {/* Exclusion pill — white blends via exclusion to auto-invert */}\n        <motion.div\n          style={{\n            position: \"absolute\",\n            top: 4,\n            left: 0,\n            height: \"calc(100% - 8px)\",\n            x: springX,\n            width: springW,\n            background: \"var(--it-pill)\",\n            borderRadius: 9999,\n            mixBlendMode: \"exclusion\",\n            pointerEvents: \"none\",\n            zIndex: 2,\n          }}\n        />\n      </div>\n    </div>\n  );\n}\n\nexport default InvertTabs;\n",
      "type": "registry:ui",
      "target": "components/ruixen/invert-tabs.tsx"
    }
  ]
}