// SSR-safe portal: renders content inline on the server (inside a
// display:contents host), then reparents the host to the target on mount.
// Marko tracks nodes by reference, so moved nodes keep working.
//
// Target resolution order (mirrors the official React/Preact Portal API):
// container() element, then `to` selector, then the owner document's body.
// getRootNode= keeps selector lookups and the body fallback correct inside
// shadow roots and iframes — pass the same closure the machine receives.
export interface Input {
  /** CSS selector for the portal target. Ignored when `container` matches. */
  to?: string;
  /**
   * Target element getter (`container=() => el()`), highest priority.
   * A closure, not a raw element — tag input must stay serializable.
   */
  container?: () => Element | null | undefined;
  /**
   * Root node resolver for custom environments (shadow DOM, iframes) —
   * scopes the `to` selector lookup and the body fallback to the right
   * document. Same shape as Zag's `getRootNode` machine prop.
   */
  getRootNode?: () => ShadowRoot | Document | Node;
  /** Disable reparenting (render in place). */
  disabled?: boolean;
  content?: Marko.Body;
}

<div/host style="display:contents" data-portal>
  <${input.content}/>
</div>

<lifecycle
  onMount() {
    if (input.disabled) return;
    const el = host();
    if (!el) return;
    const rootNode = input.getRootNode?.() ?? document;
    const doc = (rootNode.ownerDocument ?? rootNode) as Document;
    // Selector lookups run against the root node itself (a ShadowRoot
    // scopes queries to the shadow tree); the body fallback always comes
    // from the owner document.
    const queryRoot = "querySelector" in rootNode ? (rootNode as Document | ShadowRoot) : doc;
    const target =
      input.container?.() ??
      (input.to ? queryRoot.querySelector(input.to) : null) ??
      doc.body;
    if (target && el.parentNode !== target) target.appendChild(el);
  }
  onDestroy() {
    // This manual remove() is REQUIRED, not defensive: Marko's own removal
    // walks up from the tracked node through its original parent chain, but
    // that parent no longer contains this host once it has been reparented
    // to the portal target. Without this, the host would be orphaned on the
    // target instead of being cleaned up.
    host()?.remove();
  }
/>
