{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "morphing-nav",
  "type": "registry:ui",
  "title": "Morphing Nav",
  "description": "A Morphing Nav component for displaying a morphing navigation menu.",
  "files": [
    {
      "path": "src/registry/ui/morphing-nav.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { useId, useRef, useState } from \"react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\n// Indicators move on every arrow key, so they settle fast with barely any overshoot.\nconst INDICATOR_SPRING = { type: \"spring\", duration: 0.3, bounce: 0.1 } as const;\n\nexport type MorphingNavItem = {\n  value: string;\n  label: React.ReactNode;\n  icon?: React.ComponentType<{ className?: string }>;\n  /** When set, the item renders as a link instead of a button. */\n  href?: string;\n  disabled?: boolean;\n};\n\nexport type MorphingNavProps = Omit<\n  React.ComponentProps<\"div\">,\n  \"defaultValue\" | \"onChange\"\n> & {\n  items: MorphingNavItem[];\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  /** Element used for items with an `href`, e.g. Next.js `Link`. */\n  linkAs?: React.ElementType<React.ComponentProps<\"a\">>;\n  itemClassName?: string;\n  indicatorClassName?: string;\n};\n\nexport function MorphingNav({\n  items,\n  value,\n  defaultValue,\n  onValueChange,\n  linkAs: LinkComponent = \"a\",\n  className,\n  itemClassName,\n  indicatorClassName,\n  ...props\n}: MorphingNavProps) {\n  const indicatorId = useId();\n  const reduceMotion = useReducedMotion();\n  const itemRefs = useRef<(HTMLElement | null)[]>([]);\n  const [uncontrolledValue, setUncontrolledValue] = useState(\n    defaultValue ?? items[0]?.value\n  );\n  const activeValue = value ?? uncontrolledValue;\n  // Links navigate with Tab; buttons behave as a single-choice group with arrow keys.\n  const isLinkGroup = items.some((item) => item.href);\n  // A nav landmark only makes sense when the items are links.\n  const Root = (isLinkGroup ? \"nav\" : \"div\") as \"div\";\n\n  const select = (next: string) => {\n    if (value === undefined) setUncontrolledValue(next);\n    onValueChange?.(next);\n  };\n\n  const handleKeyDown = (event: React.KeyboardEvent, index: number) => {\n    if (isLinkGroup) return;\n    const step =\n      event.key === \"ArrowRight\" || event.key === \"ArrowDown\"\n        ? 1\n        : event.key === \"ArrowLeft\" || event.key === \"ArrowUp\"\n          ? -1\n          : 0;\n    if (!step) return;\n    event.preventDefault();\n\n    for (let offset = 1; offset <= items.length; offset++) {\n      const nextIndex = (index + step * offset + items.length) % items.length;\n      if (!items[nextIndex].disabled) {\n        select(items[nextIndex].value);\n        itemRefs.current[nextIndex]?.focus();\n        return;\n      }\n    }\n  };\n\n  return (\n    <Root data-slot=\"morphing-nav\" className={cn(\"inline-flex\", className)} {...props}>\n      <div\n        role={isLinkGroup ? undefined : \"radiogroup\"}\n        className=\"flex items-center gap-1 overflow-x-auto rounded-lg border bg-background p-1\"\n      >\n        {items.map((item, index) => {\n          const isActive = item.value === activeValue;\n          const Icon = item.icon;\n          const content = (\n            <>\n              {isActive && (\n                <motion.span\n                  layoutId={indicatorId}\n                  aria-hidden\n                  className={cn(\"absolute inset-0 rounded-md bg-primary\", indicatorClassName)}\n                  transition={\n                    reduceMotion ? { duration: 0 } : INDICATOR_SPRING\n                  }\n                />\n              )}\n              <span className=\"relative z-10 flex items-center gap-2\">\n                {Icon && <Icon className=\"size-4 shrink-0\" />}\n                {item.label}\n              </span>\n            </>\n          );\n          const itemClasses = cn(\n            \"relative rounded-md px-3 py-1.5 text-sm font-medium whitespace-nowrap transition-colors outline-none\",\n            \"focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\",\n            isActive\n              ? \"text-primary-foreground\"\n              : \"text-muted-foreground hover:text-foreground\",\n            itemClassName\n          );\n\n          if (item.href) {\n            return (\n              <LinkComponent\n                key={item.value}\n                ref={(node: HTMLAnchorElement | null) => {\n                  itemRefs.current[index] = node;\n                }}\n                href={item.href}\n                aria-current={isActive ? \"page\" : undefined}\n                aria-disabled={item.disabled || undefined}\n                onClick={() => select(item.value)}\n                className={itemClasses}\n              >\n                {content}\n              </LinkComponent>\n            );\n          }\n\n          return (\n            <button\n              key={item.value}\n              ref={(node) => {\n                itemRefs.current[index] = node;\n              }}\n              type=\"button\"\n              role=\"radio\"\n              aria-checked={isActive}\n              tabIndex={isActive ? 0 : -1}\n              disabled={item.disabled}\n              onClick={() => select(item.value)}\n              onKeyDown={(event) => handleKeyDown(event, index)}\n              className={itemClasses}\n            >\n              {content}\n            </button>\n          );\n        })}\n      </div>\n    </Root>\n  );\n}\n"
    }
  ],
  "dependencies": [
    "motion"
  ]
}
