{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sliding-tabs",
  "type": "registry:ui",
  "title": "Sliding Tabs",
  "description": "A pill nav whose lime indicator scales in under the first tab, then slides to follow the pointer or keyboard focus. Leaving the nav returns it to the active tab.",
  "dependencies": [
    "gsap",
    "@gsap/react"
  ],
  "registryDependencies": [],
  "cssVars": {
    "theme": {}
  },
  "files": [
    {
      "path": "components/tweenui/sliding-tabs.tsx",
      "type": "registry:component",
      "target": "components/tweenui/sliding-tabs.tsx",
      "content": "'use client';\n\nimport {\n  useRef,\n  useState,\n  type ComponentPropsWithoutRef,\n  type FocusEvent,\n  type PointerEvent,\n} from 'react';\nimport { useGSAP } from '@gsap/react';\nimport gsap from 'gsap';\nimport { CustomEase } from 'gsap/CustomEase';\nimport { cn } from '@/lib/utils';\n\ngsap.registerPlugin(useGSAP, CustomEase);\n\nconst ITEM_CLASS =\n  'relative z-10 cursor-pointer rounded-md border-0 bg-transparent px-4 py-2 font-[family-name:Outfit,sans-serif] text-sm font-normal text-white no-underline outline-none transition-colors duration-500 ease-out hover:text-[#045f64] data-[highlighted=true]:text-[#045f64] focus-visible:text-[#045f64] focus-visible:ring-2 focus-visible:ring-white motion-reduce:transition-none';\n\nconst prefersReducedMotion = () =>\n  typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n// Bounds are measured against the item's offsetParent, which must be the\n// positioned <nav>. Don't wrap the items in another positioned element or the\n// indicator will be offset relative to the wrong ancestor.\nconst getItemBounds = (item: HTMLElement) => {\n  const { offsetLeft: left, offsetTop: top, offsetWidth: width, offsetHeight: height } = item;\n  return { left, top, width, height };\n};\n\nexport interface SlidingTabsItem {\n  value: string;\n  label: string;\n  href?: string;\n}\n\ninterface SlidingTabsProps extends Omit<ComponentPropsWithoutRef<'nav'>, 'onChange'> {\n  items: SlidingTabsItem[];\n  defaultValue?: string;\n  value?: string;\n  onValueChange?: (value: string) => void;\n}\n\nexport default function SlidingTabs({\n  items,\n  defaultValue,\n  value: valueProp,\n  onValueChange,\n  className,\n  ...props\n}: SlidingTabsProps) {\n  const navRef = useRef<HTMLElement>(null);\n  const indicatorRef = useRef<HTMLDivElement>(null);\n  const visibleRef = useRef(false);\n  const [uncontrolled, setUncontrolled] = useState(defaultValue);\n  const isControlled = valueProp !== undefined;\n  const activeValue = isControlled ? valueProp : uncontrolled;\n\n  const highlightItem = (item: HTMLElement | null) => {\n    navRef.current?.querySelectorAll<HTMLElement>('[data-nav-item]').forEach((el) => {\n      el.dataset.highlighted = el === item ? 'true' : 'false';\n    });\n  };\n\n  const moveToItem = (item: HTMLElement) => {\n    const indicator = indicatorRef.current;\n    if (!indicator) return;\n\n    highlightItem(item);\n    const bounds = getItemBounds(item);\n    const reduced = prefersReducedMotion();\n\n    if (!visibleRef.current) {\n      gsap.set(indicator, { ...bounds, opacity: 1, scale: reduced ? 1 : 0 });\n      if (!reduced) {\n        gsap.to(indicator, { scale: 1, duration: 0.6, ease: 'bouncy-ease', overwrite: 'auto' });\n      }\n      visibleRef.current = true;\n      return;\n    }\n\n    if (reduced) {\n      gsap.set(indicator, { ...bounds, opacity: 1, scale: 1 });\n      return;\n    }\n\n    gsap.to(indicator, {\n      ...bounds,\n      opacity: 1,\n      scale: 1,\n      duration: 0.35,\n      ease: 'power2.out',\n      overwrite: 'auto',\n    });\n  };\n\n  const hideIndicator = () => {\n    const active = navRef.current?.querySelector<HTMLElement>(\n      '[data-nav-item][data-active=\"true\"]'\n    );\n    if (active) {\n      moveToItem(active);\n      return;\n    }\n\n    const indicator = indicatorRef.current;\n    if (!indicator) return;\n\n    highlightItem(null);\n    if (prefersReducedMotion()) {\n      gsap.set(indicator, { opacity: 0, scale: 0 });\n      visibleRef.current = false;\n      return;\n    }\n\n    gsap.to(indicator, {\n      opacity: 0,\n      scale: 0,\n      duration: 0.25,\n      ease: 'power2.in',\n      overwrite: 'auto',\n      onComplete: () => {\n        visibleRef.current = false;\n      },\n    });\n  };\n\n  useGSAP(\n    () => {\n      CustomEase.create('bouncy-ease', '0.34, 1.42, 0.64, 1');\n      const nav = navRef.current;\n      const indicator = indicatorRef.current;\n      if (!nav || !indicator) return;\n\n      gsap.set(indicator, { opacity: 0, scale: 0, transformOrigin: 'center center' });\n\n      const active = nav.querySelector<HTMLElement>('[data-nav-item][data-active=\"true\"]');\n      if (active) requestAnimationFrame(() => moveToItem(active));\n\n      const onResize = () => {\n        const current = nav.querySelector<HTMLElement>('[data-nav-item][data-active=\"true\"]');\n        if (current) {\n          highlightItem(current);\n          gsap.set(indicator, { ...getItemBounds(current), opacity: 1, scale: 1 });\n          visibleRef.current = true;\n          return;\n        }\n        highlightItem(null);\n        gsap.set(indicator, { opacity: 0, scale: 0 });\n        visibleRef.current = false;\n      };\n\n      window.addEventListener('resize', onResize);\n      return () => window.removeEventListener('resize', onResize);\n    },\n    { scope: navRef }\n  );\n\n  const onEnter = (event: FocusEvent<HTMLElement> | PointerEvent<HTMLElement>) => {\n    const item = (event.target as HTMLElement | null)?.closest('[data-nav-item]');\n    if (item instanceof HTMLElement) moveToItem(item);\n  };\n\n  return (\n    <nav\n      {...props}\n      ref={navRef}\n      data-nav-tabs\n      aria-label={props['aria-label'] ?? 'Sliding tabs'}\n      className={cn(\n        'relative inline-flex items-center rounded-xl bg-[#045f64] p-1 text-white shadow-[0_1px_1px_rgba(16,24,40,0.16)] select-none',\n        className\n      )}\n      onPointerOver={onEnter}\n      onPointerLeave={hideIndicator}\n      onFocus={onEnter}\n      onBlur={(event) => {\n        if (!event.currentTarget.contains(event.relatedTarget as Node | null)) hideIndicator();\n      }}\n    >\n      <div\n        ref={indicatorRef}\n        data-nav-indicator\n        className=\"pointer-events-none absolute origin-center rounded-md bg-[#c6f56f] motion-reduce:transition-none\"\n        aria-hidden=\"true\"\n      />\n      {items.map((item) => {\n        const isActive = item.value === activeValue;\n        const shared = {\n          'data-nav-item': true,\n          'data-active': isActive ? 'true' : 'false',\n          'data-highlighted': isActive ? 'true' : 'false',\n          className: ITEM_CLASS,\n        } as const;\n\n        if (item.href) {\n          return (\n            <a\n              key={item.value}\n              href={item.href}\n              {...shared}\n              aria-current={isActive ? 'page' : undefined}\n              onClick={() => {\n                if (!isControlled) setUncontrolled(item.value);\n                onValueChange?.(item.value);\n              }}\n            >\n              {item.label}\n            </a>\n          );\n        }\n\n        return (\n          <button\n            key={item.value}\n            type=\"button\"\n            {...shared}\n            aria-current={isActive ? 'true' : undefined}\n            onClick={() => {\n              if (!isControlled) setUncontrolled(item.value);\n              onValueChange?.(item.value);\n            }}\n          >\n            {item.label}\n          </button>\n        );\n      })}\n    </nav>\n  );\n}\n"
    }
  ],
  "docs": "Docs, props and examples: https://tween-ui.vercel.app/component/sliding-tabs\nGSAP plugins (CustomEase) are registered inside the component, so there is nothing to set up."
}