{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "app-header",
  "title": "App Header",
  "dependencies": [
    "@base-ui/react",
    "react-remove-scroll"
  ],
  "registryDependencies": [
    "https://ui.hotfix.jobs/r/tokens.json",
    "https://ui.hotfix.jobs/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/blocks/app-header/index.tsx",
      "content": "\"use client\";\n\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport {\n  Children,\n  createContext,\n  isValidElement,\n  useContext,\n  useEffect,\n  useId,\n  useMemo,\n  useState,\n  useSyncExternalStore,\n} from \"react\";\n\nconst emptySubscribe = () => () => {};\nimport { createPortal } from \"react-dom\";\nimport { RemoveScroll } from \"react-remove-scroll\";\nimport type * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/** Complete site-header block with responsive mobile panel. */\n\n/* --------------------------- context --------------------------- */\n\ninterface AppHeaderContextValue {\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  panelId: string;\n  right: React.ReactNode;\n  navChildren: React.ReactNode;\n}\n\nconst AppHeaderContext = createContext<AppHeaderContextValue | null>(null);\n\nconst AppHeaderMobileRenderContext = createContext<boolean>(false);\n\nfunction useAppHeaderContext(): AppHeaderContextValue {\n  const ctx = useContext(AppHeaderContext);\n  if (!ctx) {\n    throw new Error(\"AppHeader subcomponents must be used inside <AppHeader>.\");\n  }\n  return ctx;\n}\n\nfunction extractSlots(children: React.ReactNode): {\n  brand: React.ReactNode;\n  nav: React.ReactNode;\n  right: React.ReactNode;\n  mobileTop: React.ReactNode;\n} {\n  let brand: React.ReactNode = null;\n  let nav: React.ReactNode = null;\n  let right: React.ReactNode = null;\n  let mobileTop: React.ReactNode = null;\n  Children.forEach(children, (child) => {\n    if (!isValidElement(child)) return;\n    const type = child.type as { displayName?: string };\n    if (type === AppHeaderBrand || type.displayName === \"AppHeaderBrand\") {\n      brand = child;\n    } else if (type === AppHeaderNav || type.displayName === \"AppHeaderNav\") {\n      nav = child;\n    } else if (type === AppHeaderRight || type.displayName === \"AppHeaderRight\") {\n      right = child;\n    } else if (\n      type === AppHeaderMobileTop ||\n      type.displayName === \"AppHeaderMobileTop\"\n    ) {\n      mobileTop = child;\n    }\n  });\n  return { brand, nav, right, mobileTop };\n}\n\n/* --------------------------- root --------------------------- */\n\nexport interface AppHeaderProps extends useRender.ComponentProps<\"header\"> {\n  /** Hairline bottom border. Default true. */\n  bordered?: boolean;\n  /** Pin the header to the top of its scroll container. Default false. */\n  sticky?: boolean;\n}\n\nexport function AppHeader({\n  className,\n  bordered = true,\n  sticky = false,\n  render,\n  children,\n  ...props\n}: AppHeaderProps): React.ReactElement {\n  const [open, setOpen] = useState(false);\n  const panelId = useId();\n\n  const { brand, nav, right, mobileTop } = useMemo(\n    () => extractSlots(children),\n    [children],\n  );\n  const navChildren = useMemo(() => {\n    if (!isValidElement(nav)) return null;\n    return (nav as React.ReactElement<{ children?: React.ReactNode }>).props\n      .children;\n  }, [nav]);\n\n  const ctx: AppHeaderContextValue = { open, setOpen, panelId, right, navChildren };\n\n  const defaultProps = {\n    className: cn(\n      \"bg-base\",\n      bordered && \"border-b border-hairline\",\n      sticky ? \"sticky top-0 z-50\" : \"relative\",\n      className,\n    ),\n    \"data-slot\": \"app-header\",\n    children: (\n      <>\n        <div className=\"flex w-full items-center gap-4 px-4 py-3.5 md:px-6 md:gap-6 md:py-4\">\n          {brand}\n          {nav}\n          <AppHeaderRightWithTrigger mobileTop={mobileTop}>{right}</AppHeaderRightWithTrigger>\n        </div>\n      </>\n    ),\n  };\n\n  return (\n    <AppHeaderContext.Provider value={ctx}>\n      {useRender({\n        defaultTagName: \"header\",\n        props: mergeProps<\"header\">(defaultProps, props),\n        render,\n      })}\n      <AppHeaderMobilePanel />\n    </AppHeaderContext.Provider>\n  );\n}\n\n/* --------------------------- brand --------------------------- */\n\nexport type AppHeaderBrandProps = useRender.ComponentProps<\"div\">;\n\nexport function AppHeaderBrand({\n  className,\n  render,\n  ...props\n}: AppHeaderBrandProps): React.ReactElement {\n  const defaultProps = {\n    className: cn(\n      \"flex shrink-0 items-center gap-2 text-regular font-medium text-ink\",\n      className,\n    ),\n    \"data-slot\": \"app-header-brand\",\n  };\n\n  return useRender({\n    defaultTagName: \"div\",\n    props: mergeProps<\"div\">(defaultProps, props),\n    render,\n  });\n}\nAppHeaderBrand.displayName = \"AppHeaderBrand\";\n\n/* --------------------------- nav --------------------------- */\n\nexport type AppHeaderNavProps = useRender.ComponentProps<\"nav\">;\n\nexport function AppHeaderNav({\n  className,\n  render,\n  children,\n  ...props\n}: AppHeaderNavProps): React.ReactElement {\n  const defaultProps = {\n    className: cn(\"hidden md:flex items-center gap-1\", className),\n    \"data-slot\": \"app-header-nav\",\n    children,\n  };\n\n  return useRender({\n    defaultTagName: \"nav\",\n    props: mergeProps<\"nav\">(defaultProps, props),\n    render,\n  });\n}\nAppHeaderNav.displayName = \"AppHeaderNav\";\n\nexport type AppHeaderNavItemProps = Omit<\n  useRender.ComponentProps<\"a\">,\n  \"prefix\"\n> & {\n  /** Marks this item as the current page. */\n  active?: boolean;\n  /** Leading node (typically an icon). */\n  prefix?: React.ReactNode;\n  /** Trailing node (badge, count, chevron). Pushed to the row's end. */\n  suffix?: React.ReactNode;\n};\n\nexport function AppHeaderNavItem({\n  className,\n  active,\n  prefix,\n  suffix,\n  render,\n  onClick,\n  children,\n  ...props\n}: AppHeaderNavItemProps): React.ReactElement {\n  const mobile = useContext(AppHeaderMobileRenderContext);\n  const headerCtx = useContext(AppHeaderContext);\n\n  const baseClasses = mobile\n    ? cn(\n        \"flex items-center gap-2.5 rounded-[var(--radius-8)] px-3 py-2.5 text-regular text-ink-muted transition-colors duration-[var(--duration-state)] ease-[var(--ease-standard)] hover:bg-layer-hover hover:text-ink [&_svg]:size-5\",\n        active && \"bg-layer-hover font-medium text-ink\",\n      )\n    : cn(\n        \"inline-flex items-center gap-1.5 rounded-[var(--radius-8)] px-3 py-1.5 text-small transition-colors duration-[var(--duration-state)] ease-[var(--ease-standard)] [&_svg]:size-4\",\n        active\n          ? \"text-ink font-medium\"\n          : \"text-ink-muted hover:bg-layer-hover hover:text-ink\",\n      );\n\n  const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {\n    onClick?.(e);\n    if (mobile && headerCtx && !e.defaultPrevented) headerCtx.setOpen(false);\n  };\n\n  const defaultProps = {\n    className: cn(baseClasses, className),\n    \"data-slot\": \"app-header-nav-item\",\n    \"data-active\": active ? \"\" : undefined,\n    \"aria-current\": active ? (\"page\" as const) : undefined,\n    onClick: handleClick,\n    children: (\n      <>\n        {prefix && (\n          <span className=\"shrink-0 [&_svg]:shrink-0\" data-slot=\"app-header-nav-item-prefix\">\n            {prefix}\n          </span>\n        )}\n        <span className=\"min-w-0 flex-1 truncate\">{children}</span>\n        {suffix && (\n          <span\n            className=\"ms-auto flex shrink-0 items-center [&_svg]:shrink-0\"\n            data-slot=\"app-header-nav-item-suffix\"\n          >\n            {suffix}\n          </span>\n        )}\n      </>\n    ),\n  };\n\n  return useRender({\n    defaultTagName: \"a\",\n    props: mergeProps<\"a\">(defaultProps, props),\n    render,\n  });\n}\nAppHeaderNavItem.displayName = \"AppHeaderNavItem\";\n\n/** Section rendered as a titled group inside the mobile panel; passes through on desktop. */\nexport interface AppHeaderNavSectionProps {\n  title: React.ReactNode;\n  children: React.ReactNode;\n}\n\nexport function AppHeaderNavSection({\n  title,\n  children,\n}: AppHeaderNavSectionProps): React.ReactElement {\n  const mobile = useContext(AppHeaderMobileRenderContext);\n\n  if (!mobile) return <>{children}</>;\n\n  return (\n    <div\n      data-slot=\"app-header-nav-section\"\n      className=\"flex flex-col gap-1\"\n    >\n      <div className=\"px-3 pb-1 pt-2 text-micro text-ink-tertiary\">\n        {title}\n      </div>\n      <div className=\"flex flex-col gap-0.5\">{children}</div>\n    </div>\n  );\n}\nAppHeaderNavSection.displayName = \"AppHeaderNavSection\";\n\n/* --------------------------- right actions --------------------------- */\n\nexport type AppHeaderRightProps = useRender.ComponentProps<\"div\">;\n\nexport function AppHeaderRight({\n  children,\n}: AppHeaderRightProps): React.ReactElement {\n  return <>{children}</>;\n}\nAppHeaderRight.displayName = \"AppHeaderRight\";\n\n/** Content pinned into the mobile top bar just before the hamburger\n *  trigger. Hidden on desktop and never rendered in the mobile panel\n *  footer. Meant for one-tap actions (e.g. notification bell) that\n *  should stay reachable without opening the panel. */\nexport function AppHeaderMobileTop({\n  children,\n}: {\n  children?: React.ReactNode;\n}): React.ReactElement {\n  return <>{children}</>;\n}\nAppHeaderMobileTop.displayName = \"AppHeaderMobileTop\";\n\nfunction AppHeaderRightWithTrigger({\n  children,\n  mobileTop,\n}: {\n  children: React.ReactNode;\n  mobileTop?: React.ReactNode;\n}): React.ReactElement {\n  const { open, setOpen, panelId, navChildren } = useAppHeaderContext();\n  const hasNav = Children.count(navChildren) > 0;\n  return (\n    <div className=\"ms-auto flex items-center gap-2\" data-slot=\"app-header-right\">\n      <span className={hasNav ? \"hidden md:contents\" : \"contents\"}>{children}</span>\n      {hasNav && mobileTop && (\n        <span className=\"md:hidden contents\" data-slot=\"app-header-mobile-top\">\n          {mobileTop}\n        </span>\n      )}\n      {hasNav && (\n        <button\n          type=\"button\"\n          data-slot=\"app-header-mobile-trigger\"\n          aria-label={open ? \"Close menu\" : \"Open menu\"}\n          aria-expanded={open}\n          aria-controls={panelId}\n          onClick={() => setOpen(!open)}\n          className=\"inline-flex md:hidden size-8 items-center justify-center rounded-[var(--radius-8)] text-ink transition-colors duration-[var(--duration-state)] ease-[var(--ease-standard)] hover:bg-layer-hover\"\n        >\n          <MorphingMenuIcon open={open} />\n        </button>\n      )}\n    </div>\n  );\n}\n\nfunction groupMobileNavChildren(children: React.ReactNode): React.ReactNode {\n  const output: React.ReactNode[] = [];\n  let bareBuffer: React.ReactNode[] = [];\n  let bareKey = 0;\n\n  const flushBare = () => {\n    if (bareBuffer.length === 0) return;\n    output.push(\n      <div key={`bare-${bareKey++}`} className=\"flex flex-col gap-1\">\n        {bareBuffer}\n      </div>,\n    );\n    bareBuffer = [];\n  };\n\n  Children.forEach(children, (child) => {\n    if (!isValidElement(child)) return;\n    const type = child.type as { displayName?: string };\n    const isSection =\n      type === AppHeaderNavSection ||\n      type.displayName === \"AppHeaderNavSection\";\n\n    if (isSection) {\n      flushBare();\n      output.push(child);\n    } else {\n      bareBuffer.push(child);\n    }\n  });\n  flushBare();\n\n  return output;\n}\n\n/* --------------------------- internal: morphing icon --------------------------- */\n\nfunction MorphingMenuIcon({ open }: { open: boolean }): React.ReactElement {\n  const bar =\n    \"absolute left-1/2 top-1/2 h-px w-[15px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-current transition-transform duration-[var(--duration-state)] ease-[var(--ease-standard)]\";\n\n  return (\n    <span\n      aria-hidden=\"true\"\n      data-slot=\"app-header-menu-icon\"\n      data-state={open ? \"open\" : \"closed\"}\n      className=\"relative inline-flex h-[18px] w-[18px] items-center justify-center\"\n    >\n      <span\n        className={cn(\n          bar,\n          open\n            ? \"[transform:translateY(0px)_rotate(45deg)]\"\n            : \"[transform:translateY(-4px)_rotate(0deg)]\",\n        )}\n      />\n      <span\n        className={cn(\n          bar,\n          open\n            ? \"[transform:translateY(0px)_rotate(-45deg)]\"\n            : \"[transform:translateY(4px)_rotate(0deg)]\",\n        )}\n      />\n    </span>\n  );\n}\n\n/* --------------------------- internal: mobile panel --------------------------- */\n\nfunction AppHeaderMobilePanel(): React.ReactPortal | null {\n  const { open, setOpen, panelId, navChildren, right } = useAppHeaderContext();\n  const mounted = useSyncExternalStore(emptySubscribe, () => true, () => false);\n\n  useEffect(() => {\n    if (!open) return;\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") setOpen(false);\n    };\n    document.addEventListener(\"keydown\", onKey);\n    return () => document.removeEventListener(\"keydown\", onKey);\n  }, [open, setOpen]);\n\n  if (!mounted || !open) return null;\n\n  return createPortal(\n    <RemoveScroll forwardProps>\n      <div\n        role=\"dialog\"\n        aria-modal=\"true\"\n        id={panelId}\n        data-slot=\"app-header-mobile-panel\"\n        className=\"fixed inset-x-0 bottom-0 top-[var(--header-height,64px)] z-40 flex flex-col bg-base md:hidden [animation:patch-app-header-panel-in_var(--duration-overlay)_var(--ease-standard)]\"\n      >\n        <AppHeaderMobileRenderContext.Provider value={true}>\n          <nav className=\"flex flex-1 flex-col gap-4 overflow-y-auto p-4 pt-6\">\n            {groupMobileNavChildren(navChildren)}\n          </nav>\n          {right && Children.count(right) > 0 && (\n            <div\n              data-slot=\"app-header-mobile-footer\"\n              className=\"flex items-center gap-2 border-t border-hairline p-4 [&>button]:min-w-0 [&>button]:flex-1 [&>a]:min-w-0 [&>a]:flex-1\"\n            >\n              {right}\n            </div>\n          )}\n        </AppHeaderMobileRenderContext.Provider>\n      </div>\n    </RemoveScroll>,\n    document.body,\n  );\n}\n",
      "type": "registry:component",
      "target": "components/blocks/app-header/index.tsx"
    }
  ],
  "type": "registry:block"
}