{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glide-pill-tabs",
  "title": "Glide Pill Tabs",
  "author": "henriquegpb <https://github.com/henriquegpb>",
  "description": "A pill of filter tabs where the selection slides from option to option and a softer hover highlight glides behind them; when the options overflow, the row scrolls sideways to keep the chosen one in view.",
  "files": [
    {
      "path": "registry/microkit/glide-pill-tabs.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useLayoutEffect, useRef, useState } from \"react\";\n\nconst options = [\n  { key: \"all\", label: \"All\" },\n  { key: \"music\", label: \"Music\" },\n  { key: \"films\", label: \"Films\" },\n  { key: \"books\", label: \"Books\" },\n];\n\nexport function GlidePillTabs() {\n  const [value, setValue] = useState(\"all\");\n  const trackRef = useRef<HTMLDivElement>(null);\n  const indicatorRef = useRef<HTMLSpanElement>(null);\n  const glideRef = useRef<HTMLSpanElement>(null);\n  const placedRef = useRef(false);\n\n  useLayoutEffect(() => {\n    const track = trackRef.current;\n    const indicator = indicatorRef.current;\n    if (!track || !indicator) return;\n\n    const place = (animate: boolean) => {\n      const tab = track.querySelector<HTMLElement>(`[data-key=\"${CSS.escape(value)}\"]`);\n      if (!tab) return;\n      if (!animate) indicator.style.transition = \"none\";\n      indicator.style.transform = `translateX(${tab.offsetLeft}px)`;\n      indicator.style.width = `${tab.offsetWidth}px`;\n      indicator.dataset.visible = \"\";\n      if (!animate) {\n        void indicator.offsetWidth;\n        indicator.style.transition = \"\";\n      }\n      // The chosen tab is scrolled into view horizontally only, so the page itself never jumps.\n      const left = tab.offsetLeft - 24;\n      const right = tab.offsetLeft + tab.offsetWidth + 24;\n      if (left < track.scrollLeft) track.scrollTo({ left, behavior: animate ? \"smooth\" : \"auto\" });\n      else if (right > track.scrollLeft + track.clientWidth) {\n        track.scrollTo({ left: right - track.clientWidth, behavior: animate ? \"smooth\" : \"auto\" });\n      }\n    };\n\n    // The first placement appears in place; every change after it slides.\n    place(placedRef.current);\n    placedRef.current = true;\n\n    // Fonts loading or the row resizing move the tabs, so the pill follows them without sliding.\n    let lastWidth = track.getBoundingClientRect().width;\n    const observer = new ResizeObserver(() => {\n      const width = track.getBoundingClientRect().width;\n      if (Math.abs(width - lastWidth) < 0.5) return;\n      lastWidth = width;\n      place(false);\n    });\n    observer.observe(track);\n    return () => observer.disconnect();\n  }, [value]);\n\n  useEffect(() => {\n    const track = trackRef.current;\n    const glide = glideRef.current;\n    if (!track || !glide) return;\n\n    const controller = new AbortController();\n    const { signal } = controller;\n    let shown = false;\n\n    const place = (tab: HTMLElement) => {\n      const bounds = track.getBoundingClientRect();\n      const rect = tab.getBoundingClientRect();\n      const x = rect.left - bounds.left + track.scrollLeft - track.clientLeft;\n      const y = rect.top - bounds.top + track.scrollTop - track.clientTop;\n      // The first placement fades in where it lands; sliding in from wherever it last was would read as a glitch.\n      if (!shown) glide.style.transition = \"opacity 180ms ease\";\n      glide.style.transform = `translate(${x}px, ${y}px)`;\n      glide.style.width = `${rect.width}px`;\n      glide.style.height = `${rect.height}px`;\n      if (!shown) {\n        void glide.offsetWidth;\n        glide.style.transition = \"\";\n        shown = true;\n      }\n      glide.dataset.visible = \"\";\n    };\n\n    const hide = () => {\n      shown = false;\n      delete glide.dataset.visible;\n    };\n\n    for (const type of [\"pointerover\", \"focusin\"]) {\n      track.addEventListener(\n        type,\n        (event) => {\n          const tab = event.target instanceof Element ? event.target.closest<HTMLElement>(\"[data-pill-tab]\") : null;\n          if (tab && track.contains(tab)) place(tab);\n        },\n        { signal },\n      );\n    }\n    track.addEventListener(\"pointerleave\", hide, { signal });\n    track.addEventListener(\n      \"focusout\",\n      (event) => {\n        const inside = event.relatedTarget instanceof Node && track.contains(event.relatedTarget);\n        if (!inside && !track.matches(\":hover\")) hide();\n      },\n      { signal },\n    );\n\n    return () => controller.abort();\n  }, []);\n\n  return (\n    <div\n      className=\"relative flex w-fit max-w-full gap-0.5 overflow-x-auto rounded-full border border-[color:light-dark(rgba(20,24,30,.06),rgba(242,244,246,.06))] bg-[light-dark(rgba(20,24,30,.035),rgba(242,244,246,.035))] p-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n      role=\"tablist\"\n      aria-label=\"Categories\"\n      ref={trackRef}\n    >\n      <span\n        className=\"pointer-events-none absolute left-0 top-0 z-0 block size-0 rounded-full bg-[light-dark(rgba(20,24,30,.07),rgba(242,244,246,.07))] opacity-0 transition-[transform,width,height,opacity] duration-300 ease-[cubic-bezier(.32,.72,0,1)] data-[visible]:opacity-100 motion-reduce:transition-opacity\"\n        ref={glideRef}\n        aria-hidden=\"true\"\n      />\n      <span\n        className=\"pointer-events-none absolute inset-y-1 left-0 z-0 block w-0 rounded-full bg-[light-dark(#14181e,#f2f4f6)] opacity-0 transition-[transform,width,opacity] duration-[380ms] ease-[cubic-bezier(.32,.72,0,1)] data-[visible]:opacity-100 motion-reduce:transition-opacity\"\n        ref={indicatorRef}\n        aria-hidden=\"true\"\n      />\n      {options.map((option) => {\n        const selected = value === option.key;\n        return (\n          <button\n            key={option.key}\n            type=\"button\"\n            role=\"tab\"\n            className=\"relative z-[1] m-0 flex h-[30px] shrink-0 cursor-pointer items-center whitespace-nowrap rounded-full border-0 bg-transparent px-[13px] text-[12px] font-medium leading-none text-[color:light-dark(rgba(20,24,30,.6),rgba(242,244,246,.6))] transition-colors duration-[280ms] ease-[cubic-bezier(.4,0,.2,1)] hover:text-[color:light-dark(#14181e,#f2f4f6)] focus-visible:outline focus-visible:outline-[1.5px] focus-visible:-outline-offset-[1.5px] focus-visible:outline-[color:light-dark(rgba(20,24,30,.6),rgba(242,244,246,.6))] data-[selected]:text-[color:light-dark(#ffffff,#101216)] data-[selected]:hover:text-[color:light-dark(#ffffff,#101216)]\"\n            data-pill-tab=\"\"\n            data-key={option.key}\n            data-selected={selected || undefined}\n            aria-selected={selected}\n            onClick={() => setValue(option.key)}\n          >\n            {option.label}\n          </button>\n        );\n      })}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/microkit/glide-pill-tabs.tsx"
    }
  ],
  "docs": "Live preview and the CSS, JavaScript and Tailwind variants: https://microkit.co/components/glide-pill-tabs",
  "categories": [
    "navigation",
    "click"
  ],
  "type": "registry:component"
}