{
  "$schema": "https://mian.ro/schema/v2/registry.json",
  "schemaVersion": "2.0.0",
  "generatedAt": "2026-10-02T13:14:06.957Z",
  "id": "dropdown",
  "name": "Dropdown menu",
  "category": "Actions",
  "description": "A menu of actions anchored to a trigger. Built on the native Popover API: top layer, light dismiss and Escape are free.",
  "tags": [
    "dropdown",
    "menu",
    "context menu",
    "actions",
    "popover",
    "more"
  ],
  "requiresJs": true,
  "license": "LicenseRef-MIAN-1.0",
  "composition": {
    "role": null,
    "intents": [
      "dropdown menu",
      "more actions menu",
      "kebab menu",
      "user menu",
      "overflow menu"
    ],
    "prominence": null,
    "density": null,
    "worksAfter": [],
    "worksBefore": [],
    "alternatives": [
      "context-menu",
      "select"
    ],
    "avoid": null
  },
  "url": "https://mian.ro/components/dropdown",
  "markdownUrl": "https://mian.ro/ai/components/dropdown.md",
  "classes": [
    {
      "name": "mn-menu + popover",
      "description": "The menu panel. Give it an id and point a button's popovertarget at it."
    },
    {
      "name": "mn-menu-item",
      "description": "A menu item: button or link. Add mn-danger for destructive items."
    },
    {
      "name": "mn-menu-label · mn-menu-sep · mn-menu-shortcut",
      "description": "Group label, separator, trailing shortcut hint."
    },
    {
      "name": "data-placement",
      "description": "bottom-start (default), bottom-end, top-start, top-end, right-start, left-start…"
    },
    {
      "name": "aria-checked",
      "description": "Makes an item a checkable menuitemcheckbox; the menu stays open when toggled."
    }
  ],
  "accessibility": [
    "mian.js adds role=\"menu\" and role=\"menuitem\", wires aria-expanded and aria-haspopup on the trigger.",
    "Focus moves to the first item on open. Arrow keys, Home, End and type-ahead navigate. Escape and Tab close.",
    "Focus returns to the trigger after choosing an item.",
    "The menu emits mn:select with the chosen item."
  ],
  "examples": [
    {
      "title": "Basic",
      "description": null,
      "html": "<button class=\"mn-btn mn-btn-outline\" popovertarget=\"dd-account\">My account <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></button>\n<div class=\"mn-menu\" popover id=\"dd-account\">\n  <div class=\"mn-menu-label\">Signed in as ana@mian.ro</div>\n  <button class=\"mn-menu-item\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"8\" r=\"4\"/><path d=\"M4 21a8 8 0 0 1 16 0\"/></svg> Profile <span class=\"mn-menu-shortcut\">⌘P</span></button>\n  <button class=\"mn-menu-item\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\"/><path d=\"M3 10h18M7 15h3\"/></svg> Billing</button>\n  <button class=\"mn-menu-item\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"3\"/><path d=\"M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1Z\"/></svg> Settings <span class=\"mn-menu-shortcut\">⌘,</span></button>\n  <hr class=\"mn-menu-sep\">\n  <button class=\"mn-menu-item mn-danger\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M15 4h3a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1h-3M10 16l4-4-4-4M14 12H4\"/></svg> Log out</button>\n</div>"
    },
    {
      "title": "Checkable items",
      "description": null,
      "html": "<button class=\"mn-btn mn-btn-outline\" popovertarget=\"dd-cols\">Columns <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></button>\n<div class=\"mn-menu\" popover id=\"dd-cols\">\n  <div class=\"mn-menu-label\">Visible columns</div>\n  <button class=\"mn-menu-item\" aria-checked=\"true\">Name</button>\n  <button class=\"mn-menu-item\" aria-checked=\"true\">Email</button>\n  <button class=\"mn-menu-item\" aria-checked=\"false\">Phone</button>\n  <button class=\"mn-menu-item\" aria-checked=\"false\" aria-disabled=\"true\">ID (always shown)</button>\n</div>"
    },
    {
      "title": "Icon trigger, aligned to the end",
      "description": null,
      "html": "<button class=\"mn-btn mn-btn-icon mn-btn-ghost\" aria-label=\"Row actions\" popovertarget=\"dd-row\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"5\" cy=\"12\" r=\"1.2\"/><circle cx=\"12\" cy=\"12\" r=\"1.2\"/><circle cx=\"19\" cy=\"12\" r=\"1.2\"/></svg></button>\n<div class=\"mn-menu\" popover id=\"dd-row\" data-placement=\"bottom-end\">\n  <button class=\"mn-menu-item\"><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 20h4L19 9l-4-4L4 16v4Z\"/><path d=\"m14 6 4 4\"/></svg> Edit</button>\n  <button class=\"mn-menu-item\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect x=\"9\" y=\"9\" width=\"12\" height=\"12\" rx=\"2\"/><path d=\"M5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1\"/></svg> Duplicate</button>\n  <hr class=\"mn-menu-sep\">\n  <button class=\"mn-menu-item mn-danger\"><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 6h18M8 6V4h8v2M6 6l1 14h10l1-14\"/></svg> Delete</button>\n</div>"
    }
  ],
  "css": ".mn-menu {\n  min-width: 12rem; max-width: min(22rem, calc(100vw - 16px));\n  margin: 0; padding: 4px;\n  border: var(--mn-border-w) solid var(--mn-border);\n  border-radius: var(--mn-radius-box);\n  background: var(--mn-popover); color: var(--mn-fg);\n  box-shadow: var(--mn-shadow-lg);\n  font: 400 var(--mn-text-sm)/1.4 var(--mn-font);\n}\n.mn-menu[popover] { position: fixed; inset: auto; overflow: auto; max-height: min(24rem, calc(100vh - 24px)); }\n.mn-menu[popover]:popover-open { animation: var(--mn-enter) var(--mn-enter-duration) var(--mn-ease-enter); transform-origin: top left; }\n.mn-menu-item {\n  display: flex; align-items: center; gap: .6rem;\n  width: 100%; min-height: calc(var(--mn-size-md) - 6px); padding: .35rem .6rem;\n  border: 0; border-radius: calc(var(--mn-radius-box) - 4px);\n  background: none; color: inherit; font: inherit; text-align: start; text-decoration: none;\n  cursor: pointer; outline: none;\n}\n.mn-menu-item:hover, .mn-menu-item:focus-visible { background: var(--mn-hover); }\n.mn-menu-item:focus-visible { box-shadow: inset 0 0 0 2px var(--mn-ring); }\n.mn-menu-item svg { width: 1rem; height: 1rem; color: var(--mn-muted); flex: none; }\n.mn-menu-item[aria-disabled=\"true\"], .mn-menu-item:disabled { opacity: .5; pointer-events: none; }\n.mn-menu-item.mn-danger { color: var(--mn-danger-text); }\n.mn-menu-item.mn-danger svg { color: currentColor; }\n.mn-menu-item[aria-checked=\"true\"]::after { content: \"✓\"; margin-inline-start: auto; font-weight: 600; }\n.mn-menu-label { padding: .5rem .6rem .25rem; font-size: .75rem; font-weight: 600; color: var(--mn-muted); }\n.mn-menu-sep { height: 1px; margin: 4px -4px; border: 0; background: var(--mn-border); }\n.mn-menu-shortcut { margin-inline-start: auto; padding-inline-start: 1rem; font-size: .75rem; color: var(--mn-muted); letter-spacing: .04em; }\n\n@keyframes mn-spin { to { transform: rotate(360deg); } }\n@keyframes mn-pop { from { opacity: 0; transform: translateY(-4px) scale(.98); } }\n@keyframes mn-fade { from { opacity: 0; } }",
  "js": "/* Dropdown menus and popovers use the native Popover API for top-layer\n   rendering, light dismiss and Escape. MIAN adds anchoring and keyboard\n   navigation. */\nconst invokers = new WeakMap();\n\ndocument.addEventListener('click', (e) => {\n  const btn = e.target.closest('[popovertarget]');\n  if (btn) {\n    const target = document.getElementById(btn.getAttribute('popovertarget'));\n    if (target) invokers.set(target, btn);\n  }\n}, true);\n\nconst menuItems = (menu) => [...menu.querySelectorAll('.mn-menu-item')]\n  .filter((i) => !i.matches(':disabled, [aria-disabled=\"true\"]') && !i.closest('[hidden]'));\n\nregister('popover', (root) => {\n  each(root, '.mn-menu[popover], .mn-popover[popover]', 'Pop', (pop) => {\n    const isMenu = pop.classList.contains('mn-menu');\n    if (isMenu && !pop.getAttribute('role')) pop.setAttribute('role', 'menu');\n    if (isMenu) pop.querySelectorAll('.mn-menu-item').forEach((i) => {\n      if (!i.getAttribute('role')) i.setAttribute('role', i.hasAttribute('aria-checked') ? 'menuitemcheckbox' : 'menuitem');\n      i.tabIndex = -1;\n    });\n    // Wire aria on every invoker we can find.\n    document.querySelectorAll(`[popovertarget=\"${id(pop, 'mn-pop')}\"]`).forEach((b) => {\n      b.setAttribute('aria-expanded', 'false');\n      if (isMenu) b.setAttribute('aria-haspopup', 'menu');\n    });\n\n    let anchor = null;\n    const reposition = () => { if (anchor && pop.matches(':popover-open')) place(pop, anchor, pop.dataset.placement || 'bottom-start'); };\n\n    pop.addEventListener('toggle', (e) => {\n      anchor = invokers.get(pop) || document.querySelector(`[popovertarget=\"${pop.id}\"]`);\n      const open = e.newState === 'open';\n      anchor?.setAttribute('aria-expanded', String(open));\n      if (open) {\n        reposition();\n        addEventListener('scroll', reposition, true);\n        addEventListener('resize', reposition);\n        if (isMenu) (menuItems(pop)[0] || pop).focus({ preventScroll: true });\n      } else {\n        removeEventListener('scroll', reposition, true);\n        removeEventListener('resize', reposition);\n      }\n    });\n\n    if (!isMenu) return;\n    let buffer = '', timer = 0;\n    pop.addEventListener('keydown', (e) => {\n      const items = menuItems(pop);\n      const i = items.indexOf(document.activeElement);\n      let n = -1;\n      if (e.key === 'ArrowDown') n = (i + 1) % items.length;\n      else if (e.key === 'ArrowUp') n = (i - 1 + items.length) % items.length;\n      else if (e.key === 'Home') n = 0;\n      else if (e.key === 'End') n = items.length - 1;\n      else if (e.key === 'Tab') { pop.hidePopover(); return; }\n      else if (e.key.length === 1 && /\\S/.test(e.key)) {\n        clearTimeout(timer); buffer += e.key.toLowerCase(); timer = setTimeout(() => (buffer = ''), 500);\n        n = items.findIndex((it, k) => k > i - (buffer.length > 1 ? 1 : 0) && it.textContent.trim().toLowerCase().startsWith(buffer));\n        if (n < 0) n = items.findIndex((it) => it.textContent.trim().toLowerCase().startsWith(buffer));\n      }\n      if (n >= 0) { e.preventDefault(); items[n].focus(); }\n    });\n    pop.addEventListener('click', (e) => {\n      const item = e.target.closest('.mn-menu-item');\n      if (!item || item.matches(':disabled, [aria-disabled=\"true\"]')) return;\n      if (item.hasAttribute('aria-checked')) {\n        item.setAttribute('aria-checked', String(item.getAttribute('aria-checked') !== 'true'));\n        return;   // checkable items keep the menu open\n      }\n      emit(pop, 'select', { item, value: item.dataset.value ?? item.textContent.trim() });\n      pop.hidePopover();\n      anchor?.focus({ preventScroll: true });\n    });\n  });\n});",
  "install": {
    "css": "https://mian.ro/dist/mian.css",
    "js": "https://mian.ro/dist/mian.js",
    "registryItem": "https://mian.ro/r/dropdown.json"
  }
}
