{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "page-header",
  "type": "registry:block",
  "title": "Page header",
  "description": "A project page header that folds into a compact bar as you scroll, with tabs whose counts roll.",
  "categories": [
    "blocks"
  ],
  "dependencies": [
    "@radix-ui/react-dropdown-menu",
    "@radix-ui/react-tabs",
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/animated-counter.json",
    "https://irisui.dev/r/avatar.json",
    "https://irisui.dev/r/avatar-group.json",
    "https://irisui.dev/r/badge.json",
    "https://irisui.dev/r/button.json",
    "https://irisui.dev/r/progress.json",
    "https://irisui.dev/r/motion-tokens.json",
    "https://irisui.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "src/registry/blocks/page-header/page-header.tsx",
      "content": "\"use client\";\n\nimport { Fragment, useEffect, useId, useRef, useState } from \"react\";\nimport type { FocusEvent, ReactNode, UIEvent } from \"react\";\nimport * as DropdownPrimitive from \"@radix-ui/react-dropdown-menu\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport { AnimatePresence, LayoutGroup, motion, useReducedMotion, type Transition, type Variants } from \"motion/react\";\nimport {\n  Archive,\n  ArchiveRestore,\n  Bell,\n  BellOff,\n  BellRing,\n  CalendarDays,\n  Check,\n  ChevronRight,\n  Circle,\n  CircleAlert,\n  CircleCheck,\n  CircleDashed,\n  Ellipsis,\n  FileSpreadsheet,\n  FileText,\n  Film,\n  Link2,\n  Megaphone,\n  PenTool,\n  Plus,\n  Presentation,\n  type LucideIcon,\n} from \"lucide-react\";\nimport { AnimatedCounter } from \"../../animated-counter/animated-counter\";\nimport { Avatar } from \"../../avatar/avatar\";\nimport { AvatarGroup } from \"../../avatar-group/avatar-group\";\nimport { Badge } from \"../../badge/badge\";\nimport { Button } from \"../../button/button\";\nimport { Progress } from \"../../progress/progress\";\nimport { motionTokens } from \"../../lib/motion-tokens\";\nimport { cn } from \"../../lib/utils\";\n\n/** The header condenses past CONDENSE_AT and only opens again near the top, so it never flickers at the threshold. */\nconst CONDENSE_AT = 16;\nconst EXPAND_AT = 4;\n/** Secondary actions fold into the overflow menu when the breadcrumbs would get less room than this, and return with a little slack. */\nconst LEAD_MIN = 240;\nconst LEAD_RETURN = 272;\n/** With the nine open tasks below, the project starts at 11 of 20 tasks done. */\nconst DONE_AT_START = 11;\nconst PROJECT_URL = \"https://north-ridge.example/projects/public-launch\";\n\n/** Sample content: an mpath project (Plain terms) run by the North ridge team. Tomasz is the person viewing the page. */\ntype PersonId = \"katerina\" | \"stelios\" | \"nefeli\" | \"clara\" | \"tomasz\";\nconst people: Record<PersonId, { name: string; src: string }> = {\n  katerina: { name: \"Katerina Vlachou\", src: \"\" },\n  stelios: { name: \"Stelios Antoniou\", src: \"\" },\n  nefeli: { name: \"Nefeli Kontou\", src: \"\" },\n  clara: { name: \"Clara Weiss\", src: \"\" },\n  tomasz: { name: \"Tomasz Wrona\", src: \"\" },\n};\nconst members = ([\"katerina\", \"stelios\", \"nefeli\", \"clara\", \"tomasz\"] as const).map((key) => people[key]);\n\ntype Section = \"overview\" | \"issues\" | \"updates\" | \"files\";\nconst sections: { value: Section; label: string }[] = [\n  { value: \"overview\", label: \"Overview\" },\n  { value: \"issues\", label: \"Tasks\" },\n  { value: \"updates\", label: \"Updates\" },\n  { value: \"files\", label: \"Documents\" },\n];\nconst order = (section: Section) => sections.findIndex((item) => item.value === section);\n\ntype Issue = { id: string; title: string; owner: PersonId; label: string; fresh?: boolean };\nconst startingIssues: Issue[] = [\n  { id: \"PL-38\", title: \"Publish legal information pages\", owner: \"nefeli\", label: \"Legal\" },\n  { id: \"PL-36\", title: \"Draft pricing page copy\", owner: \"katerina\", label: \"Copy\" },\n  { id: \"PL-35\", title: \"Review cookie notice\", owner: \"clara\", label: \"Legal\" },\n  { id: \"PL-33\", title: \"Send the second wave of beta invites\", owner: \"stelios\", label: \"Beta\" },\n  { id: \"PL-31\", title: \"Invite link stops working after one use\", owner: \"stelios\", label: \"Bug\" },\n  { id: \"PL-29\", title: \"Add the contracting entity to the footer\", owner: \"nefeli\", label: \"Legal\" },\n  { id: \"PL-27\", title: \"Outline the launch notes\", owner: \"katerina\", label: \"Copy\" },\n  { id: \"PL-24\", title: \"Check the sign-up flow on a phone\", owner: \"tomasz\", label: \"Design\" },\n  { id: \"PL-22\", title: \"Pricing table wraps awkwardly at large text sizes\", owner: \"tomasz\", label: \"Bug\" },\n];\nconst draftIssues = [\n  \"Proofread the terms page\",\n  \"Link the launch brief from the dashboard\",\n  \"Add alt text to the pricing page images\",\n  \"Book the go/no-go meeting\",\n];\n\ntype Update = {\n  id: string;\n  author: PersonId;\n  date: string;\n  tone: \"success\" | \"warning\";\n  status: string;\n  body: string;\n  fresh?: boolean;\n};\nconst startingUpdates: Update[] = [\n  {\n    id: \"u4\",\n    author: \"katerina\",\n    date: \"16 Oct\",\n    tone: \"success\",\n    status: \"On track\",\n    body: \"Legal pages are live in staging. The pricing page copy is in review, and the second wave of beta invites goes out on Monday.\",\n  },\n  {\n    id: \"u3\",\n    author: \"clara\",\n    date: \"9 Oct\",\n    tone: \"success\",\n    status: \"On track\",\n    body: \"The first beta wave is in. The cookie notice is the last legal item, and the contracting entity page is drafted.\",\n  },\n  {\n    id: \"u2\",\n    author: \"stelios\",\n    date: \"2 Oct\",\n    tone: \"warning\",\n    status: \"At risk\",\n    body: \"The invite link stops working after one use, so some testers could not get in. A fix is in review and the second wave waits for it.\",\n  },\n  {\n    id: \"u1\",\n    author: \"katerina\",\n    date: \"25 Sep\",\n    tone: \"success\",\n    status: \"On track\",\n    body: \"Kickoff. Scope is legal pages, beta invites, the pricing page and launch notes, with a go/no-go on 12 Nov.\",\n  },\n];\nconst draftUpdates = [\n  \"The pricing page is in staging. If the copy review holds through Friday, it goes live with the launch notes.\",\n  \"Launch notes are drafted and with the team for review. No change to the 12 Nov go/no-go.\",\n];\n\nconst files: { name: string; kind: string; size: string; owner: PersonId; date: string; icon: LucideIcon }[] = [\n  { name: \"Public launch brief\", kind: \"Plan\", size: \"14 KB\", owner: \"katerina\", date: \"20 Oct\", icon: FileText },\n  { name: \"Pricing page mockups\", kind: \"Design file\", size: \"9.2 MB\", owner: \"nefeli\", date: \"18 Oct\", icon: PenTool },\n  { name: \"Contracting entity\", kind: \"Article\", size: \"6 KB\", owner: \"nefeli\", date: \"16 Oct\", icon: FileText },\n  {\n    name: \"Beta feedback, wave one\",\n    kind: \"Spreadsheet\",\n    size: \"640 KB\",\n    owner: \"clara\",\n    date: \"13 Oct\",\n    icon: FileSpreadsheet,\n  },\n  { name: \"Sign-up flow walkthrough\", kind: \"Video\", size: \"46 MB\", owner: \"tomasz\", date: \"11 Oct\", icon: Film },\n  { name: \"Go/no-go agenda\", kind: \"Slides\", size: \"4.8 MB\", owner: \"katerina\", date: \"9 Oct\", icon: Presentation },\n  { name: \"Cookie notice draft\", kind: \"Document\", size: \"54 KB\", owner: \"clara\", date: \"6 Oct\", icon: FileText },\n  { name: \"Launch notes outline\", kind: \"Document\", size: \"22 KB\", owner: \"katerina\", date: \"2 Oct\", icon: FileText },\n];\n\nconst milestoneIcons = { done: CircleCheck, active: CircleDashed, planned: Circle };\nconst milestones: { name: string; note: string; state: keyof typeof milestoneIcons }[] = [\n  { name: \"Beta invites\", note: \"Reached 2 Oct\", state: \"done\" },\n  { name: \"Pricing page\", note: \"In progress, due 4 Nov\", state: \"active\" },\n  { name: \"Launch notes\", note: \"Planned for 12 Nov\", state: \"planned\" },\n];\nconst activity: { who: PersonId; text: string; time: string }[] = [\n  { who: \"clara\", text: \"moved PL-35 to review\", time: \"1h ago\" },\n  { who: \"stelios\", text: \"merged the invite link fix\", time: \"3h ago\" },\n  { who: \"nefeli\", text: \"shared Pricing page mockups\", time: \"Yesterday\" },\n  { who: \"tomasz\", text: \"closed PL-19, a broken link on the terms page\", time: \"Yesterday\" },\n  { who: \"katerina\", text: \"set the go/no-go to 12 Nov\", time: \"17 Oct\" },\n];\n\ntype Notice = { key: number; text: string; tone: \"success\" | \"error\" | \"neutral\" };\ntype MenuAction = {\n  key: string;\n  label: string;\n  icon: ReactNode;\n  onSelect: () => void;\n  disabled?: boolean;\n  separatorBefore?: boolean;\n};\n\nconst still: Transition = { duration: 0 };\nconst quick: Transition = { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] };\nconst blur = (px: number) => `blur(${px}px)`;\nconst icon = { size: 16, strokeWidth: 1.75, \"aria-hidden\": true } as const;\n\n/* Breakpoints follow the frame width (the `page-header` container), not the viewport: below 560 px the gutters\n * tighten and secondary columns drop, below 460 px the create button shows only its icon. */\nconst srOnly = \"absolute h-px w-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]\";\nconst note = \"text-xs/[inherit] whitespace-nowrap text-ink-3 tabular-nums\";\nconst sectionTitle = \"m-0 mb-2 text-sm/[inherit] font-medium\";\nconst list = \"m-0 list-none p-0\";\n/** List rows split by a hairline; the last row drops it. */\nconst listRow = \"border-b border-line-soft last:border-b-0\";\n/** Issue and update rows close their own height, so the overflow only clips vertically. */\nconst motionRow = \"overflow-x-visible overflow-y-clip border-b border-line-soft last:border-b-transparent\";\n/** A new row arrives lit and settles to the page, so it is easy to find without a lasting marker. */\nconst freshGlow =\n  \"before:absolute before:inset-x-0 before:inset-y-[3px] before:-z-1 before:animate-page-header-arrived before:rounded-[10px] before:bg-[color-mix(in_oklab,var(--ink)_6%,transparent)] before:content-['']\";\nconst crumbItem =\n  \"inline-flex flex-none items-center gap-1.5 whitespace-nowrap text-ink-3 [&>svg]:flex-none [&>svg]:text-line-strong\";\nconst crumb =\n  \"m-0 cursor-pointer rounded-button border-0 bg-transparent px-0.5 py-1 text-ink-3 transition-[color] duration-fast ease-standard hover:text-ink motion-reduce:transition-none\";\nconst menuRadius = \"rounded-[calc(var(--radius-panel)-6px)]\";\n\n/** Panels slide a few pixels in the direction of the tab that was chosen. */\nconst panelSlide: Variants = {\n  enter: (direction: number) => ({ opacity: 0, x: direction * 12 }),\n  center: {\n    opacity: 1,\n    x: 0,\n    transition: {\n      x: motionTokens.spring.smooth,\n      opacity: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] },\n    },\n  },\n  exit: (direction: number) => ({\n    opacity: 0,\n    x: direction * -8,\n    transition: { duration: motionTokens.duration.instant, ease: [...motionTokens.ease.standard] },\n  }),\n};\nconst panelFade: Variants = {\n  enter: { opacity: 0, x: 0 },\n  center: { opacity: 1, x: 0, transition: { duration: motionTokens.duration.instant } },\n  exit: { opacity: 0, x: 0, transition: { duration: motionTokens.duration.instant } },\n};\n\n/** Rows open and close their own height, so the list closes the gap instead of jumping. */\nfunction rowMotion(reduce: boolean) {\n  return reduce\n    ? {\n        initial: { opacity: 0 },\n        animate: { opacity: 1 },\n        exit: { opacity: 0 },\n        transition: { duration: motionTokens.duration.instant },\n      }\n    : {\n        initial: { opacity: 0, height: 0 },\n        animate: { opacity: 1, height: \"auto\" },\n        exit: { opacity: 0, height: 0 },\n        transition: {\n          height: motionTokens.spring.smooth,\n          opacity: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.standard] },\n        },\n      };\n}\n\nfunction StatusDot() {\n  return <span className=\"size-1.5 rounded-full bg-current\" />;\n}\n\n/** The overflow button never scales: it anchors the menu. A shared highlight glides between items under the pointer. */\nfunction OverflowMenu({ actions, reduce }: { actions: MenuAction[]; reduce: boolean }) {\n  const [highlight, setHighlight] = useState<{ top: number; height: number; glide: boolean } | null>(null);\n  const pointer = useRef(false);\n  const clearTimer = useRef(0);\n  useEffect(() => () => window.clearTimeout(clearTimer.current), []);\n  function onFocus(event: FocusEvent<HTMLDivElement>) {\n    const item = event.target instanceof HTMLElement ? event.target.closest<HTMLElement>('[role=\"menuitem\"]') : null;\n    window.clearTimeout(clearTimer.current);\n    if (!item) {\n      clearTimer.current = window.setTimeout(() => setHighlight(null), pointer.current ? 70 : 0);\n      return;\n    }\n    const glide = pointer.current;\n    setHighlight((current) => ({ top: item.offsetTop, height: item.offsetHeight, glide: glide && current !== null }));\n  }\n  return (\n    <DropdownPrimitive.Root\n      onOpenChange={(open) => {\n        if (open) {\n          window.clearTimeout(clearTimer.current);\n          setHighlight(null);\n        }\n      }}\n    >\n      <DropdownPrimitive.Trigger asChild>\n        <Button variant=\"secondary\" size=\"sm\" className=\"w-control-sm px-0\" aria-label=\"More actions\">\n          <Ellipsis size={17} strokeWidth={1.75} aria-hidden=\"true\" />\n        </Button>\n      </DropdownPrimitive.Trigger>\n      <DropdownPrimitive.Portal>\n        {/* The menu grows from the overflow button. Transitions, not keyframes, so a quick reopen reverses from where it is. */}\n        <DropdownPrimitive.Content\n          className={cn(\n            \"relative z-60 min-w-[13rem] origin-(--radix-dropdown-menu-content-transform-origin) rounded-panel border border-line bg-surface p-[5px] font-body text-sm/[inherit] tracking-body text-ink shadow-floating [--menu-y:-5px] [transition:opacity_var(--duration-fast)_var(--ease-enter),transform_var(--duration-spring)_var(--ease-spring)] data-[side=top]:[--menu-y:5px]\",\n            \"data-[state=open]:starting:[transform:translateY(var(--menu-y))_scale(.97)] data-[state=open]:starting:opacity-0\",\n            \"data-[state=closed]:pointer-events-none data-[state=closed]:animate-page-header-menu-exit data-[state=closed]:[transform:translateY(calc(var(--menu-y)*.5))_scale(.985)] data-[state=closed]:opacity-0 data-[state=closed]:[transition:opacity_var(--duration-instant)_var(--ease-standard),transform_var(--duration-instant)_var(--ease-standard)]\",\n            \"motion-reduce:[transform:none] motion-reduce:[transition:opacity_var(--duration-instant)_linear] motion-reduce:data-[state=closed]:[transform:none] motion-reduce:data-[state=closed]:[transition:opacity_var(--duration-instant)_linear]\",\n          )}\n          align=\"end\"\n          sideOffset={6}\n          collisionPadding={12}\n          loop\n          onFocus={onFocus}\n          onPointerMoveCapture={() => {\n            pointer.current = true;\n          }}\n          onKeyDownCapture={() => {\n            pointer.current = false;\n          }}\n        >\n          <motion.span\n            className={cn(\"pointer-events-none absolute top-0 right-[5px] left-[5px] bg-hover opacity-0\", menuRadius)}\n            aria-hidden=\"true\"\n            initial={false}\n            animate={highlight ? { y: highlight.top, height: highlight.height, opacity: 1 } : { opacity: 0 }}\n            transition={{\n              default: highlight?.glide && !reduce ? motionTokens.spring.snappy : still,\n              opacity: { duration: reduce ? 0 : 0.08 },\n            }}\n          />\n          {actions.map((action) => (\n            <Fragment key={action.key}>\n              {action.separatorBefore && <DropdownPrimitive.Separator className=\"-mx-[5px] my-1 h-px bg-line-soft\" />}\n              <DropdownPrimitive.Item\n                className={cn(\n                  \"relative flex min-h-9 cursor-pointer items-center gap-2.5 px-[11px] outline-none select-none data-disabled:cursor-default data-disabled:opacity-45 [&>svg]:flex-none [&>svg]:text-ink-2\",\n                  menuRadius,\n                )}\n                disabled={action.disabled}\n                onSelect={action.onSelect}\n              >\n                {action.icon}\n                {action.label}\n              </DropdownPrimitive.Item>\n            </Fragment>\n          ))}\n        </DropdownPrimitive.Content>\n      </DropdownPrimitive.Portal>\n    </DropdownPrimitive.Root>\n  );\n}\n\nfunction OverviewPanel({ done, open }: { done: number; open: number }) {\n  const total = done + open;\n  return (\n    <div className=\"grid gap-9\">\n      <div className=\"max-w-[480px]\">\n        <Progress value={done} max={total} label={`${done} of ${total} tasks done`} showValue />\n      </div>\n      <div>\n        <h3 className={sectionTitle}>Milestones</h3>\n        <ol className={list}>\n          {milestones.map((item) => {\n            const Icon = milestoneIcons[item.state];\n            return (\n              <li\n                key={item.name}\n                className={cn(\n                  listRow,\n                  \"grid min-h-12 grid-cols-[16px_minmax(0,1fr)_auto] items-center gap-x-3 gap-y-0.5 @max-[559px]/page-header:grid-cols-[16px_minmax(0,1fr)] @max-[559px]/page-header:py-2.5\",\n                )}\n                data-state={item.state}\n              >\n                <Icon\n                  className={\n                    item.state === \"done\" ? \"text-green\" : item.state === \"active\" ? \"text-accent\" : \"text-ink-3\"\n                  }\n                  size={16}\n                  strokeWidth={1.75}\n                  aria-hidden=\"true\"\n                />\n                <span>{item.name}</span>\n                <span className={cn(note, \"@max-[559px]/page-header:col-start-2\")}>{item.note}</span>\n              </li>\n            );\n          })}\n        </ol>\n      </div>\n      <div>\n        <h3 className={sectionTitle}>Recent activity</h3>\n        <ul className={list}>\n          {activity.map((item) => (\n            <li\n              key={`${item.who}-${item.text}`}\n              className={cn(\n                listRow,\n                \"grid min-h-[52px] grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 @max-[559px]/page-header:grid-cols-[auto_minmax(0,1fr)] @max-[559px]/page-header:py-2.5\",\n              )}\n            >\n              <Avatar name={people[item.who].name} src={people[item.who].src} size=\"sm\" />\n              <p className=\"m-0 text-ink-2\">\n                <strong className=\"font-medium text-ink\">{people[item.who].name}</strong> {item.text}\n              </p>\n              <span className={cn(note, \"@max-[559px]/page-header:col-start-2 @max-[559px]/page-header:-mt-2\")}>\n                {item.time}\n              </span>\n            </li>\n          ))}\n        </ul>\n      </div>\n    </div>\n  );\n}\n\nfunction IssuesPanel({\n  issues,\n  closing,\n  reduce,\n  onComplete,\n  onReset,\n  register,\n}: {\n  issues: Issue[];\n  closing: string[];\n  reduce: boolean;\n  onComplete: (issue: Issue) => void;\n  onReset: () => void;\n  register: (id: string, node: HTMLButtonElement | null) => void;\n}) {\n  const row = rowMotion(reduce);\n  return (\n    <>\n      <ul className={list} aria-label=\"Open tasks\">\n        <AnimatePresence initial={false}>\n          {issues.map((issue) => {\n            const isClosing = closing.includes(issue.id);\n            const owner = people[issue.owner];\n            return (\n              <motion.li\n                key={issue.id}\n                className={motionRow}\n                data-fresh={issue.fresh || undefined}\n                data-closing={isClosing || undefined}\n                {...row}\n              >\n                <div\n                  className={cn(\n                    \"relative isolate -mx-2.5 grid min-h-[52px] grid-cols-[32px_minmax(0,1fr)_auto_auto] items-center gap-3 px-2.5 py-1 @max-[559px]/page-header:grid-cols-[32px_minmax(0,1fr)_auto] @max-[559px]/page-header:items-start @max-[559px]/page-header:py-2.5\",\n                    issue.fresh && freshGlow,\n                  )}\n                >\n                  <button\n                    ref={(node) => register(issue.id, node)}\n                    type=\"button\"\n                    className={cn(\n                      \"relative -ml-[7px] grid size-8 cursor-pointer place-items-center rounded-button border-0 bg-transparent p-0 text-ink-3 transition-[color,background-color] duration-fast ease-standard hover:bg-[color-mix(in_oklab,var(--ink)_5%,transparent)] hover:text-ink motion-reduce:transition-none @max-[559px]/page-header:-mt-1.5\",\n                      isClosing && \"cursor-default text-green hover:bg-transparent hover:text-green\",\n                    )}\n                    aria-label={`Mark ${issue.id} done`}\n                    aria-disabled={isClosing || undefined}\n                    onClick={() => onComplete(issue)}\n                  >\n                    <AnimatePresence initial={false} mode=\"popLayout\">\n                      <motion.span\n                        key={isClosing ? \"done\" : \"open\"}\n                        className=\"grid place-items-center\"\n                        initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.5 }}\n                        animate={{ opacity: 1, scale: 1 }}\n                        exit={{ opacity: 0, scale: reduce ? 1 : 0.5, transition: quick }}\n                        transition={reduce ? still : motionTokens.spring.snappy}\n                      >\n                        {isClosing ? (\n                          <CircleCheck size={18} strokeWidth={1.75} aria-hidden=\"true\" />\n                        ) : (\n                          <Circle size={18} strokeWidth={1.75} aria-hidden=\"true\" />\n                        )}\n                      </motion.span>\n                    </AnimatePresence>\n                  </button>\n                  <span className=\"flex min-w-0 items-baseline gap-3 @max-[559px]/page-header:grid @max-[559px]/page-header:gap-0.5\">\n                    <span className=\"min-w-[60px] flex-none text-xs/[inherit] text-ink-3\">{issue.id}</span>\n                    <span\n                      className={cn(\n                        \"min-w-0 overflow-hidden text-ellipsis whitespace-nowrap line-through decoration-transparent transition-[color,text-decoration-color] duration-standard ease-standard motion-reduce:transition-none @max-[559px]/page-header:overflow-visible @max-[559px]/page-header:whitespace-normal\",\n                        isClosing && \"text-ink-3 decoration-current\",\n                      )}\n                    >\n                      {issue.title}\n                    </span>\n                  </span>\n                  <span className=\"text-xs/[inherit] text-ink-3 @max-[559px]/page-header:hidden\">{issue.label}</span>\n                  <Avatar name={owner.name} src={owner.src} size=\"sm\" />\n                </div>\n              </motion.li>\n            );\n          })}\n        </AnimatePresence>\n      </ul>\n      {issues.length === 0 && (\n        <motion.div\n          className=\"grid justify-items-center gap-1 px-4 py-[72px] text-center\"\n          initial={{ opacity: 0, y: reduce ? 0 : 6 }}\n          animate={{ opacity: 1, y: 0 }}\n          transition={reduce ? { duration: motionTokens.duration.instant } : motionTokens.spring.smooth}\n        >\n          <CircleCheck className=\"mb-2 text-green\" size={24} strokeWidth={1.75} aria-hidden=\"true\" />\n          <p className=\"m-0 text-base/[inherit] font-medium\">No open tasks</p>\n          <span className=\"text-ink-3\">Everything in this project is done.</span>\n          <Button className=\"mt-3.5\" variant=\"secondary\" size=\"sm\" onClick={onReset}>\n            Restore sample tasks\n          </Button>\n        </motion.div>\n      )}\n    </>\n  );\n}\n\nfunction UpdatesPanel({ updates, reduce }: { updates: Update[]; reduce: boolean }) {\n  const row = rowMotion(reduce);\n  return (\n    <ol className={list} aria-label=\"Project updates\">\n      <AnimatePresence initial={false}>\n        {updates.map((update) => {\n          const author = people[update.author];\n          return (\n            <motion.li key={update.id} className={motionRow} data-fresh={update.fresh || undefined} {...row}>\n              <article\n                className={cn(\n                  \"relative isolate -mx-2.5 grid grid-cols-[auto_minmax(0,1fr)] gap-3.5 px-2.5 py-[18px]\",\n                  update.fresh && freshGlow,\n                )}\n              >\n                <Avatar name={author.name} src={author.src} size=\"md\" />\n                <div>\n                  <div className=\"flex min-h-[22px] flex-wrap items-center gap-x-2.5 gap-y-1\">\n                    <span className=\"font-medium\">{author.name}</span>\n                    <span className={note}>{update.date}</span>\n                    <Badge size=\"sm\" tone={update.tone} icon={<StatusDot />}>\n                      {update.status}\n                    </Badge>\n                  </div>\n                  <p className=\"mx-0 mt-1.5 mb-0 max-w-[64ch] text-ink-2 text-pretty\">{update.body}</p>\n                </div>\n              </article>\n            </motion.li>\n          );\n        })}\n      </AnimatePresence>\n    </ol>\n  );\n}\n\nfunction FilesPanel() {\n  return (\n    <ul className={list} aria-label=\"Project documents\">\n      {files.map((file) => {\n        const Icon = file.icon;\n        const owner = people[file.owner];\n        return (\n          <li\n            key={file.name}\n            className={cn(\n              listRow,\n              \"grid min-h-14 grid-cols-[16px_minmax(0,1fr)_auto_52px] items-center gap-3.5 @max-[559px]/page-header:grid-cols-[16px_minmax(0,1fr)_auto]\",\n            )}\n          >\n            <Icon className=\"text-ink-2\" size={16} strokeWidth={1.75} aria-hidden=\"true\" />\n            <span className=\"grid min-w-0\">\n              <span className=\"overflow-hidden text-ellipsis whitespace-nowrap\">{file.name}</span>\n              <span className={note}>\n                {file.kind}, {file.size}\n              </span>\n            </span>\n            <Avatar name={owner.name} src={owner.src} size=\"sm\" />\n            <span className=\"text-right text-xs/[inherit] whitespace-nowrap text-ink-3 @max-[559px]/page-header:hidden\">\n              {file.date}\n            </span>\n          </li>\n        );\n      })}\n    </ul>\n  );\n}\n\nexport function PageHeader() {\n  const id = useId();\n  const reduce = useReducedMotion() ?? false;\n  const bar = useRef<HTMLDivElement>(null);\n  const secondary = useRef<HTMLDivElement>(null);\n  const trailing = useRef<HTMLDivElement>(null);\n  const scroller = useRef<HTMLDivElement>(null);\n  const tabList = useRef<HTMLDivElement>(null);\n  const titleRef = useRef<HTMLHeadingElement>(null);\n  const toggles = useRef(new Map<string, HTMLButtonElement>());\n  const timers = useRef(new Set<number>());\n  const counters = useRef({ issue: 0, update: 0, notice: 0 });\n  const [view, setView] = useState<{ section: Section; direction: number }>({ section: \"overview\", direction: 1 });\n  const [condensed, setCondensed] = useState(false);\n  const [layout, setLayout] = useState({ measured: false, collapsed: false, animate: false });\n  const [following, setFollowing] = useState(false);\n  const [archived, setArchived] = useState(false);\n  const [sharing, setSharing] = useState<\"idle\" | \"sending\" | \"sent\">(\"idle\");\n  const [issues, setIssues] = useState(startingIssues);\n  const [closing, setClosing] = useState<string[]>([]);\n  const [done, setDone] = useState(DONE_AT_START);\n  const [updates, setUpdates] = useState(startingUpdates);\n  const [notice, setNotice] = useState<Notice | null>(null);\n\n  // Only the header width is observed, so a label that grows (Follow to Following) never folds the button you just pressed.\n  // The first measurement applies at once; later width changes fold the actions with a spring.\n  useEffect(() => {\n    const row = bar.current,\n      folded = secondary.current,\n      fixed = trailing.current;\n    if (!row || !folded || !fixed || typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(() => {\n      const room = row.clientWidth - folded.offsetWidth - fixed.offsetWidth - 16;\n      setLayout((current) => {\n        const collapsed = current.measured && current.collapsed ? room < LEAD_RETURN : room < LEAD_MIN;\n        return current.measured && current.collapsed === collapsed\n          ? current\n          : { measured: true, collapsed, animate: current.measured };\n      });\n    });\n    observer.observe(row);\n    return () => observer.disconnect();\n  }, []);\n\n  useEffect(() => {\n    const pending = timers.current;\n    return () => pending.forEach((handle) => window.clearTimeout(handle));\n  }, []);\n\n  useEffect(() => {\n    if (!notice) return;\n    const handle = window.setTimeout(() => setNotice(null), 3200);\n    return () => window.clearTimeout(handle);\n  }, [notice]);\n\n  // Keep the chosen tab in view when the list scrolls sideways on narrow screens.\n  useEffect(() => {\n    const list = tabList.current;\n    const tab = list?.querySelector<HTMLElement>('[role=\"tab\"][data-state=\"active\"]');\n    if (!list || !tab) return;\n    const start = tab.offsetLeft - 12;\n    const end = tab.offsetLeft + tab.offsetWidth + 12 - list.clientWidth;\n    const behavior = reduce ? \"auto\" : \"smooth\";\n    if (list.scrollLeft > start) list.scrollTo({ left: start, behavior });\n    else if (list.scrollLeft < end) list.scrollTo({ left: end, behavior });\n  }, [view.section, reduce]);\n\n  function later(run: () => void, delay: number) {\n    const handle = window.setTimeout(() => {\n      timers.current.delete(handle);\n      run();\n    }, delay);\n    timers.current.add(handle);\n  }\n\n  function notify(text: string, tone: Notice[\"tone\"] = \"success\") {\n    counters.current.notice += 1;\n    setNotice({ key: counters.current.notice, text, tone });\n  }\n\n  /** Switching sections while condensed shows the top of the new panel and keeps the compact bar. */\n  function selectSection(next: Section, reveal = false) {\n    setView((current) =>\n      current.section === next ? current : { section: next, direction: order(next) > order(current.section) ? 1 : -1 },\n    );\n    const node = scroller.current;\n    if (node && (reveal || next !== view.section) && node.scrollTop > CONDENSE_AT + 1) node.scrollTop = CONDENSE_AT + 1;\n  }\n\n  function onScroll(event: UIEvent<HTMLDivElement>) {\n    const top = event.currentTarget.scrollTop;\n    setCondensed((current) => (current ? top > EXPAND_AT : top > CONDENSE_AT));\n  }\n\n  function backToTop() {\n    scroller.current?.scrollTo({ top: 0, behavior: reduce ? \"auto\" : \"smooth\" });\n    titleRef.current?.focus({ preventScroll: true });\n  }\n\n  function toggleFollow() {\n    const next = !following;\n    setFollowing(next);\n    notify(next ? \"Following Public launch\" : \"Stopped following Public launch\", next ? \"success\" : \"neutral\");\n  }\n\n  function shareUpdate() {\n    if (archived || sharing !== \"idle\") return;\n    setSharing(\"sending\");\n    later(() => {\n      const index = counters.current.update++;\n      const key = `draft-${index}`;\n      setUpdates((list) => [\n        {\n          id: key,\n          author: \"tomasz\",\n          date: \"Just now\",\n          tone: \"success\",\n          status: \"On track\",\n          body: draftUpdates[index % draftUpdates.length]!,\n          fresh: true,\n        },\n        ...list,\n      ]);\n      setSharing(\"sent\");\n      selectSection(\"updates\", true);\n      notify(\"Update shared with the project team\");\n      later(() => setSharing(\"idle\"), 1800);\n      later(() => setUpdates((list) => list.map((item) => (item.id === key ? { ...item, fresh: false } : item))), 1800);\n    }, 700);\n  }\n\n  function createIssue() {\n    if (archived) return;\n    const index = counters.current.issue++;\n    const issue: Issue = {\n      id: `PL-${39 + index}`,\n      title: draftIssues[index % draftIssues.length]!,\n      owner: \"tomasz\",\n      label: \"Triage\",\n      fresh: true,\n    };\n    setIssues((list) => [issue, ...list]);\n    selectSection(\"issues\", true);\n    notify(`${issue.id} created and assigned to you`);\n    later(\n      () => setIssues((list) => list.map((item) => (item.id === issue.id ? { ...item, fresh: false } : item))),\n      1800,\n    );\n  }\n\n  function completeIssue(issue: Issue) {\n    if (closing.includes(issue.id)) return;\n    const index = issues.findIndex((item) => item.id === issue.id);\n    const remaining = issues.filter((item) => item.id !== issue.id && !closing.includes(item.id));\n    const neighbor = remaining[Math.min(index, remaining.length - 1)];\n    setClosing((list) => [...list, issue.id]);\n    later(() => {\n      const hadFocus = document.activeElement === toggles.current.get(issue.id);\n      setIssues((list) => list.filter((item) => item.id !== issue.id));\n      setClosing((list) => list.filter((item) => item !== issue.id));\n      setDone((count) => count + 1);\n      notify(`${issue.id} marked done`);\n      if (hadFocus)\n        (neighbor\n          ? toggles.current.get(neighbor.id)\n          : scroller.current?.querySelector<HTMLElement>('[role=\"tabpanel\"][data-state=\"active\"]')\n        )?.focus();\n    }, 380);\n  }\n\n  function resetIssues() {\n    setIssues(startingIssues);\n    setDone(DONE_AT_START);\n    notify(\"Sample tasks restored\", \"neutral\");\n  }\n\n  function toggleArchive() {\n    const next = !archived;\n    setArchived(next);\n    notify(next ? \"Project archived. New tasks and updates are paused.\" : \"Project restored\", \"neutral\");\n  }\n\n  async function copyLink() {\n    try {\n      await navigator.clipboard.writeText(PROJECT_URL);\n      notify(\"Link copied\");\n    } catch {\n      notify(\"Couldn't copy the link. Try again from the address bar.\", \"error\");\n    }\n  }\n\n  function openCrumb(label: string) {\n    notify(`${label} would open here.`, \"neutral\");\n  }\n\n  const status = archived\n    ? { tone: \"neutral\" as const, label: \"Archived\" }\n    : { tone: \"success\" as const, label: \"On track\" };\n  const menuActions: MenuAction[] = [\n    ...(layout.collapsed\n      ? [\n          {\n            key: \"follow\",\n            label: following ? \"Unfollow\" : \"Follow\",\n            icon: following ? <BellOff {...icon} /> : <Bell {...icon} />,\n            onSelect: toggleFollow,\n          },\n          {\n            key: \"share\",\n            label: \"Share update\",\n            icon: <Megaphone {...icon} />,\n            onSelect: shareUpdate,\n            disabled: archived || sharing !== \"idle\",\n          },\n        ]\n      : []),\n    {\n      key: \"copy\",\n      label: \"Copy link\",\n      icon: <Link2 {...icon} />,\n      onSelect: copyLink,\n      separatorBefore: layout.collapsed,\n    },\n    {\n      key: \"archive\",\n      label: archived ? \"Restore project\" : \"Archive project\",\n      icon: archived ? <ArchiveRestore {...icon} /> : <Archive {...icon} />,\n      onSelect: toggleArchive,\n    },\n  ];\n\n  /** Leaving copy fades fast; arriving copy settles on the smooth spring with a short blur. */\n  const swap = (entering: boolean): Transition =>\n    reduce\n      ? still\n      : {\n          ...motionTokens.spring.smooth,\n          opacity: {\n            duration: entering ? motionTokens.duration.standard : motionTokens.duration.fast,\n            ease: [...motionTokens.ease.standard],\n            delay: entering ? 0.05 : 0,\n          },\n          filter: {\n            duration: entering ? motionTokens.duration.standard : motionTokens.duration.fast,\n            ease: [...motionTokens.ease.standard],\n          },\n        };\n  const shown = { opacity: 1, y: 0, scale: 1, filter: blur(0) };\n\n  return (\n    <TabsPrimitive.Root asChild value={view.section} onValueChange={(value) => selectSection(value as Section)}>\n      {/* The frame is the page: a fixed header over a scrolling body. On a phone the frame is the screen, so it takes most of it rather than stopping short with the body cut mid list. */}\n      <section\n        className=\"@container/page-header relative mx-auto flex h-[640px] w-[min(100%,980px)] min-w-0 flex-col overflow-hidden rounded-[16px] border border-line bg-surface font-body text-sm leading-body tracking-body text-ink [--pad:28px] max-[560px]:h-[clamp(560px,calc(100svh-48px),760px)] max-[560px]:rounded-[14px]\"\n        aria-labelledby={`${id}-title`}\n        data-measured={layout.measured || undefined}\n      >\n        <header className=\"relative z-1 flex-none bg-surface px-(--pad) pt-4 pb-0 shadow-[inset_0_-1px_0_var(--line)] @max-[559px]/page-header:[--pad:18px]\">\n          <div ref={bar} className=\"grid min-h-control-sm grid-cols-[minmax(0,1fr)_auto] items-center gap-4\">\n            {/* Breadcrumbs and the compact title share one cell and trade places when the header condenses. */}\n            <div className=\"grid min-w-0 items-center\">\n              <motion.nav\n                className=\"min-w-0 [grid-area:1/1]\"\n                aria-label=\"Breadcrumb\"\n                inert={condensed}\n                initial={false}\n                animate={condensed ? { opacity: 0, y: -8, filter: blur(motionTokens.blur.subtle) } : shown}\n                transition={swap(!condensed)}\n              >\n                <ol className=\"m-0 flex min-w-0 list-none items-center gap-1.5 p-0\">\n                  <li className={cn(crumbItem, \"@max-[559px]/page-header:hidden\")}>\n                    <button type=\"button\" className={crumb} onClick={() => openCrumb(\"North ridge team\")}>\n                      North ridge team\n                    </button>\n                    <ChevronRight size={14} strokeWidth={1.75} aria-hidden=\"true\" />\n                  </li>\n                  <li className={crumbItem}>\n                    <button type=\"button\" className={crumb} onClick={() => openCrumb(\"Projects\")}>\n                      Projects\n                    </button>\n                    <ChevronRight size={14} strokeWidth={1.75} aria-hidden=\"true\" />\n                  </li>\n                  <li className={cn(crumbItem, \"min-w-0 flex-[0_1_auto]\")}>\n                    <span className=\"block min-w-0 overflow-hidden text-ellipsis text-ink-2\" aria-current=\"page\">\n                      Public launch\n                    </span>\n                  </li>\n                </ol>\n              </motion.nav>\n              <motion.button\n                type=\"button\"\n                className=\"-ml-2 inline-flex max-w-[calc(100%+8px)] min-w-0 cursor-pointer items-center gap-2.5 justify-self-start rounded-button border-0 bg-transparent px-2 py-[5px] text-left text-ink transition-[background-color] duration-fast ease-standard [grid-area:1/1] hover:bg-[color-mix(in_oklab,var(--ink)_4.5%,transparent)] motion-reduce:transition-none\"\n                inert={!condensed}\n                aria-label=\"Public launch, back to top\"\n                onClick={backToTop}\n                initial={false}\n                animate={condensed ? shown : { opacity: 0, y: 14, filter: blur(motionTokens.blur.soft) }}\n                transition={swap(condensed)}\n              >\n                <span className=\"min-w-0 overflow-hidden text-base/[inherit] font-medium text-ellipsis whitespace-nowrap\">\n                  Public launch\n                </span>\n                <Badge\n                  className=\"flex-none @max-[559px]/page-header:hidden\"\n                  size=\"sm\"\n                  tone={status.tone}\n                  icon={<StatusDot />}\n                >\n                  {status.label}\n                </Badge>\n              </motion.button>\n            </div>\n\n            {/* Secondary actions fold to the right, under the overflow button, as their slot narrows. Only the right edge clips. Before the first measurement, narrow frames already hide the actions that will fold, so nothing flashes on load. */}\n            <div className=\"flex items-center justify-end\">\n              <motion.div\n                className={cn(\n                  \"flex flex-none [clip-path:inset(-8px_0_-8px_-8px)]\",\n                  !layout.measured && \"@max-[719px]/page-header:w-0! @max-[719px]/page-header:opacity-0!\",\n                )}\n                inert={layout.collapsed}\n                initial={false}\n                animate={layout.collapsed ? { width: 0, opacity: 0 } : { width: \"auto\", opacity: 1 }}\n                transition={\n                  layout.animate && !reduce\n                    ? {\n                        width: motionTokens.spring.smooth,\n                        opacity: {\n                          duration: layout.collapsed ? motionTokens.duration.fast : motionTokens.duration.standard,\n                          ease: [...motionTokens.ease.standard],\n                          delay: layout.collapsed ? 0.04 : 0.08,\n                        },\n                      }\n                    : still\n                }\n              >\n                <div ref={secondary} className=\"flex flex-none gap-2 pr-2\">\n                  <Button variant=\"secondary\" size=\"sm\" onClick={toggleFollow}>\n                    {following ? (\n                      <>\n                        <BellRing {...icon} /> Following\n                      </>\n                    ) : (\n                      <>\n                        <Bell {...icon} /> Follow\n                      </>\n                    )}\n                  </Button>\n                  <Button\n                    variant=\"secondary\"\n                    size=\"sm\"\n                    loading={sharing === \"sending\"}\n                    disabled={archived}\n                    onClick={shareUpdate}\n                  >\n                    {sharing === \"sent\" ? (\n                      <>\n                        <Check {...icon} /> Shared\n                      </>\n                    ) : (\n                      <>\n                        <Megaphone {...icon} /> Share update\n                      </>\n                    )}\n                  </Button>\n                </div>\n              </motion.div>\n              <div ref={trailing} className=\"flex flex-none items-center gap-2\">\n                <OverflowMenu actions={menuActions} reduce={reduce} />\n                <Button\n                  className=\"@max-[459px]/page-header:w-control-sm @max-[459px]/page-header:px-0\"\n                  size=\"sm\"\n                  aria-label=\"New task\"\n                  disabled={archived}\n                  onClick={createIssue}\n                >\n                  <Plus {...icon} />\n                  <span className=\"@max-[459px]/page-header:hidden\">New task</span>\n                </Button>\n              </div>\n            </div>\n          </div>\n\n          <motion.div\n            className=\"overflow-clip\"\n            initial={false}\n            animate={{ height: condensed ? 0 : \"auto\" }}\n            transition={reduce ? still : motionTokens.spring.smooth}\n          >\n            <div className=\"grid justify-items-start gap-2.5 px-0 pt-[18px] pb-5 @max-[559px]/page-header:pt-4\">\n              <motion.div\n                className=\"flex origin-top-left flex-wrap items-center gap-x-3.5 gap-y-2\"\n                initial={false}\n                animate={\n                  condensed ? { opacity: 0, y: -10, scale: 0.62, filter: blur(motionTokens.blur.subtle) } : shown\n                }\n                transition={swap(!condensed)}\n              >\n                <h2\n                  ref={titleRef}\n                  id={`${id}-title`}\n                  className=\"m-0 font-display text-3xl leading-display font-medium tracking-display focus:outline-none @max-[559px]/page-header:text-2xl\"\n                  tabIndex={-1}\n                >\n                  Public launch\n                </h2>\n                <Badge tone={status.tone} icon={<StatusDot />}>\n                  {status.label}\n                </Badge>\n              </motion.div>\n              <motion.div\n                className=\"grid justify-items-start gap-3.5\"\n                initial={false}\n                animate={condensed ? { opacity: 0, y: -6 } : { opacity: 1, y: 0 }}\n                transition={swap(!condensed)}\n              >\n                <p className=\"m-0 max-w-[60ch] text-ink-2 text-pretty\">\n                  Taking the app from beta invites to a public launch: legal pages, the pricing page and launch notes,\n                  with a go/no-go on 12 Nov.\n                </p>\n                <div className=\"flex flex-wrap items-center gap-x-[18px] gap-y-2 text-xs/[inherit] text-ink-3\">\n                  <AvatarGroup members={members} max={4} size=\"sm\" label=\"Project members\" />\n                  <span>\n                    Led by <strong className=\"font-medium text-ink-2\">Katerina Vlachou</strong>\n                  </span>\n                  <span className=\"inline-flex items-center gap-1.5\">\n                    <CalendarDays size={14} strokeWidth={1.75} aria-hidden=\"true\" />\n                    Target 12 Nov\n                  </span>\n                </div>\n              </motion.div>\n            </div>\n          </motion.div>\n\n          <LayoutGroup id={id}>\n            <TabsPrimitive.List asChild aria-label=\"Project sections\">\n              <motion.div\n                ref={tabList}\n                layoutScroll\n                className=\"relative -mx-(--pad) mt-0.5 mb-0 flex gap-0.5 overflow-x-auto overscroll-x-contain px-[calc(var(--pad)-10px)] [scrollbar-width:none] @max-[559px]/page-header:pr-[calc(var(--pad)+14px)] @max-[559px]/page-header:[mask-image:linear-gradient(to_right,#000_calc(100%-28px),transparent)] [&::-webkit-scrollbar]:hidden\"\n              >\n                {sections.map((item) => {\n                  const count =\n                    item.value === \"issues\"\n                      ? issues.length\n                      : item.value === \"updates\"\n                        ? updates.length\n                        : item.value === \"files\"\n                          ? files.length\n                          : null;\n                  return (\n                    <TabsPrimitive.Trigger\n                      key={item.value}\n                      value={item.value}\n                      className=\"group/tab relative inline-flex h-11 flex-none cursor-pointer items-center gap-1.5 border-0 bg-transparent px-2.5 py-0 font-medium whitespace-nowrap text-ink-2 transition-[color] duration-fast ease-standard [-webkit-tap-highlight-color:transparent] before:absolute before:inset-x-0 before:inset-y-2 before:rounded-button before:bg-[color-mix(in_oklab,var(--ink)_4.5%,transparent)] before:opacity-0 before:transition-opacity before:duration-fast before:ease-standard before:content-[''] hover:text-ink hover:before:opacity-100 data-[state=active]:text-ink motion-reduce:transition-none motion-reduce:before:transition-none\"\n                    >\n                      <span className=\"relative\">{item.label}</span>\n                      {/* The shared counter renders at display size by default; here it takes the tab's type. */}\n                      {count !== null && (\n                        <span className=\"relative text-ink-3 tabular-nums group-data-[state=active]/tab:text-ink-2 [&>span]:[color:inherit] [&>span]:[font-family:inherit] [&>span]:[font-size:inherit] [&>span]:[font-weight:inherit] [&>span]:[letter-spacing:inherit] [&>span]:[line-height:inherit]\">\n                          <AnimatedCounter value={count} />\n                        </span>\n                      )}\n                      {view.section === item.value && (\n                        <motion.span\n                          className=\"absolute right-2.5 bottom-0 left-2.5 h-0.5 rounded-t-[2px] bg-accent\"\n                          layoutId=\"indicator\"\n                          layoutDependency={view.section}\n                          transition={reduce ? still : motionTokens.spring.morph}\n                          aria-hidden=\"true\"\n                        />\n                      )}\n                    </TabsPrimitive.Trigger>\n                  );\n                })}\n              </motion.div>\n            </TabsPrimitive.List>\n          </LayoutGroup>\n        </header>\n\n        <div\n          ref={scroller}\n          className=\"relative min-h-0 flex-auto overflow-x-hidden overflow-y-auto overscroll-contain px-(--pad) pt-7 pb-[84px] [scrollbar-color:var(--line-strong)_transparent] [scrollbar-width:thin] @max-[559px]/page-header:[--pad:18px]\"\n          onScroll={onScroll}\n        >\n          <AnimatePresence initial={false} mode=\"popLayout\" custom={view.direction}>\n            <TabsPrimitive.Content key={view.section} value={view.section} forceMount asChild>\n              {/* Every panel is taller than the condensed viewport, so condensing never clamps the scroll back to the top. */}\n              <motion.div\n                className=\"min-h-[600px]\"\n                custom={view.direction}\n                variants={reduce ? panelFade : panelSlide}\n                initial=\"enter\"\n                animate=\"center\"\n                exit=\"exit\"\n              >\n                {view.section === \"overview\" && <OverviewPanel done={done} open={issues.length} />}\n                {view.section === \"issues\" && (\n                  <IssuesPanel\n                    issues={issues}\n                    closing={closing}\n                    reduce={reduce}\n                    onComplete={completeIssue}\n                    onReset={resetIssues}\n                    register={(key, node) => {\n                      if (node) toggles.current.set(key, node);\n                      else toggles.current.delete(key);\n                    }}\n                  />\n                )}\n                {view.section === \"updates\" && <UpdatesPanel updates={updates} reduce={reduce} />}\n                {view.section === \"files\" && <FilesPanel />}\n              </motion.div>\n            </TabsPrimitive.Content>\n          </AnimatePresence>\n        </div>\n\n        <div\n          className=\"pointer-events-none absolute right-4 bottom-4 left-4 z-3 flex justify-center\"\n          aria-hidden=\"true\"\n        >\n          <AnimatePresence initial={false} mode=\"popLayout\">\n            {notice && (\n              <motion.div\n                key={notice.key}\n                className={cn(\n                  \"inline-flex max-w-full items-center gap-2 rounded-control border border-line bg-surface px-3.5 py-[9px] text-sm/[inherit] text-ink shadow-floating [&>svg]:flex-none [&>svg]:text-ink-2\",\n                  notice.tone === \"success\" && \"[&>svg]:text-green\",\n                  notice.tone === \"error\" && \"[&>svg]:text-red\",\n                )}\n                data-tone={notice.tone}\n                initial={reduce ? { opacity: 0 } : { opacity: 0, y: 14, filter: blur(motionTokens.blur.soft) }}\n                animate={{ opacity: 1, y: 0, filter: blur(0) }}\n                exit={\n                  reduce\n                    ? { opacity: 0, transition: still }\n                    : { opacity: 0, y: 8, filter: blur(motionTokens.blur.subtle), transition: quick }\n                }\n                transition={\n                  reduce\n                    ? { duration: motionTokens.duration.instant }\n                    : {\n                        ...motionTokens.spring.snappy,\n                        opacity: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] },\n                        filter: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] },\n                      }\n                }\n              >\n                {notice.tone === \"success\" ? (\n                  <Check {...icon} />\n                ) : notice.tone === \"error\" ? (\n                  <CircleAlert {...icon} />\n                ) : null}\n                <span>{notice.text}</span>\n              </motion.div>\n            )}\n          </AnimatePresence>\n        </div>\n        <p className={srOnly} role=\"status\" aria-live=\"polite\">\n          {notice?.text}\n        </p>\n      </section>\n    </TabsPrimitive.Root>\n  );\n}\n\nexport default PageHeader;\n",
      "type": "registry:component",
      "target": "@components/iris/blocks/page-header/page-header.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/page-header",
  "meta": {
    "kind": "block",
    "group": "Blocks",
    "export": "PageHeader",
    "docs": "https://irisui.dev/components/page-header"
  }
}
