{
  "$schema": "https://abackui.com/schema/registry-item.json",
  "name": "article-layout",
  "type": "registry:block",
  "title": "Article layout",
  "description": "A long-form page with meta bar, scoped typography, sticky contents, share links, and reading progress.",
  "dependencies": [
    "@tabler/icons-react"
  ],
  "registryDependencies": [
    "https://abackui.com/r/avatar.json",
    "https://abackui.com/r/badge.json",
    "https://abackui.com/r/breadcrumbs.json",
    "https://abackui.com/r/collapsible.json",
    "https://abackui.com/r/reading-progress.json",
    "https://abackui.com/r/share-buttons.json",
    "https://abackui.com/r/table-of-contents.json"
  ],
  "categories": [
    "Content"
  ],
  "docs": "https://abackui.com/docs/blocks/article-layout",
  "meta": {
    "status": "beta",
    "usedIn": [
      "aback-tools",
      "aback-launch"
    ]
  },
  "files": [
    {
      "path": "components/blocks/article-layout.tsx",
      "content": "\"use client\";\n\nimport { type ReactNode, useId, useRef } from \"react\";\nimport { IconCalendar, IconChevronDown, IconClock } from \"@tabler/icons-react\";\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Breadcrumbs, type BreadcrumbItem } from \"@/components/ui/breadcrumbs\";\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"@/components/ui/collapsible\";\nimport { ReadingProgress } from \"@/components/ui/reading-progress\";\nimport { ShareButtons } from \"@/components/ui/share-buttons\";\nimport { TableOfContents, type TableOfContentsItem } from \"@/components/ui/table-of-contents\";\n\nexport interface ArticleAuthor {\n  name: string;\n  role?: string;\n  initials?: string;\n  avatarUrl?: string;\n}\n\nexport interface ArticleLayoutBlockProps {\n  title: string;\n  /** The lead paragraph under the title. */\n  description?: ReactNode;\n  category?: string;\n  author: ArticleAuthor;\n  /** ISO date of publication. Rendered as a stable absolute date. */\n  publishedAt: string;\n  /** Such as \"6 min read\". Compute it from the real word count. */\n  readingTime?: string;\n  breadcrumbs?: BreadcrumbItem[];\n  /** One entry per h2 or h3 in the body, by id. */\n  toc?: TableOfContentsItem[];\n  /** The canonical URL, for the share links. */\n  shareUrl?: string;\n  /** The article body. Plain h2, h3, p, ul, ol, blockquote, and code are styled. */\n  children: ReactNode;\n}\n\n// Typography for the body, scoped to this block so it never leaks into the page.\nconst prose = [\n  \"flex min-w-0 max-w-[72ch] flex-col gap-4 text-sm leading-relaxed text-muted-foreground\",\n  \"[&_h2]:mt-6 [&_h2]:scroll-mt-24 [&_h2]:text-2xl [&_h2]:font-bold [&_h2]:tracking-tight [&_h2]:text-foreground\",\n  \"[&_h3]:mt-2 [&_h3]:scroll-mt-24 [&_h3]:text-lg [&_h3]:font-semibold [&_h3]:text-foreground\",\n  \"[&_strong]:font-semibold [&_strong]:text-foreground\",\n  \"[&_a]:text-link [&_a]:underline [&_a]:underline-offset-4 [&_a:hover]:text-link-hover\",\n  \"[&_ul]:ml-6 [&_ul]:list-disc [&_ul]:space-y-1.5 [&_ol]:ml-6 [&_ol]:list-decimal [&_ol]:space-y-1.5 [&_li]:marker:text-muted-foreground\",\n  \"[&_blockquote]:border-l-4 [&_blockquote]:border-primary-300 [&_blockquote]:py-1 [&_blockquote]:pl-4 [&_blockquote]:italic dark:[&_blockquote]:border-primary-500\",\n  \"[&_:not(pre)>code]:bg-muted [&_:not(pre)>code]:px-1 [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-[0.9em] [&_:not(pre)>code]:text-foreground\",\n].join(\" \");\n\nfunction initialsOf(name: string) {\n  const parts = name.trim().split(/\\s+/).filter(Boolean);\n  return parts.length > 1 ? `${parts[0]![0]}${parts[1]![0]}`.toUpperCase() : name.slice(0, 2).toUpperCase();\n}\n\n/**\n * A long-form article page: breadcrumbs, title, author and meta bar, a body\n * with scoped typography, a sticky \"On this page\" list, share links, and a\n * reading progress bar. The aside appears when the block's own container is\n * wide enough, so it adapts inside narrow layouts as well as on wide screens.\n */\nexport function ArticleLayoutBlock({\n  title,\n  description,\n  category,\n  author,\n  publishedAt,\n  readingTime,\n  breadcrumbs,\n  toc = [],\n  shareUrl,\n  children,\n}: ArticleLayoutBlockProps) {\n  const headingId = useId();\n  const article = useRef<HTMLElement>(null);\n  const published = new Date(publishedAt);\n  const validDate = !Number.isNaN(published.getTime());\n\n  return (\n    <div className=\"@container mx-auto w-full max-w-6xl px-4 py-10 md:px-6\">\n      <ReadingProgress target={article} />\n      <article ref={article} className=\"flex flex-col gap-6\" aria-labelledby={headingId}>\n        <header className=\"flex flex-col gap-4\">\n          {breadcrumbs?.length ? <Breadcrumbs items={breadcrumbs} /> : null}\n          {category ? <Badge variant=\"brand\" className=\"self-start\">{category}</Badge> : null}\n          <h1 id={headingId} className=\"max-w-3xl text-3xl font-bold leading-tight tracking-tight text-foreground text-balance sm:text-4xl\">\n            {title}\n          </h1>\n          {description ? (\n            <p className=\"max-w-[72ch] text-base leading-relaxed text-muted-foreground\">{description}</p>\n          ) : null}\n          <div className=\"flex flex-wrap items-center gap-x-4 gap-y-3 border border-border bg-muted/20 p-4\">\n            <div className=\"flex items-center gap-3\">\n              <Avatar size=\"lg\">\n                {author.avatarUrl ? <AvatarImage src={author.avatarUrl} alt=\"\" /> : null}\n                <AvatarFallback>{author.initials ?? initialsOf(author.name)}</AvatarFallback>\n              </Avatar>\n              <div className=\"flex flex-col\">\n                <span className=\"text-sm font-semibold text-foreground\">{author.name}</span>\n                {author.role ? <span className=\"text-xs text-muted-foreground\">{author.role}</span> : null}\n              </div>\n            </div>\n            <span aria-hidden=\"true\" className=\"hidden h-8 w-px bg-border sm:block\" />\n            <ul className=\"flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground\">\n              {validDate ? (\n                <li className=\"flex items-center gap-1.5\">\n                  <IconCalendar aria-hidden=\"true\" className=\"size-3.5\" />\n                  <span className=\"sr-only\">Published </span>\n                  <time dateTime={published.toISOString()}>\n                    {new Intl.DateTimeFormat(\"en-US\", { dateStyle: \"medium\", timeZone: \"UTC\" }).format(published)}\n                  </time>\n                </li>\n              ) : null}\n              {readingTime ? (\n                <li className=\"flex items-center gap-1.5\">\n                  <IconClock aria-hidden=\"true\" className=\"size-3.5\" />\n                  {readingTime}\n                </li>\n              ) : null}\n            </ul>\n          </div>\n        </header>\n\n        {toc.length ? (\n          <Collapsible className=\"border border-border bg-muted/20 @3xl:hidden\">\n            <CollapsibleTrigger className=\"group flex w-full items-center justify-between px-4 py-3 text-sm font-semibold text-foreground outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-focus-ring\">\n              Contents\n              <IconChevronDown\n                aria-hidden=\"true\"\n                className=\"size-4 text-muted-foreground transition-transform duration-200 group-data-[state=open]:rotate-180 motion-reduce:transition-none\"\n              />\n            </CollapsibleTrigger>\n            <CollapsibleContent className=\"px-4 pb-4\">\n              <TableOfContents items={toc} />\n            </CollapsibleContent>\n          </Collapsible>\n        ) : null}\n\n        <div className=\"grid gap-10 @3xl:grid-cols-[minmax(0,1fr)_240px] @3xl:items-start\">\n          <div className=\"flex min-w-0 flex-col gap-10\">\n            <div className={prose}>{children}</div>\n            {shareUrl ? (\n              <footer className=\"flex flex-wrap items-center justify-between gap-3 border-t border-border pt-6\">\n                <span className=\"text-sm font-medium text-foreground\">Share this article</span>\n                <ShareButtons url={shareUrl} title={title} networks={[\"x\", \"linkedin\", \"email\"]} />\n              </footer>\n            ) : null}\n          </div>\n          {toc.length ? (\n            <aside className=\"hidden @3xl:sticky @3xl:top-20 @3xl:block @3xl:max-h-[calc(100vh-6rem)] @3xl:overflow-y-auto\">\n              <TableOfContents items={toc} className=\"border border-border bg-muted/20 p-4\" />\n            </aside>\n          ) : null}\n        </div>\n      </article>\n    </div>\n  );\n}\n",
      "type": "registry:block",
      "target": "components/blocks/article-layout.tsx"
    }
  ]
}
