{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "modal-inert",
  "title": "Modal Inert",
  "description": "Helper used by the Get IT Done components.",
  "dependencies": [],
  "registryDependencies": [
    "https://zero-design.itdone.de/r/theme.json"
  ],
  "files": [
    {
      "path": "registry/lib/modal-inert.ts",
      "content": "'use client';\n\nimport * as React from 'react';\n\n// While a modal Sheet, Dialog or Drawer is open, everything outside its portal must leave the accessibility tree and the tab order (WCAG 4.1.2).\n// Radix hides the background with aria-hidden, but it keeps the whole ancestor chain of every aria-live element on the page. A single live region\n// (autosave status, toast, status text) anywhere inside the app shell keeps the shell and <main> exposed. This helper has no such exception:\n// only the modal layer itself and explicitly exempt elements (`data-modal-exempt`, set by the library ToastViewport) stay reachable.\nexport const MODAL_OVERLAY_ATTRIBUTE = 'data-modal-overlay';\nexport const MODAL_EXEMPT_ATTRIBUTE = 'data-modal-exempt';\n\n// Radix focus guards sit at the body edges and catch Tab and Shift+Tab leaving the layer; if they were inert, focus would leave the page once.\nconst FOCUS_GUARD_SELECTOR = '[data-radix-focus-guard]';\nconst EXEMPT_SELECTOR = `[${MODAL_EXEMPT_ATTRIBUTE}], [data-radix-toast-announce-root], [data-radix-toast-viewport], ${FOCUS_GUARD_SELECTOR}`;\n// Set by aria-hidden (used by Radix hideOthers) on every element it hides.\nconst RADIX_HIDDEN_MARKER = 'data-aria-hidden';\nconst SKIPPED_TAGS = new Set(['SCRIPT', 'STYLE', 'LINK', 'META', 'TEMPLATE', 'NOSCRIPT']);\n\ninterface Suppressed {\n  count: number;\n  inert: boolean;\n  ariaHidden: boolean;\n}\n\nconst suppressed = new WeakMap<Element, Suppressed>();\n\nconst supportsInert = () => typeof HTMLElement !== 'undefined' && 'inert' in HTMLElement.prototype;\n\ninterface SuppressOptions {\n  // Elements matching this selector stay reachable at any time while the layer is open, also when they mount later.\n  exemptSelector?: string;\n}\n\n// Marks every element outside `keep` (and outside the ancestors of `keep`) as inert, with aria-hidden where the browser has no inert. Returns the undo.\nfunction applySuppression(keep: Element[], root: HTMLElement): () => void {\n  const kept = new Set<Element>();\n  const mark = (el: Element | null) => {\n    if (!el || kept.has(el)) return;\n    kept.add(el);\n    mark(el.parentElement);\n  };\n  keep.forEach(mark);\n  const needsFallback = !supportsInert();\n  const touched: Element[] = [];\n  const walk = (parent: Element) => {\n    Array.from(parent.children).forEach((node) => {\n      if (SKIPPED_TAGS.has(node.tagName)) return;\n      if (kept.has(node)) {\n        // The kept node is the layer itself when it is a target, otherwise an ancestor of one.\n        if (!keep.includes(node)) walk(node);\n        return;\n      }\n      const entry = suppressed.get(node) ?? { count: 0, inert: false, ariaHidden: false };\n      if (entry.count === 0) {\n        if (!node.hasAttribute('inert')) {\n          node.setAttribute('inert', '');\n          entry.inert = true;\n        }\n        if (needsFallback && node.getAttribute('aria-hidden') !== 'true') {\n          node.setAttribute('aria-hidden', 'true');\n          entry.ariaHidden = true;\n        }\n      }\n      entry.count += 1;\n      suppressed.set(node, entry);\n      touched.push(node);\n    });\n  };\n  walk(root);\n  return () => {\n    touched.forEach((node) => {\n      const entry = suppressed.get(node);\n      if (!entry) return;\n      entry.count -= 1;\n      if (entry.count > 0) return;\n      if (entry.inert) node.removeAttribute('inert');\n      if (entry.ariaHidden) node.removeAttribute('aria-hidden');\n      suppressed.delete(node);\n    });\n  };\n}\n\n// Radix hideOthers hides the exempt region and its ancestors (a toast region is not aria-live); the page is inert, so the chain can be exposed again.\nfunction exposeExempt(exempt: Element[], root: HTMLElement) {\n  exempt.forEach((el) => {\n    for (let node: Element | null = el; node && node !== root; node = node.parentElement) {\n      if (node.getAttribute('aria-hidden') === 'true' && node.hasAttribute(RADIX_HIDDEN_MARKER)) node.removeAttribute('aria-hidden');\n    }\n  });\n}\n\nconst TABBABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\n// The modal focus trap and its outside-focus dismissal listen on the document, so focus events for exempt elements are stopped at the root element.\n// Focus that lands on an exempt element without anything to operate (an emptied toast list) goes back to where it was in the modal.\nfunction guardExemptFocus(keep: Element[], exemptRoots: () => Element[]): { release: () => void; settle: () => void } {\n  const host = document.documentElement;\n  // Focus guards are kept reachable but belong to the Radix focus scope, so its own handling of focus on them is left alone.\n  const exemptOf = (target: EventTarget | null) =>\n    target instanceof Element ? exemptRoots().find((el) => !el.matches(FOCUS_GUARD_SELECTOR) && el.contains(target)) : undefined;\n  let lastInside: Element | null = null;\n  let fromExempt = false;\n  const onTrack = (event: FocusEvent) => {\n    if (event.target instanceof Element && keep.some((layer) => layer.contains(event.target as Element))) lastInside = event.target;\n  };\n  const returnFocus = () => {\n    const back = lastInside && lastInside.isConnected ? lastInside : keep.map((layer) => layer.querySelector(TABBABLE)).find(Boolean);\n    if (back instanceof HTMLElement) back.focus({ preventScroll: true });\n  };\n  // Called after the exempt layer changed (a toast was removed): focus left on an emptied exempt element, or lost to the body, returns to the modal.\n  const settle = () => {\n    const active = document.activeElement;\n    const exempt = exemptOf(active);\n    if ((exempt && !exempt.querySelector(TABBABLE)) || (!exempt && fromExempt && (!active || active === document.body))) returnFocus();\n  };\n  const onFocusIn = (event: FocusEvent) => {\n    const exempt = exemptOf(event.target);\n    fromExempt = Boolean(exempt);\n    if (!exempt) return;\n    event.stopPropagation();\n    if (!exempt.querySelector(TABBABLE)) returnFocus();\n  };\n  const onFocusOut = (event: FocusEvent) => {\n    if (exemptOf(event.relatedTarget)) event.stopPropagation();\n    // Focus that leaves an exempt element for nowhere (shift+tab off the toast list) must not end on the body while the modal is open.\n    if (exemptOf(event.target) && !event.relatedTarget) {\n      requestAnimationFrame(() => {\n        const active = document.activeElement;\n        if (!active || active === document.body) returnFocus();\n      });\n    }\n  };\n  document.addEventListener('focusin', onTrack, true);\n  host.addEventListener('focusin', onFocusIn);\n  host.addEventListener('focusout', onFocusOut);\n  return {\n    settle,\n    release: () => {\n      document.removeEventListener('focusin', onTrack, true);\n      host.removeEventListener('focusin', onFocusIn);\n      host.removeEventListener('focusout', onFocusOut);\n    },\n  };\n}\n\n// Makes everything outside `keep` inert while a modal layer is open; elements matching `exemptSelector` (toast layers) stay reachable, also when mounted later.\nexport function suppressOutside(keep: Element[], root: HTMLElement = document.body, options: SuppressOptions = {}): () => void {\n  const { exemptSelector } = options;\n  if (!exemptSelector) return applySuppression(keep, root);\n  const findExempt = () => Array.from(root.querySelectorAll(exemptSelector)).filter((el) => !keep.some((layer) => layer.contains(el)));\n  let exempt = findExempt();\n  let undo = applySuppression([...keep, ...exempt], root);\n  exposeExempt(exempt, root);\n  const guard = guardExemptFocus(keep, () => exempt);\n  const observer = new MutationObserver(() => {\n    const next = findExempt();\n    if (next.length !== exempt.length || next.some((el) => !exempt.includes(el))) {\n      undo();\n      exempt = next;\n      undo = applySuppression([...keep, ...exempt], root);\n    }\n    exposeExempt(exempt, root);\n    guard.settle();\n  });\n  observer.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: [MODAL_EXEMPT_ATTRIBUTE, 'aria-hidden'] });\n  return () => {\n    observer.disconnect();\n    guard.release();\n    undo();\n  };\n}\n\n// Keeps the modal layer reachable: the content and its scrim. Exempt global live layers (toasts) are added by suppressOutside.\nfunction layerTargets(content: HTMLElement): Element[] {\n  const overlays = Array.from(content.parentElement?.children ?? []).filter((el) => el.hasAttribute(MODAL_OVERLAY_ATTRIBUTE));\n  return [content, ...overlays];\n}\n\n// Ref callback for the content element of a modal layer. Applies while the content is mounted and `enabled`, undoes on unmount or when disabled.\n// The page is freed as soon as the layer starts closing (`data-state=\"closed\"`), not when its exit animation ends: the scroll lock leaves\n// with the scrim, which is earlier than the content, so waiting for the content left the page inert after it could scroll again.\n// Nested layers stack through the shared counter. The callback is stable, so it is safe inside a composed ref.\nexport function useModalInertRef<T extends HTMLElement>(enabled: boolean) {\n  const [node, setNode] = React.useState<T | null>(null);\n  React.useEffect(() => {\n    if (!node || !enabled) return undefined;\n    let release: (() => void) | null = null;\n    const sync = () => {\n      const closing = node.getAttribute('data-state') === 'closed';\n      if (closing && release) {\n        release();\n        release = null;\n      } else if (!closing && !release) {\n        release = suppressOutside(layerTargets(node), document.body, { exemptSelector: EXEMPT_SELECTOR });\n      }\n    };\n    sync();\n    const observer = new MutationObserver(sync);\n    observer.observe(node, { attributes: true, attributeFilter: ['data-state'] });\n    return () => {\n      observer.disconnect();\n      release?.();\n    };\n  }, [node, enabled]);\n  return setNode;\n}\n\n// Runs `callback` once the layer is completely gone: content unmounted, scrim unmounted (the scroll lock leaves with it), inert removed, focus returned.\n// Call `run` from `onCloseAutoFocus`: Radix focuses the opener right after that handler, so the callback waits one task and for the scrim to leave the DOM.\nexport function useAfterModalClose<T extends HTMLElement>(onAfterClose?: () => void) {\n  const latest = React.useRef(onAfterClose);\n  latest.current = onAfterClose;\n  const scrims = React.useRef<Element[]>([]);\n  const capture = React.useCallback((node: T | null) => {\n    if (node) scrims.current = layerTargets(node).filter((el) => el !== node);\n  }, []);\n  const run = React.useCallback(() => {\n    if (!latest.current) return;\n    const started = Date.now();\n    const settle = () => {\n      const waiting = scrims.current.some((el) => el.isConnected) && Date.now() - started < 1000;\n      if (waiting) requestAnimationFrame(settle);\n      else latest.current?.();\n    };\n    setTimeout(settle, 0);\n  }, []);\n  return React.useMemo(() => ({ capture, run }), [capture, run]);\n}\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:lib"
}