{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "expanding-menu",
  "title": "Expanding menu",
  "description": "Ledge’s frosted navigation pill expands into a staggered link panel; the plus becomes a minus.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/items/expanding-menu/expanding-menu.tsx",
      "content": "\"use client\";\n\nimport { useState, type MouseEvent, type ReactNode } from \"react\";\nimport { AnimatePresence, motion, useReducedMotionConfig } from \"motion/react\";\n\nexport type MenuItem = { label: string; href: string };\n\nconst DEFAULT_ITEMS: MenuItem[] = [\n  { label: \"Home\", href: \"#\" },\n  { label: \"Services\", href: \"#\" },\n  { label: \"Case\", href: \"#\" },\n  { label: \"About\", href: \"#\" },\n  { label: \"Contact\", href: \"#\" },\n];\n\nconst spring = { type: \"spring\", stiffness: 320, damping: 32 } as const;\n\nconst COLLAPSED_W = 148;\nconst PANEL_W = 300;\n\nexport function ExpandingMenu({\n  items = DEFAULT_ITEMS,\n  label = \"Menu\",\n  footer,\n  align = \"left\",\n  onNavigate,\n}: {\n  items?: MenuItem[];\n  label?: string;\n  /** Optional node pinned to the bottom of the open panel (e.g. sign out). */\n  footer?: ReactNode;\n  /** Which edge stays anchored as the panel grows. */\n  align?: \"left\" | \"right\";\n  onNavigate?: (item: MenuItem, event: MouseEvent<HTMLAnchorElement>) => void;\n}) {\n  const [open, setOpen] = useState(false);\n  const reducedMotion = useReducedMotionConfig();\n  const close = () => setOpen(false);\n\n  // Panel height is derived so the morph lands exactly on the content size.\n  const expandedH = 56 + items.length * 46 + (footer ? 84 : 28);\n\n  return (\n    <div className=\"relative h-14 font-body\" style={{ width: COLLAPSED_W }}>\n      {/* The blurred background — this single element morphs pill -> panel. */}\n      <motion.div\n        initial={false}\n        animate={{\n          width: open ? PANEL_W : COLLAPSED_W,\n          height: open ? expandedH : 56,\n        }}\n        transition={reducedMotion ? { duration: 0 } : spring}\n        className={`absolute top-0 overflow-hidden rounded-[28px] bg-menu-surface text-menu-symbol shadow-2xl shadow-menu-shadow ring-1 ring-menu-rule backdrop-blur-xl backdrop-saturate-200 backdrop-brightness-90 ${\n          align === \"right\" ? \"right-0\" : \"left-0\"\n        }`}\n      >\n        {/* Frosted highlight — milky sheen across the top, fading down. */}\n        <div className=\"pointer-events-none absolute inset-0 rounded-[28px] bg-gradient-to-b from-menu-sheen via-menu-sheen-mid to-transparent\" />\n        <div className=\"relative flex h-full flex-col\">\n          {/* Header — toggles open/close. Stays put while the bg grows under it. */}\n          <button\n            type=\"button\"\n            onClick={() => setOpen((o) => !o)}\n            className=\"flex h-14 shrink-0 items-center justify-between px-5 text-[15px] font-medium tracking-tight\"\n          >\n            <span className=\"relative inline-grid\">\n              <AnimatePresence initial={false} mode=\"popLayout\">\n                <motion.span\n                  key={open ? \"close\" : \"menu\"}\n                  initial={{ opacity: reducedMotion ? 1 : 0, y: reducedMotion ? 0 : open ? 8 : -8 }}\n                  animate={{ opacity: 1, y: 0 }}\n                  exit={{ opacity: 0, y: reducedMotion ? 0 : open ? -8 : 8 }}\n                  transition={{ duration: reducedMotion ? 0 : 0.18 }}\n                  className=\"col-start-1 row-start-1 whitespace-nowrap text-menu-ink\"\n                >\n                  {open ? \"Close\" : label}\n                </motion.span>\n              </AnimatePresence>\n            </span>\n\n            {/* + morphs into — : drop the vertical bar. */}\n            <span className=\"relative block h-4 w-4 text-menu-ink\">\n              <span className=\"absolute inset-x-0 top-1/2 h-[1.5px] -translate-y-1/2 bg-current\" />\n              <motion.span\n                animate={{ scaleY: open ? 0 : 1, opacity: open ? 0 : 1 }}\n                transition={{ duration: reducedMotion ? 0 : 0.2 }}\n                className=\"absolute inset-y-0 left-1/2 w-[1.5px] -translate-x-1/2 bg-current\"\n              />\n            </span>\n          </button>\n\n          {/* Body — fades/staggers in once the panel has room. */}\n          <AnimatePresence>\n            {open && (\n              <motion.nav\n                initial={{ opacity: reducedMotion ? 1 : 0 }}\n                animate={{ opacity: 1 }}\n                exit={{ opacity: 0 }}\n                transition={{ duration: reducedMotion ? 0 : 0.2 }}\n                className=\"flex flex-1 flex-col px-5 pb-5\"\n              >\n                <div className=\"flex flex-1 flex-col justify-center gap-0.5\">\n                  {items.map((item, i) => (\n                    <motion.div\n                      key={item.label}\n                      initial={{ opacity: reducedMotion ? 1 : 0, y: reducedMotion ? 0 : 10 }}\n                      animate={{ opacity: 1, y: 0 }}\n                      transition={reducedMotion ? { duration: 0 } : { delay: 0.1 + i * 0.05, ...spring }}\n                    >\n                      <a\n                        href={item.href}\n                        onClick={(event) => { onNavigate?.(item, event); close(); }}\n                        className=\"block py-1 text-2xl font-light leading-snug text-menu-dim transition-colors hover:text-menu-ink\"\n                      >\n                        {item.label}\n                      </a>\n                    </motion.div>\n                  ))}\n                </div>\n\n                {footer && (\n                  <motion.div\n                    initial={{ opacity: reducedMotion ? 1 : 0 }}\n                    animate={{ opacity: 1 }}\n                    transition={{ delay: reducedMotion ? 0 : 0.1 + items.length * 0.05, ...(reducedMotion ? { duration: 0 } : {}) }}\n                    className=\"mt-4 border-t border-menu-footer-rule pt-4 text-sm text-menu-dim [&_a:hover]:text-menu-ink [&_a]:transition-colors [&_button:hover]:text-menu-ink [&_button]:transition-colors\"\n                  >\n                    {footer}\n                  </motion.div>\n                )}\n              </motion.nav>\n            )}\n          </AnimatePresence>\n        </div>\n      </motion.div>\n    </div>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "meta": {
    "kind": "widget",
    "whenToUse": "Use for compact shared navigation. Preserves Ledge’s 148×56 pill, 300px panel, fixed 28px corners, frosted layers, spring timing and staggered links; supports a footer slot.",
    "origin": {
      "project": "Ledge",
      "path": "src/components/expanding-menu.tsx"
    },
    "credits": []
  },
  "type": "registry:component"
}
