{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gooey-nav",
  "type": "registry:ui",
  "title": "Gooey Nav",
  "description": "Segmented tab navigation with dynamic organic bezier neck stretching and corner radius morphing.",
  "dependencies": [
    "clsx",
    "tailwind-merge",
    "motion",
    "next"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/gooey-nav.tsx",
      "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"
    }
  ]
}
