{
  "name": "menu",
  "title": {
    "es": "Menú desplegable",
    "en": "Dropdown menu"
  },
  "summary": {
    "es": "Lista de acciones que se abre desde un botón.",
    "en": "List of actions opened from a button."
  },
  "category": "disclosure",
  "status": "stable",
  "nivel": "componentes",
  "updated": "2026-09-29",
  "license": "MIT",
  "page": "https://componentes-lovat.vercel.app/c/menu/",
  "requires": [
    "button"
  ],
  "files": [
    {
      "type": "html",
      "url": "https://componentes-lovat.vercel.app/raw/menu/menu.html",
      "content": "<button class=\"c-btn c-btn--outline\" type=\"button\" id=\"menu-trigger\" aria-haspopup=\"menu\" aria-expanded=\"false\" aria-controls=\"menu-actions\" data-c-menu>\n  Acciones\n  <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg>\n</button>\n<div class=\"c-menu__popup\" id=\"menu-actions\" popover role=\"menu\" aria-labelledby=\"menu-trigger\">\n  <div class=\"c-menu__list\">\n    <div class=\"c-menu__label\" role=\"presentation\">Proyecto</div>\n    <button class=\"c-menu__item\" role=\"menuitem\" type=\"button\" tabindex=\"-1\">\n      <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M12 20h9\"/><path d=\"M16.376 3.622a1 1 0 0 1 3.002 3.002L7.368 18.635a2 2 0 0 1-.855.506l-2.872.838a.5.5 0 0 1-.62-.62l.838-2.872a2 2 0 0 1 .506-.854z\"/></svg>\n      Editar <span class=\"c-menu__shortcut\" aria-hidden=\"true\">⌘E</span>\n    </button>\n    <button class=\"c-menu__item\" role=\"menuitem\" type=\"button\" tabindex=\"-1\">\n      <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg>\n      Duplicar <span class=\"c-menu__shortcut\" aria-hidden=\"true\">⌘D</span>\n    </button>\n    <button class=\"c-menu__item\" role=\"menuitem\" type=\"button\" tabindex=\"-1\" aria-disabled=\"true\">\n      <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8\"/><path d=\"m16 6-4-4-4 4\"/><path d=\"M12 2v13\"/></svg>\n      Compartir\n    </button>\n    <div class=\"c-menu__separator\" role=\"separator\"></div>\n    <button class=\"c-menu__item\" role=\"menuitem\" type=\"button\" tabindex=\"-1\" data-variant=\"destructive\">\n      <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3 6h18\"/><path d=\"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6\"/><path d=\"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2\"/></svg>\n      Eliminar\n    </button>\n  </div>\n</div>\n<p class=\"c-sr-only\" role=\"status\" data-c-menu-status></p>\n"
    },
    {
      "type": "css",
      "url": "https://componentes-lovat.vercel.app/raw/menu/menu.css",
      "content": "/* menu · Componentes (MIRÓ Labs). Diseño de coss ui (MIT, © coss.com, antes Origin UI).\n   Generado por tools/build_css.py desde menu.tw.json. No editar a mano. Necesita tokens.css. */\n.c-menu__popup{position:relative}\n.c-menu__separator{margin-inline:calc(var(--spacing)*2);margin-block:calc(var(--spacing)*1)}\n.c-menu__shortcut{margin-inline-start:auto}\n.c-menu__popup{display:flex}\n.c-menu__item{display:flex}\n.c-menu__separator{height:1px}\n.c-menu__list{max-height:var(--available-height)}\n.c-menu__item{min-height:calc(var(--spacing)*8)}\n.c-menu__list{width:100%}\n.c-menu__item{cursor:default;align-items:center;gap:calc(var(--spacing)*2)}\n.c-menu__list{overflow-y:auto}\n.c-menu__popup{border-radius:var(--radius)}\n.c-menu__item{border-radius:calc(var(--radius) - 4px)}\n.c-menu__popup{border-style:var(--tw-border-style);border-width:1px}\n.c-menu__separator{background-color:var(--border)}\n.c-menu__popup{background-color:var(--popover)}\n.c-menu__list{padding:calc(var(--spacing)*1)}\n.c-menu__item{padding-inline:calc(var(--spacing)*2)}\n.c-menu__label{padding-inline:calc(var(--spacing)*2)}\n.c-menu__item{padding-block:calc(var(--spacing)*1)}\n.c-menu__label{padding-block:calc(var(--spacing)*1.5)}\n.c-menu__shortcut{font-family:var(--font-sans)}\n.c-menu__item{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}\n.c-menu__label{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}\n.c-menu__shortcut{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}\n.c-menu__label{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}\n.c-menu__shortcut{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}\n.c-menu__item{color:var(--foreground)}\n.c-menu__label{color:var(--muted-foreground)}\n.c-menu__shortcut{color:var(--muted-foreground)}\n@supports (color:color-mix(in lab, red, red)){\n.c-menu__shortcut{color:color-mix(in oklab,var(--muted-foreground)72%,transparent)}\n}\n.c-menu__popup{--tw-shadow-alpha:5%;--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000000d),0 4px 6px -4px var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}\n@supports (color:lab(0% 0 0)){\n.c-menu__popup{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,lab(0% 0 0/.05)),0 4px 6px -4px var(--tw-shadow-color,lab(0% 0 0/.05))}\n}\n.c-menu__popup{--tw-outline-style:none;outline-style:none}\n.c-menu__item{--tw-outline-style:none;outline-style:none;-webkit-user-select:none;user-select:none}\n.c-menu__popup:not(:is(.dark *)){background-clip:padding-box}\n.c-menu__popup:before{content:var(--tw-content);pointer-events:none;content:var(--tw-content);position:absolute;content:var(--tw-content);inset:calc(var(--spacing)*0);content:var(--tw-content);border-radius:calc(var(--radius-lg) - 1px);content:var(--tw-content);--tw-shadow:0 1px var(--tw-shadow-color,#0000000a)}\n@supports (color:color-mix(in lab, red, red)){\n.c-menu__popup:before{--tw-shadow:0 1px var(--tw-shadow-color,color-mix(in oklab,var(--color-black)4%,transparent))}\n}\n.c-menu__popup:before{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}\n.c-menu__popup:focus{--tw-outline-style:none;outline-style:none}\n.c-menu__item[aria-disabled=true]{pointer-events:none;opacity:.64}\n.c-menu__item:is(:hover,:focus-visible,:focus){background-color:var(--accent);color:var(--accent-foreground)}\n.c-menu__item[data-variant=destructive]{color:var(--destructive-foreground)}\n@media (min-width:40rem){\n.c-menu__item{min-height:calc(var(--spacing)*7);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}\n}\n.c-menu__popup:is(.dark *):before{content:var(--tw-content);--tw-shadow:0 -1px var(--tw-shadow-color,#ffffff0f)}\n@supports (color:color-mix(in lab, red, red)){\n.c-menu__popup:is(.dark *):before{--tw-shadow:0 -1px var(--tw-shadow-color,color-mix(in oklab,var(--color-white)6%,transparent))}\n}\n.c-menu__popup:is(.dark *):before{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}\n.c-menu__item>svg{pointer-events:none;margin-inline:calc(var(--spacing)*-.5);flex-shrink:0}\n.c-menu__item>svg:not([class*=opacity-]){opacity:.8}\n.c-menu__item>svg:not([class*=size-]){width:calc(var(--spacing)*4.5);height:calc(var(--spacing)*4.5)}\n@media (min-width:40rem){\n.c-menu__item>svg:not([class*=size-]){width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}\n}\n/* propio */\n.c-menu__popup{position:fixed;margin:0;padding:0;color:var(--popover-foreground);inset:auto;opacity:0;scale:.98;transform-origin:top left;transition:opacity .1s,scale .1s,overlay .1s allow-discrete,display .1s allow-discrete}\n.c-menu__popup:popover-open{opacity:1;scale:1}\n@starting-style{.c-menu__popup:popover-open{opacity:0;scale:.98}}\n.c-menu__item{width:100%;background:transparent;border:0;text-align:start;font:inherit;cursor:default}\n.c-menu__item[data-variant=destructive] svg{color:currentColor}\n.c-menu__list{display:grid}\n.c-menu__popup{min-width:calc(var(--spacing)*32)}\n.c-sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}\n"
    },
    {
      "type": "js",
      "url": "https://componentes-lovat.vercel.app/raw/menu/menu.js",
      "content": "/* Menú desplegable (patrón «menu button» de la W3C) sobre popover nativo.\n   El popover da la capa superior y el cierre al hacer clic fuera; aquí va el teclado y la posición. */\n(function () {\n  const C = (window.Componentes = window.Componentes || {});\n\n  function position(trigger, popup) {\n    const r = trigger.getBoundingClientRect();\n    const h = popup.offsetHeight, w = popup.offsetWidth;\n    const below = r.bottom + 4 + h <= innerHeight || r.top - 4 - h < 0;\n    popup.style.top = (below ? r.bottom + 4 : r.top - 4 - h) + 'px';\n    popup.style.left = Math.max(8, Math.min(r.left, innerWidth - w - 8)) + 'px';\n    popup.style.transformOrigin = below ? 'top left' : 'bottom left';\n  }\n\n  function setup(trigger) {\n    const popup = document.getElementById(trigger.getAttribute('aria-controls'));\n    if (!popup || !popup.showPopover) return;\n    const items = () => [...popup.querySelectorAll('[role^=\"menuitem\"]')];\n    const enabled = () => items().filter((i) => i.getAttribute('aria-disabled') !== 'true');\n    const focusAt = (i) => { const list = enabled(); if (list.length) list[(i + list.length) % list.length].focus(); };\n\n    const open = (where) => {\n      if (!popup.matches(':popover-open')) popup.showPopover();\n      position(trigger, popup);\n      requestAnimationFrame(() => focusAt(where === 'last' ? -1 : 0));\n    };\n    const close = (refocus) => { if (popup.matches(':popover-open')) popup.hidePopover(); if (refocus) trigger.focus(); };\n\n    popup.addEventListener('toggle', (e) => {\n      const isOpen = e.newState === 'open';\n      trigger.setAttribute('aria-expanded', String(isOpen));\n      if (!isOpen && popup.contains(document.activeElement)) trigger.focus();\n    });\n    trigger.addEventListener('click', () => (popup.matches(':popover-open') ? close(false) : open('first')));\n    trigger.addEventListener('keydown', (e) => {\n      if (e.key === 'ArrowDown') { e.preventDefault(); open('first'); }\n      if (e.key === 'ArrowUp') { e.preventDefault(); open('last'); }\n    });\n    popup.addEventListener('keydown', (e) => {\n      const list = enabled(), i = list.indexOf(document.activeElement);\n      if (e.key === 'ArrowDown') { e.preventDefault(); focusAt(i + 1); }\n      else if (e.key === 'ArrowUp') { e.preventDefault(); focusAt(i - 1); }\n      else if (e.key === 'Home') { e.preventDefault(); focusAt(0); }\n      else if (e.key === 'End') { e.preventDefault(); focusAt(-1); }\n      else if (e.key === 'Escape') { e.preventDefault(); close(true); }\n      else if (e.key === 'Tab') { close(false); }\n      else if (e.key.length === 1 && /\\S/.test(e.key)) {\n        const k = e.key.toLowerCase();\n        const from = list.slice(i + 1).concat(list.slice(0, i + 1));\n        const hit = from.find((el) => el.textContent.trim().toLowerCase().startsWith(k));\n        if (hit) hit.focus();\n      }\n    });\n    popup.addEventListener('pointermove', (e) => {\n      const item = e.target.closest('[role^=\"menuitem\"]');\n      if (item && item.getAttribute('aria-disabled') !== 'true' && document.activeElement !== item) item.focus({ preventScroll: true });\n    });\n    popup.addEventListener('click', (e) => {\n      const item = e.target.closest('[role^=\"menuitem\"]');\n      if (!item || item.getAttribute('aria-disabled') === 'true') return;\n      close(true);\n      const st = document.querySelector('[data-c-menu-status]');\n      if (st) st.textContent = item.textContent.replace(/\\s+/g, ' ').trim().replace(/⌘\\w$/, '').trim();\n      trigger.dispatchEvent(new CustomEvent('c-menu-select', { detail: { item }, bubbles: true }));\n    });\n    addEventListener('resize', () => { if (popup.matches(':popover-open')) position(trigger, popup); });\n    addEventListener('scroll', () => { if (popup.matches(':popover-open')) position(trigger, popup); }, true);\n  }\n\n  function init(root) {\n    (root || document).querySelectorAll('[data-c-menu]:not([data-c-ready])').forEach((t) => {\n      t.dataset.cReady = '';\n      try { setup(t); } catch (e) { console.warn('[menu]', e); }\n    });\n  }\n\n  C.menu = { init };\n  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => init());\n  else init();\n})();\n"
    },
    {
      "type": "standalone",
      "url": "https://componentes-lovat.vercel.app/r/menu.html"
    }
  ],
  "tokensFile": "https://componentes-lovat.vercel.app/tokens.css",
  "darkTheme": "add class 'dark' to <html>",
  "contexto": "herramienta",
  "densidad": "media",
  "tono": "sobria",
  "states": [
    "rest",
    "hover",
    "focus",
    "open",
    "disabled"
  ],
  "use": {
    "es": [
      "Para acciones secundarias sobre un elemento."
    ],
    "en": [
      "For secondary actions on an item."
    ]
  },
  "avoid": {
    "es": [
      "Para navegar entre páginas: usa enlaces.",
      "Para elegir un valor: usa select."
    ],
    "en": [
      "To navigate between pages: use links.",
      "To pick a value: use a select."
    ]
  },
  "a11y": {
    "roles": [
      "button",
      "menu",
      "menuitem"
    ],
    "keyboard": [
      {
        "keys": "Enter / ↓",
        "es": "Abre y enfoca la primera acción",
        "en": "Opens and focuses the first action"
      },
      {
        "keys": "↑ / ↓",
        "es": "Recorre las acciones",
        "en": "Moves through actions"
      },
      {
        "keys": "Esc",
        "es": "Cierra y vuelve al botón",
        "en": "Closes and returns to the button"
      },
      {
        "keys": "Letra / Letter",
        "es": "Salta a la acción",
        "en": "Jumps to the action"
      }
    ],
    "notes": {
      "es": [
        "Patrón «menu button» de la W3C sobre popover nativo (capa superior y clic fuera).",
        "Las acciones deshabilitadas se saltan con las flechas."
      ],
      "en": [
        "W3C “menu button” pattern over a native popover (top layer and outside click).",
        "Disabled actions are skipped with the arrows."
      ]
    },
    "test": {
      "es": [
        "Abre con Enter, baja con ↓ y cierra con Esc: el foco vuelve a «Acciones»."
      ],
      "en": [
        "Open with Enter, move with ↓, close with Esc: focus returns to “Actions”."
      ]
    }
  },
  "issues": {
    "es": [],
    "en": []
  },
  "credits": [
    {
      "source": "coss ui (MIT) · menu.tsx",
      "url": "https://coss.com/ui"
    },
    {
      "source": "W3C WAI-ARIA APG, Menu Button",
      "url": "https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/"
    }
  ],
  "changes": [
    {
      "date": "2026-09-29",
      "es": "Diseño de coss ui en HTML, CSS y JS sin dependencias",
      "en": "coss ui design in dependency-free HTML, CSS and JS"
    }
  ],
  "partes": [],
  "pieza": "https://componentes-lovat.vercel.app/r/menu.html"
}
