{
  "slug": "gooey-nav",
  "name": "Gooey Nav",
  "description": "Segmented tab navigation with dynamic organic bezier neck stretching and corner radius morphing.",
  "summary": "An organic navigation bar engineered with harmonic spring physics and dynamic SVG quadratic bezier neck interpolation. When switching active tabs, adjacent segments fluidly separate with an elastic connecting meniscus that pinches, stretches, and snaps across tabs, while border radii dynamically morph between fused and detached states.",
  "category": "navigation",
  "tags": [
    "navigation",
    "tabs",
    "gooey",
    "menu",
    "spring",
    "bezier",
    "liquid",
    "a11y"
  ],
  "dependencies": [
    "clsx",
    "tailwind-merge",
    "motion",
    "next"
  ],
  "version": "1.0.0",
  "createdDate": "2026-09-23",
  "updatedDate": "2026-09-23",
  "interactive": true,
  "supportsColor": false,
  "highlights": [
    "Dynamic SVG quadratic bezier neck with a waist that thins by a 1.4 power curve as the gap grows",
    "Simultaneous spring-driven corner radius morphing between fused (0px) and detached (corner) states",
    "Keyboard selection with ArrowLeft, ArrowRight, Home and End",
    "Two spring presets: fluid and elastic",
    "Multiple visual variants including solid elevated surface, vibrant aura glow, and frosted glass",
    "Independent active color palettes with matching linear gradients spanning the seam gaps"
  ],
  "anatomy": [
    "<nav> (Wrapper) containing <ul role=\"tablist\"> with the keyboard handler",
    "<Segment> (Spring-positioned segment with dynamic margin-left and corner radius morphing)",
    "<svg> (Connecting seam bridge rendering the dynamic bezier neck path)",
    "<linearGradient> (Dynamic two-stop color transition bridging neighboring tiles)",
    "<NavLabel> (Interactive tab link or button with ARIA attributes and active styling)"
  ],
  "physics": {
    "engine": "Motion Spring Dynamics & Quadratic Bezier Geometry",
    "description": "Damped harmonic oscillator governing seam separation distances coupled with mathematical waist thinning at NECK_BREAK = 0.44.",
    "parameters": [
      {
        "label": "Spring Stiffness",
        "value": "220 (fluid) / 320 (elastic)"
      },
      {
        "label": "Spring Damping",
        "value": "24 (fluid) / 18 (elastic)"
      },
      {
        "label": "Neck Break Threshold",
        "value": "0.44 gap / span"
      },
      {
        "label": "Waist Decay Exponent",
        "value": "1.4 power curve"
      },
      {
        "label": "Separation Span",
        "value": "14px - 24px"
      }
    ]
  },
  "accessibility": {
    "role": "tablist",
    "aria": "Standard role='tablist' on container, role='tab' on buttons, aria-selected for active state, and proper tabIndex handling.",
    "reducedMotion": "useReducedMotion fallback jumps positions instantly with zero duration.",
    "keyboard": [
      {
        "key": "Tab",
        "description": "Moves focus into the tablist (active tab only)."
      },
      {
        "key": "ArrowRight",
        "description": "Selects the next tab, wrapping from last to first."
      },
      {
        "key": "ArrowLeft",
        "description": "Selects the previous tab, wrapping from first to last."
      },
      {
        "key": "Home",
        "description": "Selects the first tab."
      },
      {
        "key": "End",
        "description": "Selects the last tab."
      },
      {
        "key": "Enter / Space",
        "description": "Activates the focused tab (button click or link navigation)."
      }
    ]
  },
  "guidelines": {
    "recommended": [
      "Primary application navigation bars and section tab switchers",
      "Dashboard sub-navigation and workspace segmented controls",
      "Interactive product feature walkthrough tabs"
    ],
    "bestPractices": [
      "Keep tab count between 3 and 7 for optimal horizontal stretching ergonomics",
      "Use distinct icons alongside labels for improved scannability",
      "Choose high-contrast active colors against dark backgrounds"
    ]
  },
  "props": [
    {
      "name": "items",
      "type": "GooeyNavItem[]",
      "required": true,
      "description": "Array of tab navigation items as strings or objects with label, icon, and optional href."
    },
    {
      "name": "value",
      "type": "number",
      "defaultValue": "undefined",
      "description": "Controlled active tab index."
    },
    {
      "name": "defaultValue",
      "type": "number",
      "defaultValue": "0",
      "description": "Initial tab index when uncontrolled."
    },
    {
      "name": "onChange",
      "type": "(index: number) => void",
      "defaultValue": "undefined",
      "description": "Callback triggered when the active tab index changes."
    },
    {
      "name": "size",
      "type": "\"xs\" | \"sm\" | \"md\" | \"lg\"",
      "defaultValue": "\"md\"",
      "description": "Size preset governing padding, typography, icon size, and separation distance."
    },
    {
      "name": "color",
      "type": "\"orange\" | \"emerald\" | \"violet\" | \"cyan\" | \"amber\" | \"monochrome\"",
      "defaultValue": "\"orange\"",
      "description": "Curated active theme palette styling."
    },
    {
      "name": "activeColor",
      "type": "string",
      "defaultValue": "undefined",
      "description": "Custom active background color override (hex/rgb)."
    },
    {
      "name": "activeLabelColor",
      "type": "string",
      "defaultValue": "undefined",
      "description": "Custom active label text color override."
    },
    {
      "name": "variant",
      "type": "\"solid\" | \"glow\" | \"glass\"",
      "defaultValue": "\"solid\"",
      "description": "Visual appearance style: solid elevated tiles, ambient glow, or frosted glass."
    },
    {
      "name": "elasticity",
      "type": "\"fluid\" | \"elastic\"",
      "defaultValue": "\"fluid\"",
      "description": "Spring dynamics preset governing transition stiffness and damping."
    },
    {
      "name": "separation",
      "type": "number",
      "defaultValue": "undefined",
      "description": "Custom gap distance in pixels between separated segments."
    },
    {
      "name": "radius",
      "type": "number",
      "defaultValue": "undefined",
      "description": "Custom corner border radius in pixels."
    },
    {
      "name": "className",
      "type": "string",
      "defaultValue": "undefined",
      "description": "Additional CSS classes passed to the outer nav element."
    }
  ],
  "usage": "\"use client\";\n\nimport { GooeyNav } from \"@/components/ui/gooey-nav\";\n\nexport function GooeyNavDemo() {\n  return (\n    <GooeyNav\n      items={[\"Home\", \"Docs\", \"Components\", \"Blog\"]}\n      onChange={(index) => console.log(index)}\n    />\n  );\n}",
  "files": [
    {
      "name": "gooey-nav.tsx",
      "path": "registry/ui/gooey-nav.tsx",
      "code": "\"use client\";\n\nimport {\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type ComponentProps,\n  type CSSProperties,\n  type KeyboardEvent,\n  type ReactNode,\n} from \"react\";\nimport Link from \"next/link\";\nimport { usePathname } from \"next/navigation\";\nimport {\n  motion,\n  useReducedMotion,\n  useSpring,\n  useTransform,\n} from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nconst SPRING_TUNING = {\n  fluid: { stiffness: 220, damping: 24, mass: 0.9 },\n  elastic: { stiffness: 320, damping: 18, mass: 1 },\n} as const;\n\nconst NECK_BREAK = 0.44;\nconst NECK_H = 100;\n\nconst SIZES = {\n  xs: {\n    label: \"gap-1.5 px-3 py-1.5 text-[11px] leading-4 [&_svg]:size-3.5\",\n    radius: 8,\n    separation: 14,\n  },\n  sm: {\n    label: \"gap-2 px-3.5 py-2 text-xs leading-4 [&_svg]:size-4\",\n    radius: 10,\n    separation: 16,\n  },\n  md: {\n    label: \"gap-2.5 px-5 py-2.5 text-sm leading-5 [&_svg]:size-4.5\",\n    radius: 12,\n    separation: 20,\n  },\n  lg: {\n    label: \"gap-3 px-6 py-3 text-base leading-6 [&_svg]:size-5\",\n    radius: 14,\n    separation: 24,\n  },\n} as const;\n\nexport type GooeyNavSize = keyof typeof SIZES;\nexport type GooeyNavElasticity = keyof typeof SPRING_TUNING;\nexport type GooeyNavVariant = \"solid\" | \"glow\" | \"glass\";\n\nexport type GooeyNavColor =\n  | \"orange\"\n  | \"emerald\"\n  | \"violet\"\n  | \"cyan\"\n  | \"amber\"\n  | \"monochrome\";\n\nconst COLOR_PRESETS: Record<\n  GooeyNavColor,\n  { hex: string; text: string; glow: string; dot: string }\n> = {\n  orange: {\n    hex: \"#FC4C01\",\n    text: \"#ffffff\",\n    glow: \"rgba(252,76,1,0.35)\",\n    dot: \"bg-[#FC4C01]\",\n  },\n  emerald: {\n    hex: \"#10B981\",\n    text: \"#ffffff\",\n    glow: \"rgba(16,185,129,0.35)\",\n    dot: \"bg-emerald-400\",\n  },\n  violet: {\n    hex: \"#8B5CF6\",\n    text: \"#ffffff\",\n    glow: \"rgba(139,92,246,0.35)\",\n    dot: \"bg-violet-400\",\n  },\n  cyan: {\n    hex: \"#06B6D4\",\n    text: \"#ffffff\",\n    glow: \"rgba(6,182,212,0.35)\",\n    dot: \"bg-cyan-400\",\n  },\n  amber: {\n    hex: \"#F59E0B\",\n    text: \"#ffffff\",\n    glow: \"rgba(245,158,11,0.35)\",\n    dot: \"bg-amber-400\",\n  },\n  monochrome: {\n    hex: \"#F4F4F5\",\n    text: \"#09090b\",\n    glow: \"rgba(244,244,245,0.25)\",\n    dot: \"bg-zinc-200\",\n  },\n};\n\nexport type GooeyNavItemObject = {\n  label: string;\n  href?: string;\n  icon?: ReactNode;\n  id?: string;\n};\n\nexport type GooeyNavItem = string | GooeyNavItemObject;\n\nconst toItem = (item: GooeyNavItem): GooeyNavItemObject =>\n  typeof item === \"string\" ? { label: item } : item;\n\nexport type GooeyNavProps = Omit<ComponentProps<\"nav\">, \"onChange\"> & {\n  items: GooeyNavItem[];\n  value?: number;\n  defaultValue?: number;\n  onChange?: (index: number) => void;\n  size?: GooeyNavSize;\n  color?: GooeyNavColor;\n  activeColor?: string;\n  activeLabelColor?: string;\n  variant?: GooeyNavVariant;\n  elasticity?: GooeyNavElasticity;\n  separation?: number;\n  radius?: number;\n};\n\nfunction neckPath(gap: number, span: number, breakRatio = NECK_BREAK) {\n  if (\n    !Number.isFinite(gap) ||\n    !Number.isFinite(span) ||\n    gap <= 0 ||\n    span <= 0\n  ) {\n    return \"\";\n  }\n  const progress = gap / (span * breakRatio);\n  if (progress >= 1) return \"\";\n  const waist = NECK_H * Math.pow(1 - progress, 1.4);\n  if (waist <= 0.2) return \"\";\n  const start = span - gap;\n  const mid = start + gap / 2;\n  const dip = (NECK_H - waist) / 2;\n  return `M${start} 0 Q${mid} ${dip} ${span} 0 L${span} ${NECK_H} Q${mid} ${NECK_H - dip} ${start} ${NECK_H} Z`;\n}\n\ntype SegmentProps = {\n  gap: number;\n  span: number;\n  hasSeam: boolean;\n  leftFill: string;\n  rightFill: string;\n  reduced: boolean;\n  radii: Record<string, number>;\n  springConfig: { stiffness: number; damping: number; mass: number };\n  className?: string;\n  style?: CSSProperties;\n  children: ReactNode;\n};\n\nfunction Segment({\n  gap,\n  span,\n  hasSeam,\n  leftFill,\n  rightFill,\n  reduced,\n  radii,\n  springConfig,\n  className,\n  style,\n  children,\n}: SegmentProps) {\n  const marginLeft = useSpring(gap, springConfig);\n  const gradientId = `gooey-neck-${useId().replace(/:/g, \"\")}`;\n\n  useEffect(() => {\n    if (reduced) marginLeft.jump(gap);\n    else marginLeft.set(gap);\n  }, [gap, marginLeft, reduced]);\n\n  const d = useTransform(marginLeft, (g) => neckPath(g, span));\n\n  return (\n    <motion.li\n      data-slot=\"gooey-nav-segment\"\n      className={cn(\"relative list-none\", className)}\n      style={{ ...style, marginLeft }}\n      initial={false}\n      animate={radii}\n      transition={\n        reduced ? { duration: 0 } : { type: \"spring\", ...springConfig }\n      }\n    >\n      {hasSeam && (\n        <svg\n          aria-hidden=\"true\"\n          width={span}\n          viewBox={`0 0 ${span} ${NECK_H}`}\n          preserveAspectRatio=\"none\"\n          className=\"pointer-events-none absolute top-0 right-full h-full overflow-visible\"\n        >\n          <defs>\n            <linearGradient id={gradientId} x1=\"0\" x2=\"1\" y1=\"0\" y2=\"0\">\n              <stop offset=\"0%\" stopColor={leftFill} />\n              <stop offset=\"100%\" stopColor={rightFill} />\n            </linearGradient>\n          </defs>\n          <motion.path d={d} fill={`url(#${gradientId})`} />\n        </svg>\n      )}\n      {children}\n    </motion.li>\n  );\n}\n\ntype NavLabelProps = GooeyNavItemObject & {\n  isActive: boolean;\n  size: GooeyNavSize;\n  activeLabelColor: string;\n  onSelect: () => void;\n  tabIndex: number;\n  id: string;\n};\n\nfunction NavLabel({\n  label,\n  href,\n  icon,\n  isActive,\n  size,\n  activeLabelColor,\n  onSelect,\n  tabIndex,\n  id,\n}: NavLabelProps) {\n  const props = {\n    id,\n    role: \"tab\",\n    \"aria-selected\": isActive,\n    tabIndex,\n    \"data-slot\": \"gooey-nav-item\",\n    \"data-active\": isActive,\n    className: cn(\n      \"relative z-10 flex cursor-pointer items-center justify-center whitespace-nowrap font-medium outline-none transition-all duration-300 ease-out select-none active:scale-95 focus-visible:ring-1 focus-visible:ring-zinc-400 dark:focus-visible:ring-white/30\",\n      SIZES[size].label,\n      isActive\n        ? \"opacity-100\"\n        : \"text-zinc-600 hover:text-zinc-950 dark:text-zinc-400 dark:hover:text-white opacity-80 hover:opacity-100\",\n    ),\n    style: isActive ? { color: activeLabelColor } : undefined,\n    onClick: onSelect,\n  } as const;\n\n  const content = (\n    <>\n      {icon && <span className=\"shrink-0\">{icon}</span>}\n      <span>{label}</span>\n    </>\n  );\n\n  return href ? (\n    <Link href={href} {...props}>\n      {content}\n    </Link>\n  ) : (\n    <button type=\"button\" {...props}>\n      {content}\n    </button>\n  );\n}\n\nexport function GooeyNav({\n  items,\n  value,\n  defaultValue = 0,\n  onChange,\n  size = \"md\",\n  color = \"orange\",\n  activeColor: customActiveColor,\n  activeLabelColor: customActiveLabelColor,\n  variant = \"solid\",\n  elasticity = \"fluid\",\n  separation,\n  radius,\n  className,\n  ...props\n}: GooeyNavProps) {\n  const pathname = usePathname();\n  const reduced = useReducedMotion() ?? false;\n  const navRef = useRef<HTMLElement>(null);\n\n  const routeIndex = items.findIndex((item) => toItem(item).href === pathname);\n  const [uncontrolled, setUncontrolled] = useState(() =>\n    routeIndex === -1 ? defaultValue : routeIndex,\n  );\n  const [seenRoute, setSeenRoute] = useState(routeIndex);\n\n  if (routeIndex !== seenRoute) {\n    setSeenRoute(routeIndex);\n    if (routeIndex !== -1 && value === undefined) setUncontrolled(routeIndex);\n  }\n\n  const active = Math.max(0, Math.min(items.length - 1, value ?? uncontrolled));\n  const span = separation ?? SIZES[size].separation;\n  const corner = radius ?? SIZES[size].radius;\n  const springConfig = SPRING_TUNING[elasticity];\n\n  const [isDark, setIsDark] = useState(true);\n\n  useEffect(() => {\n    const checkDark = () => {\n      setIsDark(document.documentElement.classList.contains(\"dark\"));\n    };\n    checkDark();\n    const observer = new MutationObserver(checkDark);\n    observer.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n    return () => observer.disconnect();\n  }, []);\n\n  const monoPreset = isDark\n    ? COLOR_PRESETS.monochrome\n    : {\n        hex: \"#18181b\",\n        text: \"#ffffff\",\n        glow: \"rgba(24,24,27,0.15)\",\n        dot: \"bg-zinc-900\",\n      };\n\n  const preset =\n    color === \"monochrome\"\n      ? monoPreset\n      : (COLOR_PRESETS[color] ?? COLOR_PRESETS.orange);\n\n  const computedActiveColor = customActiveColor ?? preset.hex;\n  const computedLabelColor = customActiveLabelColor ?? preset.text;\n\n  const open = (seam: number) =>\n    seam === 0 ||\n    seam === items.length ||\n    seam - 1 === active ||\n    seam === active;\n\n  const baseSurface =\n    variant === \"glass\"\n      ? \"bg-zinc-200/50 backdrop-blur-md border border-zinc-300/80 shadow-xs dark:bg-white/6 dark:border-white/10 dark:shadow-none\"\n      : \"bg-zinc-100 border border-zinc-200/80 shadow-xs dark:bg-[#18181b] dark:border-white/8 dark:shadow-none\";\n\n  const inactiveFill = isDark ? \"#18181b\" : \"#f4f4f5\";\n  const fill = (i: number) =>\n    i === active ? computedActiveColor : inactiveFill;\n\n  const select = (index: number) => {\n    if (value === undefined) setUncontrolled(index);\n    onChange?.(index);\n  };\n\n  const handleKeyDown = (e: KeyboardEvent<HTMLUListElement>) => {\n    const last = items.length - 1;\n    const target =\n      e.key === \"ArrowRight\"\n        ? (active + 1) % items.length\n        : e.key === \"ArrowLeft\"\n          ? (active - 1 + items.length) % items.length\n          : e.key === \"Home\"\n            ? 0\n            : e.key === \"End\"\n              ? last\n              : null;\n    if (target === null) return;\n    e.preventDefault();\n    select(target);\n    navRef.current\n      ?.querySelectorAll<HTMLElement>('[role=\"tab\"]')\n      [target]?.focus({ preventScroll: true });\n  };\n\n  useEffect(() => {\n    const nav = navRef.current;\n    const tab = nav?.querySelectorAll<HTMLElement>('[role=\"tab\"]')[active];\n    if (!nav || !tab || nav.scrollWidth <= nav.clientWidth) return;\n    const navBox = nav.getBoundingClientRect();\n    const tabBox = tab.getBoundingClientRect();\n    nav.scrollTo({\n      left:\n        nav.scrollLeft +\n        tabBox.left -\n        navBox.left -\n        (navBox.width - tabBox.width) / 2,\n      behavior: reduced ? \"auto\" : \"smooth\",\n    });\n  }, [active, reduced]);\n\n  return (\n    <nav\n      ref={navRef}\n      data-slot=\"gooey-nav\"\n      className={cn(\n        \"inline-block min-w-0 max-w-full select-none -my-6 overflow-x-auto py-6 [scrollbar-width:none]\",\n        className,\n      )}\n      {...props}\n    >\n      <ul\n        role=\"tablist\"\n        onKeyDown={handleKeyDown}\n        className=\"flex items-center m-0 p-0 list-none\"\n      >\n        {items.map((item, i) => {\n          const navItem = toItem(item);\n          const isActive = i === active;\n          const itemId = navItem.id ?? `gooey-tab-${i}`;\n\n          return (\n            <Segment\n              key={`${i}-${navItem.label}`}\n              gap={i === 0 ? 0 : open(i) ? span : -1}\n              span={span}\n              hasSeam={i > 0}\n              leftFill={fill(i - 1)}\n              rightFill={fill(i)}\n              reduced={reduced}\n              springConfig={springConfig}\n              radii={{\n                borderTopLeftRadius: open(i) ? corner : 0,\n                borderBottomLeftRadius: open(i) ? corner : 0,\n                borderTopRightRadius: open(i + 1) ? corner : 0,\n                borderBottomRightRadius: open(i + 1) ? corner : 0,\n              }}\n              className={cn(\n                \"transition-all duration-300 ease-out\",\n                baseSurface,\n                isActive &&\n                  variant === \"glow\" &&\n                  \"shadow-[0_0_24px_var(--glow-color)]\",\n              )}\n              style={\n                {\n                  \"--glow-color\": preset.glow,\n                  backgroundColor: isActive ? computedActiveColor : undefined,\n                  borderColor: isActive ? \"transparent\" : undefined,\n                } as CSSProperties\n              }\n            >\n              <NavLabel\n                {...navItem}\n                id={itemId}\n                isActive={isActive}\n                size={size}\n                activeLabelColor={computedLabelColor}\n                tabIndex={isActive ? 0 : -1}\n                onSelect={() => select(i)}\n              />\n            </Segment>\n          );\n        })}\n      </ul>\n    </nav>\n  );\n}\n\nexport default GooeyNav;\n",
      "content": "\"use client\";\n\nimport {\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type ComponentProps,\n  type CSSProperties,\n  type KeyboardEvent,\n  type ReactNode,\n} from \"react\";\nimport Link from \"next/link\";\nimport { usePathname } from \"next/navigation\";\nimport {\n  motion,\n  useReducedMotion,\n  useSpring,\n  useTransform,\n} from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nconst SPRING_TUNING = {\n  fluid: { stiffness: 220, damping: 24, mass: 0.9 },\n  elastic: { stiffness: 320, damping: 18, mass: 1 },\n} as const;\n\nconst NECK_BREAK = 0.44;\nconst NECK_H = 100;\n\nconst SIZES = {\n  xs: {\n    label: \"gap-1.5 px-3 py-1.5 text-[11px] leading-4 [&_svg]:size-3.5\",\n    radius: 8,\n    separation: 14,\n  },\n  sm: {\n    label: \"gap-2 px-3.5 py-2 text-xs leading-4 [&_svg]:size-4\",\n    radius: 10,\n    separation: 16,\n  },\n  md: {\n    label: \"gap-2.5 px-5 py-2.5 text-sm leading-5 [&_svg]:size-4.5\",\n    radius: 12,\n    separation: 20,\n  },\n  lg: {\n    label: \"gap-3 px-6 py-3 text-base leading-6 [&_svg]:size-5\",\n    radius: 14,\n    separation: 24,\n  },\n} as const;\n\nexport type GooeyNavSize = keyof typeof SIZES;\nexport type GooeyNavElasticity = keyof typeof SPRING_TUNING;\nexport type GooeyNavVariant = \"solid\" | \"glow\" | \"glass\";\n\nexport type GooeyNavColor =\n  | \"orange\"\n  | \"emerald\"\n  | \"violet\"\n  | \"cyan\"\n  | \"amber\"\n  | \"monochrome\";\n\nconst COLOR_PRESETS: Record<\n  GooeyNavColor,\n  { hex: string; text: string; glow: string; dot: string }\n> = {\n  orange: {\n    hex: \"#FC4C01\",\n    text: \"#ffffff\",\n    glow: \"rgba(252,76,1,0.35)\",\n    dot: \"bg-[#FC4C01]\",\n  },\n  emerald: {\n    hex: \"#10B981\",\n    text: \"#ffffff\",\n    glow: \"rgba(16,185,129,0.35)\",\n    dot: \"bg-emerald-400\",\n  },\n  violet: {\n    hex: \"#8B5CF6\",\n    text: \"#ffffff\",\n    glow: \"rgba(139,92,246,0.35)\",\n    dot: \"bg-violet-400\",\n  },\n  cyan: {\n    hex: \"#06B6D4\",\n    text: \"#ffffff\",\n    glow: \"rgba(6,182,212,0.35)\",\n    dot: \"bg-cyan-400\",\n  },\n  amber: {\n    hex: \"#F59E0B\",\n    text: \"#ffffff\",\n    glow: \"rgba(245,158,11,0.35)\",\n    dot: \"bg-amber-400\",\n  },\n  monochrome: {\n    hex: \"#F4F4F5\",\n    text: \"#09090b\",\n    glow: \"rgba(244,244,245,0.25)\",\n    dot: \"bg-zinc-200\",\n  },\n};\n\nexport type GooeyNavItemObject = {\n  label: string;\n  href?: string;\n  icon?: ReactNode;\n  id?: string;\n};\n\nexport type GooeyNavItem = string | GooeyNavItemObject;\n\nconst toItem = (item: GooeyNavItem): GooeyNavItemObject =>\n  typeof item === \"string\" ? { label: item } : item;\n\nexport type GooeyNavProps = Omit<ComponentProps<\"nav\">, \"onChange\"> & {\n  items: GooeyNavItem[];\n  value?: number;\n  defaultValue?: number;\n  onChange?: (index: number) => void;\n  size?: GooeyNavSize;\n  color?: GooeyNavColor;\n  activeColor?: string;\n  activeLabelColor?: string;\n  variant?: GooeyNavVariant;\n  elasticity?: GooeyNavElasticity;\n  separation?: number;\n  radius?: number;\n};\n\nfunction neckPath(gap: number, span: number, breakRatio = NECK_BREAK) {\n  if (\n    !Number.isFinite(gap) ||\n    !Number.isFinite(span) ||\n    gap <= 0 ||\n    span <= 0\n  ) {\n    return \"\";\n  }\n  const progress = gap / (span * breakRatio);\n  if (progress >= 1) return \"\";\n  const waist = NECK_H * Math.pow(1 - progress, 1.4);\n  if (waist <= 0.2) return \"\";\n  const start = span - gap;\n  const mid = start + gap / 2;\n  const dip = (NECK_H - waist) / 2;\n  return `M${start} 0 Q${mid} ${dip} ${span} 0 L${span} ${NECK_H} Q${mid} ${NECK_H - dip} ${start} ${NECK_H} Z`;\n}\n\ntype SegmentProps = {\n  gap: number;\n  span: number;\n  hasSeam: boolean;\n  leftFill: string;\n  rightFill: string;\n  reduced: boolean;\n  radii: Record<string, number>;\n  springConfig: { stiffness: number; damping: number; mass: number };\n  className?: string;\n  style?: CSSProperties;\n  children: ReactNode;\n};\n\nfunction Segment({\n  gap,\n  span,\n  hasSeam,\n  leftFill,\n  rightFill,\n  reduced,\n  radii,\n  springConfig,\n  className,\n  style,\n  children,\n}: SegmentProps) {\n  const marginLeft = useSpring(gap, springConfig);\n  const gradientId = `gooey-neck-${useId().replace(/:/g, \"\")}`;\n\n  useEffect(() => {\n    if (reduced) marginLeft.jump(gap);\n    else marginLeft.set(gap);\n  }, [gap, marginLeft, reduced]);\n\n  const d = useTransform(marginLeft, (g) => neckPath(g, span));\n\n  return (\n    <motion.li\n      data-slot=\"gooey-nav-segment\"\n      className={cn(\"relative list-none\", className)}\n      style={{ ...style, marginLeft }}\n      initial={false}\n      animate={radii}\n      transition={\n        reduced ? { duration: 0 } : { type: \"spring\", ...springConfig }\n      }\n    >\n      {hasSeam && (\n        <svg\n          aria-hidden=\"true\"\n          width={span}\n          viewBox={`0 0 ${span} ${NECK_H}`}\n          preserveAspectRatio=\"none\"\n          className=\"pointer-events-none absolute top-0 right-full h-full overflow-visible\"\n        >\n          <defs>\n            <linearGradient id={gradientId} x1=\"0\" x2=\"1\" y1=\"0\" y2=\"0\">\n              <stop offset=\"0%\" stopColor={leftFill} />\n              <stop offset=\"100%\" stopColor={rightFill} />\n            </linearGradient>\n          </defs>\n          <motion.path d={d} fill={`url(#${gradientId})`} />\n        </svg>\n      )}\n      {children}\n    </motion.li>\n  );\n}\n\ntype NavLabelProps = GooeyNavItemObject & {\n  isActive: boolean;\n  size: GooeyNavSize;\n  activeLabelColor: string;\n  onSelect: () => void;\n  tabIndex: number;\n  id: string;\n};\n\nfunction NavLabel({\n  label,\n  href,\n  icon,\n  isActive,\n  size,\n  activeLabelColor,\n  onSelect,\n  tabIndex,\n  id,\n}: NavLabelProps) {\n  const props = {\n    id,\n    role: \"tab\",\n    \"aria-selected\": isActive,\n    tabIndex,\n    \"data-slot\": \"gooey-nav-item\",\n    \"data-active\": isActive,\n    className: cn(\n      \"relative z-10 flex cursor-pointer items-center justify-center whitespace-nowrap font-medium outline-none transition-all duration-300 ease-out select-none active:scale-95 focus-visible:ring-1 focus-visible:ring-zinc-400 dark:focus-visible:ring-white/30\",\n      SIZES[size].label,\n      isActive\n        ? \"opacity-100\"\n        : \"text-zinc-600 hover:text-zinc-950 dark:text-zinc-400 dark:hover:text-white opacity-80 hover:opacity-100\",\n    ),\n    style: isActive ? { color: activeLabelColor } : undefined,\n    onClick: onSelect,\n  } as const;\n\n  const content = (\n    <>\n      {icon && <span className=\"shrink-0\">{icon}</span>}\n      <span>{label}</span>\n    </>\n  );\n\n  return href ? (\n    <Link href={href} {...props}>\n      {content}\n    </Link>\n  ) : (\n    <button type=\"button\" {...props}>\n      {content}\n    </button>\n  );\n}\n\nexport function GooeyNav({\n  items,\n  value,\n  defaultValue = 0,\n  onChange,\n  size = \"md\",\n  color = \"orange\",\n  activeColor: customActiveColor,\n  activeLabelColor: customActiveLabelColor,\n  variant = \"solid\",\n  elasticity = \"fluid\",\n  separation,\n  radius,\n  className,\n  ...props\n}: GooeyNavProps) {\n  const pathname = usePathname();\n  const reduced = useReducedMotion() ?? false;\n  const navRef = useRef<HTMLElement>(null);\n\n  const routeIndex = items.findIndex((item) => toItem(item).href === pathname);\n  const [uncontrolled, setUncontrolled] = useState(() =>\n    routeIndex === -1 ? defaultValue : routeIndex,\n  );\n  const [seenRoute, setSeenRoute] = useState(routeIndex);\n\n  if (routeIndex !== seenRoute) {\n    setSeenRoute(routeIndex);\n    if (routeIndex !== -1 && value === undefined) setUncontrolled(routeIndex);\n  }\n\n  const active = Math.max(0, Math.min(items.length - 1, value ?? uncontrolled));\n  const span = separation ?? SIZES[size].separation;\n  const corner = radius ?? SIZES[size].radius;\n  const springConfig = SPRING_TUNING[elasticity];\n\n  const [isDark, setIsDark] = useState(true);\n\n  useEffect(() => {\n    const checkDark = () => {\n      setIsDark(document.documentElement.classList.contains(\"dark\"));\n    };\n    checkDark();\n    const observer = new MutationObserver(checkDark);\n    observer.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n    return () => observer.disconnect();\n  }, []);\n\n  const monoPreset = isDark\n    ? COLOR_PRESETS.monochrome\n    : {\n        hex: \"#18181b\",\n        text: \"#ffffff\",\n        glow: \"rgba(24,24,27,0.15)\",\n        dot: \"bg-zinc-900\",\n      };\n\n  const preset =\n    color === \"monochrome\"\n      ? monoPreset\n      : (COLOR_PRESETS[color] ?? COLOR_PRESETS.orange);\n\n  const computedActiveColor = customActiveColor ?? preset.hex;\n  const computedLabelColor = customActiveLabelColor ?? preset.text;\n\n  const open = (seam: number) =>\n    seam === 0 ||\n    seam === items.length ||\n    seam - 1 === active ||\n    seam === active;\n\n  const baseSurface =\n    variant === \"glass\"\n      ? \"bg-zinc-200/50 backdrop-blur-md border border-zinc-300/80 shadow-xs dark:bg-white/6 dark:border-white/10 dark:shadow-none\"\n      : \"bg-zinc-100 border border-zinc-200/80 shadow-xs dark:bg-[#18181b] dark:border-white/8 dark:shadow-none\";\n\n  const inactiveFill = isDark ? \"#18181b\" : \"#f4f4f5\";\n  const fill = (i: number) =>\n    i === active ? computedActiveColor : inactiveFill;\n\n  const select = (index: number) => {\n    if (value === undefined) setUncontrolled(index);\n    onChange?.(index);\n  };\n\n  const handleKeyDown = (e: KeyboardEvent<HTMLUListElement>) => {\n    const last = items.length - 1;\n    const target =\n      e.key === \"ArrowRight\"\n        ? (active + 1) % items.length\n        : e.key === \"ArrowLeft\"\n          ? (active - 1 + items.length) % items.length\n          : e.key === \"Home\"\n            ? 0\n            : e.key === \"End\"\n              ? last\n              : null;\n    if (target === null) return;\n    e.preventDefault();\n    select(target);\n    navRef.current\n      ?.querySelectorAll<HTMLElement>('[role=\"tab\"]')\n      [target]?.focus({ preventScroll: true });\n  };\n\n  useEffect(() => {\n    const nav = navRef.current;\n    const tab = nav?.querySelectorAll<HTMLElement>('[role=\"tab\"]')[active];\n    if (!nav || !tab || nav.scrollWidth <= nav.clientWidth) return;\n    const navBox = nav.getBoundingClientRect();\n    const tabBox = tab.getBoundingClientRect();\n    nav.scrollTo({\n      left:\n        nav.scrollLeft +\n        tabBox.left -\n        navBox.left -\n        (navBox.width - tabBox.width) / 2,\n      behavior: reduced ? \"auto\" : \"smooth\",\n    });\n  }, [active, reduced]);\n\n  return (\n    <nav\n      ref={navRef}\n      data-slot=\"gooey-nav\"\n      className={cn(\n        \"inline-block min-w-0 max-w-full select-none -my-6 overflow-x-auto py-6 [scrollbar-width:none]\",\n        className,\n      )}\n      {...props}\n    >\n      <ul\n        role=\"tablist\"\n        onKeyDown={handleKeyDown}\n        className=\"flex items-center m-0 p-0 list-none\"\n      >\n        {items.map((item, i) => {\n          const navItem = toItem(item);\n          const isActive = i === active;\n          const itemId = navItem.id ?? `gooey-tab-${i}`;\n\n          return (\n            <Segment\n              key={`${i}-${navItem.label}`}\n              gap={i === 0 ? 0 : open(i) ? span : -1}\n              span={span}\n              hasSeam={i > 0}\n              leftFill={fill(i - 1)}\n              rightFill={fill(i)}\n              reduced={reduced}\n              springConfig={springConfig}\n              radii={{\n                borderTopLeftRadius: open(i) ? corner : 0,\n                borderBottomLeftRadius: open(i) ? corner : 0,\n                borderTopRightRadius: open(i + 1) ? corner : 0,\n                borderBottomRightRadius: open(i + 1) ? corner : 0,\n              }}\n              className={cn(\n                \"transition-all duration-300 ease-out\",\n                baseSurface,\n                isActive &&\n                  variant === \"glow\" &&\n                  \"shadow-[0_0_24px_var(--glow-color)]\",\n              )}\n              style={\n                {\n                  \"--glow-color\": preset.glow,\n                  backgroundColor: isActive ? computedActiveColor : undefined,\n                  borderColor: isActive ? \"transparent\" : undefined,\n                } as CSSProperties\n              }\n            >\n              <NavLabel\n                {...navItem}\n                id={itemId}\n                isActive={isActive}\n                size={size}\n                activeLabelColor={computedLabelColor}\n                tabIndex={isActive ? 0 : -1}\n                onSelect={() => select(i)}\n              />\n            </Segment>\n          );\n        })}\n      </ul>\n    </nav>\n  );\n}\n\nexport default GooeyNav;\n",
      "type": "registry:ui",
      "target": "@ui/gooey-nav.tsx"
    }
  ],
  "type": "registry:ui",
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "title": "Gooey Nav"
}
