{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "motion-navigation-menu",
  "title": "Motion Navigation Menu",
  "description": "A spring-animated navigation menu with a single morphing container, layout-animated active pill, and direction-aware content transitions.",
  "dependencies": [
    "framer-motion",
    "lucide-react",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "@unlumen-ui/highlight"
  ],
  "files": [
    {
      "path": "registry/components/unlumen/motion-navigation-menu/index.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cva } from \"class-variance-authority\";\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport { ChevronDownIcon } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Highlight, HighlightItem } from \"@/components/unlumen-ui/primitives/highlight\";\n\ntype Spring = {\n  type: \"spring\";\n  stiffness?: number;\n  damping?: number;\n  bounce: number;\n};\n\ntype ContentRecord = {\n  children: React.ReactNode;\n  className?: string;\n  highlightClassName?: string;\n  innerClassName?: string;\n};\n\ntype MotionNavigationMenuContextValue = {\n  activeValue: string;\n  direction: number;\n  spring: Spring;\n  viewport: boolean;\n  viewportX: number | null;\n  openValue: (value: string) => void;\n  closeMenu: () => void;\n  registerContent: (value: string, content: ContentRecord) => () => void;\n  updateViewportPosition: () => void;\n};\n\ntype MotionNavigationMenuItemContextValue = {\n  value?: string;\n};\n\nconst MotionNavigationMenuContext =\n  React.createContext<MotionNavigationMenuContextValue | null>(null);\n\nconst MotionNavigationMenuItemContext =\n  React.createContext<MotionNavigationMenuItemContextValue | null>(null);\n\nconst contentVariants = {\n  initial: (direction: number) => ({ x: `${100 * direction}%`, opacity: 0 }),\n  active: { x: \"0%\", opacity: 1 },\n  exit: (direction: number) => ({ x: `${-100 * direction}%`, opacity: 0 }),\n};\n\ntype MotionNavigationMenuProps = Omit<\n  React.ComponentPropsWithRef<\"nav\">,\n  \"onValueChange\"\n> & {\n  viewport?: boolean;\n  viewportClassName?: string;\n  springBounce?: number;\n  springStiffness?: number;\n  springDamping?: number;\n  value?: string;\n  onValueChange?: (value: string) => void;\n};\n\nfunction MotionNavigationMenu({\n  className,\n  children,\n  viewport = true,\n  viewportClassName,\n  springBounce = 0,\n  springStiffness = 350,\n  springDamping = 32,\n  value,\n  onValueChange,\n  onPointerLeave,\n  onKeyDown,\n  ref,\n  ...props\n}: MotionNavigationMenuProps) {\n  const rootRef = React.useRef<HTMLElement | null>(null);\n  const frameRef = React.useRef<number | null>(null);\n  const lastActiveValueRef = React.useRef(value ?? \"\");\n  const isControlled = value !== undefined;\n  const [internalValue, setInternalValue] = React.useState(\"\");\n  const [direction, setDirection] = React.useState(1);\n  const [viewportX, setViewportX] = React.useState<number | null>(null);\n  const [contentByValue, setContentByValue] = React.useState<\n    Record<string, ContentRecord>\n  >({});\n\n  const activeValue = value ?? internalValue;\n\n  const spring = React.useMemo(\n    () => ({\n      type: \"spring\" as const,\n      bounce: springBounce,\n      stiffness: springStiffness,\n      damping: springDamping,\n    }),\n    [springBounce, springStiffness, springDamping],\n  );\n\n  const getItemValues = React.useCallback(() => {\n    const root = rootRef.current;\n\n    if (!root) {\n      return [];\n    }\n\n    return Array.from(\n      root.querySelectorAll<HTMLElement>(\n        '[data-slot=\"navigation-menu-item\"][data-value]',\n      ),\n      (item) => item.dataset.value ?? \"\",\n    ).filter(Boolean);\n  }, []);\n\n  const updateViewportPosition = React.useCallback(() => {\n    if (frameRef.current !== null) {\n      cancelAnimationFrame(frameRef.current);\n    }\n\n    frameRef.current = requestAnimationFrame(() => {\n      const root = rootRef.current;\n\n      if (!root) {\n        return;\n      }\n\n      const rootRect = root.getBoundingClientRect();\n      const activeTrigger = root.querySelector<HTMLElement>(\n        '[data-slot=\"navigation-menu-trigger\"][data-state=\"open\"]',\n      );\n\n      if (!activeTrigger) {\n        setViewportX(rootRect.width / 2);\n        return;\n      }\n\n      const triggerRect = activeTrigger.getBoundingClientRect();\n      const idealX = triggerRect.left - rootRect.left + triggerRect.width / 2;\n\n      const measureEl = root.querySelector<HTMLElement>(\n        '[data-slot=\"navigation-menu-measure\"]',\n      );\n      const viewportEl = root.querySelector<HTMLElement>(\n        '[data-slot=\"navigation-menu-viewport\"]',\n      );\n      const contentWidth =\n        (measureEl ? measureEl.offsetWidth : 0) ||\n        (viewportEl ? viewportEl.offsetWidth : 0);\n      const half = contentWidth / 2;\n\n      if (contentWidth > 0) {\n        // Find the nearest clipping ancestor to use as the boundary\n        let boundary: DOMRect | null = null;\n        let ancestor = root.parentElement;\n        while (ancestor && ancestor !== document.body) {\n          const style = window.getComputedStyle(ancestor);\n          const overflow = style.overflow + style.overflowX;\n          if (/hidden|clip|scroll|auto/.test(overflow)) {\n            boundary = ancestor.getBoundingClientRect();\n            break;\n          }\n          ancestor = ancestor.parentElement;\n        }\n        if (!boundary) {\n          boundary = document.documentElement.getBoundingClientRect();\n        }\n\n        const margin = 8;\n        const dropLeft = rootRect.left + idealX - half;\n        const dropRight = rootRect.left + idealX + half;\n\n        let adjustment = 0;\n        if (dropLeft < boundary.left + margin) {\n          adjustment = boundary.left + margin - dropLeft;\n        } else if (dropRight > boundary.right - margin) {\n          adjustment = boundary.right - margin - dropRight;\n        }\n\n        setViewportX(idealX + adjustment);\n      } else {\n        setViewportX(idealX);\n      }\n    });\n  }, []);\n\n  const setRootRef = React.useCallback(\n    (node: HTMLElement | null) => {\n      rootRef.current = node;\n\n      if (typeof ref === \"function\") {\n        ref(node);\n      } else if (ref) {\n        ref.current = node;\n      }\n    },\n    [ref],\n  );\n\n  const setActiveValue = React.useCallback(\n    (nextValue: string) => {\n      if (!isControlled) {\n        setInternalValue(nextValue);\n      }\n\n      onValueChange?.(nextValue);\n    },\n    [isControlled, onValueChange],\n  );\n\n  const openValue = React.useCallback(\n    (nextValue: string) => {\n      if (!nextValue || nextValue === lastActiveValueRef.current) {\n        return;\n      }\n\n      const itemValues = getItemValues();\n      const previousIndex = itemValues.indexOf(lastActiveValueRef.current);\n      const nextIndex = itemValues.indexOf(nextValue);\n\n      if (previousIndex !== -1 && nextIndex !== -1) {\n        setDirection(nextIndex > previousIndex ? 1 : -1);\n      }\n\n      lastActiveValueRef.current = nextValue;\n      setActiveValue(nextValue);\n      updateViewportPosition();\n    },\n    [getItemValues, setActiveValue, updateViewportPosition],\n  );\n\n  const closeMenu = React.useCallback(() => {\n    lastActiveValueRef.current = \"\";\n    setActiveValue(\"\");\n    updateViewportPosition();\n  }, [setActiveValue, updateViewportPosition]);\n\n  const registerContent = React.useCallback(\n    (value: string, content: ContentRecord) => {\n      setContentByValue((current) => {\n        const previous = current[value];\n\n        if (\n          previous?.children === content.children &&\n          previous?.className === content.className &&\n          previous?.innerClassName === content.innerClassName\n        ) {\n          return current;\n        }\n\n        return { ...current, [value]: content };\n      });\n\n      return () => {\n        setContentByValue((current) => {\n          if (!current[value]) {\n            return current;\n          }\n\n          const next = { ...current };\n          delete next[value];\n          return next;\n        });\n      };\n    },\n    [],\n  );\n\n  React.useEffect(() => {\n    if (value === undefined) {\n      return;\n    }\n\n    if (!value) {\n      lastActiveValueRef.current = \"\";\n      return;\n    }\n\n    openValue(value);\n  }, [openValue, value]);\n\n  React.useLayoutEffect(() => {\n    updateViewportPosition();\n  }, [activeValue, updateViewportPosition]);\n\n  React.useLayoutEffect(() => {\n    const root = rootRef.current;\n\n    if (!root || typeof ResizeObserver === \"undefined\") {\n      return () => {\n        if (frameRef.current !== null) {\n          cancelAnimationFrame(frameRef.current);\n        }\n      };\n    }\n\n    const observer = new ResizeObserver(updateViewportPosition);\n    observer.observe(root);\n\n    return () => {\n      observer.disconnect();\n\n      if (frameRef.current !== null) {\n        cancelAnimationFrame(frameRef.current);\n      }\n    };\n  }, [updateViewportPosition]);\n\n  React.useEffect(() => {\n    function handlePointerDown(event: PointerEvent) {\n      if (\n        rootRef.current &&\n        event.target instanceof Node &&\n        !rootRef.current.contains(event.target)\n      ) {\n        closeMenu();\n      }\n    }\n\n    document.addEventListener(\"pointerdown\", handlePointerDown);\n    return () => document.removeEventListener(\"pointerdown\", handlePointerDown);\n  }, [closeMenu]);\n\n  const contextValue = React.useMemo(\n    () => ({\n      activeValue,\n      direction,\n      spring,\n      viewport,\n      viewportX,\n      openValue,\n      closeMenu,\n      registerContent,\n      updateViewportPosition,\n    }),\n    [\n      activeValue,\n      closeMenu,\n      direction,\n      openValue,\n      registerContent,\n      spring,\n      updateViewportPosition,\n      viewport,\n      viewportX,\n    ],\n  );\n\n  return (\n    <MotionNavigationMenuContext.Provider value={contextValue}>\n      <nav\n        ref={setRootRef}\n        data-slot=\"navigation-menu\"\n        data-viewport={viewport}\n        className={cn(\n          \"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center\",\n          className,\n        )}\n        onPointerLeave={(event) => {\n          onPointerLeave?.(event);\n          closeMenu();\n        }}\n        onKeyDown={(event) => {\n          onKeyDown?.(event);\n\n          if (event.key === \"Escape\") {\n            closeMenu();\n          }\n        }}\n        {...props}\n      >\n        {children}\n        {viewport && (\n          <MotionNavigationMenuViewport\n            className={viewportClassName}\n            contentByValue={contentByValue}\n          />\n        )}\n      </nav>\n    </MotionNavigationMenuContext.Provider>\n  );\n}\n\nfunction MotionNavigationMenuList({\n  className,\n  highlightClassName,\n  ...props\n}: React.ComponentPropsWithRef<\"ul\"> & {\n  highlightClassName?: string;\n}) {\n  return (\n    <Highlight\n      mode=\"parent\"\n      controlledItems\n      hover\n      className={cn(\n        \"bg-accent rounded-md pointer-events-none\",\n        highlightClassName,\n      )}\n      style={{ zIndex: -1 }}\n      containerClassName=\"relative\"\n    >\n      <ul\n        data-slot=\"navigation-menu-list\"\n        className={cn(\n          \"group relative z-10 flex flex-1 list-none items-center justify-center gap-1\",\n          className,\n        )}\n        {...props}\n      />\n    </Highlight>\n  );\n}\n\nfunction MotionNavigationMenuItem({\n  className,\n  value,\n  ...props\n}: React.ComponentPropsWithRef<\"li\"> & {\n  value?: string;\n}) {\n  const itemContextValue = React.useMemo(() => ({ value }), [value]);\n\n  return (\n    <MotionNavigationMenuItemContext.Provider value={itemContextValue}>\n      <li\n        data-slot=\"navigation-menu-item\"\n        data-value={value}\n        className={cn(\"relative\", className)}\n        {...props}\n      />\n    </MotionNavigationMenuItemContext.Provider>\n  );\n}\n\nconst motionNavigationMenuTriggerStyle = cva(\n  \"group inline-flex h-9 w-max items-center justify-center rounded-md bg-transparent px-4 py-2 text-sm font-medium hover:text-accent-foreground focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:text-accent-foreground focus-visible:ring-ring/50 outline-none transition-colors focus-visible:ring-[3px] focus-visible:outline-1\",\n);\n\nfunction MotionNavigationMenuTrigger({\n  className,\n  children,\n  onPointerEnter,\n  onFocus,\n  onClick,\n  ...props\n}: React.ComponentPropsWithRef<\"button\">) {\n  const context = React.useContext(MotionNavigationMenuContext);\n  const itemContext = React.useContext(MotionNavigationMenuItemContext);\n  const value = itemContext?.value;\n  const isOpen = !!value && context?.activeValue === value;\n\n  return (\n    <HighlightItem asChild>\n      <button\n        type=\"button\"\n        data-slot=\"navigation-menu-trigger\"\n        data-state={isOpen ? \"open\" : \"closed\"}\n        aria-expanded={isOpen}\n        className={cn(motionNavigationMenuTriggerStyle(), \"group\", className)}\n        onPointerEnter={(event) => {\n          onPointerEnter?.(event);\n\n          if (value) {\n            context?.openValue(value);\n          }\n        }}\n        onFocus={(event) => {\n          onFocus?.(event);\n\n          if (value) {\n            context?.openValue(value);\n          }\n        }}\n        onClick={(event) => {\n          onClick?.(event);\n\n          if (value) {\n            context?.openValue(value);\n          }\n        }}\n        {...props}\n      >\n        {children}{\" \"}\n        <motion.span\n          aria-hidden=\"true\"\n          animate={{\n            rotate: isOpen ? 180 : 0,\n            y: isOpen ? 1 : 0,\n          }}\n          transition={{\n            type: \"spring\",\n            stiffness: 400,\n            damping: 20,\n          }}\n          className=\"relative top-0 ml-1.5 inline-flex\"\n        >\n          <ChevronDownIcon className=\"size-3.5 stroke-2.5\" aria-hidden=\"true\" />\n        </motion.span>\n      </button>\n    </HighlightItem>\n  );\n}\n\nfunction MotionNavigationMenuContent({\n  className,\n  highlightClassName,\n  innerClassName,\n  children,\n}: React.ComponentPropsWithRef<\"div\"> & {\n  highlightClassName?: string;\n  innerClassName?: string;\n}) {\n  const context = React.useContext(MotionNavigationMenuContext);\n  const itemContext = React.useContext(MotionNavigationMenuItemContext);\n  const value = itemContext?.value;\n  const isOpen = !!value && context?.activeValue === value;\n\n  React.useLayoutEffect(() => {\n    if (!context || !value || !context.viewport) {\n      return;\n    }\n\n    return context.registerContent(value, {\n      children,\n      className,\n      highlightClassName,\n      innerClassName,\n    });\n  }, [children, className, context, highlightClassName, innerClassName, value]);\n\n  if (!context || !value || context.viewport) {\n    return null;\n  }\n\n  return (\n    <AnimatePresence initial={false} custom={context.direction}>\n      {isOpen && (\n        <motion.div\n          data-slot=\"navigation-menu-content\"\n          key={value}\n          custom={context.direction}\n          variants={contentVariants}\n          initial=\"initial\"\n          animate=\"active\"\n          exit=\"exit\"\n          transition={context.spring}\n          className={cn(\n            \"bg-background/90 text-popover-foreground absolute top-full left-0 z-50 mt-1.5 rounded-md border p-2 pr-2.5 shadow\",\n            className,\n          )}\n        >\n          <MotionNavigationMenuContentInner\n            highlightClassName={highlightClassName}\n            innerClassName={innerClassName}\n          >\n            {children}\n          </MotionNavigationMenuContentInner>\n        </motion.div>\n      )}\n    </AnimatePresence>\n  );\n}\n\nfunction MotionNavigationMenuContentInner({\n  highlightClassName,\n  innerClassName,\n  children,\n}: {\n  highlightClassName?: string;\n  innerClassName?: string;\n  children: React.ReactNode;\n}) {\n  return (\n    <Highlight\n      mode=\"parent\"\n      controlledItems\n      hover\n      className={cn(\n        \"bg-accent rounded-sm pointer-events-none\",\n        highlightClassName,\n      )}\n      style={{ zIndex: -1 }}\n      containerClassName=\"relative\"\n    >\n      <div className={cn(\"relative z-10\", innerClassName)}>{children}</div>\n    </Highlight>\n  );\n}\n\nfunction MotionNavigationMenuViewport({\n  className,\n  contentByValue,\n}: React.ComponentPropsWithRef<\"div\"> & {\n  contentByValue?: Record<string, ContentRecord>;\n}) {\n  const context = React.useContext(MotionNavigationMenuContext);\n  const measureRef = React.useRef<HTMLDivElement | null>(null);\n  const [size, setSize] = React.useState({ width: 0, height: 0 });\n  const [lastSize, setLastSize] = React.useState({ width: 0, height: 0 });\n  const activeContent =\n    context?.activeValue && contentByValue\n      ? contentByValue[context.activeValue]\n      : undefined;\n\n  React.useLayoutEffect(() => {\n    const node = measureRef.current;\n\n    if (!node || !activeContent) {\n      return;\n    }\n\n    const updateSize = () => {\n      const rect = node.getBoundingClientRect();\n      const nextSize = {\n        width: rect.width,\n        height: rect.height,\n      };\n\n      setSize(nextSize);\n\n      if (nextSize.width > 0 || nextSize.height > 0) {\n        setLastSize(nextSize);\n      }\n\n      context?.updateViewportPosition();\n    };\n\n    updateSize();\n\n    if (typeof ResizeObserver === \"undefined\") {\n      return;\n    }\n\n    const observer = new ResizeObserver(updateSize);\n    observer.observe(node);\n\n    return () => observer.disconnect();\n  }, [activeContent, context]);\n\n  const width = size.width > 0 ? size.width : lastSize.width;\n  const height = size.height > 0 ? size.height : lastSize.height;\n\n  return (\n    <motion.div\n      className=\"absolute top-full isolate z-50 flex -translate-x-1/2 justify-center\"\n      initial={false}\n      animate={{ left: context?.viewportX ?? \"50%\" }}\n      transition={context?.spring}\n    >\n      <motion.div\n        data-slot=\"navigation-menu-viewport\"\n        initial={false}\n        animate={{\n          width: activeContent ? width : 0,\n          height: activeContent ? height : 0,\n          opacity: activeContent ? 1 : 0,\n          scale: activeContent ? 1 : 0.95,\n        }}\n        transition={context?.spring}\n        className={cn(\n          \"bg-background text-popover-foreground relative mt-1.5 overflow-hidden rounded-md border shadow backdrop-blur-md\",\n          className,\n        )}\n      >\n        <AnimatePresence\n          mode=\"popLayout\"\n          initial={false}\n          custom={context?.direction ?? 1}\n        >\n          {activeContent && context?.activeValue && (\n            <motion.div\n              data-slot=\"navigation-menu-content\"\n              key={context.activeValue}\n              custom={context.direction}\n              variants={contentVariants}\n              initial=\"initial\"\n              animate=\"active\"\n              exit=\"exit\"\n              transition={context.spring}\n              className={cn(\"p-2 pr-2.5\", activeContent.className)}\n            >\n              <MotionNavigationMenuContentInner\n                highlightClassName={activeContent.highlightClassName}\n                innerClassName={activeContent.innerClassName}\n              >\n                {activeContent.children}\n              </MotionNavigationMenuContentInner>\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </motion.div>\n\n      <div\n        ref={measureRef}\n        aria-hidden=\"true\"\n        data-slot=\"navigation-menu-measure\"\n        className=\"pointer-events-none invisible absolute top-1.5 left-0 w-max\"\n      >\n        {activeContent && (\n          <div className={cn(\"p-2 pr-2.5\", activeContent.className)}>\n            <MotionNavigationMenuContentInner\n              highlightClassName={activeContent.highlightClassName}\n              innerClassName={activeContent.innerClassName}\n            >\n              {activeContent.children}\n            </MotionNavigationMenuContentInner>\n          </div>\n        )}\n      </div>\n    </motion.div>\n  );\n}\n\nfunction MotionNavigationMenuLink({\n  className,\n  ...props\n}: React.ComponentPropsWithRef<\"a\">) {\n  return (\n    <HighlightItem asChild>\n      <a\n        data-slot=\"navigation-menu-link\"\n        className={cn(\n          \"data-[active=true]:text-accent-foreground hover:text-accent-foreground focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-colors outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4\",\n          className,\n        )}\n        {...props}\n      />\n    </HighlightItem>\n  );\n}\n\nfunction MotionNavigationMenuIndicator({\n  className,\n  ...props\n}: React.ComponentPropsWithRef<\"div\">) {\n  return (\n    <div\n      data-slot=\"navigation-menu-indicator\"\n      className={cn(\n        \"pointer-events-none top-full z-1 flex h-1.5 items-end justify-center overflow-hidden\",\n        className,\n      )}\n      {...props}\n    >\n      <div className=\"bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md\" />\n    </div>\n  );\n}\n\nexport {\n  MotionNavigationMenu,\n  MotionNavigationMenuList,\n  MotionNavigationMenuItem,\n  MotionNavigationMenuContent,\n  MotionNavigationMenuTrigger,\n  MotionNavigationMenuLink,\n  MotionNavigationMenuViewport,\n  MotionNavigationMenuIndicator,\n  motionNavigationMenuTriggerStyle,\n};\n",
      "type": "registry:component",
      "target": "components/unlumen-ui/motion-navigation-menu.tsx"
    }
  ],
  "type": "registry:component"
}