{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "author-byline",
  "type": "registry:ui",
  "title": "Author Byline",
  "description": "The \"who wrote this + when + how long it takes to read\" adornment that sits directly under an article H1. Pure surface composition over existing primitives — Avatar (large) on the left, a vertical Stack on the right with author name (UI semibold), optional…",
  "author": "ofri-peretz <https://github.com/ofri-peretz>",
  "categories": [
    "blog",
    "pattern"
  ],
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://ds.interlace.tools/r/theme.json",
    "https://ds.interlace.tools/r/avatar.json",
    "https://ds.interlace.tools/r/cn.json",
    "https://ds.interlace.tools/r/skeleton.json",
    "https://ds.interlace.tools/r/typography.json"
  ],
  "files": [
    {
      "path": "registry/interlace-ui/patterns/author-byline.tsx",
      "target": "components/ui/patterns/author-byline.tsx",
      "type": "registry:ui",
      "content": "import * as React from 'react';\n\n// @interlace/author-byline v1.2.0 — Interlace design system.\n// Docs, props and live preview: https://ds.interlace.tools/c/author-byline\n// What changed since: https://ds.interlace.tools/c/author-byline#history\n// Generated banner — keep it, the upgrade diff reads this version.\n\n/**\n * @interlace/ui — AuthorByline\n *\n * The \"who wrote this + when + how long it takes to read\" adornment that sits\n * directly under an article H1. Pure surface composition over existing\n * primitives — Avatar (large) on the left, a vertical Stack on the right\n * with author name (UI semibold), optional one-line bio (UI-sm muted), and a\n * meta row that pairs the publication date with the reading-time chip,\n * separated by a typographic mid-dot.\n *\n * Date is rendered through a native `<time dateTime>` so RSS / Google\n * structured-data parsers + assistive tech read the ISO value, while the\n * visible text uses the short, locale-friendly `Mar 5, 2026` form. Reading\n * time is a span (not a `<time>`) because it isn't a moment in time — it's\n * an estimated duration, and the `<time>` element's `datetime` grammar for\n * durations (`PT5M`) confuses more readers than it helps.\n *\n * ## Anatomy\n *\n *   AuthorByline                     (div — data-min-viewport=320)\n *     ├─ Avatar size=lg              (left — `h-12 w-12`, AvatarImage src, AvatarFallback initial)\n *     └─ Stack vertical gap=xs       (right column)\n *         ├─ Typography ui semibold  (author name)\n *         ├─ Typography ui-sm muted  (optional bio)\n *         └─ <div> meta row          (PublishedDate · ReadingTime)\n *             ├─ <time dateTime>     (published date — short form)\n *             ├─ <span aria-hidden>· (dot separator)\n *             └─ <span>              (reading time — Clock icon + \"N min read\")\n *\n * ## MIN_VIEWPORT — 320\n *\n * Article bylines are reading-surface furniture; they MUST work on the\n * narrowest phone we support. A 320 CSS-px viewport fits one 48px-square\n * avatar + a single-line name + a two-chip meta row when the bio is omitted;\n * when the bio is present, the right column wraps naturally because every\n * row uses the same `flex-wrap` rhythm.\n *\n * | Rule | Concept                          | Where in this file                                          |\n * | ---- | -------------------------------- | ----------------------------------------------------------- |\n * | R4   | Extends native el                | `React.ComponentProps<'div'> & AuthorBylineProps`           |\n * | R6   | data-slot on root + parts        | `data-slot=\"author-byline\"`, `data-slot=\"author-byline-*\"`  |\n * | R7   | className merged + ...rest       | `cn(BASE, className)` + `{...props}`                        |\n * | R8   | No `isXxx`                       | n/a — no boolean variants                                   |\n * | R10  | Composition over primitives      | Avatar + Typography + (PublishedDate + ReadingTime inline)  |\n * | R14  | Declares min viewport            | `data-min-viewport={String(MIN_VIEWPORT)}` + exported const |\n * | R18  | Tailwind only                    | Zero inline `style`; utility classes only                   |\n * | R19  | Tokens only                      | `gap-md`/`gap-xs` from `--spacing-*`; type tokens via Typography; semantic colors |\n * | R20  | AA contrast                      | Muted tone resolves to `--muted-foreground` (AA-cleared)    |\n * | R25  | Server component                 | No hooks → no `'use client'` (Avatar is the client boundary) |\n * | R26  | A11y from native el              | `<time dateTime>` for the date; reading-time chip has visible text |\n */\n\nimport { Clock } from 'lucide-react';\n\nimport { cn } from '@/lib/utils';\nimport { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';\nimport { Skeleton } from '@/components/ui/skeleton';\nimport { Typography } from '@/components/ui/typography';\n\nexport const MIN_VIEWPORT = 320 as const;\n\n/**\n * Short, locale-friendly date for the visible byline (e.g. `Mar 5, 2026`).\n *\n * `timeZone: 'UTC'` is load-bearing, not a default. A date-only ISO string —\n * which is exactly what `publishedDateIso` documents — parses as UTC midnight,\n * and `toLocaleDateString` then renders it in the *reader's* zone. Without the\n * pin, every reader west of UTC (all of the Americas) sees the previous day on\n * every byline in the DS. A publication date is a calendar fact, not an\n * instant, so it must not be re-projected into a local zone at all.\n *\n * Returns `null` for anything unparseable, so a byline rendered before its date\n * has arrived degrades instead of printing the literal string \"Invalid Date\".\n */\nfunction formatDate(iso: string): string | null {\n  const date = new Date(iso);\n  if (Number.isNaN(date.getTime())) return null;\n  return date.toLocaleDateString('en-US', {\n    month: 'short',\n    day: 'numeric',\n    year: 'numeric',\n    timeZone: 'UTC',\n  });\n}\n\n/** First grapheme of `name`, uppercased, for the avatar fallback. */\nfunction initialOf(name: string): string {\n  const trimmed = name.trim();\n  if (!trimmed) return '?';\n  return trimmed.charAt(0).toUpperCase();\n}\n\ntype AuthorBylineProps = React.ComponentProps<'div'> & {\n  /** Author display name — required in the idle state. */\n  authorName?: string;\n  /** `<img src>` for the avatar. Decorative — `alt` derives from `authorName`. */\n  authorAvatar?: string;\n  /** Optional one-line bio (e.g. \"Staff engineer at Interlace\"). */\n  authorBio?: string;\n  /** ISO-8601 publication timestamp — drives both `<time dateTime>` and the visible short form. */\n  publishedDateIso?: string;\n  /** Estimated reading time in whole minutes. Omitting it hides the chip. */\n  readingTimeMinutes?: number;\n  /**\n   * When true, render a `<Skeleton variant=\"author-byline\" />` composite\n   * (avatar + name + date silhouette) instead of the populated byline.\n   */\n  loading?: boolean;\n};\n\n/**\n * Article hero adornment — author + date + reading time. Server component.\n */\nexport function AuthorByline({\n  className,\n  authorName,\n  authorAvatar,\n  authorBio,\n  publishedDateIso,\n  readingTimeMinutes,\n  loading,\n  ...props\n}: AuthorBylineProps) {\n  if (loading) {\n    return (\n      <Skeleton\n        variant=\"author-byline\"\n        data-slot=\"author-byline\"\n        className={className}\n      />\n    );\n  }\n  return (\n    <div\n      data-slot=\"author-byline\"\n      data-min-viewport={String(MIN_VIEWPORT)}\n      className={cn('flex flex-row items-center gap-md', className)}\n      {...props}\n    >\n      <Avatar\n        data-slot=\"author-byline-avatar\"\n        className=\"size-12 shrink-0\"\n      >\n        <AvatarImage src={authorAvatar} alt={authorName} />\n        <AvatarFallback>{initialOf(authorName ?? '')}</AvatarFallback>\n      </Avatar>\n\n      <div\n        data-slot=\"author-byline-body\"\n        className=\"flex min-w-0 flex-col gap-xs\"\n      >\n        <Typography\n          data-slot=\"author-byline-name\"\n          variant=\"ui\"\n          className=\"font-semibold\"\n        >\n          {authorName}\n        </Typography>\n\n        {authorBio ? (\n          <Typography\n            data-slot=\"author-byline-bio\"\n            variant=\"ui-sm\"\n            tone=\"muted\"\n          >\n            {authorBio}\n          </Typography>\n        ) : null}\n\n        <div\n          data-slot=\"author-byline-meta\"\n          className=\"text-muted-foreground flex flex-wrap items-center gap-2 text-ui-sm\"\n        >\n          {/* No date, or an unparseable one, renders no <time> at all —\n              `<time dateTime=\"\">` is invalid markup and \"Invalid Date\" is\n              worse than an absent byline date. */}\n          {publishedDateIso && formatDate(publishedDateIso) ? (\n            <time\n              data-slot=\"author-byline-published-date\"\n              dateTime={publishedDateIso}\n            >\n              {formatDate(publishedDateIso)}\n            </time>\n          ) : null}\n          {readingTimeMinutes !== undefined ? (\n            <>\n              <span aria-hidden className=\"opacity-60\">\n                ·\n              </span>\n              <span\n                data-slot=\"author-byline-reading-time\"\n                className=\"inline-flex items-center gap-1\"\n              >\n                <Clock className=\"size-4\" aria-hidden />\n                {readingTimeMinutes} min read\n              </span>\n            </>\n          ) : null}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
    }
  ],
  "meta": {
    "tier": "pattern",
    "client": false,
    "minViewport": 320,
    "loading": true,
    "version": "1.2.0",
    "since": "1.0.0"
  },
  "docs": "## @interlace/author-byline\n\nInstalled to `components/ui/patterns/author-byline.tsx`.\n\n```tsx\nimport { /* … */ } from '@/components/ui/patterns/author-byline';\n```\n\nProps, a11y contract, live preview and source: https://ds.interlace.tools/c/author-byline\n\nRequires the `@interlace/theme` CSS baseline (installed automatically as a registry dependency)."
}
