{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "expand-menu",
  "title": "Expand Menu",
  "description": "An animated expanding menu that reveals items from a trigger button, with configurable direction, layout, and alignment.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/components/unlumen/expand-menu/index.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion, AnimatePresence } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ExpandMenuItemType = {\n  icon: React.ReactNode;\n  label: string;\n  onClick?: () => void;\n};\n\nexport type ExpandMenuProps = {\n  items: ExpandMenuItemType[];\n  direction?: \"up\" | \"down\" | \"left\" | \"right\";\n  showLabels?: boolean;\n  triggerIcon?: React.ReactNode;\n  className?: string;\n};\n\n// anchor so the trigger stays in place while items expand away from it\nconst anchorStyle: Record<string, React.CSSProperties> = {\n  down: { top: 0, left: 0 },\n  right: { top: 0, left: 0 },\n  up: { bottom: 0, left: 0 },\n  left: { top: 0, right: 0 },\n};\n\n// \"up\" / \"left\" need the trigger last so it stays at the anchor corner\nconst triggerLast = (d: string) => d === \"up\" || d === \"left\";\n\nconst flexDirMap: Record<string, React.CSSProperties[\"flexDirection\"]> = {\n  down: \"column\",\n  up: \"column\",\n  right: \"row\",\n  left: \"row\",\n};\n\nfunction TriggerButton({\n  open,\n  triggerIcon,\n  onClick,\n}: {\n  open: boolean;\n  triggerIcon?: React.ReactNode;\n  onClick: () => void;\n}) {\n  return (\n    <motion.button\n      layout\n      onClick={onClick}\n      aria-expanded={open}\n      style={{ borderRadius: 9999, flexShrink: 0 }}\n      className=\"flex items-center justify-center w-9 h-9 hover:bg-gray-100 active:bg-gray-200 transition-colors cursor-pointer\"\n    >\n      <motion.span\n        animate={{ rotate: open ? 45 : 0 }}\n        transition={{ type: \"spring\", stiffness: 400, damping: 28 }}\n        className=\"flex items-center justify-center\"\n      >\n        {triggerIcon ?? (\n          <svg\n            width=\"16\"\n            height=\"16\"\n            viewBox=\"0 0 16 16\"\n            fill=\"currentColor\"\n            className=\"text-gray-700\"\n          >\n            <path d=\"M8 2a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3zm0 4.5a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3zm0 4.5a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3z\" />\n          </svg>\n        )}\n      </motion.span>\n    </motion.button>\n  );\n}\n\nfunction MenuItem({\n  icon,\n  label,\n  onClick,\n  index,\n  showLabels,\n}: ExpandMenuItemType & { index: number; showLabels: boolean }) {\n  return (\n    <motion.button\n      layout\n      initial={{ opacity: 0, scale: 0.7 }}\n      animate={{ opacity: 1, scale: 1 }}\n      exit={{ opacity: 0, scale: 0.7 }}\n      transition={{\n        delay: index * 0.04,\n        type: \"spring\",\n        stiffness: 400,\n        damping: 28,\n      }}\n      onClick={onClick}\n      title={!showLabels ? label : undefined}\n      style={{ borderRadius: 14, flexShrink: 0 }}\n      className={cn(\n        \"flex items-center gap-2 hover:bg-gray-100 active:bg-gray-200 transition-colors text-gray-600 hover:text-gray-900 cursor-pointer\",\n        showLabels ? \"px-3 py-2 justify-start\" : \"w-9 h-9 justify-center\",\n      )}\n    >\n      <span className=\"flex-shrink-0\">{icon}</span>\n      {showLabels && (\n        <span className=\"text-sm font-medium whitespace-nowrap text-left\">\n          {label}\n        </span>\n      )}\n    </motion.button>\n  );\n}\n\nexport function ExpandMenu({\n  items,\n  direction = \"down\",\n  showLabels = false,\n  triggerIcon,\n  className,\n}: ExpandMenuProps) {\n  const [open, setOpen] = React.useState(false);\n  const ref = React.useRef<HTMLDivElement>(null);\n\n  React.useEffect(() => {\n    const handler = (e: MouseEvent) => {\n      if (ref.current && !ref.current.contains(e.target as Node)) {\n        setOpen(false);\n      }\n    };\n    if (open) document.addEventListener(\"mousedown\", handler);\n    return () => document.removeEventListener(\"mousedown\", handler);\n  }, [open]);\n\n  const trigger = (\n    <TriggerButton\n      open={open}\n      triggerIcon={triggerIcon}\n      onClick={() => setOpen((v) => !v)}\n    />\n  );\n\n  const menuItems = (\n    <AnimatePresence>\n      {open &&\n        items.map((item, i) => (\n          <MenuItem\n            key={item.label}\n            {...item}\n            index={i}\n            showLabels={showLabels}\n            onClick={() => {\n              item.onClick?.();\n              setOpen(false);\n            }}\n          />\n        ))}\n    </AnimatePresence>\n  );\n\n  return (\n    <div\n      ref={ref}\n      className={cn(\"relative z-50\", className)}\n      style={{ width: 36, height: 36 }}\n    >\n      <motion.div\n        layout\n        style={{\n          position: \"absolute\",\n          ...anchorStyle[direction],\n          flexDirection: flexDirMap[direction],\n          borderRadius: open ? 20 : 9999,\n          padding: open ? 6 : 0,\n          gap: open ? 4 : 0,\n          display: \"inline-flex\",\n          alignItems: \"center\",\n        }}\n        className=\"bg-white border border-gray-200 shadow-md overflow-hidden\"\n        transition={{ type: \"spring\", stiffness: 380, damping: 30 }}\n      >\n        {triggerLast(direction) ? (\n          <>\n            {menuItems}\n            {trigger}\n          </>\n        ) : (\n          <>\n            {trigger}\n            {menuItems}\n          </>\n        )}\n      </motion.div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/unlumen-ui/expand-menu.tsx"
    }
  ],
  "type": "registry:ui"
}