{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "menubar-motion",
  "title": "Motion menubar",
  "description": "Styled menubar: the Motion set. 5 decorative menubar variations (SlideMenubar, FadeMenubar, PopMenubar, SpringMenubar, MorphMenubar) on the ai2 token system, powered by framer-motion, reduced-motion aware and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "motion@^12.42.2"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/menubar-motion.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Motion menubar family: 5 decorative, SELF-SUFFICIENT menubars. NO radix or\n   portal. The bar looks the same in every variant - the whole difference is in the\n   menu's OPEN/CLOSE motion: slide, fade, pop, spring and morph. Classic menubar\n   behavior: while one menu is open, hovering a sibling trigger switches to that\n   menu. It closes on an outside click, on Escape (focus returns to the trigger) and\n   on an item selection. Left/Right arrows move between the top-level menus and\n   Up/Down moves inside the open one. No transform under reduced motion (only a\n   fade). Color comes ONLY from tokens, via alpha color-mix. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\ntype MenuItem = { label: React.ReactNode; onSelect?: () => void }\ntype Menu = { label: React.ReactNode; items: MenuItem[] }\n\ninterface MenubarProps {\n  className?: string\n  size?: StyledSize\n  menus?: Menu[]\n}\n\nconst defaultMenus: Menu[] = [\n  {\n    label: \"File\",\n    items: [\n      { label: \"New File\" },\n      { label: \"Open...\" },\n      { label: \"Save\" },\n      { label: \"Save As...\" },\n    ],\n  },\n  {\n    label: \"Edit\",\n    items: [\n      { label: \"Undo\" },\n      { label: \"Redo\" },\n      { label: \"Cut\" },\n      { label: \"Copy\" },\n      { label: \"Paste\" },\n    ],\n  },\n  {\n    label: \"View\",\n    items: [{ label: \"Zoom In\" }, { label: \"Zoom Out\" }, { label: \"Full Screen\" }],\n  },\n]\n\nconst barSize: Record<StyledSize, string> = {\n  sm: \"h-8 gap-0.5 px-1 text-xs\",\n  md: \"h-9 gap-1 px-1 text-sm\",\n  lg: \"h-10 gap-1 px-1.5 text-sm\",\n  xl: \"h-11 gap-1.5 px-2 text-base\",\n}\n\nconst triggerSize: Record<StyledSize, string> = {\n  sm: \"h-6 px-2\",\n  md: \"h-7 px-2.5\",\n  lg: \"h-8 px-3\",\n  xl: \"h-9 px-3.5\",\n}\n\nconst panelSize: Record<StyledSize, string> = {\n  sm: \"min-w-40 text-xs\",\n  md: \"min-w-44 text-sm\",\n  lg: \"min-w-48 text-sm\",\n  xl: \"min-w-52 text-base\",\n}\n\nconst itemSize: Record<StyledSize, string> = {\n  sm: \"px-2 py-1.5\",\n  md: \"px-2.5 py-1.5\",\n  lg: \"px-3 py-2\",\n  xl: \"px-3.5 py-2\",\n}\n\nconst softHover =\n  \"hover:bg-[color-mix(in_oklab,var(--color-foreground)_6%,transparent)]\"\n\nconst triggerBase =\n  \"inline-flex select-none items-center gap-1.5 whitespace-nowrap rounded-md font-medium outline-none transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50 [&>svg]:size-4 [&>svg]:shrink-0 [&>i]:text-base [&>i]:leading-none\"\n\nconst itemBase =\n  \"flex w-full cursor-default select-none items-center gap-2 rounded-md text-left text-popover-foreground outline-none transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50 [&>svg]:size-4 [&>svg]:shrink-0 [&>i]:text-base [&>i]:leading-none\"\n\nconst panelBase =\n  \"absolute left-0 top-full z-50 mt-1.5 rounded-lg border border-border bg-popover p-1 text-popover-foreground shadow-lg outline-none\"\n\ntype MotionSpec = {\n  initial: Record<string, number | string>\n  animate: Record<string, number | string>\n  exit: Record<string, number | string>\n  transition: Record<string, unknown>\n  origin?: string\n}\n\nconst fadeOnly: MotionSpec = {\n  initial: { opacity: 0 },\n  animate: { opacity: 1 },\n  exit: { opacity: 0 },\n  transition: { duration: 0.12 },\n}\n\n/* The shared menubar core. The panel is always absolutely positioned inside the\n   relative wrapper of its own trigger, so many examples on a single page never\n   collide. */\nfunction MenubarCore({ props, spec }: { props: MenubarProps; spec: MotionSpec }) {\n  const { className, size = \"md\", menus = defaultMenus } = props\n  const reduce = useReducedMotion()\n  const uid = React.useId()\n  const [openIndex, setOpenIndex] = React.useState<number | null>(null)\n  const [autoFocusItem, setAutoFocusItem] = React.useState(false)\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  const triggerRefs = React.useRef<Array<HTMLButtonElement | null>>([])\n  const itemRefs = React.useRef<Array<HTMLButtonElement | null>>([])\n\n  React.useEffect(() => {\n    if (openIndex === null) return\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key !== \"Escape\") return\n      triggerRefs.current[openIndex]?.focus()\n      setOpenIndex(null)\n    }\n    const onPointer = (e: PointerEvent) => {\n      const node = rootRef.current\n      if (node && e.target instanceof Node && !node.contains(e.target)) {\n        setOpenIndex(null)\n      }\n    }\n    window.addEventListener(\"keydown\", onKey)\n    window.addEventListener(\"pointerdown\", onPointer)\n    return () => {\n      window.removeEventListener(\"keydown\", onKey)\n      window.removeEventListener(\"pointerdown\", onPointer)\n    }\n  }, [openIndex])\n\n  React.useEffect(() => {\n    if (openIndex === null || !autoFocusItem) return\n    const id = window.requestAnimationFrame(() => itemRefs.current[0]?.focus())\n    setAutoFocusItem(false)\n    return () => window.cancelAnimationFrame(id)\n  }, [openIndex, autoFocusItem])\n\n  const moveTrigger = (from: number, dir: 1 | -1) => {\n    const next = (from + dir + menus.length) % menus.length\n    triggerRefs.current[next]?.focus()\n    if (openIndex !== null) setOpenIndex(next)\n  }\n\n  const onTriggerKeyDown = (e: React.KeyboardEvent, i: number) => {\n    if (e.key === \"ArrowRight\") {\n      e.preventDefault()\n      moveTrigger(i, 1)\n    } else if (e.key === \"ArrowLeft\") {\n      e.preventDefault()\n      moveTrigger(i, -1)\n    } else if (e.key === \"ArrowDown\") {\n      e.preventDefault()\n      setOpenIndex(i)\n      setAutoFocusItem(true)\n    }\n  }\n\n  const onItemKeyDown = (e: React.KeyboardEvent, j: number, count: number, i: number) => {\n    if (e.key === \"ArrowDown\") {\n      e.preventDefault()\n      itemRefs.current[(j + 1) % count]?.focus()\n    } else if (e.key === \"ArrowUp\") {\n      e.preventDefault()\n      itemRefs.current[(j - 1 + count) % count]?.focus()\n    } else if (e.key === \"ArrowRight\" || e.key === \"ArrowLeft\") {\n      e.preventDefault()\n      moveTrigger(i, e.key === \"ArrowRight\" ? 1 : -1)\n      setAutoFocusItem(true)\n    }\n  }\n\n  const active = reduce ? fadeOnly : spec\n\n  return (\n    <div\n      ref={rootRef}\n      data-slot=\"styled-menubar\"\n      role=\"menubar\"\n      className={cn(\n        \"inline-flex items-center rounded-lg border border-border bg-card shadow-xs\",\n        barSize[size],\n        className\n      )}\n    >\n      {menus.map((menu, i) => {\n        const isOpen = openIndex === i\n        const triggerId = `${uid}-trigger-${i}`\n        const menuId = `${uid}-menu-${i}`\n        return (\n          <div key={i} className=\"relative\">\n            <button\n              type=\"button\"\n              id={triggerId}\n              ref={(node) => {\n                triggerRefs.current[i] = node\n              }}\n              role=\"menuitem\"\n              aria-haspopup=\"menu\"\n              aria-expanded={isOpen}\n              aria-controls={isOpen ? menuId : undefined}\n              onClick={() => setOpenIndex(isOpen ? null : i)}\n              onPointerEnter={() => {\n                if (openIndex !== null) setOpenIndex(i)\n              }}\n              onKeyDown={(e) => onTriggerKeyDown(e, i)}\n              className={cn(\n                triggerBase,\n                triggerSize[size],\n                isOpen\n                  ? \"bg-accent text-accent-foreground\"\n                  : cn(\"text-foreground\", softHover)\n              )}\n            >\n              {menu.label}\n            </button>\n\n            <AnimatePresence>\n              {isOpen ? (\n                <motion.div\n                  id={menuId}\n                  role=\"menu\"\n                  aria-labelledby={triggerId}\n                  className={cn(panelBase, panelSize[size], !reduce && spec.origin)}\n                  initial={active.initial}\n                  animate={active.animate}\n                  exit={active.exit}\n                  transition={active.transition}\n                >\n                  {menu.items.map((item, j) => (\n                    <button\n                      key={j}\n                      type=\"button\"\n                      role=\"menuitem\"\n                      ref={(node) => {\n                        if (openIndex === i) itemRefs.current[j] = node\n                      }}\n                      onClick={() => {\n                        item.onSelect?.()\n                        triggerRefs.current[i]?.focus()\n                        setOpenIndex(null)\n                      }}\n                      onKeyDown={(e) => onItemKeyDown(e, j, menu.items.length, i)}\n                      className={cn(itemBase, itemSize[size], softHover)}\n                    >\n                      {item.label}\n                    </button>\n                  ))}\n                </motion.div>\n              ) : null}\n            </AnimatePresence>\n          </div>\n        )\n      })}\n    </div>\n  )\n}\n\n/* Slide: the menu opens by sliding down from above. */\nexport function SlideMenubar({ className, size = \"md\", menus }: MenubarProps) {\n  return (\n    <MenubarCore\n      props={{ className, size, menus }}\n      spec={{\n        initial: { opacity: 0, y: -10 },\n        animate: { opacity: 1, y: 0 },\n        exit: { opacity: 0, y: -10 },\n        transition: { duration: 0.18, ease: \"easeOut\" },\n      }}\n    />\n  )\n}\n\n/* Fade: no transform, only an opacity transition. */\nexport function FadeMenubar({ className, size = \"md\", menus }: MenubarProps) {\n  return (\n    <MenubarCore\n      props={{ className, size, menus }}\n      spec={{\n        initial: { opacity: 0 },\n        animate: { opacity: 1 },\n        exit: { opacity: 0 },\n        transition: { duration: 0.22, ease: \"easeOut\" },\n      }}\n    />\n  )\n}\n\n/* Pop: it opens from the top corner of the trigger by growing from small. */\nexport function PopMenubar({ className, size = \"md\", menus }: MenubarProps) {\n  return (\n    <MenubarCore\n      props={{ className, size, menus }}\n      spec={{\n        initial: { opacity: 0, scale: 0.88 },\n        animate: { opacity: 1, scale: 1 },\n        exit: { opacity: 0, scale: 0.88 },\n        transition: { duration: 0.16, ease: \"easeOut\" },\n        origin: \"origin-top-left\",\n      }}\n    />\n  )\n}\n\n/* Spring: yayli, hafif zipla-yerlese acilis. */\nexport function SpringMenubar({ className, size = \"md\", menus }: MenubarProps) {\n  return (\n    <MenubarCore\n      props={{ className, size, menus }}\n      spec={{\n        initial: { opacity: 0, scale: 0.94, y: -8 },\n        animate: { opacity: 1, scale: 1, y: 0 },\n        exit: { opacity: 0, scale: 0.94, y: -8 },\n        transition: { type: \"spring\" as const, stiffness: 420, damping: 18 },\n        origin: \"origin-top\",\n      }}\n    />\n  )\n}\n\n/* Morph: the panel grows vertically like a banner unrolling downward. */\nexport function MorphMenubar({ className, size = \"md\", menus }: MenubarProps) {\n  return (\n    <MenubarCore\n      props={{ className, size, menus }}\n      spec={{\n        initial: { opacity: 0, scaleY: 0.6, scaleX: 0.96 },\n        animate: { opacity: 1, scaleY: 1, scaleX: 1 },\n        exit: { opacity: 0, scaleY: 0.6, scaleX: 0.96 },\n        transition: { duration: 0.2, ease: \"easeOut\" },\n        origin: \"origin-top\",\n      }}\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/menubar-motion.tsx"
    }
  ],
  "categories": [
    "styled",
    "menubar"
  ],
  "type": "registry:component"
}