{
  "$schema": "https://mian.ro/schema/v2/registry.json",
  "schemaVersion": "2.0.0",
  "generatedAt": "2026-10-02T13:14:06.957Z",
  "id": "popover",
  "name": "Popover",
  "category": "Overlays",
  "description": "Rich, non-modal content anchored to a trigger, on the native Popover API.",
  "tags": [
    "popover",
    "popup",
    "hover card",
    "flyout",
    "overlay"
  ],
  "requiresJs": true,
  "license": "LicenseRef-MIAN-1.0",
  "composition": {
    "role": null,
    "intents": [
      "popover",
      "floating panel",
      "date picker popup",
      "info bubble"
    ],
    "prominence": null,
    "density": null,
    "worksAfter": [],
    "worksBefore": [],
    "alternatives": [],
    "avoid": null
  },
  "url": "https://mian.ro/components/popover",
  "markdownUrl": "https://mian.ro/ai/components/popover.md",
  "classes": [
    {
      "name": "mn-popover + popover",
      "description": "The panel. Point popovertarget at it."
    },
    {
      "name": "data-placement",
      "description": "bottom (default), top, left, right, with -start or -end."
    },
    {
      "name": "mn-popover-title",
      "description": "Heading."
    }
  ],
  "accessibility": [
    "The browser handles Escape, light dismiss and aria-expanded on the invoker. Use a Modal instead if the content must be completed."
  ],
  "examples": [
    {
      "title": "Dimensions",
      "description": null,
      "html": "<button class=\"mn-btn mn-btn-outline\" popovertarget=\"pop-dim\">Dimensions</button>\n<div class=\"mn-popover\" popover id=\"pop-dim\" data-placement=\"bottom\">\n  <p class=\"mn-popover-title\">Dimensions</p>\n  <p class=\"mn-muted\" style=\"margin:0 0 .75rem\">Set the layer size.</p>\n  <div class=\"mn-form-row\" style=\"grid-template-columns:1fr 1fr\">\n    <div class=\"mn-field\"><label class=\"mn-label\" for=\"pw\">Width</label><input class=\"mn-input mn-input-sm\" id=\"pw\" value=\"100%\"></div>\n    <div class=\"mn-field\"><label class=\"mn-label\" for=\"ph\">Height</label><input class=\"mn-input mn-input-sm\" id=\"ph\" value=\"25px\"></div>\n  </div>\n</div>"
    }
  ],
  "css": ".mn-popover {\n  position: fixed; inset: auto; margin: 0;\n  width: max-content; max-width: min(20rem, calc(100vw - 16px));\n  padding: 1rem;\n  border: var(--mn-border-w) solid var(--mn-border); 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.55 var(--mn-font);\n}\n.mn-popover:popover-open { animation: var(--mn-enter) var(--mn-enter-duration) var(--mn-ease-enter); }\n.mn-popover-title { margin: 0 0 .3rem; font-weight: 600; }",
  "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/popover.json"
  }
}
