{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "shell-page",
  "title": "ShellPage",
  "description": "The standard routed-page boundary inside ShellApp, owning sticky BarTitle or BarWorkflow placement, application-supplied BarTitle capacity, optional scroll compaction, and independent page-content inset and canvas surface without creating a nested scroll container.",
  "type": "registry:ui",
  "meta": {
    "library": "@ds-mo/ui",
    "distribution": "npm",
    "storybook": "https://zainadeel.github.io/compomo/?path=/story/shell-page",
    "source": "src/wc/components/ShellPage/ShellPage.tsx",
    "intentStatus": "complete",
    "intent": {
      "audience": "general",
      "status": "stable",
      "summary": "The standard routed-page boundary inside ShellApp, owning sticky BarTitle or BarWorkflow placement, application-supplied BarTitle capacity, optional scroll compaction, and independent page-content inset and canvas surface without creating a nested scroll container.",
      "useWhen": [
        "Rendering a standard routed page inside ShellApp's default content region.",
        "BarTitle should expand at a roomy page top, compact after scroll, or use constrained action layout when the application shell reports reduced capacity.",
        "BarWorkflow needs sticky page-level placement while remaining compact.",
        "Pages need consistent content gutters with an explicit full-bleed option.",
        "The page canvas surrounding routed content must use either the primary or secondary application surface."
      ],
      "avoidWhen": [
        "Building authentication, onboarding, public, modal, drawer, or intentionally focused fullscreen layouts outside the standard ShellApp page region.",
        "Another ShellPage already owns the routed page; never nest ShellPages.",
        "A bounded card or pane needs its own header rather than a page-level main landmark."
      ],
      "alternatives": [
        {
          "when": "The composition is a persistent application-wide workspace frame.",
          "component": "component:ds-shell-app",
          "reason": "ShellApp owns global navigation, tools, and the content scroll region; ShellPage belongs inside it."
        },
        {
          "when": "Only static page title chrome is required without sticky or responsive orchestration.",
          "component": "component:ds-bar-title",
          "reason": "BarTitle can be used standalone with an explicit variant."
        }
      ],
      "commonlyComposedWith": [
        "component:ds-shell-app",
        "component:ds-bar-title",
        "component:ds-bar-workflow",
        "component:ds-tab-group"
      ],
      "patterns": [
        "pattern:shell-page"
      ],
      "accessibility": [
        "ShellPage exposes the routed page as the main landmark; render exactly one active ShellPage inside ShellApp.",
        "Provide one BarTitle or BarWorkflow through the desktop header slot and one MobileHeader through mobile-header when the page participates in the mobile shell.",
        "Keep loading, empty, validation, error, progress, success, and failure communication in the default product-content slot.",
        "Responsive variant changes preserve the same heading, controls, accessible names, and keyboard focus targets."
      ],
      "states": [
        "headerCapacity=roomy, compact, or constrained is supplied before paint by the owning application shell and describes the page-header space available after viewport and shell-chrome policy are applied.",
        "headerPresentation=auto selects the matching header variant from headerCapacity and scroll position.",
        "expanded, compact, and constrained are explicit overrides for exceptional layouts and deterministic examples.",
        "contentInset=default supplies standard page gutters; none permits maps, canvases, and other full-bleed content.",
        "contentInsetBlockStart=none removes only the gutter adjacent to the page header while retaining the responsive inline and bottom gutters.",
        "contentInsetBlockStartSize can provide an exact roomy top seam, while compactContentInsetBlockStartSize independently defines the compact and constrained seam. ShellPage exposes their resolved sticky-header offset through --ds-shell-page-content-block-start-offset for viewport-fitted descendants.",
        "scrollCompaction=false keeps a roomy header expanded and pinned during page scrolling; capacity changes still select compact or constrained variants.",
        "contentSurface=primary keeps the standard page canvas; secondary paints both the inset gutter and the canvas beneath routed content. Desktop and tablet BarTitle chrome matches that canvas in expanded, compact, and constrained presentations; MobileHeader remains primary.",
        "contentInset and contentSurface are independent: either surface supports standard responsive gutters or a full-bleed page-owned layout.",
        "The header slot accepts one desktop BarTitle or BarWorkflow; mobile-header accepts the corresponding MobileHeader presentation. The default slot owns router output and all page product content."
      ],
      "responsiveBehavior": [
        "The owning application shell resolves viewport and persistent-chrome state into the explicit headerCapacity prop; ShellPage never guesses the cause of reduced width from its own rendered size.",
        "Roomy capacity expands at the top and compacts after scroll; compact capacity stays compact; constrained capacity uses the constrained action layout.",
        "Pages with scrollCompaction=false keep roomy headers expanded while still responding to compact or constrained capacity from tools and breakpoints.",
        "A ShellPage-managed BarTitle remains concealed until headerCapacity is available and its rendered variant matches the resolved presentation, preventing a default-variant flash during route activation.",
        "responsiveMode=mobile hides and inerts the desktop header, exposes mobile-header, and disables desktop scroll-compaction geometry.",
        "Update headerCapacity synchronously with shell layout state so opening tools or crossing application breakpoints cannot briefly render the wrong presentation.",
        "The default content inset is balanced on every side: 32px in desktop mode and 16px in tablet and mobile modes.",
        "contentInsetBlockStart can independently remove the top gutter for table and canvas pages that should meet the sticky header; exact roomy and compact block-start sizes support pages whose seam changes with header capacity.",
        "The selected content surface persists unchanged across desktop, tablet, and mobile modes.",
        "On a secondary desktop or tablet page, BarTitle matches the page canvas across capacity and scroll-driven variant changes; the separate mobile-header presentation remains primary.",
        "The flow-preservation spacer follows contentSurface so compacting a secondary page cannot expose a primary strip before its content.",
        "On roomy pages, the expanded header scrolls upward until its title and action row reaches the compact row position, while its bottom divider progressively extends from the expanded inset to edge-to-edge; it then snaps variants without moving that shared visual anchor.",
        "Scroll compaction preserves the expanded page flow offset so content never jumps beneath the sticky header.",
        "ShellPage relies on ShellApp's content scroller and never introduces nested scrolling.",
        "BarWorkflow remains compact and is not assigned BarTitle's capacity or scroll variants."
      ],
      "frameworkCaveats": {
        "customElements": [
          "Import ShellPage with BarTitle or BarWorkflow in slot=header and MobileHeader in slot=mobile-header, then render route content as default children.",
          "Do not set BarTitle.variant while ShellPage owns headerPresentation."
        ],
        "react": [
          "Render the desktop header with slot=header and DsMobileHeader with slot=mobile-header inside DsShellPage.",
          "Use contentInset none only for deliberately full-bleed page products, and choose contentSurface independently from the page's spacing model."
        ],
        "angular": [
          "Import the generated standalone DsShellPage adapter with the desktop header and DsMobileHeader, preserve both header slots, and place router-outlet in the default slot.",
          "Do not add another overflow auto wrapper around the routed page."
        ]
      },
      "references": [
        {
          "label": "Page-family and responsive examples",
          "path": "src/wc/components/ShellPage/ShellPage.stories.ts"
        },
        {
          "label": "Cross-browser geometry and interaction coverage",
          "path": "tests/e2e/bar-title.spec.ts"
        },
        {
          "label": "Reusable framework composition",
          "path": "agent/patterns/shell-page/pattern.agent.json"
        }
      ]
    },
    "api": {
      "props": {
        "compactContentInsetBlockStartSize": {
          "type": "string",
          "resolvedType": "string | undefined",
          "attribute": "compact-content-inset-block-start-size",
          "required": false,
          "mutable": false,
          "description": "Exact block-start content inset while the header is compact or constrained."
        },
        "contentInset": {
          "type": "ShellPageContentInset",
          "resolvedType": "\"default\" | \"none\"",
          "attribute": "content-inset",
          "default": "'default'",
          "required": false,
          "mutable": false,
          "description": "Standard page gutters, or no inset for full-bleed page content."
        },
        "contentInsetBlockStart": {
          "type": "ShellPageContentInset",
          "resolvedType": "\"default\" | \"none\"",
          "attribute": "content-inset-block-start",
          "default": "'default'",
          "required": false,
          "mutable": false,
          "description": "Preserve the responsive side/end gutters while allowing content to meet the header."
        },
        "contentInsetBlockStartSize": {
          "type": "string",
          "resolvedType": "string | undefined",
          "attribute": "content-inset-block-start-size",
          "required": false,
          "mutable": false,
          "description": "Exact block-start content inset, overriding the responsive default when provided."
        },
        "contentSurface": {
          "type": "ShellPageContentSurface",
          "resolvedType": "\"primary\" | \"secondary\"",
          "attribute": "content-surface",
          "default": "'primary'",
          "required": false,
          "mutable": false,
          "description": "Canvas surface painted around and beneath routed page content."
        },
        "headerCapacity": {
          "type": "ShellPageCapacity",
          "resolvedType": "\"compact\" | \"constrained\" | \"roomy\" | undefined",
          "attribute": "header-capacity",
          "required": false,
          "mutable": false,
          "description": "Available page-header capacity supplied by the owning application shell."
        },
        "headerPresentation": {
          "type": "ShellPageHeaderPresentation",
          "resolvedType": "\"auto\" | \"compact\" | \"constrained\" | \"expanded\"",
          "attribute": "header-presentation",
          "default": "'auto'",
          "required": false,
          "mutable": false,
          "description": "Automatic capacity/scroll behavior, or an explicit header variant override."
        },
        "responsiveMode": {
          "type": "ShellResponsiveMode",
          "resolvedType": "\"desktop\" | \"mobile\" | \"tablet\"",
          "attribute": "responsive-mode",
          "default": "'desktop'",
          "required": false,
          "mutable": false,
          "description": "Explicit shell breakpoint presentation. Mobile uses the dedicated mobile-header slot."
        },
        "scrollCompaction": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "scroll-compaction",
          "default": "true",
          "required": false,
          "mutable": false,
          "description": "Allow roomy automatic headers to compact as the page scrolls."
        }
      },
      "events": [],
      "methods": [],
      "slots": []
    },
    "props": {
      "compactContentInsetBlockStartSize": {
        "type": "string",
        "resolvedType": "string | undefined",
        "attribute": "compact-content-inset-block-start-size",
        "required": false,
        "mutable": false,
        "description": "Exact block-start content inset while the header is compact or constrained."
      },
      "contentInset": {
        "type": "ShellPageContentInset",
        "resolvedType": "\"default\" | \"none\"",
        "attribute": "content-inset",
        "default": "'default'",
        "required": false,
        "mutable": false,
        "description": "Standard page gutters, or no inset for full-bleed page content."
      },
      "contentInsetBlockStart": {
        "type": "ShellPageContentInset",
        "resolvedType": "\"default\" | \"none\"",
        "attribute": "content-inset-block-start",
        "default": "'default'",
        "required": false,
        "mutable": false,
        "description": "Preserve the responsive side/end gutters while allowing content to meet the header."
      },
      "contentInsetBlockStartSize": {
        "type": "string",
        "resolvedType": "string | undefined",
        "attribute": "content-inset-block-start-size",
        "required": false,
        "mutable": false,
        "description": "Exact block-start content inset, overriding the responsive default when provided."
      },
      "contentSurface": {
        "type": "ShellPageContentSurface",
        "resolvedType": "\"primary\" | \"secondary\"",
        "attribute": "content-surface",
        "default": "'primary'",
        "required": false,
        "mutable": false,
        "description": "Canvas surface painted around and beneath routed page content."
      },
      "headerCapacity": {
        "type": "ShellPageCapacity",
        "resolvedType": "\"compact\" | \"constrained\" | \"roomy\" | undefined",
        "attribute": "header-capacity",
        "required": false,
        "mutable": false,
        "description": "Available page-header capacity supplied by the owning application shell."
      },
      "headerPresentation": {
        "type": "ShellPageHeaderPresentation",
        "resolvedType": "\"auto\" | \"compact\" | \"constrained\" | \"expanded\"",
        "attribute": "header-presentation",
        "default": "'auto'",
        "required": false,
        "mutable": false,
        "description": "Automatic capacity/scroll behavior, or an explicit header variant override."
      },
      "responsiveMode": {
        "type": "ShellResponsiveMode",
        "resolvedType": "\"desktop\" | \"mobile\" | \"tablet\"",
        "attribute": "responsive-mode",
        "default": "'desktop'",
        "required": false,
        "mutable": false,
        "description": "Explicit shell breakpoint presentation. Mobile uses the dedicated mobile-header slot."
      },
      "scrollCompaction": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "scroll-compaction",
        "default": "true",
        "required": false,
        "mutable": false,
        "description": "Allow roomy automatic headers to compact as the page scrolls."
      }
    },
    "events": [],
    "methods": [],
    "slots": [],
    "exports": {
      "customElement": "ds-shell-page",
      "react": "DsShellPage",
      "vue": "DsShellPage",
      "angular": "DsShellPage"
    },
    "consumption": {
      "install": "npm install @ds-mo/ui @ds-mo/tokens @ds-mo/icons",
      "cssSetup": "import '@ds-mo/tokens';\nimport '@ds-mo/tokens/reset';\nimport '@ds-mo/tokens/globals';",
      "customElements": {
        "import": "import '@ds-mo/ui/dist/components/ds-shell-page.js';",
        "example": "<ds-shell-page></ds-shell-page>"
      },
      "react": {
        "import": "import { DsShellPage } from '@ds-mo/ui/react';",
        "example": "<DsShellPage />"
      },
      "vue": {
        "import": "import { DsShellPage } from '@ds-mo/ui/vue';",
        "example": "<DsShellPage />"
      },
      "angular": {
        "import": "import { DsShellPage } from '@ds-mo/ui/angular/ds-shell-page';",
        "example": "<ds-shell-page></ds-shell-page>"
      },
      "peerDependencies": {
        "required": [
          "@ds-mo/tokens ^6.5.0",
          "@ds-mo/icons ^7.0.0"
        ],
        "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
      }
    }
  },
  "dependencies": [
    "@ds-mo/ui",
    "@ds-mo/tokens"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "src/wc/components/ShellPage/ShellPage.css",
      "content": ":host {\n  --_shell-page-content-background: var(--color-background-primary);\n  --_shell-page-header-background: var(--color-background-primary);\n  --_shell-page-responsive-content-block-start-inset: var(--dimension-space-400);\n  --_shell-page-content-block-start-inset: var(\n    --ds-shell-page-content-block-start-inset,\n    var(--_shell-page-responsive-content-block-start-inset)\n  );\n  --ds-shell-page-sticky-header-block-size: var(--dimension-size-600);\n  --ds-shell-page-content-block-start-offset: calc(\n    var(--ds-shell-page-sticky-header-block-size) +\n      var(--_shell-page-content-block-start-inset)\n  );\n  display: block;\n  position: relative;\n  min-width: 0;\n  min-height: 100%;\n  box-sizing: border-box;\n  background: var(--_shell-page-content-background);\n}\n\n:host(.shell-page-host--surface-secondary) {\n  --_shell-page-content-background: var(--color-background-secondary);\n}\n\n:host(.shell-page-host--surface-secondary[responsive-mode='desktop']),\n:host(.shell-page-host--surface-secondary[responsive-mode='tablet']) {\n  --_shell-page-header-background: var(--color-background-secondary);\n}\n\n:host([responsive-mode='tablet']),\n:host([responsive-mode='mobile']) {\n  --_shell-page-responsive-content-block-start-inset: var(--dimension-space-200);\n}\n\n:host([responsive-mode='mobile']) {\n  --ds-shell-page-sticky-header-block-size: calc(\n    var(--dimension-size-500) + var(--dimension-space-200)\n  );\n}\n\n:host(.shell-page-host--header-compact),\n:host(.shell-page-host--header-constrained) {\n  --_shell-page-content-block-start-inset: var(\n    --ds-shell-page-compact-content-block-start-inset,\n    var(--_shell-page-responsive-content-block-start-inset)\n  );\n}\n\n:host(.shell-page-host--inset-none),\n:host(.shell-page-host--block-start-inset-none) {\n  --_shell-page-content-block-start-inset: 0px;\n}\n\n.shell-page__scroll-sentinel {\n  position: absolute;\n  inset-block-start: var(--ds-shell-page-header-travel, 0);\n  inset-inline-start: 0;\n  width: var(--dimension-stroke-width-012);\n  height: var(--dimension-stroke-width-012);\n  pointer-events: none;\n}\n\n.shell-page__sticky-header {\n  position: sticky;\n  inset-block-start: var(--ds-shell-page-sticky-offset, 0);\n  z-index: var(--dimension-z-index-raised);\n  min-width: 0;\n  background: var(--_shell-page-header-background);\n  overflow: visible;\n  overflow-anchor: none;\n}\n\n.shell-page__desktop-header ::slotted(ds-bar-title),\n.shell-page__desktop-header ::slotted(ds-bar-workflow),\n.shell-page__mobile-header ::slotted(ds-mobile-header) {\n  display: block;\n  min-width: 0;\n}\n\n.shell-page__desktop-header[hidden],\n.shell-page__mobile-header[hidden] {\n  display: none;\n}\n\n:host(.shell-page-host--mobile) .shell-page__scroll-sentinel,\n:host(.shell-page-host--mobile) .shell-page__flow-spacer {\n  display: none;\n}\n\n.shell-page__flow-spacer {\n  height: var(--ds-shell-page-flow-spacer, 0);\n  background: var(--_shell-page-content-background);\n}\n\n.shell-page__content {\n  min-width: 0;\n  box-sizing: border-box;\n  background: var(--_shell-page-content-background);\n}\n\n:host(.shell-page-host--inset-default) .shell-page__content {\n  padding: var(--dimension-space-400);\n  padding-block-start: var(--_shell-page-content-block-start-inset);\n}\n\n:host(.shell-page-host--inset-default[responsive-mode='tablet']) .shell-page__content,\n:host(.shell-page-host--inset-default[responsive-mode='mobile']) .shell-page__content {\n  padding: var(--dimension-space-200);\n  padding-block-start: var(--_shell-page-content-block-start-inset);\n}\n\n:host(.shell-page-host--inset-none) .shell-page__content {\n  padding: 0;\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "src/wc/components/ShellPage/ShellPage.tsx",
      "content": "import { Component, Element, h, Host, Prop, State, Watch } from '@stencil/core';\nimport type { ShellResponsiveMode } from '../../shell/shell-responsive';\nimport { resolveCssLengthPx } from '../../utils/resolve-css-length-px';\nimport type { BarTitleVariant } from '../BarTitle/bar-title-types';\nimport { resolveShellPageHeaderVariant } from './shell-page-responsive';\nimport type {\n  ShellPageCapacity,\n  ShellPageContentInset,\n  ShellPageContentSurface,\n  ShellPageHeaderPresentation,\n} from './shell-page-types';\n\ntype BarTitleElement = HTMLElement & { variant: BarTitleVariant };\n\n@Component({\n  tag: 'ds-shell-page',\n  styleUrl: 'ShellPage.css',\n  scoped: true,\n})\nexport class ShellPage {\n  private static readonly MAX_HEADER_REVEAL_FRAMES = 12;\n  private static readonly MAX_HEADER_GEOMETRY_FRAMES = 12;\n\n  @Element() el!: HTMLElement;\n\n  /** Automatic capacity/scroll behavior, or an explicit header variant override. */\n  @Prop() headerPresentation: ShellPageHeaderPresentation = 'auto';\n\n  /** Available page-header capacity supplied by the owning application shell. */\n  @Prop() headerCapacity?: ShellPageCapacity;\n\n  /** Standard page gutters, or no inset for full-bleed page content. */\n  @Prop() contentInset: ShellPageContentInset = 'default';\n\n  /** Preserve the responsive side/end gutters while allowing content to meet the header. */\n  @Prop() contentInsetBlockStart: ShellPageContentInset = 'default';\n\n  /** Exact block-start content inset, overriding the responsive default when provided. */\n  @Prop() contentInsetBlockStartSize?: string;\n\n  /** Exact block-start content inset while the header is compact or constrained. */\n  @Prop() compactContentInsetBlockStartSize?: string;\n\n  /** Allow roomy automatic headers to compact as the page scrolls. */\n  @Prop() scrollCompaction: boolean = true;\n\n  /** Canvas surface painted around and beneath routed page content. */\n  @Prop() contentSurface: ShellPageContentSurface = 'primary';\n\n  /** Explicit shell breakpoint presentation. Mobile uses the dedicated mobile-header slot. */\n  @Prop({ attribute: 'responsive-mode', reflect: true })\n  responsiveMode: ShellResponsiveMode = 'desktop';\n\n  @State() private pageTopVisible = true;\n\n  private sentinelEl: HTMLElement | null = null;\n  private spacerEl: HTMLElement | null = null;\n  private stickyHeaderEl: HTMLElement | null = null;\n  private headerEl: BarTitleElement | null = null;\n  private expandedHeaderHeight = 0;\n  private headerTravel = 0;\n  private expandedGeometryFrozen = false;\n  private pageHeaderResizeObserver: ResizeObserver | null = null;\n  private headerResizeObserver: ResizeObserver | null = null;\n  private headerMutationObserver: MutationObserver | null = null;\n  private headerRevealFrame: number | null = null;\n  private headerGeometryFrame: number | null = null;\n  private reconnectFrame: number | null = null;\n  private scrollRoot: HTMLElement | Window | null = null;\n  private hasLoaded = false;\n\n  @Watch('headerCapacity')\n  @Watch('headerPresentation')\n  @Watch('scrollCompaction')\n  handleHeaderContractChange() {\n    this.syncHeaderVariant(true);\n  }\n\n  @Watch('responsiveMode')\n  handleResponsiveModeChange() {\n    this.el.style.removeProperty('--ds-shell-page-sticky-header-block-size');\n    this.observeHeader(this.responsiveMode === 'mobile' ? null : this.findHeader());\n  }\n\n  componentDidLoad() {\n    this.hasLoaded = true;\n    this.connectRuntime();\n  }\n\n  componentDidRender() {\n    this.syncHeaderVariant();\n  }\n\n  connectedCallback() {\n    if (!this.hasLoaded) return;\n    if (this.reconnectFrame !== null) cancelAnimationFrame(this.reconnectFrame);\n    this.reconnectFrame = requestAnimationFrame(() => {\n      this.reconnectFrame = null;\n      if (this.el.isConnected) this.connectRuntime();\n    });\n  }\n\n  disconnectedCallback() {\n    if (this.reconnectFrame !== null) cancelAnimationFrame(this.reconnectFrame);\n    this.reconnectFrame = null;\n    this.headerResizeObserver?.disconnect();\n    this.headerResizeObserver = null;\n    this.pageHeaderResizeObserver?.disconnect();\n    this.pageHeaderResizeObserver = null;\n    this.headerMutationObserver?.disconnect();\n    this.headerMutationObserver = null;\n    this.cancelHeaderReveal();\n    this.cancelHeaderGeometrySync();\n    this.headerEl?.style.removeProperty('--ds-bar-title-divider-inset');\n    this.scrollRoot?.removeEventListener('scroll', this.handleScroll);\n    this.scrollRoot = null;\n  }\n\n  private connectRuntime() {\n    this.observePageHeaderGeometry();\n    this.observeHeader(this.responsiveMode === 'mobile' ? null : this.findHeader());\n    this.connectScrollRoot();\n  }\n\n  private get effectiveVariant(): BarTitleVariant {\n    return resolveShellPageHeaderVariant(\n      this.headerPresentation,\n      this.headerCapacity ?? 'roomy',\n      this.scrollCompaction ? this.pageTopVisible : true\n    );\n  }\n\n  private get headerContractResolved(): boolean {\n    return this.headerPresentation !== 'auto' || this.headerCapacity !== undefined;\n  }\n\n  private get isScrollCompacted(): boolean {\n    return (\n      this.scrollCompaction &&\n      this.headerPresentation === 'auto' &&\n      this.headerCapacity === 'roomy' &&\n      !this.pageTopVisible\n    );\n  }\n\n  private get contentInsetStyles(): { [name: string]: string } {\n    const styles: { [name: string]: string } = {};\n    const expanded = this.contentInsetBlockStartSize?.trim();\n    const compact = this.compactContentInsetBlockStartSize?.trim();\n    if (expanded) styles['--ds-shell-page-content-block-start-inset'] = expanded;\n    if (compact) styles['--ds-shell-page-compact-content-block-start-inset'] = compact;\n    return styles;\n  }\n\n  private composedParent(element: Element): Element | null {\n    return (element as HTMLElement).assignedSlot?.parentElement ?? element.parentElement;\n  }\n\n  private findScrollRoot(): HTMLElement | Window {\n    let ancestor = this.composedParent(this.el);\n    while (ancestor && ancestor !== document.documentElement) {\n      const overflow = getComputedStyle(ancestor).overflowY;\n      if (overflow === 'auto' || overflow === 'scroll' || overflow === 'overlay') {\n        return ancestor as HTMLElement;\n      }\n      ancestor = this.composedParent(ancestor);\n    }\n    return window;\n  }\n\n  private connectScrollRoot() {\n    const next = this.findScrollRoot();\n    if (next === this.scrollRoot) return;\n    this.scrollRoot?.removeEventListener('scroll', this.handleScroll);\n    this.scrollRoot = next;\n    this.scrollRoot.addEventListener('scroll', this.handleScroll, { passive: true });\n    this.handleScroll();\n  }\n\n  private get scrollRootResolved(): boolean {\n    const shell = this.el.closest('ds-shell-app');\n    if (!shell) return true;\n    const expected = shell.querySelector<HTMLElement>('.shell-app__content');\n    return expected !== null && this.scrollRoot === expected;\n  }\n\n  private handleScroll = () => {\n    if (!this.sentinelEl || !this.scrollRoot) return;\n    const rootTop =\n      this.scrollRoot === window ? 0 : (this.scrollRoot as HTMLElement).getBoundingClientRect().top;\n    this.syncHeaderDividerInset(rootTop);\n    const nextPageTopVisible = this.sentinelEl.getBoundingClientRect().top >= rootTop;\n    if (\n      this.scrollCompaction &&\n      !nextPageTopVisible &&\n      this.pageTopVisible &&\n      this.headerEl?.classList.contains(this.variantClass('expanded'))\n    ) {\n      this.captureExpandedHeaderGeometry(this.headerEl);\n      this.expandedGeometryFrozen = true;\n    }\n    if (\n      nextPageTopVisible &&\n      this.expandedGeometryFrozen &&\n      this.el.getBoundingClientRect().top >= rootTop\n    ) {\n      this.expandedGeometryFrozen = false;\n      if (this.headerEl?.classList.contains(this.variantClass('expanded'))) {\n        this.captureExpandedHeaderGeometry(this.headerEl);\n      }\n    }\n    this.pageTopVisible = nextPageTopVisible;\n  };\n\n  private findHeader(): BarTitleElement | null {\n    return this.el.querySelector<BarTitleElement>('ds-bar-title');\n  }\n\n  private get compactHeaderHeight(): number {\n    return resolveCssLengthPx('--dimension-size-600', 0);\n  }\n\n  private syncHeaderDividerInset(rootTop?: number) {\n    const header = this.headerEl;\n    if (!header) return;\n\n    const ownsScrollTransition =\n      this.scrollCompaction &&\n      this.headerPresentation === 'auto' &&\n      this.headerCapacity === 'roomy';\n    const expandedInset = resolveCssLengthPx('--dimension-space-400', 0);\n    if (\n      !ownsScrollTransition ||\n      !this.sentinelEl ||\n      !this.scrollRoot ||\n      this.headerTravel <= 0 ||\n      expandedInset <= 0\n    ) {\n      header.style.removeProperty('--ds-bar-title-divider-inset');\n      return;\n    }\n\n    const scrollRootTop =\n      rootTop ??\n      (this.scrollRoot === window\n        ? 0\n        : (this.scrollRoot as HTMLElement).getBoundingClientRect().top);\n    const remainingTravel = this.sentinelEl.getBoundingClientRect().top - scrollRootTop;\n    const progress = Math.min(1, Math.max(0, 1 - remainingTravel / this.headerTravel));\n    header.style.setProperty('--ds-bar-title-divider-inset', `${expandedInset * (1 - progress)}px`);\n  }\n\n  private setHeaderTravel(distance: number) {\n    const next = Math.max(0, distance);\n    if (Math.abs(next - this.headerTravel) < 0.5) return;\n    this.headerTravel = next;\n    this.el.style.setProperty('--ds-shell-page-header-travel', `${next}px`);\n    this.handleScroll();\n  }\n\n  private measureHeaderTravel(header: BarTitleElement) {\n    const anchor = header.querySelector<HTMLElement>('[data-shell-page-header-anchor]');\n    const compactHeight = this.compactHeaderHeight;\n    if (!anchor || compactHeight <= 0) return;\n\n    const headerRect = header.getBoundingClientRect();\n    const anchorRect = anchor.getBoundingClientRect();\n    if (headerRect.height <= 0 || anchorRect.height <= 0) return;\n\n    const expandedAnchorTop = anchorRect.top - headerRect.top;\n    const compactAnchorTop = Math.max(0, (compactHeight - anchorRect.height) / 2);\n    this.setHeaderTravel(expandedAnchorTop - compactAnchorTop);\n  }\n\n  private captureExpandedHeaderGeometry(header: BarTitleElement) {\n    // Keep the largest observed expanded geometry for this header instance.\n    // During a variant handoff Chromium can briefly report the compact box\n    // before the child host class mutation is delivered; that transient must\n    // not erase the flow reservation captured at the roomy page top.\n    this.expandedHeaderHeight = Math.max(\n      this.expandedHeaderHeight,\n      header.getBoundingClientRect().height\n    );\n    this.measureHeaderTravel(header);\n  }\n\n  private setSpacerHeight(height: number) {\n    if (!this.spacerEl) return;\n    this.spacerEl.style.setProperty('--ds-shell-page-flow-spacer', `${Math.max(0, height)}px`);\n  }\n\n  private variantClass(variant: BarTitleVariant): string {\n    return `bar-title-host--${variant}`;\n  }\n\n  private setStickyHeaderOffset(renderedVariant: BarTitleVariant) {\n    this.stickyHeaderEl?.style.setProperty(\n      '--ds-shell-page-sticky-offset',\n      renderedVariant === 'expanded' &&\n        this.scrollCompaction &&\n        this.headerPresentation === 'auto' &&\n        this.headerCapacity === 'roomy'\n        ? `${-this.headerTravel}px`\n        : '0px'\n    );\n  }\n\n  private syncPageHeaderBlockSize() {\n    const renderedHeight = this.stickyHeaderEl?.getBoundingClientRect().height ?? 0;\n    if (renderedHeight > 0) {\n      this.el.style.setProperty('--ds-shell-page-sticky-header-block-size', `${renderedHeight}px`);\n    }\n  }\n\n  private observePageHeaderGeometry() {\n    this.pageHeaderResizeObserver?.disconnect();\n    this.pageHeaderResizeObserver = null;\n    this.syncPageHeaderBlockSize();\n\n    if (!this.stickyHeaderEl || typeof ResizeObserver === 'undefined') return;\n    this.pageHeaderResizeObserver = new ResizeObserver(() => {\n      this.syncPageHeaderBlockSize();\n    });\n    this.pageHeaderResizeObserver.observe(this.stickyHeaderEl);\n  }\n\n  private syncRenderedHeaderGeometry(header: BarTitleElement) {\n    const renderedVariant = this.effectiveVariant;\n    if (!header.classList.contains(this.variantClass(renderedVariant))) return;\n\n    this.syncPageHeaderBlockSize();\n\n    // Update both flow-preservation pieces before reading layout. Otherwise a\n    // reverse compact → expanded render can briefly contain the tall header and\n    // the compact spacer together, giving browser scroll anchoring a transient\n    // height change to compensate.\n    this.setSpacerHeight(\n      this.isScrollCompacted ? this.expandedHeaderHeight - this.compactHeaderHeight : 0\n    );\n    this.setStickyHeaderOffset(renderedVariant);\n\n    if (renderedVariant === 'expanded' && !this.expandedGeometryFrozen) {\n      this.captureExpandedHeaderGeometry(header);\n      this.setStickyHeaderOffset(renderedVariant);\n    }\n  }\n\n  private cancelHeaderGeometrySync() {\n    if (this.headerGeometryFrame !== null) cancelAnimationFrame(this.headerGeometryFrame);\n    this.headerGeometryFrame = null;\n  }\n\n  private scheduleHeaderGeometrySync(header: BarTitleElement) {\n    this.cancelHeaderGeometrySync();\n    let framesRemaining = ShellPage.MAX_HEADER_GEOMETRY_FRAMES;\n    let framesElapsed = 0;\n    const sync = () => {\n      if (this.headerEl !== header) return;\n      framesRemaining -= 1;\n      framesElapsed += 1;\n      // In WebKit a child's componentDidLoad can precede its assignment into\n      // the parent's rendered slot. Revalidate after ShellApp has had a frame\n      // to expose the actual content scroller.\n      this.connectScrollRoot();\n      this.syncRenderedHeaderGeometry(header);\n      const geometryReady =\n        this.effectiveVariant !== 'expanded' ||\n        (this.expandedHeaderHeight > 0 && this.headerTravel > 0);\n      if (\n        (geometryReady && this.scrollRootResolved && framesElapsed >= 2) ||\n        framesRemaining <= 0\n      ) {\n        this.headerGeometryFrame = null;\n        return;\n      }\n      this.headerGeometryFrame = requestAnimationFrame(sync);\n    };\n    this.headerGeometryFrame = requestAnimationFrame(sync);\n  }\n\n  private cancelHeaderReveal() {\n    if (this.headerRevealFrame !== null) cancelAnimationFrame(this.headerRevealFrame);\n    this.headerRevealFrame = null;\n    this.headerEl?.removeAttribute('data-shell-page-syncing');\n  }\n\n  private revealHeaderWhenSynced(header: BarTitleElement, variant: BarTitleVariant) {\n    // Stencil normally re-renders within a frame or two of a prop change. Stop\n    // polling at the cap, but fail closed: a stale variant must never paint.\n    let framesRemaining = ShellPage.MAX_HEADER_REVEAL_FRAMES;\n    const reveal = () => {\n      if (this.headerEl !== header) {\n        header.removeAttribute('data-shell-page-syncing');\n        return;\n      }\n      framesRemaining -= 1;\n      if (header.classList.contains(this.variantClass(variant))) {\n        header.removeAttribute('data-shell-page-syncing');\n        this.headerRevealFrame = null;\n        return;\n      }\n      if (framesRemaining <= 0) {\n        this.headerRevealFrame = null;\n        return;\n      }\n      this.headerRevealFrame = requestAnimationFrame(reveal);\n    };\n    this.headerRevealFrame = requestAnimationFrame(reveal);\n  }\n\n  private syncHeaderVariant(concealUntilSynced = false) {\n    const header = this.headerEl;\n    if (!header) return;\n\n    this.syncHeaderDividerInset();\n    if (!this.headerContractResolved) {\n      this.cancelHeaderReveal();\n      header.setAttribute('data-shell-page-syncing', '');\n      return;\n    }\n\n    const next = this.effectiveVariant;\n    const renderedVariantIsStale = !header.classList.contains(this.variantClass(next));\n    if (!renderedVariantIsStale) this.syncRenderedHeaderGeometry(header);\n    if (renderedVariantIsStale && this.isScrollCompacted) {\n      // Preserve the expanded flow before asking BarTitle to become compact.\n      // This prevents the sentinel from moving back into view during the\n      // parent/child render handoff and keeps the variant transition stable.\n      this.setSpacerHeight(this.expandedHeaderHeight - this.compactHeaderHeight);\n      this.setStickyHeaderOffset(next);\n    }\n    if (concealUntilSynced) {\n      this.cancelHeaderReveal();\n      header.setAttribute('data-shell-page-syncing', '');\n    }\n    if (header.variant !== next) {\n      header.variant = next;\n      // The child update and its MutationObserver callback can land in\n      // different phases across engines. Poll the rendered class as the\n      // authoritative handoff before committing sticky/spacer geometry.\n      this.scheduleHeaderGeometrySync(header);\n    }\n    if (concealUntilSynced) {\n      this.revealHeaderWhenSynced(header, next);\n    }\n  }\n\n  private observeHeader(header: BarTitleElement | null) {\n    this.cancelHeaderReveal();\n    this.cancelHeaderGeometrySync();\n    this.headerResizeObserver?.disconnect();\n    this.headerResizeObserver = null;\n    this.headerMutationObserver?.disconnect();\n    this.headerMutationObserver = null;\n    this.headerEl?.style.removeProperty('--ds-bar-title-divider-inset');\n    this.headerEl = header;\n    this.expandedHeaderHeight = 0;\n    this.expandedGeometryFrozen = false;\n    this.setHeaderTravel(0);\n    this.setSpacerHeight(0);\n    this.stickyHeaderEl?.style.setProperty('--ds-shell-page-sticky-offset', '0px');\n\n    if (!header) return;\n    if (header.classList.contains(this.variantClass('expanded'))) {\n      this.captureExpandedHeaderGeometry(header);\n    }\n    if (typeof ResizeObserver !== 'undefined') {\n      this.headerResizeObserver = new ResizeObserver(() => {\n        this.syncRenderedHeaderGeometry(header);\n      });\n      this.headerResizeObserver.observe(header);\n    }\n    if (typeof MutationObserver !== 'undefined') {\n      this.headerMutationObserver = new MutationObserver(() => {\n        this.syncRenderedHeaderGeometry(header);\n      });\n      this.headerMutationObserver.observe(header, {\n        attributes: true,\n        attributeFilter: ['class'],\n        childList: true,\n        subtree: true,\n      });\n    }\n    this.syncHeaderVariant(true);\n    this.scheduleHeaderGeometrySync(header);\n  }\n\n  private handleHeaderSlotChange = (event: Event) => {\n    if (this.responsiveMode === 'mobile') return;\n    const slot = event.target as HTMLSlotElement;\n    const header = slot\n      .assignedElements()\n      .find(element => element.tagName.toLowerCase() === 'ds-bar-title');\n    this.observeHeader((header as BarTitleElement | undefined) ?? null);\n  };\n\n  render() {\n    const mobile = this.responsiveMode === 'mobile';\n    return (\n      <Host\n        role=\"main\"\n        style={this.contentInsetStyles}\n        class={{\n          'shell-page-host--inset-default': this.contentInset === 'default',\n          'shell-page-host--inset-none': this.contentInset === 'none',\n          'shell-page-host--block-start-inset-none': this.contentInsetBlockStart === 'none',\n          'shell-page-host--surface-primary': this.contentSurface === 'primary',\n          'shell-page-host--surface-secondary': this.contentSurface === 'secondary',\n          'shell-page-host--mobile': mobile,\n          [`shell-page-host--header-${this.effectiveVariant}`]: true,\n        }}\n      >\n        <div\n          ref={el => {\n            this.sentinelEl = el ?? null;\n          }}\n          class=\"shell-page__scroll-sentinel\"\n          aria-hidden=\"true\"\n        />\n        <div\n          ref={el => {\n            this.stickyHeaderEl = el ?? null;\n          }}\n          class=\"shell-page__sticky-header\"\n        >\n          <div\n            class=\"shell-page__desktop-header\"\n            hidden={mobile}\n            aria-hidden={mobile ? 'true' : undefined}\n            inert={mobile ? true : undefined}\n          >\n            <slot name=\"header\" onSlotchange={this.handleHeaderSlotChange} />\n          </div>\n          <div\n            class=\"shell-page__mobile-header\"\n            hidden={!mobile}\n            aria-hidden={!mobile ? 'true' : undefined}\n            inert={!mobile ? true : undefined}\n          >\n            <slot name=\"mobile-header\" />\n          </div>\n        </div>\n        <div\n          ref={el => {\n            this.spacerEl = el ?? null;\n          }}\n          class=\"shell-page__flow-spacer\"\n          aria-hidden=\"true\"\n        />\n        <div class=\"shell-page__content\">\n          <slot />\n        </div>\n      </Host>\n    );\n  }\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "src/wc/components/ShellPage/shell-page-responsive.ts",
      "content": "import type { ShellPageCapacity, ShellPageHeaderPresentation } from './shell-page-types';\nimport type { BarTitleVariant } from '../BarTitle/bar-title-types';\n\nexport function resolveShellPageHeaderVariant(\n  presentation: ShellPageHeaderPresentation,\n  capacity: ShellPageCapacity,\n  pageTopVisible: boolean\n): BarTitleVariant {\n  if (presentation !== 'auto') return presentation;\n  if (capacity === 'constrained') return 'constrained';\n  if (capacity === 'compact' || !pageTopVisible) return 'compact';\n  return 'expanded';\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "src/wc/components/ShellPage/shell-page-types.ts",
      "content": "import type { BarTitleVariant } from '../BarTitle/bar-title-types';\n\nexport type ShellPageHeaderPresentation = 'auto' | BarTitleVariant;\n\nexport type ShellPageContentInset = 'default' | 'none';\n\nexport type ShellPageContentSurface = 'primary' | 'secondary';\n\nexport type ShellPageCapacity = 'roomy' | 'compact' | 'constrained';\n",
      "type": "registry:ui"
    }
  ]
}
