{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "changelog-1",
  "title": "Changelog 1: release notes timeline",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A release-notes page: a sticky version and date beside each release card, optional banner art for headline releases, typed changes with filters and counts, and a show-older button.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/blog-1.json",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/i18n.json",
    "https://ui.ballmac.com/r/media.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/changelog-1/changelog-1.tsx",
      "content": "// Ballmac UI: Changelog 1. https://ui.ballmac.com/blocks/changelog-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { Bug, ChevronDown, Rss, Sparkles, Wand2 } from \"lucide-react\"\n\nimport { BlogCover } from \"@/components/ballmac/blocks/blog-1/blog-1\"\nimport { buttonVariants } from \"@/components/ballmac/button\"\nimport { cn } from \"@/lib/utils\"\nimport { useLocale } from \"@/lib/ballmac/i18n\"\nimport { Media, type MediaSource } from \"@/components/ballmac/media\"\n\ntype Changelog1Kind = \"new\" | \"improved\" | \"fixed\"\n\ntype Changelog1Change = {\n  type: Changelog1Kind\n  /** One line describing the change. */\n  text: string\n}\n\ntype Changelog1Release = {\n  /** Version label, e.g. \"2.4.0\". */\n  version: string\n  /** Release date as an ISO string (YYYY-MM-DD), shown in UTC. */\n  date: string\n  /** Headline for the release. */\n  title: string\n  /** One or two sentences about the release. */\n  summary?: string\n  /** The individual changes. */\n  changes: Changelog1Change[]\n  /** Which generated banner to draw (0 to 4) for a headline release. Omit for a plain release. */\n  cover?: number\n  /** Your own banner for a headline release: an image URL (give it imageAlt), an object with alt text and a dark-mode file, or your own element. Wins over `cover`. */\n  image?: MediaSource\n  /** Describes `image` when it is a plain URL. */\n  imageAlt?: string\n}\n\ntype Changelog1Props = Omit<React.ComponentProps<\"section\">, \"title\"> & {\n  /** Section heading. */\n  title?: string\n  /** One sentence under the heading. */\n  description?: string\n  /** Releases, newest first. */\n  releases?: Changelog1Release[]\n  /** Releases shown before the \"Show older releases\" button. */\n  initialCount?: number\n  /** Link to your feed. Pass null to hide the button. */\n  feed?: { label: string; href: string } | null\n  /** Locale for dates. Fixed by default so server and browser match. */\n  locale?: string\n}\n\nconst kinds: Record<Changelog1Kind, { label: string; icon: typeof Sparkles; chip: string }> = {\n  new: { label: \"New\", icon: Sparkles, chip: \"bg-chart-2/15\" },\n  improved: { label: \"Improved\", icon: Wand2, chip: \"bg-chart-1/15\" },\n  fixed: { label: \"Fixed\", icon: Bug, chip: \"bg-chart-3/20\" },\n}\n\nconst defaults: Changelog1Release[] = [\n  {\n    version: \"2.4.0\", date: \"2026-09-29\", title: \"Approvals, rebuilt\", cover: 0,\n    summary: \"Approvals now happen where the work is: in the invoice, in Slack and from your phone. Reviewers see a short daily queue instead of a monthly pile.\",\n    changes: [\n      { type: \"new\", text: \"Approve or reject from Slack with one tap\" },\n      { type: \"new\", text: \"Daily approval digest with the three items that need you most\" },\n      { type: \"improved\", text: \"Invoice pages load about twice as fast on large accounts\" },\n      { type: \"fixed\", text: \"Approvers added mid-flow now receive the pending request\" },\n    ],\n  },\n  {\n    version: \"2.3.2\", date: \"2026-09-18\", title: \"Small fixes\",\n    changes: [\n      { type: \"fixed\", text: \"CSV exports no longer drop the last row when it has no trailing newline\" },\n      { type: \"fixed\", text: \"Dark mode: chart tooltips were unreadable on some displays\" },\n      { type: \"improved\", text: \"Clearer error when a bank connection needs to be refreshed\" },\n    ],\n  },\n  {\n    version: \"2.3.0\", date: \"2026-09-04\", title: \"SAML SSO and SCIM\", cover: 3,\n    summary: \"Sign in with Okta, Entra ID or Google Workspace, and keep your directory in sync automatically.\",\n    changes: [\n      { type: \"new\", text: \"SAML single sign-on for Pro and Scale\" },\n      { type: \"new\", text: \"SCIM user provisioning and deprovisioning\" },\n      { type: \"improved\", text: \"Audit log now records the identity provider used to sign in\" },\n    ],\n  },\n  {\n    version: \"2.2.1\", date: \"2026-08-21\", title: \"Reliability\",\n    changes: [\n      { type: \"fixed\", text: \"Webhook retries could fire twice after a deploy\" },\n      { type: \"improved\", text: \"Faster search across more than 100,000 transactions\" },\n    ],\n  },\n  {\n    version: \"2.2.0\", date: \"2026-08-07\", title: \"Custom reports\",\n    summary: \"Build the report you actually need, save it and schedule it by email.\",\n    changes: [\n      { type: \"new\", text: \"Drag-and-drop report builder with saved views\" },\n      { type: \"new\", text: \"Schedule any report to arrive weekly or monthly\" },\n      { type: \"improved\", text: \"Currency formatting follows each customer's locale\" },\n    ],\n  },\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 Changelog1({\n  title = \"What’s new\",\n  description = \"Every improvement we ship, newest first.\",\n  releases = defaults,\n  initialCount = 3,\n  feed = { label: \"Subscribe via RSS\", href: \"#\" },\n  locale,\n  className,\n  ...props\n}: Changelog1Props) {\n  const defaultLocale = useLocale()\n  locale ??= defaultLocale\n  const [filter, setFilter] = React.useState<\"all\" | Changelog1Kind>(\"all\")\n  const [expanded, setExpanded] = React.useState(false)\n  const counts = { all: releases.reduce((n, r) => n + r.changes.length, 0), new: 0, improved: 0, fixed: 0 }\n  for (const r of releases) for (const c of r.changes) counts[c.type]++\n\n  const visible = releases\n    .map((r) => ({ ...r, changes: r.changes.filter((c) => filter === \"all\" || c.type === filter) }))\n    .filter((r) => r.changes.length > 0)\n  const shown = expanded ? visible : visible.slice(0, initialCount)\n  const hidden = visible.length - shown.length\n\n  return (\n    <section data-slot=\"changelog-1\" className={cn(\"mx-auto max-w-5xl px-4 py-20 sm:px-6 md:py-28\", className)} {...props}>\n      <div className=\"flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between\">\n        <div className=\"max-w-xl\">\n          <h2 className=\"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        {feed && (\n          <a href={feed.href} className={buttonVariants({ variant: \"outline\", shape: \"pill\", className: \"w-fit\" })}>\n            <Rss /> {feed.label}\n          </a>\n        )}\n      </div>\n\n      <div role=\"group\" aria-label=\"Filter by type of change\" className=\"mt-10 flex flex-wrap gap-1.5\">\n        {([\"all\", \"new\", \"improved\", \"fixed\"] as const).map((k) => (\n          <button\n            key={k}\n            type=\"button\"\n            aria-pressed={filter === k}\n            onClick={() => setFilter(k)}\n            className={cn(\n              \"focus-visible:ring-ring/50 inline-flex h-9 items-center gap-2 rounded-full border px-4 text-sm font-medium outline-none transition-colors focus-visible:ring-[3px]\",\n              filter === k ? \"bg-foreground text-background border-transparent\" : \"text-muted-foreground hover:text-foreground hover:bg-accent\"\n            )}\n          >\n            {k === \"all\" ? \"All changes\" : kinds[k].label}\n            <span className={cn(\"text-xs tabular-nums\", filter === k ? \"text-background/70\" : \"text-muted-foreground\")}>{counts[k]}</span>\n          </button>\n        ))}\n      </div>\n\n      <p role=\"status\" className=\"sr-only\">{visible.length} {visible.length === 1 ? \"release\" : \"releases\"} shown.</p>\n\n      <ol className=\"relative mt-12 space-y-14\">\n        {shown.map((r, i) => (\n          <li key={r.version} className=\"grid gap-4 md:grid-cols-[9rem_1fr] md:gap-10\">\n            <div className=\"md:sticky md:top-24 md:self-start\">\n              <p className=\"font-mono text-2xl font-semibold tracking-tight tabular-nums\">{r.version}</p>\n              <p className=\"text-muted-foreground mt-1 text-sm\"><time dateTime={r.date}>{formatDate(r.date, locale)}</time></p>\n              {i === 0 && filter === \"all\" && !expanded && <span className=\"bg-foreground text-background mt-3 inline-block rounded-full px-2.5 py-0.5 text-xs font-medium\">Latest</span>}\n            </div>\n            <article className=\"bg-card overflow-hidden rounded-3xl border\">\n              {r.image ? <Media media={r.image} alt={r.imageAlt} aspect=\"21/8\" className=\"w-full\" /> : r.cover !== undefined && <BlogCover variant={r.cover} className=\"aspect-[21/8] w-full\" />}\n              <div className=\"p-6 sm:p-8\">\n                <h3 className=\"text-xl font-semibold tracking-[-0.02em] text-balance sm:text-2xl\">{r.title}</h3>\n                {r.summary && <p className=\"text-muted-foreground mt-2 max-w-2xl text-pretty\">{r.summary}</p>}\n                <ul className=\"mt-6 space-y-3\">\n                  {r.changes.map((c) => {\n                    const k = kinds[c.type]\n                    return (\n                      <li key={c.text} className=\"flex items-start gap-3 text-[15px] leading-6\">\n                        <span className={cn(\"mt-0.5 inline-flex h-6 w-[5.5rem] shrink-0 items-center gap-1.5 rounded-full px-2 text-xs font-medium\", k.chip)}>\n                          <k.icon className=\"size-3.5\" aria-hidden=\"true\" />\n                          {k.label}\n                        </span>\n                        <span className=\"text-pretty\">{c.text}</span>\n                      </li>\n                    )\n                  })}\n                </ul>\n              </div>\n            </article>\n          </li>\n        ))}\n      </ol>\n\n      {visible.length === 0 && <p className=\"text-muted-foreground mt-12 text-center\">No changes of this type yet.</p>}\n\n      {hidden > 0 && (\n        <div className=\"mt-12 flex justify-center\">\n          <button type=\"button\" onClick={() => setExpanded(true)} className={buttonVariants({ variant: \"outline\", shape: \"pill\" })}>\n            Show {hidden} older {hidden === 1 ? \"release\" : \"releases\"} <ChevronDown />\n          </button>\n        </div>\n      )}\n    </section>\n  )\n}\n\nexport { Changelog1, type Changelog1Props, type Changelog1Release, type Changelog1Change, type Changelog1Kind }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/changelog-1/changelog-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "changelog",
      "release notes",
      "updates",
      "versions",
      "timeline",
      "filter"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "A public changelog page. Pass releases=[{ version, date, title, summary?, changes: { type: 'new' | 'improved' | 'fixed', text }[], cover? }] newest first.",
      "whenToUse": [
        "Product release notes",
        "Open-source project updates"
      ],
      "whenNotToUse": [
        "A compact in-app feed (use changelog-feed)"
      ],
      "composesWith": [
        "newsletter-1",
        "header-2",
        "footer-2",
        "hero-6"
      ],
      "a11y": [
        {
          "keys": "Enter / Space on a filter",
          "action": "Filters the changes (aria-pressed); the release count is announced politely"
        },
        {
          "keys": "Tab",
          "action": "Each change type is also spelled out in text, not only colour"
        }
      ],
      "customization": [
        "releases: cover (0 to 4) adds banner art to a headline release",
        "initialCount: releases shown before the button",
        "feed: null hides the RSS button"
      ]
    },
    "examples": [
      "changelog-1-demo",
      "changelog-1-minimal"
    ],
    "url": "https://ui.ballmac.com/components/changelog-1"
  },
  "categories": [
    "blocks",
    "changelog"
  ],
  "type": "registry:block"
}