{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "page-header",
  "title": "page-header",
  "description": "An app page header: muted icon slot, title and description, actions, and a tabs or toolbar row; sticky with a frosted glass state once the page scrolls under it.",
  "dependencies": [
    "class-variance-authority@^0.7"
  ],
  "registryDependencies": [
    "@dwellir/dwellir-theme",
    "@dwellir/utils"
  ],
  "files": [
    {
      "path": "components/ui/page-header.tsx",
      "content": "'use client';\n\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { type ComponentProps, type ReactNode, useEffect, useRef, useState } from 'react';\nimport { cn } from '@/lib/utils';\n\nconst pageHeaderVariants = cva(\n  'flex w-full min-w-0 flex-col transition-[background-color,border-color,box-shadow] duration-200 motion-reduce:transition-none',\n  {\n    variants: {\n      size: {\n        /* An app top bar, as in the dashboard's root panel. */\n        md: 'gap-2 py-3',\n        /* A page lead above the content, as on a status page. */\n        lg: 'gap-3 py-6',\n      },\n      sticky: {\n        true: 'sticky top-0 z-10',\n        false: '',\n      },\n      /* A hairline under the header at rest. A scrolled sticky header always takes one. */\n      bordered: {\n        true: 'border-b-(length:--border-hairline) border-border',\n        false: 'border-b-(length:--border-hairline) border-transparent',\n      },\n      /* The header's own side padding, for a bar that spans the page. Inside a padded container, keep none. */\n      inset: {\n        none: '',\n        page: 'px-4 sm:px-6',\n      },\n    },\n    defaultVariants: { size: 'md', sticky: false, bordered: false, inset: 'none' },\n  },\n);\n\nconst pageHeaderTitleVariants = cva('truncate font-display font-bold', {\n  variants: {\n    size: {\n      md: 'text-xl leading-tight',\n      lg: 'text-3xl leading-tight tracking-tight',\n    },\n  },\n  defaultVariants: { size: 'md' },\n});\n\nconst pageHeaderDescriptionVariants = cva('text-foreground', {\n  variants: {\n    size: {\n      md: 'truncate text-xs',\n      lg: 'line-clamp-2 text-sm',\n    },\n  },\n  defaultVariants: { size: 'md' },\n});\n\n/* The nearest ancestor that scrolls, as sticky sticks to it. Null means the viewport. */\nfunction scrollRoot(node: Element) {\n  let parent = node.parentElement;\n  while (parent) {\n    const overflow = getComputedStyle(parent).overflowY;\n    if (overflow === 'auto' || overflow === 'scroll' || overflow === 'overlay') return parent;\n    parent = parent.parentElement;\n  }\n  return null;\n}\n\n/*\n * A sentinel just before the header: once it scrolls above the top of its\n * scrollport, the sticky header counts as scrolled. The observer's root is\n * the scroll parent (an app's main column, a panel), not the viewport, so a\n * nested scroller frosts the header too. No scroll listener. Without\n * IntersectionObserver the header stays in its at-rest look.\n */\nfunction useScrolled(enabled: boolean) {\n  const sentinelRef = useRef<HTMLSpanElement>(null);\n  const [scrolled, setScrolled] = useState(false);\n  useEffect(() => {\n    const node = sentinelRef.current;\n    if (!enabled || !node || typeof IntersectionObserver === 'undefined') return;\n    const observer = new IntersectionObserver(\n      ([entry]) =>\n        setScrolled(\n          !entry.isIntersecting && entry.boundingClientRect.top < (entry.rootBounds?.top ?? 0),\n        ),\n      { root: scrollRoot(node), threshold: 0 },\n    );\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, [enabled]);\n  return { sentinelRef, scrolled };\n}\n\ntype PageHeaderProps = Omit<ComponentProps<'header'>, 'title'> &\n  VariantProps<typeof pageHeaderVariants> & {\n    /** The page's name. Renders as its heading (h1 by default). */\n    title: ReactNode;\n    /** A quiet line under the title. It truncates rather than push the actions off a phone. */\n    description?: ReactNode;\n    /** The page's mark, muted before the title, such as the sidebar row's icon. A slot, so the app picks the glyph. */\n    icon?: ReactNode;\n    /** Before the title group: a sidebar trigger, a back button. */\n    leading?: ReactNode;\n    /** Quiet, secondary readings beside the title, such as a network's regions. Hidden on phones. */\n    meta?: ReactNode;\n    /** The page's actions, at the end of the title row. The row can shrink; a control that must keep its width takes shrink-0. */\n    actions?: ReactNode;\n    /** The heading level of the title. An app page takes h1; a page that already has one takes h2. */\n    as?: 'h1' | 'h2';\n    /** Keep the heading for screen readers and show no title group, for a page that leads with its own heading. */\n    hideTitle?: boolean;\n    /** The heading's id, so the page region can name itself with aria-labelledby. */\n    titleId?: string;\n    /** A row under the title: page tabs or a toolbar. It sticks and frosts with the header. */\n    children?: ReactNode;\n  };\n\n/**\n * An app page's header: a muted leading icon, the title and a quiet\n * description, quiet readings beside them, and the page's actions, with a\n * tabs or toolbar row under it all. With sticky, it stays at the top of its\n * scroll container and takes the frosted glass look once the page scrolls\n * under it; at rest it is clear, so the page's own ground shows. The title\n * group shrinks and truncates on a phone; the actions never wrap. A bar that\n * spans the page takes inset=\"page\"; inside a padded container, none.\n */\nfunction PageHeader({\n  className,\n  size,\n  sticky = false,\n  bordered,\n  inset,\n  title,\n  description,\n  icon,\n  leading,\n  meta,\n  actions,\n  as: Heading = 'h1',\n  hideTitle = false,\n  titleId,\n  children,\n  ...props\n}: PageHeaderProps) {\n  const { sentinelRef, scrolled } = useScrolled(Boolean(sticky));\n  const heading = (\n    <Heading\n      data-slot=\"page-header-title\"\n      id={titleId}\n      className={hideTitle ? 'sr-only' : pageHeaderTitleVariants({ size })}\n    >\n      {title}\n    </Heading>\n  );\n  return (\n    <>\n      {sticky ? (\n        <span\n          ref={sentinelRef}\n          data-slot=\"page-header-sentinel\"\n          aria-hidden=\"true\"\n          className=\"-mb-px block h-px\"\n        />\n      ) : null}\n      <header\n        data-slot=\"page-header\"\n        data-size={size ?? 'md'}\n        data-scrolled={scrolled ? '' : undefined}\n        className={cn(\n          pageHeaderVariants({\n            size,\n            sticky: Boolean(sticky),\n            bordered: Boolean(bordered),\n            inset,\n          }),\n          /* Scrolled: frosted over the page, with a hairline and a small shadow. */\n          scrolled && 'glass border-border shadow-xs',\n          className,\n        )}\n        {...props}\n      >\n        <div data-slot=\"page-header-main\" className=\"flex min-w-0 items-center gap-3\">\n          {leading ? (\n            <div data-slot=\"page-header-leading\" className=\"flex shrink-0 items-center\">\n              {leading}\n            </div>\n          ) : null}\n          {hideTitle ? (\n            heading\n          ) : (\n            <>\n              {icon ? (\n                <span\n                  data-slot=\"page-header-icon\"\n                  aria-hidden=\"true\"\n                  className=\"flex shrink-0 items-center text-muted-foreground [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-6\"\n                >\n                  {icon}\n                </span>\n              ) : null}\n              <div data-slot=\"page-header-heading\" className=\"min-w-0 flex-1\">\n                {heading}\n                {description ? (\n                  <p\n                    data-slot=\"page-header-description\"\n                    className={pageHeaderDescriptionVariants({ size })}\n                  >\n                    {description}\n                  </p>\n                ) : null}\n              </div>\n            </>\n          )}\n          {meta ? (\n            <div\n              data-slot=\"page-header-meta\"\n              className=\"hidden min-w-0 shrink-0 items-center gap-2 sm:flex\"\n            >\n              {meta}\n            </div>\n          ) : null}\n          {actions ? (\n            /* The actions can shrink, so a wide control narrows instead of covering the title. */\n            <div\n              data-slot=\"page-header-actions\"\n              className=\"ml-auto flex min-w-0 items-center gap-2 *:min-w-0\"\n            >\n              {actions}\n            </div>\n          ) : null}\n        </div>\n        {children ? (\n          <div data-slot=\"page-header-row\" className=\"flex min-w-0 items-center gap-2\">\n            {children}\n          </div>\n        ) : null}\n      </header>\n    </>\n  );\n}\n\nexport { PageHeader, type PageHeaderProps, pageHeaderVariants };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}