{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "mega-menu",
  "type": "registry:ui",
  "title": "Mega Menu",
  "description": "Site menu with one panel that morphs between triggers and slides content.",
  "categories": [
    "app-nav"
  ],
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/icons.json",
    "https://www.stealth.pm/r/motion.json",
    "https://www.stealth.pm/r/use-controllable-state.json"
  ],
  "files": [
    {
      "path": "components/ui/mega-menu.tsx",
      "content": "\"use client\";\nimport { NavigationMenu } from \"@base-ui/react/navigation-menu\";\nimport { animate, motion, useMotionValue, useReducedMotion } from \"motion/react\";\nimport { createContext, useCallback, useContext, useLayoutEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { ArrowRight, ChevronDown } from \"@/lib/icons\";\nimport { spring } from \"@/lib/motion\";\nimport { useControllableState } from \"@/lib/use-controllable-state\";\n\n/* -------------------------------------------------------------------------------------------------\n * Root: the trigger row, the one pill under it, and the one panel that morphs between menus\n * -----------------------------------------------------------------------------------------------*/\n\ntype PillContextValue = { enter: (el: HTMLElement) => void };\nconst PillContext = createContext<PillContextValue | null>(null);\n\nexport type MegaMenuProps = Omit<NavigationMenu.Root.Props, \"value\" | \"defaultValue\" | \"onValueChange\" | \"className\"> & {\n  className?: string;\n  /** The open item's value, or null when closed. */\n  value?: string | null;\n  defaultValue?: string | null;\n  onValueChange?: (value: string | null) => void;\n  /** Distance from the trigger row to the panel, in px. */\n  sideOffset?: number;\n  /** Render the panel inside this element instead of the body. */\n  container?: React.RefObject<HTMLElement | null>;\n  /**\n   * What the panel hangs from. \"row\" keeps it under the start of the trigger row, so wide\n   * panels never run off the edge and only the size changes between menus. \"trigger\"\n   * slides it along the row to sit under each trigger.\n   */\n  anchorTo?: \"row\" | \"trigger\";\n  /** How the panel lines up with its anchor. */\n  align?: \"start\" | \"center\" | \"end\";\n};\n\nexport function MegaMenu({\n  value,\n  defaultValue = null,\n  onValueChange,\n  sideOffset = 8,\n  container,\n  anchorTo = \"row\",\n  align = \"start\",\n  delay = 80,\n  closeDelay = 160,\n  className,\n  children,\n  ...rest\n}: MegaMenuProps) {\n  const [open, setOpen] = useControllableState<string | null>({ value, defaultValue, onChange: onValueChange });\n  const reduce = !!useReducedMotion();\n  const listRef = useRef<HTMLDivElement>(null);\n  const x = useMotionValue(0);\n  const width = useMotionValue(0);\n  const [on, setOn] = useState(false);\n  const shown = useRef(false);\n  const hovering = useRef(false);\n  const openedAt = useRef(0);\n\n  // One pill for the whole row. It follows the pointer across triggers and links,\n  // and when the pointer leaves the row it rests on the trigger whose panel is open,\n  // so you always know which menu you are reading.\n  const moveTo = useCallback(\n    (el: HTMLElement | null) => {\n      const list = listRef.current;\n      if (!el || !list) {\n        shown.current = false;\n        setOn(false);\n        return;\n      }\n      const left = el.getBoundingClientRect().left - list.getBoundingClientRect().left;\n      if (shown.current && !reduce) {\n        animate(x, left, spring.follow);\n        animate(width, el.offsetWidth, spring.follow);\n      } else {\n        x.jump(left);\n        width.jump(el.offsetWidth);\n      }\n      shown.current = true;\n      setOn(true);\n    },\n    [reduce, x, width],\n  );\n  const home = useCallback(() => moveTo(listRef.current?.querySelector<HTMLElement>(\"[data-popup-open]\") ?? null), [moveTo]);\n\n  useLayoutEffect(() => {\n    if (!hovering.current) home();\n  }, [open, home]);\n\n  return (\n    <NavigationMenu.Root\n      value={open}\n      onValueChange={(next, details) => {\n        // A press that lands just after hover already opened this panel is the person\n        // confirming what they see, not asking to close it. Without this, a quick click on\n        // a trigger races the hover delay and shuts the menu under the pointer.\n        if (next == null && details.reason === \"trigger-press\" && performance.now() - openedAt.current < 450) {\n          details.cancel();\n          return;\n        }\n        if (next != null && next !== open) openedAt.current = performance.now();\n        setOpen((next as string | null) ?? null);\n      }}\n      delay={delay}\n      closeDelay={closeDelay}\n      className={cn(\"relative min-w-0 text-fg\", className)}\n      {...rest}\n    >\n      <PillContext.Provider value={{ enter: (el) => ((hovering.current = true), moveTo(el)) }}>\n        <div\n          ref={listRef}\n          className=\"relative\"\n          onPointerLeave={() => {\n            hovering.current = false;\n            home();\n          }}\n        >\n          <motion.span\n            aria-hidden\n            className=\"pointer-events-none absolute left-0 top-0 h-8 rounded-full bg-hover\"\n            style={{ x, width }}\n            initial={false}\n            animate={{ opacity: on ? 1 : 0 }}\n            transition={{ duration: on ? 0.12 : 0.15, ease: \"easeOut\" }}\n          />\n          <NavigationMenu.List className=\"relative flex items-center\">{children}</NavigationMenu.List>\n        </div>\n      </PillContext.Provider>\n\n      <NavigationMenu.Portal container={container}>\n        <NavigationMenu.Positioner\n          sideOffset={sideOffset}\n          align={align}\n          alignOffset={align === \"center\" ? 0 : -4}\n          anchor={anchorTo === \"row\" ? listRef : undefined}\n          collisionPadding={{ top: 8, bottom: 8, left: 16, right: 16 }}\n          collisionAvoidance={{ side: \"none\" }}\n          className={cn(\n            \"z-(--z-dropdown) h-(--positioner-height) w-(--positioner-width) max-w-(--available-width)\",\n            // The panel slides along the row to sit under each trigger it opens for.\n            \"transition-[top,left,right,bottom] duration-300 ease-out-expo data-instant:transition-none\",\n            // A bridge over the gap, so the pointer can travel from trigger to panel without closing it.\n            \"before:absolute before:inset-x-0 before:-top-(--gap) before:h-(--gap) before:content-['']\",\n          )}\n          style={{ [\"--gap\" as string]: `${sideOffset + 2}px` }}\n        >\n          <NavigationMenu.Popup\n            className={cn(\n              \"relative h-(--popup-height) w-(--popup-width) origin-(--transform-origin) overflow-hidden rounded-xl border border-line-2 bg-raised text-fg shadow-pop outline-none\",\n              // Between menus the panel morphs its size instead of closing and reopening.\n              \"transition-[width,height,opacity,scale] duration-300 ease-out-expo\",\n              \"data-starting-style:scale-96 data-starting-style:opacity-0\",\n              \"data-ending-style:scale-98 data-ending-style:opacity-0 data-ending-style:duration-150 data-ending-style:ease-out-quart\",\n              \"motion-reduce:data-starting-style:scale-100 motion-reduce:data-ending-style:scale-100\",\n            )}\n          >\n            <NavigationMenu.Viewport className=\"relative size-full overflow-hidden\" />\n          </NavigationMenu.Popup>\n        </NavigationMenu.Positioner>\n      </NavigationMenu.Portal>\n    </NavigationMenu.Root>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Row parts\n * -----------------------------------------------------------------------------------------------*/\n\nconst rowItem = cn(\n  \"relative flex h-8 select-none items-center gap-1 whitespace-nowrap rounded-full px-2 text-[13px] text-fg-2 no-underline outline-none sm:px-3\",\n  \"transition-[color,scale] duration-150 hover:text-fg active:scale-[0.97] active:duration-75 data-popup-open:text-fg aria-[current=page]:text-fg\",\n  \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-fg-3\",\n  \"before:absolute before:inset-x-0 before:-inset-y-1.5 before:content-[''] pointer-fine:before:hidden\",\n);\n\nfunction usePillEnter() {\n  const pill = useContext(PillContext);\n  return (e: React.PointerEvent<HTMLElement>) => {\n    if (e.pointerType !== \"touch\") pill?.enter(e.currentTarget);\n  };\n}\n\nexport type MegaMenuItemProps = NavigationMenu.Item.Props;\n\n/** One entry in the row: a trigger with its content, or a plain link. */\nexport function MegaMenuItem(props: MegaMenuItemProps) {\n  return <NavigationMenu.Item {...props} />;\n}\n\nexport type MegaMenuTriggerProps = Omit<NavigationMenu.Trigger.Props, \"className\"> & { className?: string };\n\nexport function MegaMenuTrigger({ className, children, onPointerEnter, ...rest }: MegaMenuTriggerProps) {\n  const enter = usePillEnter();\n  return (\n    <NavigationMenu.Trigger\n      className={cn(rowItem, \"group/trigger\", className)}\n      onPointerEnter={(e) => {\n        onPointerEnter?.(e);\n        enter(e);\n      }}\n      {...rest}\n    >\n      {children}\n      <NavigationMenu.Icon className=\"-mr-1 grid size-3.5 place-items-center text-fg-3 transition-[rotate,color] duration-200 ease-out-expo group-hover/trigger:text-fg-2 data-popup-open:rotate-180 data-popup-open:text-fg-2\">\n        <ChevronDown size={12} />\n      </NavigationMenu.Icon>\n    </NavigationMenu.Trigger>\n  );\n}\n\nexport type MegaMenuTopLinkProps = Omit<NavigationMenu.Link.Props, \"className\"> & { className?: string };\n\n/** A link in the row that opens nothing, such as Pricing. */\nexport function MegaMenuTopLink({ className, onPointerEnter, ...rest }: MegaMenuTopLinkProps) {\n  const enter = usePillEnter();\n  return (\n    <NavigationMenu.Link\n      className={cn(rowItem, className)}\n      onPointerEnter={(e) => {\n        onPointerEnter?.(e);\n        enter(e);\n      }}\n      {...rest}\n    />\n  );\n}\n\nexport type MegaMenuContentProps = Omit<NavigationMenu.Content.Props, \"className\"> & { className?: string };\n\n/**\n * What a trigger opens. Moving to a neighbor slides the old content out and the new\n * in from the side you moved toward, while the panel resizes around them.\n */\nexport function MegaMenuContent({ className, ...rest }: MegaMenuContentProps) {\n  return (\n    <NavigationMenu.Content\n      className={cn(\n        \"h-full w-[calc(100vw-32px)] p-2 sm:w-max\",\n        \"transition-[opacity,translate,filter] duration-300 ease-out-expo data-starting-style:opacity-0 data-ending-style:opacity-0 data-ending-style:duration-200\",\n        \"data-[activation-direction=right]:data-starting-style:translate-x-6 data-[activation-direction=left]:data-starting-style:-translate-x-6\",\n        \"data-[activation-direction=right]:data-ending-style:-translate-x-6 data-[activation-direction=left]:data-ending-style:translate-x-6\",\n        \"motion-reduce:translate-x-0\",\n        className,\n      )}\n      {...rest}\n    />\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Panel parts\n * -----------------------------------------------------------------------------------------------*/\n\nexport type MegaMenuLinkProps = Omit<NavigationMenu.Link.Props, \"className\" | \"title\"> & {\n  className?: string;\n  title: React.ReactNode;\n  description?: React.ReactNode;\n  /** A 16px icon, drawn in a small tile. */\n  icon?: React.ReactNode;\n};\n\n/** A destination inside a panel: an icon tile, a title and one line of description. */\nexport function MegaMenuLink({ title, description, icon, className, closeOnClick = true, ...rest }: MegaMenuLinkProps) {\n  return (\n    <NavigationMenu.Link\n      closeOnClick={closeOnClick}\n      className={cn(\n        \"group/link flex min-w-0 items-start gap-3 rounded-lg p-2.5 text-left no-underline outline-none\",\n        \"transition-[background-color,scale] duration-150 hover:bg-hover active:scale-[0.985] active:duration-75 data-active:bg-hover\",\n        \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-fg-3\",\n        className,\n      )}\n      {...rest}\n    >\n      {icon && (\n        <span\n          aria-hidden\n          className=\"grid size-8 shrink-0 place-items-center rounded-md border border-line-2 bg-frame text-fg-2 shadow-[var(--shadow)] transition-colors duration-150 group-hover/link:text-fg [&_svg]:size-4\"\n        >\n          {icon}\n        </span>\n      )}\n      <span className=\"flex min-w-0 flex-col gap-0.5 pt-px\">\n        <span className=\"text-[13px] font-medium leading-[18px] tracking-[-0.005em] text-fg\">{title}</span>\n        {description && <span className=\"line-clamp-2 text-[12px] leading-4 text-fg-3\">{description}</span>}\n      </span>\n    </NavigationMenu.Link>\n  );\n}\n\nexport type MegaMenuSectionProps = Omit<React.ComponentProps<\"div\">, \"title\"> & { title?: string };\n\n/** A labeled column of links. */\nexport function MegaMenuSection({ title, className, children, ...rest }: MegaMenuSectionProps) {\n  return (\n    <div className={cn(\"flex min-w-0 flex-col\", className)} {...rest}>\n      {title && <p className=\"px-2.5 pb-1.5 pt-2 font-mono text-[10.5px] uppercase leading-none tracking-[0.08em] text-fg-3\">{title}</p>}\n      <ul className=\"flex flex-col\">\n        {Array.isArray(children) ? children.map((c, i) => <li key={i}>{c}</li>) : <li>{children}</li>}\n      </ul>\n    </div>\n  );\n}\n\nexport type MegaMenuFooterLinkProps = Omit<NavigationMenu.Link.Props, \"className\"> & { className?: string; label?: React.ReactNode };\n\n/** A strip along the bottom of a panel: \"What's new\", \"All integrations\". The arrow nudges on hover. */\nexport function MegaMenuFooterLink({ label, className, children, closeOnClick = true, ...rest }: MegaMenuFooterLinkProps) {\n  return (\n    <NavigationMenu.Link\n      closeOnClick={closeOnClick}\n      className={cn(\n        \"group/foot -mx-2 -mb-2 mt-2 flex min-w-0 items-center gap-2 border-t border-line bg-frame/60 px-4.5 py-3 text-[12.5px] text-fg-2 no-underline outline-none\",\n        \"transition-colors duration-150 hover:text-fg focus-visible:outline-solid focus-visible:outline-1 focus-visible:-outline-offset-2 focus-visible:outline-fg-3\",\n        className,\n      )}\n      {...rest}\n    >\n      {label && <span className=\"shrink-0 font-mono text-[10.5px] uppercase tracking-[0.08em] text-fg-3\">{label}</span>}\n      <span className=\"min-w-0 flex-1 truncate\">{children}</span>\n      <ArrowRight size={14} className=\"shrink-0 text-fg-3 transition-[translate,color] duration-200 ease-out-expo group-hover/foot:translate-x-0.5 group-hover/foot:text-fg-2\" />\n    </NavigationMenu.Link>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/mega-menu"
}
