{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "blog-1",
  "title": "Blog 1: featured post with filterable grid",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A blog index with a large featured post, topic filter chips and a three-column grid. Posts without a photo get a generated cover drawn from theme tokens, in five compositions.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/i18n.json",
    "https://ui.ballmac.com/r/media.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/blog-1/blog-1.tsx",
      "content": "// Ballmac UI: Blog 1. https://ui.ballmac.com/blocks/blog-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowUpRight, Clock } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useLocale } from \"@/lib/ballmac/i18n\"\nimport { Media, type MediaSource } from \"@/components/ballmac/media\"\n\ntype Blog1Post = {\n  title: string\n  /** One or two sentences under the title. */\n  excerpt: string\n  /** Topic, used for the filter chips. */\n  category: string\n  /** Publication date as an ISO string (YYYY-MM-DD). Shown in UTC so server and browser agree. */\n  date: string\n  /** Estimated minutes to read. */\n  readMinutes: number\n  /** Author's name. */\n  author: string\n  /** Author's role. */\n  role?: string\n  /** Where the post lives. */\n  href: string\n  /** Cover photo: an image URL, an object with alt text and a dark-mode file, or your own element. A generated cover is drawn when omitted. */\n  image?: MediaSource\n  /** Describes `image` when it is a plain URL. The title sits beside the cover, so the default is decorative (empty). */\n  imageAlt?: string\n  /** Which generated cover to draw (0 to 4). Defaults to the post's position. */\n  cover?: number\n}\n\ntype Blog1Props = Omit<React.ComponentProps<\"section\">, \"title\"> & {\n  /** Label above the heading. */\n  eyebrow?: string\n  /** Section heading. */\n  title?: string\n  /** One sentence under the heading. */\n  description?: string\n  /** Posts, newest first. The first one is featured. */\n  posts?: Blog1Post[]\n  /** Show category filter chips. */\n  filterable?: boolean\n  /** Locale for dates. Fixed by default so server and browser match. */\n  locale?: string\n}\n\nconst tones = [\n  { bg: \"from-chart-1/30 to-chart-1/5\", solid: \"bg-chart-1\", soft: \"bg-chart-1/25\", text: \"text-chart-1\", stroke: \"stroke-chart-1\" },\n  { bg: \"from-chart-3/30 to-chart-3/5\", solid: \"bg-chart-3\", soft: \"bg-chart-3/25\", text: \"text-chart-3\", stroke: \"stroke-chart-3\" },\n  { bg: \"from-chart-5/30 to-chart-5/5\", solid: \"bg-chart-5\", soft: \"bg-chart-5/25\", text: \"text-chart-5\", stroke: \"stroke-chart-5\" },\n  { bg: \"from-chart-2/30 to-chart-2/5\", solid: \"bg-chart-2\", soft: \"bg-chart-2/25\", text: \"text-chart-2\", stroke: \"stroke-chart-2\" },\n  { bg: \"from-chart-4/30 to-chart-4/5\", solid: \"bg-chart-4\", soft: \"bg-chart-4/25\", text: \"text-chart-4\", stroke: \"stroke-chart-4\" },\n]\n\ntype BlogCoverProps = React.ComponentProps<\"div\"> & {\n  /** Which generated cover to draw. Wraps after the fifth. */\n  variant?: number\n}\n\n/** A decorative generated cover drawn from theme tokens: five compositions, each in a different chart colour. */\nfunction BlogCover({ variant = 0, className, ...props }: BlogCoverProps) {\n  const i = ((variant % 5) + 5) % 5\n  const t = tones[i]\n  return (\n    <div\n      aria-hidden=\"true\"\n      data-slot=\"blog-cover\"\n      className={cn(\"relative isolate overflow-hidden bg-gradient-to-br\", t.bg, className)}\n      {...props}\n    >\n      {i === 0 && (\n        <>\n          {[88, 64, 40, 18].map((s, k) => (\n            <span key={s} className={cn(\"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full border\", t.soft, k === 3 && t.solid, \"border-foreground/5\")} style={{ width: `${s}%`, aspectRatio: \"1\" }} />\n          ))}\n        </>\n      )}\n      {i === 1 && (\n        <div className=\"absolute inset-0 flex -skew-x-12 items-end justify-center gap-[4%] px-[12%]\">\n          {[44, 68, 54, 82, 62, 94].map((h, k) => (\n            <span key={k} className={cn(\"w-[10%] rounded-t-xl\", k % 2 ? t.solid : t.soft)} style={{ height: `${h}%` }} />\n          ))}\n        </div>\n      )}\n      {i === 2 && (\n        <>\n          <div className=\"absolute inset-0 bg-[radial-gradient(color-mix(in_oklch,var(--foreground)_20%,transparent)_1px,transparent_1px)] [background-size:16px_16px]\" />\n          <span className={cn(\"absolute top-[18%] start-[16%] size-[46%] rotate-12 rounded-[22%]\", t.solid)} />\n          <span className={cn(\"absolute end-[14%] bottom-[16%] size-[34%] -rotate-6 rounded-[28%] border border-foreground/10\", t.soft)} />\n        </>\n      )}\n      {i === 3 && (\n        <>\n          <span className={cn(\"absolute -top-[20%] -start-[10%] size-[70%] rounded-full blur-2xl\", t.soft)} />\n          <span className={cn(\"absolute top-[22%] end-[8%] size-[56%] rounded-[45%_55%_60%_40%]\", t.solid)} />\n          <span className=\"bg-background/60 absolute bottom-[12%] start-[18%] h-[22%] w-[44%] rounded-full backdrop-blur-sm\" />\n        </>\n      )}\n      {i === 4 && (\n        <svg viewBox=\"0 0 400 250\" preserveAspectRatio=\"xMidYMid slice\" className=\"absolute inset-0 size-full\">\n          {[0, 1, 2, 3, 4, 5, 6].map((k) => (\n            <path key={k} d={`M-20 ${60 + k * 22} C 80 ${10 + k * 24}, 140 ${150 + k * 12}, 230 ${90 + k * 18} S 360 ${30 + k * 20}, 430 ${100 + k * 16}`} fill=\"none\" strokeWidth={k === 3 ? 3 : 1.5} className={cn(t.stroke, k === 3 ? \"opacity-100\" : \"opacity-40\")} />\n          ))}\n        </svg>\n      )}\n    </div>\n  )\n}\n\nconst defaults: Blog1Post[] = [\n  { title: \"How we cut month-end close from five days to one\", excerpt: \"A look at the three workflow changes behind our own fastest close yet, and the dashboards that made them stick.\", category: \"Product\", date: \"2026-09-24\", readMinutes: 8, author: \"Priya Raman\", role: \"CEO\", href: \"#\" },\n  { title: \"Designing calm software for busy teams\", excerpt: \"Notes on restraint: fewer colours, quieter motion and why every empty state is a chance to help.\", category: \"Design\", date: \"2026-09-17\", readMinutes: 6, author: \"Elena Fischer\", role: \"Head of Design\", href: \"#\" },\n  { title: \"Inside our new audit log\", excerpt: \"Append-only storage, signed exports and a query language your auditors can read.\", category: \"Engineering\", date: \"2026-09-09\", readMinutes: 11, author: \"Amara Singh\", role: \"Staff Engineer\", href: \"#\" },\n  { title: \"What 2,000 invoices taught us about getting paid\", excerpt: \"The wording, timing and layout choices that move the median payout from 14 days to 5.\", category: \"Research\", date: \"2026-08-28\", readMinutes: 7, author: \"Jonas Ortiz\", role: \"Product Manager\", href: \"#\" },\n  { title: \"Announcing SAML SSO and SCIM\", excerpt: \"Enterprise sign-in is here for Pro and Scale, with setup guides for Okta, Entra ID and Google.\", category: \"Product\", date: \"2026-08-19\", readMinutes: 4, author: \"Marcus Webb\", role: \"CTO\", href: \"#\" },\n  { title: \"A field guide to resilient webhooks\", excerpt: \"Retries, idempotency keys and replay tools: the boring details that keep integrations alive.\", category: \"Engineering\", date: \"2026-08-05\", readMinutes: 9, author: \"Amara Singh\", role: \"Staff Engineer\", href: \"#\" },\n  { title: \"Customer story: how Northwind went paperless\", excerpt: \"Forty suppliers, one inbox and no more lost PDFs. A conversation with their finance lead.\", category: \"Customers\", date: \"2026-07-30\", readMinutes: 5, author: \"Tomás Herrera\", role: \"Customer Success\", href: \"#\" },\n]\n\nfunction formatDate(iso: string, locale: string) {\n  return new Intl.DateTimeFormat(locale, { dateStyle: \"medium\", timeZone: \"UTC\" }).format(new Date(`${iso}T00:00:00Z`))\n}\n\nfunction Meta({ post, locale, className }: { post: Blog1Post; locale: string; className?: string }) {\n  return (\n    <p className={cn(\"text-muted-foreground flex flex-wrap items-center gap-x-2 gap-y-1 text-sm\", className)}>\n      <time dateTime={post.date}>{formatDate(post.date, locale)}</time>\n      <span aria-hidden=\"true\">·</span>\n      <span className=\"inline-flex items-center gap-1\">\n        <Clock className=\"size-3.5\" aria-hidden=\"true\" />\n        {post.readMinutes} min read\n      </span>\n    </p>\n  )\n}\n\nfunction Cover({ post, index, className }: { post: Blog1Post; index: number; className?: string }) {\n  if (post.image) return <Media media={post.image} alt={post.imageAlt ?? \"\"} fill className={className} />\n  return <BlogCover variant={post.cover ?? index} className={className} />\n}\n\nfunction Blog1({\n  eyebrow = \"The Acme blog\",\n  title = \"Ideas, updates and field notes.\",\n  description = \"What we are building, how we think about it and what we learn from the teams who use it.\",\n  posts = defaults,\n  filterable = true,\n  locale,\n  className,\n  ...props\n}: Blog1Props) {\n  const defaultLocale = useLocale()\n  locale ??= defaultLocale\n  const [category, setCategory] = React.useState(\"All\")\n  const categories = [\"All\", ...Array.from(new Set(posts.map((p) => p.category)))]\n  const shown = posts.filter((p) => category === \"All\" || p.category === category)\n  const [featured, ...rest] = shown\n\n  return (\n    <section data-slot=\"blog-1\" className={cn(\"mx-auto max-w-6xl px-4 py-20 sm:px-6 md:py-28\", className)} {...props}>\n      <div className=\"max-w-2xl\">\n        <p className=\"text-muted-foreground text-sm font-medium\">{eyebrow}</p>\n        <h2 className=\"mt-3 text-3xl font-semibold tracking-[-0.035em] text-balance sm:text-4xl lg:text-5xl\">{title}</h2>\n        <p className=\"text-muted-foreground mt-4 text-lg text-pretty\">{description}</p>\n      </div>\n\n      {filterable && categories.length > 2 && (\n        <div role=\"group\" aria-label=\"Filter by topic\" className=\"-mx-4 mt-8 flex gap-1.5 overflow-x-auto px-4 pb-1 [scrollbar-width:none] sm:mx-0 sm:px-0\">\n          {categories.map((c) => (\n            <button\n              key={c}\n              type=\"button\"\n              aria-pressed={category === c}\n              onClick={() => setCategory(c)}\n              className={cn(\n                \"focus-visible:ring-ring/50 h-9 shrink-0 rounded-full border px-4 text-sm font-medium outline-none transition-colors focus-visible:ring-[3px]\",\n                category === c ? \"bg-foreground text-background border-transparent\" : \"text-muted-foreground hover:text-foreground hover:bg-accent\"\n              )}\n            >\n              {c}\n            </button>\n          ))}\n        </div>\n      )}\n\n      <p role=\"status\" className=\"sr-only\">{shown.length} {shown.length === 1 ? \"post\" : \"posts\"} shown.</p>\n\n      {featured && (\n        <article className=\"group/post bg-card relative mt-10 grid overflow-hidden rounded-3xl border transition-shadow duration-300 hover:shadow-[0_30px_70px_-40px_rgb(0_0_0/0.45)] md:grid-cols-[1.15fr_1fr]\">\n          <Cover post={featured} index={posts.indexOf(featured)} className=\"aspect-[16/10] w-full transition-transform duration-700 ease-out group-hover/post:scale-[1.03] md:aspect-auto md:h-full motion-reduce:transition-none motion-reduce:group-hover/post:scale-100\" />\n          <div className=\"bg-card relative flex flex-col justify-center p-6 sm:p-10 lg:p-12\">\n            <span className=\"bg-secondary w-fit rounded-full px-2.5 py-1 text-xs font-medium\">{featured.category}</span>\n            <h3 className=\"mt-4 text-2xl font-semibold tracking-[-0.03em] text-balance sm:text-3xl\">\n              <a href={featured.href} className=\"focus-visible:ring-ring/50 rounded-sm outline-none after:absolute after:inset-0 focus-visible:ring-[3px]\">{featured.title}</a>\n            </h3>\n            <p className=\"text-muted-foreground mt-3 text-pretty\">{featured.excerpt}</p>\n            <Meta post={featured} locale={locale} className=\"mt-6\" />\n            <p className=\"mt-4 text-sm\">\n              <span className=\"font-medium\">{featured.author}</span>\n              {featured.role && <span className=\"text-muted-foreground\">, {featured.role}</span>}\n            </p>\n          </div>\n        </article>\n      )}\n\n      {rest.length > 0 && (\n        <ul className=\"mt-8 grid gap-x-6 gap-y-10 sm:grid-cols-2 lg:grid-cols-3\">\n          {rest.map((post) => (\n            <li key={post.title}>\n              <article className=\"group/post relative flex h-full flex-col\">\n                <div className=\"overflow-hidden rounded-2xl border\">\n                  <Cover post={post} index={posts.indexOf(post)} className=\"aspect-[16/10] w-full transition-transform duration-700 ease-out group-hover/post:scale-[1.04] motion-reduce:transition-none motion-reduce:group-hover/post:scale-100\" />\n                </div>\n                <div className=\"mt-4 flex flex-1 flex-col\">\n                  <p className=\"text-muted-foreground text-xs font-medium tracking-wide uppercase\">{post.category}</p>\n                  <h3 className=\"mt-2 text-lg leading-snug font-semibold tracking-[-0.02em] text-balance\">\n                    <a href={post.href} className=\"focus-visible:ring-ring/50 rounded-sm outline-none after:absolute after:inset-0 focus-visible:ring-[3px]\">\n                      {post.title}\n                      <ArrowUpRight className=\"ms-1 inline size-4 -translate-x-1 opacity-0 transition-all group-hover/post:translate-x-0 group-hover/post:opacity-100 motion-reduce:transition-none rtl:-scale-x-100\" aria-hidden=\"true\" />\n                    </a>\n                  </h3>\n                  <p className=\"text-muted-foreground mt-2 line-clamp-2 text-sm text-pretty\">{post.excerpt}</p>\n                  <Meta post={post} locale={locale} className=\"mt-auto pt-4\" />\n                </div>\n              </article>\n            </li>\n          ))}\n        </ul>\n      )}\n    </section>\n  )\n}\n\nexport { Blog1, BlogCover, type Blog1Props, type Blog1Post, type BlogCoverProps }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/blog-1/blog-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "blog",
      "posts",
      "articles",
      "index",
      "filter",
      "cards"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "The index page of a blog. Pass posts=[{ title, excerpt, category, date, readMinutes, author, role?, href, image?, cover? }]; the first post is featured. Also exports BlogCover for your own cards.",
      "whenToUse": [
        "Company and product blogs",
        "Resource and news pages"
      ],
      "whenNotToUse": [
        "A single article (use blog-post-1)",
        "Hundreds of posts that need pagination and search"
      ],
      "composesWith": [
        "blog-post-1",
        "newsletter-1",
        "header-2",
        "footer-2"
      ],
      "a11y": [
        {
          "keys": "Enter / Space on a topic chip",
          "action": "Filters the list (aria-pressed); the count is announced politely"
        },
        {
          "keys": "Tab",
          "action": "Each card has one link; the whole card is clickable"
        }
      ],
      "customization": [
        "posts: image for a photo, or cover (0 to 4) to choose a generated cover",
        "filterable: false hides the chips",
        "locale for dates (UTC, fixed by default)"
      ]
    },
    "examples": [
      "blog-1-demo",
      "blog-1-compact"
    ],
    "url": "https://ui.ballmac.com/components/blog-1"
  },
  "categories": [
    "blocks",
    "blog"
  ],
  "type": "registry:block"
}